Added stream support to "custom-request"
| @@ -3,10 +3,13 @@ import { extractMessageFromData, getGenerateUrl, getRequestHeaders } from '../sc | |||
| 3 | import { getTextGenServer } from './textgen-settings.js'; | 3 | import { getTextGenServer } from './textgen-settings.js'; |
| 4 | import { extractReasoningFromData } from './reasoning.js'; | 4 | import { extractReasoningFromData } from './reasoning.js'; |
| 5 | import { formatInstructModeChat, formatInstructModePrompt, names_behavior_types } from './instruct-mode.js'; | 5 | import { formatInstructModeChat, formatInstructModePrompt, names_behavior_types } from './instruct-mode.js'; |
| 6 | import { getStreamingReply, tryParseStreamingError } from './openai.js'; | ||
| 7 | import EventSourceStream from './sse-stream.js'; | ||
| 6 | 8 | ||
| 7 | // #region Type Definitions | 9 | // #region Type Definitions |
| 8 | /** | 10 | /** |
| 9 | * @typedef {Object} TextCompletionRequestBase | 11 | * @typedef {Object} TextCompletionRequestBase |
| 12 | * @property {boolean?} [stream=false] - Whether to stream the response | ||
| 10 | * @property {number} max_tokens - Maximum number of tokens to generate | 13 | * @property {number} max_tokens - Maximum number of tokens to generate |
| 11 | * @property {string} [model] - Optional model name | 14 | * @property {string} [model] - Optional model name |
| 12 | * @property {string} api_type - Type of API to use | 15 | * @property {string} api_type - Type of API to use |
| @@ -17,6 +20,7 @@ import { formatInstructModeChat, formatInstructModePrompt, names_behavior_types | |||
| 17 | 20 | ||
| 18 | /** | 21 | /** |
| 19 | * @typedef {Object} TextCompletionPayloadBase | 22 | * @typedef {Object} TextCompletionPayloadBase |
| 23 | * @property {boolean?} [stream=false] - Whether to stream the response | ||
| 20 | * @property {string} prompt - The text prompt for completion | 24 | * @property {string} prompt - The text prompt for completion |
| 21 | * @property {number} max_tokens - Maximum number of tokens to generate | 25 | * @property {number} max_tokens - Maximum number of tokens to generate |
| 22 | * @property {number} max_new_tokens - Alias for max_tokens | 26 | * @property {number} max_new_tokens - Alias for max_tokens |
| @@ -36,6 +40,7 @@ import { formatInstructModeChat, formatInstructModePrompt, names_behavior_types | |||
| 36 | 40 | ||
| 37 | /** | 41 | /** |
| 38 | * @typedef {Object} ChatCompletionPayloadBase | 42 | * @typedef {Object} ChatCompletionPayloadBase |
| 43 | * @property {boolean?} [stream=false] - Whether to stream the response | ||
| 39 | * @property {ChatCompletionMessage[]} messages - Array of chat messages | 44 | * @property {ChatCompletionMessage[]} messages - Array of chat messages |
| 40 | * @property {string} [model] - Optional model name to use for completion | 45 | * @property {string} [model] - Optional model name to use for completion |
| 41 | * @property {string} chat_completion_source - Source provider for chat completion | 46 | * @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 | */ |
| 54 | 59 | ||
| 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 | // #endregion | 70 | // #endregion |
| 56 | 71 | ||
| 57 | /** | 72 | /** |
| 58 | * Creates & sends a text completion request. Streaming is not supported. | 73 | * Creates & sends a text completion request. |
| 59 | */ | 74 | */ |
| 60 | export class TextCompletionService { | 75 | export 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}} custom | 79 | * @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 | }; |
| 80 | 95 | ||
| 81 | // Remove undefined values to avoid API errors | 96 | // Remove undefined values to avoid API errors |
| @@ -92,34 +107,81 @@ export class TextCompletionService { | |||
| 92 | * Sends a text completion request to the specified server | 107 | * Sends a text completion request to the specified server |
| 93 | * @param {TextCompletionPayload} data Request data | 108 | * @param {TextCompletionPayload} data Request data |
| 94 | * @param {boolean?} extractData Extract message from the response. Default true | 109 | * @param {boolean?} extractData Extract message from the response. Default true |
| 95 | * @returns {Promise<ExtractedData | any>} Extracted data or the raw response | 110 | * @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 | }); |
| 106 | 150 | ||
| 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 | } | ||
| 111 | 154 | ||
| 112 | if (!extractData) { | 155 | throw new Error(`Got response status ${response.status}`); |
| 113 | return json; | ||
| 114 | } | 156 | } |
| 115 | 157 | ||
| 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 | } |
| 125 | 187 | ||
| @@ -130,13 +192,15 @@ export class TextCompletionService { | |||
| 130 | * @param {string?} [options.presetName] - Name of the preset to use for generation settings | 192 | * @param {string?} [options.presetName] - Name of the preset to use for generation settings |
| 131 | * @param {string?} [options.instructName] - Name of instruct preset for message formatting | 193 | * @param {string?} [options.instructName] - Name of instruct preset for message formatting |
| 132 | * @param {boolean} extractData - Whether to extract structured data from response | 194 | * @param {boolean} extractData - Whether to extract structured data from response |
| 133 | * @returns {Promise<ExtractedData | any>} Extracted data or the raw response | 195 | * @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-ignore | 284 | // @ts-ignore |
| 221 | const data = this.createRequestData(requestData); | 285 | const data = this.createRequestData(requestData); |
| 222 | 286 | ||
| 223 | return await this.sendRequest(data, extractData); | 287 | return await this.sendRequest(data, extractData, signal); |
| 224 | } | 288 | } |
| 225 | 289 | ||
| 226 | /** | 290 | /** |
| @@ -256,7 +320,7 @@ export class TextCompletionService { | |||
| 256 | } | 320 | } |
| 257 | 321 | ||
| 258 | /** | 322 | /** |
| 259 | * Creates & sends a chat completion request. Streaming is not supported. | 323 | * Creates & sends a chat completion request. |
| 260 | */ | 324 | */ |
| 261 | export class ChatCompletionService { | 325 | export class ChatCompletionService { |
| 262 | static TYPE = 'openai'; | 326 | static TYPE = 'openai'; |
| @@ -265,16 +329,16 @@ export class ChatCompletionService { | |||
| 265 | * @param {ChatCompletionPayload} custom | 329 | * @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 | }; |
| 279 | 343 | ||
| 280 | // Remove undefined values to avoid API errors | 344 | // Remove undefined values to avoid API errors |
| @@ -291,34 +355,74 @@ export class ChatCompletionService { | |||
| 291 | * Sends a chat completion request | 355 | * Sends a chat completion request |
| 292 | * @param {ChatCompletionPayload} data Request data | 356 | * @param {ChatCompletionPayload} data Request data |
| 293 | * @param {boolean?} extractData Extract message from the response. Default true | 357 | * @param {boolean?} extractData Extract message from the response. Default true |
| 294 | * @returns {Promise<ExtractedData | any>} Extracted data or the raw response | 358 | * @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 | }); |
| 305 | 370 | ||
| 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 | } |
| 310 | 390 | ||
| 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 | } |
| 314 | 397 | ||
| 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 | } |
| 324 | 428 | ||
| @@ -327,11 +431,12 @@ export class ChatCompletionService { | |||
| 327 | * @param {ChatCompletionPayload} custom | 431 | * @param {ChatCompletionPayload} custom |
| 328 | * @param {Object} options - Configuration options | 432 | * @param {Object} options - Configuration options |
| 329 | * @param {string?} [options.presetName] - Name of the preset to use for generation settings | 433 | * @param {string?} [options.presetName] - Name of the preset to use for generation settings |
| 330 | * @param {boolean} extractData - Whether to extract structured data from response | 434 | * @param {boolean} [extractData=true] - Whether to extract structured data from response |
| 331 | * @returns {Promise<ExtractedData | any>} Extracted data or the raw response | 435 | * @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 }; |
| 337 | 442 | ||
| @@ -354,7 +459,7 @@ export class ChatCompletionService { | |||
| 354 | 459 | ||
| 355 | const data = this.createRequestData(requestData); | 460 | const data = this.createRequestData(requestData); |
| 356 | 461 | ||
| 357 | return await this.sendRequest(data, extractData); | 462 | return await this.sendRequest(data, extractData, signal); |
| 358 | } | 463 | } |
| 359 | 464 | ||
| 360 | /** | 465 | /** |
| @@ -276,10 +276,12 @@ export async function getWebLlmContextSize() { | |||
| 276 | } | 276 | } |
| 277 | 277 | ||
| 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 | */ |
| 281 | export class ConnectionManagerRequestService { | 281 | export 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} profileId | 298 | * @param {string} profileId |
| 297 | * @param {string | (import('../custom-request.js').ChatCompletionMessage & {ignoreInstruct?: boolean})[]} prompt | 299 | * @param {string | (import('../custom-request.js').ChatCompletionMessage & {ignoreInstruct?: boolean})[]} prompt |
| 298 | * @param {number} maxTokens | 300 | * @param {number} maxTokens |
| 299 | * @param {{extractData?: boolean, includePreset?: boolean, includeInstruct?: boolean}} custom - default values are true | 301 | * @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 response | 302 | * @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 }; |
| 304 | 306 | ||
| 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 { | |||
| 319 | 321 | ||
| 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 | } |
| 335 | 338 | ||
| 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}`); |
| @@ -1444,8 +1444,9 @@ export async function prepareOpenAIMessages({ | |||
| 1444 | * Handles errors during streaming requests. | 1444 | * Handles errors during streaming requests. |
| 1445 | * @param {Response} response | 1445 | * @param {Response} response |
| 1446 | * @param {string} decoded - response text or decoded stream data | 1446 | * @param {string} decoded - response text or decoded stream data |
| 1447 | * @param {boolean?} [supressToastr=false] | ||
| 1447 | */ | 1448 | */ |
| 1448 | function tryParseStreamingError(response, decoded) { | 1449 | export function tryParseStreamingError(response, decoded, supressToastr = false) { |
| 1449 | try { | 1450 | try { |
| 1450 | const data = JSON.parse(decoded); | 1451 | const data = JSON.parse(decoded); |
| 1451 | 1452 | ||
| @@ -1453,19 +1454,19 @@ function tryParseStreamingError(response, decoded) { | |||
| 1453 | return; | 1454 | return; |
| 1454 | } | 1455 | } |
| 1455 | 1456 | ||
| 1456 | checkQuotaError(data); | 1457 | checkQuotaError(data, supressToastr); |
| 1457 | checkModerationError(data); | 1458 | checkModerationError(data, supressToastr); |
| 1458 | 1459 | ||
| 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 here | 1461 | // if trying to fix "[object Object]" displayed to users, start here |
| 1461 | 1462 | ||
| 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 | } |
| 1466 | 1467 | ||
| 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 data | 1480 | * @param data |
| 1481 | * @param {boolean?} [supressToastr=false] | ||
| 1480 | * @returns {void} | 1482 | * @returns {void} |
| 1481 | * @throws {object} - response JSON | 1483 | * @throws {object} - response JSON |
| 1482 | */ | 1484 | */ |
| 1483 | function checkQuotaError(data) { | 1485 | function checkQuotaError(data, supressToastr = false) { |
| 1484 | if (!data) { | 1486 | if (!data) { |
| 1485 | return; | 1487 | return; |
| 1486 | } | 1488 | } |
| 1487 | 1489 | ||
| 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)); |
| 1490 | 1492 | ||
| 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 here | 1494 | // if trying to fix "[object Object]" displayed to users, start here |
| @@ -1494,9 +1496,13 @@ function checkQuotaError(data) { | |||
| 1494 | } | 1496 | } |
| 1495 | } | 1497 | } |
| 1496 | 1498 | ||
| 1497 | function checkModerationError(data) { | 1499 | /** |
| 1500 | * @param {any} data | ||
| 1501 | * @param {boolean?} [supressToastr=false] | ||
| 1502 | */ | ||
| 1503 | function 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 source | 2261 | * Extracts the reply from the response data from a chat completions-like source |
| 2256 | * @param {object} data Response data from the chat completions-like source | 2262 | * @param {object} data Response data from the chat completions-like source |
| 2257 | * @param {object} state Additional state to keep track of | 2263 | * @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 data | 2267 | * @returns {string} The reply extracted from the response data |
| 2259 | */ | 2268 | */ |
| 2260 | function getStreamingReply(data, state) { | 2269 | export 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 ?? |