Added stream support to "custom-request"

ec474f557182f624856e9d5fe252be9373d152d0

bmen25124 <bmen25124@gmail.com>

3 files changed, +192 -71Ignore whitespace
public/scripts/custom-request.js+149 -44
@@ -3,10 +3,13 @@ import { extractMessageFromData, getGenerateUrl, getRequestHeaders } from '../sc
3import { getTextGenServer } from './textgen-settings.js';3import { getTextGenServer } from './textgen-settings.js';
4import { extractReasoningFromData } from './reasoning.js';4import { extractReasoningFromData } from './reasoning.js';
5import { formatInstructModeChat, formatInstructModePrompt, names_behavior_types } from './instruct-mode.js';5import { formatInstructModeChat, formatInstructModePrompt, names_behavior_types } from './instruct-mode.js';
6import { getStreamingReply, tryParseStreamingError } from './openai.js';
7import EventSourceStream from './sse-stream.js';
68
7// #region Type Definitions9// #region Type Definitions
8/**10/**
9 * @typedef {Object} TextCompletionRequestBase11 * @typedef {Object} TextCompletionRequestBase
12 * @property {boolean?} [stream=false] - Whether to stream the response
10 * @property {number} max_tokens - Maximum number of tokens to generate13 * @property {number} max_tokens - Maximum number of tokens to generate
11 * @property {string} [model] - Optional model name14 * @property {string} [model] - Optional model name
12 * @property {string} api_type - Type of API to use15 * @property {string} api_type - Type of API to use
@@ -17,6 +20,7 @@ import { formatInstructModeChat, formatInstructModePrompt, names_behavior_types
1720
18/**21/**
19 * @typedef {Object} TextCompletionPayloadBase22 * @typedef {Object} TextCompletionPayloadBase
23 * @property {boolean?} [stream=false] - Whether to stream the response
20 * @property {string} prompt - The text prompt for completion24 * @property {string} prompt - The text prompt for completion
21 * @property {number} max_tokens - Maximum number of tokens to generate25 * @property {number} max_tokens - Maximum number of tokens to generate
22 * @property {number} max_new_tokens - Alias for max_tokens26 * @property {number} max_new_tokens - Alias for max_tokens
@@ -36,6 +40,7 @@ import { formatInstructModeChat, formatInstructModePrompt, names_behavior_types
3640
37/**41/**
38 * @typedef {Object} ChatCompletionPayloadBase42 * @typedef {Object} ChatCompletionPayloadBase
43 * @property {boolean?} [stream=false] - Whether to stream the response
39 * @property {ChatCompletionMessage[]} messages - Array of chat messages44 * @property {ChatCompletionMessage[]} messages - Array of chat messages
40 * @property {string} [model] - Optional model name to use for completion45 * @property {string} [model] - Optional model name to use for completion
41 * @property {string} chat_completion_source - Source provider for chat completion46 * @property {string} chat_completion_source - Source provider for chat completion
@@ -52,10 +57,20 @@ import { formatInstructModeChat, formatInstructModePrompt, names_behavior_types
52 * @property {string} reasoning - Extracted reasoning.57 * @property {string} reasoning - Extracted reasoning.
53 */58 */
5459
60/**
61 * @typedef {Object} StreamResponse
62 * @property {string} text - Generated text.
63 * @property {string[]} swipes - Generated swipes
64 * @property {Object} state - Generated state
65 * @property {string?} [state.reasoning] - Generated reasoning
66 * @property {string?} [state.image] - Generated image
67 * @returns {StreamResponse}
68 */
69
55// #endregion70// #endregion
5671
57/**72/**
58 * Creates & sends a text completion request. Streaming is not supported.73 * Creates & sends a text completion request.
59 */74 */
60export class TextCompletionService {75export class TextCompletionService {
61 static TYPE = 'textgenerationwebui';76 static TYPE = 'textgenerationwebui';
@@ -64,9 +79,10 @@ export class TextCompletionService {
64 * @param {Record<string, any> & TextCompletionRequestBase & {prompt: string}} custom79 * @param {Record<string, any> & TextCompletionRequestBase & {prompt: string}} custom
65 * @returns {TextCompletionPayload}80 * @returns {TextCompletionPayload}
66 */81 */
67 static createRequestData({ prompt, max_tokens, model, api_type, api_server, temperature, min_p, ...props }) {82 static createRequestData({ stream = false, prompt, max_tokens, model, api_type, api_server, temperature, min_p, ...props }) {
68 const payload = {83 const payload = {
69 ...props,84 ...props,
85 stream,
70 prompt,86 prompt,
71 max_tokens,87 max_tokens,
72 max_new_tokens: max_tokens,88 max_new_tokens: max_tokens,
@@ -75,7 +91,6 @@ export class TextCompletionService {
75 api_server: api_server ?? getTextGenServer(api_type),91 api_server: api_server ?? getTextGenServer(api_type),
76 temperature,92 temperature,
77 min_p,93 min_p,
78 stream: false,
79 };94 };
8095
81 // Remove undefined values to avoid API errors96 // Remove undefined values to avoid API errors
@@ -92,34 +107,81 @@ export class TextCompletionService {
92 * Sends a text completion request to the specified server107 * Sends a text completion request to the specified server
93 * @param {TextCompletionPayload} data Request data108 * @param {TextCompletionPayload} data Request data
94 * @param {boolean?} extractData Extract message from the response. Default true109 * @param {boolean?} extractData Extract message from the response. Default true
95 * @returns {Promise<ExtractedData | any>} Extracted data or the raw response110 * @param {AbortSignal?} signal
111 * @returns {Promise<ExtractedData | (() => AsyncGenerator<StreamResponse>)>} If not streaming, returns extracted data; if streaming, returns a function that creates an AsyncGenerator
96 * @throws {Error}112 * @throws {Error}
97 */113 */
98 static async sendRequest(data, extractData = true) {114 static async sendRequest(data, extractData = true, signal = null) {
99 const response = await fetch(getGenerateUrl(this.TYPE), {115 if (!data.stream) {
116 const response = await fetch(getGenerateUrl(this.TYPE), {
117 method: 'POST',
118 headers: getRequestHeaders(),
119 cache: 'no-cache',
120 body: JSON.stringify(data),
121 signal: signal ?? new AbortController().signal,
122 });
123
124 const json = await response.json();
125 if (!response.ok || json.error) {
126 throw json;
127 }
128
129 if (!extractData) {
130 return json;
131 }
132
133 return {
134 content: extractMessageFromData(json, this.TYPE),
135 reasoning: extractReasoningFromData(json, {
136 mainApi: this.TYPE,
137 textGenType: data.api_type,
138 ignoreShowThoughts: true,
139 }),
140 };
141 }
142
143 const response = await fetch('/api/backends/text-completions/generate', {
100 method: 'POST',144 method: 'POST',
101 headers: getRequestHeaders(),145 headers: getRequestHeaders(),
102 cache: 'no-cache',146 cache: 'no-cache',
103 body: JSON.stringify(data),147 body: JSON.stringify(data),
104 signal: new AbortController().signal,148 signal: signal ?? new AbortController().signal,
105 });149 });
106150
107 const json = await response.json();151 if (!response.ok) {
108 if (!response.ok || json.error) {152 const text = await response.text();
109 throw json;153 tryParseStreamingError(response, text, true);
110 }
111154
112 if (!extractData) {155 throw new Error(`Got response status ${response.status}`);
113 return json;
114 }156 }
115157
116 return {158 const eventStream = new EventSourceStream();
117 content: extractMessageFromData(json, this.TYPE),159 response.body.pipeThrough(eventStream);
118 reasoning: extractReasoningFromData(json, {160 const reader = eventStream.readable.getReader();
119 mainApi: this.TYPE,161 return async function* streamData() {
120 textGenType: data.api_type,162 let text = '';
121 ignoreShowThoughts: true,163 const swipes = [];
122 }),164 const state = { reasoning: '' };
165 while (true) {
166 const { done, value } = await reader.read();
167 if (done) return;
168 if (value.data === '[DONE]') return;
169
170 tryParseStreamingError(response, value.data, true);
171
172 let data = JSON.parse(value.data);
173
174 if (data?.choices?.[0]?.index > 0) {
175 const swipeIndex = data.choices[0].index - 1;
176 swipes[swipeIndex] = (swipes[swipeIndex] || '') + data.choices[0].text;
177 } else {
178 const newText = data?.choices?.[0]?.text || data?.content || '';
179 text += newText;
180 state.reasoning += data?.choices?.[0]?.reasoning ?? '';
181 }
182
183 yield { text, swipes, state };
184 }
123 };185 };
124 }186 }
125187
@@ -130,13 +192,15 @@ export class TextCompletionService {
130 * @param {string?} [options.presetName] - Name of the preset to use for generation settings192 * @param {string?} [options.presetName] - Name of the preset to use for generation settings
131 * @param {string?} [options.instructName] - Name of instruct preset for message formatting193 * @param {string?} [options.instructName] - Name of instruct preset for message formatting
132 * @param {boolean} extractData - Whether to extract structured data from response194 * @param {boolean} extractData - Whether to extract structured data from response
133 * @returns {Promise<ExtractedData | any>} Extracted data or the raw response195 * @param {AbortSignal?} [signal]
196 * @returns {Promise<ExtractedData | (() => AsyncGenerator<StreamResponse>)>} If not streaming, returns extracted data; if streaming, returns a function that creates an AsyncGenerator
134 * @throws {Error}197 * @throws {Error}
135 */198 */
136 static async processRequest(199 static async processRequest(
137 custom,200 custom,
138 options = {},201 options = {},
139 extractData = true,202 extractData = true,
203 signal = null,
140 ) {204 ) {
141 const { presetName, instructName } = options;205 const { presetName, instructName } = options;
142 let requestData = { ...custom };206 let requestData = { ...custom };
@@ -220,7 +284,7 @@ export class TextCompletionService {
220 // @ts-ignore284 // @ts-ignore
221 const data = this.createRequestData(requestData);285 const data = this.createRequestData(requestData);
222286
223 return await this.sendRequest(data, extractData);287 return await this.sendRequest(data, extractData, signal);
224 }288 }
225289
226 /**290 /**
@@ -256,7 +320,7 @@ export class TextCompletionService {
256}320}
257321
258/**322/**
259 * Creates & sends a chat completion request. Streaming is not supported.323 * Creates & sends a chat completion request.
260 */324 */
261export class ChatCompletionService {325export class ChatCompletionService {
262 static TYPE = 'openai';326 static TYPE = 'openai';
@@ -265,16 +329,16 @@ export class ChatCompletionService {
265 * @param {ChatCompletionPayload} custom329 * @param {ChatCompletionPayload} custom
266 * @returns {ChatCompletionPayload}330 * @returns {ChatCompletionPayload}
267 */331 */
268 static createRequestData({ messages, model, chat_completion_source, max_tokens, temperature, custom_url, ...props }) {332 static createRequestData({ stream = false, messages, model, chat_completion_source, max_tokens, temperature, custom_url, ...props }) {
269 const payload = {333 const payload = {
270 ...props,334 ...props,
335 stream,
271 messages,336 messages,
272 model,337 model,
273 chat_completion_source,338 chat_completion_source,
274 max_tokens,339 max_tokens,
275 temperature,340 temperature,
276 custom_url,341 custom_url,
277 stream: false,
278 };342 };
279343
280 // Remove undefined values to avoid API errors344 // Remove undefined values to avoid API errors
@@ -291,34 +355,74 @@ export class ChatCompletionService {
291 * Sends a chat completion request355 * Sends a chat completion request
292 * @param {ChatCompletionPayload} data Request data356 * @param {ChatCompletionPayload} data Request data
293 * @param {boolean?} extractData Extract message from the response. Default true357 * @param {boolean?} extractData Extract message from the response. Default true
294 * @returns {Promise<ExtractedData | any>} Extracted data or the raw response358 * @param {AbortSignal?} signal Abort signal
359 * @returns {Promise<ExtractedData | (() => AsyncGenerator<StreamResponse>)>} If not streaming, returns extracted data; if streaming, returns a function that creates an AsyncGenerator
295 * @throws {Error}360 * @throws {Error}
296 */361 */
297 static async sendRequest(data, extractData = true) {362 static async sendRequest(data, extractData = true, signal = null) {
298 const response = await fetch('/api/backends/chat-completions/generate', {363 const response = await fetch('/api/backends/chat-completions/generate', {
299 method: 'POST',364 method: 'POST',
300 headers: getRequestHeaders(),365 headers: getRequestHeaders(),
301 cache: 'no-cache',366 cache: 'no-cache',
302 body: JSON.stringify(data),367 body: JSON.stringify(data),
303 signal: new AbortController().signal,368 signal: signal ?? new AbortController().signal,
304 });369 });
305370
306 const json = await response.json();371 if (!data.stream) {
307 if (!response.ok || json.error) {372 const json = await response.json();
308 throw json;373 if (!response.ok || json.error) {
374 throw json;
375 }
376
377 if (!extractData) {
378 return json;
379 }
380
381 return {
382 content: extractMessageFromData(json, this.TYPE),
383 reasoning: extractReasoningFromData(json, {
384 mainApi: this.TYPE,
385 textGenType: data.chat_completion_source,
386 ignoreShowThoughts: true,
387 }),
388 };
309 }389 }
310390
311 if (!extractData) {391 if (!response.ok) {
312 return json;392 const text = await response.text();
393 tryParseStreamingError(response, text, true);
394
395 throw new Error(`Got response status ${response.status}`);
313 }396 }
314397
315 return {398 const eventStream = new EventSourceStream();
316 content: extractMessageFromData(json, this.TYPE),399 response.body.pipeThrough(eventStream);
317 reasoning: extractReasoningFromData(json, {400 const reader = eventStream.readable.getReader();
318 mainApi: this.TYPE,401 return async function* streamData() {
319 textGenType: data.chat_completion_source,402 let text = '';
320 ignoreShowThoughts: true,403 const swipes = [];
321 }),404 const state = { reasoning: '', image: '' };
405 while (true) {
406 const { done, value } = await reader.read();
407 if (done) return;
408 const rawData = value.data;
409 if (rawData === '[DONE]') return;
410 tryParseStreamingError(response, rawData, true);
411 const parsed = JSON.parse(rawData);
412
413 const reply = getStreamingReply(parsed, state, {
414 chatCompletionSource: data.chat_completion_source,
415 ignoreShowThoughts: true,
416 });
417 if (Array.isArray(parsed?.choices) && parsed?.choices?.[0]?.index > 0) {
418 const swipeIndex = parsed.choices[0].index - 1;
419 swipes[swipeIndex] = (swipes[swipeIndex] || '') + reply;
420 } else {
421 text += reply;
422 }
423
424 yield { text, swipes: swipes, state };
425 }
322 };426 };
323 }427 }
324428
@@ -327,11 +431,12 @@ export class ChatCompletionService {
327 * @param {ChatCompletionPayload} custom431 * @param {ChatCompletionPayload} custom
328 * @param {Object} options - Configuration options432 * @param {Object} options - Configuration options
329 * @param {string?} [options.presetName] - Name of the preset to use for generation settings433 * @param {string?} [options.presetName] - Name of the preset to use for generation settings
330 * @param {boolean} extractData - Whether to extract structured data from response434 * @param {boolean} [extractData=true] - Whether to extract structured data from response
331 * @returns {Promise<ExtractedData | any>} Extracted data or the raw response435 * @param {AbortSignal?} [signal] - Abort signal
436 * @returns {Promise<ExtractedData | (() => AsyncGenerator<StreamResponse>)>} If not streaming, returns extracted data; if streaming, returns a function that creates an AsyncGenerator
332 * @throws {Error}437 * @throws {Error}
333 */438 */
334 static async processRequest(custom, options, extractData = true) {439 static async processRequest(custom, options, extractData = true, signal = null) {
335 const { presetName } = options;440 const { presetName } = options;
336 let requestData = { ...custom };441 let requestData = { ...custom };
337442
@@ -354,7 +459,7 @@ export class ChatCompletionService {
354459
355 const data = this.createRequestData(requestData);460 const data = this.createRequestData(requestData);
356461
357 return await this.sendRequest(data, extractData);462 return await this.sendRequest(data, extractData, signal);
358 }463 }
359464
360 /**465 /**
public/scripts/extensions/shared.js+10 -6
@@ -276,10 +276,12 @@ export async function getWebLlmContextSize() {
276}276}
277277
278/**278/**
279 * It uses the profiles to send a generate request to the API. Doesn't support streaming.279 * It uses the profiles to send a generate request to the API.
280 */280 */
281export class ConnectionManagerRequestService {281export class ConnectionManagerRequestService {
282 static defaultSendRequestParams = {282 static defaultSendRequestParams = {
283 stream: false,
284 signal: null,
283 extractData: true,285 extractData: true,
284 includePreset: true,286 includePreset: true,
285 includeInstruct: true,287 includeInstruct: true,
@@ -296,11 +298,11 @@ export class ConnectionManagerRequestService {
296 * @param {string} profileId298 * @param {string} profileId
297 * @param {string | (import('../custom-request.js').ChatCompletionMessage & {ignoreInstruct?: boolean})[]} prompt299 * @param {string | (import('../custom-request.js').ChatCompletionMessage & {ignoreInstruct?: boolean})[]} prompt
298 * @param {number} maxTokens300 * @param {number} maxTokens
299 * @param {{extractData?: boolean, includePreset?: boolean, includeInstruct?: boolean}} custom - default values are true301 * @param {{stream?: boolean, signal?: AbortSignal, extractData?: boolean, includePreset?: boolean, includeInstruct?: boolean}} custom - default values are true
300 * @returns {Promise<import('../custom-request.js').ExtractedData | any>} Extracted data or the raw response302 * @returns {Promise<import('../custom-request.js').ExtractedData | (() => AsyncGenerator<import('../custom-request.js').StreamResponse>)>} If not streaming, returns extracted data; if streaming, returns a function that creates an AsyncGenerator
301 */303 */
302 static async sendRequest(profileId, prompt, maxTokens, custom = this.defaultSendRequestParams) {304 static async sendRequest(profileId, prompt, maxTokens, custom = this.defaultSendRequestParams) {
303 const { extractData, includePreset, includeInstruct } = { ...this.defaultSendRequestParams, ...custom };305 const { stream, signal, extractData, includePreset, includeInstruct } = { ...this.defaultSendRequestParams, ...custom };
304306
305 const context = SillyTavern.getContext();307 const context = SillyTavern.getContext();
306 if (context.extensionSettings.disabledExtensions.includes('connection-manager')) {308 if (context.extensionSettings.disabledExtensions.includes('connection-manager')) {
@@ -319,6 +321,7 @@ export class ConnectionManagerRequestService {
319321
320 const messages = Array.isArray(prompt) ? prompt : [{ role: 'user', content: prompt }];322 const messages = Array.isArray(prompt) ? prompt : [{ role: 'user', content: prompt }];
321 return await context.ChatCompletionService.processRequest({323 return await context.ChatCompletionService.processRequest({
324 stream,
322 messages,325 messages,
323 max_tokens: maxTokens,326 max_tokens: maxTokens,
324 model: profile.model,327 model: profile.model,
@@ -326,7 +329,7 @@ export class ConnectionManagerRequestService {
326 custom_url: profile['api-url'],329 custom_url: profile['api-url'],
327 }, {330 }, {
328 presetName: includePreset ? profile.preset : undefined,331 presetName: includePreset ? profile.preset : undefined,
329 }, extractData);332 }, extractData, signal);
330 }333 }
331 case 'textgenerationwebui': {334 case 'textgenerationwebui': {
332 if (!selectedApiMap.type) {335 if (!selectedApiMap.type) {
@@ -334,6 +337,7 @@ export class ConnectionManagerRequestService {
334 }337 }
335338
336 return await context.TextCompletionService.processRequest({339 return await context.TextCompletionService.processRequest({
340 stream,
337 prompt,341 prompt,
338 max_tokens: maxTokens,342 max_tokens: maxTokens,
339 model: profile.model,343 model: profile.model,
@@ -342,7 +346,7 @@ export class ConnectionManagerRequestService {
342 }, {346 }, {
343 instructName: includeInstruct ? profile.instruct : undefined,347 instructName: includeInstruct ? profile.instruct : undefined,
344 presetName: includePreset ? profile.preset : undefined,348 presetName: includePreset ? profile.preset : undefined,
345 }, extractData);349 }, extractData, signal);
346 }350 }
347 default: {351 default: {
348 throw new Error(`Unknown API type ${selectedApiMap.selected}`);352 throw new Error(`Unknown API type ${selectedApiMap.selected}`);
public/scripts/openai.js+33 -21
@@ -1444,8 +1444,9 @@ export async function prepareOpenAIMessages({
1444 * Handles errors during streaming requests.1444 * Handles errors during streaming requests.
1445 * @param {Response} response1445 * @param {Response} response
1446 * @param {string} decoded - response text or decoded stream data1446 * @param {string} decoded - response text or decoded stream data
1447 * @param {boolean?} [supressToastr=false]
1447 */1448 */
1448function tryParseStreamingError(response, decoded) {1449export function tryParseStreamingError(response, decoded, supressToastr = false) {
1449 try {1450 try {
1450 const data = JSON.parse(decoded);1451 const data = JSON.parse(decoded);
14511452
@@ -1453,19 +1454,19 @@ function tryParseStreamingError(response, decoded) {
1453 return;1454 return;
1454 }1455 }
14551456
1456 checkQuotaError(data);1457 checkQuotaError(data, supressToastr);
1457 checkModerationError(data);1458 checkModerationError(data, supressToastr);
14581459
1459 // these do not throw correctly (equiv to Error("[object Object]"))1460 // these do not throw correctly (equiv to Error("[object Object]"))
1460 // if trying to fix "[object Object]" displayed to users, start here1461 // if trying to fix "[object Object]" displayed to users, start here
14611462
1462 if (data.error) {1463 if (data.error) {
1463 toastr.error(data.error.message || response.statusText, 'Chat Completion API');1464 !supressToastr && toastr.error(data.error.message || response.statusText, 'Chat Completion API');
1464 throw new Error(data);1465 throw new Error(data);
1465 }1466 }
14661467
1467 if (data.message) {1468 if (data.message) {
1468 toastr.error(data.message, 'Chat Completion API');1469 !supressToastr && toastr.error(data.message, 'Chat Completion API');
1469 throw new Error(data);1470 throw new Error(data);
1470 }1471 }
1471 }1472 }
@@ -1477,16 +1478,17 @@ function tryParseStreamingError(response, decoded) {
1477/**1478/**
1478 * Checks if the response contains a quota error and displays a popup if it does.1479 * Checks if the response contains a quota error and displays a popup if it does.
1479 * @param data1480 * @param data
1481 * @param {boolean?} [supressToastr=false]
1480 * @returns {void}1482 * @returns {void}
1481 * @throws {object} - response JSON1483 * @throws {object} - response JSON
1482 */1484 */
1483function checkQuotaError(data) {1485function checkQuotaError(data, supressToastr = false) {
1484 if (!data) {1486 if (!data) {
1485 return;1487 return;
1486 }1488 }
14871489
1488 if (data.quota_error) {1490 if (data.quota_error) {
1489 renderTemplateAsync('quotaError').then((html) => Popup.show.text('Quota Error', html));1491 !supressToastr && renderTemplateAsync('quotaError').then((html) => Popup.show.text('Quota Error', html));
14901492
1491 // this does not throw correctly (equiv to Error("[object Object]"))1493 // this does not throw correctly (equiv to Error("[object Object]"))
1492 // if trying to fix "[object Object]" displayed to users, start here1494 // if trying to fix "[object Object]" displayed to users, start here
@@ -1494,9 +1496,13 @@ function checkQuotaError(data) {
1494 }1496 }
1495}1497}
14961498
1497function checkModerationError(data) {1499/**
1500 * @param {any} data
1501 * @param {boolean?} [supressToastr=false]
1502 */
1503function checkModerationError(data, supressToastr = false) {
1498 const moderationError = data?.error?.message?.includes('requires moderation');1504 const moderationError = data?.error?.message?.includes('requires moderation');
1499 if (moderationError) {1505 if (moderationError && !supressToastr) {
1500 const moderationReason = `Reasons: ${data?.error?.metadata?.reasons?.join(', ') ?? '(N/A)'}`;1506 const moderationReason = `Reasons: ${data?.error?.metadata?.reasons?.join(', ') ?? '(N/A)'}`;
1501 const flaggedText = data?.error?.metadata?.flagged_input ?? '(N/A)';1507 const flaggedText = data?.error?.metadata?.flagged_input ?? '(N/A)';
1502 toastr.info(flaggedText, moderationReason, { timeOut: 10000 });1508 toastr.info(flaggedText, moderationReason, { timeOut: 10000 });
@@ -2255,37 +2261,43 @@ async function sendOpenAIRequest(type, messages, signal) {
2255 * Extracts the reply from the response data from a chat completions-like source2261 * Extracts the reply from the response data from a chat completions-like source
2256 * @param {object} data Response data from the chat completions-like source2262 * @param {object} data Response data from the chat completions-like source
2257 * @param {object} state Additional state to keep track of2263 * @param {object} state Additional state to keep track of
2264 * @param {object} options Additional options
2265 * @param {string?} [options.chatCompletionSource] Chat completion source
2266 * @param {boolean?} [options.ignoreShowThoughts] Ignore show thoughts
2258 * @returns {string} The reply extracted from the response data2267 * @returns {string} The reply extracted from the response data
2259 */2268 */
2260function getStreamingReply(data, state) {2269export function getStreamingReply(data, state, { chatCompletionSource = null, ignoreShowThoughts = false } = {}) {
2261 if (oai_settings.chat_completion_source === chat_completion_sources.CLAUDE) {2270 const chat_completion_source = chatCompletionSource ?? oai_settings.chat_completion_source;
2262 if (oai_settings.show_thoughts) {2271 const show_thoughts = ignoreShowThoughts ? true : oai_settings.show_thoughts;
2272
2273 if (chat_completion_source === chat_completion_sources.CLAUDE) {
2274 if (show_thoughts) {
2263 state.reasoning += data?.delta?.thinking || '';2275 state.reasoning += data?.delta?.thinking || '';
2264 }2276 }
2265 return data?.delta?.text || '';2277 return data?.delta?.text || '';
2266 } else if (oai_settings.chat_completion_source === chat_completion_sources.MAKERSUITE) {2278 } else if (chat_completion_source === chat_completion_sources.MAKERSUITE) {
2267 const inlineData = data?.candidates?.[0]?.content?.parts?.find(x => x.inlineData)?.inlineData;2279 const inlineData = data?.candidates?.[0]?.content?.parts?.find(x => x.inlineData)?.inlineData;
2268 if (inlineData) {2280 if (inlineData) {
2269 state.image = `data:${inlineData.mimeType};base64,${inlineData.data}`;2281 state.image = `data:${inlineData.mimeType};base64,${inlineData.data}`;
2270 }2282 }
2271 if (oai_settings.show_thoughts) {2283 if (show_thoughts) {
2272 state.reasoning += (data?.candidates?.[0]?.content?.parts?.filter(x => x.thought)?.map(x => x.text)?.[0] || '');2284 state.reasoning += (data?.candidates?.[0]?.content?.parts?.filter(x => x.thought)?.map(x => x.text)?.[0] || '');
2273 }2285 }
2274 return data?.candidates?.[0]?.content?.parts?.filter(x => !x.thought)?.map(x => x.text)?.[0] || '';2286 return data?.candidates?.[0]?.content?.parts?.filter(x => !x.thought)?.map(x => x.text)?.[0] || '';
2275 } else if (oai_settings.chat_completion_source === chat_completion_sources.COHERE) {2287 } else if (chat_completion_source === chat_completion_sources.COHERE) {
2276 return data?.delta?.message?.content?.text || data?.delta?.message?.tool_plan || '';2288 return data?.delta?.message?.content?.text || data?.delta?.message?.tool_plan || '';
2277 } else if (oai_settings.chat_completion_source === chat_completion_sources.DEEPSEEK) {2289 } else if (chat_completion_source === chat_completion_sources.DEEPSEEK) {
2278 if (oai_settings.show_thoughts) {2290 if (show_thoughts) {
2279 state.reasoning += (data.choices?.filter(x => x?.delta?.reasoning_content)?.[0]?.delta?.reasoning_content || '');2291 state.reasoning += (data.choices?.filter(x => x?.delta?.reasoning_content)?.[0]?.delta?.reasoning_content || '');
2280 }2292 }
2281 return data.choices?.[0]?.delta?.content || '';2293 return data.choices?.[0]?.delta?.content || '';
2282 } else if (oai_settings.chat_completion_source === chat_completion_sources.OPENROUTER) {2294 } else if (chat_completion_source === chat_completion_sources.OPENROUTER) {
2283 if (oai_settings.show_thoughts) {2295 if (show_thoughts) {
2284 state.reasoning += (data.choices?.filter(x => x?.delta?.reasoning)?.[0]?.delta?.reasoning || '');2296 state.reasoning += (data.choices?.filter(x => x?.delta?.reasoning)?.[0]?.delta?.reasoning || '');
2285 }2297 }
2286 return data.choices?.[0]?.delta?.content ?? data.choices?.[0]?.message?.content ?? data.choices?.[0]?.text ?? '';2298 return data.choices?.[0]?.delta?.content ?? data.choices?.[0]?.message?.content ?? data.choices?.[0]?.text ?? '';
2287 } else if (oai_settings.chat_completion_source === chat_completion_sources.CUSTOM) {2299 } else if (chat_completion_source === chat_completion_sources.CUSTOM) {
2288 if (oai_settings.show_thoughts) {2300 if (show_thoughts) {
2289 state.reasoning +=2301 state.reasoning +=
2290 data.choices?.filter(x => x?.delta?.reasoning_content)?.[0]?.delta?.reasoning_content ??2302 data.choices?.filter(x => x?.delta?.reasoning_content)?.[0]?.delta?.reasoning_content ??
2291 data.choices?.filter(x => x?.delta?.reasoning)?.[0]?.delta?.reasoning ??2303 data.choices?.filter(x => x?.delta?.reasoning)?.[0]?.delta?.reasoning ??