diff --git a/src/providers/provider-models.js b/src/providers/provider-models.js
index 8c815a0..a007db4 100644
--- a/src/providers/provider-models.js
+++ b/src/providers/provider-models.js
@@ -1,8 +1,10 @@
+import { convertData } from '../convert/convert.js';
+import { MODEL_PROVIDER } from '../utils/common.js';
+
/**
* 各提供商支持的模型列表
* 用于前端UI选择不支持的模型
*/
-
export const PROVIDER_MODELS = {
'gemini-cli-oauth': [
'gemini-2.5-flash',
@@ -110,6 +112,81 @@ export const PROVIDER_MODELS = {
]
};
+export const MANAGED_MODEL_LIST_PROVIDERS = [
+ MODEL_PROVIDER.OPENAI_CUSTOM,
+ MODEL_PROVIDER.OPENAI_CUSTOM_RESPONSES
+];
+
+export function getManagedModelListProviderType(providerType) {
+ return MANAGED_MODEL_LIST_PROVIDERS.find(baseType =>
+ providerType === baseType || providerType.startsWith(baseType + '-')
+ ) || null;
+}
+
+export function usesManagedModelList(providerType) {
+ return getManagedModelListProviderType(providerType) !== null;
+}
+
+export function normalizeModelIds(models = []) {
+ return [...new Set(
+ (Array.isArray(models) ? models : [])
+ .filter(model => typeof model === 'string')
+ .map(model => model.trim())
+ .filter(Boolean)
+ )].sort((a, b) => a.localeCompare(b));
+}
+
+function extractModelIdsFromListShape(modelList) {
+ if (!modelList) {
+ return [];
+ }
+
+ if (Array.isArray(modelList)) {
+ return modelList.map(item => {
+ if (typeof item === 'string') return item;
+ return item?.id || item?.name || item?.model || null;
+ }).filter(Boolean);
+ }
+
+ if (Array.isArray(modelList.data)) {
+ return modelList.data.map(item => item?.id || item?.name || item?.model || null).filter(Boolean);
+ }
+
+ if (Array.isArray(modelList.models)) {
+ return modelList.models.map(item => {
+ if (typeof item === 'string') return item;
+ return item?.id || item?.name || item?.model || null;
+ }).filter(Boolean);
+ }
+
+ return [];
+}
+
+export function extractModelIdsFromNativeList(modelList, providerType) {
+ let convertedModelList = modelList;
+
+ try {
+ convertedModelList = convertData(modelList, 'modelList', providerType, MODEL_PROVIDER.OPENAI_CUSTOM);
+ } catch {
+ convertedModelList = modelList;
+ }
+
+ const convertedIds = normalizeModelIds(extractModelIdsFromListShape(convertedModelList));
+ if (convertedIds.length > 0) {
+ return convertedIds;
+ }
+
+ return normalizeModelIds(extractModelIdsFromListShape(modelList));
+}
+
+export function getConfiguredSupportedModels(providerType, providerConfig = {}) {
+ if (!usesManagedModelList(providerType)) {
+ return [];
+ }
+
+ return normalizeModelIds(providerConfig?.supportedModels);
+}
+
/**
* 获取指定提供商类型支持的模型列表
* @param {string} providerType - 提供商类型
@@ -119,14 +196,14 @@ export function getProviderModels(providerType) {
if (PROVIDER_MODELS[providerType]) {
return PROVIDER_MODELS[providerType];
}
-
+
// 尝试前缀匹配 (例如 openai-custom-1 -> openai-custom)
for (const key of Object.keys(PROVIDER_MODELS)) {
if (providerType.startsWith(key + '-')) {
return PROVIDER_MODELS[key];
}
}
-
+
return [];
}
diff --git a/src/providers/provider-pool-manager.js b/src/providers/provider-pool-manager.js
index ab87f0a..25609cd 100644
--- a/src/providers/provider-pool-manager.js
+++ b/src/providers/provider-pool-manager.js
@@ -2,9 +2,13 @@ import * as fs from 'fs';
import { getServiceAdapter, getRegisteredProviders } from './adapter.js';
import logger from '../utils/logger.js';
import { MODEL_PROVIDER, getProtocolPrefix } from '../utils/common.js';
-import { getProviderModels } from './provider-models.js';
-import { broadcastEvent } from '../ui-modules/event-broadcast.js';
import { convertData } from '../convert/convert.js';
+import {
+ getConfiguredSupportedModels,
+ getProviderModels,
+ normalizeModelIds
+} from './provider-models.js';
+import { broadcastEvent } from '../ui-modules/event-broadcast.js';
import { ENDPOINT_TYPE } from '../utils/common.js';
/**
@@ -885,6 +889,10 @@ export class ProviderPoolManager {
// 如果指定了模型,则排除不支持该模型的提供商
if (requestedModel) {
const modelFilteredProviders = availableAndHealthyProviders.filter(p => {
+ const supportedModels = getConfiguredSupportedModels(providerType, p.config);
+ if (supportedModels.length > 0) {
+ return supportedModels.includes(requestedModel);
+ }
// 如果提供商没有配置 notSupportedModels,则认为它支持所有模型
if (!p.config.notSupportedModels || !Array.isArray(p.config.notSupportedModels)) {
return true;
@@ -1282,6 +1290,15 @@ export class ProviderPoolManager {
for (const providerType of allProviderTypes) {
if (this.providerStatus[providerType]) {
let models = getProviderModels(providerType);
+ const configuredSupportedModels = normalizeModelIds(
+ this.providerStatus[providerType].flatMap(providerStatus =>
+ getConfiguredSupportedModels(providerType, providerStatus.config)
+ )
+ );
+
+ if (configuredSupportedModels.length > 0) {
+ models = configuredSupportedModels;
+ }
// 如果硬编码的模型列表为空,或者该类型的提供商在号池中没有配置节点,尝试从服务获取
if (models.length === 0) {
diff --git a/src/services/ui-manager.js b/src/services/ui-manager.js
index cd71bb9..f1d6bea 100644
--- a/src/services/ui-manager.js
+++ b/src/services/ui-manager.js
@@ -144,7 +144,7 @@ export async function handleUIApiRequests(method, pathParam, req, res, currentCo
const providerModelsMatch = pathParam.match(/^\/api\/provider-models\/([^\/]+)$/);
if (method === 'GET' && providerModelsMatch) {
const providerType = decodeURIComponent(providerModelsMatch[1]);
- return await providerApi.handleGetProviderTypeModels(req, res, providerType);
+ return await providerApi.handleGetProviderTypeModels(req, res, currentConfig, providerPoolManager, providerType);
}
// Add new provider configuration
@@ -168,6 +168,22 @@ export async function handleUIApiRequests(method, pathParam, req, res, currentCo
return await providerApi.handleHealthCheck(req, res, currentConfig, providerPoolManager, providerType);
}
+ // Detect available models for a specific provider node
+ const detectModelsMatch = pathParam.match(/^\/api\/providers\/([^\/]+)\/([^\/]+)\/detect-models$/);
+ if (method === 'POST' && detectModelsMatch) {
+ const providerType = decodeURIComponent(detectModelsMatch[1]);
+ const providerUuid = detectModelsMatch[2];
+ return await providerApi.handleDetectProviderModels(req, res, currentConfig, providerPoolManager, providerType, providerUuid);
+ }
+
+ // Perform health check for a specific provider node
+ const singleHealthCheckMatch = pathParam.match(/^\/api\/providers\/([^\/]+)\/([^\/]+)\/health-check$/);
+ if (method === 'POST' && singleHealthCheckMatch) {
+ const providerType = decodeURIComponent(singleHealthCheckMatch[1]);
+ const providerUuid = singleHealthCheckMatch[2];
+ return await providerApi.handleSingleProviderHealthCheck(req, res, currentConfig, providerPoolManager, providerType, providerUuid);
+ }
+
// Delete all unhealthy providers for a specific type
// NOTE: This must be before the generic /{providerType}/{uuid} route to avoid matching 'delete-unhealthy' as UUID
const deleteUnhealthyMatch = pathParam.match(/^\/api\/providers\/([^\/]+)\/delete-unhealthy$/);
@@ -348,4 +364,4 @@ export async function handleUIApiRequests(method, pathParam, req, res, currentCo
}
return false;
-}
\ No newline at end of file
+}
diff --git a/src/ui-modules/provider-api.js b/src/ui-modules/provider-api.js
index d6692ad..7d680ca 100644
--- a/src/ui-modules/provider-api.js
+++ b/src/ui-modules/provider-api.js
@@ -1,10 +1,16 @@
import { existsSync, readFileSync, writeFileSync } from 'fs';
import logger from '../utils/logger.js';
import { getRequestBody } from '../utils/common.js';
-import { getAllProviderModels, getProviderModels } from '../providers/provider-models.js';
+import {
+ extractModelIdsFromNativeList,
+ getConfiguredSupportedModels,
+ getProviderModels,
+ normalizeModelIds,
+ usesManagedModelList
+} from '../providers/provider-models.js';
import { generateUUID, createProviderConfig, formatSystemPath, detectProviderFromPath, addToUsedPaths, isPathUsed, pathsEqual } from '../utils/provider-utils.js';
import { broadcastEvent } from './event-broadcast.js';
-import { getRegisteredProviders } from '../providers/adapter.js';
+import { getRegisteredProviders, getServiceAdapter, serviceInstances } from '../providers/adapter.js';
// 文件级互斥锁:防止并发读写导致数据丢失
// 安全净化:移除用户输入字段中的危险内容(script、事件处理器、javascript:协议等),
@@ -87,6 +93,110 @@ function filterMaskedData(data) {
return result;
}
+function getProviderPoolsFilePath(currentConfig) {
+ return currentConfig.PROVIDER_POOLS_FILE_PATH || 'configs/provider_pools.json';
+}
+
+function loadProviderPools(currentConfig, providerPoolManager) {
+ const filePath = getProviderPoolsFilePath(currentConfig);
+
+ if (providerPoolManager?.providerPools) {
+ return providerPoolManager.providerPools;
+ }
+
+ if (!existsSync(filePath)) {
+ return {};
+ }
+
+ return JSON.parse(readFileSync(filePath, 'utf-8'));
+}
+
+function getManagedSupportedModels(providerType, providers = []) {
+ return normalizeModelIds(
+ providers.flatMap(provider => getConfiguredSupportedModels(providerType, provider))
+ );
+}
+
+function persistProviderStatusToFile(currentConfig, providerPoolManager) {
+ const filePath = currentConfig.PROVIDER_POOLS_FILE_PATH || 'configs/provider_pools.json';
+ const providerPools = {};
+
+ for (const providerType in providerPoolManager.providerStatus) {
+ providerPools[providerType] = providerPoolManager.providerStatus[providerType].map(providerStatus => providerStatus.config);
+ }
+
+ writeFileSync(filePath, JSON.stringify(providerPools, null, 2), 'utf-8');
+ return filePath;
+}
+
+function isAuthHealthCheckError(errorMessage = '') {
+ return /\b(401|403)\b/.test(errorMessage) ||
+ /\b(Unauthorized|Forbidden|AccessDenied|InvalidToken|ExpiredToken)\b/i.test(errorMessage);
+}
+
+async function runProviderHealthCheck(providerPoolManager, providerType, providerStatus) {
+ const providerConfig = providerStatus.config;
+
+ try {
+ const healthResult = await providerPoolManager._checkProviderHealth(providerType, providerConfig);
+
+ if (healthResult.success) {
+ providerPoolManager.markProviderHealthy(providerType, providerConfig, false, healthResult.modelName);
+ return {
+ uuid: providerConfig.uuid,
+ success: true,
+ healthy: true,
+ modelName: healthResult.modelName,
+ message: 'Healthy'
+ };
+ }
+
+ const errorMessage = healthResult.errorMessage || 'Check failed';
+ const isAuthError = isAuthHealthCheckError(errorMessage);
+
+ if (isAuthError) {
+ providerPoolManager.markProviderUnhealthyImmediately(providerType, providerConfig, errorMessage);
+ logger.info(`[UI API] Auth error detected for ${providerConfig.uuid}, immediately marked as unhealthy`);
+ } else {
+ providerPoolManager.markProviderUnhealthy(providerType, providerConfig, errorMessage);
+ }
+
+ providerStatus.config.lastHealthCheckTime = new Date().toISOString();
+ if (healthResult.modelName) {
+ providerStatus.config.lastHealthCheckModel = healthResult.modelName;
+ }
+
+ return {
+ uuid: providerConfig.uuid,
+ success: false,
+ healthy: false,
+ modelName: healthResult.modelName,
+ message: errorMessage,
+ isAuthError
+ };
+ } catch (error) {
+ const errorMessage = error.message || 'Unknown error';
+ const isAuthError = isAuthHealthCheckError(errorMessage);
+
+ if (isAuthError) {
+ providerPoolManager.markProviderUnhealthyImmediately(providerType, providerConfig, errorMessage);
+ logger.info(`[UI API] Auth error detected for ${providerConfig.uuid}, immediately marked as unhealthy`);
+ } else {
+ providerPoolManager.markProviderUnhealthy(providerType, providerConfig, errorMessage);
+ }
+
+ providerStatus.config.lastHealthCheckTime = new Date().toISOString();
+
+ return {
+ uuid: providerConfig.uuid,
+ success: false,
+ healthy: false,
+ message: errorMessage,
+ isAuthError
+ };
+ }
+}
+
// 使用 Promise 链式队列,确保文件操作顺序执行
let _fileLockChain = Promise.resolve();
@@ -228,26 +338,27 @@ export async function handleGetSupportedProviders(req, res, currentConfig, provi
*/
export async function handleGetProviderModels(req, res, currentConfig, providerPoolManager) {
const registeredProviders = getRegisteredProviders();
- let poolTypes = [];
+ let providerPools = {};
// 获取所有存在的类型(基础 + 动态)
- const filePath = currentConfig.PROVIDER_POOLS_FILE_PATH || 'configs/provider_pools.json';
try {
- if (providerPoolManager && providerPoolManager.providerPools) {
- poolTypes = Object.keys(providerPoolManager.providerPools);
- } else if (existsSync(filePath)) {
- const poolsData = JSON.parse(readFileSync(filePath, 'utf-8'));
- poolTypes = Object.keys(poolsData);
- }
+ providerPools = loadProviderPools(currentConfig, providerPoolManager);
} catch (error) {
logger.warn('[UI API] Failed to load provider pools for models:', error.message);
}
+ const poolTypes = Object.keys(providerPools);
const allTypes = [...new Set([...registeredProviders, ...poolTypes])];
const allModels = {};
allTypes.forEach(type => {
- const models = getProviderModels(type);
+ let models = getProviderModels(type);
+ if (usesManagedModelList(type)) {
+ const managedModels = getManagedSupportedModels(type, providerPools[type] || []);
+ if (managedModels.length > 0) {
+ models = managedModels;
+ }
+ }
if (models && models.length > 0) {
allModels[type] = models;
}
@@ -261,8 +372,19 @@ export async function handleGetProviderModels(req, res, currentConfig, providerP
/**
* 获取特定提供商类型的可用模型
*/
-export async function handleGetProviderTypeModels(req, res, providerType) {
- const models = getProviderModels(providerType);
+export async function handleGetProviderTypeModels(req, res, currentConfig, providerPoolManager, providerType) {
+ let models = getProviderModels(providerType);
+ if (usesManagedModelList(providerType)) {
+ try {
+ const providerPools = loadProviderPools(currentConfig, providerPoolManager);
+ const managedModels = getManagedSupportedModels(providerType, providerPools[providerType] || []);
+ if (managedModels.length > 0) {
+ models = managedModels;
+ }
+ } catch (error) {
+ logger.warn('[UI API] Failed to load managed provider models:', error.message);
+ }
+ }
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
providerType,
@@ -271,6 +393,70 @@ export async function handleGetProviderTypeModels(req, res, providerType) {
return true;
}
+/**
+ * Detect available models for a specific provider node.
+ */
+export async function handleDetectProviderModels(req, res, currentConfig, providerPoolManager, providerType, providerUuid) {
+ try {
+ if (!usesManagedModelList(providerType)) {
+ res.writeHead(400, { 'Content-Type': 'application/json' });
+ res.end(JSON.stringify({ error: { message: `Model detection is not supported for provider type: ${providerType}` } }));
+ return true;
+ }
+
+ const providerPools = loadProviderPools(currentConfig, providerPoolManager);
+ const providers = providerPools[providerType] || [];
+ const existingProvider = providers.find(provider => provider.uuid === providerUuid);
+
+ if (!existingProvider) {
+ res.writeHead(404, { 'Content-Type': 'application/json' });
+ res.end(JSON.stringify({ error: { message: 'Provider not found' } }));
+ return true;
+ }
+
+ const body = await getRequestBody(req);
+ const draftConfig = filterMaskedData(body?.providerConfig || {});
+ const detectionUuid = `${providerUuid}-detect-models`;
+ const instanceKey = `${providerType}${detectionUuid}`;
+ const tempConfig = {
+ ...currentConfig,
+ ...existingProvider,
+ ...draftConfig,
+ MODEL_PROVIDER: providerType,
+ uuid: detectionUuid
+ };
+
+ let models = [];
+ try {
+ delete serviceInstances[instanceKey];
+ const serviceAdapter = getServiceAdapter(tempConfig);
+ if (typeof serviceAdapter.listModels !== 'function') {
+ throw new Error(`Provider ${providerType} does not support model detection`);
+ }
+
+ const nativeModels = await serviceAdapter.listModels();
+ models = extractModelIdsFromNativeList(nativeModels, providerType);
+ } finally {
+ delete serviceInstances[instanceKey];
+ }
+
+ res.writeHead(200, { 'Content-Type': 'application/json' });
+ res.end(JSON.stringify({
+ success: true,
+ providerType,
+ uuid: providerUuid,
+ count: models.length,
+ models,
+ selectedModels: getConfiguredSupportedModels(providerType, existingProvider)
+ }));
+ return true;
+ } catch (error) {
+ res.writeHead(500, { 'Content-Type': 'application/json' });
+ res.end(JSON.stringify({ error: { message: error.message } }));
+ return true;
+ }
+}
+
/**
* 添加新的提供商配置
*/
@@ -324,6 +510,10 @@ async function _handleAddProvider(req, res, currentConfig, providerPoolManager)
// 过滤掉脱敏字段
const filteredConfig = filterMaskedData(providerConfig);
+ if (usesManagedModelList(providerType)) {
+ filteredConfig.supportedModels = normalizeModelIds(filteredConfig.supportedModels);
+ filteredConfig.notSupportedModels = [];
+ }
providerPools[providerType].push(filteredConfig);
// Save to file
@@ -419,6 +609,10 @@ async function _handleUpdateProvider(req, res, currentConfig, providerPoolManage
// 过滤掉传入配置中的脱敏占位符,避免覆盖真实数据
const filteredConfig = filterMaskedData(providerConfig);
+ if (usesManagedModelList(providerType)) {
+ filteredConfig.supportedModels = normalizeModelIds(filteredConfig.supportedModels);
+ filteredConfig.notSupportedModels = [];
+ }
const updatedProvider = {
...existingProvider,
@@ -1030,6 +1224,59 @@ export async function handleHealthCheck(req, res, currentConfig, providerPoolMan
* 快速链接配置文件到对应的提供商
* 支持单个文件路径或文件路径数组
*/
+export async function handleSingleProviderHealthCheck(req, res, currentConfig, providerPoolManager, providerType, providerUuid) {
+ try {
+ if (!providerPoolManager) {
+ res.writeHead(400, { 'Content-Type': 'application/json' });
+ res.end(JSON.stringify({ error: { message: 'Provider pool manager not initialized' } }));
+ return true;
+ }
+
+ const providers = providerPoolManager.providerStatus[providerType] || [];
+ const providerStatus = providers.find(item => item.config?.uuid === providerUuid);
+
+ if (!providerStatus) {
+ res.writeHead(404, { 'Content-Type': 'application/json' });
+ res.end(JSON.stringify({ error: { message: 'Provider not found' } }));
+ return true;
+ }
+
+ logger.info(`[UI API] Starting single health check for provider ${providerUuid} in ${providerType}`);
+
+ const result = await runProviderHealthCheck(providerPoolManager, providerType, providerStatus);
+ const filePath = persistProviderStatusToFile(currentConfig, providerPoolManager);
+
+ broadcastEvent('config_update', {
+ action: 'health_check_single',
+ filePath,
+ providerType,
+ providerUuid,
+ result: {
+ ...result,
+ message: sanitizeProviderData({ message: result.message }).message
+ },
+ timestamp: new Date().toISOString()
+ });
+
+ res.writeHead(200, { 'Content-Type': 'application/json' });
+ res.end(JSON.stringify({
+ success: true,
+ providerType,
+ uuid: providerUuid,
+ healthy: result.healthy,
+ modelName: result.modelName || null,
+ message: result.message,
+ isAuthError: result.isAuthError || false
+ }));
+ return true;
+ } catch (error) {
+ logger.error('[UI API] Single health check error:', error);
+ res.writeHead(500, { 'Content-Type': 'application/json' });
+ res.end(JSON.stringify({ error: { message: error.message } }));
+ return true;
+ }
+}
+
export async function handleQuickLinkProvider(req, res, currentConfig, providerPoolManager) {
try {
const body = await getRequestBody(req);
@@ -1252,4 +1499,4 @@ export async function handleRefreshProviderUuid(req, res, currentConfig, provide
res.end(JSON.stringify({ error: { message: error.message } }));
return true;
}
-}
\ No newline at end of file
+}
diff --git a/src/utils/common.js b/src/utils/common.js
index 3582277..f6b801f 100644
--- a/src/utils/common.js
+++ b/src/utils/common.js
@@ -76,6 +76,41 @@ export const MODEL_PROVIDER = {
AUTO: 'auto',
}
+const MANAGED_MODEL_LIST_PROVIDER_TYPES = new Set([
+ 'openai-custom',
+ 'openaiResponses-custom'
+]);
+
+function usesManagedModelList(providerType = '') {
+ return [...MANAGED_MODEL_LIST_PROVIDER_TYPES].some(baseType =>
+ providerType === baseType || providerType.startsWith(baseType + '-')
+ );
+}
+
+function getConfiguredSupportedModels(providerType, providerConfig = {}) {
+ if (!usesManagedModelList(providerType)) {
+ return [];
+ }
+
+ return [...new Set(
+ (Array.isArray(providerConfig?.supportedModels) ? providerConfig.supportedModels : [])
+ .filter(model => typeof model === 'string')
+ .map(model => model.trim())
+ .filter(Boolean)
+ )].sort((a, b) => a.localeCompare(b));
+}
+
+function getConfiguredSupportedModelsFromPool(providerPoolManager, providerType) {
+ if (!providerPoolManager?.providerStatus?.[providerType]) {
+ return [];
+ }
+
+ return [...new Set(
+ providerPoolManager.providerStatus[providerType]
+ .flatMap(providerStatus => getConfiguredSupportedModels(providerType, providerStatus.config))
+ )].sort((a, b) => a.localeCompare(b));
+}
+
/**
* Extracts the protocol prefix from a given model provider string.
* This is used to determine if two providers belong to the same underlying protocol (e.g., gemini, openai, claude).
@@ -822,6 +857,35 @@ export async function handleModelListRequest(req, res, service, endpointType, CO
let clientModelList;
+ const buildConfiguredModelListResponse = (models, providerType, listEndpointType) => {
+ if (listEndpointType === ENDPOINT_TYPE.OPENAI_MODEL_LIST) {
+ return {
+ object: 'list',
+ data: models.map(model => ({
+ id: model,
+ object: 'model',
+ created: Math.floor(Date.now() / 1000),
+ owned_by: providerType
+ }))
+ };
+ }
+
+ if (listEndpointType === ENDPOINT_TYPE.GEMINI_MODEL_LIST) {
+ return {
+ models: models.map(model => ({
+ name: `models/${model}`,
+ baseModelId: model,
+ version: 'v1',
+ displayName: model,
+ description: `Model ${model} provided by ${providerType}`,
+ supportedGenerationMethods: ['generateContent', 'countTokens']
+ }))
+ };
+ }
+
+ return { data: [] };
+ };
+
// --- 核心逻辑: auto 路由模式下的模型聚合 ---
if (CONFIG.MODEL_PROVIDER === MODEL_PROVIDER.AUTO && providerPoolManager) {
logger.info(`[ModelList] Aggregating models for 'auto' mode...`);
@@ -829,6 +893,15 @@ export async function handleModelListRequest(req, res, service, endpointType, CO
} else {
// --- 单提供商逻辑 ---
const toProvider = CONFIG.MODEL_PROVIDER;
+ const pooledSupportedModels = getConfiguredSupportedModelsFromPool(providerPoolManager, toProvider);
+ const configuredSupportedModels = pooledSupportedModels.length > 0
+ ? pooledSupportedModels
+ : getConfiguredSupportedModels(toProvider, CONFIG);
+
+ if (usesManagedModelList(toProvider) && configuredSupportedModels.length > 0) {
+ logger.info(`[ModelList] Returning configured supported models for ${toProvider}: ${configuredSupportedModels.join(', ')}`);
+ clientModelList = buildConfiguredModelListResponse(configuredSupportedModels, toProvider, endpointType);
+ } else {
// service 可能未在上层预先注入(例如仅改了路径 provider 前缀),这里兜底获取
let resolvedService = service;
@@ -852,6 +925,7 @@ export async function handleModelListRequest(req, res, service, endpointType, CO
} else {
logger.info(`[ModelList Convert] Model list format matches. No conversion needed.`);
}
+ }
}
// logger.info(`[ModelList Response] Sending model list to client: ${JSON.stringify(clientModelList)}`);
diff --git a/static/app/i18n.js b/static/app/i18n.js
index 9aa38d1..5c7f6a6 100644
--- a/static/app/i18n.js
+++ b/static/app/i18n.js
@@ -509,6 +509,17 @@ const translations = {
'modal.provider.loadingModels': '加载模型列表...',
'modal.provider.unsupportedModels': '不支持的模型',
'modal.provider.unsupportedModelsHelp': '选择此提供商不支持的模型,系统会自动排除这些模型',
+ 'modal.provider.supportedModels': '模型列表',
+ 'modal.provider.supportedModelsHelp': '仅保留你想让这个节点使用的模型,健康检查模型也可以直接从这里挑选',
+ 'modal.provider.supportedModelsEmpty': '还没有选择模型。进入编辑后可点击“检测可用模型”获取候选列表。',
+ 'modal.provider.detectModels': '检测可用模型',
+ 'modal.provider.detectModelsFailed': '检测可用模型失败',
+ 'modal.provider.detectModelsNoResults': '没有检测到可用模型',
+ 'modal.provider.modelPickerTitle': '{type} 可用模型',
+ 'modal.provider.modelPickerSearchPlaceholder': '搜索模型名称',
+ 'modal.provider.modelPickerSelectAll': '全选当前结果',
+ 'modal.provider.modelPickerClearAll': '清空已选',
+ 'modal.provider.modelPickerSelected': '已选择 {count} 个模型',
'modal.provider.addTitle': '添加新提供商配置',
'modal.provider.customName': '自定义名称',
'modal.provider.checkModelName': '检查模型名称',
@@ -582,6 +593,11 @@ const translations = {
'modal.provider.refreshUnhealthyUuids.failed': '刷新失败',
'modal.provider.kiroAuthHint': '使用 AWS Builder ID 登录方式时,需要 clientId 和 clientSecret 字段,可在同文件夹下的另一个 JSON 文件中获取',
+ 'modal.provider.healthCheckCurrentTitle': '对当前节点立即执行一次健康检查',
+ 'modal.provider.healthCheckSingleSuccess': '健康检查通过',
+ 'modal.provider.healthCheckSingleSuccessWithModel': '健康检查通过,使用模型: {model}',
+ 'modal.provider.healthCheckSingleFailed': '健康检查失败: {message}',
+
// Pagination
'pagination.showing': '显示 {start}-{end} / 共 {total} 条',
'pagination.jumpTo': '跳转到',
@@ -818,6 +834,7 @@ const translations = {
// Common
'common.confirm': '确定',
'common.cancel': '取消',
+ 'common.close': '关闭',
'common.success': '成功',
'common.enabled': '已启用',
'common.disabled': '已禁用',
@@ -1381,6 +1398,17 @@ const translations = {
'modal.provider.loadingModels': 'Loading models...',
'modal.provider.unsupportedModels': 'Unsupported Models',
'modal.provider.unsupportedModelsHelp': 'Select models not supported by this provider; they will be excluded automatically',
+ 'modal.provider.supportedModels': 'Model List',
+ 'modal.provider.supportedModelsHelp': 'Keep only the models you want this node to use, including a convenient health-check model choice',
+ 'modal.provider.supportedModelsEmpty': 'No models selected yet. Enter edit mode and click "Detect Available Models" to load candidates.',
+ 'modal.provider.detectModels': 'Detect Available Models',
+ 'modal.provider.detectModelsFailed': 'Failed to detect available models',
+ 'modal.provider.detectModelsNoResults': 'No available models were detected',
+ 'modal.provider.modelPickerTitle': '{type} Available Models',
+ 'modal.provider.modelPickerSearchPlaceholder': 'Search model names',
+ 'modal.provider.modelPickerSelectAll': 'Select visible results',
+ 'modal.provider.modelPickerClearAll': 'Clear selected',
+ 'modal.provider.modelPickerSelected': '{count} models selected',
'modal.provider.addTitle': 'Add New Provider Config',
'modal.provider.customName': 'Custom Name',
'modal.provider.checkModelName': 'Check Model Name',
@@ -1453,6 +1481,10 @@ const translations = {
'modal.provider.refreshUnhealthyUuids.success': 'Refreshed {count} UUID(s)',
'modal.provider.refreshUnhealthyUuids.failed': 'Refresh failed',
'modal.provider.kiroAuthHint': 'When using AWS Builder ID login, clientId and clientSecret fields are required, which can be found in another JSON file in the same folder',
+ 'modal.provider.healthCheckCurrentTitle': 'Run a health check for this node now',
+ 'modal.provider.healthCheckSingleSuccess': 'Health check passed',
+ 'modal.provider.healthCheckSingleSuccessWithModel': 'Health check passed using model: {model}',
+ 'modal.provider.healthCheckSingleFailed': 'Health check failed: {message}',
// Pagination
'pagination.showing': 'Showing {start}-{end} of {total}',
@@ -1691,6 +1723,7 @@ const translations = {
'common.togglePassword': 'Show/Hide Password',
'common.confirm': 'Confirm',
'common.cancel': 'Cancel',
+ 'common.close': 'Close',
'common.success': 'Success',
'common.enabled': 'Enabled',
'common.disabled': 'Disabled',
diff --git a/static/app/modal.js b/static/app/modal.js
index 16132eb..404df5f 100644
--- a/static/app/modal.js
+++ b/static/app/modal.js
@@ -1,16 +1,383 @@
// 模态框管理模块
-import { showToast, getFieldLabel, getProviderTypeFields } from './utils.js';
+import { escapeHtml, showToast, getFieldLabel, getProviderTypeFields } from './utils.js';
import { handleProviderPasswordToggle } from './event-handlers.js';
import { t } from './i18n.js';
+const MANAGED_MODEL_LIST_PROVIDERS = new Set(['openai-custom', 'openaiResponses-custom']);
+
// 分页配置
const PROVIDERS_PER_PAGE = 5;
let currentPage = 1;
let currentProviders = [];
let currentProviderType = '';
+
+function usesManagedModelList(providerType = '') {
+ return Array.from(MANAGED_MODEL_LIST_PROVIDERS).some(baseType =>
+ providerType === baseType || providerType.startsWith(`${baseType}-`)
+ );
+}
+
+function normalizeModelList(models = []) {
+ return [...new Set(
+ (Array.isArray(models) ? models : [])
+ .filter(model => typeof model === 'string')
+ .map(model => model.trim())
+ .filter(Boolean)
+ )].sort((a, b) => a.localeCompare(b));
+}
+
+function serializeModelsData(models = []) {
+ return encodeURIComponent(JSON.stringify(normalizeModelList(models)));
+}
+
+function parseModelsData(rawValue = '') {
+ if (!rawValue) {
+ return [];
+ }
+
+ try {
+ return normalizeModelList(JSON.parse(decodeURIComponent(rawValue)));
+ } catch (error) {
+ console.warn('Failed to parse models data:', error);
+ return [];
+ }
+}
+
+function renderSupportedModelsValue(models = []) {
+ const selectedModels = normalizeModelList(models);
+ if (selectedModels.length === 0) {
+ return `