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>

2cb1861db6f25f4a4d41fce2a8d79dd0e1f27415

Xiangzhe <32761048+xz-dev@users.noreply.github.com>

Signed
12 files changed, +202 -12Showing whitespace changes
public/index.html+5 -0
@@ -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>
public/scripts/custom-request.js+1 -1
@@ -591,7 +591,7 @@ export class ChatCompletionService {
591 }591 }
592592
593 // Ensure api-url is properly applied for all sources that accept it593 // 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 settings595 // 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 });
public/scripts/extensions/shared.js+1 -0
@@ -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'],
public/scripts/extensions/vectors/index.js+55 -1
@@ -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 }
935941
@@ -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}
11691179
@@ -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}
13141324
1325async 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
1349function 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 execute1366 * @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);
public/scripts/extensions/vectors/settings.html+10 -0
@@ -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>
197207
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.">
public/scripts/openai.js+19 -0
@@ -263,6 +263,11 @@ export const ZAI_ENDPOINT = {
263 CODING: 'coding',263 CODING: 'coding',
264};264};
265265
266export const SILICONFLOW_ENDPOINT = {
267 GLOBAL: 'global',
268 CN: 'cn',
269};
270
266const sensitiveFields = [271const 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 }
28062813
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-&-nucleus2818 // 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 }
42594270
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);
public/scripts/slash-commands.js+30 -3
@@ -70,7 +70,7 @@ import { hideChatMessageRange } from './chats.js';
70import { getContext, saveMetadataDebounced } from './extensions.js';70import { getContext, saveMetadataDebounced } from './extensions.js';
71import { getRegexedString, regex_placement } from './extensions/regex/engine.js';71import { getRegexedString, regex_placement } from './extensions/regex/engine.js';
72import { findGroupMemberId, groups, is_group_generating, openGroupById, regenerateGroup, resetSelectedGroup, saveGroupChat, selected_group, getGroupMembers } from './group-chats.js';72import { findGroupMemberId, groups, is_group_generating, openGroupById, regenerateGroup, resetSelectedGroup, saveGroupChat, selected_group, getGroupMembers } from './group-chats.js';
73import { chat_completion_sources, oai_settings, promptManager, ZAI_ENDPOINT } from './openai.js';73import { chat_completion_sources, oai_settings, promptManager, SILICONFLOW_ENDPOINT, ZAI_ENDPOINT } from './openai.js';
74import { user_avatar } from './personas.js';74import { user_avatar } from './personas.js';
75import { addEphemeralStoppingString, chat_styles, context_presets, flushEphemeralStoppingStrings, playMessageSound, power_user } from './power-user.js';75import { addEphemeralStoppingString, chat_styles, context_presets, flushEphemeralStoppingStrings, playMessageSound, power_user } from './power-user.js';
76import { SERVER_INPUTS, textgen_types, textgenerationwebui_settings } from './textgen-settings.js';76import { 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 }
67176718
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';
src/constants.js+5 -0
@@ -551,3 +551,8 @@ export const ZAI_ENDPOINT = {
551 COMMON: 'common',551 COMMON: 'common',
552 CODING: 'coding',552 CODING: 'coding',
553};553};
554
555export const SILICONFLOW_ENDPOINT = {
556 GLOBAL: 'global',
557 CN: 'cn',
558};
src/endpoints/backends/chat-completions.js+9 -2
@@ -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';
22import {23import {
@@ -87,6 +88,7 @@ const API_COMETAPI = 'https://api.cometapi.com/v1';
87const API_ZAI_COMMON = 'https://api.z.ai/api/paas/v4';88const API_ZAI_COMMON = 'https://api.z.ai/api/paas/v4';
88const API_ZAI_CODING = 'https://api.z.ai/api/coding/paas/v4';89const API_ZAI_CODING = 'https://api.z.ai/api/coding/paas/v4';
89const API_SILICONFLOW = 'https://api.siliconflow.com/v1';90const API_SILICONFLOW = 'https://api.siliconflow.com/v1';
91const API_SILICONFLOW_CN = 'https://api.siliconflow.cn/v1';
90const API_OPENROUTER = 'https://openrouter.ai/api/v1';92const API_OPENROUTER = 'https://openrouter.ai/api/v1';
9193
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 = {};
src/endpoints/openai.js+42 -1
@@ -8,7 +8,7 @@ import express from 'express';
8import { getConfigValue, mergeObjectWithYaml, excludeKeysByYaml, trimV1, delay } from '../util.js';8import { getConfigValue, mergeObjectWithYaml, excludeKeysByYaml, trimV1, delay } from '../util.js';
9import { setAdditionalHeaders } from '../additional-headers.js';9import { setAdditionalHeaders } from '../additional-headers.js';
10import { readSecret, SECRET_KEYS } from './secrets.js';10import { readSecret, SECRET_KEYS } from './secrets.js';
11import { AIMLAPI_HEADERS, OPENROUTER_HEADERS, ZAI_ENDPOINT } from '../constants.js';11import { AIMLAPI_HEADERS, OPENROUTER_HEADERS, SILICONFLOW_ENDPOINT, ZAI_ENDPOINT } from '../constants.js';
1212
13export const router = express.Router();13export const router = express.Router();
1414
@@ -529,6 +529,47 @@ router.post('/nanogpt/models/embedding', async (request, response) => {
529 }529 }
530});530});
531531
532router.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
532router.post('/generate-image', async (request, response) => {573router.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);
src/endpoints/vectors.js+12 -0
@@ -38,6 +38,7 @@ const SOURCES = [
38 'openrouter',38 'openrouter',
39 'chutes',39 'chutes',
40 'nanogpt',40 'nanogpt',
41 'siliconflow',
41];42];
4243
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 }
8992
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 }
src/vectors/openai-vectors.js+13 -4
@@ -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};
5865
59/**66/**
@@ -62,9 +69,10 @@ const SOURCES = {
62 * @param {string} source - The source of the vector69 * @param {string} source - The source of the vector
63 * @param {import('../users.js').UserDirectoryList} directories - The directories object for the user70 * @param {import('../users.js').UserDirectoryList} directories - The directories object for the user
64 * @param {string} model - The model to use for the embedding71 * @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 texts73 * @returns {Promise<number[][]>} - The array of vectors for the texts
66 */74 */
67export async function getOpenAIBatchVector(texts, source, directories, model = '') {75export async function getOpenAIBatchVector(texts, source, directories, model = '', urlOverride = null) {
68 const config = SOURCES[source];76 const config = SOURCES[source];
6977
70 if (!config) {78 if (!config) {
@@ -80,7 +88,7 @@ export async function getOpenAIBatchVector(texts, source, directories, model = '
80 }88 }
8189
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 vector135 * @param {string} source - The source of the vector
128 * @param {import('../users.js').UserDirectoryList} directories - The directories object for the user136 * @param {import('../users.js').UserDirectoryList} directories - The directories object for the user
129 * @param {string} model - The model to use for the embedding137 * @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 text139 * @returns {Promise<number[]>} - The vector for the text
131 */140 */
132export async function getOpenAIVector(text, source, directories, model = '') {141export 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}