feat: add SiliconFlow.cn chat completion and embedding support (#5316) * feat: add SiliconFlow.cn endpoint support and embedding vectors Chat completion: - Add endpoint selection dropdown (Global/.com vs China/.cn) to existing SiliconFlow provider, following the Z.AI endpoint pattern - Backend switches API URL based on selected endpoint - Add /api-url slash command support for endpoint switching Embeddings: - Add SiliconFlow as a vector/embedding source (OpenAI-compatible) - Support both .com and .cn endpoints via siliconflow_endpoint setting borrowed from the main connection panel (Vertex AI pattern) - Superset model list with platform attribution (.cn) markers - Models: Qwen3-Embedding (0.6B/4B/8B) + BGE/BCE models (.cn only) * Add filter by models type * Load embedding models from endpoint * Improve api-url command declaration * Support endpoint override in custom-request service --------- Co-authored-by: Cohee <18619528+Cohee1207@users.noreply.github.com>
Signed| @@ -3556,6 +3556,11 @@ | |||
| 3556 | <div data-for="api_key_siliconflow" class="neutral_warning" data-i18n="For privacy reasons, your API key will be hidden after you click 'Connect'."> | 3556 | <div data-for="api_key_siliconflow" class="neutral_warning" data-i18n="For privacy reasons, your API key will be hidden after you click 'Connect'."> |
| 3557 | For privacy reasons, your API key will be hidden after you click 'Connect'. | 3557 | For privacy reasons, your API key will be hidden after you click 'Connect'. |
| 3558 | </div> | 3558 | </div> |
| 3559 | <h4 data-i18n="SiliconFlow Endpoint">SiliconFlow Endpoint</h4> | ||
| 3560 | <select id="siliconflow_endpoint"> | ||
| 3561 | <option value="global" data-i18n="Global (siliconflow.com)">Global (siliconflow.com)</option> | ||
| 3562 | <option value="cn" data-i18n="China (siliconflow.cn)">China (siliconflow.cn)</option> | ||
| 3563 | </select> | ||
| 3559 | <h4>SiliconFlow Model</h4> | 3564 | <h4>SiliconFlow Model</h4> |
| 3560 | <select id="model_siliconflow_select"> | 3565 | <select id="model_siliconflow_select"> |
| 3561 | <option value="" data-i18n="-- Connect to the API --">-- Connect to the API --</option> | 3566 | <option value="" data-i18n="-- Connect to the API --">-- Connect to the API --</option> |
| @@ -591,7 +591,7 @@ export class ChatCompletionService { | |||
| 591 | } | 591 | } |
| 592 | 592 | ||
| 593 | // Ensure api-url is properly applied for all sources that accept it | 593 | // Ensure api-url is properly applied for all sources that accept it |
| 594 | ['custom_url', 'vertexai_region', 'zai_endpoint'].forEach(field => { | 594 | ['custom_url', 'vertexai_region', 'zai_endpoint', 'siliconflow_endpoint'].forEach(field => { |
| 595 | // The order is: connection profile => CC preset => CC settings | 595 | // The order is: connection profile => CC preset => CC settings |
| 596 | overridePayload[field] = overridePayload[field] || settings[field] || oai_settings[field]; | 596 | overridePayload[field] = overridePayload[field] || settings[field] || oai_settings[field]; |
| 597 | }); | 597 | }); |
| @@ -438,6 +438,7 @@ export class ConnectionManagerRequestService { | |||
| 438 | custom_url: profile['api-url'], | 438 | custom_url: profile['api-url'], |
| 439 | vertexai_region: profile['api-url'], | 439 | vertexai_region: profile['api-url'], |
| 440 | zai_endpoint: profile['api-url'], | 440 | zai_endpoint: profile['api-url'], |
| 441 | siliconflow_endpoint: profile['api-url'], | ||
| 441 | reverse_proxy: proxyPreset?.url, | 442 | reverse_proxy: proxyPreset?.url, |
| 442 | proxy_password: proxyPreset?.password, | 443 | proxy_password: proxyPreset?.password, |
| 443 | custom_prompt_post_processing: profile['prompt-post-processing'], | 444 | custom_prompt_post_processing: profile['prompt-post-processing'], |
| @@ -72,6 +72,7 @@ const settings = { | |||
| 72 | google_model: 'text-embedding-005', | 72 | google_model: 'text-embedding-005', |
| 73 | chutes_model: 'chutes-qwen-qwen3-embedding-8b', | 73 | chutes_model: 'chutes-qwen-qwen3-embedding-8b', |
| 74 | nanogpt_model: 'text-embedding-3-small', | 74 | nanogpt_model: 'text-embedding-3-small', |
| 75 | siliconflow_model: 'Qwen/Qwen3-Embedding-0.6B', | ||
| 75 | summarize: false, | 76 | summarize: false, |
| 76 | summarize_sent: false, | 77 | summarize_sent: false, |
| 77 | summary_source: 'main', | 78 | summary_source: 'main', |
| @@ -837,6 +838,10 @@ function getVectorsRequestBody(args = {}) { | |||
| 837 | case 'nanogpt': | 838 | case 'nanogpt': |
| 838 | body.model = extension_settings.vectors.nanogpt_model; | 839 | body.model = extension_settings.vectors.nanogpt_model; |
| 839 | break; | 840 | break; |
| 841 | case 'siliconflow': | ||
| 842 | body.model = extension_settings.vectors.siliconflow_model; | ||
| 843 | body.siliconflow_endpoint = oai_settings.siliconflow_endpoint; | ||
| 844 | break; | ||
| 840 | default: | 845 | default: |
| 841 | break; | 846 | break; |
| 842 | } | 847 | } |
| @@ -929,7 +934,8 @@ function throwIfSourceInvalid() { | |||
| 929 | settings.source === 'mistral' && !secret_state[SECRET_KEYS.MISTRALAI] || | 934 | settings.source === 'mistral' && !secret_state[SECRET_KEYS.MISTRALAI] || |
| 930 | settings.source === 'togetherai' && !secret_state[SECRET_KEYS.TOGETHERAI] || | 935 | settings.source === 'togetherai' && !secret_state[SECRET_KEYS.TOGETHERAI] || |
| 931 | settings.source === 'nomicai' && !secret_state[SECRET_KEYS.NOMICAI] || | 936 | settings.source === 'nomicai' && !secret_state[SECRET_KEYS.NOMICAI] || |
| 932 | settings.source === 'cohere' && !secret_state[SECRET_KEYS.COHERE]) { | 937 | settings.source === 'cohere' && !secret_state[SECRET_KEYS.COHERE] || |
| 938 | settings.source === 'siliconflow' && !secret_state[SECRET_KEYS.SILICONFLOW]) { | ||
| 933 | throw new Error('Vectors: API key missing', { cause: 'api_key_missing' }); | 939 | throw new Error('Vectors: API key missing', { cause: 'api_key_missing' }); |
| 934 | } | 940 | } |
| 935 | 941 | ||
| @@ -1147,6 +1153,7 @@ function toggleSettings() { | |||
| 1147 | $('#webllm_vectorsModel').toggle(settings.source === 'webllm'); | 1153 | $('#webllm_vectorsModel').toggle(settings.source === 'webllm'); |
| 1148 | $('#koboldcpp_vectorsModel').toggle(settings.source === 'koboldcpp'); | 1154 | $('#koboldcpp_vectorsModel').toggle(settings.source === 'koboldcpp'); |
| 1149 | $('#google_vectorsModel').toggle(settings.source === 'palm' || settings.source === 'vertexai'); | 1155 | $('#google_vectorsModel').toggle(settings.source === 'palm' || settings.source === 'vertexai'); |
| 1156 | $('#siliconflow_vectorsModel').toggle(settings.source === 'siliconflow'); | ||
| 1150 | $('#vector_altEndpointUrl').toggle(vectorApiRequiresUrl.includes(settings.source)); | 1157 | $('#vector_altEndpointUrl').toggle(vectorApiRequiresUrl.includes(settings.source)); |
| 1151 | switch (settings.source) { | 1158 | switch (settings.source) { |
| 1152 | case 'webllm': | 1159 | case 'webllm': |
| @@ -1164,6 +1171,9 @@ function toggleSettings() { | |||
| 1164 | case 'nanogpt': | 1171 | case 'nanogpt': |
| 1165 | loadNanoGPTModels(); | 1172 | loadNanoGPTModels(); |
| 1166 | break; | 1173 | break; |
| 1174 | case 'siliconflow': | ||
| 1175 | loadSiliconFlowModels(); | ||
| 1176 | break; | ||
| 1167 | } | 1177 | } |
| 1168 | } | 1178 | } |
| 1169 | 1179 | ||
| @@ -1312,6 +1322,45 @@ function populateOpenRouterModelSelect(models) { | |||
| 1312 | $('#vectors_openrouter_model').val(settings.openrouter_model); | 1322 | $('#vectors_openrouter_model').val(settings.openrouter_model); |
| 1313 | } | 1323 | } |
| 1314 | 1324 | ||
| 1325 | async function loadSiliconFlowModels() { | ||
| 1326 | try { | ||
| 1327 | const response = await fetch('/api/openai/siliconflow/models/embedding', { | ||
| 1328 | method: 'POST', | ||
| 1329 | headers: getRequestHeaders(), | ||
| 1330 | body: JSON.stringify({ | ||
| 1331 | siliconflow_endpoint: oai_settings.siliconflow_endpoint, | ||
| 1332 | }), | ||
| 1333 | }); | ||
| 1334 | |||
| 1335 | if (!response.ok) { | ||
| 1336 | throw new Error(`HTTP ${response.status}`); | ||
| 1337 | } | ||
| 1338 | |||
| 1339 | /** @type {Array<any>} */ | ||
| 1340 | const data = await response.json(); | ||
| 1341 | const models = Array.isArray(data) ? data : []; | ||
| 1342 | populateSiliconFlowModelSelect(models); | ||
| 1343 | } catch (err) { | ||
| 1344 | console.warn('SiliconFlow models fetch failed', err); | ||
| 1345 | populateSiliconFlowModelSelect([]); | ||
| 1346 | } | ||
| 1347 | } | ||
| 1348 | |||
| 1349 | function populateSiliconFlowModelSelect(models) { | ||
| 1350 | const select = $('#vectors_siliconflow_model'); | ||
| 1351 | select.empty(); | ||
| 1352 | for (const m of models) { | ||
| 1353 | const option = document.createElement('option'); | ||
| 1354 | option.value = m.id; | ||
| 1355 | option.text = m.id; | ||
| 1356 | select.append(option); | ||
| 1357 | } | ||
| 1358 | if (!settings.siliconflow_model && models.length) { | ||
| 1359 | settings.siliconflow_model = models[0].id; | ||
| 1360 | } | ||
| 1361 | $('#vectors_siliconflow_model').val(settings.siliconflow_model); | ||
| 1362 | } | ||
| 1363 | |||
| 1315 | /** | 1364 | /** |
| 1316 | * Executes a function with WebLLM error handling. | 1365 | * Executes a function with WebLLM error handling. |
| 1317 | * @param {function(): Promise<T>} func Function to execute | 1366 | * @param {function(): Promise<T>} func Function to execute |
| @@ -1724,6 +1773,11 @@ jQuery(async () => { | |||
| 1724 | Object.assign(extension_settings.vectors, settings); | 1773 | Object.assign(extension_settings.vectors, settings); |
| 1725 | saveSettingsDebounced(); | 1774 | saveSettingsDebounced(); |
| 1726 | }); | 1775 | }); |
| 1776 | $('#vectors_siliconflow_model').val(settings.siliconflow_model).on('change', () => { | ||
| 1777 | settings.siliconflow_model = String($('#vectors_siliconflow_model').val()); | ||
| 1778 | Object.assign(extension_settings.vectors, settings); | ||
| 1779 | saveSettingsDebounced(); | ||
| 1780 | }); | ||
| 1727 | $('#vectors_openrouter_model').val(settings.openrouter_model).on('change', () => { | 1781 | $('#vectors_openrouter_model').val(settings.openrouter_model).on('change', () => { |
| 1728 | settings.openrouter_model = String($('#vectors_openrouter_model').val()); | 1782 | settings.openrouter_model = String($('#vectors_openrouter_model').val()); |
| 1729 | Object.assign(extension_settings.vectors, settings); | 1783 | Object.assign(extension_settings.vectors, settings); |
| @@ -25,6 +25,7 @@ | |||
| 25 | <option value="ollama">Ollama</option> | 25 | <option value="ollama">Ollama</option> |
| 26 | <option value="openai">OpenAI</option> | 26 | <option value="openai">OpenAI</option> |
| 27 | <option value="openrouter">OpenRouter</option> | 27 | <option value="openrouter">OpenRouter</option> |
| 28 | <option value="siliconflow">SiliconFlow</option> | ||
| 28 | <option value="togetherai">TogetherAI</option> | 29 | <option value="togetherai">TogetherAI</option> |
| 29 | <option value="vllm">vLLM</option> | 30 | <option value="vllm">vLLM</option> |
| 30 | <option value="webllm" data-i18n="WebLLM Extension">WebLLM Extension</option> | 31 | <option value="webllm" data-i18n="WebLLM Extension">WebLLM Extension</option> |
| @@ -194,6 +195,15 @@ | |||
| 194 | Hint: Set your OpenRouter API key in API Connections. | 195 | Hint: Set your OpenRouter API key in API Connections. |
| 195 | </i> | 196 | </i> |
| 196 | </div> | 197 | </div> |
| 198 | <div class="flex-container flexFlowColumn" id="siliconflow_vectorsModel"> | ||
| 199 | <label for="vectors_siliconflow_model" data-i18n="Vectorization Model"> | ||
| 200 | Vectorization Model | ||
| 201 | </label> | ||
| 202 | <select id="vectors_siliconflow_model" class="text_pole"></select> | ||
| 203 | <i data-i18n="Hint: Set your SiliconFlow API key in API Connections."> | ||
| 204 | Hint: Set your SiliconFlow API key in API Connections. | ||
| 205 | </i> | ||
| 206 | </div> | ||
| 197 | 207 | ||
| 198 | <div class="flex-container marginTopBot5"> | 208 | <div class="flex-container marginTopBot5"> |
| 199 | <div class="flex-container flex1 flexFlowColumn" title="How many last messages will be matched for relevance."> | 209 | <div class="flex-container flex1 flexFlowColumn" title="How many last messages will be matched for relevance."> |
| @@ -263,6 +263,11 @@ export const ZAI_ENDPOINT = { | |||
| 263 | CODING: 'coding', | 263 | CODING: 'coding', |
| 264 | }; | 264 | }; |
| 265 | 265 | ||
| 266 | export const SILICONFLOW_ENDPOINT = { | ||
| 267 | GLOBAL: 'global', | ||
| 268 | CN: 'cn', | ||
| 269 | }; | ||
| 270 | |||
| 266 | const sensitiveFields = [ | 271 | const sensitiveFields = [ |
| 267 | 'reverse_proxy', | 272 | 'reverse_proxy', |
| 268 | 'proxy_password', | 273 | 'proxy_password', |
| @@ -310,6 +315,7 @@ export const settingsToUpdate = { | |||
| 310 | chutes_model: ['#model_chutes_select', 'chutes_model', false, true], | 315 | chutes_model: ['#model_chutes_select', 'chutes_model', false, true], |
| 311 | chutes_sort_models: ['#chutes_sort_models', 'chutes_sort_models', false, true], | 316 | chutes_sort_models: ['#chutes_sort_models', 'chutes_sort_models', false, true], |
| 312 | siliconflow_model: ['#model_siliconflow_select', 'siliconflow_model', false, true], | 317 | siliconflow_model: ['#model_siliconflow_select', 'siliconflow_model', false, true], |
| 318 | siliconflow_endpoint: ['#siliconflow_endpoint', 'siliconflow_endpoint', false, true], | ||
| 313 | electronhub_model: ['#model_electronhub_select', 'electronhub_model', false, true], | 319 | electronhub_model: ['#model_electronhub_select', 'electronhub_model', false, true], |
| 314 | electronhub_sort_models: ['#electronhub_sort_models', 'electronhub_sort_models', false, true], | 320 | electronhub_sort_models: ['#electronhub_sort_models', 'electronhub_sort_models', false, true], |
| 315 | electronhub_group_models: ['#electronhub_group_models', 'electronhub_group_models', false, true], | 321 | electronhub_group_models: ['#electronhub_group_models', 'electronhub_group_models', false, true], |
| @@ -419,6 +425,7 @@ const default_settings = { | |||
| 419 | chutes_model: 'deepseek-ai/DeepSeek-V3-0324', | 425 | chutes_model: 'deepseek-ai/DeepSeek-V3-0324', |
| 420 | chutes_sort_models: 'alphabetically', | 426 | chutes_sort_models: 'alphabetically', |
| 421 | siliconflow_model: 'deepseek-ai/DeepSeek-V3', | 427 | siliconflow_model: 'deepseek-ai/DeepSeek-V3', |
| 428 | siliconflow_endpoint: SILICONFLOW_ENDPOINT.GLOBAL, | ||
| 422 | electronhub_model: 'gpt-4o-mini', | 429 | electronhub_model: 'gpt-4o-mini', |
| 423 | electronhub_sort_models: 'alphabetically', | 430 | electronhub_sort_models: 'alphabetically', |
| 424 | electronhub_group_models: false, | 431 | electronhub_group_models: false, |
| @@ -2804,6 +2811,10 @@ export async function createGenerationParameters(settings, model, type, messages | |||
| 2804 | delete generate_data.frequency_penalty; | 2811 | delete generate_data.frequency_penalty; |
| 2805 | } | 2812 | } |
| 2806 | 2813 | ||
| 2814 | if (settings.chat_completion_source === chat_completion_sources.SILICONFLOW) { | ||
| 2815 | generate_data.siliconflow_endpoint = settings.siliconflow_endpoint || SILICONFLOW_ENDPOINT.GLOBAL; | ||
| 2816 | } | ||
| 2817 | |||
| 2807 | // https://docs.nano-gpt.com/api-reference/endpoint/chat-completion#temperature-&-nucleus | 2818 | // https://docs.nano-gpt.com/api-reference/endpoint/chat-completion#temperature-&-nucleus |
| 2808 | if (settings.chat_completion_source === chat_completion_sources.NANOGPT) { | 2819 | if (settings.chat_completion_source === chat_completion_sources.NANOGPT) { |
| 2809 | generate_data.top_k = Number(settings.top_k_openai); | 2820 | generate_data.top_k = Number(settings.top_k_openai); |
| @@ -4257,6 +4268,10 @@ async function getStatusOpen() { | |||
| 4257 | data.azure_api_version = oai_settings.azure_api_version; | 4268 | data.azure_api_version = oai_settings.azure_api_version; |
| 4258 | } | 4269 | } |
| 4259 | 4270 | ||
| 4271 | if (oai_settings.chat_completion_source === chat_completion_sources.SILICONFLOW) { | ||
| 4272 | data.siliconflow_endpoint = oai_settings.siliconflow_endpoint; | ||
| 4273 | } | ||
| 4274 | |||
| 4260 | 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; | 4275 | 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; |
| 4261 | if (canBypass) { | 4276 | if (canBypass) { |
| 4262 | setOnlineStatus(t`Status check bypassed`); | 4277 | setOnlineStatus(t`Status check bypassed`); |
| @@ -6929,6 +6944,10 @@ export function initOpenAI() { | |||
| 6929 | oai_settings.zai_endpoint = String($(this).val()); | 6944 | oai_settings.zai_endpoint = String($(this).val()); |
| 6930 | saveSettingsDebounced(); | 6945 | saveSettingsDebounced(); |
| 6931 | }); | 6946 | }); |
| 6947 | $('#siliconflow_endpoint').on('input', function () { | ||
| 6948 | oai_settings.siliconflow_endpoint = String($(this).val()); | ||
| 6949 | saveSettingsDebounced(); | ||
| 6950 | }); | ||
| 6932 | $('#vertexai_service_account_json').on('input', onVertexAIServiceAccountJsonChange); | 6951 | $('#vertexai_service_account_json').on('input', onVertexAIServiceAccountJsonChange); |
| 6933 | $('#vertexai_validate_service_account').on('click', onVertexAIValidateServiceAccount); | 6952 | $('#vertexai_validate_service_account').on('click', onVertexAIValidateServiceAccount); |
| 6934 | $('#vertexai_clear_service_account').on('click', onVertexAIClearServiceAccount); | 6953 | $('#vertexai_clear_service_account').on('click', onVertexAIClearServiceAccount); |
| @@ -70,7 +70,7 @@ import { hideChatMessageRange } from './chats.js'; | |||
| 70 | import { getContext, saveMetadataDebounced } from './extensions.js'; | 70 | import { getContext, saveMetadataDebounced } from './extensions.js'; |
| 71 | import { getRegexedString, regex_placement } from './extensions/regex/engine.js'; | 71 | import { getRegexedString, regex_placement } from './extensions/regex/engine.js'; |
| 72 | import { findGroupMemberId, groups, is_group_generating, openGroupById, regenerateGroup, resetSelectedGroup, saveGroupChat, selected_group, getGroupMembers } from './group-chats.js'; | 72 | import { findGroupMemberId, groups, is_group_generating, openGroupById, regenerateGroup, resetSelectedGroup, saveGroupChat, selected_group, getGroupMembers } from './group-chats.js'; |
| 73 | import { chat_completion_sources, oai_settings, promptManager, ZAI_ENDPOINT } from './openai.js'; | 73 | import { chat_completion_sources, oai_settings, promptManager, SILICONFLOW_ENDPOINT, ZAI_ENDPOINT } from './openai.js'; |
| 74 | import { user_avatar } from './personas.js'; | 74 | import { user_avatar } from './personas.js'; |
| 75 | import { addEphemeralStoppingString, chat_styles, context_presets, flushEphemeralStoppingStrings, playMessageSound, power_user } from './power-user.js'; | 75 | import { addEphemeralStoppingString, chat_styles, context_presets, flushEphemeralStoppingStrings, playMessageSound, power_user } from './power-user.js'; |
| 76 | import { SERVER_INPUTS, textgen_types, textgenerationwebui_settings } from './textgen-settings.js'; | 76 | import { SERVER_INPUTS, textgen_types, textgenerationwebui_settings } from './textgen-settings.js'; |
| @@ -3132,8 +3132,9 @@ export function initDefaultSlashCommands() { | |||
| 3132 | new SlashCommandEnumValue('custom', 'custom OpenAI-compatible', enumTypes.getBasedOnIndex(UNIQUE_APIS.findIndex(x => x === 'openai')), 'O'), | 3132 | new SlashCommandEnumValue('custom', 'custom OpenAI-compatible', enumTypes.getBasedOnIndex(UNIQUE_APIS.findIndex(x => x === 'openai')), 'O'), |
| 3133 | new SlashCommandEnumValue('zai', 'Z.AI', enumTypes.getBasedOnIndex(UNIQUE_APIS.findIndex(x => x === 'zai')), 'Z'), | 3133 | new SlashCommandEnumValue('zai', 'Z.AI', enumTypes.getBasedOnIndex(UNIQUE_APIS.findIndex(x => x === 'zai')), 'Z'), |
| 3134 | new SlashCommandEnumValue('vertexai', 'Google Vertex AI', enumTypes.getBasedOnIndex(UNIQUE_APIS.findIndex(x => x === 'vertexai')), 'V'), | 3134 | new SlashCommandEnumValue('vertexai', 'Google Vertex AI', enumTypes.getBasedOnIndex(UNIQUE_APIS.findIndex(x => x === 'vertexai')), 'V'), |
| 3135 | new SlashCommandEnumValue('siliconflow', 'SiliconFlow', enumTypes.getBasedOnIndex(UNIQUE_APIS.findIndex(x => x === 'siliconflow')), 'S'), | ||
| 3135 | new SlashCommandEnumValue('kobold', 'KoboldAI Classic', enumTypes.getBasedOnIndex(UNIQUE_APIS.findIndex(x => x === 'kobold')), 'K'), | 3136 | new SlashCommandEnumValue('kobold', 'KoboldAI Classic', enumTypes.getBasedOnIndex(UNIQUE_APIS.findIndex(x => x === 'kobold')), 'K'), |
| 3136 | ...Object.values(textgen_types).map(api => new SlashCommandEnumValue(api, null, enumTypes.getBasedOnIndex(UNIQUE_APIS.findIndex(x => x === 'textgenerationwebui')), 'T')), | 3137 | ...Object.values(textgen_types).filter(api => Object.keys(SERVER_INPUTS).includes(api)).map(api => new SlashCommandEnumValue(api, null, enumTypes.getBasedOnIndex(UNIQUE_APIS.findIndex(x => x === 'textgenerationwebui')), 'T')), |
| 3137 | ], | 3138 | ], |
| 3138 | }), | 3139 | }), |
| 3139 | SlashCommandNamedArgument.fromProps({ | 3140 | SlashCommandNamedArgument.fromProps({ |
| @@ -3165,7 +3166,7 @@ export function initDefaultSlashCommands() { | |||
| 3165 | ${t`If a manual API is provided to <b>set</b> the URL, make sure to set <code>connect=false</code>, as auto-connect only works for the currently selected API, or consider switching to it with <code>/api</code> first.`} | 3166 | ${t`If a manual API is provided to <b>set</b> the URL, make sure to set <code>connect=false</code>, as auto-connect only works for the currently selected API, or consider switching to it with <code>/api</code> first.`} |
| 3166 | </div> | 3167 | </div> |
| 3167 | <div> | 3168 | <div> |
| 3168 | ${t`This slash command works for most of the Text Completion sources, KoboldAI Classic, and also Custom OpenAI compatible, Z.AI, and Google Vertex AI for the Chat Completion sources. If unsure which APIs are supported, check the auto-completion of the optional <code>api</code> argument of this command.`} | 3169 | ${t`This slash command works for most of the Text Completion sources, KoboldAI Classic, and also Custom OpenAI compatible, Z.AI, SiliconFlow, and Google Vertex AI for the Chat Completion sources. If unsure which APIs are supported, check the auto-completion of the optional <code>api</code> argument of this command.`} |
| 3169 | </div> | 3170 | </div> |
| 3170 | `, | 3171 | `, |
| 3171 | })); | 3172 | })); |
| @@ -6715,6 +6716,32 @@ async function setApiUrlCallback({ api = null, connect = 'true', quiet = 'false' | |||
| 6715 | return oai_settings.zai_endpoint || ZAI_ENDPOINT.COMMON; | 6716 | return oai_settings.zai_endpoint || ZAI_ENDPOINT.COMMON; |
| 6716 | } | 6717 | } |
| 6717 | 6718 | ||
| 6719 | const isCurrentlySiliconFlow = main_api === 'openai' && oai_settings.chat_completion_source === chat_completion_sources.SILICONFLOW; | ||
| 6720 | if (api === chat_completion_sources.SILICONFLOW || (!api && isCurrentlySiliconFlow)) { | ||
| 6721 | if (!url) { | ||
| 6722 | return oai_settings.siliconflow_endpoint || SILICONFLOW_ENDPOINT.GLOBAL; | ||
| 6723 | } | ||
| 6724 | |||
| 6725 | const permittedValues = Object.values(SILICONFLOW_ENDPOINT); | ||
| 6726 | if (!permittedValues.includes(url)) { | ||
| 6727 | !isQuiet && toastr.warning(t`Valid options are: ${permittedValues.join(', ')}`, t`SiliconFlow endpoint '${url}' is not a valid option.`); | ||
| 6728 | return ''; | ||
| 6729 | } | ||
| 6730 | |||
| 6731 | if (!isCurrentlySiliconFlow && autoConnect) { | ||
| 6732 | toastr.warning(t`SiliconFlow is not the currently selected API, so we cannot do an auto-connect. Consider switching to it via /api beforehand.`); | ||
| 6733 | return ''; | ||
| 6734 | } | ||
| 6735 | |||
| 6736 | $('#siliconflow_endpoint').val(url).trigger('input'); | ||
| 6737 | |||
| 6738 | if (autoConnect) { | ||
| 6739 | $('#api_button_openai').trigger('click'); | ||
| 6740 | } | ||
| 6741 | |||
| 6742 | return oai_settings.siliconflow_endpoint || SILICONFLOW_ENDPOINT.GLOBAL; | ||
| 6743 | } | ||
| 6744 | |||
| 6718 | const isCurrentlyVertexAI = main_api === 'openai' && oai_settings.chat_completion_source === chat_completion_sources.VERTEXAI; | 6745 | const isCurrentlyVertexAI = main_api === 'openai' && oai_settings.chat_completion_source === chat_completion_sources.VERTEXAI; |
| 6719 | if (api === chat_completion_sources.VERTEXAI || (!api && isCurrentlyVertexAI)) { | 6746 | if (api === chat_completion_sources.VERTEXAI || (!api && isCurrentlyVertexAI)) { |
| 6720 | const defaultRegion = 'us-central1'; | 6747 | const defaultRegion = 'us-central1'; |
| @@ -551,3 +551,8 @@ export const ZAI_ENDPOINT = { | |||
| 551 | COMMON: 'common', | 551 | COMMON: 'common', |
| 552 | CODING: 'coding', | 552 | CODING: 'coding', |
| 553 | }; | 553 | }; |
| 554 | |||
| 555 | export const SILICONFLOW_ENDPOINT = { | ||
| 556 | GLOBAL: 'global', | ||
| 557 | CN: 'cn', | ||
| 558 | }; | ||
| @@ -17,6 +17,7 @@ import { | |||
| 17 | OPENAI_VERBOSITY_MODELS, | 17 | OPENAI_VERBOSITY_MODELS, |
| 18 | OPENROUTER_HEADERS, | 18 | OPENROUTER_HEADERS, |
| 19 | VERTEX_SAFETY, | 19 | VERTEX_SAFETY, |
| 20 | SILICONFLOW_ENDPOINT, | ||
| 20 | ZAI_ENDPOINT, | 21 | ZAI_ENDPOINT, |
| 21 | } from '../../constants.js'; | 22 | } from '../../constants.js'; |
| 22 | import { | 23 | import { |
| @@ -87,6 +88,7 @@ const API_COMETAPI = 'https://api.cometapi.com/v1'; | |||
| 87 | const API_ZAI_COMMON = 'https://api.z.ai/api/paas/v4'; | 88 | const API_ZAI_COMMON = 'https://api.z.ai/api/paas/v4'; |
| 88 | const API_ZAI_CODING = 'https://api.z.ai/api/coding/paas/v4'; | 89 | const API_ZAI_CODING = 'https://api.z.ai/api/coding/paas/v4'; |
| 89 | const API_SILICONFLOW = 'https://api.siliconflow.com/v1'; | 90 | const API_SILICONFLOW = 'https://api.siliconflow.com/v1'; |
| 91 | const API_SILICONFLOW_CN = 'https://api.siliconflow.cn/v1'; | ||
| 90 | const API_OPENROUTER = 'https://openrouter.ai/api/v1'; | 92 | const API_OPENROUTER = 'https://openrouter.ai/api/v1'; |
| 91 | 93 | ||
| 92 | /** | 94 | /** |
| @@ -1825,9 +1827,12 @@ router.post('/status', async function (request, statusResponse) { | |||
| 1825 | return statusResponse.status(500).send({ error: true, message: 'Failed to connect to the Azure endpoint.' }); | 1827 | return statusResponse.status(500).send({ error: true, message: 'Failed to connect to the Azure endpoint.' }); |
| 1826 | } | 1828 | } |
| 1827 | } else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.SILICONFLOW) { | 1829 | } else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.SILICONFLOW) { |
| 1828 | apiUrl = API_SILICONFLOW; | 1830 | const defaultApiUrl = request.body.siliconflow_endpoint === SILICONFLOW_ENDPOINT.CN |
| 1831 | ? API_SILICONFLOW_CN : API_SILICONFLOW; | ||
| 1832 | apiUrl = defaultApiUrl; | ||
| 1829 | apiKey = readSecret(request.user.directories, SECRET_KEYS.SILICONFLOW); | 1833 | apiKey = readSecret(request.user.directories, SECRET_KEYS.SILICONFLOW); |
| 1830 | headers = {}; | 1834 | headers = {}; |
| 1835 | queryParams = { type: 'text', sub_type: 'chat' }; | ||
| 1831 | } else { | 1836 | } else { |
| 1832 | console.warn('This chat completion source is not supported yet.'); | 1837 | console.warn('This chat completion source is not supported yet.'); |
| 1833 | return statusResponse.status(400).send({ error: true }); | 1838 | return statusResponse.status(400).send({ error: true }); |
| @@ -2301,7 +2306,9 @@ router.post('/generate', async function (request, response) { | |||
| 2301 | setJsonObjectFormat(bodyParams, request.body.messages, request.body.json_schema); | 2306 | setJsonObjectFormat(bodyParams, request.body.messages, request.body.json_schema); |
| 2302 | } | 2307 | } |
| 2303 | } else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.SILICONFLOW) { | 2308 | } else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.SILICONFLOW) { |
| 2304 | apiUrl = API_SILICONFLOW; | 2309 | const defaultApiUrl = request.body.siliconflow_endpoint === SILICONFLOW_ENDPOINT.CN |
| 2310 | ? API_SILICONFLOW_CN : API_SILICONFLOW; | ||
| 2311 | apiUrl = defaultApiUrl; | ||
| 2305 | apiKey = readSecret(request.user.directories, SECRET_KEYS.SILICONFLOW); | 2312 | apiKey = readSecret(request.user.directories, SECRET_KEYS.SILICONFLOW); |
| 2306 | headers = {}; | 2313 | headers = {}; |
| 2307 | bodyParams = {}; | 2314 | bodyParams = {}; |
| @@ -8,7 +8,7 @@ import express from 'express'; | |||
| 8 | import { getConfigValue, mergeObjectWithYaml, excludeKeysByYaml, trimV1, delay } from '../util.js'; | 8 | import { getConfigValue, mergeObjectWithYaml, excludeKeysByYaml, trimV1, delay } from '../util.js'; |
| 9 | import { setAdditionalHeaders } from '../additional-headers.js'; | 9 | import { setAdditionalHeaders } from '../additional-headers.js'; |
| 10 | import { readSecret, SECRET_KEYS } from './secrets.js'; | 10 | import { readSecret, SECRET_KEYS } from './secrets.js'; |
| 11 | import { AIMLAPI_HEADERS, OPENROUTER_HEADERS, ZAI_ENDPOINT } from '../constants.js'; | 11 | import { AIMLAPI_HEADERS, OPENROUTER_HEADERS, SILICONFLOW_ENDPOINT, ZAI_ENDPOINT } from '../constants.js'; |
| 12 | 12 | ||
| 13 | export const router = express.Router(); | 13 | export const router = express.Router(); |
| 14 | 14 | ||
| @@ -529,6 +529,47 @@ router.post('/nanogpt/models/embedding', async (request, response) => { | |||
| 529 | } | 529 | } |
| 530 | }); | 530 | }); |
| 531 | 531 | ||
| 532 | router.post('/siliconflow/models/embedding', async (request, response) => { | ||
| 533 | try { | ||
| 534 | const key = readSecret(request.user.directories, SECRET_KEYS.SILICONFLOW); | ||
| 535 | |||
| 536 | if (!key) { | ||
| 537 | console.warn('No SiliconFlow key found'); | ||
| 538 | return response.sendStatus(400); | ||
| 539 | } | ||
| 540 | |||
| 541 | const apiUrl = request.body.siliconflow_endpoint === SILICONFLOW_ENDPOINT.CN | ||
| 542 | ? 'https://api.siliconflow.cn/v1/models?type=text&sub_type=embedding' | ||
| 543 | : 'https://api.siliconflow.com/v1/models?type=text&sub_type=embedding'; | ||
| 544 | |||
| 545 | const result = await fetch(apiUrl, { | ||
| 546 | method: 'GET', | ||
| 547 | headers: { | ||
| 548 | Authorization: `Bearer ${key}`, | ||
| 549 | }, | ||
| 550 | }); | ||
| 551 | |||
| 552 | if (!result.ok) { | ||
| 553 | const text = await result.text(); | ||
| 554 | console.warn('SiliconFlow embedding models request failed', result.statusText, text); | ||
| 555 | return response.status(500).send(text); | ||
| 556 | } | ||
| 557 | |||
| 558 | /** @type {any} */ | ||
| 559 | const data = await result.json(); | ||
| 560 | |||
| 561 | if (!Array.isArray(data?.data)) { | ||
| 562 | console.warn('SiliconFlow embedding models response invalid', data); | ||
| 563 | return response.sendStatus(500); | ||
| 564 | } | ||
| 565 | |||
| 566 | return response.json(data.data); | ||
| 567 | } catch (error) { | ||
| 568 | console.error('SiliconFlow embedding models fetch failed', error); | ||
| 569 | response.sendStatus(500); | ||
| 570 | } | ||
| 571 | }); | ||
| 572 | |||
| 532 | router.post('/generate-image', async (request, response) => { | 573 | router.post('/generate-image', async (request, response) => { |
| 533 | try { | 574 | try { |
| 534 | const key = readSecret(request.user.directories, SECRET_KEYS.OPENAI); | 575 | const key = readSecret(request.user.directories, SECRET_KEYS.OPENAI); |
| @@ -38,6 +38,7 @@ const SOURCES = [ | |||
| 38 | 'openrouter', | 38 | 'openrouter', |
| 39 | 'chutes', | 39 | 'chutes', |
| 40 | 'nanogpt', | 40 | 'nanogpt', |
| 41 | 'siliconflow', | ||
| 41 | ]; | 42 | ]; |
| 42 | 43 | ||
| 43 | /** | 44 | /** |
| @@ -85,6 +86,8 @@ async function getVector(source, sourceSettings, text, isQuery, directories) { | |||
| 85 | return getOpenAIVector(text, source, directories, sourceSettings.model); | 86 | return getOpenAIVector(text, source, directories, sourceSettings.model); |
| 86 | case 'nanogpt': | 87 | case 'nanogpt': |
| 87 | return getOpenAIVector(text, source, directories, sourceSettings.model); | 88 | return getOpenAIVector(text, source, directories, sourceSettings.model); |
| 89 | case 'siliconflow': | ||
| 90 | return getOpenAIVector(text, source, directories, sourceSettings.model, sourceSettings.urlOverride); | ||
| 88 | } | 91 | } |
| 89 | 92 | ||
| 90 | throw new Error(`Unknown vector source ${source}`); | 93 | throw new Error(`Unknown vector source ${source}`); |
| @@ -156,6 +159,9 @@ async function getBatchVector(source, sourceSettings, texts, isQuery, directorie | |||
| 156 | case 'nanogpt': | 159 | case 'nanogpt': |
| 157 | results.push(...await getOpenAIBatchVector(batch, source, directories, sourceSettings.model)); | 160 | results.push(...await getOpenAIBatchVector(batch, source, directories, sourceSettings.model)); |
| 158 | break; | 161 | break; |
| 162 | case 'siliconflow': | ||
| 163 | results.push(...await getOpenAIBatchVector(batch, source, directories, sourceSettings.model, sourceSettings.urlOverride)); | ||
| 164 | break; | ||
| 159 | default: | 165 | default: |
| 160 | throw new Error(`Unknown vector source ${source}`); | 166 | throw new Error(`Unknown vector source ${source}`); |
| 161 | } | 167 | } |
| @@ -248,6 +254,12 @@ function getSourceSettings(source, request) { | |||
| 248 | return { | 254 | return { |
| 249 | model: String(request.body.model || 'text-embedding-3-small'), | 255 | model: String(request.body.model || 'text-embedding-3-small'), |
| 250 | }; | 256 | }; |
| 257 | case 'siliconflow': | ||
| 258 | return { | ||
| 259 | model: String(request.body.model || 'Qwen/Qwen3-Embedding-0.6B'), | ||
| 260 | urlOverride: request.body.siliconflow_endpoint === 'cn' | ||
| 261 | ? 'https://api.siliconflow.cn/v1' : null, | ||
| 262 | }; | ||
| 251 | default: | 263 | default: |
| 252 | return {}; | 264 | return {}; |
| 253 | } | 265 | } |
| @@ -54,6 +54,13 @@ const SOURCES = { | |||
| 54 | headers: {}, | 54 | headers: {}, |
| 55 | processBody: () => {}, | 55 | processBody: () => {}, |
| 56 | }, | 56 | }, |
| 57 | 'siliconflow': { | ||
| 58 | secretKey: SECRET_KEYS.SILICONFLOW, | ||
| 59 | url: 'https://api.siliconflow.com/v1', | ||
| 60 | model: 'Qwen/Qwen3-Embedding-0.6B', | ||
| 61 | headers: {}, | ||
| 62 | processBody: () => {}, | ||
| 63 | }, | ||
| 57 | }; | 64 | }; |
| 58 | 65 | ||
| 59 | /** | 66 | /** |
| @@ -62,9 +69,10 @@ const SOURCES = { | |||
| 62 | * @param {string} source - The source of the vector | 69 | * @param {string} source - The source of the vector |
| 63 | * @param {import('../users.js').UserDirectoryList} directories - The directories object for the user | 70 | * @param {import('../users.js').UserDirectoryList} directories - The directories object for the user |
| 64 | * @param {string} model - The model to use for the embedding | 71 | * @param {string} model - The model to use for the embedding |
| 72 | * @param {string|null} urlOverride - Optional URL override for the API endpoint | ||
| 65 | * @returns {Promise<number[][]>} - The array of vectors for the texts | 73 | * @returns {Promise<number[][]>} - The array of vectors for the texts |
| 66 | */ | 74 | */ |
| 67 | export async function getOpenAIBatchVector(texts, source, directories, model = '') { | 75 | export async function getOpenAIBatchVector(texts, source, directories, model = '', urlOverride = null) { |
| 68 | const config = SOURCES[source]; | 76 | const config = SOURCES[source]; |
| 69 | 77 | ||
| 70 | if (!config) { | 78 | if (!config) { |
| @@ -80,7 +88,7 @@ export async function getOpenAIBatchVector(texts, source, directories, model = ' | |||
| 80 | } | 88 | } |
| 81 | 89 | ||
| 82 | const modelName = model || config.model; | 90 | const modelName = model || config.model; |
| 83 | const url = config.url.replace('{{MODEL}}', modelName); | 91 | const url = urlOverride || config.url.replace('{{MODEL}}', modelName); |
| 84 | const body = { | 92 | const body = { |
| 85 | input: texts, | 93 | input: texts, |
| 86 | model: modelName, | 94 | model: modelName, |
| @@ -127,9 +135,10 @@ export async function getOpenAIBatchVector(texts, source, directories, model = ' | |||
| 127 | * @param {string} source - The source of the vector | 135 | * @param {string} source - The source of the vector |
| 128 | * @param {import('../users.js').UserDirectoryList} directories - The directories object for the user | 136 | * @param {import('../users.js').UserDirectoryList} directories - The directories object for the user |
| 129 | * @param {string} model - The model to use for the embedding | 137 | * @param {string} model - The model to use for the embedding |
| 138 | * @param {string|null} urlOverride - Optional URL override for the API endpoint | ||
| 130 | * @returns {Promise<number[]>} - The vector for the text | 139 | * @returns {Promise<number[]>} - The vector for the text |
| 131 | */ | 140 | */ |
| 132 | export async function getOpenAIVector(text, source, directories, model = '') { | 141 | export async function getOpenAIVector(text, source, directories, model = '', urlOverride = null) { |
| 133 | const vectors = await getOpenAIBatchVector([text], source, directories, model); | 142 | const vectors = await getOpenAIBatchVector([text], source, directories, model, urlOverride); |
| 134 | return vectors[0]; | 143 | return vectors[0]; |
| 135 | } | 144 | } |