diff --git a/config.json.example b/config.json.example index d842c52..3d316d7 100644 --- a/config.json.example +++ b/config.json.example @@ -1,7 +1,7 @@ { "REQUIRED_API_KEY": "123456", "SERVER_PORT": 3000, - "HOST": "127.0.0.1", + "HOST": "0.0.0.0", "MODEL_PROVIDER": "gemini-cli-oauth", "OPENAI_API_KEY": "xxx", "OPENAI_BASE_URL": "https://openai/v1", diff --git a/provider_pools.json.example b/provider_pools.json.example index e80f201..7b01a21 100644 --- a/provider_pools.json.example +++ b/provider_pools.json.example @@ -5,6 +5,7 @@ "OPENAI_BASE_URL": "https://api.openai.com/v1", "checkModelName": null, "checkHealth": true, + "notSupportedModels": ["gpt-4-turbo"], "uuid": "2f579c65-d3c5-41b1-9985-9f6e3d7bf39c", "isHealthy": true, "isDisabled": false, @@ -18,6 +19,7 @@ "OPENAI_BASE_URL": "https://api.openai.com/v1", "checkModelName": null, "checkHealth": true, + "notSupportedModels": ["gpt-4-turbo", "gpt-4"], "uuid": "e284628d-302f-456d-91f3-6095386fb3b8", "isHealthy": true, "isDisabled": true, diff --git a/src/claude/claude-kiro.js b/src/claude/claude-kiro.js index 44e9072..d4bfe43 100644 --- a/src/claude/claude-kiro.js +++ b/src/claude/claude-kiro.js @@ -4,6 +4,7 @@ import { promises as fs } from 'fs'; import * as path from 'path'; import * as os from 'os'; import * as crypto from 'crypto'; +import { getProviderModels } from '../provider-models.js'; const KIRO_CONSTANTS = { REFRESH_URL: 'https://prod.{{region}}.auth.desktop.kiro.dev/refreshToken', @@ -20,7 +21,11 @@ const KIRO_CONSTANTS = { ORIGIN_AI_EDITOR: 'AI_EDITOR', }; -const MODEL_MAPPING = { +// 从 provider-models.js 获取支持的模型列表 +const KIRO_MODELS = getProviderModels('claude-kiro-oauth'); + +// 完整的模型映射表 +const FULL_MODEL_MAPPING = { "claude-opus-4-5":"claude-opus-4.5", "claude-sonnet-4-5": "CLAUDE_SONNET_4_5_20250929_V1_0", "claude-sonnet-4-5-20250929": "CLAUDE_SONNET_4_5_20250929_V1_0", @@ -30,6 +35,11 @@ const MODEL_MAPPING = { "amazonq-claude-3-7-sonnet-20250219": "CLAUDE_3_7_SONNET_20250219_V1_0" }; +// 只保留 KIRO_MODELS 中存在的模型映射 +const MODEL_MAPPING = Object.fromEntries( + Object.entries(FULL_MODEL_MAPPING).filter(([key]) => KIRO_MODELS.includes(key)) +); + const KIRO_AUTH_TOKEN_FILE = "kiro-auth-token.json"; /** @@ -1115,7 +1125,7 @@ async initializeAuth(forceRefresh = false) { * List available models */ async listModels() { - const models = Object.keys(MODEL_MAPPING).map(id => ({ + const models = KIRO_MODELS.map(id => ({ name: id })); diff --git a/src/common.js b/src/common.js index e2d4946..131376e 100644 --- a/src/common.js +++ b/src/common.js @@ -399,6 +399,13 @@ export async function handleContentGenerationRequest(req, res, service, endpoint } console.log(`[Content Generation] Model: ${model}, Stream: ${isStream}`); + // 2.5. 如果使用了提供商池,根据模型重新选择提供商 + if (providerPoolManager && CONFIG.providerPools && CONFIG.providerPools[CONFIG.MODEL_PROVIDER]) { + const { getApiService } = await import('./service-manager.js'); + service = await getApiService(CONFIG, model); + console.log(`[Content Generation] Re-selected service adapter based on model: ${model}`); + } + // 3. Apply system prompt from file if configured. processedRequestBody = await _applySystemPromptFromFile(CONFIG, processedRequestBody, toProvider); await _manageSystemPrompt(processedRequestBody, toProvider); diff --git a/src/gemini/gemini-core.js b/src/gemini/gemini-core.js index 8a0296e..1a0acd8 100644 --- a/src/gemini/gemini-core.js +++ b/src/gemini/gemini-core.js @@ -5,6 +5,7 @@ import * as path from 'path'; import * as os from 'os'; import * as readline from 'readline'; import { API_ACTIONS, formatExpiryTime } from '../common.js'; +import { getProviderModels } from '../provider-models.js'; // --- Constants --- const AUTH_REDIRECT_PORT = 8085; @@ -14,7 +15,7 @@ const CODE_ASSIST_ENDPOINT = 'https://cloudcode-pa.googleapis.com'; const CODE_ASSIST_API_VERSION = 'v1internal'; const OAUTH_CLIENT_ID = '681255809395-oo8ft2oprdrnp9e3aqf6av3hmdib135j.apps.googleusercontent.com'; const OAUTH_CLIENT_SECRET = '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'; -const GEMINI_MODELS = ['gemini-2.5-flash', 'gemini-2.5-flash-lite', 'gemini-2.5-pro' , 'gemini-2.5-pro-preview-06-05', 'gemini-2.5-flash-preview-09-2025', 'gemini-3-pro-preview']; +const GEMINI_MODELS = getProviderModels('gemini-cli-oauth'); const ANTI_TRUNCATION_MODELS = GEMINI_MODELS.map(model => `anti-${model}`); function is_anti_truncation_model(model) { diff --git a/src/ollama-handler.js b/src/ollama-handler.js index 78b56fc..b2b2eca 100644 --- a/src/ollama-handler.js +++ b/src/ollama-handler.js @@ -505,7 +505,7 @@ export async function handleOllamaChat(req, res, apiService, currentConfig, prov if (detectedProvider !== currentConfig.MODEL_PROVIDER && providerPoolManager) { // Select provider from pool - const providerConfig = providerPoolManager.selectProvider(detectedProvider); + const providerConfig = providerPoolManager.selectProvider(detectedProvider, modelName); if (providerConfig) { actualConfig = { ...currentConfig, @@ -601,7 +601,7 @@ export async function handleOllamaGenerate(req, res, apiService, currentConfig, if (detectedProvider !== currentConfig.MODEL_PROVIDER && providerPoolManager) { // Select provider from pool - const providerConfig = providerPoolManager.selectProvider(detectedProvider); + const providerConfig = providerPoolManager.selectProvider(detectedProvider, modelName); if (providerConfig) { actualConfig = { ...currentConfig, diff --git a/src/openai/qwen-core.js b/src/openai/qwen-core.js index 5a6a1d4..c830d90 100644 --- a/src/openai/qwen-core.js +++ b/src/openai/qwen-core.js @@ -6,14 +6,17 @@ import * as os from 'os'; import open from 'open'; import { EventEmitter } from 'events'; import { randomUUID } from 'node:crypto'; +import { getProviderModels } from '../provider-models.js'; // --- Constants --- const QWEN_DIR = '.qwen'; const QWEN_CREDENTIAL_FILENAME = 'oauth_creds.json'; -const QWEN_MODEL_LIST = [ - { id: 'qwen3-coder-plus', name: 'Qwen3 Coder Plus' }, - { id: 'qwen3-coder-flash', name: 'Qwen3 Coder Flash' }, -]; +// 从 provider-models.js 获取支持的模型列表 +const QWEN_MODELS = getProviderModels('openai-qwen-oauth'); +const QWEN_MODEL_LIST = QWEN_MODELS.map(id => ({ + id: id, + name: id.split('-').map(word => word.charAt(0).toUpperCase() + word.slice(1)).join(' ') +})); const TOKEN_REFRESH_BUFFER_MS = 30 * 1000; const LOCK_TIMEOUT_MS = 10000; diff --git a/src/provider-models.js b/src/provider-models.js new file mode 100644 index 0000000..28150aa --- /dev/null +++ b/src/provider-models.js @@ -0,0 +1,48 @@ +/** + * 各提供商支持的模型列表 + * 用于前端UI选择不支持的模型 + */ + +export const PROVIDER_MODELS = { + 'gemini-cli-oauth': [ + 'gemini-2.5-flash', + 'gemini-2.5-flash-lite', + 'gemini-2.5-pro', + 'gemini-2.5-pro-preview-06-05', + 'gemini-2.5-flash-preview-09-2025', + 'gemini-3-pro-preview' + ], + 'claude-custom': [], + 'claude-kiro-oauth': [ + 'claude-opus-4-5', + 'claude-sonnet-4-5', + 'claude-sonnet-4-5-20250929', + 'claude-sonnet-4-20250514', + 'claude-3-7-sonnet-20250219', + 'amazonq-claude-sonnet-4-20250514', + 'amazonq-claude-3-7-sonnet-20250219' + ], + 'openai-custom': [], + 'openaiResponses-custom': [], + 'openai-qwen-oauth': [ + 'qwen3-coder-plus', + 'qwen3-coder-flash' + ] +}; + +/** + * 获取指定提供商类型支持的模型列表 + * @param {string} providerType - 提供商类型 + * @returns {Array} 模型列表 + */ +export function getProviderModels(providerType) { + return PROVIDER_MODELS[providerType] || []; +} + +/** + * 获取所有提供商的模型列表 + * @returns {Object} 所有提供商的模型映射 + */ +export function getAllProviderModels() { + return PROVIDER_MODELS; +} \ No newline at end of file diff --git a/src/provider-pool-manager.js b/src/provider-pool-manager.js index 2ec7346..de34726 100644 --- a/src/provider-pool-manager.js +++ b/src/provider-pool-manager.js @@ -93,10 +93,12 @@ export class ProviderPoolManager { /** * Selects a provider from the pool for a given provider type. * Currently uses a simple round-robin for healthy providers. + * If requestedModel is provided, providers that don't support the model will be excluded. * @param {string} providerType - The type of provider to select (e.g., 'gemini-cli', 'openai-custom'). + * @param {string} [requestedModel] - Optional. The model name to filter providers by. * @returns {object|null} The selected provider's configuration, or null if no healthy provider is found. */ - selectProvider(providerType) { + selectProvider(providerType, requestedModel = null) { // 参数校验 if (!providerType || typeof providerType !== 'string') { this._log('error', `Invalid providerType: ${providerType}`); @@ -104,28 +106,52 @@ export class ProviderPoolManager { } const availableProviders = this.providerStatus[providerType] || []; - const availableAndHealthyProviders = availableProviders.filter(p => + let availableAndHealthyProviders = availableProviders.filter(p => p.config.isHealthy && !p.config.isDisabled ); + // 如果指定了模型,则排除不支持该模型的提供商 + if (requestedModel) { + const modelFilteredProviders = availableAndHealthyProviders.filter(p => { + // 如果提供商没有配置 notSupportedModels,则认为它支持所有模型 + if (!p.config.notSupportedModels || !Array.isArray(p.config.notSupportedModels)) { + return true; + } + // 检查 notSupportedModels 数组中是否包含请求的模型,如果包含则排除 + return !p.config.notSupportedModels.includes(requestedModel); + }); + + if (modelFilteredProviders.length === 0) { + this._log('warn', `No available providers for type: ${providerType} that support model: ${requestedModel}`); + return null; + } + + availableAndHealthyProviders = modelFilteredProviders; + this._log('debug', `Filtered ${modelFilteredProviders.length} providers supporting model: ${requestedModel}`); + } + if (availableAndHealthyProviders.length === 0) { this._log('warn', `No available and healthy providers for type: ${providerType}`); return null; } - // 简化轮询逻辑 - const currentIndex = this.roundRobinIndex[providerType] || 0; + // 为每个提供商类型和模型组合维护独立的轮询索引 + // 使用组合键:providerType 或 providerType:model + const indexKey = requestedModel ? `${providerType}:${requestedModel}` : providerType; + const currentIndex = this.roundRobinIndex[indexKey] || 0; + + // 使用取模确保索引始终在有效范围内,即使列表长度变化 const providerIndex = currentIndex % availableAndHealthyProviders.length; const selected = availableAndHealthyProviders[providerIndex]; // 更新下次轮询的索引 - this.roundRobinIndex[providerType] = (providerIndex + 1) % availableAndHealthyProviders.length; + this.roundRobinIndex[indexKey] = (currentIndex + 1) % availableAndHealthyProviders.length; // 更新使用信息 selected.config.lastUsed = new Date().toISOString(); selected.config.usageCount++; - this._log('debug', `Selected provider for ${providerType} (round-robin): ${selected.config.uuid}`); + this._log('debug', `Selected provider for ${providerType} (round-robin): ${selected.config.uuid}${requestedModel ? ` for model: ${requestedModel}` : ''}`); // 使用防抖保存 this._debouncedSave(providerType); diff --git a/src/service-manager.js b/src/service-manager.js index 3a381f0..03ab4f5 100644 --- a/src/service-manager.js +++ b/src/service-manager.js @@ -59,21 +59,22 @@ export async function initApiService(config) { /** * Get API service adapter, considering provider pools * @param {Object} config - The current request configuration + * @param {string} [requestedModel] - Optional. The model name to filter providers by. * @returns {Promise} The API service adapter */ -export async function getApiService(config) { +export async function getApiService(config, requestedModel = null) { let serviceConfig = config; if (providerPoolManager && config.providerPools && config.providerPools[config.MODEL_PROVIDER]) { // 如果有号池管理器,并且当前模型提供者类型有对应的号池,则从号池中选择一个提供者配置 - const selectedProviderConfig = providerPoolManager.selectProvider(config.MODEL_PROVIDER); + const selectedProviderConfig = providerPoolManager.selectProvider(config.MODEL_PROVIDER, requestedModel); if (selectedProviderConfig) { // 合并选中的提供者配置到当前请求的 config 中 serviceConfig = deepmerge(config, selectedProviderConfig); delete serviceConfig.providerPools; // 移除 providerPools 属性 config.uuid = serviceConfig.uuid; - console.log(`[API Service] Using pooled configuration for ${config.MODEL_PROVIDER}: ${serviceConfig.uuid}`); + console.log(`[API Service] Using pooled configuration for ${config.MODEL_PROVIDER}: ${serviceConfig.uuid}${requestedModel ? ` (model: ${requestedModel})` : ''}`); } else { - console.warn(`[API Service] No healthy provider found in pool for ${config.MODEL_PROVIDER}. Falling back to main config.`); + console.warn(`[API Service] No healthy provider found in pool for ${config.MODEL_PROVIDER}${requestedModel ? ` supporting model: ${requestedModel}` : ''}. Falling back to main config.`); } } return getServiceAdapter(serviceConfig); diff --git a/src/ui-manager.js b/src/ui-manager.js index f8a7bca..af0deb9 100644 --- a/src/ui-manager.js +++ b/src/ui-manager.js @@ -4,6 +4,7 @@ import path from 'path'; import multer from 'multer'; import crypto from 'crypto'; import { getRequestBody } from './common.js'; +import { getAllProviderModels, getProviderModels } from './provider-models.js'; import { CONFIG } from './config-manager.js'; import { serviceInstances } from './adapter.js'; import { initApiService } from './service-manager.js'; @@ -669,6 +670,27 @@ export async function handleUIApiRequests(method, pathParam, req, res, currentCo return true; } + // Get available models for all providers or specific provider type + if (method === 'GET' && pathParam === '/api/provider-models') { + const allModels = getAllProviderModels(); + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify(allModels)); + return true; + } + + // Get available models for a specific provider type + const providerModelsMatch = pathParam.match(/^\/api\/provider-models\/([^\/]+)$/); + if (method === 'GET' && providerModelsMatch) { + const providerType = decodeURIComponent(providerModelsMatch[1]); + const models = getProviderModels(providerType); + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ + providerType, + models + })); + return true; + } + // Add new provider configuration if (method === 'POST' && pathParam === '/api/providers') { try { diff --git a/static/app/modal.js b/static/app/modal.js index 51e57f8..8984238 100644 --- a/static/app/modal.js +++ b/static/app/modal.js @@ -61,6 +61,36 @@ function showProviderManagerModal(data) { // 添加模态框事件监听 addModalEventListeners(modal); + + // 先获取该提供商类型的模型列表(只调用一次API) + loadModelsForProviderType(providerType, providers); +} + +/** + * 为提供商类型加载模型列表(优化:只调用一次API) + * @param {string} providerType - 提供商类型 + * @param {Array} providers - 提供商列表 + */ +async function loadModelsForProviderType(providerType, providers) { + try { + // 只调用一次API获取模型列表 + const response = await window.apiClient.get(`/provider-models/${encodeURIComponent(providerType)}`); + const models = response.models || []; + + // 为每个提供商渲染模型选择器 + providers.forEach(provider => { + renderNotSupportedModelsSelector(provider.uuid, models, provider.notSupportedModels || []); + }); + } catch (error) { + console.error('Failed to load models for provider type:', error); + // 如果加载失败,为每个提供商显示错误信息 + providers.forEach(provider => { + const container = document.querySelector(`.not-supported-models-container[data-uuid="${provider.uuid}"]`); + if (container) { + container.innerHTML = '
加载模型列表失败
'; + } + }); + } } /** @@ -381,6 +411,23 @@ function renderProviderConfig(provider) { html += ''; } + // 添加 notSupportedModels 配置区域 + html += '
'; + html += ` +
+ +
+
+ 加载模型列表... +
+
+
+ `; + html += '
'; + return html; } @@ -456,6 +503,15 @@ function editProvider(uuid, event) { select.disabled = false; }); + // 启用模型复选框 + const modelCheckboxes = providerDetail.querySelectorAll('.model-checkbox'); + modelCheckboxes.forEach(checkbox => { + checkbox.disabled = false; + }); + + // 添加编辑状态类 + providerDetail.classList.add('editing'); + // 替换编辑按钮为保存和取消按钮,但保留禁用/启用按钮 const actionsGroup = providerDetail.querySelector('.provider-actions-group'); const toggleButton = actionsGroup.querySelector('[onclick*="toggleProviderStatus"]'); @@ -501,6 +557,15 @@ function cancelEdit(uuid, event) { } }); + // 禁用模型复选框 + const modelCheckboxes = providerDetail.querySelectorAll('.model-checkbox'); + modelCheckboxes.forEach(checkbox => { + checkbox.disabled = true; + }); + + // 移除编辑状态类 + providerDetail.classList.remove('editing'); + // 禁用文件上传按钮 const uploadButtons = providerDetail.querySelectorAll('.upload-btn'); uploadButtons.forEach(button => { @@ -563,6 +628,11 @@ async function saveProvider(uuid, event) { providerConfig[key] = value; }); + // 收集不支持的模型列表 + const modelCheckboxes = providerDetail.querySelectorAll(`.model-checkbox[data-uuid="${uuid}"]:checked`); + const notSupportedModels = Array.from(modelCheckboxes).map(checkbox => checkbox.value); + providerConfig.notSupportedModels = notSupportedModels; + try { await window.apiClient.put(`/providers/${encodeURIComponent(providerType)}/${uuid}`, { providerConfig }); await window.apiClient.post('/reload-config'); @@ -630,6 +700,9 @@ async function refreshProviderConfig(providerType) { if (providerList) { providerList.innerHTML = renderProviderList(data.providers); } + + // 重新加载模型列表 + loadModelsForProviderType(providerType, data.providers); } // 同时更新主界面的提供商统计数据 @@ -923,6 +996,42 @@ async function toggleProviderStatus(uuid, event) { } } +/** + * 渲染不支持的模型选择器(不调用API,直接使用传入的模型列表) + * @param {string} uuid - 提供商UUID + * @param {Array} models - 模型列表 + * @param {Array} notSupportedModels - 当前不支持的模型列表 + */ +function renderNotSupportedModelsSelector(uuid, models, notSupportedModels = []) { + const container = document.querySelector(`.not-supported-models-container[data-uuid="${uuid}"]`); + if (!container) return; + + if (models.length === 0) { + container.innerHTML = '
该提供商类型暂无可用模型列表
'; + return; + } + + // 渲染模型复选框列表 + let html = '
'; + models.forEach(model => { + const isChecked = notSupportedModels.includes(model); + html += ` + + `; + }); + html += '
'; + + container.innerHTML = html; +} + // 导出所有函数,并挂载到window对象供HTML调用 export { showProviderManagerModal, @@ -935,7 +1044,9 @@ export { refreshProviderConfig, showAddProviderForm, addProvider, - toggleProviderStatus + toggleProviderStatus, + loadModelsForProviderType, + renderNotSupportedModelsSelector }; // 将函数挂载到window对象 diff --git a/static/app/styles.css b/static/app/styles.css index a4a03c9..45bb74d 100644 --- a/static/app/styles.css +++ b/static/app/styles.css @@ -2985,3 +2985,115 @@ input:checked + .toggle-slider:before { } } + + +/* 不支持的模型选择器样式 */ +.not-supported-models-section { + grid-column: 1 / -1; + margin-top: 16px; +} + +.not-supported-models-section label { + display: flex; + align-items: center; + gap: 8px; + font-weight: 600; + color: #2c3e50; + margin-bottom: 12px; +} + +.not-supported-models-section .help-text { + font-size: 12px; + font-weight: normal; + color: #6c757d; + margin-left: 4px; +} + +.not-supported-models-container { + background: #f8f9fa; + border: 1px solid #dee2e6; + border-radius: 8px; + padding: 16px; + min-height: 100px; +} + +.models-loading { + display: flex; + align-items: center; + justify-content: center; + gap: 8px; + color: #6c757d; + padding: 20px; +} + +.models-checkbox-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); + gap: 12px; +} + +.model-checkbox-label { + display: flex; + align-items: center; + gap: 8px; + padding: 8px 12px; + background: white; + border: 1px solid #dee2e6; + border-radius: 6px; + cursor: pointer; + transition: all 0.2s ease; +} + +.model-checkbox-label:hover { + background: #e9ecef; + border-color: #adb5bd; +} + +.model-checkbox-label input[type="checkbox"] { + cursor: pointer; +} + +.model-checkbox-label input[type="checkbox"]:disabled { + cursor: not-allowed; +} + +.model-checkbox-label .model-name { + font-size: 13px; + color: #495057; + user-select: none; +} + +.model-checkbox-label input[type="checkbox"]:checked + .model-name { + color: #dc3545; + font-weight: 500; +} + +.no-models, +.error-message { + text-align: center; + padding: 20px; + color: #6c757d; + font-size: 14px; +} + +.error-message { + color: #dc3545; +} + +/* 编辑模式下的样式 */ +.provider-item-detail.editing .model-checkbox-label { + cursor: pointer; +} + +.provider-item-detail.editing .model-checkbox-label input[type="checkbox"] { + cursor: pointer; +} + +.provider-item-detail.editing .model-checkbox-label input[type="checkbox"]:not(:disabled) { + cursor: pointer; +} + +/* 全宽配置项 */ +.form-grid.full-width { + grid-column: 1 / -1; +}