Blame Raw
Cohee · 51ad27fb · · 7249 lines (292.4 KB)
10 contributors
1/*
2* CODE FOR OPENAI SUPPORT
3* By CncAnon (@CncAnon1)
4* https://github.com/CncAnon1/TavernAITurbo
5*/
6import { Fuse, DOMPurify } from '../lib.js';
7
8import {
9 abortStatusCheck,
10 cancelStatusCheck,
11 characters,
12 event_types,
13 eventSource,
14 extension_prompt_roles,
15 extension_prompt_types,
16 Generate,
17 getExtensionPrompt,
18 getExtensionPromptMaxDepth,
19 getMediaDisplay,
20 getMediaIndex,
21 getRequestHeaders,
22 is_send_press,
23 main_api,
24 name1,
25 name2,
26 resultCheckStatus,
27 saveSettingsDebounced,
28 setOnlineStatus,
29 startStatusLoading,
30 substituteParams,
31 substituteParamsExtended,
32 system_message_types,
33 this_chid,
34} from '../script.js';
35import { getGroupNames, selected_group } from './group-chats.js';
36
37import {
38 chatCompletionDefaultPrompts,
39 INJECTION_POSITION,
40 Prompt,
41 PromptManager,
42 promptManagerDefaultPromptOrders,
43} from './PromptManager.js';
44
45import { forceCharacterEditorTokenize, getCustomStoppingStrings, persona_description_positions, power_user } from './power-user.js';
46import { SECRET_KEYS, secret_state, writeSecret } from './secrets.js';
47
48import { getEventSourceStream } from './sse-stream.js';
49import {
50 clamp,
51 createThumbnail,
52 delay,
53 download,
54 getAudioDurationFromDataURL,
55 getBase64Async,
56 getFileText,
57 getImageSizeFromDataURL,
58 getSortableDelay,
59 getStringHash,
60 getVideoDurationFromDataURL,
61 isDataURL,
62 isUuid,
63 isValidUrl,
64 parseJsonFile,
65 resetScrollHeight,
66 stringFormat,
67 textValueMatcher,
68 uuidv4,
69} from './utils.js';
70import { countTokensOpenAIAsync, getTokenizerModel } from './tokenizers.js';
71import { isMobile } from './RossAscends-mods.js';
72import { saveLogprobsForActiveMessage } from './logprobs.js';
73import { SlashCommandParser } from './slash-commands/SlashCommandParser.js';
74import { SlashCommand } from './slash-commands/SlashCommand.js';
75import { ARGUMENT_TYPE, SlashCommandArgument } from './slash-commands/SlashCommandArgument.js';
76import { renderTemplateAsync } from './templates.js';
77import { SlashCommandEnumValue } from './slash-commands/SlashCommandEnumValue.js';
78import { callGenericPopup, Popup, POPUP_RESULT, POPUP_TYPE } from './popup.js';
79import { t } from './i18n.js';
80import { ToolManager } from './tool-calling.js';
81import { accountStorage } from './util/AccountStorage.js';
82import { COMETAPI_IGNORE_PATTERNS, IGNORE_SYMBOL, MEDIA_DISPLAY, MEDIA_TYPE } from './constants.js';
83import { syncNanoGptProvidersForModel, syncOpenRouterProvidersForModel, updateNanoGptProvidersWarning, updateOpenRouterProvidersWarning } from './textgen-models.js';
84
85export {
86 openai_messages_count,
87 oai_settings,
88 loadOpenAISettings,
89 setOpenAIMessages,
90 setOpenAIMessageExamples,
91 setupChatCompletionPromptManager,
92 sendOpenAIRequest,
93 TokenHandler,
94 IdentifierNotFoundError,
95 Message,
96 MessageCollection,
97};
98
99let openai_messages_count = 0;
100
101const default_main_prompt = 'Write {{char}}\'s next reply in a fictional chat between {{charIfNotGroup}} and {{user}}.';
102const default_nsfw_prompt = '';
103const default_jailbreak_prompt = '';
104const default_impersonation_prompt = '[Write your next reply from the point of view of {{user}}, using the chat history so far as a guideline for the writing style of {{user}}. Don\'t write as {{char}} or system. Don\'t describe actions of {{char}}.]';
105const default_enhance_definitions_prompt = 'If you have more knowledge of {{char}}, add to the character\'s lore and personality to enhance them but keep the Character Sheet\'s definitions absolute.';
106const default_wi_format = '{0}';
107const default_new_chat_prompt = '[Start a new Chat]';
108const default_new_group_chat_prompt = '[Start a new group chat. Group members: {{group}}]';
109const default_new_example_chat_prompt = '[Example Chat]';
110const default_continue_nudge_prompt = '[Continue your last message without repeating its original content.]';
111const default_bias = 'Default (none)';
112const default_personality_format = '{{personality}}';
113const default_scenario_format = '{{scenario}}';
114const default_group_nudge_prompt = '[Write the next reply only as {{char}}.]';
115const default_bias_presets = {
116 [default_bias]: [],
117 'Anti-bond': [
118 { id: '22154f79-dd98-41bc-8e34-87015d6a0eaf', text: ' bond', value: -50 },
119 { id: '8ad2d5c4-d8ef-49e4-bc5e-13e7f4690e0f', text: ' future', value: -50 },
120 { id: '52a4b280-0956-4940-ac52-4111f83e4046', text: ' bonding', value: -50 },
121 { id: 'e63037c7-c9d1-4724-ab2d-7756008b433b', text: ' connection', value: -25 },
122 ],
123};
124
125const max_2k = 2047;
126const max_4k = 4095;
127const max_8k = 8191;
128const max_16k = 16383;
129const max_32k = 32767;
130const max_64k = 65535;
131const max_128k = 128 * 1000;
132const max_200k = 200 * 1000;
133const max_256k = 256 * 1000;
134const max_400k = 400 * 1000;
135const max_1mil = 1000 * 1000;
136const max_2mil = 2000 * 1000;
137const unlocked_max = max_2mil;
138const oai_max_temp = 2.0;
139const claude_max_temp = 1.0;
140const mistral_max_temp = 1.5;
141const openrouter_website_model = 'OR_Website';
142const openai_max_stop_strings = 4;
143
144const textCompletionModels = [
145 'gpt-3.5-turbo-instruct',
146 'gpt-3.5-turbo-instruct-0914',
147 'text-davinci-003',
148 'text-davinci-002',
149 'text-davinci-001',
150 'text-curie-001',
151 'text-babbage-001',
152 'text-ada-001',
153 'code-davinci-002',
154 'code-davinci-001',
155 'code-cushman-002',
156 'code-cushman-001',
157 'text-davinci-edit-001',
158 'code-davinci-edit-001',
159 'text-embedding-ada-002',
160 'text-similarity-davinci-001',
161 'text-similarity-curie-001',
162 'text-similarity-babbage-001',
163 'text-similarity-ada-001',
164 'text-search-davinci-doc-001',
165 'text-search-curie-doc-001',
166 'text-search-babbage-doc-001',
167 'text-search-ada-doc-001',
168 'code-search-babbage-code-001',
169 'code-search-ada-code-001',
170];
171
172let biasCache = undefined;
173export let model_list = [];
174
175export const chat_completion_sources = {
176 OPENAI: 'openai',
177 CLAUDE: 'claude',
178 OPENROUTER: 'openrouter',
179 AI21: 'ai21',
180 MAKERSUITE: 'makersuite',
181 VERTEXAI: 'vertexai',
182 MISTRALAI: 'mistralai',
183 CUSTOM: 'custom',
184 COHERE: 'cohere',
185 PERPLEXITY: 'perplexity',
186 GROQ: 'groq',
187 ELECTRONHUB: 'electronhub',
188 CHUTES: 'chutes',
189 NANOGPT: 'nanogpt',
190 DEEPSEEK: 'deepseek',
191 AIMLAPI: 'aimlapi',
192 XAI: 'xai',
193 POLLINATIONS: 'pollinations',
194 MOONSHOT: 'moonshot',
195 FIREWORKS: 'fireworks',
196 COMETAPI: 'cometapi',
197 AZURE_OPENAI: 'azure_openai',
198 ZAI: 'zai',
199 SILICONFLOW: 'siliconflow',
200 WORKERS_AI: 'workers_ai',
201 MINIMAX: 'minimax',
202};
203
204const character_names_behavior = {
205 NONE: -1,
206 DEFAULT: 0,
207 COMPLETION: 1,
208 CONTENT: 2,
209};
210
211const continue_postfix_types = {
212 NONE: '',
213 SPACE: ' ',
214 NEWLINE: '\n',
215 DOUBLE_NEWLINE: '\n\n',
216};
217
218export const custom_prompt_post_processing_types = {
219 NONE: '',
220 /** @deprecated Use MERGE instead. */
221 CLAUDE: 'claude',
222 MERGE: 'merge',
223 MERGE_TOOLS: 'merge_tools',
224 SEMI: 'semi',
225 SEMI_TOOLS: 'semi_tools',
226 STRICT: 'strict',
227 STRICT_TOOLS: 'strict_tools',
228 SINGLE: 'single',
229};
230
231const openrouter_middleout_types = {
232 AUTO: 'auto',
233 ON: 'on',
234 OFF: 'off',
235};
236
237export const reasoning_effort_types = {
238 auto: 'auto',
239 low: 'low',
240 medium: 'medium',
241 high: 'high',
242 min: 'min',
243 max: 'max',
244};
245
246export const verbosity_levels = {
247 auto: 'auto',
248 low: 'low',
249 medium: 'medium',
250 high: 'high',
251};
252
253export const tool_reasoning_modes = {
254 DISABLED: 'disabled',
255 SINCE_LAST_USER: 'since_last_user',
256 ACTIVE_CHAIN: 'active_chain',
257};
258
259// Providers that support interleaved reasoning forwarding in tool-call chains.
260const interleaved_reasoning_providers = [
261 chat_completion_sources.OPENROUTER,
262 chat_completion_sources.CUSTOM,
263];
264
265export const ZAI_ENDPOINT = {
266 COMMON: 'common',
267 CODING: 'coding',
268};
269
270export const SILICONFLOW_ENDPOINT = {
271 GLOBAL: 'global',
272 CN: 'cn',
273};
274
275export const MINIMAX_ENDPOINT = {
276 GLOBAL: 'global',
277 CN: 'cn',
278};
279
280const sensitiveFields = [
281 'reverse_proxy',
282 'proxy_password',
283 'custom_url',
284 'custom_include_body',
285 'custom_exclude_body',
286 'custom_include_headers',
287 'vertexai_region',
288 'vertexai_express_project_id',
289 'azure_base_url',
290 'azure_deployment_name',
291 'workers_ai_account_id',
292];
293
294/**
295 * preset_name -> [selector, setting_name, is_checkbox, is_connection]
296 * @type {Record<string, [string, string, boolean, boolean]>}
297 */
298export const settingsToUpdate = {
299 chat_completion_source: ['#chat_completion_source', 'chat_completion_source', false, true],
300 temperature: ['#temp_openai', 'temp_openai', false, false],
301 frequency_penalty: ['#freq_pen_openai', 'freq_pen_openai', false, false],
302 presence_penalty: ['#pres_pen_openai', 'pres_pen_openai', false, false],
303 top_p: ['#top_p_openai', 'top_p_openai', false, false],
304 top_k: ['#top_k_openai', 'top_k_openai', false, false],
305 top_a: ['#top_a_openai', 'top_a_openai', false, false],
306 min_p: ['#min_p_openai', 'min_p_openai', false, false],
307 repetition_penalty: ['#repetition_penalty_openai', 'repetition_penalty_openai', false, false],
308 max_context_unlocked: ['#oai_max_context_unlocked', 'max_context_unlocked', true, false],
309 group_models: ['#cc_group_models', 'group_models', true, true],
310 sort_models: ['#cc_sort_models', 'sort_models', false, true],
311 openai_model: ['#model_openai_select', 'openai_model', false, true],
312 claude_model: ['#model_claude_select', 'claude_model', false, true],
313 openrouter_model: ['#model_openrouter_select', 'openrouter_model', false, true],
314 openrouter_use_fallback: ['#openrouter_use_fallback', 'openrouter_use_fallback', true, true],
315 openrouter_providers: ['#openrouter_providers_chat', 'openrouter_providers', false, true],
316 openrouter_quantizations: ['#openrouter_quantizations_chat', 'openrouter_quantizations', false, true],
317 openrouter_allow_fallbacks: ['#openrouter_allow_fallbacks', 'openrouter_allow_fallbacks', true, true],
318 openrouter_middleout: ['#openrouter_middleout', 'openrouter_middleout', false, true],
319 tool_reasoning_mode: ['#tool_reasoning_mode', 'tool_reasoning_mode', false, false],
320 ai21_model: ['#model_ai21_select', 'ai21_model', false, true],
321 mistralai_model: ['#model_mistralai_select', 'mistralai_model', false, true],
322 cohere_model: ['#model_cohere_select', 'cohere_model', false, true],
323 perplexity_model: ['#model_perplexity_select', 'perplexity_model', false, true],
324 groq_model: ['#model_groq_select', 'groq_model', false, true],
325 chutes_model: ['#model_chutes_select', 'chutes_model', false, true],
326 siliconflow_model: ['#model_siliconflow_select', 'siliconflow_model', false, true],
327 siliconflow_endpoint: ['#siliconflow_endpoint', 'siliconflow_endpoint', false, true],
328 minimax_model: ['#model_minimax_select', 'minimax_model', false, true],
329 minimax_endpoint: ['#minimax_endpoint', 'minimax_endpoint', false, true],
330 electronhub_model: ['#model_electronhub_select', 'electronhub_model', false, true],
331 nanogpt_model: ['#model_nanogpt_select', 'nanogpt_model', false, true],
332 nanogpt_provider: ['#nanogpt_provider', 'nanogpt_provider', false, true],
333 nanogpt_payg_override: ['#nanogpt_payg_override', 'nanogpt_payg_override', true, true],
334 deepseek_model: ['#model_deepseek_select', 'deepseek_model', false, true],
335 aimlapi_model: ['#model_aimlapi_select', 'aimlapi_model', false, true],
336 xai_model: ['#model_xai_select', 'xai_model', false, true],
337 pollinations_model: ['#model_pollinations_select', 'pollinations_model', false, true],
338 moonshot_model: ['#model_moonshot_select', 'moonshot_model', false, true],
339 fireworks_model: ['#model_fireworks_select', 'fireworks_model', false, true],
340 cometapi_model: ['#model_cometapi_select', 'cometapi_model', false, true],
341 custom_model: ['#custom_model_id', 'custom_model', false, true],
342 custom_url: ['#custom_api_url_text', 'custom_url', false, true],
343 custom_include_body: ['#custom_include_body', 'custom_include_body', false, true],
344 custom_exclude_body: ['#custom_exclude_body', 'custom_exclude_body', false, true],
345 custom_include_headers: ['#custom_include_headers', 'custom_include_headers', false, true],
346 custom_prompt_post_processing: ['#custom_prompt_post_processing', 'custom_prompt_post_processing', false, true],
347 google_model: ['#model_google_select', 'google_model', false, true],
348 vertexai_model: ['#model_vertexai_select', 'vertexai_model', false, true],
349 zai_model: ['#model_zai_select', 'zai_model', false, true],
350 zai_endpoint: ['#zai_endpoint', 'zai_endpoint', false, true],
351 workers_ai_model: ['#model_workers_ai_select', 'workers_ai_model', false, true],
352 workers_ai_account_id: ['#workers_ai_account_id', 'workers_ai_account_id', false, true],
353 openai_max_context: ['#openai_max_context', 'openai_max_context', false, false],
354 openai_max_tokens: ['#openai_max_tokens', 'openai_max_tokens', false, false],
355 names_behavior: ['#names_behavior', 'names_behavior', false, false],
356 send_if_empty: ['#send_if_empty_textarea', 'send_if_empty', false, false],
357 impersonation_prompt: ['#impersonation_prompt_textarea', 'impersonation_prompt', false, false],
358 new_chat_prompt: ['#newchat_prompt_textarea', 'new_chat_prompt', false, false],
359 new_group_chat_prompt: ['#newgroupchat_prompt_textarea', 'new_group_chat_prompt', false, false],
360 new_example_chat_prompt: ['#newexamplechat_prompt_textarea', 'new_example_chat_prompt', false, false],
361 continue_nudge_prompt: ['#continue_nudge_prompt_textarea', 'continue_nudge_prompt', false, false],
362 bias_preset_selected: ['#openai_logit_bias_preset', 'bias_preset_selected', false, false],
363 reverse_proxy: ['#openai_reverse_proxy', 'reverse_proxy', false, true],
364 wi_format: ['#wi_format_textarea', 'wi_format', false, false],
365 scenario_format: ['#scenario_format_textarea', 'scenario_format', false, false],
366 personality_format: ['#personality_format_textarea', 'personality_format', false, false],
367 group_nudge_prompt: ['#group_nudge_prompt_textarea', 'group_nudge_prompt', false, false],
368 stream_openai: ['#stream_toggle', 'stream_openai', true, false],
369 prompts: ['', 'prompts', false, false],
370 prompt_order: ['', 'prompt_order', false, false],
371 show_external_models: ['#openai_show_external_models', 'show_external_models', true, true],
372 proxy_password: ['#openai_proxy_password', 'proxy_password', false, true],
373 assistant_prefill: ['#claude_assistant_prefill', 'assistant_prefill', false, false],
374 assistant_impersonation: ['#claude_assistant_impersonation', 'assistant_impersonation', false, false],
375 use_sysprompt: ['#use_sysprompt', 'use_sysprompt', true, false],
376 vertexai_auth_mode: ['#vertexai_auth_mode', 'vertexai_auth_mode', false, true],
377 vertexai_region: ['#vertexai_region', 'vertexai_region', false, true],
378 vertexai_express_project_id: ['#vertexai_express_project_id', 'vertexai_express_project_id', false, true],
379 squash_system_messages: ['#squash_system_messages', 'squash_system_messages', true, false],
380 media_inlining: ['#openai_media_inlining', 'media_inlining', true, false],
381 inline_image_quality: ['#openai_inline_image_quality', 'inline_image_quality', false, false],
382 continue_prefill: ['#continue_prefill', 'continue_prefill', true, false],
383 continue_postfix: ['#continue_postfix', 'continue_postfix', false, false],
384 function_calling: ['#openai_function_calling', 'function_calling', true, false],
385 tool_call_recurse_limit: ['#tool_call_recurse_limit', 'tool_call_recurse_limit', false, false],
386 show_thoughts: ['#openai_show_thoughts', 'show_thoughts', true, false],
387 reasoning_effort: ['#openai_reasoning_effort', 'reasoning_effort', false, false],
388 verbosity: ['#openai_verbosity', 'verbosity', false, false],
389 enable_web_search: ['#openai_enable_web_search', 'enable_web_search', true, false],
390 seed: ['#seed_openai', 'seed', false, false],
391 n: ['#n_openai', 'n', false, false],
392 bypass_status_check: ['#openai_bypass_status_check', 'bypass_status_check', true, true],
393 request_images: ['#openai_request_images', 'request_images', true, false],
394 request_image_aspect_ratio: ['#request_image_aspect_ratio', 'request_image_aspect_ratio', false, false],
395 request_image_resolution: ['#request_image_resolution', 'request_image_resolution', false, false],
396 azure_base_url: ['#azure_base_url', 'azure_base_url', false, true],
397 azure_deployment_name: ['#azure_deployment_name', 'azure_deployment_name', false, true],
398 azure_api_version: ['#azure_api_version', 'azure_api_version', false, true],
399 azure_openai_model: ['#azure_openai_model', 'azure_openai_model', false, true],
400 extensions: ['#NULL_SELECTOR', 'extensions', false, false],
401};
402
403const default_settings = {
404 preset_settings_openai: 'Default',
405 temp_openai: 1.0,
406 freq_pen_openai: 0,
407 pres_pen_openai: 0,
408 top_p_openai: 1.0,
409 top_k_openai: 0,
410 min_p_openai: 0,
411 top_a_openai: 0,
412 repetition_penalty_openai: 1,
413 stream_openai: false,
414 openai_max_context: max_4k,
415 openai_max_tokens: 300,
416 ...chatCompletionDefaultPrompts,
417 ...promptManagerDefaultPromptOrders,
418 send_if_empty: '',
419 impersonation_prompt: default_impersonation_prompt,
420 new_chat_prompt: default_new_chat_prompt,
421 new_group_chat_prompt: default_new_group_chat_prompt,
422 new_example_chat_prompt: default_new_example_chat_prompt,
423 continue_nudge_prompt: default_continue_nudge_prompt,
424 bias_preset_selected: default_bias,
425 bias_presets: default_bias_presets,
426 wi_format: default_wi_format,
427 group_nudge_prompt: default_group_nudge_prompt,
428 scenario_format: default_scenario_format,
429 personality_format: default_personality_format,
430 sort_models: 'alphabetically',
431 group_models: false,
432 openai_model: 'gpt-4-turbo',
433 claude_model: 'claude-sonnet-4-5',
434 google_model: 'gemini-2.5-pro',
435 vertexai_model: 'gemini-2.5-pro',
436 ai21_model: 'jamba-large',
437 mistralai_model: 'mistral-large-latest',
438 cohere_model: 'command-r-plus',
439 perplexity_model: 'sonar-pro',
440 groq_model: 'llama-3.3-70b-versatile',
441 chutes_model: 'deepseek-ai/DeepSeek-V3-0324',
442 siliconflow_model: 'deepseek-ai/DeepSeek-V3',
443 siliconflow_endpoint: SILICONFLOW_ENDPOINT.GLOBAL,
444 minimax_model: 'MiniMax-M2.7',
445 minimax_endpoint: MINIMAX_ENDPOINT.GLOBAL,
446 electronhub_model: 'gpt-4o-mini',
447 nanogpt_model: 'gpt-4o-mini',
448 nanogpt_provider: '',
449 nanogpt_payg_override: false,
450 deepseek_model: 'deepseek-v4-flash',
451 aimlapi_model: 'chatgpt-4o-latest',
452 xai_model: 'grok-3-beta',
453 pollinations_model: 'openai',
454 cometapi_model: 'gpt-4o',
455 moonshot_model: 'kimi-latest',
456 fireworks_model: 'accounts/fireworks/models/kimi-k2-instruct',
457 zai_model: 'glm-4.6',
458 zai_endpoint: ZAI_ENDPOINT.COMMON,
459 workers_ai_model: '@cf/meta/llama-3.3-70b-instruct-fp8-fast',
460 workers_ai_account_id: '',
461 azure_base_url: '',
462 azure_deployment_name: '',
463 azure_api_version: '2024-02-15-preview',
464 azure_openai_model: '',
465 custom_model: '',
466 custom_url: '',
467 custom_include_body: '',
468 custom_exclude_body: '',
469 custom_include_headers: '',
470 openrouter_model: openrouter_website_model,
471 openrouter_use_fallback: false,
472 openrouter_providers: [],
473 openrouter_quantizations: [],
474 openrouter_allow_fallbacks: true,
475 openrouter_middleout: openrouter_middleout_types.ON,
476 tool_reasoning_mode: tool_reasoning_modes.DISABLED,
477 reverse_proxy: '',
478 chat_completion_source: chat_completion_sources.OPENAI,
479 max_context_unlocked: false,
480 show_external_models: false,
481 proxy_password: '',
482 assistant_prefill: '',
483 assistant_impersonation: '',
484 use_sysprompt: false,
485 vertexai_auth_mode: 'express',
486 vertexai_region: 'us-central1',
487 vertexai_express_project_id: '',
488 squash_system_messages: false,
489 media_inlining: true,
490 inline_image_quality: 'auto',
491 bypass_status_check: false,
492 continue_prefill: false,
493 function_calling: false,
494 tool_call_recurse_limit: 5,
495 names_behavior: character_names_behavior.DEFAULT,
496 continue_postfix: continue_postfix_types.SPACE,
497 custom_prompt_post_processing: custom_prompt_post_processing_types.NONE,
498 show_thoughts: true,
499 reasoning_effort: reasoning_effort_types.auto,
500 verbosity: verbosity_levels.auto,
501 enable_web_search: false,
502 request_images: false,
503 request_image_aspect_ratio: '',
504 request_image_resolution: '',
505 seed: -1,
506 n: 1,
507 bind_preset_to_connection: true,
508 extensions: {},
509};
510
511const oai_settings = structuredClone(default_settings);
512
513export let proxies = [
514 {
515 name: 'None',
516 url: '',
517 password: '',
518 },
519];
520export let selected_proxy = proxies[0];
521
522export let openai_setting_names;
523export let openai_settings;
524
525/** @type {import('./PromptManager.js').PromptManager} */
526export let promptManager = null;
527
528async function validateReverseProxy() {
529 if (!oai_settings.reverse_proxy) {
530 return;
531 }
532
533 try {
534 new URL(oai_settings.reverse_proxy);
535 } catch (err) {
536 toastr.error(t`Entered reverse proxy address is not a valid URL`);
537 setOnlineStatus('no_connection');
538 resultCheckStatus();
539 throw err;
540 }
541 const rememberKey = `Proxy_SkipConfirm_${getStringHash(oai_settings.reverse_proxy)}`;
542 const skipConfirm = accountStorage.getItem(rememberKey) === 'true';
543
544 const confirmation = skipConfirm || await Popup.show.confirm(t`Connecting To Proxy`, await renderTemplateAsync('proxyConnectionWarning', { proxyURL: DOMPurify.sanitize(oai_settings.reverse_proxy) }));
545
546 if (!confirmation) {
547 toastr.error(t`Update or remove your reverse proxy settings.`);
548 setOnlineStatus('no_connection');
549 resultCheckStatus();
550 throw new Error('Proxy connection denied.');
551 }
552
553 accountStorage.setItem(rememberKey, String(true));
554}
555
556/**
557 * Formats chat messages into chat completion messages.
558 * @param {ChatMessage[]} chat - Array containing all messages.
559 * @returns {object[]} - Array containing all messages formatted for chat completion.
560 */
561function setOpenAIMessages(chat) {
562 let j = 0;
563 // clean openai msgs
564 const messages = [];
565 // Get current API and model for thought signature validation
566 const currentApi = oai_settings.chat_completion_source;
567 const currentModel = getChatCompletionModel();
568
569 for (let i = chat.length - 1; i >= 0; i--) {
570 let role = chat[j].is_user ? 'user' : 'assistant';
571 let content = chat[j].mes;
572
573 // If this symbol flag is set, completely ignore the message.
574 // This can be used to hide messages without affecting the number of messages in the chat.
575 if (chat[j].extra?.[IGNORE_SYMBOL]) {
576 j++;
577 continue;
578 }
579
580 // 100% legal way to send a message as system
581 if (chat[j].extra?.type === system_message_types.NARRATOR) {
582 role = 'system';
583 }
584
585 // for groups or sendas command - prepend a character's name
586 switch (oai_settings.names_behavior) {
587 case character_names_behavior.NONE:
588 break;
589 case character_names_behavior.DEFAULT:
590 if ((selected_group && chat[j].name !== name1) || (chat[j].force_avatar && chat[j].name !== name1 && chat[j].extra?.type !== system_message_types.NARRATOR)) {
591 content = `${chat[j].name}: ${content}`;
592 }
593 break;
594 case character_names_behavior.CONTENT:
595 if (chat[j].extra?.type !== system_message_types.NARRATOR) {
596 content = `${chat[j].name}: ${content}`;
597 }
598 break;
599 case character_names_behavior.COMPLETION:
600 break;
601 default:
602 break;
603 }
604
605 // remove caret return (waste of tokens)
606 content = content.replace(/\r/gm, '');
607
608 const name = chat[j].name;
609 const media = chat[j]?.extra?.media;
610 const mediaDisplay = getMediaDisplay(chat[j]);
611 const mediaIndex = getMediaIndex(chat[j]);
612 const invocations = chat[j]?.extra?.tool_invocations?.slice();
613
614 // Only send thought signatures if they were generated by the same API and model
615 const originApi = chat[j]?.extra?.api;
616 const originModel = chat[j]?.extra?.model;
617 const isSameModel = originApi === currentApi && originModel === currentModel;
618 // In group chats, only include reasoning from the currently generating character
619 const isOtherGroupMember = selected_group && chat[j].name !== name2;
620 const signature = isSameModel && !isOtherGroupMember ? chat[j]?.extra?.reasoning_signature : null;
621 const reasoning = isSameModel && !isOtherGroupMember ? String(chat[j]?.extra?.reasoning ?? '') : '';
622
623 // Remove reasoning metadata from invocations if the API/model don't match
624 if (Array.isArray(invocations) && invocations.length > 0) {
625 invocations.forEach((invocation, index) => {
626 if (!isSameModel && (invocation.signature || invocation.reasoning)) {
627 const cloneInvocation = structuredClone(invocation);
628 delete cloneInvocation.signature;
629 delete cloneInvocation.reasoning;
630 invocations[index] = cloneInvocation;
631 }
632 });
633 }
634
635 messages[i] = { 'role': role, 'content': content, name: name, 'media': media, 'mediaDisplay': mediaDisplay, 'mediaIndex': mediaIndex, 'invocations': invocations, 'signature': signature, 'reasoning': reasoning };
636 j++;
637 }
638
639 return messages;
640}
641
642/**
643 * Formats chat examples into chat completion messages.
644 * @param {string[]} mesExamplesArray - Array containing all examples.
645 * @returns {object[]} - Array containing all examples formatted for chat completion.
646 */
647function setOpenAIMessageExamples(mesExamplesArray) {
648 // get a nice array of all blocks of all example messages = array of arrays (important!)
649 const examples = [];
650 for (let item of mesExamplesArray) {
651 // remove <START> {Example Dialogue:} and replace \r\n with just \n
652 let replaced = item.replace(/<START>/i, '{Example Dialogue:}').replace(/\r/gm, '');
653 let parsed = parseExampleIntoIndividual(replaced, true);
654 // add to the example message blocks array
655 examples.push(parsed);
656 }
657 return examples;
658}
659
660/**
661 * One-time setup for prompt manager module.
662 *
663 * @param openAiSettings
664 * @returns {PromptManager|null}
665 */
666function setupChatCompletionPromptManager(openAiSettings) {
667 // Do not set up prompt manager more than once
668 if (promptManager) {
669 promptManager.render(false);
670 return promptManager;
671 }
672
673 promptManager = new PromptManager();
674
675 const configuration = {
676 prefix: 'completion_',
677 containerIdentifier: 'completion_prompt_manager',
678 listIdentifier: 'completion_prompt_manager_list',
679 toggleDisabled: [],
680 sortableDelay: getSortableDelay(),
681 defaultPrompts: {
682 main: default_main_prompt,
683 nsfw: default_nsfw_prompt,
684 jailbreak: default_jailbreak_prompt,
685 enhanceDefinitions: default_enhance_definitions_prompt,
686 },
687 promptOrder: {
688 strategy: 'global',
689 dummyId: 100001,
690 },
691 };
692
693 promptManager.saveServiceSettings = () => {
694 saveSettingsDebounced();
695 return new Promise((resolve) => eventSource.once(event_types.SETTINGS_UPDATED, resolve));
696 };
697
698 promptManager.tryGenerate = () => {
699 if (characters[this_chid]) {
700 return Generate('normal', {}, true);
701 } else {
702 return Promise.resolve();
703 }
704 };
705
706 promptManager.tokenHandler = tokenHandler;
707
708 promptManager.init(configuration, openAiSettings);
709 promptManager.render(false);
710
711 return promptManager;
712}
713
714/**
715 * Parses the example messages into individual messages.
716 * @param {string} messageExampleString - The string containing the example messages
717 * @param {boolean} appendNamesForGroup - Whether to append the character name for group chats
718 * @returns {Message[]} Array of message objects
719 */
720export function parseExampleIntoIndividual(messageExampleString, appendNamesForGroup = true) {
721 const groupBotNames = getGroupNames().map(name => `${name}:`);
722
723 let result = []; // array of msgs
724 let tmp = messageExampleString.split('\n');
725 let cur_msg_lines = [];
726 let in_user = false;
727 let in_bot = false;
728 let botName = name2;
729
730 // DRY my cock and balls :)
731 function add_msg(name, role, system_name) {
732 // join different newlines (we split them by \n and join by \n)
733 // remove char name
734 // strip to remove extra spaces
735 let parsed_msg = cur_msg_lines.join('\n').replace(name + ':', '').trim();
736
737 if (appendNamesForGroup && selected_group && ['example_user', 'example_assistant'].includes(system_name)) {
738 parsed_msg = `${name}: ${parsed_msg}`;
739 }
740
741 result.push({ 'role': role, 'content': parsed_msg, 'name': system_name });
742 cur_msg_lines = [];
743 }
744 // skip first line as it'll always be "This is how {bot name} should talk"
745 for (let i = 1; i < tmp.length; i++) {
746 let cur_str = tmp[i];
747 // if it's the user message, switch into user mode and out of bot mode
748 // yes, repeated code, but I don't care
749 if (cur_str.startsWith(name1 + ':')) {
750 in_user = true;
751 // we were in the bot mode previously, add the message
752 if (in_bot) {
753 add_msg(botName, 'system', 'example_assistant');
754 }
755 in_bot = false;
756 } else if (cur_str.startsWith(name2 + ':') || groupBotNames.some(n => cur_str.startsWith(n))) {
757 if (!cur_str.startsWith(name2 + ':') && groupBotNames.length) {
758 botName = cur_str.split(':')[0];
759 }
760
761 in_bot = true;
762 // we were in the user mode previously, add the message
763 if (in_user) {
764 add_msg(name1, 'system', 'example_user');
765 }
766 in_user = false;
767 }
768 // push the current line into the current message array only after checking for presence of user/bot
769 cur_msg_lines.push(cur_str);
770 }
771 // Special case for last message in a block because we don't have a new message to trigger the switch
772 if (in_user) {
773 add_msg(name1, 'system', 'example_user');
774 } else if (in_bot) {
775 add_msg(botName, 'system', 'example_assistant');
776 }
777 return result;
778}
779
780export function formatWorldInfo(value, { wiFormat = null } = {}) {
781 if (!value) {
782 return '';
783 }
784
785 const format = wiFormat ?? oai_settings.wi_format;
786
787 if (!format.trim()) {
788 return value;
789 }
790
791 return stringFormat(format, value);
792}
793
794/**
795 * This function populates the injections in the conversation.
796 *
797 * @param {Prompt[]} prompts - Array containing injection prompts.
798 * @param {Object[]} messages - Array containing all messages.
799 * @returns {Promise<Object[]>} - Array containing all messages with injections.
800 */
801async function populationInjectionPrompts(prompts, messages) {
802 let totalInsertedMessages = 0;
803
804 const roleTypes = {
805 'system': extension_prompt_roles.SYSTEM,
806 'user': extension_prompt_roles.USER,
807 'assistant': extension_prompt_roles.ASSISTANT,
808 };
809
810 const maxDepth = getExtensionPromptMaxDepth();
811 for (let i = 0; i <= maxDepth; i++) {
812 // Get prompts for current depth
813 const depthPrompts = prompts.filter(prompt => prompt.injection_depth === i && prompt.content);
814
815 const roleMessages = [];
816 const separator = '\n';
817 const wrap = false;
818
819 // Group prompts by priority
820 const extensionPromptsOrder = '100';
821 const orderGroups = {
822 [extensionPromptsOrder]: [],
823 };
824 for (const prompt of depthPrompts) {
825 const order = prompt.injection_order ?? 100;
826 if (!orderGroups[order]) {
827 orderGroups[order] = [];
828 }
829 orderGroups[order].push(prompt);
830 }
831
832 // Process each order group in order (b - a = low to high ; a - b = high to low)
833 const orders = Object.keys(orderGroups).sort((a, b) => +b - +a);
834 for (const order of orders) {
835 const orderPrompts = orderGroups[order];
836
837 // Order of priority for roles (most important go lower)
838 const roles = ['system', 'user', 'assistant'];
839 for (const role of roles) {
840 const rolePrompts = orderPrompts
841 .filter(prompt => prompt.role === role)
842 .map(x => x.content)
843 .join(separator);
844
845 // Get extension prompt
846 const extensionPrompt = order === extensionPromptsOrder
847 ? await getExtensionPrompt(extension_prompt_types.IN_CHAT, i, separator, roleTypes[role], wrap)
848 : '';
849 const jointPrompt = [rolePrompts, extensionPrompt].filter(x => x).map(x => x.trim()).join(separator);
850
851 if (jointPrompt && jointPrompt.length) {
852 roleMessages.push({ 'role': role, 'content': jointPrompt, injected: true });
853 }
854 }
855 }
856
857 if (roleMessages.length) {
858 const injectIdx = i + totalInsertedMessages;
859 messages.splice(injectIdx, 0, ...roleMessages);
860 totalInsertedMessages += roleMessages.length;
861 }
862 }
863
864 messages = messages.reverse();
865 return messages;
866}
867
868/**
869 * Populates the chat history of the conversation.
870 * @param {object[]} messages - Array containing all messages.
871 * @param {import('./PromptManager').PromptCollection} prompts - Map object containing all prompts where the key is the prompt identifier and the value is the prompt object.
872 * @param {ChatCompletion} chatCompletion - An instance of ChatCompletion class that will be populated with the prompts.
873 * @param type
874 * @param cyclePrompt
875 */
876async function populateChatHistory(messages, prompts, chatCompletion, type = null, cyclePrompt = null) {
877 if (!prompts.has('chatHistory')) {
878 return;
879 }
880
881 chatCompletion.add(new MessageCollection('chatHistory'), prompts.index('chatHistory'));
882
883 // Reserve budget for new chat message
884 const newChat = selected_group ? oai_settings.new_group_chat_prompt : oai_settings.new_chat_prompt;
885 const newChatMessage = await Message.createAsync('system', substituteParams(newChat), 'newMainChat');
886 chatCompletion.reserveBudget(newChatMessage);
887
888 // Reserve budget for group nudge
889 let groupNudgeMessage = null;
890 const noGroupNudgeTypes = ['impersonate'];
891 if (selected_group && prompts.has('groupNudge') && !noGroupNudgeTypes.includes(type)) {
892 groupNudgeMessage = await Message.fromPromptAsync(prompts.get('groupNudge'));
893 chatCompletion.reserveBudget(groupNudgeMessage);
894 }
895
896 // Reserve budget for continue nudge
897 let continueMessageCollection = null;
898 if (type === 'continue' && cyclePrompt && !oai_settings.continue_prefill) {
899 const promptObject = {
900 identifier: 'continueNudge',
901 role: 'system',
902 content: substituteParamsExtended(oai_settings.continue_nudge_prompt, { lastChatMessage: String(cyclePrompt).trim() }),
903 system_prompt: true,
904 };
905 continueMessageCollection = new MessageCollection('continueNudge');
906 const continueMessageIndex = messages.findLastIndex(x => !x.injected);
907 if (continueMessageIndex >= 0) {
908 const continueMessage = messages.splice(continueMessageIndex, 1)[0];
909 const prompt = new Prompt(continueMessage);
910 const chatMessage = await Message.fromPromptAsync(promptManager.preparePrompt(prompt));
911 continueMessageCollection.add(chatMessage);
912 }
913 const continueNudgePrompt = new Prompt(promptObject);
914 const preparedNudgePrompt = promptManager.preparePrompt(continueNudgePrompt);
915 const continueNudgeMessage = await Message.fromPromptAsync(preparedNudgePrompt);
916 continueMessageCollection.add(continueNudgeMessage);
917 chatCompletion.reserveBudget(continueMessageCollection);
918 }
919
920 const lastChatPrompt = messages[messages.length - 1];
921 const message = await Message.createAsync('user', oai_settings.send_if_empty, 'emptyUserMessageReplacement');
922 if (lastChatPrompt && lastChatPrompt.role === 'assistant' && oai_settings.send_if_empty && chatCompletion.canAfford(message)) {
923 chatCompletion.insert(message, 'chatHistory');
924 }
925
926 const imageInlining = isImageInliningSupported();
927 const videoInlining = isVideoInliningSupported();
928 const audioInlining = isAudioInliningSupported();
929 const canUseTools = ToolManager.isToolCallingSupported();
930 const includeSignature = isReasoningSignatureSupported();
931 const isToolReasoningProvider = interleaved_reasoning_providers.includes(oai_settings.chat_completion_source);
932 const toolReasoningMode = isToolReasoningProvider
933 ? getEffectiveToolReasoningMode()
934 : tool_reasoning_modes.DISABLED;
935 const includeToolReasoning = toolReasoningMode !== tool_reasoning_modes.DISABLED;
936 const lastUserIdx = messages.findLastIndex(x => x.role === 'user');
937
938 // Insert chat messages as long as there is budget available
939 const chatPool = [...messages].reverse();
940 for (let index = 0; index < chatPool.length; index++) {
941 const chatPrompt = chatPool[index];
942
943 // We do not want to mutate the prompt
944 const prompt = new Prompt(chatPrompt);
945 prompt.identifier = `chatHistory-${messages.length - index}`;
946 const chatMessage = await Message.fromPromptAsync(promptManager.preparePrompt(prompt));
947
948 if (promptManager.serviceSettings.names_behavior === character_names_behavior.COMPLETION && prompt.name) {
949 const messageName = promptManager.isValidName(prompt.name) ? prompt.name : promptManager.sanitizeName(prompt.name);
950 await chatMessage.setName(messageName);
951 }
952
953 /**
954 * Inline a media attachment into the chat message.
955 * @param {MediaAttachment} media - The media attachment to inline.
956 */
957 async function inlineMediaAttachment(media) {
958 if (!media || !media.url) {
959 return;
960 }
961 if (!media.type) {
962 media.type = MEDIA_TYPE.IMAGE;
963 }
964 if (imageInlining && media.type === MEDIA_TYPE.IMAGE) {
965 await chatMessage.addImage(media.url);
966 }
967 if (videoInlining && media.type === MEDIA_TYPE.VIDEO) {
968 await chatMessage.addVideo(media.url);
969 }
970 if (audioInlining && media.type === MEDIA_TYPE.AUDIO) {
971 await chatMessage.addAudio(media.url);
972 }
973 }
974
975 if (Array.isArray(chatPrompt.media) && chatPrompt.media.length) {
976 if (chatPrompt.mediaDisplay === MEDIA_DISPLAY.LIST) {
977 for (const media of chatPrompt.media) {
978 await inlineMediaAttachment(media);
979 }
980 }
981 if (chatPrompt.mediaDisplay === MEDIA_DISPLAY.GALLERY) {
982 const media = chatPrompt.media[chatPrompt.mediaIndex];
983 await inlineMediaAttachment(media);
984 }
985 }
986
987 if (canUseTools && Array.isArray(chatPrompt.invocations)) {
988 const promptIdx = messages.indexOf(chatPrompt);
989 const reasoningIsEligible = toolReasoningMode !== tool_reasoning_modes.DISABLED
990 && promptIdx > lastUserIdx;
991 let previousAssistantReasoning = '';
992 if (reasoningIsEligible) {
993 if (toolReasoningMode === tool_reasoning_modes.ACTIVE_CHAIN) {
994 // Strict chain mode: skip tool/tool-call messages, then use only the first assistant text boundary.
995 for (let idx = promptIdx - 1; idx > lastUserIdx; idx--) {
996 const candidate = messages[idx];
997 if (candidate?.role === 'tool') {
998 continue;
999 }
1000 if (candidate?.role === 'assistant' && Array.isArray(candidate.invocations)) {
1001 continue;
1002 }
1003 const hasAssistantText = candidate?.role === 'assistant'
1004 && !Array.isArray(candidate.invocations)
1005 && typeof candidate.content === 'string'
1006 && candidate.content.trim().length > 0;
1007 if (hasAssistantText) {
1008 previousAssistantReasoning = String(candidate.reasoning ?? '');
1009 }
1010 break;
1011 }
1012 } else if (toolReasoningMode === tool_reasoning_modes.SINCE_LAST_USER) {
1013 // Broad mode: use the latest assistant text reasoning anywhere since the last user.
1014 for (let idx = promptIdx - 1; idx > lastUserIdx; idx--) {
1015 const candidate = messages[idx];
1016 const hasAssistantText = candidate?.role === 'assistant'
1017 && !Array.isArray(candidate.invocations)
1018 && typeof candidate.content === 'string'
1019 && candidate.content.trim().length > 0;
1020 if (!hasAssistantText) {
1021 continue;
1022 }
1023 const candidateReasoning = String(candidate.reasoning ?? '');
1024 if (candidateReasoning) {
1025 previousAssistantReasoning = candidateReasoning;
1026 break;
1027 }
1028 }
1029 }
1030 }
1031 /** @type {import('./tool-calling.js').ToolInvocation[]} */
1032 const invocations = chatPrompt.invocations.map(invocation => {
1033 const clone = structuredClone(invocation);
1034 if (!reasoningIsEligible) {
1035 delete clone.reasoning;
1036 } else if (previousAssistantReasoning && !clone.reasoning) {
1037 // Fall back to adjacent assistant-text reasoning only when the invocation has none of its own.
1038 clone.reasoning = previousAssistantReasoning;
1039 }
1040 return clone;
1041 });
1042 const toolCallMessage = await Message.createAsync(chatMessage.role, undefined, 'toolCall-' + chatMessage.identifier);
1043 const toolResultMessages = await Promise.all(invocations.slice().reverse().map((invocation) => Message.createAsync('tool', invocation.result || '[No content]', invocation.id)));
1044 await toolCallMessage.setToolCalls(invocations, includeSignature, includeToolReasoning);
1045 if (chatCompletion.canAffordAll([toolCallMessage, ...toolResultMessages])) {
1046 for (const resultMessage of toolResultMessages) {
1047 chatCompletion.insertAtStart(resultMessage, 'chatHistory');
1048 }
1049 chatCompletion.insertAtStart(toolCallMessage, 'chatHistory');
1050 } else {
1051 break;
1052 }
1053
1054 continue;
1055 }
1056
1057 if (includeSignature && chatPrompt.signature) {
1058 chatMessage.signature = chatPrompt.signature;
1059 }
1060
1061 if (chatCompletion.canAfford(chatMessage)) {
1062 chatCompletion.insertAtStart(chatMessage, 'chatHistory');
1063 } else {
1064 break;
1065 }
1066 }
1067
1068 // Insert and free new chat
1069 chatCompletion.freeBudget(newChatMessage);
1070 chatCompletion.insertAtStart(newChatMessage, 'chatHistory');
1071
1072 // Reserve budget for group nudge
1073 if (selected_group && groupNudgeMessage) {
1074 chatCompletion.freeBudget(groupNudgeMessage);
1075 chatCompletion.insertAtEnd(groupNudgeMessage, 'chatHistory');
1076 }
1077
1078 // Insert and free continue nudge
1079 if (type === 'continue' && continueMessageCollection) {
1080 chatCompletion.freeBudget(continueMessageCollection);
1081 chatCompletion.add(continueMessageCollection, -1);
1082 }
1083}
1084
1085/**
1086 * This function populates the dialogue examples in the conversation.
1087 *
1088 * @param {import('./PromptManager').PromptCollection} prompts - Map object containing all prompts where the key is the prompt identifier and the value is the prompt object.
1089 * @param {ChatCompletion} chatCompletion - An instance of ChatCompletion class that will be populated with the prompts.
1090 * @param {Object[]} messageExamples - Array containing all message examples.
1091 */
1092async function populateDialogueExamples(prompts, chatCompletion, messageExamples) {
1093 if (!prompts.has('dialogueExamples')) {
1094 return;
1095 }
1096
1097 chatCompletion.add(new MessageCollection('dialogueExamples'), prompts.index('dialogueExamples'));
1098 if (Array.isArray(messageExamples) && messageExamples.length) {
1099 const newExampleChat = await Message.createAsync('system', substituteParams(oai_settings.new_example_chat_prompt), 'newChat');
1100 for (const dialogue of [...messageExamples]) {
1101 const dialogueIndex = messageExamples.indexOf(dialogue);
1102 const chatMessages = [];
1103
1104 for (let promptIndex = 0; promptIndex < dialogue.length; promptIndex++) {
1105 const prompt = dialogue[promptIndex];
1106 const role = 'system';
1107 const content = prompt.content || '';
1108 const identifier = `dialogueExamples ${dialogueIndex}-${promptIndex}`;
1109
1110 const chatMessage = await Message.createAsync(role, content, identifier);
1111 await chatMessage.setName(prompt.name);
1112 chatMessages.push(chatMessage);
1113 }
1114
1115 if (!chatCompletion.canAffordAll([newExampleChat, ...chatMessages])) {
1116 break;
1117 }
1118
1119 chatCompletion.insert(newExampleChat, 'dialogueExamples');
1120 for (const chatMessage of chatMessages) {
1121 chatCompletion.insert(chatMessage, 'dialogueExamples');
1122 }
1123 }
1124 }
1125}
1126
1127/**
1128 * @param {number} position - Prompt position in the extensions object.
1129 * @returns {string|false} - The prompt position for prompt collection.
1130 */
1131export function getPromptPosition(position) {
1132 if (position == extension_prompt_types.BEFORE_PROMPT) {
1133 return 'start';
1134 }
1135
1136 if (position == extension_prompt_types.IN_PROMPT) {
1137 return 'end';
1138 }
1139
1140 return false;
1141}
1142
1143/**
1144 * Gets a Chat Completion role based on the prompt role.
1145 * @param {number} role Role of the prompt.
1146 * @returns {string} Mapped role.
1147 */
1148export function getPromptRole(role) {
1149 switch (role) {
1150 case extension_prompt_roles.SYSTEM:
1151 return 'system';
1152 case extension_prompt_roles.USER:
1153 return 'user';
1154 case extension_prompt_roles.ASSISTANT:
1155 return 'assistant';
1156 default:
1157 return 'system';
1158 }
1159}
1160
1161/**
1162 * Populate a chat conversation by adding prompts to the conversation and managing system and user prompts.
1163 *
1164 * @param {import('./PromptManager.js').PromptCollection} prompts - PromptCollection containing all prompts where the key is the prompt identifier and the value is the prompt object.
1165 * @param {ChatCompletion} chatCompletion - An instance of ChatCompletion class that will be populated with the prompts.
1166 * @param {Object} options - An object with optional settings.
1167 * @param {string} options.bias - A bias to be added in the conversation.
1168 * @param {string} options.quietPrompt - Instruction prompt for extras
1169 * @param {string} options.quietImage - Image prompt for extras
1170 * @param {string} options.type - The type of the chat, can be 'impersonate'.
1171 * @param {string} options.cyclePrompt - The last prompt in the conversation.
1172 * @param {object[]} options.messages - Array containing all messages.
1173 * @param {object[]} options.messageExamples - Array containing all message examples.
1174 * @returns {Promise<void>}
1175 */
1176async function populateChatCompletion(prompts, chatCompletion, { bias, quietPrompt, quietImage, type, cyclePrompt, messages, messageExamples }) {
1177 // Helper function for preparing a prompt, that already exists within the prompt collection, for completion
1178 const addToChatCompletion = async (source, target = null) => {
1179 // We need the prompts array to determine a position for the source.
1180 if (false === prompts.has(source)) return;
1181
1182 if (promptManager.isPromptDisabledForActiveCharacter(source) && source !== 'main') {
1183 promptManager.log(`Skipping prompt ${source} because it is disabled`);
1184 return;
1185 }
1186
1187 const prompt = prompts.get(source);
1188
1189 if (prompt.injection_position === INJECTION_POSITION.ABSOLUTE) {
1190 promptManager.log(`Skipping prompt ${source} because it is an absolute prompt`);
1191 return;
1192 }
1193
1194 const index = target ? prompts.index(target) : prompts.index(source);
1195 const collection = new MessageCollection(source);
1196 const message = await Message.fromPromptAsync(prompt);
1197 collection.add(message);
1198 chatCompletion.add(collection, index);
1199 };
1200
1201 chatCompletion.reserveBudget(3); // every reply is primed with <|start|>assistant<|message|>
1202 // Character and world information
1203 await addToChatCompletion('worldInfoBefore');
1204 await addToChatCompletion('main');
1205 await addToChatCompletion('worldInfoAfter');
1206 await addToChatCompletion('charDescription');
1207 await addToChatCompletion('charPersonality');
1208 await addToChatCompletion('scenario');
1209 await addToChatCompletion('personaDescription');
1210
1211 // Collection of control prompts that will always be positioned last
1212 chatCompletion.setOverriddenPrompts(prompts.overriddenPrompts);
1213 const controlPrompts = new MessageCollection('controlPrompts');
1214
1215 const impersonateMessage = await Message.fromPromptAsync(prompts.get('impersonate')) ?? null;
1216 if (type === 'impersonate') controlPrompts.add(impersonateMessage);
1217
1218 // Add quiet prompt to control prompts
1219 // This should always be last, even in control prompts. Add all further control prompts BEFORE this prompt
1220 const quietPromptMessage = await Message.fromPromptAsync(prompts.get('quietPrompt')) ?? null;
1221 if (quietPromptMessage && quietPromptMessage.content) {
1222 if (isImageInliningSupported() && quietImage) {
1223 await quietPromptMessage.addImage(quietImage);
1224 }
1225
1226 controlPrompts.add(quietPromptMessage);
1227 }
1228
1229 chatCompletion.reserveBudget(controlPrompts);
1230
1231 // Add ordered system and user prompts
1232 const systemPrompts = ['nsfw', 'jailbreak'];
1233 const userRelativePrompts = prompts.collection
1234 .filter((prompt) => false === prompt.system_prompt && prompt.injection_position !== INJECTION_POSITION.ABSOLUTE)
1235 .reduce((acc, prompt) => {
1236 acc.push(prompt.identifier);
1237 return acc;
1238 }, []);
1239 const absolutePrompts = prompts.collection
1240 .filter((prompt) => prompt.injection_position === INJECTION_POSITION.ABSOLUTE)
1241 .reduce((acc, prompt) => {
1242 acc.push(prompt);
1243 return acc;
1244 }, []);
1245
1246 for (const identifier of [...systemPrompts, ...userRelativePrompts]) {
1247 await addToChatCompletion(identifier);
1248 }
1249
1250 // Add enhance definition instruction
1251 if (prompts.has('enhanceDefinitions')) await addToChatCompletion('enhanceDefinitions');
1252
1253 // Bias
1254 if (bias && bias.trim().length) await addToChatCompletion('bias');
1255
1256 const injectToMain = async (/** @type {Prompt} */ prompt, /** @type {string|number} */ position) => {
1257 if (chatCompletion.has('main')) {
1258 const message = await Message.fromPromptAsync(prompt);
1259 chatCompletion.insert(message, 'main', position);
1260 } else {
1261 // Convert the relative prompt to an injection and place it relative to main prompt
1262 // Keeping prompts in the same order bucket will squash them together during in-chat injection
1263 const indexOfMain = absolutePrompts.findIndex(p => p.identifier === 'main');
1264 if (indexOfMain >= 0) {
1265 const main = absolutePrompts[indexOfMain];
1266 const promptCopy = new Prompt(prompt);
1267 promptCopy.role = main.role;
1268 promptCopy.injection_position = main.injection_position;
1269 promptCopy.injection_depth = main.injection_depth;
1270 promptCopy.injection_order = main.injection_order;
1271 const newIndex = position === 'end' ? indexOfMain + 1 : indexOfMain;
1272 absolutePrompts.splice(newIndex, 0, promptCopy);
1273 }
1274 }
1275 };
1276
1277 const knownPrompts = [
1278 'summary',
1279 'authorsNote',
1280 'vectorsMemory',
1281 'vectorsDataBank',
1282 'smartContext',
1283 ];
1284
1285 // Known relative extension prompts
1286 for (const key of knownPrompts) {
1287 if (prompts.has(key)) {
1288 const prompt = prompts.get(key);
1289 if (prompt.position) {
1290 await injectToMain(prompt, prompt.position);
1291 }
1292 }
1293 }
1294
1295 // Other relative extension prompts
1296 for (const prompt of prompts.collection.filter(p => p.extension && p.position)) {
1297 await injectToMain(prompt, prompt.position);
1298 }
1299
1300 // Pre-allocation of tokens for tool data
1301 if (ToolManager.canPerformToolCalls(type)) {
1302 const toolData = {};
1303 await ToolManager.registerFunctionToolsOpenAI(toolData);
1304 const toolMessage = [{ role: 'user', content: JSON.stringify(toolData) }];
1305 const toolTokens = await tokenHandler.countAsync(toolMessage);
1306 chatCompletion.reserveBudget(toolTokens);
1307 }
1308
1309 // Displace the message to be continued from its original position before performing in-chat injections
1310 // In case if it is an assistant message, we want to prepend the users assistant prefill on the message
1311 if (type === 'continue' && oai_settings.continue_prefill && messages.length) {
1312 const chatMessage = messages.shift();
1313 const isAssistantRole = chatMessage.role === 'assistant';
1314 const supportsAssistantPrefill = oai_settings.chat_completion_source === chat_completion_sources.CLAUDE;
1315 const namesInCompletion = oai_settings.names_behavior === character_names_behavior.COMPLETION;
1316 const assistantPrefill = isAssistantRole && supportsAssistantPrefill ? substituteParams(oai_settings.assistant_prefill) : '';
1317 const messageContent = [assistantPrefill, chatMessage.content].filter(x => x).join('\n\n');
1318 const continueMessage = await Message.createAsync(chatMessage.role, messageContent, 'continuePrefill');
1319 chatMessage.name && namesInCompletion && await continueMessage.setName(promptManager.sanitizeName(chatMessage.name));
1320 controlPrompts.add(continueMessage);
1321 chatCompletion.reserveBudget(continueMessage);
1322 }
1323
1324 // Add in-chat injections
1325 messages = await populationInjectionPrompts(absolutePrompts, messages);
1326
1327 // Decide whether dialogue examples should always be added
1328 if (power_user.pin_examples) {
1329 await populateDialogueExamples(prompts, chatCompletion, messageExamples);
1330 await populateChatHistory(messages, prompts, chatCompletion, type, cyclePrompt);
1331 } else {
1332 await populateChatHistory(messages, prompts, chatCompletion, type, cyclePrompt);
1333 await populateDialogueExamples(prompts, chatCompletion, messageExamples);
1334 }
1335
1336 chatCompletion.freeBudget(controlPrompts);
1337 if (controlPrompts.collection.length) chatCompletion.add(controlPrompts);
1338}
1339
1340/**
1341 * Combines system prompts with prompt manager prompts
1342 *
1343 * @param {Object} options - An object with optional settings.
1344 * @param {string} options.scenario - The scenario or context of the dialogue.
1345 * @param {string} options.charPersonality - Description of the character's personality.
1346 * @param {string} options.name2 - The second name to be used in the messages.
1347 * @param {string} options.worldInfoBefore - The world info to be added before the main conversation.
1348 * @param {string} options.worldInfoAfter - The world info to be added after the main conversation.
1349 * @param {string} options.charDescription - Description of the character.
1350 * @param {string} options.quietPrompt - The quiet prompt to be used in the conversation.
1351 * @param {string} options.bias - The bias to be added in the conversation.
1352 * @param {Object} options.extensionPrompts - An object containing additional prompts.
1353 * @param {string} options.systemPromptOverride - Character card override of the main prompt
1354 * @param {string} options.jailbreakPromptOverride - Character card override of the PHI
1355 * @param {string} options.type - The type of generation that triggered the prompt
1356 * @returns {Promise<Object>} prompts - The prepared and merged system and user-defined prompts.
1357 */
1358async function preparePromptsForChatCompletion({ scenario, charPersonality, name2, worldInfoBefore, worldInfoAfter, charDescription, quietPrompt, bias, extensionPrompts, systemPromptOverride, jailbreakPromptOverride, type }) {
1359 const scenarioText = scenario && oai_settings.scenario_format ? substituteParams(oai_settings.scenario_format) : (scenario || '');
1360 const charPersonalityText = charPersonality && oai_settings.personality_format ? substituteParams(oai_settings.personality_format) : (charPersonality || '');
1361 const groupNudge = substituteParams(oai_settings.group_nudge_prompt);
1362 const impersonationPrompt = oai_settings.impersonation_prompt ? substituteParams(oai_settings.impersonation_prompt) : '';
1363
1364 // Create entries for system prompts
1365 const systemPrompts = [
1366 // Ordered prompts for which a marker should exist
1367 { role: 'system', content: formatWorldInfo(worldInfoBefore), identifier: 'worldInfoBefore' },
1368 { role: 'system', content: formatWorldInfo(worldInfoAfter), identifier: 'worldInfoAfter' },
1369 { role: 'system', content: charDescription, identifier: 'charDescription' },
1370 { role: 'system', content: charPersonalityText, identifier: 'charPersonality' },
1371 { role: 'system', content: scenarioText, identifier: 'scenario' },
1372 // Unordered prompts without marker
1373 { role: 'system', content: impersonationPrompt, identifier: 'impersonate' },
1374 { role: 'system', content: quietPrompt, identifier: 'quietPrompt' },
1375 { role: 'system', content: groupNudge, identifier: 'groupNudge' },
1376 { role: 'assistant', content: bias, identifier: 'bias' },
1377 ];
1378
1379 // Tavern Extras - Summary
1380 const summary = extensionPrompts['1_memory'];
1381 if (summary && summary.value) systemPrompts.push({
1382 role: getPromptRole(summary.role),
1383 content: summary.value,
1384 identifier: 'summary',
1385 position: getPromptPosition(summary.position),
1386 });
1387
1388 // Authors Note
1389 const authorsNote = extensionPrompts['2_floating_prompt'];
1390 if (authorsNote && authorsNote.value) systemPrompts.push({
1391 role: getPromptRole(authorsNote.role),
1392 content: authorsNote.value,
1393 identifier: 'authorsNote',
1394 position: getPromptPosition(authorsNote.position),
1395 });
1396
1397 // Vectors Memory
1398 const vectorsMemory = extensionPrompts['3_vectors'];
1399 if (vectorsMemory && vectorsMemory.value) systemPrompts.push({
1400 role: 'system',
1401 content: vectorsMemory.value,
1402 identifier: 'vectorsMemory',
1403 position: getPromptPosition(vectorsMemory.position),
1404 });
1405
1406 const vectorsDataBank = extensionPrompts['4_vectors_data_bank'];
1407 if (vectorsDataBank && vectorsDataBank.value) systemPrompts.push({
1408 role: getPromptRole(vectorsDataBank.role),
1409 content: vectorsDataBank.value,
1410 identifier: 'vectorsDataBank',
1411 position: getPromptPosition(vectorsDataBank.position),
1412 });
1413
1414 // Smart Context (ChromaDB)
1415 const smartContext = extensionPrompts.chromadb;
1416 if (smartContext && smartContext.value) systemPrompts.push({
1417 role: 'system',
1418 content: smartContext.value,
1419 identifier: 'smartContext',
1420 position: getPromptPosition(smartContext.position),
1421 });
1422
1423 // Persona Description
1424 if (power_user.persona_description && power_user.persona_description_position === persona_description_positions.IN_PROMPT) {
1425 systemPrompts.push({ role: 'system', content: power_user.persona_description, identifier: 'personaDescription' });
1426 }
1427
1428 const knownExtensionPrompts = [
1429 '1_memory',
1430 '2_floating_prompt',
1431 '3_vectors',
1432 '4_vectors_data_bank',
1433 'chromadb',
1434 'PERSONA_DESCRIPTION',
1435 'QUIET_PROMPT',
1436 'DEPTH_PROMPT',
1437 ];
1438
1439 // Anything that is not a known extension prompt
1440 for (const key in extensionPrompts) {
1441 if (Object.hasOwn(extensionPrompts, key)) {
1442 const prompt = extensionPrompts[key];
1443 if (knownExtensionPrompts.includes(key)) continue;
1444 if (!extensionPrompts[key].value) continue;
1445 if (![extension_prompt_types.BEFORE_PROMPT, extension_prompt_types.IN_PROMPT].includes(prompt.position)) continue;
1446
1447 const hasFilter = typeof prompt.filter === 'function';
1448 if (hasFilter && !await prompt.filter()) continue;
1449
1450 systemPrompts.push({
1451 identifier: key.replace(/\W/g, '_'),
1452 position: getPromptPosition(prompt.position),
1453 role: getPromptRole(prompt.role),
1454 content: prompt.value,
1455 extension: true,
1456 });
1457 }
1458 }
1459
1460 // This is the prompt order defined by the user
1461 const prompts = promptManager.getPromptCollection(type);
1462
1463 // Merge system prompts with prompt manager prompts
1464 systemPrompts.forEach(prompt => {
1465 const collectionPrompt = prompts.get(prompt.identifier);
1466
1467 // Apply system prompt role/depth overrides if they set in the prompt manager
1468 if (collectionPrompt) {
1469 // In-Chat / Relative
1470 prompt.injection_position = collectionPrompt.injection_position ?? prompt.injection_position;
1471 // Depth for In-Chat
1472 prompt.injection_depth = collectionPrompt.injection_depth ?? prompt.injection_depth;
1473 // Priority for In-Chat
1474 prompt.injection_order = collectionPrompt.injection_order ?? prompt.injection_order;
1475 // Role (system, user, assistant)
1476 prompt.role = collectionPrompt.role ?? prompt.role;
1477 }
1478
1479 const newPrompt = promptManager.preparePrompt(prompt);
1480 const markerIndex = prompts.index(prompt.identifier);
1481
1482 if (-1 !== markerIndex) prompts.collection[markerIndex] = newPrompt;
1483 else prompts.add(newPrompt);
1484 });
1485
1486 // Apply character-specific main prompt
1487 const systemPrompt = prompts.get('main') ?? null;
1488 const isSystemPromptDisabled = promptManager.isPromptDisabledForActiveCharacter('main');
1489 if (systemPromptOverride && systemPrompt && systemPrompt.forbid_overrides !== true && !isSystemPromptDisabled) {
1490 const mainOriginalContent = systemPrompt.content;
1491 systemPrompt.content = systemPromptOverride;
1492 const mainReplacement = promptManager.preparePrompt(systemPrompt, mainOriginalContent);
1493 prompts.override(mainReplacement, prompts.index('main'));
1494 }
1495
1496 // Apply character-specific jailbreak
1497 const jailbreakPrompt = prompts.get('jailbreak') ?? null;
1498 const isJailbreakPromptDisabled = promptManager.isPromptDisabledForActiveCharacter('jailbreak');
1499 if (jailbreakPromptOverride && jailbreakPrompt && jailbreakPrompt.forbid_overrides !== true && !isJailbreakPromptDisabled) {
1500 const jbOriginalContent = jailbreakPrompt.content;
1501 jailbreakPrompt.content = jailbreakPromptOverride;
1502 const jbReplacement = promptManager.preparePrompt(jailbreakPrompt, jbOriginalContent);
1503 prompts.override(jbReplacement, prompts.index('jailbreak'));
1504 }
1505
1506 return prompts;
1507}
1508
1509/**
1510 * Take a configuration object and prepares messages for a chat with OpenAI's chat completion API.
1511 * Handles prompts, prepares chat history, manages token budget, and processes various user settings.
1512 *
1513 * @param {Object} content - System prompts provided by SillyTavern
1514 * @param {string} content.name2 - The second name to be used in the messages.
1515 * @param {string} content.charDescription - Description of the character.
1516 * @param {string} content.charPersonality - Description of the character's personality.
1517 * @param {string} content.scenario - The scenario or context of the dialogue.
1518 * @param {string} content.worldInfoBefore - The world info to be added before the main conversation.
1519 * @param {string} content.worldInfoAfter - The world info to be added after the main conversation.
1520 * @param {string} content.bias - The bias to be added in the conversation.
1521 * @param {string} content.type - The type of the chat, can be 'impersonate'.
1522 * @param {string} content.quietPrompt - The quiet prompt to be used in the conversation.
1523 * @param {string} content.quietImage - Image prompt for extras
1524 * @param {string} content.cyclePrompt - The last prompt used for chat message continuation.
1525 * @param {string} content.systemPromptOverride - The system prompt override.
1526 * @param {string} content.jailbreakPromptOverride - The jailbreak prompt override.
1527 * @param {object} content.extensionPrompts - An array of additional prompts.
1528 * @param {object[]} content.messages - An array of messages to be used as chat history.
1529 * @param {string[]} content.messageExamples - An array of messages to be used as dialogue examples.
1530 * @param dryRun - Whether this is a live call or not.
1531 * @returns {Promise<(any[]|boolean)[]>} An array where the first element is the prepared chat and the second element is a boolean flag.
1532 */
1533export async function prepareOpenAIMessages({
1534 name2,
1535 charDescription,
1536 charPersonality,
1537 scenario,
1538 worldInfoBefore,
1539 worldInfoAfter,
1540 bias,
1541 type,
1542 quietPrompt,
1543 quietImage,
1544 extensionPrompts,
1545 cyclePrompt,
1546 systemPromptOverride,
1547 jailbreakPromptOverride,
1548 messages,
1549 messageExamples,
1550}, dryRun) {
1551 // Without a character selected, there is no way to accurately calculate tokens
1552 if (!promptManager.activeCharacter && dryRun) return [null, false];
1553
1554 const chatCompletion = new ChatCompletion();
1555 if (power_user.console_log_prompts) chatCompletion.enableLogging();
1556
1557 const userSettings = promptManager.serviceSettings;
1558 chatCompletion.setTokenBudget(userSettings.openai_max_context, userSettings.openai_max_tokens);
1559
1560 try {
1561 // Merge markers and ordered user prompts with system prompts
1562 const prompts = await preparePromptsForChatCompletion({
1563 scenario,
1564 charPersonality,
1565 name2,
1566 worldInfoBefore,
1567 worldInfoAfter,
1568 charDescription,
1569 quietPrompt,
1570 bias,
1571 extensionPrompts,
1572 systemPromptOverride,
1573 jailbreakPromptOverride,
1574 type,
1575 });
1576
1577 // Fill the chat completion with as much context as the budget allows
1578 await populateChatCompletion(prompts, chatCompletion, { bias, quietPrompt, quietImage, type, cyclePrompt, messages, messageExamples });
1579 } catch (error) {
1580 if (error instanceof TokenBudgetExceededError) {
1581 toastr.error(t`Mandatory prompts exceed the context size.`);
1582 chatCompletion.log('Mandatory prompts exceed the context size.');
1583 promptManager.error = t`Not enough free tokens for mandatory prompts. Raise your token limit or disable custom prompts.`;
1584 } else if (error instanceof InvalidCharacterNameError) {
1585 toastr.warning(t`An error occurred while counting tokens: Invalid character name`);
1586 chatCompletion.log('Invalid character name');
1587 promptManager.error = t`The name of at least one character contained whitespaces or special characters. Please check your user and character name.`;
1588 } else {
1589 toastr.error(t`An unknown error occurred while counting tokens. Further information may be available in console.`);
1590 chatCompletion.log('----- Unexpected error while preparing prompts -----');
1591 chatCompletion.log(error);
1592 chatCompletion.log(error.stack);
1593 chatCompletion.log('----------------------------------------------------');
1594 }
1595 } finally {
1596 // Pass chat completion to prompt manager for inspection
1597 promptManager.setChatCompletion(chatCompletion);
1598
1599 if (oai_settings.squash_system_messages && dryRun == false) {
1600 await chatCompletion.squashSystemMessages();
1601 }
1602
1603 // All information is up-to-date, render.
1604 if (false === dryRun) promptManager.render(false);
1605 }
1606
1607 const chat = chatCompletion.getChat();
1608
1609 const eventData = { chat, dryRun };
1610 await eventSource.emit(event_types.CHAT_COMPLETION_PROMPT_READY, eventData);
1611
1612 openai_messages_count = chat.filter(x => !x?.tool_calls && ['user', 'assistant', 'tool'].includes(x?.role)).length || 0;
1613
1614 return [chat, promptManager.tokenHandler.counts];
1615}
1616
1617/**
1618 * Handles errors during streaming requests.
1619 * @param {Response} response
1620 * @param {string} decoded - response text or decoded stream data
1621 * @param {object} [options]
1622 * @param {boolean?} [options.quiet=false] Suppress toast messages
1623 */
1624export function tryParseStreamingError(response, decoded, { quiet = false } = {}) {
1625 try {
1626 const data = JSON.parse(decoded);
1627
1628 if (!data) {
1629 return;
1630 }
1631
1632 checkQuotaError(data, { quiet });
1633 checkModerationError(data, { quiet });
1634
1635 // these do not throw correctly (equiv to Error("[object Object]"))
1636 // if trying to fix "[object Object]" displayed to users, start here
1637
1638 if (data.error) {
1639 !quiet && toastr.error(data.error.message || response.statusText, 'Chat Completion API');
1640 throw new Error(data);
1641 }
1642
1643 if (data.message) {
1644 !quiet && toastr.error(data.message, 'Chat Completion API');
1645 throw new Error(data);
1646 }
1647
1648 if (data.detail) {
1649 !quiet && toastr.error(data.detail?.error?.message || response.statusText, 'Chat Completion API');
1650 throw new Error(data);
1651 }
1652 } catch {
1653 // No JSON. Do nothing.
1654 }
1655}
1656
1657/**
1658 * Checks if the response contains a quota error and displays a popup if it does.
1659 * @param data
1660 * @param {object} [options]
1661 * @param {boolean?} [options.quiet=false] Suppress toast messages
1662 * @returns {void}
1663 * @throws {object} - response JSON
1664 */
1665function checkQuotaError(data, { quiet = false } = {}) {
1666 if (!data) {
1667 return;
1668 }
1669
1670 if (data.quota_error) {
1671 !quiet && renderTemplateAsync('quotaError').then((html) => Popup.show.text('Quota Error', html));
1672
1673 // this does not throw correctly (equiv to Error("[object Object]"))
1674 // if trying to fix "[object Object]" displayed to users, start here
1675 throw new Error(data);
1676 }
1677}
1678
1679/**
1680 * @param {any} data
1681 * @param {object} [options]
1682 * @param {boolean?} [options.quiet=false] Suppress toast messages
1683 */
1684function checkModerationError(data, { quiet = false } = {}) {
1685 const moderationError = data?.error?.message?.includes('requires moderation');
1686 if (moderationError && !quiet) {
1687 const moderationReason = `Reasons: ${data?.error?.metadata?.reasons?.join(', ') ?? '(N/A)'}`;
1688 const flaggedText = data?.error?.metadata?.flagged_input ?? '(N/A)';
1689 toastr.info(flaggedText, moderationReason, { timeOut: 10000 });
1690 }
1691}
1692
1693/**
1694 * Gets the API model for the selected chat completion source.
1695 * @param {ChatCompletionSettings} settings Chat completion settings
1696 * @returns {string} API model
1697 */
1698export function getChatCompletionModel(settings = null) {
1699 settings = settings ?? oai_settings;
1700 const source = settings.chat_completion_source;
1701 switch (source) {
1702 case chat_completion_sources.CLAUDE:
1703 return settings.claude_model;
1704 case chat_completion_sources.OPENAI:
1705 return settings.openai_model;
1706 case chat_completion_sources.MAKERSUITE:
1707 return settings.google_model;
1708 case chat_completion_sources.VERTEXAI:
1709 return settings.vertexai_model;
1710 case chat_completion_sources.OPENROUTER:
1711 return settings.openrouter_model !== openrouter_website_model ? settings.openrouter_model : null;
1712 case chat_completion_sources.AI21:
1713 return settings.ai21_model;
1714 case chat_completion_sources.MISTRALAI:
1715 return settings.mistralai_model;
1716 case chat_completion_sources.CUSTOM:
1717 return settings.custom_model;
1718 case chat_completion_sources.COHERE:
1719 return settings.cohere_model;
1720 case chat_completion_sources.PERPLEXITY:
1721 return settings.perplexity_model;
1722 case chat_completion_sources.GROQ:
1723 return settings.groq_model;
1724 case chat_completion_sources.SILICONFLOW:
1725 return settings.siliconflow_model;
1726 case chat_completion_sources.MINIMAX:
1727 return settings.minimax_model;
1728 case chat_completion_sources.ELECTRONHUB:
1729 return settings.electronhub_model;
1730 case chat_completion_sources.CHUTES:
1731 return settings.chutes_model;
1732 case chat_completion_sources.NANOGPT:
1733 return settings.nanogpt_model;
1734 case chat_completion_sources.DEEPSEEK:
1735 return settings.deepseek_model;
1736 case chat_completion_sources.AIMLAPI:
1737 return settings.aimlapi_model;
1738 case chat_completion_sources.XAI:
1739 return settings.xai_model;
1740 case chat_completion_sources.POLLINATIONS:
1741 return settings.pollinations_model;
1742 case chat_completion_sources.COMETAPI:
1743 return settings.cometapi_model;
1744 case chat_completion_sources.MOONSHOT:
1745 return settings.moonshot_model;
1746 case chat_completion_sources.FIREWORKS:
1747 return settings.fireworks_model;
1748 case chat_completion_sources.AZURE_OPENAI:
1749 return settings.azure_openai_model;
1750 case chat_completion_sources.ZAI:
1751 return settings.zai_model;
1752 case chat_completion_sources.WORKERS_AI:
1753 return settings.workers_ai_model;
1754 default:
1755 console.error(`Unknown chat completion source: ${source}`);
1756 return '';
1757 }
1758}
1759
1760function getOpenRouterModelTemplate(option) {
1761 const model = model_list.find(x => x.id === option?.element?.value);
1762
1763 if (!option.id || !model) {
1764 return option.text;
1765 }
1766
1767 let tokens_dollar = Number(1 / (1000 * model.pricing?.prompt));
1768 let tokens_rounded = (Math.round(tokens_dollar * 1000) / 1000).toFixed(0);
1769
1770 const price = 0 === Number(model.pricing?.prompt) ? 'Free' : `${tokens_rounded}k t/$ `;
1771
1772 return $((`
1773 <div class="flex-container flexFlowColumn" title="${DOMPurify.sanitize(model.id)}">
1774 <div><strong>${DOMPurify.sanitize(model.name)}</strong> | ${model.context_length} ctx | <small>${price}</small></div>
1775 </div>
1776 `));
1777}
1778
1779function calculateOpenRouterCost() {
1780 if (oai_settings.chat_completion_source !== chat_completion_sources.OPENROUTER) {
1781 return;
1782 }
1783
1784 let cost = 'Unknown';
1785 const model = model_list.find(x => x.id === oai_settings.openrouter_model);
1786
1787 if (model?.pricing) {
1788 const completionCost = Number(model.pricing.completion);
1789 const promptCost = Number(model.pricing.prompt);
1790 const completionTokens = oai_settings.openai_max_tokens;
1791 const promptTokens = (oai_settings.openai_max_context - completionTokens);
1792 const totalCost = (completionCost * completionTokens) + (promptCost * promptTokens);
1793 if (!isNaN(totalCost)) {
1794 cost = '$' + totalCost.toFixed(3);
1795 }
1796 }
1797
1798 if (oai_settings.enable_web_search) {
1799 const webSearchCost = (0.02).toFixed(2);
1800 cost = t`${cost} + $${webSearchCost}`;
1801 }
1802
1803 $('#openrouter_max_prompt_cost').text(cost);
1804}
1805
1806function getElectronHubModelTemplate(option) {
1807 const model = model_list.find(x => x.id === option?.element?.value);
1808
1809 if (!option.id || !model) {
1810 return option.text;
1811 }
1812
1813 const inputPrice = model.pricing?.input;
1814 const outputPrice = model.pricing?.output;
1815 const price = inputPrice && outputPrice ? `$${inputPrice}/$${outputPrice} in/out Mtoken` : 'Unknown';
1816
1817 const visionIcon = model.metadata?.vision ? '<i class="fa-solid fa-eye fa-sm" title="This model supports vision"></i>' : '';
1818 const reasoningIcon = model.metadata?.reasoning ? '<i class="fa-solid fa-brain fa-sm" title="This model supports reasoning"></i>' : '';
1819 const toolCallsIcon = model.metadata?.function_call ? '<i class="fa-solid fa-wrench fa-sm" title="This model supports function tools"></i>' : '';
1820 const premiumIcon = model?.premium_model ? '<i class="fa-solid fa-crown fa-sm" title="This model requires a subscription"></i>' : '';
1821
1822 const iconsContainer = document.createElement('span');
1823 iconsContainer.insertAdjacentHTML('beforeend', visionIcon);
1824 iconsContainer.insertAdjacentHTML('beforeend', reasoningIcon);
1825 iconsContainer.insertAdjacentHTML('beforeend', toolCallsIcon);
1826 iconsContainer.insertAdjacentHTML('beforeend', premiumIcon);
1827
1828 const capabilities = (iconsContainer.children.length) ? ` | ${iconsContainer.innerHTML}` : '';
1829
1830 return $((`
1831 <div class="flex-container alignItemsBaseline" title="${DOMPurify.sanitize(model.id)}">
1832 <strong>${DOMPurify.sanitize(model.name)}</strong> | ${model.tokens} ctx | <small>${price}</small>${capabilities}
1833 </div>
1834 `));
1835}
1836
1837function calculateElectronHubCost() {
1838 if (oai_settings.chat_completion_source !== chat_completion_sources.ELECTRONHUB) {
1839 return;
1840 }
1841
1842 let cost = 'Unknown';
1843 const model = model_list.find(x => x.id === oai_settings.electronhub_model);
1844
1845 if (model?.pricing) {
1846 const outputCost = Number(model.pricing.output / 1000000);
1847 const inputCost = Number(model.pricing.input / 1000000);
1848 const outputTokens = oai_settings.openai_max_tokens;
1849 const inputTokens = (oai_settings.openai_max_context - outputTokens);
1850 const totalCost = (outputCost * outputTokens) + (inputCost * inputTokens);
1851 if (!isNaN(totalCost)) {
1852 cost = '$' + totalCost.toFixed(4);
1853 }
1854 }
1855
1856 $('#electronhub_max_prompt_cost').text(cost);
1857}
1858
1859function getChutesModelTemplate(option) {
1860 const model = model_list.find(x => x.id === option?.element?.value);
1861
1862 if (!option.id || !model) {
1863 return option.text;
1864 }
1865
1866 const inputPrice = model.pricing?.input;
1867 const outputPrice = model.pricing?.output;
1868
1869 let price = 'Unknown';
1870 if (inputPrice !== undefined && outputPrice !== undefined) {
1871 // Check if both prices are 0 (free model)
1872 if (inputPrice === 0 && outputPrice === 0) {
1873 price = 'Free';
1874 } else {
1875 price = `$${inputPrice}/$${outputPrice} in/out Mtoken`;
1876 }
1877 }
1878
1879 const contextLength = model.context_length || model.max_model_len || 'Unknown';
1880 const visionIcon = model.input_modalities?.includes('image') ? '<i class="fa-solid fa-eye fa-sm" title="This model supports vision"></i>' : '';
1881 const reasoningIcon = model.supported_features?.includes('reasoning') ? '<i class="fa-solid fa-brain fa-sm" title="This model supports reasoning"></i>' : '';
1882 const toolCallsIcon = model.supported_features?.includes('structured_outputs') ? '<i class="fa-solid fa-wrench fa-sm" title="This model supports function tools"></i>' : '';
1883
1884 const iconsContainer = document.createElement('span');
1885 iconsContainer.insertAdjacentHTML('beforeend', visionIcon);
1886 iconsContainer.insertAdjacentHTML('beforeend', reasoningIcon);
1887 iconsContainer.insertAdjacentHTML('beforeend', toolCallsIcon);
1888
1889 const capabilities = (iconsContainer.children.length) ? ` | ${iconsContainer.innerHTML}` : '';
1890
1891 return $((`
1892 <div class="flex-container alignItemsBaseline" title="${DOMPurify.sanitize(model.id)}">
1893 <strong>${DOMPurify.sanitize(model.id)}</strong> | ${contextLength} ctx | <small>${price}</small>${capabilities}
1894 </div>
1895 `));
1896}
1897
1898function calculateChutesCost() {
1899 if (oai_settings.chat_completion_source !== chat_completion_sources.CHUTES) {
1900 return;
1901 }
1902
1903 let cost = 'Unknown';
1904 const model = model_list.find(x => x.id === oai_settings.chutes_model);
1905
1906 if (model?.pricing) {
1907 const outputPrice = model.pricing?.output;
1908 const inputPrice = model.pricing?.input;
1909
1910 if (outputPrice !== undefined && inputPrice !== undefined) {
1911 const outputCost = Number(outputPrice / 1000000);
1912 const inputCost = Number(inputPrice / 1000000);
1913 const outputTokens = oai_settings.openai_max_tokens;
1914 const inputTokens = (oai_settings.openai_max_context - outputTokens);
1915 const totalCost = (outputCost * outputTokens) + (inputCost * inputTokens);
1916 if (!isNaN(totalCost)) {
1917 cost = '$' + totalCost.toFixed(4);
1918 }
1919 }
1920 }
1921
1922 $('#chutes_max_prompt_cost').text(cost);
1923}
1924
1925function getNanoGptModelTemplate(option) {
1926 const model = model_list.find(x => x.id === option?.element?.value);
1927
1928 if (!option.id || !model) {
1929 return option.text;
1930 }
1931
1932 const inputPrice = model.pricing?.prompt;
1933 const outputPrice = model.pricing?.completion;
1934 let price = 'Unknown';
1935
1936 if (inputPrice !== undefined && outputPrice !== undefined) {
1937 if (inputPrice === 0 && outputPrice === 0) {
1938 price = 'Free';
1939 } else {
1940 price = `$${Math.round(inputPrice * 100) / 100}/$${Math.round(outputPrice * 100) / 100} in/out Mtoken`;
1941 }
1942 }
1943
1944 const visionIcon = model.capabilities?.vision ? '<i class="fa-solid fa-eye fa-sm" title="This model supports vision"></i>' : '';
1945 const reasoningIcon = model.capabilities?.reasoning ? '<i class="fa-solid fa-brain fa-sm" title="This model supports reasoning"></i>' : '';
1946 const toolCallsIcon = model.capabilities?.tool_calling ? '<i class="fa-solid fa-wrench fa-sm" title="This model supports tool calling"></i>' : '';
1947
1948 let subHtml = '';
1949 const sub = model.subscription;
1950
1951 if (sub) {
1952 if (sub.included) {
1953 let titleText = 'Included in subscription';
1954 let multiplierText = '';
1955
1956 if (sub.inputTokenMultiplier && sub.inputTokenMultiplier !== 1) {
1957 multiplierText = ` (${sub.inputTokenMultiplier}x)`;
1958 titleText += ` - Input Multiplier: ${sub.inputTokenMultiplier}x`;
1959 }
1960 subHtml = ` <small title="${titleText}"><i class="fa-solid fa-crown fa-sm"></i> Sub${multiplierText}</small>`;
1961 } else if (sub.note) {
1962 const safeNote = DOMPurify.sanitize(sub.note);
1963 subHtml = ` <small title="${safeNote}"><i class="fa-solid fa-circle-info fa-sm"></i> Not in Sub</small>`;
1964 }
1965 }
1966
1967 const iconsContainer = document.createElement('span');
1968 iconsContainer.insertAdjacentHTML('beforeend', visionIcon);
1969 iconsContainer.insertAdjacentHTML('beforeend', reasoningIcon);
1970 iconsContainer.insertAdjacentHTML('beforeend', toolCallsIcon);
1971 iconsContainer.insertAdjacentHTML('beforeend', subHtml);
1972
1973 const capabilities = (iconsContainer.children.length) ? ` | ${iconsContainer.innerHTML}` : '';
1974
1975 const contextLength = model.context_length || 'Unknown';
1976 const modelName = model.name || model.id;
1977
1978 return $((`
1979 <div class="flex-container alignItemsBaseline" title="${DOMPurify.sanitize(model.id)}">
1980 <strong>${DOMPurify.sanitize(modelName)}</strong> | ${contextLength} ctx | <small>${price}</small>${capabilities}
1981 </div>
1982 `));
1983}
1984
1985function getAimlapiModelTemplate(option) {
1986 const model = model_list.find(x => x.id === option?.element?.value);
1987
1988 if (!option.id || !model) {
1989 return option.text;
1990 }
1991
1992 const vendor = model.id.split('/')[0];
1993
1994 return $((`
1995 <div class="flex-container flexFlowColumn" title="${DOMPurify.sanitize(model.id)}">
1996 <div><strong>${DOMPurify.sanitize(model.info?.name || model.name || model.id)}</strong> | ${vendor}</div>
1997 </div>
1998 `));
1999}
2000
2001function saveModelList(data) {
2002 model_list = data.map((model) => ({ ...model }));
2003 model_list.sort((a, b) => a?.id && b?.id && a.id.localeCompare(b.id));
2004
2005 if (oai_settings.chat_completion_source == chat_completion_sources.OPENROUTER) {
2006 model_list = sortModelsBy(model_list, oai_settings.sort_models, chat_completion_sources.OPENROUTER);
2007 $('#model_openrouter_select').empty();
2008 $('#model_openrouter_select').append($('<option>', { value: openrouter_website_model, text: t`Use OpenRouter website setting` }));
2009
2010 if (oai_settings.group_models) {
2011 groupModelsByVendor(model_list, chat_completion_sources.OPENROUTER).forEach((models, vendor) => {
2012 const optgroup = $('<optgroup>').attr('label', vendor);
2013 models.forEach((model) => {
2014 optgroup.append($('<option>', { value: model.id, text: model.name }));
2015 });
2016 $('#model_openrouter_select').append(optgroup);
2017 });
2018 } else {
2019 model_list.forEach((model) => {
2020 $('#model_openrouter_select').append($('<option>', { value: model.id, text: model.name }));
2021 });
2022 }
2023
2024 $('#model_openrouter_select').val(oai_settings.openrouter_model).trigger('change');
2025 }
2026
2027 if (oai_settings.chat_completion_source == chat_completion_sources.OPENAI) {
2028 $('#openai_external_category').empty();
2029 model_list.forEach((model) => {
2030 $('#openai_external_category').append(
2031 $('<option>', {
2032 value: model.id,
2033 text: model.id,
2034 }));
2035 });
2036 // If the selected model is not in the list, revert to default
2037 if (oai_settings.show_external_models) {
2038 const model = model_list.findIndex((model) => model.id == oai_settings.openai_model) !== -1 ? oai_settings.openai_model : default_settings.openai_model;
2039 $('#model_openai_select').val(model).trigger('change');
2040 }
2041 }
2042
2043 if (oai_settings.chat_completion_source == chat_completion_sources.CUSTOM) {
2044 $('.model_custom_select').empty();
2045 $('.model_custom_select').append('<option value="">None</option>');
2046 model_list.forEach((model) => {
2047 $('.model_custom_select').append(
2048 $('<option>', {
2049 value: model.id,
2050 text: model.id,
2051 selected: model.id == oai_settings.custom_model,
2052 }));
2053 });
2054
2055 if (!oai_settings.custom_model && model_list.length > 0) {
2056 $('#model_custom_select').val(model_list[0].id).trigger('change');
2057 }
2058 }
2059
2060 if (oai_settings.chat_completion_source == chat_completion_sources.AIMLAPI) {
2061 model_list = model_list.filter(m => m.type === 'chat-completion');
2062 model_list = sortModelsBy(model_list, oai_settings.sort_models, chat_completion_sources.AIMLAPI);
2063 $('#model_aimlapi_select').empty();
2064
2065 if (oai_settings.group_models) {
2066 groupModelsByVendor(model_list, chat_completion_sources.AIMLAPI).forEach((models, vendor) => {
2067 const optgroup = $('<optgroup>').attr('label', vendor);
2068 models.forEach((model) => {
2069 optgroup.append($('<option>', { value: model.id, text: model.info?.name || model.id }));
2070 });
2071 $('#model_aimlapi_select').append(optgroup);
2072 });
2073 } else {
2074 model_list.forEach((model) => {
2075 $('#model_aimlapi_select').append($('<option>', { value: model.id, text: model.info?.name || model.id }));
2076 });
2077 }
2078
2079 if (!oai_settings.aimlapi_model && model_list.length > 0) {
2080 oai_settings.aimlapi_model = model_list[0].id;
2081 }
2082
2083 $('#model_aimlapi_select').val(oai_settings.aimlapi_model).trigger('change');
2084 }
2085
2086 if (oai_settings.chat_completion_source == chat_completion_sources.MISTRALAI) {
2087 $('#model_mistralai_select').empty();
2088
2089 for (const model of model_list.filter(model => model?.capabilities?.completion_chat)) {
2090 $('#model_mistralai_select').append(new Option(model.id, model.id));
2091 }
2092
2093 const selectedModel = model_list.find(model => model.id === oai_settings.mistralai_model);
2094 if (!selectedModel) {
2095 oai_settings.mistralai_model = model_list.find(model => model?.capabilities?.completion_chat)?.id;
2096 }
2097
2098 $('#model_mistralai_select').val(oai_settings.mistralai_model).trigger('change');
2099 }
2100
2101 if (oai_settings.chat_completion_source == chat_completion_sources.ELECTRONHUB) {
2102 model_list = model_list.filter(model => model?.endpoints?.includes('/v1/chat/completions'));
2103 model_list = sortModelsBy(model_list, oai_settings.sort_models, chat_completion_sources.ELECTRONHUB);
2104 $('#model_electronhub_select').empty();
2105
2106 if (oai_settings.group_models) {
2107 groupModelsByVendor(model_list, chat_completion_sources.ELECTRONHUB).forEach((models, vendor) => {
2108 const optgroup = $('<optgroup>').attr('label', vendor);
2109 models.forEach((model) => {
2110 optgroup.append($('<option>', { value: model.id, text: model.name }));
2111 });
2112 $('#model_electronhub_select').append(optgroup);
2113 });
2114 } else {
2115 model_list.forEach((model) => {
2116 $('#model_electronhub_select').append($('<option>', { value: model.id, text: model.name }));
2117 });
2118 }
2119
2120 const selectedModel = model_list.find(model => model.id === oai_settings.electronhub_model);
2121 if (model_list.length > 0 && (!selectedModel || !oai_settings.electronhub_model)) {
2122 oai_settings.electronhub_model = model_list[0].id;
2123 }
2124
2125 $('#model_electronhub_select').val(oai_settings.electronhub_model).trigger('change');
2126 }
2127
2128 if (oai_settings.chat_completion_source == chat_completion_sources.CHUTES) {
2129 model_list = model_list.filter(model => typeof model.id === 'string' && !model.id.toLowerCase().includes('affine'));
2130 model_list = sortModelsBy(model_list, oai_settings.sort_models, chat_completion_sources.CHUTES);
2131 $('#model_chutes_select').empty();
2132
2133 if (oai_settings.group_models) {
2134 groupModelsByVendor(model_list, chat_completion_sources.CHUTES).forEach((models, vendor) => {
2135 const optgroup = $('<optgroup>').attr('label', vendor);
2136 models.forEach((model) => {
2137 optgroup.append($('<option>', { value: model.id, text: model.id }));
2138 });
2139 $('#model_chutes_select').append(optgroup);
2140 });
2141 } else {
2142 model_list.forEach((model) => {
2143 $('#model_chutes_select').append($('<option>', { value: model.id, text: model.id }));
2144 });
2145 }
2146
2147 const selectedModel = model_list.find(model => model.id === oai_settings.chutes_model);
2148 if (model_list.length > 0 && (!selectedModel || !oai_settings.chutes_model)) {
2149 oai_settings.chutes_model = model_list[0].id;
2150 }
2151
2152 $('#model_chutes_select').val(oai_settings.chutes_model).trigger('change');
2153 }
2154
2155 if (oai_settings.chat_completion_source == chat_completion_sources.NANOGPT) {
2156 model_list = sortModelsBy(model_list, oai_settings.sort_models, chat_completion_sources.NANOGPT);
2157 $('#model_nanogpt_select').empty();
2158
2159 if (oai_settings.group_models) {
2160 groupModelsByVendor(model_list, chat_completion_sources.NANOGPT).forEach((models, vendor) => {
2161 const optgroup = $('<optgroup>').attr('label', vendor);
2162 models.forEach((model) => {
2163 optgroup.append($('<option>', { value: model.id, text: model.name || model.id }));
2164 });
2165 $('#model_nanogpt_select').append(optgroup);
2166 });
2167 } else {
2168 model_list.forEach((model) => {
2169 $('#model_nanogpt_select').append($('<option>', { value: model.id, text: model.name || model.id }));
2170 });
2171 }
2172
2173 const selectedModel = model_list.find(model => model.id === oai_settings.nanogpt_model);
2174 if (model_list.length > 0 && (!selectedModel || !oai_settings.nanogpt_model)) {
2175 oai_settings.nanogpt_model = model_list[0].id;
2176 }
2177
2178 $('#model_nanogpt_select').val(oai_settings.nanogpt_model).trigger('change');
2179 }
2180
2181 if (oai_settings.chat_completion_source == chat_completion_sources.DEEPSEEK) {
2182 $('#model_deepseek_select').empty();
2183 model_list.forEach((model) => {
2184 $('#model_deepseek_select').append($('<option>', { value: model.id, text: model.id }));
2185 });
2186
2187 const selectedModel = model_list.find(model => model.id === oai_settings.deepseek_model);
2188 if (model_list.length > 0 && (!selectedModel || !oai_settings.deepseek_model)) {
2189 oai_settings.deepseek_model = model_list[0].id;
2190 }
2191
2192 $('#model_deepseek_select').val(oai_settings.deepseek_model).trigger('change');
2193 }
2194
2195 if (oai_settings.chat_completion_source === chat_completion_sources.POLLINATIONS) {
2196 $('#model_pollinations_select').empty();
2197 model_list.forEach((model) => {
2198 $('#model_pollinations_select').append($('<option>', { value: model.id, text: model.id }));
2199 });
2200
2201 const selectedModel = model_list.find(model => model.id === oai_settings.pollinations_model);
2202 if (model_list.length > 0 && (!selectedModel || !oai_settings.pollinations_model)) {
2203 oai_settings.pollinations_model = model_list[0].id;
2204 }
2205
2206 $('#model_pollinations_select').val(oai_settings.pollinations_model).trigger('change');
2207 }
2208
2209 if (oai_settings.chat_completion_source === chat_completion_sources.MAKERSUITE) {
2210 // Clear only the "Other" optgroup for dynamic models
2211 $('#google_other_models').empty();
2212
2213 // Get static model options that are already in the HTML
2214 const staticModels = [];
2215 $('#model_google_select option').each(function () {
2216 staticModels.push($(this).val());
2217 });
2218
2219 // Add dynamic models to the "Other" group
2220 model_list.forEach((model) => {
2221 // Only add if not already in static list
2222 if (!staticModels.includes(model.id)) {
2223 $('#google_other_models').append(
2224 $('<option>', {
2225 value: model.id,
2226 text: model.id,
2227 }));
2228 }
2229 });
2230
2231 // Merge static models into model_list
2232 staticModels.forEach(modelId => {
2233 if (!model_list.some(model => model.id === modelId)) {
2234 model_list.push({ id: modelId });
2235 }
2236 });
2237
2238 const selectedModel = model_list.find(model => model.id === oai_settings.google_model);
2239 if (model_list.length > 0 && (!selectedModel || !oai_settings.google_model)) {
2240 oai_settings.google_model = model_list[0].id;
2241 }
2242
2243 $('#model_google_select').val(oai_settings.google_model).trigger('change');
2244 }
2245
2246 if (oai_settings.chat_completion_source === chat_completion_sources.GROQ) {
2247 $('#model_groq_select').empty();
2248 model_list.forEach((model) => {
2249 $('#model_groq_select').append(
2250 $('<option>', {
2251 value: model.id,
2252 text: model.id,
2253 }));
2254 });
2255
2256 const selectedModel = model_list.find(model => model.id === oai_settings.groq_model);
2257 if (model_list.length > 0 && (!selectedModel || !oai_settings.groq_model)) {
2258 oai_settings.groq_model = model_list[0].id;
2259 }
2260
2261 $('#model_groq_select').val(oai_settings.groq_model).trigger('change');
2262 }
2263
2264 if (oai_settings.chat_completion_source === chat_completion_sources.SILICONFLOW) {
2265 $('#model_siliconflow_select').empty();
2266 model_list.forEach((model) => {
2267 $('#model_siliconflow_select').append(
2268 $('<option>', {
2269 value: model.id,
2270 text: model.id,
2271 }));
2272 });
2273
2274 const selectedModel = model_list.find(model => model.id === oai_settings.siliconflow_model);
2275 if (model_list.length > 0 && (!selectedModel || !oai_settings.siliconflow_model)) {
2276 oai_settings.siliconflow_model = model_list[0].id;
2277 }
2278
2279 $('#model_siliconflow_select').val(oai_settings.siliconflow_model).trigger('change');
2280 }
2281
2282 if (oai_settings.chat_completion_source === chat_completion_sources.FIREWORKS) {
2283 $('#model_fireworks_select').empty();
2284 model_list.forEach((model) => {
2285 if (!model?.supports_chat) {
2286 return;
2287 }
2288 $('#model_fireworks_select').append(
2289 $('<option>', {
2290 value: model.id,
2291 text: model.id,
2292 }));
2293 });
2294
2295 const selectedModel = model_list.find(model => model.id === oai_settings.fireworks_model);
2296 if (model_list.length > 0 && (!selectedModel || !oai_settings.fireworks_model)) {
2297 oai_settings.fireworks_model = model_list[0].id;
2298 }
2299
2300 $('#model_fireworks_select').val(oai_settings.fireworks_model).trigger('change');
2301 }
2302
2303 if (oai_settings.chat_completion_source === chat_completion_sources.WORKERS_AI) {
2304 $('#model_workers_ai_select').empty();
2305 model_list.forEach((model) => {
2306 $('#model_workers_ai_select').append(
2307 $('<option>', {
2308 value: model.id,
2309 text: model.id,
2310 }));
2311 });
2312
2313 const selectedModel = model_list.find(model => model.id === oai_settings.workers_ai_model);
2314 if (model_list.length > 0 && (!selectedModel || !oai_settings.workers_ai_model)) {
2315 oai_settings.workers_ai_model = model_list[0].id;
2316 }
2317
2318 $('#model_workers_ai_select').val(oai_settings.workers_ai_model).trigger('change');
2319 }
2320
2321 if (oai_settings.chat_completion_source === chat_completion_sources.COMETAPI) {
2322 $('#model_cometapi_select').empty();
2323
2324 model_list.forEach((model) => {
2325 const modelId = model.id.toLowerCase();
2326 const isIgnoredModel = COMETAPI_IGNORE_PATTERNS.some(pattern => modelId.includes(pattern));
2327
2328 if (isIgnoredModel) {
2329 return;
2330 }
2331
2332 $('#model_cometapi_select').append(new Option(model.id, model.id));
2333 });
2334
2335 const selectedModel = model_list.find(model => model.id === oai_settings.cometapi_model);
2336 if (model_list.length > 0 && (!selectedModel || !oai_settings.cometapi_model)) {
2337 oai_settings.cometapi_model = model_list[0].id;
2338 saveSettingsDebounced();
2339 }
2340
2341 $('#model_cometapi_select').val(oai_settings.cometapi_model).trigger('change');
2342 }
2343
2344 if (oai_settings.chat_completion_source == chat_completion_sources.AZURE_OPENAI) {
2345 const modelId = model_list?.[0]?.id || '';
2346 oai_settings.azure_openai_model = modelId;
2347
2348 $('#azure_openai_model')
2349 .empty()
2350 .append(new Option(modelId || 'None', modelId || '', true, true))
2351 .trigger('change');
2352 }
2353
2354 if (oai_settings.chat_completion_source == chat_completion_sources.XAI) {
2355 $('#model_xai_select').empty();
2356 model_list.forEach((model) => {
2357 $('#model_xai_select').append(
2358 $('<option>', {
2359 value: model.id,
2360 text: model.id,
2361 }));
2362 });
2363
2364 const selectedModel = model_list.find(model => model.id === oai_settings.xai_model);
2365 if (model_list.length > 0 && (!selectedModel || !oai_settings.xai_model)) {
2366 oai_settings.xai_model = model_list[0].id;
2367 }
2368
2369 $('#model_xai_select').val(oai_settings.xai_model).trigger('change');
2370 }
2371
2372 if (oai_settings.chat_completion_source == chat_completion_sources.MOONSHOT) {
2373 $('#model_moonshot_select').empty();
2374 model_list.forEach((model) => {
2375 $('#model_moonshot_select').append(new Option(model.id, model.id));
2376 });
2377
2378 const selectedModel = model_list.find(model => model.id === oai_settings.moonshot_model);
2379 if (model_list.length > 0 && (!selectedModel || !oai_settings.moonshot_model)) {
2380 oai_settings.moonshot_model = model_list[0].id;
2381 }
2382
2383 $('#model_moonshot_select').val(oai_settings.moonshot_model).trigger('change');
2384 }
2385}
2386
2387/**
2388 * Sorts models by the specified property for the given source.
2389 * @param {object[]} data - Array of model objects
2390 * @param {string} property - Sort property ('alphabetically', 'context_length', 'pricing.prompt', 'pricing.completion')
2391 * @param {string} source - Chat Completion source (e.g., 'openrouter', 'chutes', 'electronhub', 'nanogpt')
2392 * @returns {object[]} Sorted array of model objects
2393 */
2394function sortModelsBy(data, property, source) {
2395 switch (source) {
2396 case chat_completion_sources.OPENROUTER:
2397 return data.sort((a, b) => {
2398 if (property === 'context_length') {
2399 return (b.context_length || 0) - (a.context_length || 0);
2400 } else if (property === 'pricing.input' || property === 'pricing.prompt') {
2401 return parseFloat(a.pricing?.prompt || 0) - parseFloat(b.pricing?.prompt || 0);
2402 } else if (property === 'pricing.output' || property === 'pricing.completion') {
2403 return parseFloat(a.pricing?.completion || 0) - parseFloat(b.pricing?.completion || 0);
2404 } else {
2405 return a?.name && b?.name ? a.name.localeCompare(b.name) : 0;
2406 }
2407 });
2408 case chat_completion_sources.CHUTES:
2409 return data.sort((a, b) => {
2410 if (property === 'context_length') {
2411 return (b.context_length || 0) - (a.context_length || 0);
2412 } else if (property === 'pricing.input' || property === 'pricing.prompt') {
2413 return parseFloat(a.pricing?.input || 0) - parseFloat(b.pricing?.input || 0);
2414 } else if (property === 'pricing.output' || property === 'pricing.completion') {
2415 return parseFloat(a.pricing?.output || 0) - parseFloat(b.pricing?.output || 0);
2416 } else {
2417 return a?.id && b?.id ? a.id.localeCompare(b.id) : 0;
2418 }
2419 });
2420 case chat_completion_sources.ELECTRONHUB:
2421 return data.sort((a, b) => {
2422 if (property === 'context_length') {
2423 return (b.tokens || 0) - (a.tokens || 0);
2424 } else if (property === 'pricing.input' || property === 'pricing.prompt') {
2425 return parseFloat(a.pricing?.input || 0) - parseFloat(b.pricing?.input || 0);
2426 } else if (property === 'pricing.output' || property === 'pricing.completion') {
2427 return parseFloat(a.pricing?.output || 0) - parseFloat(b.pricing?.output || 0);
2428 } else {
2429 return a?.name && b?.name ? a.name.localeCompare(b.name) : 0;
2430 }
2431 });
2432 case chat_completion_sources.NANOGPT:
2433 return data.sort((a, b) => {
2434 if (property === 'context_length') {
2435 return (b.context_length || 0) - (a.context_length || 0);
2436 } else if (property === 'pricing.input' || property === 'pricing.prompt') {
2437 return parseFloat(a.pricing?.prompt || 0) - parseFloat(b.pricing?.prompt || 0);
2438 } else if (property === 'pricing.output' || property === 'pricing.completion') {
2439 return parseFloat(a.pricing?.completion || 0) - parseFloat(b.pricing?.completion || 0);
2440 } else {
2441 return a?.name && b?.name ? a.name.localeCompare(b.name) : 0;
2442 }
2443 });
2444 case chat_completion_sources.AIMLAPI:
2445 return data.sort((a, b) => {
2446 if (property === 'context_length') {
2447 return (b.info?.contextLength || 0) - (a.info?.contextLength || 0);
2448 } else {
2449 // No pricing information on the API. Sort alphabetically by name.
2450 return a?.info?.name && b?.info?.name ? a.info.name.localeCompare(b.info.name) : 0;
2451 }
2452 });
2453 default:
2454 return data;
2455 }
2456}
2457
2458/**
2459 * Groups models by vendor for the given source. If not supported, returns a map with a single entry containing all models.
2460 * @param {object[]} array Array of model objects
2461 * @param {string} source Chat Completion source (e.g., 'openrouter')
2462 * @returns {Map<string, object[]>} Map of vendor to array of models
2463 */
2464function groupModelsByVendor(array, source) {
2465 switch (source) {
2466 case chat_completion_sources.OPENROUTER:
2467 return array.reduce((acc, curr) => {
2468 const vendor = curr.id.split('/')[0];
2469 if (!acc.has(vendor)) {
2470 acc.set(vendor, []);
2471 }
2472 acc.get(vendor).push(curr);
2473 return acc;
2474 }, new Map());
2475 case chat_completion_sources.ELECTRONHUB:
2476 return array.reduce((acc, curr) => {
2477 const vendor = String(curr?.name || curr?.id || 'Other').split(':')[0].trim() || 'Other';
2478 if (!acc.has(vendor)) {
2479 acc.set(vendor, []);
2480 }
2481 acc.get(vendor).push(curr);
2482 return acc;
2483 }, new Map());
2484 case chat_completion_sources.NANOGPT:
2485 return array.reduce((acc, curr) => {
2486 const vendorPart = /\//.test(curr.id) ? curr.id.split('/')[0] : curr.id.split('-')[0];
2487 const vendor = String(vendorPart?.trim()?.toLowerCase() || 'Other');
2488 if (!acc.has(vendor)) {
2489 acc.set(vendor, []);
2490 }
2491 acc.get(vendor).push(curr);
2492 return acc;
2493 }, new Map());
2494 case chat_completion_sources.CHUTES:
2495 return array.reduce((acc, curr) => {
2496 const vendor = curr.id.split('/')[0];
2497 if (!acc.has(vendor)) {
2498 acc.set(vendor, []);
2499 }
2500 acc.get(vendor).push(curr);
2501 return acc;
2502 }, new Map());
2503 case chat_completion_sources.AIMLAPI:
2504 return array.reduce((acc, curr) => {
2505 const vendor = curr.info?.developer || 'Other';
2506 if (!acc.has(vendor)) {
2507 acc.set(vendor, []);
2508 }
2509 acc.get(vendor).push(curr);
2510 return acc;
2511 }, new Map());
2512 default:
2513 return new Map([['', array]]);
2514 }
2515}
2516
2517/**
2518 * Get the reasoning effort from chat completion settings
2519 * @param {ChatCompletionSettings} settings Chat completion settings
2520 * @param {string} model Model name (optional, used for ElectronHub)
2521 * @returns {string} Reasoning effort, if present
2522 */
2523function getReasoningEffort(settings = null, model = null) {
2524 settings = settings ?? oai_settings;
2525 model = model ?? getChatCompletionModel(settings);
2526
2527 // These sources expect the effort as string.
2528 const reasoningEffortSources = [
2529 chat_completion_sources.OPENAI,
2530 chat_completion_sources.AZURE_OPENAI,
2531 chat_completion_sources.CUSTOM,
2532 chat_completion_sources.XAI,
2533 chat_completion_sources.AIMLAPI,
2534 chat_completion_sources.OPENROUTER,
2535 chat_completion_sources.POLLINATIONS,
2536 chat_completion_sources.PERPLEXITY,
2537 chat_completion_sources.COMETAPI,
2538 chat_completion_sources.ELECTRONHUB,
2539 chat_completion_sources.CHUTES,
2540 chat_completion_sources.DEEPSEEK,
2541 ];
2542
2543 if (!reasoningEffortSources.includes(settings.chat_completion_source)) {
2544 return settings.reasoning_effort;
2545 }
2546
2547 function resolveReasoningEffort() {
2548 if (settings.chat_completion_source === chat_completion_sources.DEEPSEEK) {
2549 switch (settings.reasoning_effort) {
2550 case reasoning_effort_types.auto:
2551 return undefined;
2552 case reasoning_effort_types.max:
2553 return reasoning_effort_types.max;
2554 default:
2555 return reasoning_effort_types.high;
2556 }
2557 }
2558
2559 if (settings.chat_completion_source === chat_completion_sources.CUSTOM && /^koboldcpp\/(.+)$/.test(model)) {
2560 switch (settings.reasoning_effort) {
2561 case reasoning_effort_types.auto:
2562 return undefined;
2563 case reasoning_effort_types.min:
2564 return 'minimal';
2565 case reasoning_effort_types.low:
2566 return 'low';
2567 case reasoning_effort_types.medium:
2568 return 'medium';
2569 case reasoning_effort_types.high:
2570 return 'high';
2571 case reasoning_effort_types.max:
2572 return 'xhigh';
2573 default:
2574 return settings.reasoning_effort;
2575 }
2576 }
2577
2578 switch (settings.reasoning_effort) {
2579 case reasoning_effort_types.auto:
2580 return undefined;
2581 case reasoning_effort_types.min:
2582 if (chat_completion_sources.OPENROUTER === settings.chat_completion_source && !settings.show_thoughts) {
2583 return 'none';
2584 }
2585
2586 if ([chat_completion_sources.OPENAI, chat_completion_sources.AZURE_OPENAI].includes(settings.chat_completion_source)) {
2587 if (/^gpt-5\.(4|5)/.test(model)) {
2588 return 'none';
2589 }
2590 if (/^gpt-5/.test(model)) {
2591 return reasoning_effort_types.min;
2592 }
2593 }
2594
2595 return reasoning_effort_types.low;
2596 case reasoning_effort_types.max:
2597 return reasoning_effort_types.high;
2598 default:
2599 return settings.reasoning_effort;
2600 }
2601 }
2602
2603 const reasoningEffort = resolveReasoningEffort();
2604
2605 // Check if the resolved effort supported by the model
2606 if (settings.chat_completion_source === chat_completion_sources.ELECTRONHUB) {
2607 if (Array.isArray(model_list) && reasoningEffort) {
2608 const currentModel = model_list.find(m => m.id === model);
2609 const supportedEfforts = currentModel?.metadata?.supported_reasoning_efforts;
2610 if (Array.isArray(supportedEfforts) && supportedEfforts.includes(reasoningEffort)) {
2611 return reasoningEffort;
2612 }
2613 return undefined;
2614 }
2615 }
2616
2617 return reasoningEffort;
2618}
2619
2620/**
2621 * Get the verbosity from chat completion settings
2622 * @param {ChatCompletionSettings} settings Chat completion settings
2623 * @returns {string} Verbosity level, if present
2624 */
2625function getVerbosity(settings = null) {
2626 settings = settings ?? oai_settings;
2627
2628 if (settings.verbosity === verbosity_levels.auto) {
2629 return undefined;
2630 }
2631
2632 // TODO: Adjust verbosity based on model capabilities
2633 return settings.verbosity;
2634}
2635
2636/**
2637 * Build the generation parameter object for an OAI request.
2638 * @param {ChatCompletionSettings} settings Initial chat completion settings
2639 * @param {string} model Model name
2640 * @param {string} type Request type (impersonate, quiet, continue, etc)
2641 * @param {ChatCompletionMessage[]} messages Array of chat completion messages
2642 * @param {import('../script.js').AdditionalRequestOptions} options Additional request options
2643 * @returns {Promise<object>} Final generation parameters object appropriate for the chat completion source
2644 */
2645export async function createGenerationParameters(settings, model, type, messages, { jsonSchema = null } = {}) {
2646 // HACK: Filter out null and non-object messages
2647 if (!Array.isArray(messages)) {
2648 throw new Error('messages must be an array');
2649 }
2650 messages = messages.filter(msg => msg && typeof msg === 'object');
2651
2652 // "OpenAI-like" sources
2653 const gptSources = [
2654 chat_completion_sources.OPENAI,
2655 chat_completion_sources.AZURE_OPENAI,
2656 chat_completion_sources.OPENROUTER,
2657 ];
2658
2659 // Sources that support the "seed" parameter
2660 const seedSupportedSources = [
2661 chat_completion_sources.OPENAI,
2662 chat_completion_sources.AZURE_OPENAI,
2663 chat_completion_sources.OPENROUTER,
2664 chat_completion_sources.MISTRALAI,
2665 chat_completion_sources.CUSTOM,
2666 chat_completion_sources.COHERE,
2667 chat_completion_sources.GROQ,
2668 chat_completion_sources.ELECTRONHUB,
2669 chat_completion_sources.NANOGPT,
2670 chat_completion_sources.XAI,
2671 chat_completion_sources.POLLINATIONS,
2672 chat_completion_sources.AIMLAPI,
2673 chat_completion_sources.VERTEXAI,
2674 chat_completion_sources.MAKERSUITE,
2675 chat_completion_sources.CHUTES,
2676 ];
2677
2678 // Sources that support proxying
2679 const proxySupportedSources = [
2680 chat_completion_sources.CLAUDE,
2681 chat_completion_sources.OPENAI,
2682 chat_completion_sources.MISTRALAI,
2683 chat_completion_sources.MAKERSUITE,
2684 chat_completion_sources.VERTEXAI,
2685 chat_completion_sources.DEEPSEEK,
2686 chat_completion_sources.XAI,
2687 chat_completion_sources.ZAI,
2688 chat_completion_sources.MOONSHOT,
2689 ];
2690
2691 // Sources that support logprobs
2692 const logprobsSupportedSources = [
2693 chat_completion_sources.OPENAI,
2694 chat_completion_sources.AZURE_OPENAI,
2695 chat_completion_sources.CUSTOM,
2696 chat_completion_sources.DEEPSEEK,
2697 chat_completion_sources.XAI,
2698 chat_completion_sources.AIMLAPI,
2699 chat_completion_sources.CHUTES,
2700 ];
2701
2702 // Sources that support logit bias
2703 const logitBiasSources = [
2704 chat_completion_sources.OPENAI,
2705 chat_completion_sources.AZURE_OPENAI,
2706 chat_completion_sources.OPENROUTER,
2707 chat_completion_sources.ELECTRONHUB,
2708 chat_completion_sources.CHUTES,
2709 chat_completion_sources.CUSTOM,
2710 ];
2711
2712 // Sources that support "n" parameter for multi-swipe
2713 const multiswipeSources = [
2714 chat_completion_sources.OPENAI,
2715 chat_completion_sources.AZURE_OPENAI,
2716 chat_completion_sources.CUSTOM,
2717 chat_completion_sources.XAI,
2718 chat_completion_sources.AIMLAPI,
2719 chat_completion_sources.MOONSHOT,
2720 ];
2721
2722 const isO1 = gptSources.includes(settings.chat_completion_source) && ['o1-2024-12-17', 'o1'].includes(model);
2723 const isWorkersAIJsonMode = settings.chat_completion_source === chat_completion_sources.WORKERS_AI && jsonSchema;
2724 const stream = settings.stream_openai && type !== 'quiet' && !isO1 && !isWorkersAIJsonMode;
2725
2726 const noMultiSwipeTypes = ['quiet', 'impersonate', 'continue'];
2727 const canMultiSwipe = settings.n > 1 && !noMultiSwipeTypes.includes(type) && multiswipeSources.includes(settings.chat_completion_source);
2728
2729 let logit_bias = {};
2730 if (settings.bias_preset_selected
2731 && logitBiasSources.includes(settings.chat_completion_source)
2732 && Array.isArray(settings.bias_presets[settings.bias_preset_selected])
2733 && settings.bias_presets[settings.bias_preset_selected].length) {
2734 logit_bias = biasCache || await calculateLogitBias();
2735 biasCache = logit_bias;
2736 }
2737
2738 if (Object.keys(logit_bias).length === 0) {
2739 logit_bias = undefined;
2740 }
2741
2742 const generate_data = {
2743 'type': type,
2744 'messages': messages,
2745 'model': model,
2746 'temperature': Number(settings.temp_openai),
2747 'frequency_penalty': Number(settings.freq_pen_openai),
2748 'presence_penalty': Number(settings.pres_pen_openai),
2749 'top_p': Number(settings.top_p_openai),
2750 'max_tokens': settings.openai_max_tokens,
2751 'stream': stream,
2752 'logit_bias': logit_bias,
2753 'stop': getCustomStoppingStrings(openai_max_stop_strings),
2754 'chat_completion_source': settings.chat_completion_source,
2755 'n': canMultiSwipe ? settings.n : undefined,
2756 'user_name': name1,
2757 'char_name': name2,
2758 'group_names': getGroupNames(),
2759 'include_reasoning': Boolean(settings.show_thoughts),
2760 'reasoning_effort': getReasoningEffort(settings, model),
2761 'enable_web_search': Boolean(settings.enable_web_search),
2762 'request_images': Boolean(settings.request_images),
2763 'request_image_resolution': String(settings.request_image_resolution),
2764 'request_image_aspect_ratio': String(settings.request_image_aspect_ratio),
2765 'custom_prompt_post_processing': settings.custom_prompt_post_processing,
2766 'verbosity': getVerbosity(settings),
2767 };
2768
2769 if (settings.chat_completion_source === chat_completion_sources.AZURE_OPENAI) {
2770 generate_data.azure_base_url = settings.azure_base_url;
2771 generate_data.azure_deployment_name = settings.azure_deployment_name;
2772 generate_data.azure_api_version = settings.azure_api_version;
2773 // Reasoning effort is not supported on some Azure models (e.g. GPT-3.x, GPT-4.x)
2774 if (/^gpt-[34]/.test(model)) {
2775 delete generate_data.reasoning_effort;
2776 }
2777 }
2778
2779 if (!canMultiSwipe && ToolManager.canPerformToolCalls(type, settings, model)) {
2780 await ToolManager.registerFunctionToolsOpenAI(generate_data);
2781 }
2782
2783 // Empty array will produce a validation error
2784 if (!Array.isArray(generate_data.stop) || !generate_data.stop.length) {
2785 delete generate_data.stop;
2786 }
2787
2788 if (settings.reverse_proxy && proxySupportedSources.includes(settings.chat_completion_source)) {
2789 await validateReverseProxy();
2790 generate_data.reverse_proxy = settings.reverse_proxy;
2791 generate_data.proxy_password = settings.proxy_password;
2792 }
2793
2794 // Add logprobs request (max 5 per OpenAI docs)
2795 const useLogprobs = !!power_user.request_token_probabilities;
2796 if (useLogprobs && logprobsSupportedSources.includes(settings.chat_completion_source)) {
2797 generate_data.logprobs = 5;
2798 }
2799
2800 // Remove logit bias/logprobs/stop-strings if not supported by the model
2801 const isVision = (m) => ['gpt', 'vision'].every(x => typeof m === 'string' && m.includes(x));
2802 if (gptSources.includes(settings.chat_completion_source) && isVision(model)) {
2803 delete generate_data.logit_bias;
2804 delete generate_data.stop;
2805 delete generate_data.logprobs;
2806 }
2807 if (gptSources.includes(settings.chat_completion_source) && /gpt-4.5/.test(model)) {
2808 delete generate_data.logprobs;
2809 }
2810
2811 if (settings.chat_completion_source === chat_completion_sources.CLAUDE) {
2812 generate_data.top_k = Number(settings.top_k_openai);
2813 generate_data.use_sysprompt = settings.use_sysprompt;
2814 generate_data.stop = getCustomStoppingStrings(); // Claude shouldn't have limits on stop strings.
2815 // Don't add a prefill on quiet gens (summarization) and when using continue prefill.
2816 if (type !== 'quiet' && !(type === 'continue' && settings.continue_prefill)) {
2817 generate_data.assistant_prefill = type === 'impersonate'
2818 ? substituteParams(settings.assistant_impersonation)
2819 : substituteParams(settings.assistant_prefill);
2820 }
2821 }
2822
2823 if (settings.chat_completion_source === chat_completion_sources.OPENROUTER) {
2824 generate_data.top_k = Number(settings.top_k_openai);
2825 generate_data.min_p = Number(settings.min_p_openai);
2826 generate_data.repetition_penalty = Number(settings.repetition_penalty_openai);
2827 generate_data.top_a = Number(settings.top_a_openai);
2828 generate_data.use_fallback = settings.openrouter_use_fallback;
2829 generate_data.provider = settings.openrouter_providers;
2830 generate_data.quantizations = settings.openrouter_quantizations;
2831 generate_data.allow_fallbacks = settings.openrouter_allow_fallbacks;
2832 generate_data.middleout = settings.openrouter_middleout;
2833 }
2834
2835 if (settings.chat_completion_source === chat_completion_sources.NANOGPT) {
2836 generate_data.nanogpt_provider = settings.nanogpt_provider;
2837 generate_data.nanogpt_payg_override = settings.nanogpt_payg_override;
2838 }
2839
2840 if ([chat_completion_sources.MAKERSUITE, chat_completion_sources.VERTEXAI].includes(settings.chat_completion_source)) {
2841 const stopStringsLimit = 5;
2842 generate_data.top_k = Number(settings.top_k_openai);
2843 generate_data.stop = getCustomStoppingStrings(stopStringsLimit).slice(0, stopStringsLimit).filter(x => x.length >= 1 && x.length <= 16);
2844 generate_data.use_sysprompt = settings.use_sysprompt;
2845 if (settings.chat_completion_source === chat_completion_sources.VERTEXAI) {
2846 generate_data.vertexai_auth_mode = settings.vertexai_auth_mode;
2847 generate_data.vertexai_region = settings.vertexai_region;
2848 generate_data.vertexai_express_project_id = settings.vertexai_express_project_id;
2849 }
2850 }
2851
2852 if (settings.chat_completion_source === chat_completion_sources.MISTRALAI) {
2853 generate_data.safe_prompt = false; // already defaults to false, but just incase they change that in the future.
2854 generate_data.stop = getCustomStoppingStrings(); // Mistral shouldn't have limits on stop strings.
2855 }
2856
2857 if (settings.chat_completion_source === chat_completion_sources.CUSTOM) {
2858 generate_data.custom_url = settings.custom_url;
2859 generate_data.custom_include_body = settings.custom_include_body;
2860 generate_data.custom_exclude_body = settings.custom_exclude_body;
2861 generate_data.custom_include_headers = settings.custom_include_headers;
2862 }
2863
2864 if (settings.chat_completion_source === chat_completion_sources.COHERE) {
2865 // Clamp to 0.01 -> 0.99
2866 generate_data.top_p = Math.min(Math.max(Number(settings.top_p_openai), 0.01), 0.99);
2867 generate_data.top_k = Number(settings.top_k_openai);
2868 // Clamp to 0 -> 1
2869 generate_data.frequency_penalty = Math.min(Math.max(Number(settings.freq_pen_openai), 0), 1);
2870 generate_data.presence_penalty = Math.min(Math.max(Number(settings.pres_pen_openai), 0), 1);
2871 generate_data.stop = getCustomStoppingStrings(5);
2872 }
2873
2874 if (settings.chat_completion_source === chat_completion_sources.PERPLEXITY) {
2875 generate_data.top_k = Number(settings.top_k_openai);
2876 generate_data.frequency_penalty = Number(settings.freq_pen_openai);
2877 generate_data.presence_penalty = Number(settings.pres_pen_openai);
2878 delete generate_data.stop;
2879 }
2880
2881 // https://console.groq.com/docs/openai
2882 if (settings.chat_completion_source === chat_completion_sources.GROQ) {
2883 delete generate_data.logprobs;
2884 delete generate_data.logit_bias;
2885 delete generate_data.top_logprobs;
2886 delete generate_data.n;
2887 }
2888
2889 // https://api-docs.deepseek.com/api/create-chat-completion
2890 if (settings.chat_completion_source === chat_completion_sources.DEEPSEEK) {
2891 generate_data.top_p = generate_data.top_p || Number.EPSILON;
2892 }
2893
2894 if (settings.chat_completion_source === chat_completion_sources.XAI) {
2895 if (model.includes('grok-3-mini')) {
2896 delete generate_data.presence_penalty;
2897 delete generate_data.frequency_penalty;
2898 delete generate_data.stop;
2899 } else {
2900 // As of 2025/09/21, only grok-3-mini accepts reasoning_effort
2901 delete generate_data.reasoning_effort;
2902 }
2903
2904 if (model.includes('grok-4') || model.includes('grok-code')) {
2905 delete generate_data.presence_penalty;
2906 delete generate_data.frequency_penalty;
2907
2908 // grok-4-fast-non-reasoning accepts stop
2909 if (!model.includes('grok-4-fast-non-reasoning')) {
2910 delete generate_data.stop;
2911 }
2912 }
2913 }
2914
2915 // https://docs.electronhub.ai/api-reference/chat/completions
2916 if (settings.chat_completion_source === chat_completion_sources.ELECTRONHUB) {
2917 generate_data.top_k = Number(settings.top_k_openai);
2918 }
2919
2920 if (settings.chat_completion_source === chat_completion_sources.CHUTES) {
2921 generate_data.min_p = Number(settings.min_p_openai);
2922 generate_data.top_k = settings.top_k_openai > 0 ? Number(settings.top_k_openai) : undefined;
2923 generate_data.repetition_penalty = Number(settings.repetition_penalty_openai);
2924 generate_data.stop = getCustomStoppingStrings();
2925 }
2926
2927 // https://docs.z.ai/api-reference/llm/chat-completion
2928 if (settings.chat_completion_source === chat_completion_sources.ZAI) {
2929 generate_data.top_p = generate_data.top_p || 0.01;
2930 generate_data.stop = getCustomStoppingStrings(1);
2931 generate_data.zai_endpoint = settings.zai_endpoint || ZAI_ENDPOINT.COMMON;
2932 delete generate_data.presence_penalty;
2933 delete generate_data.frequency_penalty;
2934 }
2935
2936 if (settings.chat_completion_source === chat_completion_sources.SILICONFLOW) {
2937 generate_data.siliconflow_endpoint = settings.siliconflow_endpoint || SILICONFLOW_ENDPOINT.GLOBAL;
2938 }
2939
2940 if (settings.chat_completion_source === chat_completion_sources.MINIMAX) {
2941 generate_data.minimax_endpoint = settings.minimax_endpoint || MINIMAX_ENDPOINT.GLOBAL;
2942 // MiniMax requires temperature in (0.0, 1.0]; zero is rejected.
2943 if (Number.isFinite(generate_data.temperature)) {
2944 generate_data.temperature = clamp(generate_data.temperature, Number.EPSILON, 1.0);
2945 }
2946 }
2947
2948 if (settings.chat_completion_source === chat_completion_sources.WORKERS_AI) {
2949 generate_data.workers_ai_account_id = settings.workers_ai_account_id;
2950 generate_data.top_k = settings.top_k_openai > 0 ? Math.min(Number(settings.top_k_openai), 50) : undefined;
2951 generate_data.repetition_penalty = Number(settings.repetition_penalty_openai);
2952 generate_data.seed = settings.seed >= 1 ? Number(settings.seed) : undefined;
2953 generate_data.top_p = Math.max(Number(settings.top_p_openai), 0.001);
2954 delete generate_data.n;
2955 delete generate_data.logit_bias;
2956 }
2957
2958 // https://docs.nano-gpt.com/api-reference/endpoint/chat-completion#temperature-&-nucleus
2959 if (settings.chat_completion_source === chat_completion_sources.NANOGPT) {
2960 generate_data.top_k = Number(settings.top_k_openai);
2961 generate_data.min_p = Number(settings.min_p_openai);
2962 generate_data.repetition_penalty = Number(settings.repetition_penalty_openai);
2963 generate_data.top_a = Number(settings.top_a_openai);
2964 }
2965
2966 // https://platform.moonshot.ai/docs/api/chat#public-service-address
2967 if (settings.chat_completion_source === chat_completion_sources.MOONSHOT) {
2968 // >Kimi API is fully compatible with OpenAI's API format
2969 if (/kimi-k2.5/.test(model)) {
2970 delete generate_data.temperature;
2971 delete generate_data.top_p;
2972 delete generate_data.frequency_penalty;
2973 delete generate_data.presence_penalty;
2974 }
2975 }
2976
2977 if (seedSupportedSources.includes(settings.chat_completion_source) && settings.seed >= 0) {
2978 generate_data.seed = settings.seed;
2979 }
2980
2981 if ([chat_completion_sources.OPENAI, chat_completion_sources.AZURE_OPENAI].includes(settings.chat_completion_source) && /^(o1|o3|o4)/.test(model) ||
2982 (chat_completion_sources.OPENROUTER === settings.chat_completion_source && /^openai\/(o1|o3|o4)/.test(model))) {
2983 generate_data.max_completion_tokens = generate_data.max_tokens;
2984 delete generate_data.max_tokens;
2985 delete generate_data.logprobs;
2986 delete generate_data.top_logprobs;
2987 delete generate_data.stop;
2988 delete generate_data.logit_bias;
2989 delete generate_data.temperature;
2990 delete generate_data.top_p;
2991 delete generate_data.frequency_penalty;
2992 delete generate_data.presence_penalty;
2993 if (/^(openai\/)?(o1)/.test(model)) {
2994 generate_data.messages.forEach((msg) => {
2995 if (msg.role === 'system') {
2996 msg.role = 'user';
2997 }
2998 });
2999 delete generate_data.n;
3000 delete generate_data.tools;
3001 delete generate_data.tool_choice;
3002 }
3003 }
3004
3005 if (gptSources.includes(settings.chat_completion_source) && /gpt-5/.test(model)) {
3006 generate_data.max_completion_tokens = generate_data.max_tokens;
3007 delete generate_data.max_tokens;
3008 delete generate_data.logprobs;
3009 delete generate_data.top_logprobs;
3010 if (/gpt-5-chat-latest/.test(model)) {
3011 delete generate_data.tools;
3012 delete generate_data.tool_choice;
3013 } else if (/gpt-5\.(1|2|3|4)/.test(model) && !/chat-latest/.test(model) && !generate_data.reasoning_effort) {
3014 delete generate_data.frequency_penalty;
3015 delete generate_data.presence_penalty;
3016 delete generate_data.logit_bias;
3017 delete generate_data.stop;
3018 } else {
3019 delete generate_data.temperature;
3020 delete generate_data.top_p;
3021 delete generate_data.frequency_penalty;
3022 delete generate_data.presence_penalty;
3023 delete generate_data.logit_bias;
3024 delete generate_data.stop;
3025 }
3026 }
3027
3028 if (jsonSchema) {
3029 generate_data.json_schema = jsonSchema;
3030 }
3031
3032 return { generate_data, stream, canMultiSwipe };
3033}
3034
3035/**
3036 * Send a chat completion request to backend
3037 * @param {string} type Request type (impersonate, quiet, continue, etc)
3038 * @param {ChatCompletionMessage[]} messages Array of chat completion messages
3039 * @param {AbortSignal?} signal Abort signal for request cancellation
3040 * @param {import('../script.js').AdditionalRequestOptions} options Additional request options
3041 * @returns {Promise<unknown>}
3042 * @throws {Error}
3043 */
3044async function sendOpenAIRequest(type, messages, signal, { jsonSchema = null } = {}) {
3045 // Provide default abort signal
3046 if (!signal) {
3047 signal = new AbortController().signal;
3048 }
3049
3050 const model = getChatCompletionModel(oai_settings);
3051 const { generate_data, stream, canMultiSwipe } = await createGenerationParameters(oai_settings, model, type, messages, { jsonSchema });
3052 await eventSource.emit(event_types.CHAT_COMPLETION_SETTINGS_READY, generate_data);
3053
3054 const generate_url = '/api/backends/chat-completions/generate';
3055 const response = await fetch(generate_url, {
3056 method: 'POST',
3057 body: JSON.stringify(generate_data),
3058 headers: getRequestHeaders(),
3059 signal: signal,
3060 });
3061
3062 if (!response.ok) {
3063 tryParseStreamingError(response, await response.text());
3064 throw new Error(`Got response status ${response.status}`);
3065 }
3066 if (stream) {
3067 const eventStream = getEventSourceStream();
3068 response.body.pipeThrough(eventStream);
3069 const reader = eventStream.readable.getReader();
3070 return async function* streamData() {
3071 let text = '';
3072 const swipes = [];
3073 const toolCalls = [];
3074 const state = { reasoning: '', images: [], signature: '', toolSignatures: {} };
3075 while (true) {
3076 const { done, value } = await reader.read();
3077 if (done) return;
3078 const rawData = value.data;
3079 if (rawData === '[DONE]') return;
3080 tryParseStreamingError(response, rawData);
3081 const parsed = JSON.parse(rawData);
3082
3083 if (canMultiSwipe && Array.isArray(parsed?.choices) && parsed?.choices?.[0]?.index > 0) {
3084 const swipeIndex = parsed.choices[0].index - 1;
3085 // FIXME: state.reasoning should be an array to support multi-swipe
3086 swipes[swipeIndex] = (swipes[swipeIndex] || '') + getStreamingReply(parsed, state, { overrideShowThoughts: false });
3087 } else {
3088 text += getStreamingReply(parsed, state);
3089 }
3090
3091 ToolManager.parseToolCalls(toolCalls, parsed, state.toolSignatures);
3092
3093 yield { text, swipes: swipes, logprobs: parseChatCompletionLogprobs(parsed), toolCalls: toolCalls, state: state };
3094 }
3095 };
3096 } else {
3097 const data = await response.json();
3098
3099 checkQuotaError(data);
3100 checkModerationError(data);
3101
3102 if (data.error) {
3103 const message = data.error.message || response.statusText || t`Unknown error`;
3104 toastr.error(message, t`API returned an error`);
3105 throw new Error(message);
3106 }
3107
3108 if (type !== 'quiet') {
3109 const logprobs = parseChatCompletionLogprobs(data);
3110 // Delay is required to allow the active message to be updated to
3111 // the one we are generating (happens right after sendOpenAIRequest)
3112 delay(1).then(() => saveLogprobsForActiveMessage(logprobs, null));
3113 }
3114
3115 return data;
3116 }
3117}
3118
3119/**
3120 * Extracts the reply from the response data from a chat completions-like source
3121 * @param {object} data Response data from the chat completions-like source
3122 * @param {object} state Additional state to keep track of
3123 * @param {object} [options] Additional options
3124 * @param {string?} [options.chatCompletionSource] Chat completion source
3125 * @param {boolean?} [options.overrideShowThoughts] Override show thoughts
3126 * @returns {string} The reply extracted from the response data
3127 */
3128export function getStreamingReply(data, state, { chatCompletionSource = null, overrideShowThoughts = null } = {}) {
3129 const chat_completion_source = chatCompletionSource ?? oai_settings.chat_completion_source;
3130 const show_thoughts = overrideShowThoughts ?? oai_settings.show_thoughts;
3131
3132 if (chat_completion_source === chat_completion_sources.CLAUDE) {
3133 if (show_thoughts) {
3134 state.reasoning += data?.delta?.thinking || '';
3135 }
3136 return data?.delta?.text || '';
3137 } else if ([chat_completion_sources.MAKERSUITE, chat_completion_sources.VERTEXAI].includes(chat_completion_source)) {
3138 const inlineData = data?.candidates?.[0]?.content?.parts?.filter(x => x.inlineData && !x.thought)?.map(x => x.inlineData) || [];
3139 if (Array.isArray(inlineData) && inlineData.length > 0) {
3140 state.images.push(...inlineData.map(x => `data:${x.mimeType};base64,${x.data}`).filter(isDataURL));
3141 }
3142 if (show_thoughts) {
3143 state.reasoning += (data?.candidates?.[0]?.content?.parts?.filter(x => x.thought)?.map(x => x.text)?.[0] || '');
3144 }
3145 // Extract thought signatures from streaming chunks (typically in final chunk)
3146 const parts = data?.candidates?.[0]?.content?.parts || [];
3147 parts.forEach((part) => {
3148 if (part.thoughtSignature && typeof part.text === 'string') {
3149 state.signature = part.thoughtSignature;
3150 }
3151 });
3152 return data?.candidates?.[0]?.content?.parts?.filter(x => !x.thought)?.map(x => x.text)?.[0] || '';
3153 } else if (chat_completion_source === chat_completion_sources.COHERE) {
3154 return data?.delta?.message?.content?.text || data?.delta?.message?.tool_plan || '';
3155 } else if (chat_completion_source === chat_completion_sources.DEEPSEEK) {
3156 if (show_thoughts) {
3157 state.reasoning += (data.choices?.filter(x => x?.delta?.reasoning_content)?.[0]?.delta?.reasoning_content || '');
3158 }
3159 return data.choices?.[0]?.delta?.content || '';
3160 } else if (chat_completion_source === chat_completion_sources.XAI) {
3161 if (show_thoughts) {
3162 state.reasoning += (data.choices?.filter(x => x?.delta?.reasoning_content)?.[0]?.delta?.reasoning_content || '');
3163 }
3164 return data.choices?.[0]?.delta?.content || '';
3165 } else if (chat_completion_source === chat_completion_sources.OPENROUTER) {
3166 const imageUrls = data?.choices?.[0]?.delta?.images?.filter(x => x.type === 'image_url')?.map(x => x?.image_url?.url) || [];
3167 if (Array.isArray(imageUrls) && imageUrls.length > 0) {
3168 state.images.push(...imageUrls.filter(isDataURL));
3169 }
3170 if (show_thoughts) {
3171 state.reasoning +=
3172 data.choices?.filter(x => x?.delta?.reasoning)?.[0]?.delta?.reasoning ??
3173 data.choices?.filter(x => x?.delta?.reasoning_content)?.[0]?.delta?.reasoning_content ??
3174 data.choices?.filter(x => x?.message?.reasoning)?.[0]?.message?.reasoning ??
3175 data.choices?.filter(x => x?.message?.reasoning_content)?.[0]?.message?.reasoning_content ??
3176 '';
3177 }
3178 // Extract thought signatures from OpenRouter streaming.
3179 const reasoningDetails = [
3180 ...(data?.choices?.[0]?.delta?.reasoning_details || []),
3181 ...(data?.choices?.[0]?.message?.reasoning_details || []),
3182 ];
3183 reasoningDetails.forEach((detail) => {
3184 if (detail.type === 'reasoning.encrypted' && detail.data) {
3185 const isToolLikeId = typeof detail.id === 'string' && /^(tool_|call_)/.test(detail.id);
3186 if (typeof detail.id === 'string' && detail.id.length > 0) {
3187 state.toolSignatures[detail.id] = detail.data;
3188 }
3189 if (!isToolLikeId) {
3190 state.signature = detail.data;
3191 }
3192 }
3193 });
3194 return data.choices?.[0]?.delta?.content ?? data.choices?.[0]?.message?.content ?? data.choices?.[0]?.text ?? '';
3195 } else if ([chat_completion_sources.CUSTOM, chat_completion_sources.POLLINATIONS, chat_completion_sources.AIMLAPI, chat_completion_sources.MOONSHOT, chat_completion_sources.COMETAPI, chat_completion_sources.ELECTRONHUB, chat_completion_sources.NANOGPT, chat_completion_sources.ZAI, chat_completion_sources.SILICONFLOW, chat_completion_sources.CHUTES, chat_completion_sources.WORKERS_AI].includes(chat_completion_source)) {
3196 if (show_thoughts) {
3197 state.reasoning +=
3198 data.choices?.filter(x => x?.delta?.reasoning_content)?.[0]?.delta?.reasoning_content ??
3199 data.choices?.filter(x => x?.delta?.reasoning)?.[0]?.delta?.reasoning ??
3200 '';
3201 }
3202 return data.choices?.[0]?.delta?.content ?? data.choices?.[0]?.message?.content ?? data.choices?.[0]?.text ?? '';
3203 } else if (chat_completion_source === chat_completion_sources.MISTRALAI) {
3204 if (show_thoughts) {
3205 state.reasoning += (data.choices?.filter(x => x?.delta?.content?.[0]?.thinking)?.[0]?.delta?.content?.[0]?.thinking?.[0]?.text || '');
3206 }
3207 const content = data.choices?.[0]?.delta?.content ?? data.choices?.[0]?.message?.content ?? data.choices?.[0]?.text ?? '';
3208 return Array.isArray(content) ? content.map(x => x.text).filter(x => x).join('') : content;
3209 } else {
3210 return data.choices?.[0]?.delta?.content ?? data.choices?.[0]?.message?.content ?? data.choices?.[0]?.text ?? '';
3211 }
3212}
3213
3214/**
3215 * parseChatCompletionLogprobs converts the response data returned from a chat
3216 * completions-like source into an array of TokenLogprobs found in the response.
3217 * @param {Object} data - response data from a chat completions-like source
3218 * @returns {import('./logprobs.js').TokenLogprobs[] | null} converted logprobs
3219 */
3220function parseChatCompletionLogprobs(data) {
3221 if (!data) {
3222 return null;
3223 }
3224
3225 switch (oai_settings.chat_completion_source) {
3226 case chat_completion_sources.AIMLAPI:
3227 return Object.keys(data?.choices?.[0]?.logprobs ?? {}).includes('content')
3228 ? parseOpenAIChatLogprobs(data.choices[0]?.logprobs)
3229 : parseOpenAITextLogprobs(data.choices[0]?.logprobs);
3230 case chat_completion_sources.OPENAI:
3231 case chat_completion_sources.AZURE_OPENAI:
3232 case chat_completion_sources.DEEPSEEK:
3233 case chat_completion_sources.XAI:
3234 case chat_completion_sources.CUSTOM:
3235 case chat_completion_sources.CHUTES:
3236 if (!data.choices?.length) {
3237 return null;
3238 }
3239 // OpenAI Text Completion API is treated as a chat completion source
3240 // by SillyTavern, hence its presence in this function.
3241 return textCompletionModels.includes(getChatCompletionModel())
3242 ? parseOpenAITextLogprobs(data.choices[0]?.logprobs)
3243 : parseOpenAIChatLogprobs(data.choices[0]?.logprobs);
3244 default:
3245 // implement other chat completion sources here
3246 }
3247 return null;
3248}
3249
3250/**
3251 * parseOpenAIChatLogprobs receives a `logprobs` response from OpenAI's chat
3252 * completion API and converts into the structure used by the Token Probabilities
3253 * view.
3254 * @param {{content: { token: string, logprob: number, top_logprobs: { token: string, logprob: number }[] }[]}} logprobs
3255 * @returns {import('./logprobs.js').TokenLogprobs[] | null} converted logprobs
3256 */
3257function parseOpenAIChatLogprobs(logprobs) {
3258 const { content } = logprobs ?? {};
3259
3260 if (!Array.isArray(content)) {
3261 return null;
3262 }
3263
3264 /** @type {(x: { token: string, logprob: number }) => [string, number]} */
3265 const toTuple = (x) => [x.token, x.logprob];
3266
3267 return content.map(({ token, logprob, top_logprobs = [] }) => {
3268 // Add the chosen token to top_logprobs if it's not already there, then
3269 // convert to a list of [token, logprob] pairs
3270 const chosenTopToken = top_logprobs.some((top) => token === top.token);
3271 /** @type {import('./logprobs.js').Candidate[]} */
3272 const topLogprobs = chosenTopToken
3273 ? top_logprobs.map(toTuple)
3274 : [...top_logprobs.map(toTuple), [token, logprob]];
3275 return { token, topLogprobs };
3276 });
3277}
3278
3279/**
3280 * parseOpenAITextLogprobs receives a `logprobs` response from OpenAI's text
3281 * completion API and converts into the structure used by the Token Probabilities
3282 * view.
3283 * @param {{tokens: string[], token_logprobs: number[], top_logprobs: { token: string, logprob: number }[][]}} logprobs
3284 * @returns {import('./logprobs.js').TokenLogprobs[] | null} converted logprobs
3285 */
3286function parseOpenAITextLogprobs(logprobs) {
3287 const { tokens, token_logprobs, top_logprobs } = logprobs ?? {};
3288
3289 if (!Array.isArray(tokens)) {
3290 return null;
3291 }
3292
3293 return tokens.map((token, i) => {
3294 // Add the chosen token to top_logprobs if it's not already there, then
3295 // convert to a list of [token, logprob] pairs
3296 /** @type {any[]} */
3297 const topLogprobs = top_logprobs[i] ? Object.entries(top_logprobs[i]) : [];
3298 const chosenTopToken = topLogprobs.some(([topToken]) => token === topToken);
3299 if (!chosenTopToken) {
3300 topLogprobs.push([token, token_logprobs[i]]);
3301 }
3302 return { token, topLogprobs };
3303 });
3304}
3305
3306async function calculateLogitBias() {
3307 const body = JSON.stringify(oai_settings.bias_presets[oai_settings.bias_preset_selected]);
3308 let result = {};
3309
3310 try {
3311 const reply = await fetch(`/api/backends/chat-completions/bias?model=${getTokenizerModel()}`, {
3312 method: 'POST',
3313 headers: getRequestHeaders(),
3314 body,
3315 });
3316
3317 result = await reply.json();
3318 } catch (err) {
3319 result = {};
3320 console.error(err);
3321 }
3322 return result;
3323}
3324
3325class TokenHandler {
3326 /**
3327 * @param {(messages: object[] | object, full?: boolean) => Promise<number>} countTokenAsyncFn Function to count tokens
3328 */
3329 constructor(countTokenAsyncFn) {
3330 this.countTokenAsyncFn = countTokenAsyncFn;
3331 this.counts = {
3332 'start_chat': 0,
3333 'prompt': 0,
3334 'bias': 0,
3335 'nudge': 0,
3336 'jailbreak': 0,
3337 'impersonate': 0,
3338 'examples': 0,
3339 'conversation': 0,
3340 };
3341 }
3342
3343 getCounts() {
3344 return this.counts;
3345 }
3346
3347 resetCounts() {
3348 Object.keys(this.counts).forEach((key) => this.counts[key] = 0);
3349 }
3350
3351 setCounts(counts) {
3352 this.counts = counts;
3353 }
3354
3355 uncount(value, type) {
3356 this.counts[type] -= value;
3357 }
3358
3359 /**
3360 * Count tokens for a message or messages.
3361 * @param {object|any[]} messages Messages to count tokens for
3362 * @param {boolean} [full] Count full tokens
3363 * @param {string} [type] Identifier for the token count
3364 * @returns {Promise<number>} The token count
3365 */
3366 async countAsync(messages, full, type) {
3367 const token_count = await this.countTokenAsyncFn(messages, full);
3368 this.counts[type] += token_count;
3369
3370 return token_count;
3371 }
3372
3373 getTokensForIdentifier(identifier) {
3374 return this.counts[identifier] ?? 0;
3375 }
3376
3377 getTotal() {
3378 return Object.values(this.counts).reduce((a, b) => a + (isNaN(b) ? 0 : b), 0);
3379 }
3380
3381 log() {
3382 console.table({ ...this.counts, 'total': this.getTotal() });
3383 }
3384}
3385
3386
3387const tokenHandler = new TokenHandler(countTokensOpenAIAsync);
3388
3389// Thrown by ChatCompletion when a requested prompt couldn't be found.
3390class IdentifierNotFoundError extends Error {
3391 constructor(identifier) {
3392 super(`Identifier ${identifier} not found.`);
3393 this.name = 'IdentifierNotFoundError';
3394 }
3395}
3396
3397// Thrown by ChatCompletion when the token budget is unexpectedly exceeded
3398class TokenBudgetExceededError extends Error {
3399 constructor(identifier = '') {
3400 super(`Token budged exceeded. Message: ${identifier}`);
3401 this.name = 'TokenBudgetExceeded';
3402 }
3403}
3404
3405// Thrown when a character name is invalid
3406class InvalidCharacterNameError extends Error {
3407 constructor(identifier = '') {
3408 super(`Invalid character name. Message: ${identifier}`);
3409 this.name = 'InvalidCharacterName';
3410 }
3411}
3412
3413/**
3414 * Used for creating, managing, and interacting with a specific message object.
3415 */
3416class Message {
3417 static tokensPerImage = 85;
3418
3419 /** @type {number} */
3420 tokens;
3421 /** @type {string} */
3422 identifier;
3423 /** @type {string} */
3424 role;
3425 /** @type {string|any[]} */
3426 content;
3427 /** @type {string} */
3428 name;
3429 /** @type {object} */
3430 tool_call = null;
3431 /** @type {string?} */
3432 signature = null;
3433 /** @type {string?} */
3434 reasoning = null;
3435
3436 /**
3437 * @constructor
3438 * @param {string} role - The role of the entity creating the message.
3439 * @param {string} content - The actual content of the message.
3440 * @param {string} identifier - A unique identifier for the message.
3441 * @private Don't use this constructor directly. Use createAsync instead.
3442 */
3443 constructor(role, content, identifier) {
3444 this.identifier = identifier;
3445 this.role = role;
3446 this.content = content;
3447
3448 if (!this.role) {
3449 console.log(`Message role not set, defaulting to 'system' for identifier '${this.identifier}'`);
3450 this.role = 'system';
3451 }
3452
3453 this.tokens = 0;
3454 }
3455
3456 /**
3457 * Create a new Message instance.
3458 * @param {string} role
3459 * @param {string} content
3460 * @param {string} identifier
3461 * @returns {Promise<Message>} Message instance
3462 */
3463 static async createAsync(role, content, identifier) {
3464 const message = new Message(role, content, identifier);
3465
3466 if (typeof message.content === 'string' && message.content.length > 0) {
3467 message.tokens = await tokenHandler.countAsync({ role: message.role, content: message.content });
3468 }
3469
3470 return message;
3471 }
3472
3473 /**
3474 * Reconstruct the message from a tool invocation.
3475 * @param {import('./tool-calling.js').ToolInvocation[]} invocations - The tool invocations to reconstruct the message from.
3476 * @param {boolean} includeSignature Whether to include the signature in the tool calls.
3477 * @param {boolean} includeReasoning Whether to include plaintext reasoning fallback.
3478 * @returns {Promise<void>}
3479 */
3480 async setToolCalls(invocations, includeSignature, includeReasoning = false) {
3481 this.tool_calls = invocations.map(i => ({
3482 id: i.id,
3483 type: 'function',
3484 function: {
3485 arguments: i.parameters,
3486 name: i.name,
3487 },
3488 ...(includeSignature && i.signature ? { signature: i.signature } : {}),
3489 }));
3490 const fallbackReasoning = invocations.find(i => typeof i.reasoning === 'string' && i.reasoning.length > 0)?.reasoning || null;
3491 this.reasoning = includeReasoning ? fallbackReasoning : null;
3492 this.tokens = await tokenHandler.countAsync({
3493 role: this.role,
3494 tool_calls: JSON.stringify(this.tool_calls),
3495 ...(this.reasoning ? { reasoning: this.reasoning } : {}),
3496 });
3497 }
3498
3499 /**
3500 * Add a name to the message.
3501 * @param {string} name Name to set for the message.
3502 * @returns {Promise<void>}
3503 */
3504 async setName(name) {
3505 this.name = name;
3506 this.tokens = await tokenHandler.countAsync({ role: this.role, content: this.content, name: this.name });
3507 }
3508
3509 /**
3510 * Ensures the content is an array. If it's a string, converts it to an array with a single text object.
3511 * @returns {any[]} Content as an array
3512 */
3513 ensureContentIsArray() {
3514 const textContent = this.content;
3515 if (!Array.isArray(this.content)) {
3516 this.content = [];
3517 if (typeof textContent === 'string') {
3518 this.content.push({ type: 'text', text: textContent });
3519 }
3520 }
3521 return this.content;
3522 }
3523
3524 /**
3525 * Adds an image to the message.
3526 * @param {string} image Image URL or Data URL.
3527 * @returns {Promise<void>}
3528 */
3529 async addImage(image) {
3530 this.content = this.ensureContentIsArray();
3531 const isDataUrl = isDataURL(image);
3532 if (!isDataUrl) {
3533 try {
3534 const response = await fetch(image, { method: 'GET', cache: 'force-cache' });
3535 if (!response.ok) throw new Error('Failed to fetch image');
3536 const blob = await response.blob();
3537 image = await getBase64Async(blob);
3538 } catch (error) {
3539 console.error('Image adding skipped', error);
3540 return;
3541 }
3542 }
3543
3544 image = await this.compressImage(image);
3545
3546 const quality = oai_settings.inline_image_quality || default_settings.inline_image_quality;
3547 this.content.push({ type: 'image_url', image_url: { 'url': image, 'detail': quality } });
3548
3549 try {
3550 const tokens = await this.getImageTokenCost(image, quality);
3551 this.tokens += tokens;
3552 } catch (error) {
3553 this.tokens += Message.tokensPerImage;
3554 console.error('Failed to get image token cost', error);
3555 }
3556 }
3557
3558 /**
3559 * Adds a video to the message.
3560 * @param {string} video Video URL or Data URL.
3561 * @returns {Promise<void>}
3562 */
3563 async addVideo(video) {
3564 this.content = this.ensureContentIsArray();
3565 const isDataUrl = isDataURL(video);
3566 if (!isDataUrl) {
3567 try {
3568 const response = await fetch(video, { method: 'GET', cache: 'force-cache' });
3569 if (!response.ok) throw new Error('Failed to fetch video');
3570 const blob = await response.blob();
3571 video = await getBase64Async(blob);
3572 } catch (error) {
3573 console.error('Video adding skipped', error);
3574 return;
3575 }
3576 }
3577
3578 // Note: No compression for videos (unlike images)
3579 const quality = oai_settings.inline_image_quality || default_settings.inline_image_quality;
3580 this.content.push({ type: 'video_url', video_url: { 'url': video, 'detail': quality } });
3581
3582 try {
3583 // Using Gemini calculation (263 tokens per second)
3584 const duration = await getVideoDurationFromDataURL(video);
3585 this.tokens += 263 * Math.ceil(duration);
3586 } catch (error) {
3587 // Convservative estimate for video token cost without knowing duration
3588 this.tokens += 263 * 40; // ~40 second video (60 seconds max)
3589 console.error('Failed to get video token cost', error);
3590 }
3591 }
3592
3593 /**
3594 * Adds a audio to the message.
3595 * @param {string} audio Audio URL or Data URL.
3596 * @returns {Promise<void>}
3597 */
3598 async addAudio(audio) {
3599 this.content = this.ensureContentIsArray();
3600 const isDataUrl = isDataURL(audio);
3601 if (!isDataUrl) {
3602 try {
3603 const response = await fetch(audio, { method: 'GET', cache: 'force-cache' });
3604 if (!response.ok) throw new Error('Failed to fetch audio');
3605 const blob = await response.blob();
3606 audio = await getBase64Async(blob);
3607 } catch (error) {
3608 console.error('Audio adding skipped', error);
3609 return;
3610 }
3611 }
3612
3613 this.content.push({ type: 'audio_url', audio_url: { 'url': audio } });
3614
3615 try {
3616 // Using Gemini calculation (32 tokens per second)
3617 const duration = await getAudioDurationFromDataURL(audio);
3618 this.tokens += 32 * Math.ceil(duration);
3619 } catch (error) {
3620 // Estimate for audio token cost without knowing duration
3621 const tokens = 32 * 300; // ~5 minute audio
3622 this.tokens += tokens;
3623 console.error('Failed to get audio token cost', error);
3624 }
3625 }
3626
3627 /**
3628 * Compress an image if it exceeds the size threshold for the current chat completion source.
3629 * @param {string} image Data URL of the image.
3630 * @returns {Promise<string>} Compressed image as a Data URL.
3631 */
3632 async compressImage(image) {
3633 const compressImageSources = [
3634 chat_completion_sources.OPENROUTER,
3635 chat_completion_sources.MAKERSUITE,
3636 chat_completion_sources.MISTRALAI,
3637 chat_completion_sources.VERTEXAI,
3638 ];
3639 const sizeThreshold = 2 * 1024 * 1024;
3640 const dataSize = image.length * 0.75;
3641 const safeMimeTypes = ['image/jpeg', 'image/png', 'image/webp'];
3642 const mimeType = image?.split(';')?.[0]?.split(':')?.[1];
3643 if (compressImageSources.includes(oai_settings.chat_completion_source) && dataSize > sizeThreshold) {
3644 const maxSide = 2048;
3645 image = await createThumbnail(image, maxSide, maxSide);
3646 } else if (!safeMimeTypes.includes(mimeType)) {
3647 image = await createThumbnail(image, null, null);
3648 }
3649 return image;
3650 }
3651
3652 /**
3653 * Get the token cost of an image.
3654 * @param {string} dataUrl Data URL of the image.
3655 * @param {string} quality String representing the quality of the image. Can be 'low', 'auto', or 'high'.
3656 * @returns {Promise<number>} The token cost of the image.
3657 */
3658 async getImageTokenCost(dataUrl, quality) {
3659 if (quality === 'low') {
3660 return Message.tokensPerImage;
3661 }
3662
3663 const size = await getImageSizeFromDataURL(dataUrl);
3664
3665 // If the image is small enough, we can use the low quality token cost
3666 if (quality === 'auto' && size.width <= 512 && size.height <= 512) {
3667 return Message.tokensPerImage;
3668 }
3669
3670 /*
3671 * Images are first scaled to fit within a 2048 x 2048 square, maintaining their aspect ratio.
3672 * Then, they are scaled such that the shortest side of the image is 768px long.
3673 * Finally, we count how many 512px squares the image consists of.
3674 * Each of those squares costs 170 tokens. Another 85 tokens are always added to the final total.
3675 * https://platform.openai.com/docs/guides/vision/calculating-costs
3676 */
3677
3678 const scale = 2048 / Math.min(size.width, size.height);
3679 const scaledWidth = Math.round(size.width * scale);
3680 const scaledHeight = Math.round(size.height * scale);
3681
3682 const finalScale = 768 / Math.min(scaledWidth, scaledHeight);
3683 const finalWidth = Math.round(scaledWidth * finalScale);
3684 const finalHeight = Math.round(scaledHeight * finalScale);
3685
3686 const squares = Math.ceil(finalWidth / 512) * Math.ceil(finalHeight / 512);
3687 const tokens = squares * 170 + 85;
3688 return tokens;
3689 }
3690
3691 /**
3692 * Create a new Message instance from a prompt asynchronously.
3693 * @static
3694 * @param {Object} prompt - The prompt object.
3695 * @returns {Promise<Message>} A new instance of Message.
3696 */
3697 static fromPromptAsync(prompt) {
3698 return Message.createAsync(prompt.role, prompt.content, prompt.identifier);
3699 }
3700
3701 /**
3702 * Returns the number of tokens in the message.
3703 * @returns {number} Number of tokens in the message.
3704 */
3705 getTokens() { return this.tokens; }
3706}
3707
3708/**
3709 * Used for creating, managing, and interacting with a collection of Message instances.
3710 *
3711 * @class MessageCollection
3712 */
3713class MessageCollection {
3714 collection = [];
3715 identifier;
3716
3717 /**
3718 * @constructor
3719 * @param {string} identifier - A unique identifier for the MessageCollection.
3720 * @param {...Object} items - An array of Message or MessageCollection instances to be added to the collection.
3721 */
3722 constructor(identifier, ...items) {
3723 for (let item of items) {
3724 if (!(item instanceof Message || item instanceof MessageCollection)) {
3725 throw new Error('Only Message and MessageCollection instances can be added to MessageCollection');
3726 }
3727 }
3728
3729 this.collection.push(...items);
3730 this.identifier = identifier;
3731 }
3732
3733 /**
3734 * Get chat in the format of {role, name, content, tool_calls}.
3735 * @returns {Array} Array of objects with role, name, and content properties.
3736 */
3737 getChat() {
3738 return this.collection.reduce((acc, message) => {
3739 if (message.content || message.tool_calls) {
3740 acc.push({
3741 role: message.role,
3742 content: message.content,
3743 ...(message.name && { name: message.name }),
3744 ...(message.tool_calls && { tool_calls: message.tool_calls }),
3745 ...(message.role === 'tool' && { tool_call_id: message.identifier }),
3746 ...(message.signature && { signature: message.signature }),
3747 ...(message.reasoning && { reasoning: message.reasoning }),
3748 });
3749 }
3750 return acc;
3751 }, []);
3752 }
3753
3754 /**
3755 * Method to get the collection of messages.
3756 * @returns {Array} The collection of Message instances.
3757 */
3758 getCollection() {
3759 return this.collection;
3760 }
3761
3762 /**
3763 * Add a new item to the collection.
3764 * @param {Object} item - The Message or MessageCollection instance to be added.
3765 */
3766 add(item) {
3767 this.collection.push(item);
3768 }
3769
3770 /**
3771 * Get an item from the collection by its identifier.
3772 * @param {string} identifier - The identifier of the item to be found.
3773 * @returns {Object} The found item, or undefined if no item was found.
3774 */
3775 getItemByIdentifier(identifier) {
3776 return this.collection.find(item => item?.identifier === identifier);
3777 }
3778
3779 /**
3780 * Check if an item with the given identifier exists in the collection.
3781 * @param {string} identifier - The identifier to check.
3782 * @returns {boolean} True if an item with the given identifier exists, false otherwise.
3783 */
3784 hasItemWithIdentifier(identifier) {
3785 return this.collection.some(message => message.identifier === identifier);
3786 }
3787
3788 /**
3789 * Get the total number of tokens in the collection.
3790 * @returns {number} The total number of tokens.
3791 */
3792 getTokens() {
3793 return this.collection.reduce((tokens, message) => tokens + message.getTokens(), 0);
3794 }
3795
3796 /**
3797 * Combines message collections into a single collection.
3798 * @returns {Message[]} The collection of messages flattened into a single array.
3799 */
3800 flatten() {
3801 return this.collection.reduce((acc, message) => {
3802 if (message instanceof MessageCollection) {
3803 acc.push(...message.flatten());
3804 } else {
3805 acc.push(message);
3806 }
3807 return acc;
3808 }, []);
3809 }
3810}
3811
3812/**
3813 * OpenAI API chat completion representation
3814 * const map = [{identifier: 'example', message: {role: 'system', content: 'exampleContent'}}, ...];
3815 *
3816 * This class creates a chat context that can be sent to Open AI's api
3817 * Includes message management and token budgeting.
3818 *
3819 * @see https://platform.openai.com/docs/guides/gpt/chat-completions-api
3820 *
3821 */
3822export class ChatCompletion {
3823 /**
3824 * Combines consecutive system messages into one if they have no name attached.
3825 * @returns {Promise<void>}
3826 */
3827 async squashSystemMessages() {
3828 const excludeList = ['newMainChat', 'newChat', 'groupNudge'];
3829 this.messages.collection = this.messages.flatten();
3830
3831 let lastMessage = null;
3832 let squashedMessages = [];
3833
3834 for (let message of this.messages.collection) {
3835 // Force exclude empty messages
3836 if (message.role === 'system' && !message.content) {
3837 continue;
3838 }
3839
3840 const shouldSquash = (message) => {
3841 return !excludeList.includes(message.identifier) && message.role === 'system' && !message.name;
3842 };
3843
3844 if (shouldSquash(message)) {
3845 if (lastMessage && shouldSquash(lastMessage)) {
3846 lastMessage.content += '\n' + message.content;
3847 lastMessage.tokens = await tokenHandler.countAsync({ role: lastMessage.role, content: lastMessage.content });
3848 } else {
3849 squashedMessages.push(message);
3850 lastMessage = message;
3851 }
3852 } else {
3853 squashedMessages.push(message);
3854 lastMessage = message;
3855 }
3856 }
3857
3858 this.messages.collection = squashedMessages;
3859 }
3860
3861 /**
3862 * Initializes a new instance of ChatCompletion.
3863 * Sets up the initial token budget and a new message collection.
3864 */
3865 constructor() {
3866 this.tokenBudget = 0;
3867 this.messages = new MessageCollection('root');
3868 this.loggingEnabled = false;
3869 this.overriddenPrompts = [];
3870 }
3871
3872 /**
3873 * Retrieves all messages.
3874 *
3875 * @returns {MessageCollection} The MessageCollection instance holding all messages.
3876 */
3877 getMessages() {
3878 return this.messages;
3879 }
3880
3881 /**
3882 * Calculates and sets the token budget based on context and response.
3883 *
3884 * @param {number} context - Number of tokens in the context.
3885 * @param {number} response - Number of tokens in the response.
3886 */
3887 setTokenBudget(context, response) {
3888 this.log(`Prompt tokens: ${context}`);
3889 this.log(`Completion tokens: ${response}`);
3890
3891 this.tokenBudget = context - response;
3892
3893 this.log(`Token budget: ${this.tokenBudget}`);
3894 }
3895
3896 /**
3897 * Adds a message or message collection to the collection.
3898 *
3899 * @param {Message|MessageCollection} collection - The message or message collection to add.
3900 * @param {number|null} position - The position at which to add the collection.
3901 * @returns {ChatCompletion} The current instance for chaining.
3902 */
3903 add(collection, position = null) {
3904 this.validateMessageCollection(collection);
3905 this.checkTokenBudget(collection, collection.identifier);
3906
3907 if (null !== position && -1 !== position) {
3908 this.messages.collection[position] = collection;
3909 } else {
3910 this.messages.collection.push(collection);
3911 }
3912
3913 this.decreaseTokenBudgetBy(collection.getTokens());
3914
3915 this.log(`Added ${collection.identifier}. Remaining tokens: ${this.tokenBudget}`);
3916
3917 return this;
3918 }
3919
3920 /**
3921 * Inserts a message at the start of the specified collection.
3922 *
3923 * @param {Message} message - The message to insert.
3924 * @param {string} identifier - The identifier of the collection where to insert the message.
3925 */
3926 insertAtStart(message, identifier) {
3927 this.insert(message, identifier, 'start');
3928 }
3929
3930 /**
3931 * Inserts a message at the end of the specified collection.
3932 *
3933 * @param {Message} message - The message to insert.
3934 * @param {string} identifier - The identifier of the collection where to insert the message.
3935 */
3936 insertAtEnd(message, identifier) {
3937 this.insert(message, identifier, 'end');
3938 }
3939
3940 /**
3941 * Inserts a message at the specified position in the specified collection.
3942 *
3943 * @param {Message} message - The message to insert.
3944 * @param {string} identifier - The identifier of the collection where to insert the message.
3945 * @param {string|number} position - The position at which to insert the message ('start' or 'end').
3946 */
3947 insert(message, identifier, position = 'end') {
3948 this.validateMessage(message);
3949 this.checkTokenBudget(message, message.identifier);
3950
3951 const index = this.findMessageIndex(identifier);
3952 if (message.content || message.tool_calls) {
3953 if ('start' === position) this.messages.collection[index].collection.unshift(message);
3954 else if ('end' === position) this.messages.collection[index].collection.push(message);
3955 else if (typeof position === 'number') this.messages.collection[index].collection.splice(position, 0, message);
3956
3957 this.decreaseTokenBudgetBy(message.getTokens());
3958
3959 this.log(`Inserted ${message.identifier} into ${identifier}. Remaining tokens: ${this.tokenBudget}`);
3960 }
3961 }
3962
3963 /**
3964 * Remove the last item of the collection
3965 *
3966 * @param identifier
3967 */
3968 removeLastFrom(identifier) {
3969 const index = this.findMessageIndex(identifier);
3970 const message = this.messages.collection[index].collection.pop();
3971
3972 if (!message) {
3973 this.log(`No message to remove from ${identifier}`);
3974 return;
3975 }
3976
3977 this.increaseTokenBudgetBy(message.getTokens());
3978
3979 this.log(`Removed ${message.identifier} from ${identifier}. Remaining tokens: ${this.tokenBudget}`);
3980 }
3981
3982 /**
3983 * Checks if the token budget can afford the tokens of the specified message.
3984 *
3985 * @param {Message|MessageCollection} message - The message to check for affordability.
3986 * @returns {boolean} True if the budget can afford the message, false otherwise.
3987 */
3988 canAfford(message) {
3989 return 0 <= this.tokenBudget - message.getTokens();
3990 }
3991
3992 /**
3993 * Checks if the token budget can afford the tokens of all the specified messages.
3994 * @param {Message[]} messages - The messages to check for affordability.
3995 * @returns {boolean} True if the budget can afford all the messages, false otherwise.
3996 */
3997 canAffordAll(messages) {
3998 return 0 <= this.tokenBudget - messages.reduce((total, message) => total + message.getTokens(), 0);
3999 }
4000
4001 /**
4002 * Checks if a message with the specified identifier exists in the collection.
4003 *
4004 * @param {string} identifier - The identifier to check for existence.
4005 * @returns {boolean} True if a message with the specified identifier exists, false otherwise.
4006 */
4007 has(identifier) {
4008 return this.messages.hasItemWithIdentifier(identifier);
4009 }
4010
4011 /**
4012 * Retrieves the total number of tokens in the collection.
4013 *
4014 * @returns {number} The total number of tokens.
4015 */
4016 getTotalTokenCount() {
4017 return this.messages.getTokens();
4018 }
4019
4020 /**
4021 * Retrieves the chat as a flattened array of messages.
4022 *
4023 * @returns {Array} The chat messages.
4024 */
4025 getChat() {
4026 const chat = [];
4027 for (let item of this.messages.collection) {
4028 if (item instanceof MessageCollection) {
4029 chat.push(...item.getChat());
4030 } else if (item instanceof Message && (item.content || item.tool_calls)) {
4031 const message = {
4032 role: item.role,
4033 content: item.content,
4034 ...(item.name ? { name: item.name } : {}),
4035 ...(item.tool_calls ? { tool_calls: item.tool_calls } : {}),
4036 ...(item.role === 'tool' ? { tool_call_id: item.identifier } : {}),
4037 ...(item.signature ? { signature: item.signature } : {}),
4038 ...(item.reasoning ? { reasoning: item.reasoning } : {}),
4039 };
4040 chat.push(message);
4041 } else {
4042 this.log(`Skipping invalid or empty message in collection: ${JSON.stringify(item)}`);
4043 }
4044 }
4045 return chat;
4046 }
4047
4048 /**
4049 * Logs an output message to the console if logging is enabled.
4050 *
4051 * @param {string} output - The output message to log.
4052 */
4053 log(output) {
4054 if (this.loggingEnabled) console.log('[ChatCompletion] ' + output);
4055 }
4056
4057 /**
4058 * Enables logging of output messages to the console.
4059 */
4060 enableLogging() {
4061 this.loggingEnabled = true;
4062 }
4063
4064 /**
4065 * Disables logging of output messages to the console.
4066 */
4067 disableLogging() {
4068 this.loggingEnabled = false;
4069 }
4070
4071 /**
4072 * Validates if the given argument is an instance of MessageCollection.
4073 * Throws an error if the validation fails.
4074 *
4075 * @param {MessageCollection|Message} collection - The collection to validate.
4076 */
4077 validateMessageCollection(collection) {
4078 if (!(collection instanceof MessageCollection)) {
4079 console.log(collection);
4080 throw new Error('Argument must be an instance of MessageCollection');
4081 }
4082 }
4083
4084 /**
4085 * Validates if the given argument is an instance of Message.
4086 * Throws an error if the validation fails.
4087 *
4088 * @param {Message} message - The message to validate.
4089 */
4090 validateMessage(message) {
4091 if (!(message instanceof Message)) {
4092 console.log(message);
4093 throw new Error('Argument must be an instance of Message');
4094 }
4095 }
4096
4097 /**
4098 * Checks if the token budget can afford the tokens of the given message.
4099 * Throws an error if the budget can't afford the message.
4100 *
4101 * @param {Message|MessageCollection} message - The message to check.
4102 * @param {string} identifier - The identifier of the message.
4103 */
4104 checkTokenBudget(message, identifier) {
4105 if (!this.canAfford(message)) {
4106 throw new TokenBudgetExceededError(identifier);
4107 }
4108 }
4109
4110 /**
4111 * Reserves the tokens required by the given message from the token budget.
4112 *
4113 * @param {Message|MessageCollection|number} message - The message whose tokens to reserve.
4114 */
4115 reserveBudget(message) {
4116 const tokens = typeof message === 'number' ? message : message.getTokens();
4117 this.decreaseTokenBudgetBy(tokens);
4118 }
4119
4120 /**
4121 * Frees up the tokens used by the given message from the token budget.
4122 *
4123 * @param {Message|MessageCollection} message - The message whose tokens to free.
4124 */
4125 freeBudget(message) { this.increaseTokenBudgetBy(message.getTokens()); }
4126
4127 /**
4128 * Increases the token budget by the given number of tokens.
4129 * This function should be used sparingly, per design the completion should be able to work with its initial budget.
4130 *
4131 * @param {number} tokens - The number of tokens to increase the budget by.
4132 */
4133 increaseTokenBudgetBy(tokens) {
4134 this.tokenBudget += tokens;
4135 }
4136
4137 /**
4138 * Decreases the token budget by the given number of tokens.
4139 * This function should be used sparingly, per design the completion should be able to work with its initial budget.
4140 *
4141 * @param {number} tokens - The number of tokens to decrease the budget by.
4142 */
4143 decreaseTokenBudgetBy(tokens) {
4144 this.tokenBudget -= tokens;
4145 }
4146
4147 /**
4148 * Finds the index of a message in the collection by its identifier.
4149 * Throws an error if a message with the given identifier is not found.
4150 *
4151 * @param {string} identifier - The identifier of the message to find.
4152 * @returns {number} The index of the message in the collection.
4153 */
4154 findMessageIndex(identifier) {
4155 const index = this.messages.collection.findIndex(item => item?.identifier === identifier);
4156 if (index < 0) {
4157 throw new IdentifierNotFoundError(identifier);
4158 }
4159 return index;
4160 }
4161
4162 /**
4163 * Sets the list of overridden prompts.
4164 * @param {string[]} list A list of prompts that were overridden.
4165 */
4166 setOverriddenPrompts(list) {
4167 this.overriddenPrompts = list;
4168 }
4169
4170 getOverriddenPrompts() {
4171 return this.overriddenPrompts ?? [];
4172 }
4173}
4174
4175/**
4176 * Migrate old Chat Completion settings to new format.
4177 * @param {ChatCompletionSettings} settings Settings to migrate
4178 */
4179function migrateChatCompletionSettings(settings) {
4180 const migrateMap = [
4181 { oldKey: 'names_in_completion', oldValue: true, newKey: 'names_behavior', newValue: character_names_behavior.COMPLETION },
4182 { oldKey: 'chat_completion_source', oldValue: 'palm', newKey: 'chat_completion_source', newValue: chat_completion_sources.MAKERSUITE },
4183 { oldKey: 'custom_prompt_post_processing', oldValue: custom_prompt_post_processing_types.CLAUDE, newKey: 'custom_prompt_post_processing', newValue: custom_prompt_post_processing_types.MERGE },
4184 { oldKey: 'ai21_model', oldValue: /^j2-/, newKey: 'ai21_model', newValue: 'jamba-large' },
4185 { oldKey: 'image_inlining', oldValue: false, newKey: 'media_inlining', newValue: false },
4186 { oldKey: 'image_inlining', oldValue: true, newKey: 'media_inlining', newValue: true },
4187 { oldKey: 'video_inlining', oldValue: true, newKey: 'media_inlining', newValue: true },
4188 { oldKey: 'audio_inlining', oldValue: true, newKey: 'media_inlining', newValue: true },
4189 { oldKey: 'claude_use_sysprompt', oldValue: true, newKey: 'use_sysprompt', newValue: true },
4190 { oldKey: 'use_makersuite_sysprompt', oldValue: true, newKey: 'use_sysprompt', newValue: true },
4191 { oldKey: 'mistralai_model', oldValue: /^(mistral-medium|mistral-small)$/, newKey: 'mistralai_model', newValue: (settings.mistralai_model + '-latest') },
4192 { oldKey: 'deepseek_model', oldValue: /^deepseek-(chat|reasoner|coder)$/, newKey: 'deepseek_model', newValue: 'deepseek-v4-flash' },
4193 { oldKey: 'openrouter_sort_models', oldValue: 'alphabetically', newKey: 'sort_models', newValue: 'alphabetically' },
4194 { oldKey: 'openrouter_sort_models', oldValue: 'pricing.prompt', newKey: 'sort_models', newValue: 'pricing.prompt' },
4195 { oldKey: 'openrouter_sort_models', oldValue: 'context_length', newKey: 'sort_models', newValue: 'context_length' },
4196 { oldKey: 'openrouter_group_models', oldValue: true, newKey: 'group_models', newValue: true },
4197 ];
4198
4199 for (const migration of migrateMap) {
4200 if (Object.hasOwn(settings, migration.oldKey)) {
4201 const shouldMigrate = migration.oldValue instanceof RegExp
4202 ? migration.oldValue.test(settings[migration.oldKey])
4203 : settings[migration.oldKey] === migration.oldValue;
4204 if (shouldMigrate) {
4205 settings[migration.newKey] = migration.newValue;
4206 }
4207 if (migration.oldKey !== migration.newKey) {
4208 delete settings[migration.oldKey];
4209 }
4210 }
4211 }
4212}
4213
4214/**
4215 * Load OpenAI settings from backend data
4216 * @param {any} data Settings data from backend
4217 * @param {ChatCompletionSettings} settings Saved settings from backend
4218 */
4219function loadOpenAISettings(data, settings) {
4220 openai_setting_names = data.openai_setting_names;
4221 openai_settings = data.openai_settings;
4222 openai_settings.forEach(function (item, i) {
4223 openai_settings[i] = JSON.parse(item);
4224 });
4225
4226 $('#settings_preset_openai').empty();
4227 const settingNames = {};
4228 openai_setting_names.forEach(function (item, i) {
4229 settingNames[item] = i;
4230 const option = document.createElement('option');
4231 option.value = i;
4232 option.text = item;
4233 $('#settings_preset_openai').append(option);
4234 });
4235 openai_setting_names = settingNames;
4236
4237 migrateChatCompletionSettings(settings);
4238
4239 for (const key of Object.keys(default_settings)) {
4240 oai_settings[key] = settings[key] ?? default_settings[key];
4241 const settingToUpdate = Object.values(settingsToUpdate).find(([_, k]) => k === key);
4242 if (settingToUpdate) {
4243 const [selector] = settingToUpdate;
4244 const $element = $(selector);
4245
4246 if ($element.length === 0) {
4247 continue;
4248 }
4249
4250 if ($element.is('input[type="checkbox"]')) {
4251 $element.prop('checked', oai_settings[key]);
4252 } else if ($element.is('select')) {
4253 $element.val(oai_settings[key]);
4254 $element.find(`option[value="${CSS.escape(oai_settings[key])}"]`).prop('selected', true);
4255 } else {
4256 $element.val(oai_settings[key]);
4257 if ($element.is('input[type="range"]')) {
4258 const id = $element.attr('id');
4259 const $counter = $(`input[type="number"][data-for="${id}"]`);
4260 if ($counter.length > 0) {
4261 $counter.val(Number(oai_settings[key]));
4262 }
4263 }
4264 }
4265 }
4266 }
4267
4268 $(`#settings_preset_openai option[value="${openai_setting_names[oai_settings.preset_settings_openai]}"]`).prop('selected', true);
4269 $('#bind_preset_to_connection').prop('checked', oai_settings.bind_preset_to_connection);
4270 $('#openai_external_category').toggle(oai_settings.show_external_models);
4271 $('.reverse_proxy_warning').toggle(oai_settings.reverse_proxy !== '');
4272
4273 // Don't display Service Account JSON in textarea - it's stored in backend secrets
4274 $('#vertexai_service_account_json').val('');
4275 updateVertexAIServiceAccountStatus();
4276
4277 $('#openai_logit_bias_preset').empty();
4278 for (const preset of Object.keys(oai_settings.bias_presets)) {
4279 // Backfill missing IDs
4280 if (Array.isArray(oai_settings.bias_presets[preset])) {
4281 oai_settings.bias_presets[preset].forEach((bias) => {
4282 if (bias && !bias.id) {
4283 bias.id = uuidv4();
4284 }
4285 });
4286 }
4287 const option = document.createElement('option');
4288 option.innerText = preset;
4289 option.value = preset;
4290 option.selected = preset === oai_settings.bias_preset_selected;
4291 $('#openai_logit_bias_preset').append(option);
4292 }
4293 $('#openai_logit_bias_preset').trigger('change');
4294
4295 setNamesBehaviorControls();
4296 setContinuePostfixControls();
4297 setToolReasoningControls();
4298 ToolManager.RECURSE_LIMIT = oai_settings.tool_call_recurse_limit;
4299
4300 $('#openrouter_providers_chat').trigger('change');
4301 $('#openrouter_quantizations_chat').trigger('change');
4302 $('#nanogpt_provider').trigger('change');
4303 $('#chat_completion_source').trigger('change');
4304}
4305
4306function setNamesBehaviorControls() {
4307 switch (oai_settings.names_behavior) {
4308 case character_names_behavior.NONE:
4309 $('#character_names_none').prop('checked', true);
4310 break;
4311 case character_names_behavior.DEFAULT:
4312 $('#character_names_default').prop('checked', true);
4313 break;
4314 case character_names_behavior.COMPLETION:
4315 $('#character_names_completion').prop('checked', true);
4316 break;
4317 case character_names_behavior.CONTENT:
4318 $('#character_names_content').prop('checked', true);
4319 break;
4320 }
4321
4322 const checkedItemText = $('input[name="character_names"]:checked ~ span').text().trim();
4323 $('#character_names_display').text(checkedItemText);
4324}
4325
4326function setContinuePostfixControls() {
4327 switch (oai_settings.continue_postfix) {
4328 case continue_postfix_types.NONE:
4329 $('#continue_postfix_none').prop('checked', true);
4330 break;
4331 case continue_postfix_types.SPACE:
4332 $('#continue_postfix_space').prop('checked', true);
4333 break;
4334 case continue_postfix_types.NEWLINE:
4335 $('#continue_postfix_newline').prop('checked', true);
4336 break;
4337 case continue_postfix_types.DOUBLE_NEWLINE:
4338 $('#continue_postfix_double_newline').prop('checked', true);
4339 break;
4340 default:
4341 // Prevent preset value abuse
4342 oai_settings.continue_postfix = continue_postfix_types.SPACE;
4343 $('#continue_postfix_space').prop('checked', true);
4344 break;
4345 }
4346
4347 $('#continue_postfix').val(oai_settings.continue_postfix);
4348 const checkedItemText = $('input[name="continue_postfix"]:checked ~ span').text().trim();
4349 $('#continue_postfix_display').text(checkedItemText);
4350}
4351
4352function setToolReasoningControls() {
4353 const isEnabled = oai_settings.show_thoughts;
4354 $('#tool_reasoning_mode').prop('disabled', !isEnabled);
4355 $('#openrouter_interleaved_thinking_disabled_hint').toggle(!isEnabled);
4356}
4357
4358async function getStatusOpen() {
4359 const noValidateSources = [
4360 chat_completion_sources.CLAUDE,
4361 chat_completion_sources.AI21,
4362 chat_completion_sources.VERTEXAI,
4363 chat_completion_sources.PERPLEXITY,
4364 chat_completion_sources.ZAI,
4365 chat_completion_sources.MINIMAX,
4366 ];
4367 if (noValidateSources.includes(oai_settings.chat_completion_source)) {
4368 let status = t`Key saved; press \"Test Message\" to verify.`;
4369 setOnlineStatus(status);
4370 updateFeatureSupportFlags();
4371 return resultCheckStatus();
4372 }
4373
4374 if (oai_settings.chat_completion_source === chat_completion_sources.CUSTOM && !isValidUrl(oai_settings.custom_url)) {
4375 console.debug('Invalid endpoint URL of Custom OpenAI API:', oai_settings.custom_url);
4376 setOnlineStatus(t`Invalid endpoint URL. Requests may fail.`);
4377 return resultCheckStatus();
4378 }
4379
4380 if (oai_settings.chat_completion_source === chat_completion_sources.AZURE_OPENAI && !isValidUrl(oai_settings.azure_base_url)) {
4381 console.debug('Invalid endpoint URL of Azure OpenAI API:', oai_settings.azure_base_url);
4382 setOnlineStatus(t`Invalid Azure endpoint URL. Requests may fail.`);
4383 return resultCheckStatus();
4384 }
4385
4386 let data = {
4387 reverse_proxy: oai_settings.reverse_proxy,
4388 proxy_password: oai_settings.proxy_password,
4389 chat_completion_source: oai_settings.chat_completion_source,
4390 };
4391
4392 const validateProxySources = [
4393 chat_completion_sources.CLAUDE,
4394 chat_completion_sources.OPENAI,
4395 chat_completion_sources.MISTRALAI,
4396 chat_completion_sources.MAKERSUITE,
4397 chat_completion_sources.VERTEXAI,
4398 chat_completion_sources.DEEPSEEK,
4399 chat_completion_sources.XAI,
4400 chat_completion_sources.ZAI,
4401 chat_completion_sources.MOONSHOT,
4402 ];
4403 if (oai_settings.reverse_proxy && validateProxySources.includes(oai_settings.chat_completion_source)) {
4404 await validateReverseProxy();
4405 }
4406
4407 if (oai_settings.chat_completion_source === chat_completion_sources.CUSTOM) {
4408 $('.model_custom_select').empty();
4409 data.custom_url = oai_settings.custom_url;
4410 data.custom_include_headers = oai_settings.custom_include_headers;
4411 }
4412
4413 if (oai_settings.chat_completion_source === chat_completion_sources.AZURE_OPENAI) {
4414 data.azure_base_url = oai_settings.azure_base_url;
4415 data.azure_deployment_name = oai_settings.azure_deployment_name;
4416 data.azure_api_version = oai_settings.azure_api_version;
4417 }
4418
4419 if (oai_settings.chat_completion_source === chat_completion_sources.SILICONFLOW) {
4420 data.siliconflow_endpoint = oai_settings.siliconflow_endpoint;
4421 }
4422
4423 if (oai_settings.chat_completion_source === chat_completion_sources.MINIMAX) {
4424 data.minimax_endpoint = oai_settings.minimax_endpoint;
4425 }
4426
4427 if (oai_settings.chat_completion_source === chat_completion_sources.WORKERS_AI) {
4428 data.workers_ai_account_id = oai_settings.workers_ai_account_id;
4429 }
4430
4431 const canBypass = (oai_settings.chat_completion_source === chat_completion_sources.OPENAI && oai_settings.bypass_status_check) || oai_settings.chat_completion_source === chat_completion_sources.CUSTOM;
4432 if (canBypass) {
4433 setOnlineStatus(t`Status check bypassed`);
4434 }
4435
4436 try {
4437 const response = await fetch('/api/backends/chat-completions/status', {
4438 method: 'POST',
4439 headers: getRequestHeaders(),
4440 body: JSON.stringify(data),
4441 signal: abortStatusCheck.signal,
4442 cache: 'no-cache',
4443 });
4444
4445 if (!response.ok) {
4446 throw new Error(response.statusText);
4447 }
4448
4449 const responseData = await response.json();
4450
4451 if ('data' in responseData && Array.isArray(responseData.data)) {
4452 saveModelList(responseData.data);
4453 }
4454 if (!('error' in responseData)) {
4455 setOnlineStatus(t`Valid`);
4456 }
4457 if (responseData.bypass) {
4458 setOnlineStatus(t`Status check bypassed`);
4459 }
4460 } catch (error) {
4461 console.error(error);
4462
4463 if (!canBypass) {
4464 setOnlineStatus('no_connection');
4465 }
4466 }
4467
4468 updateFeatureSupportFlags();
4469 return resultCheckStatus();
4470}
4471
4472/**
4473 * Get OpenAI preset body from settings
4474 * @param {ChatCompletionSettings} settings The settings object
4475 * @returns {Object} The preset body object
4476 */
4477export function getChatCompletionPreset(settings = oai_settings) {
4478 const presetBody = {};
4479 for (const [presetKey, [, settingsKey]] of Object.entries(settingsToUpdate)) {
4480 presetBody[presetKey] = settings[settingsKey];
4481 }
4482 return structuredClone(presetBody);
4483}
4484
4485/**
4486 * Persist a settings preset with the given name
4487 *
4488 * @param {string} name - Name of the preset
4489 * @param {ChatCompletionSettings} settings The settings object
4490 * @param {boolean} triggerUi Whether the change event of preset UI element should be emitted
4491 * @returns {Promise<void>}
4492 */
4493async function saveOpenAIPreset(name, settings, triggerUi = true) {
4494 const presetBody = getChatCompletionPreset(settings);
4495 const savePresetSettings = await fetch('/api/presets/save', {
4496 method: 'POST',
4497 headers: getRequestHeaders(),
4498 body: JSON.stringify({
4499 apiId: 'openai',
4500 name: name,
4501 preset: presetBody,
4502 }),
4503 });
4504
4505 if (savePresetSettings.ok) {
4506 const data = await savePresetSettings.json();
4507
4508 if (Object.keys(openai_setting_names).includes(data.name)) {
4509 oai_settings.preset_settings_openai = data.name;
4510 const value = openai_setting_names[data.name];
4511 Object.assign(openai_settings[value], presetBody);
4512 $(`#settings_preset_openai option[value="${value}"]`).prop('selected', true);
4513 if (triggerUi) $('#settings_preset_openai').trigger('change');
4514 } else {
4515 openai_settings.push(presetBody);
4516 openai_setting_names[data.name] = openai_settings.length - 1;
4517 const option = document.createElement('option');
4518 option.selected = true;
4519 option.value = String(openai_settings.length - 1);
4520 option.innerText = data.name;
4521 if (triggerUi) $('#settings_preset_openai').append(option).trigger('change');
4522 }
4523 } else {
4524 toastr.error(t`Failed to save preset`);
4525 throw new Error('Failed to save preset');
4526 }
4527}
4528
4529function onLogitBiasPresetChange() {
4530 const value = String($('#openai_logit_bias_preset').find(':selected').val());
4531 const preset = oai_settings.bias_presets[value];
4532
4533 if (!Array.isArray(preset)) {
4534 console.error('Preset not found');
4535 return;
4536 }
4537
4538 oai_settings.bias_preset_selected = value;
4539 const list = $('.openai_logit_bias_list');
4540 list.empty();
4541
4542 for (const entry of preset) {
4543 if (entry) {
4544 createLogitBiasListItem(entry);
4545 }
4546 }
4547
4548 // Check if a sortable instance exists
4549 if (list.sortable('instance') !== undefined) {
4550 // Destroy the instance
4551 list.sortable('destroy');
4552 }
4553
4554 // Make the list sortable
4555 list.sortable({
4556 delay: getSortableDelay(),
4557 handle: '.drag-handle',
4558 stop: function () {
4559 const order = [];
4560 list.children().each(function () {
4561 order.unshift($(this).data('id'));
4562 });
4563 preset.sort((a, b) => order.indexOf(a.id) - order.indexOf(b.id));
4564 console.log('Logit bias reordered:', preset);
4565 saveSettingsDebounced();
4566 },
4567 });
4568
4569 biasCache = undefined;
4570 saveSettingsDebounced();
4571}
4572
4573function createNewLogitBiasEntry() {
4574 const entry = { id: uuidv4(), text: '', value: 0 };
4575 oai_settings.bias_presets[oai_settings.bias_preset_selected].push(entry);
4576 biasCache = undefined;
4577 createLogitBiasListItem(entry);
4578 saveSettingsDebounced();
4579}
4580
4581function createLogitBiasListItem(entry) {
4582 if (!entry.id) {
4583 entry.id = uuidv4();
4584 }
4585 const id = entry.id;
4586 const template = $('#openai_logit_bias_template .openai_logit_bias_form').clone();
4587 template.data('id', id);
4588 template.find('.openai_logit_bias_text').val(entry.text).on('input', function () {
4589 entry.text = String($(this).val());
4590 biasCache = undefined;
4591 saveSettingsDebounced();
4592 });
4593 template.find('.openai_logit_bias_value').val(entry.value).on('input', function () {
4594 const min = Number($(this).attr('min'));
4595 const max = Number($(this).attr('max'));
4596 let value = Number($(this).val());
4597
4598 if (value < min) {
4599 $(this).val(min);
4600 value = min;
4601 }
4602
4603 if (value > max) {
4604 $(this).val(max);
4605 value = max;
4606 }
4607
4608 entry.value = value;
4609 biasCache = undefined;
4610 saveSettingsDebounced();
4611 });
4612 template.find('.openai_logit_bias_remove').on('click', function () {
4613 $(this).closest('.openai_logit_bias_form').remove();
4614 const preset = oai_settings.bias_presets[oai_settings.bias_preset_selected];
4615 const index = preset.findIndex(item => item.id === id);
4616 if (index >= 0) {
4617 preset.splice(index, 1);
4618 }
4619 onLogitBiasPresetChange();
4620 });
4621 $('.openai_logit_bias_list').prepend(template);
4622}
4623
4624async function createNewLogitBiasPreset() {
4625 const name = await Popup.show.input(t`Preset name:`, null);
4626
4627 if (!name) {
4628 return;
4629 }
4630
4631 if (name in oai_settings.bias_presets) {
4632 toastr.error(t`Preset name should be unique.`);
4633 return;
4634 }
4635
4636 oai_settings.bias_preset_selected = name;
4637 oai_settings.bias_presets[name] = [];
4638
4639 addLogitBiasPresetOption(name);
4640 saveSettingsDebounced();
4641}
4642
4643function addLogitBiasPresetOption(name) {
4644 const option = document.createElement('option');
4645 option.innerText = name;
4646 option.value = name;
4647 option.selected = true;
4648
4649 $('#openai_logit_bias_preset').append(option);
4650 $('#openai_logit_bias_preset').trigger('change');
4651}
4652
4653function onImportPresetClick() {
4654 $('#openai_preset_import_file').trigger('click');
4655}
4656
4657function onLogitBiasPresetImportClick() {
4658 $('#openai_logit_bias_import_file').trigger('click');
4659}
4660
4661async function onPresetImportFileChange(e) {
4662 const file = e.target.files[0];
4663
4664 if (!file) {
4665 return;
4666 }
4667
4668 const name = file.name.replace(/\.[^/.]+$/, '');
4669 const importedFile = await getFileText(file);
4670 let presetBody;
4671 e.target.value = '';
4672
4673 try {
4674 presetBody = JSON.parse(importedFile);
4675 } catch (err) {
4676 toastr.error(t`Invalid file`);
4677 return;
4678 }
4679
4680 const fields = sensitiveFields.filter(field => presetBody[field]).map(field => `<b>${field}</b>`);
4681 const shouldConfirm = fields.length > 0;
4682
4683 if (shouldConfirm) {
4684 const textHeader = 'The imported preset contains proxy and/or custom endpoint settings.';
4685 const textMessage = fields.join('<br>');
4686 const cancelButton = { text: 'Cancel import', result: POPUP_RESULT.CANCELLED, appendAtEnd: true };
4687 const popupOptions = { customButtons: [cancelButton], okButton: 'Remove them', cancelButton: 'Import as-is' };
4688 const popupResult = await Popup.show.confirm(textHeader, textMessage, popupOptions);
4689
4690 if (popupResult === POPUP_RESULT.CANCELLED) {
4691 console.log('Import cancelled by user');
4692 return;
4693 }
4694
4695 if (popupResult === POPUP_RESULT.AFFIRMATIVE) {
4696 sensitiveFields.forEach(field => delete presetBody[field]);
4697 }
4698 }
4699
4700 if (name in openai_setting_names) {
4701 const confirm = await callGenericPopup('Preset name already exists. Overwrite?', POPUP_TYPE.CONFIRM);
4702
4703 if (!confirm) {
4704 return;
4705 }
4706 }
4707
4708 await eventSource.emit(event_types.OAI_PRESET_IMPORT_READY, { data: presetBody, presetName: name });
4709
4710 const savePresetSettings = await fetch('/api/presets/save', {
4711 method: 'POST',
4712 headers: getRequestHeaders(),
4713 body: JSON.stringify({
4714 apiId: 'openai',
4715 name: name,
4716 preset: presetBody,
4717 }),
4718 });
4719
4720 if (!savePresetSettings.ok) {
4721 toastr.error(t`Failed to save preset`);
4722 return;
4723 }
4724
4725 const data = await savePresetSettings.json();
4726
4727 if (Object.keys(openai_setting_names).includes(data.name)) {
4728 oai_settings.preset_settings_openai = data.name;
4729 const value = openai_setting_names[data.name];
4730 Object.assign(openai_settings[value], presetBody);
4731 $(`#settings_preset_openai option[value="${value}"]`).prop('selected', true);
4732 $('#settings_preset_openai').trigger('change');
4733 } else {
4734 openai_settings.push(presetBody);
4735 openai_setting_names[data.name] = openai_settings.length - 1;
4736 const option = document.createElement('option');
4737 option.selected = true;
4738 option.value = String(openai_settings.length - 1);
4739 option.innerText = data.name;
4740 $('#settings_preset_openai').append(option).trigger('change');
4741 }
4742}
4743
4744async function onExportPresetClick() {
4745 if (!oai_settings.preset_settings_openai) {
4746 toastr.error(t`No preset selected`);
4747 return;
4748 }
4749
4750 const preset = structuredClone(openai_settings[openai_setting_names[oai_settings.preset_settings_openai]]);
4751
4752 const fieldValues = sensitiveFields.filter(field => preset[field]).map(field => `<b>${field}</b>: <code>${preset[field]}</code>`);
4753 if (fieldValues.length > 0) {
4754 const textHeader = t`Your preset contains proxy and/or custom endpoint settings.`;
4755 const textMessage = '<div>' + t`Do you want to remove these fields before exporting?` + `</div><br>${DOMPurify.sanitize(fieldValues.join('<br>'))}`;
4756 const cancelButton = { text: 'Cancel', result: POPUP_RESULT.CANCELLED, appendAtEnd: true };
4757 const popupOptions = { customButtons: [cancelButton] };
4758 const popupResult = await Popup.show.confirm(textHeader, textMessage, popupOptions);
4759
4760 if (popupResult === POPUP_RESULT.CANCELLED) {
4761 console.log('Export cancelled by user');
4762 return;
4763 }
4764
4765 if (popupResult === POPUP_RESULT.AFFIRMATIVE) {
4766 sensitiveFields.forEach(field => delete preset[field]);
4767 }
4768 }
4769
4770 const exportConnectionTemplate = $(await renderTemplateAsync('exportPreset'));
4771 await new Popup(exportConnectionTemplate, POPUP_TYPE.TEXT).show();
4772
4773 const removeConnectionData = exportConnectionTemplate.find('input[name="export_connection_data"]:checked').val() === 'false';
4774 if (removeConnectionData) {
4775 for (const [, [, settingName, , isConnection]] of Object.entries(settingsToUpdate)) {
4776 if (isConnection) {
4777 delete preset[settingName];
4778 }
4779 }
4780 }
4781
4782 await eventSource.emit(event_types.OAI_PRESET_EXPORT_READY, preset);
4783 const presetJsonString = JSON.stringify(preset, null, 4);
4784 const presetFileName = `${oai_settings.preset_settings_openai}.json`;
4785 download(presetJsonString, presetFileName, 'application/json');
4786}
4787
4788async function onLogitBiasPresetImportFileChange(e) {
4789 const file = e.target.files[0];
4790
4791 if (!file || file.type !== 'application/json') {
4792 return;
4793 }
4794
4795 const name = file.name.replace(/\.[^/.]+$/, '');
4796 const importedFile = await parseJsonFile(file);
4797 e.target.value = '';
4798
4799 if (name in oai_settings.bias_presets) {
4800 toastr.error(t`Preset name should be unique.`);
4801 return;
4802 }
4803
4804 if (!Array.isArray(importedFile)) {
4805 toastr.error(t`Invalid logit bias preset file.`);
4806 return;
4807 }
4808
4809 const validEntries = [];
4810
4811 for (const entry of importedFile) {
4812 if (typeof entry == 'object' && entry !== null) {
4813 if (Object.hasOwn(entry, 'text') &&
4814 Object.hasOwn(entry, 'value')) {
4815 if (!entry.id) {
4816 entry.id = uuidv4();
4817 }
4818 validEntries.push(entry);
4819 }
4820 }
4821 }
4822
4823 oai_settings.bias_presets[name] = validEntries;
4824 oai_settings.bias_preset_selected = name;
4825
4826 addLogitBiasPresetOption(name);
4827 saveSettingsDebounced();
4828}
4829
4830function onLogitBiasPresetExportClick() {
4831 if (!oai_settings.bias_preset_selected || Object.keys(oai_settings.bias_presets).length === 0) {
4832 return;
4833 }
4834
4835 const presetJsonString = JSON.stringify(oai_settings.bias_presets[oai_settings.bias_preset_selected], null, 4);
4836 const presetFileName = `${oai_settings.bias_preset_selected}.json`;
4837 download(presetJsonString, presetFileName, 'application/json');
4838}
4839
4840async function onDeletePresetClick() {
4841 const confirm = await callGenericPopup(t`Delete the preset? This action is irreversible and your current settings will be overwritten.`, POPUP_TYPE.CONFIRM);
4842
4843 if (!confirm) {
4844 return;
4845 }
4846
4847 const nameToDelete = oai_settings.preset_settings_openai;
4848 const value = openai_setting_names[oai_settings.preset_settings_openai];
4849 $(`#settings_preset_openai option[value="${value}"]`).remove();
4850 delete openai_setting_names[oai_settings.preset_settings_openai];
4851 oai_settings.preset_settings_openai = null;
4852
4853 if (Object.keys(openai_setting_names).length) {
4854 oai_settings.preset_settings_openai = Object.keys(openai_setting_names)[0];
4855 const newValue = openai_setting_names[oai_settings.preset_settings_openai];
4856 $(`#settings_preset_openai option[value="${newValue}"]`).prop('selected', true);
4857 $('#settings_preset_openai').trigger('change');
4858 }
4859
4860 const response = await fetch('/api/presets/delete', {
4861 method: 'POST',
4862 headers: getRequestHeaders(),
4863 body: JSON.stringify({ apiId: 'openai', name: nameToDelete }),
4864 });
4865
4866 if (!response.ok) {
4867 toastr.warning(t`Preset was not deleted from server`);
4868 } else {
4869 toastr.success(t`Preset deleted`);
4870 await eventSource.emit(event_types.PRESET_DELETED, { apiId: 'openai', name: nameToDelete });
4871 }
4872
4873 saveSettingsDebounced();
4874}
4875
4876async function onLogitBiasPresetDeleteClick() {
4877 const value = await callGenericPopup(t`Delete the preset?`, POPUP_TYPE.CONFIRM);
4878
4879 if (!value) {
4880 return;
4881 }
4882
4883 $(`#openai_logit_bias_preset option[value="${oai_settings.bias_preset_selected}"]`).remove();
4884 delete oai_settings.bias_presets[oai_settings.bias_preset_selected];
4885 oai_settings.bias_preset_selected = null;
4886
4887 if (Object.keys(oai_settings.bias_presets).length) {
4888 oai_settings.bias_preset_selected = Object.keys(oai_settings.bias_presets)[0];
4889 $(`#openai_logit_bias_preset option[value="${oai_settings.bias_preset_selected}"]`).prop('selected', true);
4890 $('#openai_logit_bias_preset').trigger('change');
4891 }
4892
4893 biasCache = undefined;
4894 saveSettingsDebounced();
4895}
4896
4897// Load OpenAI preset settings
4898function onSettingsPresetChange() {
4899 const presetNameBefore = oai_settings.preset_settings_openai;
4900
4901 const presetName = $('#settings_preset_openai').find(':selected').text();
4902 oai_settings.preset_settings_openai = presetName;
4903
4904 const preset = structuredClone(openai_settings[openai_setting_names[oai_settings.preset_settings_openai]]);
4905
4906 migrateChatCompletionSettings(preset);
4907
4908 const updateInput = (selector, value) => $(selector).val(value).trigger('input', { source: 'preset' });
4909 const updateCheckbox = (selector, value) => $(selector).prop('checked', value).trigger('input', { source: 'preset' });
4910
4911 // Allow subscribers to alter the preset before applying deltas
4912 eventSource.emit(event_types.OAI_PRESET_CHANGED_BEFORE, {
4913 preset: preset,
4914 presetName: presetName,
4915 settingsToUpdate: settingsToUpdate,
4916 settings: oai_settings,
4917 savePreset: saveOpenAIPreset,
4918 presetNameBefore: presetNameBefore,
4919 }).finally(async () => {
4920 if (oai_settings.bind_preset_to_connection) {
4921 $('.model_custom_select').empty();
4922 }
4923
4924 for (const [key, [selector, setting, isCheckbox, isConnection]] of Object.entries(settingsToUpdate)) {
4925 if (isConnection && !oai_settings.bind_preset_to_connection) {
4926 continue;
4927 }
4928
4929 // Extensions don't need UI updates and shouldn't fallback to current settings
4930 if (key === 'extensions') {
4931 oai_settings.extensions = preset.extensions || {};
4932 continue;
4933 }
4934
4935 if (preset[key] !== undefined) {
4936 if (isCheckbox) {
4937 updateCheckbox(selector, preset[key]);
4938 } else {
4939 updateInput(selector, preset[key]);
4940 }
4941 oai_settings[setting] = preset[key];
4942 }
4943 }
4944
4945 // These cannot be changed via preset if unbound to connection
4946 if (oai_settings.bind_preset_to_connection) {
4947 $('#chat_completion_source').trigger('change');
4948 $('#openrouter_providers_chat').trigger('change');
4949 $('#openrouter_quantizations_chat').trigger('change');
4950 $('#nanogpt_provider').trigger('change');
4951 }
4952
4953 $('#openai_logit_bias_preset').trigger('change');
4954
4955 saveSettingsDebounced();
4956 await eventSource.emit(event_types.OAI_PRESET_CHANGED_AFTER);
4957 await eventSource.emit(event_types.PRESET_CHANGED, { apiId: 'openai', name: presetName });
4958 });
4959}
4960
4961/**
4962 * Get the maximum context size for the OpenAI model
4963 * @param {string} value Model identifier
4964 * @returns {number} Maximum context size in tokens
4965 */
4966function getMaxContextOpenAI(value) {
4967 if (oai_settings.max_context_unlocked) {
4968 return unlocked_max;
4969 }
4970
4971 /** @type {[RegExp, number][]} */
4972 const contextMap = [
4973 [/^gpt-5\.[45]/, max_1mil],
4974 [/^gpt-5/, max_400k],
4975 [/gpt-4\.1/, max_1mil],
4976 [/gpt-audio/, max_128k],
4977 [/^o1/, max_128k],
4978 [/^o[34]/, max_200k],
4979 [/chatgpt-4o-latest|gpt-4-turbo|gpt-4o|gpt-4-1106|gpt-4-0125|gpt-4-vision/, max_128k],
4980 [/gpt-3\.5-turbo-1106/, max_16k],
4981 [/^(gpt-4|gpt-4-0314|gpt-4-0613)$/, max_8k],
4982 [/^(gpt-4-32k|gpt-4-32k-0314|gpt-4-32k-0613)$/, max_32k],
4983 [/gpt-realtime/, max_32k],
4984 [/^(gpt-3\.5-turbo-16k|gpt-3\.5-turbo-16k-0613)$/, max_16k],
4985 [/^code-davinci-002$/, max_8k],
4986 [/^(text-curie-001|text-babbage-001|text-ada-001)$/, max_2k],
4987 [/gpt-3/, max_4k],
4988 ];
4989
4990 for (const [regex, max] of contextMap) {
4991 if (regex.test(value)) {
4992 return max;
4993 }
4994 }
4995
4996 // Safe default for most modern models
4997 return max_128k;
4998}
4999
5000/**
5001 * Get the maximum context size for Gemini models based on model identifier and optional model list.
5002 * @param {string} model Model identifier
5003 * @param {boolean} isUnlocked Whether context limits are unlocked
5004 * @returns {number} Maximum context size in tokens
5005 */
5006function getGeminiMaxContext(model, isUnlocked) {
5007 if (isUnlocked) {
5008 return unlocked_max;
5009 }
5010
5011 if (Array.isArray(model_list) && model_list.length > 0) {
5012 const contextLength = model_list.find((record) => record.id === model)?.inputTokenLimit;
5013 if (Number.isFinite(contextLength) && contextLength > 0) {
5014 return contextLength;
5015 }
5016 }
5017
5018 /** @type {[RegExp, number][]} */
5019 const contextMap = [
5020 [/gemini-2\.5-flash-image/, max_32k],
5021 [/gemini-3-pro-image/, max_64k],
5022 [/gemini-(?:3[.\d]*|2\.(?:5|0))-(pro|flash)/, max_1mil],
5023 [/(gemini-exp|learnlm-2\.0-flash|gemini-robotics)/, max_1mil],
5024 [/gemma-3-27b-it/, max_128k],
5025 [/gemma-3n-e4b-it/, max_8k],
5026 [/gemma-3/, max_32k],
5027 [/gemma-4/, max_256k],
5028 ];
5029
5030 for (const [regex, max] of contextMap) {
5031 if (regex.test(model)) {
5032 return max;
5033 }
5034 }
5035
5036 return max_128k;
5037}
5038
5039/**
5040 * Get the maximum temperature for Gemini models based on model identifier and optional model list.
5041 * @param {string} model Model identifier
5042 * @returns {number} Maximum temperature for Gemini models
5043 */
5044function getGeminiMaxTemp(model) {
5045 if (Array.isArray(model_list) && model_list.length > 0) {
5046 const temp = model_list.find((record) => record.id === model)?.maxTemperature;
5047 if (Number.isFinite(temp) && temp > 0) {
5048 return temp;
5049 }
5050 }
5051
5052 if (/(vision|ultra|gemma)/.test(model)) {
5053 return 1.0;
5054 }
5055
5056 return 2.0;
5057}
5058
5059/**
5060 * Get the maximum context size for the Mistral model
5061 * @param {string} model Model identifier
5062 * @param {boolean} isUnlocked Whether context limits are unlocked
5063 * @returns {number} Maximum context size in tokens
5064 */
5065function getMistralMaxContext(model, isUnlocked) {
5066 if (isUnlocked) {
5067 return unlocked_max;
5068 }
5069
5070 if (Array.isArray(model_list) && model_list.length > 0) {
5071 const contextLength = model_list.find((record) => record.id === model)?.max_context_length;
5072 if (contextLength) {
5073 return contextLength;
5074 }
5075 }
5076
5077 // Return context size if model found, otherwise default to 32k
5078 return max_32k;
5079}
5080
5081/**
5082 * Get the maximum context size for the Groq model
5083 * @param {string} model Model identifier
5084 * @param {boolean} isUnlocked Whether context limits are unlocked
5085 * @returns {number} Maximum context size in tokens
5086 */
5087function getGroqMaxContext(model, isUnlocked) {
5088 if (isUnlocked) {
5089 return unlocked_max;
5090 }
5091
5092 if (Array.isArray(model_list) && model_list.length > 0) {
5093 const contextLength = model_list.find((record) => record.id === model)?.context_window;
5094 if (contextLength) {
5095 return contextLength;
5096 }
5097 }
5098
5099 const contextMap = {
5100 'gemma2-9b-it': max_8k,
5101 'llama-3.3-70b-versatile': max_128k,
5102 'llama-3.1-8b-instant': max_128k,
5103 'llama3-70b-8192': max_8k,
5104 'llama3-8b-8192': max_8k,
5105 'llama-guard-3-8b': max_8k,
5106 'mixtral-8x7b-32768': max_32k,
5107 'deepseek-r1-distill-llama-70b': max_128k,
5108 'llama-3.3-70b-specdec': max_8k,
5109 'llama-3.2-1b-preview': max_128k,
5110 'llama-3.2-3b-preview': max_128k,
5111 'llama-3.2-11b-vision-preview': max_128k,
5112 'llama-3.2-90b-vision-preview': max_128k,
5113 'qwen-2.5-32b': max_128k,
5114 'deepseek-r1-distill-qwen-32b': max_128k,
5115 'deepseek-r1-distill-llama-70b-specdec': max_128k,
5116 'mistral-saba-24b': max_32k,
5117 'meta-llama/llama-4-scout-17b-16e-instruct': max_128k,
5118 'meta-llama/llama-4-maverick-17b-128e-instruct': max_128k,
5119 'compound-beta': max_128k,
5120 'compound-beta-mini': max_128k,
5121 'qwen/qwen3-32b': max_128k,
5122 };
5123
5124 // Return context size if model found, otherwise default to 128k
5125 return Object.entries(contextMap).find(([key]) => model.includes(key))?.[1] || max_128k;
5126}
5127
5128/**
5129 * Get the maximum context size for the Z.AI model
5130 * @param {string} model Model identifier
5131 * @param {boolean} isUnlocked If context limits are unlocked
5132 * @returns {number} Maximum context size in tokens
5133 */
5134function getZaiMaxContext(model, isUnlocked) {
5135 if (isUnlocked) {
5136 return unlocked_max;
5137 }
5138
5139 const contextMap = {
5140 'glm-5.1': max_200k,
5141 'glm-5-turbo': max_200k,
5142 'glm-5v-turbo': max_200k,
5143 'glm-5': max_200k,
5144 'glm-4.7': max_200k,
5145 'glm-4.7-flash': max_200k,
5146 'glm-4.7-flashx': max_200k,
5147 'glm-4.6v': max_128k,
5148 'glm-4.6v-flash': max_128k,
5149 'glm-4.6v-flashx': max_128k,
5150 'glm-4.6': max_200k,
5151 'glm-4.5': max_128k,
5152 'glm-4-32b-0414-128k': max_128k,
5153 'glm-4.5-air': max_128k,
5154 'glm-4.5v': max_64k,
5155 'autoglm-phone-multilingual': max_64k,
5156 };
5157
5158 // Return context size if model found, otherwise default to 128k
5159 return Object.entries(contextMap).find(([key]) => model.includes(key))?.[1] || max_128k;
5160}
5161
5162/**
5163 * Get the maximum context size for the SiliconFlow model
5164 * @param {string} model Model identifier
5165 * @param {boolean} isUnlocked Whether context limits are unlocked
5166 * @returns {number} Maximum context size in tokens
5167 */
5168function getSiliconflowMaxContext(model, isUnlocked) {
5169 if (isUnlocked) {
5170 return unlocked_max;
5171 }
5172
5173 const contextMap = {
5174 'baidu/ERNIE-4.5-300B-A47B': max_128k,
5175 'ByteDance-Seed/Seed-OSS-36B-Instruct': max_256k,
5176 'deepseek-ai/DeepSeek-R1': max_128k,
5177 'deepseek-ai/DeepSeek-V3': max_128k,
5178 'deepseek-ai/DeepSeek-V3.1': max_128k,
5179 'deepseek-ai/DeepSeek-V3.1-Terminus': max_128k,
5180 'deepseek-ai/DeepSeek-V3.2-Exp': max_128k,
5181 'deepseek-ai/deepseek-vl2': max_4k,
5182 'inclusionAI/Ling-1T': max_128k,
5183 'inclusionAI/Ling-flash-2.0': max_128k,
5184 'inclusionAI/Ling-mini-2.0': max_128k,
5185 'inclusionAI/Ring-1T': max_128k,
5186 'inclusionAI/Ring-flash-2.0': max_128k,
5187 'meta-llama/Llama-3.3-70B-Instruct': max_32k,
5188 'meta-llama/Meta-Llama-3.1-8B-Instruct': max_32k,
5189 'MiniMaxAI/MiniMax-M1-80k': max_128k,
5190 'MiniMaxAI/MiniMax-M2': max_128k,
5191 'moonshotai/Kimi-K2-Instruct': max_128k,
5192 'moonshotai/Kimi-K2-Instruct-0905': max_256k,
5193 'moonshotai/Kimi-K2-Thinking': max_256k,
5194 'openai/gpt-oss-120b': max_128k,
5195 'openai/gpt-oss-20b': max_128k,
5196 'Qwen/Qwen3-235B-A22B-Instruct-2507': max_256k,
5197 'Qwen/Qwen3-235B-A22B-Thinking-2507': max_256k,
5198 'Qwen/Qwen3-30B-A3B-Instruct-2507': max_256k,
5199 'Qwen/Qwen3-30B-A3B-Thinking-2507': max_256k,
5200 'Qwen/Qwen3-VL-235B-A22B-Instruct': max_256k,
5201 'Qwen/Qwen3-VL-235B-A22B-Thinking': max_256k,
5202 'Qwen/Qwen3-VL-30B-A3B-Instruct': max_256k,
5203 'Qwen/Qwen3-VL-30B-A3B-Thinking': max_256k,
5204 'Qwen/Qwen3-VL-32B-Instruct': max_256k,
5205 'Qwen/Qwen3-VL-32B-Thinking': max_256k,
5206 'Qwen/Qwen3-VL-8B-Instruct': max_256k,
5207 'Qwen/Qwen3-VL-8B-Thinking': max_256k,
5208 'stepfun-ai/step3': max_64k,
5209 'tencent/Hunyuan-A13B-Instruct': max_128k,
5210 'zai-org/GLM-4.5': max_128k,
5211 'zai-org/GLM-4.5-Air': max_128k,
5212 'zai-org/GLM-4.5V': max_64k,
5213 'zai-org/GLM-4.6': max_200k,
5214 };
5215
5216 // Return context size if model found, otherwise default to 32k
5217 return Object.entries(contextMap).find(([key]) => model.includes(key))?.[1] || max_32k;
5218}
5219
5220/**
5221 * Get the maximum context size for the Moonshot model
5222 * @param {string} model Model identifier
5223 * @param {boolean} isUnlocked If context limits are unlocked
5224 * @returns {number} Maximum context size in tokens
5225 */
5226function getMoonshotMaxContext(model, isUnlocked) {
5227 if (isUnlocked) {
5228 return unlocked_max;
5229 }
5230
5231 if (Array.isArray(model_list) && model_list.length > 0) {
5232 const modelInfo = model_list.find((record) => record.id === model);
5233 if (modelInfo?.context_length) {
5234 return modelInfo.context_length;
5235 }
5236 }
5237
5238 const contextMap = {
5239 'moonshot-v1-8k': max_8k,
5240 'moonshot-v1-32k': max_32k,
5241 'moonshot-v1-128k': max_128k,
5242 'moonshot-v1-auto': max_128k,
5243 'moonshot-v1-8k-vision-preview': max_8k,
5244 'moonshot-v1-32k-vision-preview': max_32k,
5245 'moonshot-v1-128k-vision-preview': max_128k,
5246 'kimi-k2-0711-preview': max_32k,
5247 'kimi-latest': max_256k,
5248 'kimi-thinking-preview': max_32k,
5249 'kimi-k2.5': max_256k,
5250 'kimi-k2-0905-preview': max_256k,
5251 'kimi-k2-turbo-preview': max_256k,
5252 'kimi-k2-thinking': max_256k,
5253 'kimi-k2-thinking-turbo': max_256k,
5254 };
5255
5256 // Return context size if model found, otherwise default to 32k
5257 return Object.entries(contextMap).find(([key]) => model.includes(key))?.[1] || max_32k;
5258}
5259
5260/**
5261 * Get the maximum context size for the Fireworks model
5262 * @param {string} model Model identifier
5263 * @param {boolean} isUnlocked Whether context limits are unlocked
5264 * @returns {number} Maximum context size in tokens
5265 */
5266function getFireworksMaxContext(model, isUnlocked) {
5267 if (isUnlocked) {
5268 return unlocked_max;
5269 }
5270
5271 // First check if model info is available from model_list
5272 if (Array.isArray(model_list) && model_list.length > 0) {
5273 const modelInfo = model_list.find((record) => record.id === model);
5274 if (modelInfo?.context_length) {
5275 return modelInfo.context_length;
5276 }
5277 if (modelInfo?.context_window) {
5278 return modelInfo.context_window;
5279 }
5280 }
5281
5282 return max_32k;
5283}
5284
5285/**
5286 * Get the maximum context size for the Chutes model
5287 * @param {string} model Model identifier
5288 * @param {boolean} isUnlocked Whether context limits are unlocked
5289 * @returns {number} Maximum context size in tokens
5290 */
5291function getChutesMaxContext(model, isUnlocked) {
5292 if (isUnlocked) {
5293 return unlocked_max;
5294 }
5295
5296 if (Array.isArray(model_list)) {
5297 const modelInfo = model_list.find(m => m.id === model);
5298 if (modelInfo?.context_length) {
5299 return modelInfo.context_length;
5300 }
5301 }
5302 return max_8k;
5303}
5304
5305/**
5306 * Get the maximum context size for the ElectronHub model
5307 * @param {string} model Model identifier
5308 * @param {boolean} isUnlocked Whether context limits are unlocked
5309 * @returns {number} Maximum context size in tokens
5310 */
5311function getElectronHubMaxContext(model, isUnlocked) {
5312 if (isUnlocked) {
5313 return unlocked_max;
5314 }
5315
5316 if (Array.isArray(model_list)) {
5317 const modelInfo = model_list.find(m => m.id === model);
5318 if (modelInfo?.tokens) {
5319 return modelInfo.tokens;
5320 }
5321 }
5322 return max_128k;
5323}
5324
5325/**
5326 * Get the maximum context size for the NanoGPT model
5327 * @param {string} model Model identifier
5328 * @param {boolean} isUnlocked Whether context limits are unlocked
5329 * @returns {number} Maximum context size in tokens
5330 */
5331function getNanoGptMaxContext(model, isUnlocked) {
5332 if (isUnlocked) {
5333 return unlocked_max;
5334 }
5335
5336 if (Array.isArray(model_list)) {
5337 const modelInfo = model_list.find(m => m.id === model);
5338 if (modelInfo?.context_length) {
5339 return modelInfo.context_length;
5340 }
5341 }
5342
5343 return max_128k;
5344}
5345
5346async function onModelChange() {
5347 biasCache = undefined;
5348 let value = String($(this).val() || '');
5349
5350 // Skip setting the context size for sources that get it from external APIs
5351 const hasModelsLoaded = Array.isArray(model_list) && model_list.length > 0;
5352
5353 if ($(this).is('#model_claude_select')) {
5354 if (value.includes('-v')) {
5355 value = value.replace('-v', '-');
5356 } else if (value === '' || value === 'claude-2') {
5357 value = default_settings.claude_model;
5358 }
5359 console.log('Claude model changed to', value);
5360 oai_settings.claude_model = value;
5361 $('#model_claude_select').val(oai_settings.claude_model);
5362 }
5363
5364 if ($(this).is('#model_openai_select')) {
5365 console.log('OpenAI model changed to', value);
5366 oai_settings.openai_model = value;
5367 }
5368
5369 if ($(this).is('#model_openrouter_select')) {
5370 if (!value || !hasModelsLoaded) {
5371 console.debug('Null OR model selected. Ignoring.');
5372 return;
5373 }
5374
5375 console.log('OpenRouter model changed to', value);
5376 oai_settings.openrouter_model = value;
5377 syncOpenRouterProvidersForModel(value, '#openrouter_providers_chat');
5378 }
5379
5380 if ($(this).is('#model_ai21_select')) {
5381 if (value === '' || value.startsWith('j2-')) {
5382 value = 'jamba-large';
5383 $('#model_ai21_select').val(value);
5384 }
5385
5386 console.log('AI21 model changed to', value);
5387 oai_settings.ai21_model = value;
5388 }
5389
5390 if ($(this).is('#model_google_select')) {
5391 if (!value) {
5392 console.debug('Null Google model selected. Ignoring.');
5393 return;
5394 }
5395
5396 console.log('Google model changed to', value);
5397 oai_settings.google_model = value;
5398 }
5399
5400 if ($(this).is('#model_vertexai_select')) {
5401 console.log('Vertex AI model changed to', value);
5402 oai_settings.vertexai_model = value;
5403 }
5404
5405 if ($(this).is('#model_mistralai_select')) {
5406 if (!value || !hasModelsLoaded) {
5407 console.debug('Null MistralAI model selected. Ignoring.');
5408 return;
5409 }
5410 console.log('MistralAI model changed to', value);
5411 oai_settings.mistralai_model = value;
5412 $('#model_mistralai_select').val(oai_settings.mistralai_model);
5413 }
5414
5415 if ($(this).is('#model_cohere_select')) {
5416 console.log('Cohere model changed to', value);
5417 oai_settings.cohere_model = value;
5418 }
5419
5420 if ($(this).is('#model_perplexity_select')) {
5421 console.log('Perplexity model changed to', value);
5422 oai_settings.perplexity_model = value;
5423 }
5424
5425 if ($(this).is('#model_groq_select')) {
5426 if (!value || !hasModelsLoaded) {
5427 console.debug('Null Groq model selected. Ignoring.');
5428 return;
5429 }
5430 console.log('Groq model changed to', value);
5431 oai_settings.groq_model = value;
5432 }
5433
5434 if ($(this).is('#model_siliconflow_select')) {
5435 if (!value) {
5436 console.debug('Null SiliconFlow model selected. Ignoring.');
5437 return;
5438 }
5439 console.log('SiliconFlow model changed to', value);
5440 oai_settings.siliconflow_model = value;
5441 }
5442
5443 if ($(this).is('#model_minimax_select')) {
5444 if (!value) {
5445 console.debug('Null MiniMax model selected. Ignoring.');
5446 return;
5447 }
5448 console.log('MiniMax model changed to', value);
5449 oai_settings.minimax_model = value;
5450 }
5451
5452 if ($(this).is('#model_electronhub_select')) {
5453 if (!value || !hasModelsLoaded) {
5454 console.debug('Null ElectronHub model selected. Ignoring.');
5455 return;
5456 }
5457 console.log('ElectronHub model changed to', value);
5458 oai_settings.electronhub_model = value;
5459 }
5460
5461 if ($(this).is('#model_chutes_select')) {
5462 if (!value || !hasModelsLoaded) {
5463 console.debug('Null Chutes model selected. Ignoring.');
5464 return;
5465 }
5466 console.log('Chutes model changed to', value);
5467 oai_settings.chutes_model = value;
5468 }
5469
5470 if ($(this).is('#model_nanogpt_select')) {
5471 if (!value || !hasModelsLoaded) {
5472 console.debug('Null NanoGPT model selected. Ignoring.');
5473 return;
5474 }
5475
5476 console.log('NanoGPT model changed to', value);
5477 oai_settings.nanogpt_model = value;
5478 syncNanoGptProvidersForModel(value, '#nanogpt_provider');
5479 }
5480
5481 if ($(this).is('#model_deepseek_select')) {
5482 if (!value) {
5483 console.debug('Null DeepSeek model selected. Ignoring.');
5484 return;
5485 }
5486
5487 console.log('DeepSeek model changed to', value);
5488 oai_settings.deepseek_model = value;
5489 }
5490
5491 if (value && $(this).is('#model_custom_select')) {
5492 console.log('Custom model changed to', value);
5493 oai_settings.custom_model = value;
5494 $('#custom_model_id').val(value).trigger('input');
5495 }
5496
5497 if (value && $(this).is('#model_pollinations_select')) {
5498 console.log('Pollinations model changed to', value);
5499 oai_settings.pollinations_model = value;
5500 }
5501
5502 if ($(this).is('#model_aimlapi_select')) {
5503 if (!value || !hasModelsLoaded) {
5504 console.debug('Null AI/ML model selected. Ignoring.');
5505 return;
5506 }
5507 console.log('AI/ML model changed to', value);
5508 oai_settings.aimlapi_model = value;
5509 }
5510
5511 if ($(this).is('#model_xai_select')) {
5512 if (!value) {
5513 console.debug('Null XAI model selected. Ignoring.');
5514 return;
5515 }
5516 console.log('XAI model changed to', value);
5517 oai_settings.xai_model = value;
5518 }
5519
5520 if ($(this).is('#model_moonshot_select')) {
5521 if (!value || !hasModelsLoaded) {
5522 console.debug('Null Moonshot model selected. Ignoring.');
5523 return;
5524 }
5525 console.log('Moonshot model changed to', value);
5526 oai_settings.moonshot_model = value;
5527 }
5528
5529 if ($(this).is('#model_fireworks_select')) {
5530 if (!value || !hasModelsLoaded) {
5531 console.debug('Null Fireworks model selected. Ignoring.');
5532 return;
5533 }
5534 console.log('Fireworks model changed to', value);
5535 oai_settings.fireworks_model = value;
5536 }
5537
5538 if ($(this).is('#model_cometapi_select')) {
5539 if (!value) {
5540 console.debug('Null CometAPI model selected. Ignoring.');
5541 return;
5542 }
5543 console.log('CometAPI model changed to', value);
5544 oai_settings.cometapi_model = value;
5545 }
5546
5547 if ($(this).is('#azure_openai_model')) {
5548 if (!value) {
5549 console.debug('Null Azure OpenAI model selected. Ignoring.');
5550 return;
5551 }
5552 oai_settings.azure_openai_model = value;
5553 }
5554
5555 if ($(this).is('#model_zai_select')) {
5556 console.log('ZAI model changed to', value);
5557 oai_settings.zai_model = value;
5558 }
5559
5560 if ($(this).is('#model_workers_ai_select')) {
5561 if (!value || !hasModelsLoaded) {
5562 console.debug('Null Workers AI model selected. Ignoring.');
5563 return;
5564 }
5565 console.log('Workers AI model changed to', value);
5566 oai_settings.workers_ai_model = value;
5567 }
5568
5569 if ([chat_completion_sources.MAKERSUITE, chat_completion_sources.VERTEXAI].includes(oai_settings.chat_completion_source)) {
5570 const contextSize = getGeminiMaxContext(value, oai_settings.max_context_unlocked);
5571 const maxTemp = getGeminiMaxTemp(value);
5572 $('#openai_max_context').attr('max', contextSize);
5573 oai_settings.temp_openai = Math.min(maxTemp, oai_settings.temp_openai);
5574 $('#temp_openai').attr('max', maxTemp).val(oai_settings.temp_openai).trigger('input');
5575 oai_settings.openai_max_context = Math.min(Number($('#openai_max_context').attr('max')), oai_settings.openai_max_context);
5576 $('#openai_max_context').val(oai_settings.openai_max_context).trigger('input');
5577 }
5578
5579 if (oai_settings.chat_completion_source == chat_completion_sources.OPENROUTER) {
5580 if (oai_settings.max_context_unlocked) {
5581 $('#openai_max_context').attr('max', unlocked_max);
5582 } else {
5583 const model = model_list.find(m => m.id == oai_settings.openrouter_model);
5584 if (model?.context_length) {
5585 $('#openai_max_context').attr('max', model.context_length);
5586 } else {
5587 $('#openai_max_context').attr('max', max_128k);
5588 }
5589 }
5590 oai_settings.openai_max_context = Math.min(Number($('#openai_max_context').attr('max')), oai_settings.openai_max_context);
5591 $('#openai_max_context').val(oai_settings.openai_max_context).trigger('input');
5592
5593 if (value && (value.includes('claude') || value.includes('palm-2'))) {
5594 oai_settings.temp_openai = Math.min(claude_max_temp, oai_settings.temp_openai);
5595 $('#temp_openai').attr('max', claude_max_temp).val(oai_settings.temp_openai).trigger('input');
5596 } else {
5597 oai_settings.temp_openai = Math.min(oai_max_temp, oai_settings.temp_openai);
5598 $('#temp_openai').attr('max', oai_max_temp).val(oai_settings.temp_openai).trigger('input');
5599 }
5600
5601 calculateOpenRouterCost();
5602 }
5603
5604 if (oai_settings.chat_completion_source == chat_completion_sources.CLAUDE) {
5605 if (oai_settings.max_context_unlocked) {
5606 $('#openai_max_context').attr('max', unlocked_max);
5607 } else if (/^claude-(sonnet-4-5|sonnet-4-6|opus-4-6|opus-4-7)/.test(value)) {
5608 $('#openai_max_context').attr('max', max_1mil);
5609 } else if (/^claude-(3|opus|haiku|sonnet)/.test(value)) {
5610 $('#openai_max_context').attr('max', max_200k);
5611 } else {
5612 $('#openai_max_context').attr('max', max_200k);
5613 }
5614
5615 oai_settings.openai_max_context = Math.min(oai_settings.openai_max_context, Number($('#openai_max_context').attr('max')));
5616 $('#openai_max_context').val(oai_settings.openai_max_context).trigger('input');
5617
5618 $('#openai_reverse_proxy').attr('placeholder', 'https://api.anthropic.com/v1');
5619
5620 oai_settings.temp_openai = Math.min(claude_max_temp, oai_settings.temp_openai);
5621 $('#temp_openai').attr('max', claude_max_temp).val(oai_settings.temp_openai).trigger('input');
5622 }
5623
5624 if ([chat_completion_sources.AZURE_OPENAI, chat_completion_sources.OPENAI].includes(oai_settings.chat_completion_source)) {
5625 $('#openai_max_context').attr('max', getMaxContextOpenAI(value));
5626 oai_settings.openai_max_context = Math.min(oai_settings.openai_max_context, Number($('#openai_max_context').attr('max')));
5627 $('#openai_max_context').val(oai_settings.openai_max_context).trigger('input');
5628
5629 $('#openai_reverse_proxy').attr('placeholder', 'https://api.openai.com/v1');
5630
5631 oai_settings.temp_openai = Math.min(oai_max_temp, oai_settings.temp_openai);
5632 $('#temp_openai').attr('max', oai_max_temp).val(oai_settings.temp_openai).trigger('input');
5633 }
5634
5635 if (oai_settings.chat_completion_source === chat_completion_sources.MISTRALAI) {
5636 const maxContext = getMistralMaxContext(oai_settings.mistralai_model, oai_settings.max_context_unlocked);
5637 $('#openai_max_context').attr('max', maxContext);
5638 oai_settings.openai_max_context = Math.min(oai_settings.openai_max_context, Number($('#openai_max_context').attr('max')));
5639 $('#openai_max_context').val(oai_settings.openai_max_context).trigger('input');
5640 oai_settings.temp_openai = Math.min(mistral_max_temp, oai_settings.temp_openai);
5641 $('#temp_openai').attr('max', mistral_max_temp).val(oai_settings.temp_openai).trigger('input');
5642 }
5643
5644 if (oai_settings.chat_completion_source === chat_completion_sources.COHERE) {
5645 if (oai_settings.max_context_unlocked) {
5646 $('#openai_max_context').attr('max', unlocked_max);
5647 } else if (['command-light-nightly', 'command-light', 'command'].includes(oai_settings.cohere_model)) {
5648 $('#openai_max_context').attr('max', max_4k);
5649 } else if (oai_settings.cohere_model.includes('command-r') || ['c4ai-aya-23', 'c4ai-aya-expanse-32b', 'command-nightly', 'command-a-vision-07-2025'].includes(oai_settings.cohere_model)) {
5650 $('#openai_max_context').attr('max', max_128k);
5651 } else if (['command-a-03-2025'].includes(oai_settings.cohere_model)) {
5652 $('#openai_max_context').attr('max', max_256k);
5653 } else if (['c4ai-aya-23-8b', 'c4ai-aya-expanse-8b'].includes(oai_settings.cohere_model)) {
5654 $('#openai_max_context').attr('max', max_8k);
5655 } else if (['c4ai-aya-vision-8b', 'c4ai-aya-vision-32b'].includes(oai_settings.cohere_model)) {
5656 $('#openai_max_context').attr('max', max_16k);
5657 } else {
5658 $('#openai_max_context').attr('max', max_4k);
5659 }
5660 oai_settings.openai_max_context = Math.min(Number($('#openai_max_context').attr('max')), oai_settings.openai_max_context);
5661 $('#openai_max_context').val(oai_settings.openai_max_context).trigger('input');
5662 $('#temp_openai').attr('max', claude_max_temp).val(oai_settings.temp_openai).trigger('input');
5663 }
5664
5665 if (oai_settings.chat_completion_source === chat_completion_sources.PERPLEXITY) {
5666 if (oai_settings.max_context_unlocked) {
5667 $('#openai_max_context').attr('max', unlocked_max);
5668 } else if (['sonar', 'sonar-reasoning', 'sonar-reasoning-pro', 'r1-1776'].includes(oai_settings.perplexity_model)) {
5669 $('#openai_max_context').attr('max', 127000);
5670 } else if (['sonar-pro'].includes(oai_settings.perplexity_model)) {
5671 $('#openai_max_context').attr('max', 200000);
5672 } else if (oai_settings.perplexity_model.includes('llama-3.1')) {
5673 const isOnline = oai_settings.perplexity_model.includes('online');
5674 const contextSize = isOnline ? 128 * 1024 - 4000 : 128 * 1024;
5675 $('#openai_max_context').attr('max', contextSize);
5676 } else {
5677 $('#openai_max_context').attr('max', max_128k);
5678 }
5679 oai_settings.openai_max_context = Math.min(Number($('#openai_max_context').attr('max')), oai_settings.openai_max_context);
5680 $('#openai_max_context').val(oai_settings.openai_max_context).trigger('input');
5681 oai_settings.temp_openai = Math.min(oai_max_temp, oai_settings.temp_openai);
5682 $('#temp_openai').attr('max', oai_max_temp).val(oai_settings.temp_openai).trigger('input');
5683 }
5684
5685 if (oai_settings.chat_completion_source == chat_completion_sources.GROQ) {
5686 const maxContext = getGroqMaxContext(oai_settings.groq_model, oai_settings.max_context_unlocked);
5687 $('#openai_max_context').attr('max', maxContext);
5688 oai_settings.openai_max_context = Math.min(Number($('#openai_max_context').attr('max')), oai_settings.openai_max_context);
5689 $('#openai_max_context').val(oai_settings.openai_max_context).trigger('input');
5690 oai_settings.temp_openai = Math.min(oai_max_temp, oai_settings.temp_openai);
5691 $('#temp_openai').attr('max', oai_max_temp).val(oai_settings.temp_openai).trigger('input');
5692 }
5693
5694 if (oai_settings.chat_completion_source == chat_completion_sources.AI21) {
5695 if (oai_settings.max_context_unlocked) {
5696 $('#openai_max_context').attr('max', unlocked_max);
5697 } else if (oai_settings.ai21_model.startsWith('jamba-')) {
5698 $('#openai_max_context').attr('max', max_256k);
5699 }
5700
5701 oai_settings.openai_max_context = Math.min(Number($('#openai_max_context').attr('max')), oai_settings.openai_max_context);
5702 $('#openai_max_context').val(oai_settings.openai_max_context).trigger('input');
5703 $('#temp_openai').attr('max', oai_max_temp).val(oai_settings.temp_openai).trigger('input');
5704 }
5705
5706 if (oai_settings.chat_completion_source == chat_completion_sources.CUSTOM) {
5707 $('#openai_max_context').attr('max', unlocked_max);
5708 oai_settings.openai_max_context = Math.min(Number($('#openai_max_context').attr('max')), oai_settings.openai_max_context);
5709 $('#openai_max_context').val(oai_settings.openai_max_context).trigger('input');
5710 $('#temp_openai').attr('max', oai_max_temp).val(oai_settings.temp_openai).trigger('input');
5711 }
5712
5713 if (oai_settings.chat_completion_source == chat_completion_sources.CHUTES) {
5714 const maxContext = getChutesMaxContext(oai_settings.chutes_model, oai_settings.max_context_unlocked);
5715 $('#openai_max_context').attr('max', maxContext);
5716 oai_settings.openai_max_context = Math.min(Number($('#openai_max_context').attr('max')), oai_settings.openai_max_context);
5717 $('#openai_max_context').val(oai_settings.openai_max_context).trigger('input');
5718 oai_settings.temp_openai = Math.min(oai_max_temp, oai_settings.temp_openai);
5719 $('#temp_openai').attr('max', oai_max_temp).val(oai_settings.temp_openai).trigger('input');
5720
5721 calculateChutesCost();
5722 }
5723
5724 if (oai_settings.chat_completion_source == chat_completion_sources.ELECTRONHUB) {
5725 const maxContext = getElectronHubMaxContext(oai_settings.electronhub_model, oai_settings.max_context_unlocked);
5726 $('#openai_max_context').attr('max', maxContext);
5727 oai_settings.openai_max_context = Math.min(Number($('#openai_max_context').attr('max')), oai_settings.openai_max_context);
5728 $('#openai_max_context').val(oai_settings.openai_max_context).trigger('input');
5729 oai_settings.temp_openai = Math.min(oai_max_temp, oai_settings.temp_openai);
5730 $('#temp_openai').attr('max', oai_max_temp).val(oai_settings.temp_openai).trigger('input');
5731
5732 calculateElectronHubCost();
5733 }
5734
5735 if (oai_settings.chat_completion_source === chat_completion_sources.NANOGPT) {
5736 const maxContext = getNanoGptMaxContext(oai_settings.nanogpt_model, oai_settings.max_context_unlocked);
5737 $('#openai_max_context').attr('max', maxContext);
5738 oai_settings.openai_max_context = Math.min(Number($('#openai_max_context').attr('max')), oai_settings.openai_max_context);
5739 $('#openai_max_context').val(oai_settings.openai_max_context).trigger('input');
5740 oai_settings.temp_openai = Math.min(oai_max_temp, oai_settings.temp_openai);
5741 $('#temp_openai').attr('max', oai_max_temp).val(oai_settings.temp_openai).trigger('input');
5742 }
5743
5744 if (oai_settings.chat_completion_source === chat_completion_sources.POLLINATIONS) {
5745 if (oai_settings.max_context_unlocked) {
5746 $('#openai_max_context').attr('max', unlocked_max);
5747 } else {
5748 $('#openai_max_context').attr('max', max_128k);
5749 }
5750
5751 oai_settings.openai_max_context = Math.min(Number($('#openai_max_context').attr('max')), oai_settings.openai_max_context);
5752 $('#openai_max_context').val(oai_settings.openai_max_context).trigger('input');
5753 $('#temp_openai').attr('max', oai_max_temp).val(oai_settings.temp_openai).trigger('input');
5754 }
5755
5756 if (oai_settings.chat_completion_source === chat_completion_sources.DEEPSEEK) {
5757 const maxContext = oai_settings.max_context_unlocked ? unlocked_max : max_1mil;
5758 $('#openai_max_context').attr('max', maxContext);
5759 oai_settings.openai_max_context = Math.min(Number($('#openai_max_context').attr('max')), oai_settings.openai_max_context);
5760 $('#openai_max_context').val(oai_settings.openai_max_context).trigger('input');
5761 $('#temp_openai').attr('max', oai_max_temp).val(oai_settings.temp_openai).trigger('input');
5762 }
5763
5764 if (oai_settings.chat_completion_source === chat_completion_sources.WORKERS_AI) {
5765 if (oai_settings.max_context_unlocked) {
5766 $('#openai_max_context').attr('max', unlocked_max);
5767 } else {
5768 const model = model_list.find(m => m.id === oai_settings.workers_ai_model);
5769 const ctxProp = Array.isArray(model?.properties) && model.properties.find(p => p.property_id === 'context_window');
5770 const contextLength = ctxProp ? Number(ctxProp.value) : max_8k;
5771 $('#openai_max_context').attr('max', contextLength || max_8k);
5772 }
5773 oai_settings.openai_max_context = Math.min(Number($('#openai_max_context').attr('max')), oai_settings.openai_max_context);
5774 $('#openai_max_context').val(oai_settings.openai_max_context).trigger('input');
5775 const workersAiMaxTemp = 5.0;
5776 oai_settings.temp_openai = Math.min(workersAiMaxTemp, oai_settings.temp_openai);
5777 $('#temp_openai').attr('max', workersAiMaxTemp).val(oai_settings.temp_openai).trigger('input');
5778 }
5779
5780 if (oai_settings.chat_completion_source === chat_completion_sources.COMETAPI) {
5781 $('#openai_max_context').attr('max', oai_settings.max_context_unlocked ? unlocked_max : max_128k);
5782 oai_settings.openai_max_context = Math.min(Number($('#openai_max_context').attr('max')), oai_settings.openai_max_context);
5783 $('#openai_max_context').val(oai_settings.openai_max_context).trigger('input');
5784 $('#temp_openai').attr('max', oai_max_temp).val(oai_settings.temp_openai).trigger('input');
5785 }
5786
5787 if (oai_settings.chat_completion_source === chat_completion_sources.XAI) {
5788 if (oai_settings.max_context_unlocked) {
5789 $('#openai_max_context').attr('max', unlocked_max);
5790 } else if (oai_settings.xai_model.includes('grok-2-vision')) {
5791 $('#openai_max_context').attr('max', max_32k);
5792 } else if (oai_settings.xai_model.includes('grok-4-fast')) {
5793 $('#openai_max_context').attr('max', max_2mil);
5794 } else if (oai_settings.xai_model.includes('grok-4')) {
5795 $('#openai_max_context').attr('max', max_256k);
5796 } else if (oai_settings.xai_model.includes('grok-code')) {
5797 $('#openai_max_context').attr('max', max_256k);
5798 } else {
5799 // grok 2 and grok 3
5800 $('#openai_max_context').attr('max', max_128k);
5801 }
5802
5803 oai_settings.openai_max_context = Math.min(Number($('#openai_max_context').attr('max')), oai_settings.openai_max_context);
5804 $('#openai_max_context').val(oai_settings.openai_max_context).trigger('input');
5805 $('#temp_openai').attr('max', oai_max_temp).val(oai_settings.temp_openai).trigger('input');
5806 }
5807
5808 if (oai_settings.chat_completion_source === chat_completion_sources.AIMLAPI) {
5809 let maxContext;
5810 if (oai_settings.max_context_unlocked) {
5811 maxContext = unlocked_max;
5812 } else {
5813 const model = model_list.find(m => m.id === oai_settings.aimlapi_model);
5814 maxContext = (model?.info?.contextLength ?? model?.context_length) || max_32k;
5815 console.log('[AI/ML API] Model CTX:', model?.info?.contextLength);
5816 }
5817
5818 $('#openai_max_context')
5819 .prop('max', maxContext)
5820 .val(Math.min(Number(oai_settings.openai_max_context), maxContext))
5821 .trigger('input');
5822
5823 $('#temp_openai')
5824 .prop('max', oai_max_temp)
5825 .val(Number(oai_settings.temp_openai))
5826 .trigger('input');
5827
5828 oai_settings.openai_max_context = Number($('#openai_max_context').val());
5829 oai_settings.temp_openai = Number($('#temp_openai').val());
5830 }
5831
5832 if (oai_settings.chat_completion_source === chat_completion_sources.COHERE) {
5833 oai_settings.pres_pen_openai = Math.min(Math.max(0, oai_settings.pres_pen_openai), 1);
5834 $('#pres_pen_openai').attr('max', 1).attr('min', 0).val(oai_settings.pres_pen_openai).trigger('input');
5835 oai_settings.freq_pen_openai = Math.min(Math.max(0, oai_settings.freq_pen_openai), 1);
5836 $('#freq_pen_openai').attr('max', 1).attr('min', 0).val(oai_settings.freq_pen_openai).trigger('input');
5837 } else {
5838 $('#pres_pen_openai').attr('max', 2).attr('min', -2).val(oai_settings.pres_pen_openai).trigger('input');
5839 $('#freq_pen_openai').attr('max', 2).attr('min', -2).val(oai_settings.freq_pen_openai).trigger('input');
5840 }
5841
5842 if (oai_settings.chat_completion_source === chat_completion_sources.MOONSHOT) {
5843 const maxContext = getMoonshotMaxContext(oai_settings.moonshot_model, oai_settings.max_context_unlocked);
5844 $('#openai_max_context').attr('max', maxContext);
5845 oai_settings.openai_max_context = Math.min(Number($('#openai_max_context').attr('max')), oai_settings.openai_max_context);
5846 $('#openai_max_context').val(oai_settings.openai_max_context).trigger('input');
5847 oai_settings.temp_openai = Math.min(claude_max_temp, oai_settings.temp_openai);
5848 $('#temp_openai').attr('max', claude_max_temp).val(oai_settings.temp_openai).trigger('input');
5849 }
5850
5851 if (oai_settings.chat_completion_source === chat_completion_sources.FIREWORKS) {
5852 const maxContext = getFireworksMaxContext(oai_settings.fireworks_model, oai_settings.max_context_unlocked);
5853 $('#openai_max_context').attr('max', maxContext);
5854 oai_settings.openai_max_context = Math.min(Number($('#openai_max_context').attr('max')), oai_settings.openai_max_context);
5855 $('#openai_max_context').val(oai_settings.openai_max_context).trigger('input');
5856 oai_settings.temp_openai = Math.min(oai_max_temp, oai_settings.temp_openai);
5857 $('#temp_openai').attr('max', oai_max_temp).val(oai_settings.temp_openai).trigger('input');
5858 }
5859
5860 if (oai_settings.chat_completion_source === chat_completion_sources.SILICONFLOW) {
5861 const maxContext = getSiliconflowMaxContext(oai_settings.siliconflow_model, oai_settings.max_context_unlocked);
5862 $('#openai_max_context').attr('max', maxContext);
5863 oai_settings.openai_max_context = Math.min(Number($('#openai_max_context').attr('max')), oai_settings.openai_max_context);
5864 $('#openai_max_context').val(oai_settings.openai_max_context).trigger('input');
5865 oai_settings.temp_openai = Math.min(oai_max_temp, oai_settings.temp_openai);
5866 $('#temp_openai').attr('max', oai_max_temp).val(oai_settings.temp_openai).trigger('input');
5867 }
5868
5869 if (oai_settings.chat_completion_source === chat_completion_sources.MINIMAX) {
5870 const maxContext = oai_settings.minimax_model === 'M2-her' ? 65536 : 204800;
5871 $('#openai_max_context').attr('max', maxContext);
5872 oai_settings.openai_max_context = Math.min(Number($('#openai_max_context').attr('max')), oai_settings.openai_max_context);
5873 $('#openai_max_context').val(oai_settings.openai_max_context).trigger('input');
5874 oai_settings.temp_openai = Math.min(claude_max_temp, oai_settings.temp_openai);
5875 $('#temp_openai').attr('max', claude_max_temp).val(oai_settings.temp_openai).trigger('input');
5876 }
5877
5878 if (oai_settings.chat_completion_source == chat_completion_sources.ZAI) {
5879 const maxContext = getZaiMaxContext(oai_settings.zai_model, oai_settings.max_context_unlocked);
5880 $('#openai_max_context').attr('max', maxContext);
5881 oai_settings.openai_max_context = Math.min(Number($('#openai_max_context').attr('max')), oai_settings.openai_max_context);
5882 $('#openai_max_context').val(oai_settings.openai_max_context).trigger('input');
5883 oai_settings.temp_openai = Math.min(claude_max_temp, oai_settings.temp_openai);
5884 $('#temp_openai').attr('max', claude_max_temp).val(oai_settings.temp_openai).trigger('input');
5885 }
5886
5887 $('#openai_max_context_counter').attr('max', Number($('#openai_max_context').attr('max')));
5888
5889 saveSettingsDebounced();
5890 updateFeatureSupportFlags();
5891 eventSource.emit(event_types.CHATCOMPLETION_MODEL_CHANGED, value);
5892}
5893
5894async function onNewPresetClick() {
5895 const name = await Popup.show.input(t`Preset name:`, t`Hint: Use a character/group name to bind preset to a specific chat.`, oai_settings.preset_settings_openai);
5896
5897 if (!name) {
5898 return;
5899 }
5900
5901 await saveOpenAIPreset(name, oai_settings);
5902}
5903
5904function onReverseProxyInput() {
5905 oai_settings.reverse_proxy = String($(this).val());
5906 $('.reverse_proxy_warning').toggle(oai_settings.reverse_proxy != '');
5907 saveSettingsDebounced();
5908}
5909
5910async function onConnectButtonClick(e) {
5911 e.stopPropagation();
5912
5913 /** @type {Object.<string, {key: string, selector: string, proxy?: boolean, keyless?: boolean}>} */
5914 const apiSourceConfig = {
5915 [chat_completion_sources.OPENROUTER]: { key: SECRET_KEYS.OPENROUTER, selector: '#api_key_openrouter', proxy: false },
5916 [chat_completion_sources.MAKERSUITE]: { key: SECRET_KEYS.MAKERSUITE, selector: '#api_key_makersuite', proxy: true },
5917 [chat_completion_sources.CLAUDE]: { key: SECRET_KEYS.CLAUDE, selector: '#api_key_claude', proxy: true },
5918 [chat_completion_sources.OPENAI]: { key: SECRET_KEYS.OPENAI, selector: '#api_key_openai', proxy: true },
5919 [chat_completion_sources.AI21]: { key: SECRET_KEYS.AI21, selector: '#api_key_ai21', proxy: false },
5920 [chat_completion_sources.MISTRALAI]: { key: SECRET_KEYS.MISTRALAI, selector: '#api_key_mistralai', proxy: true },
5921 [chat_completion_sources.CUSTOM]: { key: SECRET_KEYS.CUSTOM, selector: '#api_key_custom', proxy: false, keyless: true },
5922 [chat_completion_sources.COHERE]: { key: SECRET_KEYS.COHERE, selector: '#api_key_cohere', proxy: false },
5923 [chat_completion_sources.PERPLEXITY]: { key: SECRET_KEYS.PERPLEXITY, selector: '#api_key_perplexity', proxy: false },
5924 [chat_completion_sources.GROQ]: { key: SECRET_KEYS.GROQ, selector: '#api_key_groq', proxy: false },
5925 [chat_completion_sources.SILICONFLOW]: { key: SECRET_KEYS.SILICONFLOW, selector: '#api_key_siliconflow', proxy: false },
5926 [chat_completion_sources.ELECTRONHUB]: { key: SECRET_KEYS.ELECTRONHUB, selector: '#api_key_electronhub', proxy: false },
5927 [chat_completion_sources.NANOGPT]: { key: SECRET_KEYS.NANOGPT, selector: '#api_key_nanogpt', proxy: false },
5928 [chat_completion_sources.DEEPSEEK]: { key: SECRET_KEYS.DEEPSEEK, selector: '#api_key_deepseek', proxy: true },
5929 [chat_completion_sources.XAI]: { key: SECRET_KEYS.XAI, selector: '#api_key_xai', proxy: true },
5930 [chat_completion_sources.AIMLAPI]: { key: SECRET_KEYS.AIMLAPI, selector: '#api_key_aimlapi', proxy: false },
5931 [chat_completion_sources.MOONSHOT]: { key: SECRET_KEYS.MOONSHOT, selector: '#api_key_moonshot', proxy: true },
5932 [chat_completion_sources.FIREWORKS]: { key: SECRET_KEYS.FIREWORKS, selector: '#api_key_fireworks', proxy: false },
5933 [chat_completion_sources.COMETAPI]: { key: SECRET_KEYS.COMETAPI, selector: '#api_key_cometapi', proxy: false },
5934 [chat_completion_sources.AZURE_OPENAI]: { key: SECRET_KEYS.AZURE_OPENAI, selector: '#api_key_azure_openai', proxy: false },
5935 [chat_completion_sources.ZAI]: { key: SECRET_KEYS.ZAI, selector: '#api_key_zai', proxy: true },
5936 [chat_completion_sources.CHUTES]: { key: SECRET_KEYS.CHUTES, selector: '#api_key_chutes', proxy: false },
5937 [chat_completion_sources.POLLINATIONS]: { key: SECRET_KEYS.POLLINATIONS, selector: '#api_key_pollinations', proxy: false },
5938 [chat_completion_sources.WORKERS_AI]: { key: SECRET_KEYS.WORKERS_AI, selector: '#api_key_workers_ai', proxy: false },
5939 [chat_completion_sources.MINIMAX]: { key: SECRET_KEYS.MINIMAX, selector: '#api_key_minimax', proxy: false },
5940 };
5941
5942 // Vertex AI Express version - use API key
5943 if (oai_settings.vertexai_auth_mode === 'express') {
5944 apiSourceConfig[chat_completion_sources.VERTEXAI] = { key: SECRET_KEYS.VERTEXAI, selector: '#api_key_vertexai', proxy: true };
5945 }
5946
5947 // Vertex AI Full version - use service account
5948 if (oai_settings.chat_completion_source === chat_completion_sources.VERTEXAI && oai_settings.vertexai_auth_mode === 'full') {
5949 if (!secret_state[SECRET_KEYS.VERTEXAI_SERVICE_ACCOUNT]) {
5950 toastr.error(t`Service Account JSON is required for Vertex AI full version. Please validate and save your Service Account JSON.`);
5951 return;
5952 }
5953 }
5954
5955 // Other generic configs
5956 const config = apiSourceConfig[oai_settings.chat_completion_source];
5957 if (config) {
5958 const apiKey = String($(config.selector).val()).trim();
5959 if (apiKey.length) {
5960 await writeSecret(config.key, apiKey);
5961 }
5962
5963 if (!secret_state[config.key] && (!config.proxy || !oai_settings.reverse_proxy) && !config.keyless) {
5964 console.log(`No secret key saved for ${oai_settings.chat_completion_source}`);
5965 return;
5966 }
5967 }
5968
5969 startStatusLoading();
5970 saveSettingsDebounced();
5971 await getStatusOpen();
5972}
5973
5974function toggleChatCompletionForms() {
5975 if (oai_settings.chat_completion_source == chat_completion_sources.CLAUDE) {
5976 $('#model_claude_select').trigger('change');
5977 } else if (oai_settings.chat_completion_source == chat_completion_sources.OPENAI) {
5978 if (oai_settings.show_external_models && (!Array.isArray(model_list) || model_list.length == 0)) {
5979 // Wait until the models list is loaded so that we could show a proper saved model
5980 } else {
5981 $('#model_openai_select').trigger('change');
5982 }
5983 } else if (oai_settings.chat_completion_source == chat_completion_sources.MAKERSUITE) {
5984 $('#model_google_select').trigger('change');
5985 } else if (oai_settings.chat_completion_source == chat_completion_sources.VERTEXAI) {
5986 $('#model_vertexai_select').trigger('change');
5987 // Update UI based on authentication mode
5988 onVertexAIAuthModeChange.call($('#vertexai_auth_mode')[0]);
5989 } else if (oai_settings.chat_completion_source == chat_completion_sources.OPENROUTER) {
5990 $('#model_openrouter_select').trigger('change');
5991 } else if (oai_settings.chat_completion_source == chat_completion_sources.AI21) {
5992 $('#model_ai21_select').trigger('change');
5993 } else if (oai_settings.chat_completion_source == chat_completion_sources.MISTRALAI) {
5994 $('#model_mistralai_select').trigger('change');
5995 } else if (oai_settings.chat_completion_source == chat_completion_sources.COHERE) {
5996 $('#model_cohere_select').trigger('change');
5997 } else if (oai_settings.chat_completion_source == chat_completion_sources.PERPLEXITY) {
5998 $('#model_perplexity_select').trigger('change');
5999 } else if (oai_settings.chat_completion_source == chat_completion_sources.GROQ) {
6000 $('#model_groq_select').trigger('change');
6001 } else if (oai_settings.chat_completion_source == chat_completion_sources.CHUTES) {
6002 $('#model_chutes_select').trigger('change');
6003 } else if (oai_settings.chat_completion_source == chat_completion_sources.SILICONFLOW) {
6004 $('#model_siliconflow_select').trigger('change');
6005 } else if (oai_settings.chat_completion_source == chat_completion_sources.MINIMAX) {
6006 $('#model_minimax_select').trigger('change');
6007 } else if (oai_settings.chat_completion_source == chat_completion_sources.ELECTRONHUB) {
6008 $('#model_electronhub_select').trigger('change');
6009 } else if (oai_settings.chat_completion_source == chat_completion_sources.NANOGPT) {
6010 $('#model_nanogpt_select').trigger('change');
6011 } else if (oai_settings.chat_completion_source == chat_completion_sources.CUSTOM) {
6012 $('#model_custom_select').trigger('change');
6013 } else if (oai_settings.chat_completion_source == chat_completion_sources.DEEPSEEK) {
6014 $('#model_deepseek_select').trigger('change');
6015 } else if (oai_settings.chat_completion_source == chat_completion_sources.AIMLAPI) {
6016 $('#model_aimlapi_select').trigger('change');
6017 } else if (oai_settings.chat_completion_source == chat_completion_sources.XAI) {
6018 $('#model_xai_select').trigger('change');
6019 } else if (oai_settings.chat_completion_source == chat_completion_sources.POLLINATIONS) {
6020 $('#model_pollinations_select').trigger('change');
6021 } else if (oai_settings.chat_completion_source == chat_completion_sources.MOONSHOT) {
6022 $('#model_moonshot_select').trigger('change');
6023 } else if (oai_settings.chat_completion_source == chat_completion_sources.FIREWORKS) {
6024 $('#model_fireworks_select').trigger('change');
6025 } else if (oai_settings.chat_completion_source == chat_completion_sources.COMETAPI) {
6026 $('#model_cometapi_select').trigger('change');
6027 } else if (oai_settings.chat_completion_source == chat_completion_sources.AZURE_OPENAI) {
6028 $('#azure_openai_model').trigger('change');
6029 } else if (oai_settings.chat_completion_source == chat_completion_sources.ZAI) {
6030 $('#model_zai_select').trigger('change');
6031 } else if (oai_settings.chat_completion_source == chat_completion_sources.WORKERS_AI) {
6032 $('#model_workers_ai_select').trigger('change');
6033 }
6034
6035 $('[data-source]').each(function () {
6036 const mode = $(this).data('source-mode');
6037 const validSources = $(this).data('source').split(',');
6038 const matchesSource = validSources.includes(oai_settings.chat_completion_source);
6039 $(this).toggle(mode !== 'except' ? matchesSource : !matchesSource);
6040 });
6041
6042 setToolReasoningControls();
6043}
6044
6045async function testApiConnection() {
6046 // Check if the previous request is still in progress
6047 if (is_send_press) {
6048 toastr.info(t`Please wait for the previous request to complete.`);
6049 return;
6050 }
6051
6052 try {
6053 const reply = await sendOpenAIRequest('quiet', [{ 'role': 'user', 'content': 'Hi' }], new AbortController().signal);
6054 console.log(reply);
6055 toastr.success(t`API connection successful!`);
6056 } catch (err) {
6057 toastr.error(t`Could not get a reply from API. Check your connection settings / API key and try again.`);
6058 }
6059}
6060
6061function reconnectOpenAi() {
6062 if (main_api == 'openai') {
6063 setOnlineStatus('no_connection');
6064 resultCheckStatus();
6065 $('#api_button_openai').trigger('click');
6066 }
6067}
6068
6069function onProxyPasswordShowClick() {
6070 const $input = $('#openai_proxy_password');
6071 const type = $input.attr('type') === 'password' ? 'text' : 'password';
6072 $input.attr('type', type);
6073 $(this).toggleClass('fa-eye-slash fa-eye');
6074}
6075
6076async function onCustomizeParametersClick() {
6077 const template = $(await renderTemplateAsync('customEndpointAdditionalParameters'));
6078
6079 template.find('#custom_include_body').val(oai_settings.custom_include_body).on('input', function () {
6080 oai_settings.custom_include_body = String($(this).val());
6081 saveSettingsDebounced();
6082 });
6083
6084 template.find('#custom_exclude_body').val(oai_settings.custom_exclude_body).on('input', function () {
6085 oai_settings.custom_exclude_body = String($(this).val());
6086 saveSettingsDebounced();
6087 });
6088
6089 template.find('#custom_include_headers').val(oai_settings.custom_include_headers).on('input', function () {
6090 oai_settings.custom_include_headers = String($(this).val());
6091 saveSettingsDebounced();
6092 });
6093
6094 await callGenericPopup(template, POPUP_TYPE.TEXT, '', { wide: true, large: true });
6095}
6096
6097/**
6098 * Check if the model supports image inlining
6099 * @returns {boolean} True if the model supports image inlining
6100 */
6101export function isImageInliningSupported() {
6102 if (main_api !== 'openai') {
6103 return false;
6104 }
6105
6106 if (!oai_settings.media_inlining) {
6107 return false;
6108 }
6109
6110 // gultra just isn't being offered as multimodal, thanks google.
6111 const visionSupportedModels = [
6112 // OpenAI
6113 'chatgpt-4o-latest',
6114 'gpt-4-turbo',
6115 'gpt-4-vision',
6116 'gpt-4.1',
6117 'gpt-4.5-preview',
6118 'gpt-4o',
6119 'gpt-5',
6120 'o1',
6121 'o3',
6122 'o4-mini',
6123 // Claude
6124 'claude-3',
6125 'claude-opus-4',
6126 'claude-sonnet-4',
6127 'claude-haiku-4',
6128 // Cohere
6129 'c4ai-aya-vision',
6130 'command-a-vision',
6131 // Google AI Studio
6132 'gemini-2.0',
6133 'gemini-2.5',
6134 'gemini-3',
6135 'gemini-exp-1206',
6136 'learnlm',
6137 'gemini-robotics',
6138 'gemma-3-27b',
6139 'gemma-3-12b',
6140 'gemma-3-4b',
6141 'gemma-4',
6142 // MistralAI
6143 'mistral-small-2503',
6144 'mistral-small-2506',
6145 'mistral-small-latest',
6146 'mistral-medium-latest',
6147 'mistral-medium-2505',
6148 'mistral-medium-2508',
6149 'pixtral',
6150 // xAI (Grok)
6151 'grok-4',
6152 'grok-2-vision',
6153 // Moonshot
6154 'moonshot-v1-8k-vision-preview',
6155 'moonshot-v1-32k-vision-preview',
6156 'moonshot-v1-128k-vision-preview',
6157 'kimi-k2.5',
6158 'kimi-latest',
6159 // Z.AI (GLM)
6160 'glm-4.5v',
6161 'glm-4.6v',
6162 'glm-5v-turbo',
6163 'autoglm-phone',
6164 // SiliconFlow
6165 'Qwen/Qwen3-VL-32B-Instruct',
6166 'Qwen/Qwen3-VL-8B-Instruct',
6167 'Qwen/Qwen3-VL-235B-A22B-Instruct',
6168 'Qwen/Qwen3-VL-30B-A3B-Instruct',
6169 'zai-org/GLM-4.5V',
6170 ];
6171
6172 switch (oai_settings.chat_completion_source) {
6173 case chat_completion_sources.OPENAI:
6174 case chat_completion_sources.AZURE_OPENAI: {
6175 const modelToCheck = oai_settings.chat_completion_source === chat_completion_sources.AZURE_OPENAI
6176 ? oai_settings.azure_openai_model
6177 : oai_settings.openai_model;
6178 return visionSupportedModels.some(model =>
6179 modelToCheck.includes(model)
6180 && ['gpt-4-turbo-preview', 'o1-mini', 'o3-mini'].some(x => !modelToCheck.includes(x)),
6181 );
6182 }
6183 case chat_completion_sources.MAKERSUITE:
6184 return visionSupportedModels.some(model => oai_settings.google_model.includes(model));
6185 case chat_completion_sources.VERTEXAI:
6186 return visionSupportedModels.some(model => oai_settings.vertexai_model.includes(model));
6187 case chat_completion_sources.CLAUDE:
6188 return visionSupportedModels.some(model => oai_settings.claude_model.includes(model));
6189 case chat_completion_sources.OPENROUTER:
6190 return (Array.isArray(model_list) && model_list.find(m => m.id === oai_settings.openrouter_model)?.architecture?.input_modalities?.includes('image'));
6191 case chat_completion_sources.CUSTOM:
6192 return true;
6193 case chat_completion_sources.MISTRALAI:
6194 return (Array.isArray(model_list) && model_list.find(m => m.id === oai_settings.mistralai_model)?.capabilities?.vision);
6195 case chat_completion_sources.COHERE:
6196 return visionSupportedModels.some(model => oai_settings.cohere_model.includes(model));
6197 case chat_completion_sources.XAI:
6198 // TODO: xAI's /models endpoint doesn't return modality info
6199 return visionSupportedModels.some(model => oai_settings.xai_model.includes(model));
6200 case chat_completion_sources.AIMLAPI:
6201 return (Array.isArray(model_list) && model_list.find(m => m.id === oai_settings.aimlapi_model)?.features?.includes('openai/chat-completion.vision'));
6202 case chat_completion_sources.CHUTES:
6203 return (Array.isArray(model_list) && model_list.find(m => m.id === oai_settings.chutes_model)?.input_modalities?.includes('image'));
6204 case chat_completion_sources.ELECTRONHUB:
6205 return (Array.isArray(model_list) && model_list.find(m => m.id === oai_settings.electronhub_model)?.metadata?.vision);
6206 case chat_completion_sources.POLLINATIONS:
6207 return (Array.isArray(model_list) && model_list.find(m => m.id === oai_settings.pollinations_model)?.input_modalities?.includes('image'));
6208 case chat_completion_sources.COMETAPI:
6209 return true;
6210 case chat_completion_sources.MOONSHOT:
6211 return (Array.isArray(model_list) && model_list.find(m => m.id === oai_settings.moonshot_model)?.supports_image_in);
6212 case chat_completion_sources.NANOGPT:
6213 return (Array.isArray(model_list) && model_list.find(m => m.id === oai_settings.nanogpt_model)?.capabilities?.vision);
6214 case chat_completion_sources.ZAI:
6215 return visionSupportedModels.some(model => oai_settings.zai_model.includes(model));
6216 case chat_completion_sources.SILICONFLOW:
6217 return visionSupportedModels.some(model => oai_settings.siliconflow_model.includes(model));
6218 case chat_completion_sources.WORKERS_AI: {
6219 const waiModel = Array.isArray(model_list) && model_list.find(m => m.id === oai_settings.workers_ai_model);
6220 return Boolean(waiModel && Array.isArray(waiModel.properties) && waiModel.properties.some(p => p.property_id === 'vision' && p.value === 'true'));
6221 }
6222 default:
6223 return false;
6224 }
6225}
6226
6227/**
6228 * Check if the model supports video inlining
6229 * @returns {boolean} True if the model supports video inlining
6230 */
6231export function isVideoInliningSupported() {
6232 if (main_api !== 'openai') {
6233 return false;
6234 }
6235
6236 if (!oai_settings.media_inlining) {
6237 return false;
6238 }
6239
6240 const videoSupportedModels = [
6241 // Gemini
6242 'gemini-2.0',
6243 'gemini-2.5',
6244 'gemini-exp-1206',
6245 'gemini-3',
6246 'gemma-4',
6247 // Z.AI (GLM)
6248 'glm-4.5v',
6249 'glm-4.6v',
6250 'glm-5v-turbo',
6251 ];
6252
6253 switch (oai_settings.chat_completion_source) {
6254 case chat_completion_sources.MAKERSUITE:
6255 return videoSupportedModels.some(model => oai_settings.google_model.includes(model));
6256 case chat_completion_sources.VERTEXAI:
6257 return videoSupportedModels.some(model => oai_settings.vertexai_model.includes(model));
6258 case chat_completion_sources.OPENROUTER:
6259 return (Array.isArray(model_list) && model_list.find(m => m.id === oai_settings.openrouter_model)?.architecture?.input_modalities?.includes('video'));
6260 case chat_completion_sources.ZAI:
6261 return videoSupportedModels.some(model => oai_settings.zai_model.includes(model));
6262 default:
6263 return false;
6264 }
6265}
6266
6267/**
6268 * Check if the model supports video inlining
6269 * @returns {boolean} True if the model supports audio inlining
6270 */
6271export function isAudioInliningSupported() {
6272 if (main_api !== 'openai') {
6273 return false;
6274 }
6275
6276 if (!oai_settings.media_inlining) {
6277 return false;
6278 }
6279
6280 const audioSupportedModels = [
6281 'gemini-2.0',
6282 'gemini-2.5',
6283 'gemini-3',
6284 'gemini-exp-1206',
6285 'gpt-4o-audio',
6286 'gpt-4o-realtime',
6287 'gpt-4o-mini-audio',
6288 'gpt-4o-mini-realtime',
6289 'gpt-audio',
6290 'gpt-realtime',
6291 ];
6292
6293 switch (oai_settings.chat_completion_source) {
6294 case chat_completion_sources.OPENAI:
6295 return audioSupportedModels.some(model => oai_settings.openai_model.includes(model));
6296 case chat_completion_sources.MAKERSUITE:
6297 return audioSupportedModels.some(model => oai_settings.google_model.includes(model));
6298 case chat_completion_sources.VERTEXAI:
6299 return audioSupportedModels.some(model => oai_settings.vertexai_model.includes(model));
6300 case chat_completion_sources.OPENROUTER:
6301 return (Array.isArray(model_list) && model_list.find(m => m.id === oai_settings.openrouter_model)?.architecture?.input_modalities?.includes('audio'));
6302 case chat_completion_sources.CUSTOM:
6303 return true;
6304 default:
6305 return false;
6306 }
6307}
6308
6309/**
6310 * Gets the tool-call reasoning forwarding mode.
6311 * @param {ChatCompletionSettings} settings Settings object to use
6312 * @returns {string} Reasoning forwarding mode
6313 */
6314function getToolReasoningMode(settings = oai_settings) {
6315 const mode = String(settings.tool_reasoning_mode ?? '');
6316 if (Object.values(tool_reasoning_modes).includes(mode)) {
6317 return mode;
6318 }
6319 return tool_reasoning_modes.DISABLED;
6320}
6321
6322/**
6323 * Gets the effective tool-call reasoning forwarding mode.
6324 * Interleaved thinking requires explicit reasoning requests.
6325 * @param {ChatCompletionSettings} settings Settings object to use
6326 * @returns {string} Effective reasoning forwarding mode
6327 */
6328function getEffectiveToolReasoningMode(settings = oai_settings) {
6329 if (!settings.show_thoughts) {
6330 return tool_reasoning_modes.DISABLED;
6331 }
6332
6333 return getToolReasoningMode(settings);
6334}
6335
6336/**
6337 * Check if the model supports encrypted reasoning signatures.
6338 * @param {ChatCompletionSettings} settings Settings object to use
6339 * @returns {boolean} True if reasoning signatures should be included in the request
6340 */
6341export function isReasoningSignatureSupported(settings = oai_settings) {
6342 // If it's Vertex AI or Makersuite, that's OK - convertGooglePrompt() will handle it later
6343 const isGoogle = [chat_completion_sources.VERTEXAI, chat_completion_sources.MAKERSUITE].includes(settings.chat_completion_source);
6344 // Need a more crunchy check for OpenRouter: look for Gemini models
6345 const isOpenRouterGemini = settings.chat_completion_source === chat_completion_sources.OPENROUTER && /google\/gemini/i.test(settings.openrouter_model);
6346 return isGoogle || isOpenRouterGemini;
6347}
6348
6349/**
6350 * Proxy stuff
6351 */
6352export function loadProxyPresets(settings) {
6353 let proxyPresets = settings.proxies;
6354 selected_proxy = settings.selected_proxy || selected_proxy;
6355 if (!Array.isArray(proxyPresets) || proxyPresets.length === 0) {
6356 proxyPresets = proxies;
6357 } else {
6358 proxies = proxyPresets;
6359 }
6360
6361 $('#openai_proxy_preset').empty();
6362
6363 for (const preset of proxyPresets) {
6364 const option = document.createElement('option');
6365 option.innerText = preset.name;
6366 option.value = preset.name;
6367 option.selected = preset.name === 'None';
6368 $('#openai_proxy_preset').append(option);
6369 }
6370 $('#openai_proxy_preset').val(selected_proxy.name);
6371 setProxyPreset(selected_proxy.name, selected_proxy.url, selected_proxy.password);
6372}
6373
6374function setProxyPreset(name, url, password) {
6375 const preset = proxies.find(p => p.name === name);
6376 if (preset) {
6377 preset.url = url;
6378 preset.password = password;
6379 selected_proxy = preset;
6380 } else {
6381 let new_proxy = { name, url, password };
6382 proxies.push(new_proxy);
6383 selected_proxy = new_proxy;
6384 }
6385
6386 $('#openai_reverse_proxy_name').val(name);
6387 oai_settings.reverse_proxy = url;
6388 $('#openai_reverse_proxy').val(oai_settings.reverse_proxy);
6389 oai_settings.proxy_password = password;
6390 $('#openai_proxy_password').val(oai_settings.proxy_password);
6391 reconnectOpenAi();
6392}
6393
6394function onProxyPresetChange() {
6395 const value = String($('#openai_proxy_preset').find(':selected').val());
6396 const selectedPreset = proxies.find(preset => preset.name === value);
6397
6398 if (selectedPreset) {
6399 setProxyPreset(selectedPreset.name, selectedPreset.url, selectedPreset.password);
6400 } else {
6401 console.error(t`Proxy preset '${value}' not found in proxies array.`);
6402 }
6403 saveSettingsDebounced();
6404}
6405
6406$('#save_proxy').on('click', async function () {
6407 const presetName = $('#openai_reverse_proxy_name').val();
6408 const reverseProxy = $('#openai_reverse_proxy').val();
6409 const proxyPassword = $('#openai_proxy_password').val();
6410
6411 setProxyPreset(presetName, reverseProxy, proxyPassword);
6412 saveSettingsDebounced();
6413 toastr.success(t`Proxy Saved`);
6414 if ($('#openai_proxy_preset').val() !== presetName) {
6415 const option = document.createElement('option');
6416 option.text = String(presetName);
6417 option.value = String(presetName);
6418
6419 $('#openai_proxy_preset').append(option);
6420 }
6421 $('#openai_proxy_preset').val(presetName);
6422});
6423
6424$('#delete_proxy').on('click', async function () {
6425 const presetName = $('#openai_reverse_proxy_name').val();
6426 const index = proxies.findIndex(preset => preset.name === presetName);
6427
6428 if (index !== -1) {
6429 proxies.splice(index, 1);
6430 $('#openai_proxy_preset option[value="' + presetName + '"]').remove();
6431
6432 if (proxies.length > 0) {
6433 const newIndex = Math.max(0, index - 1);
6434 selected_proxy = proxies[newIndex];
6435 } else {
6436 selected_proxy = { name: 'None', url: '', password: '' };
6437 }
6438
6439 $('#openai_reverse_proxy_name').val(selected_proxy.name);
6440 oai_settings.reverse_proxy = selected_proxy.url;
6441 $('#openai_reverse_proxy').val(selected_proxy.url);
6442 oai_settings.proxy_password = selected_proxy.password;
6443 $('#openai_proxy_password').val(selected_proxy.password);
6444
6445 saveSettingsDebounced();
6446 $('#openai_proxy_preset').val(selected_proxy.name);
6447 toastr.success(t`Proxy Deleted`);
6448 } else {
6449 toastr.error(t`Could not find proxy with name '${presetName}'`);
6450 }
6451});
6452
6453function runProxyCallback(_, value) {
6454 if (!value) {
6455 return selected_proxy?.name || '';
6456 }
6457
6458 const proxyNames = proxies.map(preset => preset.name);
6459 const fuse = new Fuse(proxyNames);
6460 const result = fuse.search(value);
6461
6462 if (result.length === 0) {
6463 toastr.warning(t`Proxy preset '${value}' not found`);
6464 return '';
6465 }
6466
6467 const foundName = result[0].item;
6468 $('#openai_proxy_preset').val(foundName).trigger('change');
6469 return foundName;
6470}
6471
6472/**
6473 * Handle Vertex AI authentication mode change
6474 */
6475function onVertexAIAuthModeChange() {
6476 const authMode = String($(this).val());
6477 oai_settings.vertexai_auth_mode = authMode;
6478
6479 $('#vertexai_form [data-mode]').each(function () {
6480 const mode = $(this).data('mode');
6481 $(this).toggle(mode === authMode);
6482 $(this).find('option').toggle(mode === authMode);
6483 });
6484
6485 saveSettingsDebounced();
6486}
6487
6488/**
6489 * Validate Vertex AI service account JSON
6490 */
6491async function onVertexAIValidateServiceAccount() {
6492 const jsonContent = String($('#vertexai_service_account_json').val()).trim();
6493
6494 if (!jsonContent) {
6495 toastr.error(t`Please enter Service Account JSON content`);
6496 return;
6497 }
6498
6499 try {
6500 const serviceAccount = JSON.parse(jsonContent);
6501 const requiredFields = ['type', 'project_id', 'private_key', 'client_email', 'client_id'];
6502 const missingFields = requiredFields.filter(field => !serviceAccount[field]);
6503
6504 if (missingFields.length > 0) {
6505 toastr.error(t`Missing required fields: ${missingFields.join(', ')}`);
6506 updateVertexAIServiceAccountStatus(false, t`Missing fields: ${missingFields.join(', ')}`);
6507 return;
6508 }
6509
6510 if (serviceAccount.type !== 'service_account') {
6511 toastr.error(t`Invalid service account type. Expected "service_account"`);
6512 updateVertexAIServiceAccountStatus(false, t`Invalid service account type`);
6513 return;
6514 }
6515
6516 // Save to backend secret storage
6517 const keyLabel = serviceAccount.client_email || '';
6518 await writeSecret(SECRET_KEYS.VERTEXAI_SERVICE_ACCOUNT, jsonContent, keyLabel);
6519
6520 // Show success status
6521 updateVertexAIServiceAccountStatus(true, `Project: ${serviceAccount.project_id}, Email: ${serviceAccount.client_email}`);
6522
6523 toastr.success(t`Service Account JSON is valid and saved securely`);
6524 saveSettingsDebounced();
6525 } catch (error) {
6526 console.error('JSON validation error:', error);
6527 toastr.error(t`Invalid JSON format`);
6528 updateVertexAIServiceAccountStatus(false, t`Invalid JSON format`);
6529 }
6530}
6531
6532/**
6533 * Clear Vertex AI service account JSON
6534 */
6535async function onVertexAIClearServiceAccount() {
6536 $('#vertexai_service_account_json').val('');
6537
6538 // Clear from backend secret storage
6539 await writeSecret(SECRET_KEYS.VERTEXAI_SERVICE_ACCOUNT, '');
6540
6541 updateVertexAIServiceAccountStatus(false);
6542 toastr.info(t`Service Account JSON cleared`);
6543 saveSettingsDebounced();
6544}
6545
6546/**
6547 * Handle Vertex AI service account JSON input change
6548 */
6549function onVertexAIServiceAccountJsonChange() {
6550 const jsonContent = String($(this).val()).trim();
6551
6552 // Autocomplete has been triggered, don't validate if the input is a UUID
6553 if (isUuid(jsonContent)) {
6554 return;
6555 }
6556
6557 if (jsonContent) {
6558 // Auto-validate when content is pasted
6559 try {
6560 const serviceAccount = JSON.parse(jsonContent);
6561 const requiredFields = ['type', 'project_id', 'private_key', 'client_email'];
6562 const hasAllFields = requiredFields.every(field => serviceAccount[field]);
6563
6564 if (hasAllFields && serviceAccount.type === 'service_account') {
6565 updateVertexAIServiceAccountStatus(false, t`JSON appears valid - click "Validate JSON" to save`);
6566 } else {
6567 updateVertexAIServiceAccountStatus(false, t`Incomplete or invalid JSON`);
6568 }
6569 } catch (error) {
6570 updateVertexAIServiceAccountStatus(false, t`Invalid JSON format`);
6571 }
6572 } else {
6573 updateVertexAIServiceAccountStatus(false);
6574 }
6575
6576 // Don't save settings automatically
6577 // saveSettingsDebounced();
6578}
6579
6580/**
6581 * Update the Vertex AI service account status display
6582 * @param {boolean} isValid - Whether the service account is valid
6583 * @param {string} message - Status message to display
6584 */
6585function updateVertexAIServiceAccountStatus(isValid = false, message = '') {
6586 const statusDiv = $('#vertexai_service_account_status');
6587 const infoSpan = $('#vertexai_service_account_info');
6588
6589 // If no explicit message provided, check if we have a saved service account
6590 if (!message && secret_state[SECRET_KEYS.VERTEXAI_SERVICE_ACCOUNT]) {
6591 isValid = true;
6592 message = t`Service Account JSON is saved and ready to use`;
6593 }
6594
6595 if (isValid && message) {
6596 infoSpan.html(`<i class="fa-solid fa-check-circle" style="color: green;"></i> ${message}`);
6597 statusDiv.show();
6598 } else if (!isValid && message) {
6599 infoSpan.html(`<i class="fa-solid fa-exclamation-triangle" style="color: orange;"></i> ${message}`);
6600 statusDiv.show();
6601 } else {
6602 statusDiv.hide();
6603 }
6604}
6605
6606function updateFeatureSupportFlags() {
6607 const featureFlags = {
6608 openai_function_calling_supported: ToolManager.isToolCallingSupported(),
6609 openai_image_inlining_supported: isImageInliningSupported(),
6610 openai_video_inlining_supported: isVideoInliningSupported(),
6611 openai_audio_inlining_supported: isAudioInliningSupported(),
6612 };
6613
6614 for (const [key, value] of Object.entries(featureFlags)) {
6615 const element = document.getElementById(key);
6616 if (element) {
6617 element.dataset.ccToggle = String(value ?? false);
6618 }
6619 }
6620}
6621
6622export function initOpenAI() {
6623 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
6624 name: 'proxy',
6625 callback: runProxyCallback,
6626 returns: 'current proxy',
6627 namedArgumentList: [],
6628 unnamedArgumentList: [
6629 SlashCommandArgument.fromProps({
6630 description: 'name',
6631 typeList: [ARGUMENT_TYPE.STRING],
6632 isRequired: true,
6633 enumProvider: () => proxies.map(preset => new SlashCommandEnumValue(preset.name, preset.url)),
6634 }),
6635 ],
6636 helpString: 'Sets a proxy preset by name.',
6637 }));
6638
6639 $('#test_api_button').on('click', testApiConnection);
6640
6641 $('#temp_openai').on('input', function () {
6642 oai_settings.temp_openai = Number($(this).val());
6643 $('#temp_counter_openai').val(Number($(this).val()).toFixed(2));
6644 saveSettingsDebounced();
6645 });
6646
6647 $('#freq_pen_openai').on('input', function () {
6648 oai_settings.freq_pen_openai = Number($(this).val());
6649 $('#freq_pen_counter_openai').val(Number($(this).val()).toFixed(2));
6650 saveSettingsDebounced();
6651 });
6652
6653 $('#pres_pen_openai').on('input', function () {
6654 oai_settings.pres_pen_openai = Number($(this).val());
6655 $('#pres_pen_counter_openai').val(Number($(this).val()).toFixed(2));
6656 saveSettingsDebounced();
6657 });
6658
6659 $('#top_p_openai').on('input', function () {
6660 oai_settings.top_p_openai = Number($(this).val());
6661 $('#top_p_counter_openai').val(Number($(this).val()).toFixed(2));
6662 saveSettingsDebounced();
6663 });
6664
6665 $('#top_k_openai').on('input', function () {
6666 oai_settings.top_k_openai = Number($(this).val());
6667 $('#top_k_counter_openai').val(Number($(this).val()).toFixed(0));
6668 saveSettingsDebounced();
6669 });
6670
6671 $('#top_a_openai').on('input', function () {
6672 oai_settings.top_a_openai = Number($(this).val());
6673 $('#top_a_counter_openai').val(Number($(this).val()));
6674 saveSettingsDebounced();
6675 });
6676
6677 $('#min_p_openai').on('input', function () {
6678 oai_settings.min_p_openai = Number($(this).val());
6679 $('#min_p_counter_openai').val(Number($(this).val()));
6680 saveSettingsDebounced();
6681 });
6682
6683 $('#repetition_penalty_openai').on('input', function () {
6684 oai_settings.repetition_penalty_openai = Number($(this).val());
6685 $('#repetition_penalty_counter_openai').val(Number($(this).val()));
6686 saveSettingsDebounced();
6687 });
6688
6689 $('#openai_max_context').on('input', function () {
6690 oai_settings.openai_max_context = Number($(this).val());
6691 $('#openai_max_context_counter').val(`${$(this).val()}`);
6692 calculateOpenRouterCost();
6693 calculateElectronHubCost();
6694 calculateChutesCost();
6695 saveSettingsDebounced();
6696 });
6697
6698 $('#openai_max_tokens').on('input', function () {
6699 oai_settings.openai_max_tokens = Number($(this).val());
6700 calculateOpenRouterCost();
6701 calculateElectronHubCost();
6702 calculateChutesCost();
6703 saveSettingsDebounced();
6704 });
6705
6706 $('#stream_toggle').on('change', function () {
6707 oai_settings.stream_openai = !!$('#stream_toggle').prop('checked');
6708 saveSettingsDebounced();
6709 });
6710
6711 $('#use_sysprompt').on('change', function () {
6712 oai_settings.use_sysprompt = !!$('#use_sysprompt').prop('checked');
6713 saveSettingsDebounced();
6714 });
6715
6716 $('#send_if_empty_textarea').on('input', function () {
6717 oai_settings.send_if_empty = String($('#send_if_empty_textarea').val());
6718 saveSettingsDebounced();
6719 });
6720
6721 $('#impersonation_prompt_textarea').on('input', function () {
6722 oai_settings.impersonation_prompt = String($('#impersonation_prompt_textarea').val());
6723 saveSettingsDebounced();
6724 });
6725
6726 $('#newchat_prompt_textarea').on('input', function () {
6727 oai_settings.new_chat_prompt = String($('#newchat_prompt_textarea').val());
6728 saveSettingsDebounced();
6729 });
6730
6731 $('#newgroupchat_prompt_textarea').on('input', function () {
6732 oai_settings.new_group_chat_prompt = String($('#newgroupchat_prompt_textarea').val());
6733 saveSettingsDebounced();
6734 });
6735
6736 $('#newexamplechat_prompt_textarea').on('input', function () {
6737 oai_settings.new_example_chat_prompt = String($('#newexamplechat_prompt_textarea').val());
6738 saveSettingsDebounced();
6739 });
6740
6741 $('#continue_nudge_prompt_textarea').on('input', function () {
6742 oai_settings.continue_nudge_prompt = String($('#continue_nudge_prompt_textarea').val());
6743 saveSettingsDebounced();
6744 });
6745
6746 $('#wi_format_textarea').on('input', function () {
6747 oai_settings.wi_format = String($('#wi_format_textarea').val());
6748 saveSettingsDebounced();
6749 });
6750
6751 $('#scenario_format_textarea').on('input', function () {
6752 oai_settings.scenario_format = String($('#scenario_format_textarea').val());
6753 saveSettingsDebounced();
6754 });
6755
6756 $('#personality_format_textarea').on('input', function () {
6757 oai_settings.personality_format = String($('#personality_format_textarea').val());
6758 saveSettingsDebounced();
6759 });
6760
6761 $('#group_nudge_prompt_textarea').on('input', function () {
6762 oai_settings.group_nudge_prompt = String($('#group_nudge_prompt_textarea').val());
6763 saveSettingsDebounced();
6764 });
6765
6766 $('#update_oai_preset').on('click', async function () {
6767 const name = oai_settings.preset_settings_openai;
6768 await saveOpenAIPreset(name, oai_settings, false);
6769 toastr.success(t`Preset updated`);
6770 });
6771
6772 $('#impersonation_prompt_restore').on('click', function () {
6773 oai_settings.impersonation_prompt = default_impersonation_prompt;
6774 $('#impersonation_prompt_textarea').val(oai_settings.impersonation_prompt);
6775 saveSettingsDebounced();
6776 });
6777
6778 $('#newchat_prompt_restore').on('click', function () {
6779 oai_settings.new_chat_prompt = default_new_chat_prompt;
6780 $('#newchat_prompt_textarea').val(oai_settings.new_chat_prompt);
6781 saveSettingsDebounced();
6782 });
6783
6784 $('#newgroupchat_prompt_restore').on('click', function () {
6785 oai_settings.new_group_chat_prompt = default_new_group_chat_prompt;
6786 $('#newgroupchat_prompt_textarea').val(oai_settings.new_group_chat_prompt);
6787 saveSettingsDebounced();
6788 });
6789
6790 $('#newexamplechat_prompt_restore').on('click', function () {
6791 oai_settings.new_example_chat_prompt = default_new_example_chat_prompt;
6792 $('#newexamplechat_prompt_textarea').val(oai_settings.new_example_chat_prompt);
6793 saveSettingsDebounced();
6794 });
6795
6796 $('#continue_nudge_prompt_restore').on('click', function () {
6797 oai_settings.continue_nudge_prompt = default_continue_nudge_prompt;
6798 $('#continue_nudge_prompt_textarea').val(oai_settings.continue_nudge_prompt);
6799 saveSettingsDebounced();
6800 });
6801
6802 $('#wi_format_restore').on('click', function () {
6803 oai_settings.wi_format = default_wi_format;
6804 $('#wi_format_textarea').val(oai_settings.wi_format);
6805 saveSettingsDebounced();
6806 });
6807
6808 $('#scenario_format_restore').on('click', function () {
6809 oai_settings.scenario_format = default_scenario_format;
6810 $('#scenario_format_textarea').val(oai_settings.scenario_format);
6811 saveSettingsDebounced();
6812 });
6813
6814 $('#personality_format_restore').on('click', function () {
6815 oai_settings.personality_format = default_personality_format;
6816 $('#personality_format_textarea').val(oai_settings.personality_format);
6817 saveSettingsDebounced();
6818 });
6819
6820 $('#group_nudge_prompt_restore').on('click', function () {
6821 oai_settings.group_nudge_prompt = default_group_nudge_prompt;
6822 $('#group_nudge_prompt_textarea').val(oai_settings.group_nudge_prompt);
6823 saveSettingsDebounced();
6824 });
6825
6826 $('#openai_bypass_status_check').on('input', function () {
6827 oai_settings.bypass_status_check = !!$(this).prop('checked');
6828 getStatusOpen();
6829 saveSettingsDebounced();
6830 });
6831
6832 $('#chat_completion_source').on('change', function () {
6833 cancelStatusCheck('Chat Completion source changed');
6834 model_list = [];
6835 oai_settings.chat_completion_source = String($(this).find(':selected').val());
6836 toggleChatCompletionForms();
6837 saveSettingsDebounced();
6838 reconnectOpenAi();
6839 forceCharacterEditorTokenize();
6840 updateFeatureSupportFlags();
6841 eventSource.emit(event_types.CHATCOMPLETION_SOURCE_CHANGED, oai_settings.chat_completion_source);
6842 });
6843
6844 $('#oai_max_context_unlocked').on('input', function (_e, data) {
6845 oai_settings.max_context_unlocked = !!$(this).prop('checked');
6846 if (data?.source !== 'preset') {
6847 $('#chat_completion_source').trigger('change');
6848 }
6849 saveSettingsDebounced();
6850 });
6851
6852 $('#openai_show_external_models').on('input', function () {
6853 oai_settings.show_external_models = !!$(this).prop('checked');
6854 $('#openai_external_category').toggle(oai_settings.show_external_models);
6855 saveSettingsDebounced();
6856 });
6857
6858 $('#openai_proxy_password').on('input', function () {
6859 oai_settings.proxy_password = String($(this).val());
6860 saveSettingsDebounced();
6861 });
6862
6863 $('#claude_assistant_prefill').on('input', function () {
6864 oai_settings.assistant_prefill = String($(this).val());
6865 saveSettingsDebounced();
6866 });
6867
6868 $('#claude_assistant_impersonation').on('input', function () {
6869 oai_settings.assistant_impersonation = String($(this).val());
6870 saveSettingsDebounced();
6871 });
6872
6873 $('#openrouter_use_fallback').on('input', function () {
6874 oai_settings.openrouter_use_fallback = !!$(this).prop('checked');
6875 saveSettingsDebounced();
6876 });
6877
6878 $('#openrouter_allow_fallbacks').on('input', function () {
6879 oai_settings.openrouter_allow_fallbacks = !!$(this).prop('checked');
6880 updateOpenRouterProvidersWarning('#openrouter_providers_chat');
6881 saveSettingsDebounced();
6882 });
6883
6884 $('#openrouter_middleout').on('input', function () {
6885 oai_settings.openrouter_middleout = String($(this).val());
6886 saveSettingsDebounced();
6887 });
6888
6889 $('#squash_system_messages').on('input', function () {
6890 oai_settings.squash_system_messages = !!$(this).prop('checked');
6891 saveSettingsDebounced();
6892 });
6893
6894 $('#openai_media_inlining').on('input', function () {
6895 oai_settings.media_inlining = !!$(this).prop('checked');
6896 updateFeatureSupportFlags();
6897 saveSettingsDebounced();
6898 });
6899
6900 $('#openai_inline_image_quality').on('input', function () {
6901 oai_settings.inline_image_quality = String($(this).val());
6902 saveSettingsDebounced();
6903 });
6904
6905 $('#continue_prefill').on('input', function () {
6906 oai_settings.continue_prefill = !!$(this).prop('checked');
6907 saveSettingsDebounced();
6908 });
6909
6910 $('#openai_function_calling').on('input', function () {
6911 oai_settings.function_calling = !!$(this).prop('checked');
6912 updateFeatureSupportFlags();
6913 saveSettingsDebounced();
6914 });
6915
6916 $('#tool_call_recurse_limit').on('input', function () {
6917 oai_settings.tool_call_recurse_limit = Number($(this).val());
6918 $('#tool_call_recurse_limit_counter').val(oai_settings.tool_call_recurse_limit);
6919 ToolManager.RECURSE_LIMIT = oai_settings.tool_call_recurse_limit;
6920 saveSettingsDebounced();
6921 });
6922
6923 $('#tool_reasoning_mode').on('input', function () {
6924 oai_settings.tool_reasoning_mode = getToolReasoningMode({
6925 ...oai_settings,
6926 tool_reasoning_mode: String($(this).val()),
6927 });
6928 saveSettingsDebounced();
6929 });
6930
6931 $('#seed_openai').on('input', function () {
6932 oai_settings.seed = Number($(this).val());
6933 saveSettingsDebounced();
6934 });
6935
6936 $('#n_openai').on('input', function () {
6937 oai_settings.n = Number($(this).val());
6938 saveSettingsDebounced();
6939 });
6940
6941 $('#custom_api_url_text').on('input', function () {
6942 oai_settings.custom_url = String($(this).val());
6943 saveSettingsDebounced();
6944 });
6945
6946 $('#custom_model_id').on('input', function () {
6947 oai_settings.custom_model = String($(this).val());
6948 saveSettingsDebounced();
6949 });
6950
6951 $('#custom_prompt_post_processing').on('change', function () {
6952 oai_settings.custom_prompt_post_processing = String($(this).val());
6953 updateFeatureSupportFlags();
6954 saveSettingsDebounced();
6955 });
6956
6957 $('#names_behavior').on('input', function () {
6958 oai_settings.names_behavior = Number($(this).val());
6959 setNamesBehaviorControls();
6960 saveSettingsDebounced();
6961 });
6962
6963 $('#azure_base_url').on('input', function () {
6964 oai_settings.azure_base_url = String($(this).val());
6965 saveSettingsDebounced();
6966 });
6967
6968 $('#azure_deployment_name').on('input', function () {
6969 oai_settings.azure_deployment_name = String($(this).val());
6970 saveSettingsDebounced();
6971 });
6972
6973 $('#azure_api_version').on('input change', function () {
6974 oai_settings.azure_api_version = String($(this).val());
6975 saveSettingsDebounced();
6976 });
6977
6978 $('#character_names_none').on('input', function () {
6979 oai_settings.names_behavior = character_names_behavior.NONE;
6980 setNamesBehaviorControls();
6981 saveSettingsDebounced();
6982 });
6983
6984 $('#character_names_default').on('input', function () {
6985 oai_settings.names_behavior = character_names_behavior.DEFAULT;
6986 setNamesBehaviorControls();
6987 saveSettingsDebounced();
6988 });
6989
6990 $('#character_names_completion').on('input', function () {
6991 oai_settings.names_behavior = character_names_behavior.COMPLETION;
6992 setNamesBehaviorControls();
6993 saveSettingsDebounced();
6994 });
6995
6996 $('#character_names_content').on('input', function () {
6997 oai_settings.names_behavior = character_names_behavior.CONTENT;
6998 setNamesBehaviorControls();
6999 saveSettingsDebounced();
7000 });
7001
7002 $('#continue_postifx').on('input', function () {
7003 oai_settings.continue_postfix = String($(this).val());
7004 setContinuePostfixControls();
7005 saveSettingsDebounced();
7006 });
7007
7008 $('#continue_postfix_none').on('input', function () {
7009 oai_settings.continue_postfix = continue_postfix_types.NONE;
7010 setContinuePostfixControls();
7011 saveSettingsDebounced();
7012 });
7013
7014 $('#continue_postfix_space').on('input', function () {
7015 oai_settings.continue_postfix = continue_postfix_types.SPACE;
7016 setContinuePostfixControls();
7017 saveSettingsDebounced();
7018 });
7019
7020 $('#continue_postfix_newline').on('input', function () {
7021 oai_settings.continue_postfix = continue_postfix_types.NEWLINE;
7022 setContinuePostfixControls();
7023 saveSettingsDebounced();
7024 });
7025
7026 $('#continue_postfix_double_newline').on('input', function () {
7027 oai_settings.continue_postfix = continue_postfix_types.DOUBLE_NEWLINE;
7028 setContinuePostfixControls();
7029 saveSettingsDebounced();
7030 });
7031
7032 $('#openai_show_thoughts').on('input', function () {
7033 oai_settings.show_thoughts = !!$(this).prop('checked');
7034 setToolReasoningControls();
7035 saveSettingsDebounced();
7036 });
7037
7038 $('#openai_reasoning_effort').on('input', function () {
7039 oai_settings.reasoning_effort = String($(this).val());
7040 saveSettingsDebounced();
7041 });
7042
7043 $('#openai_verbosity').on('input', function () {
7044 oai_settings.verbosity = String($(this).val());
7045 saveSettingsDebounced();
7046 });
7047
7048 $('#openai_enable_web_search').on('input', function () {
7049 oai_settings.enable_web_search = !!$(this).prop('checked');
7050 calculateOpenRouterCost();
7051 saveSettingsDebounced();
7052 });
7053
7054 $('#openai_request_images').on('input', function () {
7055 oai_settings.request_images = !!$(this).prop('checked');
7056 saveSettingsDebounced();
7057 });
7058
7059 $('#request_image_resolution').on('input', function () {
7060 oai_settings.request_image_resolution = String($(this).val());
7061 saveSettingsDebounced();
7062 });
7063
7064 $('#request_image_aspect_ratio').on('input', function () {
7065 oai_settings.request_image_aspect_ratio = String($(this).val());
7066 saveSettingsDebounced();
7067 });
7068
7069 if (!CSS.supports('field-sizing', 'content')) {
7070 $(document).on('input', '#openai_settings .autoSetHeight', function () {
7071 resetScrollHeight($(this));
7072 });
7073 }
7074
7075 if (!isMobile()) {
7076 $('#model_openrouter_select').select2({
7077 placeholder: t`Select a model`,
7078 searchInputPlaceholder: t`Search models...`,
7079 searchInputCssClass: 'text_pole',
7080 width: '100%',
7081 templateResult: getOpenRouterModelTemplate,
7082 matcher: textValueMatcher,
7083 });
7084 $('#model_aimlapi_select').select2({
7085 placeholder: t`Select a model`,
7086 searchInputPlaceholder: t`Search models...`,
7087 searchInputCssClass: 'text_pole',
7088 width: '100%',
7089 templateResult: getAimlapiModelTemplate,
7090 });
7091 $('#model_electronhub_select').select2({
7092 placeholder: t`Select a model`,
7093 searchInputPlaceholder: t`Search models...`,
7094 searchInputCssClass: 'text_pole',
7095 width: '100%',
7096 templateResult: getElectronHubModelTemplate,
7097 matcher: textValueMatcher,
7098 });
7099 $('#model_chutes_select').select2({
7100 placeholder: t`Select a model`,
7101 searchInputPlaceholder: t`Search models...`,
7102 searchInputCssClass: 'text_pole',
7103 width: '100%',
7104 templateResult: getChutesModelTemplate,
7105 matcher: textValueMatcher,
7106 });
7107 $('#model_nanogpt_select').select2({
7108 placeholder: t`Select a model`,
7109 searchInputPlaceholder: t`Search models...`,
7110 searchInputCssClass: 'text_pole',
7111 width: '100%',
7112 templateResult: getNanoGptModelTemplate,
7113 matcher: textValueMatcher,
7114 });
7115 $('#completion_prompt_manager_popup_entry_form_injection_trigger').select2({
7116 placeholder: t`All types (default)`,
7117 width: '100%',
7118 closeOnSelect: false,
7119 });
7120 }
7121
7122 $('#openrouter_providers_chat').on('change', function () {
7123 const selectedProviders = $(this).val();
7124
7125 // Not a multiple select?
7126 if (!Array.isArray(selectedProviders)) {
7127 return;
7128 }
7129
7130 oai_settings.openrouter_providers = selectedProviders;
7131
7132 updateOpenRouterProvidersWarning('#openrouter_providers_chat');
7133 saveSettingsDebounced();
7134 });
7135
7136 $('#openrouter_quantizations_chat').on('change', function () {
7137 const selectedQuantizations = $(this).val();
7138
7139 // Not a multiple select?
7140 if (!Array.isArray(selectedQuantizations)) {
7141 return;
7142 }
7143
7144 oai_settings.openrouter_quantizations = selectedQuantizations;
7145
7146 saveSettingsDebounced();
7147 });
7148
7149 $('#nanogpt_provider').on('change', function () {
7150 oai_settings.nanogpt_provider = String($(this).val() || '');
7151 updateNanoGptProvidersWarning('#nanogpt_provider');
7152 saveSettingsDebounced();
7153 });
7154
7155 $('#nanogpt_payg_override').on('input', function () {
7156 oai_settings.nanogpt_payg_override = !!$(this).prop('checked');
7157 saveSettingsDebounced();
7158 });
7159
7160 $('#bind_preset_to_connection').on('input', function () {
7161 oai_settings.bind_preset_to_connection = !!$(this).prop('checked');
7162 saveSettingsDebounced();
7163 });
7164
7165 $('#cc_group_models').on('input', async () => {
7166 oai_settings.group_models = $('#cc_group_models').prop('checked');
7167 reconnectOpenAi();
7168 saveSettingsDebounced();
7169 });
7170
7171 $('#cc_sort_models').on('input', async () => {
7172 oai_settings.sort_models = $('#cc_sort_models').val().toString();
7173 reconnectOpenAi();
7174 saveSettingsDebounced();
7175 });
7176
7177 $('#api_button_openai').on('click', onConnectButtonClick);
7178 $('#openai_reverse_proxy').on('input', onReverseProxyInput);
7179 $('#model_openai_select').on('change', onModelChange);
7180 $('#model_claude_select').on('change', onModelChange);
7181 $('#model_google_select').on('change', onModelChange);
7182 $('#model_vertexai_select').on('change', onModelChange);
7183 $('#vertexai_auth_mode').on('change', onVertexAIAuthModeChange);
7184 $('#vertexai_region').on('input', function () {
7185 oai_settings.vertexai_region = String($(this).val());
7186 saveSettingsDebounced();
7187 });
7188 $('#vertexai_express_project_id').on('input', function () {
7189 oai_settings.vertexai_express_project_id = String($(this).val());
7190 saveSettingsDebounced();
7191 });
7192 $('#zai_endpoint').on('input', function () {
7193 oai_settings.zai_endpoint = String($(this).val());
7194 saveSettingsDebounced();
7195 });
7196 $('#siliconflow_endpoint').on('input', function () {
7197 oai_settings.siliconflow_endpoint = String($(this).val());
7198 saveSettingsDebounced();
7199 });
7200 $('#minimax_endpoint').on('input', function () {
7201 oai_settings.minimax_endpoint = String($(this).val());
7202 saveSettingsDebounced();
7203 });
7204 $('#workers_ai_account_id').on('input', function () {
7205 oai_settings.workers_ai_account_id = String($(this).val());
7206 saveSettingsDebounced();
7207 });
7208 $('#vertexai_service_account_json').on('input', onVertexAIServiceAccountJsonChange);
7209 $('#vertexai_validate_service_account').on('click', onVertexAIValidateServiceAccount);
7210 $('#vertexai_clear_service_account').on('click', onVertexAIClearServiceAccount);
7211 $('#model_openrouter_select').on('change', onModelChange);
7212 $('#model_ai21_select').on('change', onModelChange);
7213 $('#model_mistralai_select').on('change', onModelChange);
7214 $('#model_cohere_select').on('change', onModelChange);
7215 $('#model_perplexity_select').on('change', onModelChange);
7216 $('#model_groq_select').on('change', onModelChange);
7217 $('#model_chutes_select').on('change', onModelChange);
7218 $('#model_siliconflow_select').on('change', onModelChange);
7219 $('#model_minimax_select').on('change', onModelChange);
7220 $('#model_electronhub_select').on('change', onModelChange);
7221 $('#model_nanogpt_select').on('change', onModelChange);
7222 $('#model_deepseek_select').on('change', onModelChange);
7223 $('#model_aimlapi_select').on('change', onModelChange);
7224 $('#model_custom_select').on('change', onModelChange);
7225 $('#model_xai_select').on('change', onModelChange);
7226 $('#model_pollinations_select').on('change', onModelChange);
7227 $('#model_cometapi_select').on('change', onModelChange);
7228 $('#model_moonshot_select').on('change', onModelChange);
7229 $('#model_fireworks_select').on('change', onModelChange);
7230 $('#azure_openai_model').on('change', onModelChange);
7231 $('#model_zai_select').on('change', onModelChange);
7232 $('#model_workers_ai_select').on('change', onModelChange);
7233 $('#settings_preset_openai').on('change', onSettingsPresetChange);
7234 $('#new_oai_preset').on('click', onNewPresetClick);
7235 $('#delete_oai_preset').on('click', onDeletePresetClick);
7236 $('#openai_logit_bias_preset').on('change', onLogitBiasPresetChange);
7237 $('#openai_logit_bias_new_preset').on('click', createNewLogitBiasPreset);
7238 $('#openai_logit_bias_new_entry').on('click', createNewLogitBiasEntry);
7239 $('#openai_logit_bias_import_file').on('input', onLogitBiasPresetImportFileChange);
7240 $('#openai_preset_import_file').on('input', onPresetImportFileChange);
7241 $('#export_oai_preset').on('click', onExportPresetClick);
7242 $('#openai_logit_bias_import_preset').on('click', onLogitBiasPresetImportClick);
7243 $('#openai_logit_bias_export_preset').on('click', onLogitBiasPresetExportClick);
7244 $('#openai_logit_bias_delete_preset').on('click', onLogitBiasPresetDeleteClick);
7245 $('#import_oai_preset').on('click', onImportPresetClick);
7246 $('#openai_proxy_password_show').on('click', onProxyPasswordShowClick);
7247 $('#customize_additional_parameters').on('click', onCustomizeParametersClick);
7248 $('#openai_proxy_preset').on('change', onProxyPresetChange);
7249}