Cohere: new stream parser

ebab976221d1774babf5583bc5f487dda5d98130

Cohee <18619528+Cohee1207@users.noreply.github.com>

2 files changed, +136 -21Showing whitespace changes
src/cohere-stream.js+126 -0
@@ -0,0 +1,126 @@
1const DATA_PREFIX = 'data:';
2
3/**
4 * Borrowed from Cohere SDK (MIT License)
5 * https://github.com/cohere-ai/cohere-typescript/blob/main/src/core/streaming-fetcher/Stream.ts
6 * Copyright (c) 2021 Cohere
7 */
8class CohereStream {
9 /** @type {ReadableStream} */
10 stream;
11 /** @type {string} */
12 prefix;
13 /** @type {string} */
14 messageTerminator;
15 /** @type {string|undefined} */
16 streamTerminator;
17 /** @type {AbortController} */
18 controller = new AbortController();
19
20 constructor({ stream, eventShape }) {
21 this.stream = stream;
22 if (eventShape.type === 'sse') {
23 this.prefix = DATA_PREFIX;
24 this.messageTerminator = '\n';
25 this.streamTerminator = eventShape.streamTerminator;
26 } else {
27 this.messageTerminator = eventShape.messageTerminator;
28 }
29 }
30
31 async *iterMessages() {
32 const stream = readableStreamAsyncIterable(this.stream);
33 let buf = '';
34 let prefixSeen = false;
35 let parsedAnyMessages = false;
36 for await (const chunk of stream) {
37 buf += this.decodeChunk(chunk);
38
39 let terminatorIndex;
40 // Parse the chunk into as many messages as possible
41 while ((terminatorIndex = buf.indexOf(this.messageTerminator)) >= 0) {
42 // Extract the line from the buffer
43 let line = buf.slice(0, terminatorIndex + 1);
44 buf = buf.slice(terminatorIndex + 1);
45
46 // Skip empty lines
47 if (line.length === 0) {
48 continue;
49 }
50
51 // Skip the chunk until the prefix is found
52 if (!prefixSeen && this.prefix != null) {
53 const prefixIndex = line.indexOf(this.prefix);
54 if (prefixIndex === -1) {
55 continue;
56 }
57 prefixSeen = true;
58 line = line.slice(prefixIndex + this.prefix.length);
59 }
60
61 // If the stream terminator is present, return
62 if (this.streamTerminator != null && line.includes(this.streamTerminator)) {
63 return;
64 }
65
66 // Otherwise, yield message from the prefix to the terminator
67 const message = JSON.parse(line);
68 yield message;
69 prefixSeen = false;
70 parsedAnyMessages = true;
71 }
72 }
73
74 if (!parsedAnyMessages && buf.length > 0) {
75 try {
76 yield JSON.parse(buf);
77 } catch (e) {
78 console.error('Error parsing message:', e);
79 }
80 }
81 }
82
83 async *[Symbol.asyncIterator]() {
84 for await (const message of this.iterMessages()) {
85 yield message;
86 }
87 }
88
89 decodeChunk(chunk) {
90 const decoder = new TextDecoder('utf8');
91 return decoder.decode(chunk);
92 }
93}
94
95function readableStreamAsyncIterable(stream) {
96 if (stream[Symbol.asyncIterator]) {
97 return stream;
98 }
99
100 const reader = stream.getReader();
101 return {
102 async next() {
103 try {
104 const result = await reader.read();
105 if (result?.done) {
106 reader.releaseLock();
107 } // release lock when stream becomes closed
108 return result;
109 } catch (e) {
110 reader.releaseLock(); // release lock when stream becomes errored
111 throw e;
112 }
113 },
114 async return() {
115 const cancelPromise = reader.cancel();
116 reader.releaseLock();
117 await cancelPromise;
118 return { done: true, value: undefined };
119 },
120 [Symbol.asyncIterator]() {
121 return this;
122 },
123 };
124}
125
126module.exports = CohereStream;
src/endpoints/backends/chat-completions.js+10 -21
@@ -6,6 +6,7 @@ const { jsonParser } = require('../../express-common');
6const { CHAT_COMPLETION_SOURCES, GEMINI_SAFETY, BISON_SAFETY, OPENROUTER_HEADERS } = require('../../constants');6const { CHAT_COMPLETION_SOURCES, GEMINI_SAFETY, BISON_SAFETY, OPENROUTER_HEADERS } = require('../../constants');
7const { forwardFetchResponse, getConfigValue, tryParse, uuidv4, mergeObjectWithYaml, excludeKeysByYaml, color } = require('../../util');7const { forwardFetchResponse, getConfigValue, tryParse, uuidv4, mergeObjectWithYaml, excludeKeysByYaml, color } = require('../../util');
8const { convertClaudeMessages, convertGooglePrompt, convertTextCompletionPrompt, convertCohereMessages, convertMistralMessages, convertCohereTools, convertAI21Messages } = require('../../prompt-converters');8const { convertClaudeMessages, convertGooglePrompt, convertTextCompletionPrompt, convertCohereMessages, convertMistralMessages, convertCohereTools, convertAI21Messages } = require('../../prompt-converters');
9const CohereStream = require('../../cohere-stream');
910
10const { readSecret, SECRET_KEYS } = require('../secrets');11const { readSecret, SECRET_KEYS } = require('../secrets');
11const { getTokenizerModel, getSentencepiceTokenizer, getTiktokenTokenizer, sentencepieceTokenizers, TEXT_COMPLETION_MODELS } = require('../tokenizers');12const { getTokenizerModel, getSentencepiceTokenizer, getTiktokenTokenizer, sentencepieceTokenizers, TEXT_COMPLETION_MODELS } = require('../tokenizers');
@@ -41,16 +42,16 @@ function postProcessPrompt(messages, type, charName, userName) {
41/**42/**
42 * Ollama strikes back. Special boy #2's steaming routine.43 * Ollama strikes back. Special boy #2's steaming routine.
43 * Wrap this abomination into proper SSE stream, again.44 * Wrap this abomination into proper SSE stream, again.
44 * @param {import('node-fetch').Response} jsonStream JSON stream45 * @param {Response} jsonStream JSON stream
45 * @param {import('express').Request} request Express request46 * @param {import('express').Request} request Express request
46 * @param {import('express').Response} response Express response47 * @param {import('express').Response} response Express response
47 * @returns {Promise<any>} Nothing valuable48 * @returns {Promise<any>} Nothing valuable
48 */49 */
49async function parseCohereStream(jsonStream, request, response) {50async function parseCohereStream(jsonStream, request, response) {
50 try {51 try {
51 jsonStream.body.on('data', (data) => {52 const stream = new CohereStream({ stream: jsonStream.body, eventShape: { type: 'json', messageTerminator: '\n' } });
52 try {53
53 const json = JSON.parse(data.toString());54 for await (const json of stream.iterMessages()) {
54 if (json.message) {55 if (json.message) {
55 const message = json.message || 'Unknown error';56 const message = json.message || 'Unknown error';
56 const chunk = { error: { message: message } };57 const chunk = { error: { message: message } };
@@ -59,24 +60,12 @@ async function parseCohereStream(jsonStream, request, response) {
59 const text = json.text || '';60 const text = json.text || '';
60 const chunk = { choices: [{ text }] };61 const chunk = { choices: [{ text }] };
61 response.write(`data: ${JSON.stringify(chunk)}\n\n`);62 response.write(`data: ${JSON.stringify(chunk)}\n\n`);
62 } else {
63 return;
64 }63 }
65 } catch (e) {
66 // ignore
67 }64 }
68 });
6965
70 request.socket.on('close', function () {
71 if (jsonStream.body instanceof Readable) jsonStream.body.destroy();
72 response.end();
73 });
74
75 jsonStream.body.on('end', () => {
76 console.log('Streaming request finished');66 console.log('Streaming request finished');
77 response.write('data: [DONE]\n\n');67 response.write('data: [DONE]\n\n');
78 response.end();68 response.end();
79 });
80 } catch (error) {69 } catch (error) {
81 console.log('Error forwarding streaming response:', error);70 console.log('Error forwarding streaming response:', error);
82 if (!response.headersSent) {71 if (!response.headersSent) {
@@ -598,15 +587,15 @@ async function sendCohereRequest(request, response) {
598 const apiUrl = API_COHERE + '/chat';587 const apiUrl = API_COHERE + '/chat';
599588
600 if (request.body.stream) {589 if (request.body.stream) {
601 const stream = await fetch(apiUrl, config);590 const stream = await global.fetch(apiUrl, config);
602 parseCohereStream(stream, request, response);591 parseCohereStream(stream, request, response);
603 } else {592 } else {
604 const generateResponse = await fetch(apiUrl, config);593 const generateResponse = await fetch(apiUrl, config);
605 if (!generateResponse.ok) {594 if (!generateResponse.ok) {
606 console.log(`Cohere API returned error: ${generateResponse.status} ${generateResponse.statusText} ${await generateResponse.text()}`);595 const errorText = await generateResponse.text();
607 // a 401 unauthorized response breaks the frontend auth, so return a 500 instead. prob a better way of dealing with this.596 console.log(`Cohere API returned error: ${generateResponse.status} ${generateResponse.statusText} ${errorText}`);
608 // 401s are already handled by the streaming processor and dont pop up an error toast, that should probably be fixed too.597 const errorJson = tryParse(errorText) ?? { error: true };
609 return response.status(generateResponse.status === 401 ? 500 : generateResponse.status).send({ error: true });598 return response.status(generateResponse.status === 401 ? 500 : generateResponse.status).send(errorJson);
610 }599 }
611 const generateResponseJson = await generateResponse.json();600 const generateResponseJson = await generateResponse.json();
612 console.log('Cohere response:', generateResponseJson);601 console.log('Cohere response:', generateResponseJson);