Cohere: new stream parser
| @@ -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; | |
| @@ -6,6 +6,7 @@ const { jsonParser } = require('../../express-common'); | ||
| 6 | 6 | const { CHAT_COMPLETION_SOURCES, GEMINI_SAFETY, BISON_SAFETY, OPENROUTER_HEADERS } = require('../../constants'); |
| 7 | 7 | const { forwardFetchResponse, getConfigValue, tryParse, uuidv4, mergeObjectWithYaml, excludeKeysByYaml, color } = require('../../util'); |
| 8 | 8 | const { convertClaudeMessages, convertGooglePrompt, convertTextCompletionPrompt, convertCohereMessages, convertMistralMessages, convertCohereTools, convertAI21Messages } = require('../../prompt-converters'); |
| 9 | +const CohereStream = require('../../cohere-stream'); | |
| 9 | 10 | |
| 10 | 11 | const { readSecret, SECRET_KEYS } = require('../secrets'); |
| 11 | 12 | const { getTokenizerModel, getSentencepiceTokenizer, getTiktokenTokenizer, sentencepieceTokenizers, TEXT_COMPLETION_MODELS } = require('../tokenizers'); |
| @@ -41,42 +42,30 @@ function postProcessPrompt(messages, type, charName, userName) { | ||
| 41 | 42 | /** |
| 42 | 43 | * Ollama strikes back. Special boy #2's steaming routine. |
| 43 | 44 | * Wrap this abomination into proper SSE stream, again. |
| 44 | 45 | * @param {import('node-fetch').Response} jsonStream JSON stream |
| 45 | 46 | * @param {import('express').Request} request Express request |
| 46 | 47 | * @param {import('express').Response} response Express response |
| 47 | 48 | * @returns {Promise<any>} Nothing valuable |
| 48 | 49 | */ |
| 49 | 50 | async function parseCohereStream(jsonStream, request, response) { |
| 50 | 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 | 55 | if (json.message) { |
| 55 | 56 | const message = json.message || 'Unknown error'; |
| 56 | 57 | const chunk = { error: { message: message } }; |
| 57 | 58 | response.write(`data: ${JSON.stringify(chunk)}\n\n`); |
| 58 | 59 | } else if (json.event_type === 'text-generation') { |
| 59 | 60 | const text = json.text || ''; |
| 60 | 61 | const chunk = { choices: [{ text }] }; |
| 61 | 62 | response.write(`data: ${JSON.stringify(chunk)}\n\n`); |
| 62 | - } else { | |
| 63 | - return; | |
| 64 | - } | |
| 65 | - } catch (e) { | |
| 66 | - // ignore | |
| 67 | 63 | } |
| 68 | 64 | }); |
| 69 | - | |
| 70 | - request.socket.on('close', function () { | |
| 71 | - if (jsonStream.body instanceof Readable) jsonStream.body.destroy(); | |
| 72 | - response.end(); | |
| 73 | - }); | |
| 74 | 65 | |
| 75 | - jsonStream.body.on('end', () => { | |
| 66 | + console.log('Streaming request finished'); | |
| 76 | - console.log('Streaming request finished'); | |
| 67 | + response.write('data: [DONE]\n\n'); | |
| 77 | - response.write('data: [DONE]\n\n'); | |
| 68 | + response.end(); | |
| 78 | - response.end(); | |
| 79 | - }); | |
| 80 | 69 | } catch (error) { |
| 81 | 70 | console.log('Error forwarding streaming response:', error); |
| 82 | 71 | if (!response.headersSent) { |
| @@ -598,15 +587,15 @@ async function sendCohereRequest(request, response) { | ||
| 598 | 587 | const apiUrl = API_COHERE + '/chat'; |
| 599 | 588 | |
| 600 | 589 | if (request.body.stream) { |
| 601 | 590 | const stream = await global.fetch(apiUrl, config); |
| 602 | 591 | parseCohereStream(stream, request, response); |
| 603 | 592 | } else { |
| 604 | 593 | const generateResponse = await fetch(apiUrl, config); |
| 605 | 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 | 598 | return response.status(generateResponse.status === 401 ? 500 : generateResponse.status).send({ error: true }errorJson); |
| 610 | 599 | } |
| 611 | 600 | const generateResponseJson = await generateResponse.json(); |
| 612 | 601 | console.log('Cohere response:', generateResponseJson); |