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 | const { CHAT_COMPLETION_SOURCES, GEMINI_SAFETY, BISON_SAFETY, OPENROUTER_HEADERS } = require('../../constants'); | 6 | const { CHAT_COMPLETION_SOURCES, GEMINI_SAFETY, BISON_SAFETY, OPENROUTER_HEADERS } = require('../../constants'); |
| 7 | const { forwardFetchResponse, getConfigValue, tryParse, uuidv4, mergeObjectWithYaml, excludeKeysByYaml, color } = require('../../util'); | 7 | const { forwardFetchResponse, getConfigValue, tryParse, uuidv4, mergeObjectWithYaml, excludeKeysByYaml, color } = require('../../util'); |
| 8 | const { convertClaudeMessages, convertGooglePrompt, convertTextCompletionPrompt, convertCohereMessages, convertMistralMessages, convertCohereTools, convertAI21Messages } = require('../../prompt-converters'); | 8 | const { convertClaudeMessages, convertGooglePrompt, convertTextCompletionPrompt, convertCohereMessages, convertMistralMessages, convertCohereTools, convertAI21Messages } = require('../../prompt-converters'); |
| 9 | const CohereStream = require('../../cohere-stream'); | ||
| 9 | 10 | ||
| 10 | const { readSecret, SECRET_KEYS } = require('../secrets'); | 11 | const { readSecret, SECRET_KEYS } = require('../secrets'); |
| 11 | const { getTokenizerModel, getSentencepiceTokenizer, getTiktokenTokenizer, sentencepieceTokenizers, TEXT_COMPLETION_MODELS } = require('../tokenizers'); | 12 | const { 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 stream | 45 | * @param {Response} jsonStream JSON stream |
| 45 | * @param {import('express').Request} request Express request | 46 | * @param {import('express').Request} request Express request |
| 46 | * @param {import('express').Response} response Express response | 47 | * @param {import('express').Response} response Express response |
| 47 | * @returns {Promise<any>} Nothing valuable | 48 | * @returns {Promise<any>} Nothing valuable |
| 48 | */ | 49 | */ |
| 49 | async function parseCohereStream(jsonStream, request, response) { | 50 | async 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 | }); | ||
| 69 | 65 | ||
| 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'; |
| 599 | 588 | ||
| 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); |