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 @@
1+const 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+ */
8+class 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+
95+function 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+
126+module.exports = CohereStream;
src/endpoints/backends/chat-completions.js+10 -21
@@ -6,6 +6,7 @@ const { jsonParser } = require('../../express-common');
66const { CHAT_COMPLETION_SOURCES, GEMINI_SAFETY, BISON_SAFETY, OPENROUTER_HEADERS } = require('../../constants');
77const { forwardFetchResponse, getConfigValue, tryParse, uuidv4, mergeObjectWithYaml, excludeKeysByYaml, color } = require('../../util');
88const { convertClaudeMessages, convertGooglePrompt, convertTextCompletionPrompt, convertCohereMessages, convertMistralMessages, convertCohereTools, convertAI21Messages } = require('../../prompt-converters');
9+const CohereStream = require('../../cohere-stream');
910
1011const { readSecret, SECRET_KEYS } = require('../secrets');
1112const { getTokenizerModel, getSentencepiceTokenizer, getTiktokenTokenizer, sentencepieceTokenizers, TEXT_COMPLETION_MODELS } = require('../tokenizers');
@@ -41,16 +42,16 @@ function postProcessPrompt(messages, type, charName, userName) {
4142/**
4243 * Ollama strikes back. Special boy #2's steaming routine.
4344 * Wrap this abomination into proper SSE stream, again.
4445 * @param {import('node-fetch').Response} jsonStream JSON stream
4546 * @param {import('express').Request} request Express request
4647 * @param {import('express').Response} response Express response
4748 * @returns {Promise<any>} Nothing valuable
4849 */
4950async function parseCohereStream(jsonStream, request, response) {
5051 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()) {
5455 if (json.message) {
5556 const message = json.message || 'Unknown error';
5657 const chunk = { error: { message: message } };
@@ -59,24 +60,12 @@ async function parseCohereStream(jsonStream, request, response) {
5960 const text = json.text || '';
6061 const chunk = { choices: [{ text }] };
6162 response.write(`data: ${JSON.stringify(chunk)}\n\n`);
62- } else {
63- return;
6463 }
65- } catch (e) {
66- // ignore
6764 }
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', () => {
7666 console.log('Streaming request finished');
7767 response.write('data: [DONE]\n\n');
7868 response.end();
79- });
8069 } catch (error) {
8170 console.log('Error forwarding streaming response:', error);
8271 if (!response.headersSent) {
@@ -598,15 +587,15 @@ async function sendCohereRequest(request, response) {
598587 const apiUrl = API_COHERE + '/chat';
599588
600589 if (request.body.stream) {
601590 const stream = await global.fetch(apiUrl, config);
602591 parseCohereStream(stream, request, response);
603592 } else {
604593 const generateResponse = await fetch(apiUrl, config);
605594 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 };
609598 return response.status(generateResponse.status === 401 ? 500 : generateResponse.status).send({ error: true }errorJson);
610599 }
611600 const generateResponseJson = await generateResponse.json();
612601 console.log('Cohere response:', generateResponseJson);