feat(provider): 实现基于模型选择的提供商池管理功能

添加 provider-models.js 集中管理各提供商支持的模型列表
修改 provider-pool-manager.js 支持根据请求模型过滤提供商
更新服务管理器和请求处理逻辑以支持模型感知的提供商选择
添加前端UI支持配置不支持的模型列表
更新示例配置文件展示新功能
This commit is contained in:
hex2077 2025-11-29 16:56:59 +08:00
parent 9c114e2a7d
commit fec0b19bd4
13 changed files with 364 additions and 21 deletions

View file

@ -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",

View file

@ -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,

View file

@ -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
}));

View file

@ -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);

View file

@ -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) {

View file

@ -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,

View file

@ -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;

48
src/provider-models.js Normal file
View file

@ -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<string>} 模型列表
*/
export function getProviderModels(providerType) {
return PROVIDER_MODELS[providerType] || [];
}
/**
* 获取所有提供商的模型列表
* @returns {Object} 所有提供商的模型映射
*/
export function getAllProviderModels() {
return PROVIDER_MODELS;
}

View file

@ -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);

View file

@ -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<Object>} 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);

View file

@ -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 {

View file

@ -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 = '<div class="error-message">加载模型列表失败</div>';
}
});
}
}
/**
@ -381,6 +411,23 @@ function renderProviderConfig(provider) {
html += '</div>';
}
// 添加 notSupportedModels 配置区域
html += '<div class="form-grid full-width">';
html += `
<div class="config-item not-supported-models-section">
<label>
<i class="fas fa-ban"></i>
<span class="help-text">选择此提供商不支持的模型系统会自动排除这些模型</span>
</label>
<div class="not-supported-models-container" data-uuid="${provider.uuid}">
<div class="models-loading">
<i class="fas fa-spinner fa-spin"></i> ...
</div>
</div>
</div>
`;
html += '</div>';
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 = '<div class="no-models">该提供商类型暂无可用模型列表</div>';
return;
}
// 渲染模型复选框列表
let html = '<div class="models-checkbox-grid">';
models.forEach(model => {
const isChecked = notSupportedModels.includes(model);
html += `
<label class="model-checkbox-label">
<input type="checkbox"
class="model-checkbox"
value="${model}"
data-uuid="${uuid}"
${isChecked ? 'checked' : ''}
disabled>
<span class="model-name">${model}</span>
</label>
`;
});
html += '</div>';
container.innerHTML = html;
}
// 导出所有函数并挂载到window对象供HTML调用
export {
showProviderManagerModal,
@ -935,7 +1044,9 @@ export {
refreshProviderConfig,
showAddProviderForm,
addProvider,
toggleProviderStatus
toggleProviderStatus,
loadModelsForProviderType,
renderNotSupportedModelsSelector
};
// 将函数挂载到window对象

View file

@ -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;
}