Merge pull request #3742 from bmen25124/custom_request_stream Added stream support to "custom-request"

50334890a64b14949690e40ff215c56b5bd2e7da

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

Signed
3 files changed, +168 -45Showing whitespace changes
public/scripts/custom-request.js+122 -18
@@ -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,19 @@ 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 */
68
55// #endregion69// #endregion
5670
57/**71/**
58 * Creates & sends a text completion request. Streaming is not supported.72 * Creates & sends a text completion request.
59 */73 */
60export class TextCompletionService {74export class TextCompletionService {
61 static TYPE = 'textgenerationwebui';75 static TYPE = 'textgenerationwebui';
@@ -64,9 +78,10 @@ export class TextCompletionService {
64 * @param {Record<string, any> & TextCompletionRequestBase & {prompt: string}} custom78 * @param {Record<string, any> & TextCompletionRequestBase & {prompt: string}} custom
65 * @returns {TextCompletionPayload}79 * @returns {TextCompletionPayload}
66 */80 */
67 static createRequestData({ prompt, max_tokens, model, api_type, api_server, temperature, min_p, ...props }) {81 static createRequestData({ stream = false, prompt, max_tokens, model, api_type, api_server, temperature, min_p, ...props }) {
68 const payload = {82 const payload = {
69 ...props,83 ...props,
84 stream,
70 prompt,85 prompt,
71 max_tokens,86 max_tokens,
72 max_new_tokens: max_tokens,87 max_new_tokens: max_tokens,
@@ -75,7 +90,6 @@ export class TextCompletionService {
75 api_server: api_server ?? getTextGenServer(api_type),90 api_server: api_server ?? getTextGenServer(api_type),
76 temperature,91 temperature,
77 min_p,92 min_p,
78 stream: false,
79 };93 };
8094
81 // Remove undefined values to avoid API errors95 // Remove undefined values to avoid API errors
@@ -92,16 +106,18 @@ export class TextCompletionService {
92 * Sends a text completion request to the specified server106 * Sends a text completion request to the specified server
93 * @param {TextCompletionPayload} data Request data107 * @param {TextCompletionPayload} data Request data
94 * @param {boolean?} extractData Extract message from the response. Default true108 * @param {boolean?} extractData Extract message from the response. Default true
95 * @returns {Promise<ExtractedData | any>} Extracted data or the raw response109 * @param {AbortSignal?} signal
110 * @returns {Promise<ExtractedData | (() => AsyncGenerator<StreamResponse>)>} If not streaming, returns extracted data; if streaming, returns a function that creates an AsyncGenerator
96 * @throws {Error}111 * @throws {Error}
97 */112 */
98 static async sendRequest(data, extractData = true) {113 static async sendRequest(data, extractData = true, signal = null) {
114 if (!data.stream) {
99 const response = await fetch(getGenerateUrl(this.TYPE), {115 const response = await fetch(getGenerateUrl(this.TYPE), {
100 method: 'POST',116 method: 'POST',
101 headers: getRequestHeaders(),117 headers: getRequestHeaders(),
102 cache: 'no-cache',118 cache: 'no-cache',
103 body: JSON.stringify(data),119 body: JSON.stringify(data),
104 signal: new AbortController().signal,120 signal: signal ?? new AbortController().signal,
105 });121 });
106122
107 const json = await response.json();123 const json = await response.json();
@@ -123,6 +139,51 @@ export class TextCompletionService {
123 };139 };
124 }140 }
125141
142 const response = await fetch('/api/backends/text-completions/generate', {
143 method: 'POST',
144 headers: getRequestHeaders(),
145 cache: 'no-cache',
146 body: JSON.stringify(data),
147 signal: signal ?? new AbortController().signal,
148 });
149
150 if (!response.ok) {
151 const text = await response.text();
152 tryParseStreamingError(response, text, { quiet: true });
153
154 throw new Error(`Got response status ${response.status}`);
155 }
156
157 const eventStream = new EventSourceStream();
158 response.body.pipeThrough(eventStream);
159 const reader = eventStream.readable.getReader();
160 return async function* streamData() {
161 let text = '';
162 const swipes = [];
163 const state = { reasoning: '' };
164 while (true) {
165 const { done, value } = await reader.read();
166 if (done) return;
167 if (value.data === '[DONE]') return;
168
169 tryParseStreamingError(response, value.data, { quiet: true });
170
171 let data = JSON.parse(value.data);
172
173 if (data?.choices?.[0]?.index > 0) {
174 const swipeIndex = data.choices[0].index - 1;
175 swipes[swipeIndex] = (swipes[swipeIndex] || '') + data.choices[0].text;
176 } else {
177 const newText = data?.choices?.[0]?.text || data?.content || '';
178 text += newText;
179 state.reasoning += data?.choices?.[0]?.reasoning ?? '';
180 }
181
182 yield { text, swipes, state };
183 }
184 };
185 }
186
126 /**187 /**
127 * Process and send a text completion request with optional preset & instruct188 * Process and send a text completion request with optional preset & instruct
128 * @param {Record<string, any> & TextCompletionRequestBase & {prompt: (ChatCompletionMessage & {ignoreInstruct?: boolean})[] |string}} custom189 * @param {Record<string, any> & TextCompletionRequestBase & {prompt: (ChatCompletionMessage & {ignoreInstruct?: boolean})[] |string}} custom
@@ -130,13 +191,15 @@ export class TextCompletionService {
130 * @param {string?} [options.presetName] - Name of the preset to use for generation settings191 * @param {string?} [options.presetName] - Name of the preset to use for generation settings
131 * @param {string?} [options.instructName] - Name of instruct preset for message formatting192 * @param {string?} [options.instructName] - Name of instruct preset for message formatting
132 * @param {boolean} extractData - Whether to extract structured data from response193 * @param {boolean} extractData - Whether to extract structured data from response
133 * @returns {Promise<ExtractedData | any>} Extracted data or the raw response194 * @param {AbortSignal?} [signal]
195 * @returns {Promise<ExtractedData | (() => AsyncGenerator<StreamResponse>)>} If not streaming, returns extracted data; if streaming, returns a function that creates an AsyncGenerator
134 * @throws {Error}196 * @throws {Error}
135 */197 */
136 static async processRequest(198 static async processRequest(
137 custom,199 custom,
138 options = {},200 options = {},
139 extractData = true,201 extractData = true,
202 signal = null,
140 ) {203 ) {
141 const { presetName, instructName } = options;204 const { presetName, instructName } = options;
142 let requestData = { ...custom };205 let requestData = { ...custom };
@@ -220,7 +283,7 @@ export class TextCompletionService {
220 // @ts-ignore283 // @ts-ignore
221 const data = this.createRequestData(requestData);284 const data = this.createRequestData(requestData);
222285
223 return await this.sendRequest(data, extractData);286 return await this.sendRequest(data, extractData, signal);
224 }287 }
225288
226 /**289 /**
@@ -256,7 +319,7 @@ export class TextCompletionService {
256}319}
257320
258/**321/**
259 * Creates & sends a chat completion request. Streaming is not supported.322 * Creates & sends a chat completion request.
260 */323 */
261export class ChatCompletionService {324export class ChatCompletionService {
262 static TYPE = 'openai';325 static TYPE = 'openai';
@@ -265,16 +328,16 @@ export class ChatCompletionService {
265 * @param {ChatCompletionPayload} custom328 * @param {ChatCompletionPayload} custom
266 * @returns {ChatCompletionPayload}329 * @returns {ChatCompletionPayload}
267 */330 */
268 static createRequestData({ messages, model, chat_completion_source, max_tokens, temperature, custom_url, ...props }) {331 static createRequestData({ stream = false, messages, model, chat_completion_source, max_tokens, temperature, custom_url, ...props }) {
269 const payload = {332 const payload = {
270 ...props,333 ...props,
334 stream,
271 messages,335 messages,
272 model,336 model,
273 chat_completion_source,337 chat_completion_source,
274 max_tokens,338 max_tokens,
275 temperature,339 temperature,
276 custom_url,340 custom_url,
277 stream: false,
278 };341 };
279342
280 // Remove undefined values to avoid API errors343 // Remove undefined values to avoid API errors
@@ -291,18 +354,20 @@ export class ChatCompletionService {
291 * Sends a chat completion request354 * Sends a chat completion request
292 * @param {ChatCompletionPayload} data Request data355 * @param {ChatCompletionPayload} data Request data
293 * @param {boolean?} extractData Extract message from the response. Default true356 * @param {boolean?} extractData Extract message from the response. Default true
294 * @returns {Promise<ExtractedData | any>} Extracted data or the raw response357 * @param {AbortSignal?} signal Abort signal
358 * @returns {Promise<ExtractedData | (() => AsyncGenerator<StreamResponse>)>} If not streaming, returns extracted data; if streaming, returns a function that creates an AsyncGenerator
295 * @throws {Error}359 * @throws {Error}
296 */360 */
297 static async sendRequest(data, extractData = true) {361 static async sendRequest(data, extractData = true, signal = null) {
298 const response = await fetch('/api/backends/chat-completions/generate', {362 const response = await fetch('/api/backends/chat-completions/generate', {
299 method: 'POST',363 method: 'POST',
300 headers: getRequestHeaders(),364 headers: getRequestHeaders(),
301 cache: 'no-cache',365 cache: 'no-cache',
302 body: JSON.stringify(data),366 body: JSON.stringify(data),
303 signal: new AbortController().signal,367 signal: signal ?? new AbortController().signal,
304 });368 });
305369
370 if (!data.stream) {
306 const json = await response.json();371 const json = await response.json();
307 if (!response.ok || json.error) {372 if (!response.ok || json.error) {
308 throw json;373 throw json;
@@ -322,16 +387,55 @@ export class ChatCompletionService {
322 };387 };
323 }388 }
324389
390 if (!response.ok) {
391 const text = await response.text();
392 tryParseStreamingError(response, text, { quiet: true });
393
394 throw new Error(`Got response status ${response.status}`);
395 }
396
397 const eventStream = new EventSourceStream();
398 response.body.pipeThrough(eventStream);
399 const reader = eventStream.readable.getReader();
400 return async function* streamData() {
401 let text = '';
402 const swipes = [];
403 const state = { reasoning: '', image: '' };
404 while (true) {
405 const { done, value } = await reader.read();
406 if (done) return;
407 const rawData = value.data;
408 if (rawData === '[DONE]') return;
409 tryParseStreamingError(response, rawData, { quiet: true });
410 const parsed = JSON.parse(rawData);
411
412 const reply = getStreamingReply(parsed, state, {
413 chatCompletionSource: data.chat_completion_source,
414 overrideShowThoughts: true,
415 });
416 if (Array.isArray(parsed?.choices) && parsed?.choices?.[0]?.index > 0) {
417 const swipeIndex = parsed.choices[0].index - 1;
418 swipes[swipeIndex] = (swipes[swipeIndex] || '') + reply;
419 } else {
420 text += reply;
421 }
422
423 yield { text, swipes: swipes, state };
424 }
425 };
426 }
427
325 /**428 /**
326 * Process and send a chat completion request with optional preset429 * Process and send a chat completion request with optional preset
327 * @param {ChatCompletionPayload} custom430 * @param {ChatCompletionPayload} custom
328 * @param {Object} options - Configuration options431 * @param {Object} options - Configuration options
329 * @param {string?} [options.presetName] - Name of the preset to use for generation settings432 * @param {string?} [options.presetName] - Name of the preset to use for generation settings
330 * @param {boolean} extractData - Whether to extract structured data from response433 * @param {boolean} [extractData=true] - Whether to extract structured data from response
331 * @returns {Promise<ExtractedData | any>} Extracted data or the raw response434 * @param {AbortSignal?} [signal] - Abort signal
435 * @returns {Promise<ExtractedData | (() => AsyncGenerator<StreamResponse>)>} If not streaming, returns extracted data; if streaming, returns a function that creates an AsyncGenerator
332 * @throws {Error}436 * @throws {Error}
333 */437 */
334 static async processRequest(custom, options, extractData = true) {438 static async processRequest(custom, options, extractData = true, signal = null) {
335 const { presetName } = options;439 const { presetName } = options;
336 let requestData = { ...custom };440 let requestData = { ...custom };
337441
@@ -354,7 +458,7 @@ export class ChatCompletionService {
354458
355 const data = this.createRequestData(requestData);459 const data = this.createRequestData(requestData);
356460
357 return await this.sendRequest(data, extractData);461 return await this.sendRequest(data, extractData, signal);
358 }462 }
359463
360 /**464 /**
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+36 -21
@@ -1444,8 +1444,10 @@ 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 {object} [options]
1448 * @param {boolean?} [options.quiet=false] Suppress toast messages
1447 */1449 */
1448function tryParseStreamingError(response, decoded) {1450export function tryParseStreamingError(response, decoded, { quiet = false } = {}) {
1449 try {1451 try {
1450 const data = JSON.parse(decoded);1452 const data = JSON.parse(decoded);
14511453
@@ -1453,19 +1455,19 @@ function tryParseStreamingError(response, decoded) {
1453 return;1455 return;
1454 }1456 }
14551457
1456 checkQuotaError(data);1458 checkQuotaError(data, { quiet });
1457 checkModerationError(data);1459 checkModerationError(data, { quiet });
14581460
1459 // these do not throw correctly (equiv to Error("[object Object]"))1461 // these do not throw correctly (equiv to Error("[object Object]"))
1460 // if trying to fix "[object Object]" displayed to users, start here1462 // if trying to fix "[object Object]" displayed to users, start here
14611463
1462 if (data.error) {1464 if (data.error) {
1463 toastr.error(data.error.message || response.statusText, 'Chat Completion API');1465 !quiet && toastr.error(data.error.message || response.statusText, 'Chat Completion API');
1464 throw new Error(data);1466 throw new Error(data);
1465 }1467 }
14661468
1467 if (data.message) {1469 if (data.message) {
1468 toastr.error(data.message, 'Chat Completion API');1470 !quiet && toastr.error(data.message, 'Chat Completion API');
1469 throw new Error(data);1471 throw new Error(data);
1470 }1472 }
1471 }1473 }
@@ -1477,16 +1479,18 @@ function tryParseStreamingError(response, decoded) {
1477/**1479/**
1478 * Checks if the response contains a quota error and displays a popup if it does.1480 * Checks if the response contains a quota error and displays a popup if it does.
1479 * @param data1481 * @param data
1482 * @param {object} [options]
1483 * @param {boolean?} [options.quiet=false] Suppress toast messages
1480 * @returns {void}1484 * @returns {void}
1481 * @throws {object} - response JSON1485 * @throws {object} - response JSON
1482 */1486 */
1483function checkQuotaError(data) {1487function checkQuotaError(data, { quiet = false } = {}) {
1484 if (!data) {1488 if (!data) {
1485 return;1489 return;
1486 }1490 }
14871491
1488 if (data.quota_error) {1492 if (data.quota_error) {
1489 renderTemplateAsync('quotaError').then((html) => Popup.show.text('Quota Error', html));1493 !quiet && renderTemplateAsync('quotaError').then((html) => Popup.show.text('Quota Error', html));
14901494
1491 // this does not throw correctly (equiv to Error("[object Object]"))1495 // this does not throw correctly (equiv to Error("[object Object]"))
1492 // if trying to fix "[object Object]" displayed to users, start here1496 // if trying to fix "[object Object]" displayed to users, start here
@@ -1494,9 +1498,14 @@ function checkQuotaError(data) {
1494 }1498 }
1495}1499}
14961500
1497function checkModerationError(data) {1501/**
1502 * @param {any} data
1503 * @param {object} [options]
1504 * @param {boolean?} [options.quiet=false] Suppress toast messages
1505 */
1506function checkModerationError(data, { quiet = false } = {}) {
1498 const moderationError = data?.error?.message?.includes('requires moderation');1507 const moderationError = data?.error?.message?.includes('requires moderation');
1499 if (moderationError) {1508 if (moderationError && !quiet) {
1500 const moderationReason = `Reasons: ${data?.error?.metadata?.reasons?.join(', ') ?? '(N/A)'}`;1509 const moderationReason = `Reasons: ${data?.error?.metadata?.reasons?.join(', ') ?? '(N/A)'}`;
1501 const flaggedText = data?.error?.metadata?.flagged_input ?? '(N/A)';1510 const flaggedText = data?.error?.metadata?.flagged_input ?? '(N/A)';
1502 toastr.info(flaggedText, moderationReason, { timeOut: 10000 });1511 toastr.info(flaggedText, moderationReason, { timeOut: 10000 });
@@ -2255,37 +2264,43 @@ async function sendOpenAIRequest(type, messages, signal) {
2255 * Extracts the reply from the response data from a chat completions-like source2264 * Extracts the reply from the response data from a chat completions-like source
2256 * @param {object} data Response data from the chat completions-like source2265 * @param {object} data Response data from the chat completions-like source
2257 * @param {object} state Additional state to keep track of2266 * @param {object} state Additional state to keep track of
2267 * @param {object} [options] Additional options
2268 * @param {string?} [options.chatCompletionSource] Chat completion source
2269 * @param {boolean?} [options.overrideShowThoughts] Override show thoughts
2258 * @returns {string} The reply extracted from the response data2270 * @returns {string} The reply extracted from the response data
2259 */2271 */
2260function getStreamingReply(data, state) {2272export function getStreamingReply(data, state, { chatCompletionSource = null, overrideShowThoughts = null } = {}) {
2261 if (oai_settings.chat_completion_source === chat_completion_sources.CLAUDE) {2273 const chat_completion_source = chatCompletionSource ?? oai_settings.chat_completion_source;
2262 if (oai_settings.show_thoughts) {2274 const show_thoughts = overrideShowThoughts ?? oai_settings.show_thoughts;
2275
2276 if (chat_completion_source === chat_completion_sources.CLAUDE) {
2277 if (show_thoughts) {
2263 state.reasoning += data?.delta?.thinking || '';2278 state.reasoning += data?.delta?.thinking || '';
2264 }2279 }
2265 return data?.delta?.text || '';2280 return data?.delta?.text || '';
2266 } else if (oai_settings.chat_completion_source === chat_completion_sources.MAKERSUITE) {2281 } else if (chat_completion_source === chat_completion_sources.MAKERSUITE) {
2267 const inlineData = data?.candidates?.[0]?.content?.parts?.find(x => x.inlineData)?.inlineData;2282 const inlineData = data?.candidates?.[0]?.content?.parts?.find(x => x.inlineData)?.inlineData;
2268 if (inlineData) {2283 if (inlineData) {
2269 state.image = `data:${inlineData.mimeType};base64,${inlineData.data}`;2284 state.image = `data:${inlineData.mimeType};base64,${inlineData.data}`;
2270 }2285 }
2271 if (oai_settings.show_thoughts) {2286 if (show_thoughts) {
2272 state.reasoning += (data?.candidates?.[0]?.content?.parts?.filter(x => x.thought)?.map(x => x.text)?.[0] || '');2287 state.reasoning += (data?.candidates?.[0]?.content?.parts?.filter(x => x.thought)?.map(x => x.text)?.[0] || '');
2273 }2288 }
2274 return data?.candidates?.[0]?.content?.parts?.filter(x => !x.thought)?.map(x => x.text)?.[0] || '';2289 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) {2290 } else if (chat_completion_source === chat_completion_sources.COHERE) {
2276 return data?.delta?.message?.content?.text || data?.delta?.message?.tool_plan || '';2291 return data?.delta?.message?.content?.text || data?.delta?.message?.tool_plan || '';
2277 } else if (oai_settings.chat_completion_source === chat_completion_sources.DEEPSEEK) {2292 } else if (chat_completion_source === chat_completion_sources.DEEPSEEK) {
2278 if (oai_settings.show_thoughts) {2293 if (show_thoughts) {
2279 state.reasoning += (data.choices?.filter(x => x?.delta?.reasoning_content)?.[0]?.delta?.reasoning_content || '');2294 state.reasoning += (data.choices?.filter(x => x?.delta?.reasoning_content)?.[0]?.delta?.reasoning_content || '');
2280 }2295 }
2281 return data.choices?.[0]?.delta?.content || '';2296 return data.choices?.[0]?.delta?.content || '';
2282 } else if (oai_settings.chat_completion_source === chat_completion_sources.OPENROUTER) {2297 } else if (chat_completion_source === chat_completion_sources.OPENROUTER) {
2283 if (oai_settings.show_thoughts) {2298 if (show_thoughts) {
2284 state.reasoning += (data.choices?.filter(x => x?.delta?.reasoning)?.[0]?.delta?.reasoning || '');2299 state.reasoning += (data.choices?.filter(x => x?.delta?.reasoning)?.[0]?.delta?.reasoning || '');
2285 }2300 }
2286 return data.choices?.[0]?.delta?.content ?? data.choices?.[0]?.message?.content ?? data.choices?.[0]?.text ?? '';2301 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) {2302 } else if (chat_completion_source === chat_completion_sources.CUSTOM) {
2288 if (oai_settings.show_thoughts) {2303 if (show_thoughts) {
2289 state.reasoning +=2304 state.reasoning +=
2290 data.choices?.filter(x => x?.delta?.reasoning_content)?.[0]?.delta?.reasoning_content ??2305 data.choices?.filter(x => x?.delta?.reasoning_content)?.[0]?.delta?.reasoning_content ??
2291 data.choices?.filter(x => x?.delta?.reasoning)?.[0]?.delta?.reasoning ??2306 data.choices?.filter(x => x?.delta?.reasoning)?.[0]?.delta?.reasoning ??