diff --git a/src/handlers/request-handler.js b/src/handlers/request-handler.js index c9c1f2a..6fb542e 100644 --- a/src/handlers/request-handler.js +++ b/src/handlers/request-handler.js @@ -142,7 +142,7 @@ export function createRequestHandler(config, providerPoolManager) { return true; } catch (error) { logger.info(`[Server] req provider_health error: ${error.message}`); - handleError(res, { statusCode: 500, message: `Failed to get providers health: ${error.message}` }, currentConfig.MODEL_PROVIDER); + handleError(res, { status: 500, message: `Failed to get providers health: ${error.message}` }, currentConfig.MODEL_PROVIDER, null, req); return; } } @@ -157,8 +157,7 @@ export function createRequestHandler(config, providerPoolManager) { logger.info(`[Config] MODEL_PROVIDER overridden by header to: ${currentConfig.MODEL_PROVIDER}`); } else { logger.warn(`[Config] Provider ${modelProviderHeader} in header is not available.`); - res.writeHead(400, { 'Content-Type': 'application/json' }); - res.end(JSON.stringify({ error: { message: `Provider ${modelProviderHeader} is not available.` } })); + handleError(res, { status: 400, message: `Provider ${modelProviderHeader} in header is not available.` }, currentConfig.MODEL_PROVIDER, null, req); return; } } @@ -180,8 +179,7 @@ export function createRequestHandler(config, providerPoolManager) { } else if (firstSegment && Object.values(MODEL_PROVIDER).includes(firstSegment)) { // 如果在 MODEL_PROVIDER 中但没注册适配器,拦截并报错 logger.warn(`[Config] Provider ${firstSegment} is recognized but no adapter is registered.`); - res.writeHead(400, { 'Content-Type': 'application/json' }); - res.end(JSON.stringify({ error: { message: `Provider ${firstSegment} is not available.` } })); + handleError(res, { status: 400, message: `Provider ${firstSegment} is not available.` }, currentConfig.MODEL_PROVIDER, null, req); return; } else if (firstSegment && !isValidProvider) { logger.info(`[Config] Ignoring invalid MODEL_PROVIDER in path segment: ${firstSegment}`); @@ -195,9 +193,8 @@ export function createRequestHandler(config, providerPoolManager) { return; } if (!authResult.authorized) { - // 没有认证插件授权,返回 401 - res.writeHead(401, { 'Content-Type': 'application/json' }); - res.end(JSON.stringify({ error: { message: 'Unauthorized: API key is invalid or missing.' } })); + // 没有认证插件授权,使用 handleError 返回 401 + handleError(res, { status: 401, message: 'Unauthorized: API key is invalid or missing.' }, currentConfig.MODEL_PROVIDER, null, req); return; } @@ -228,7 +225,7 @@ export function createRequestHandler(config, providerPoolManager) { return true; } catch (error) { logger.error(`[Server] count_tokens error: ${error.message}`); - handleError(res, { statusCode: 500, message: `Failed to count tokens: ${error.message}` }, currentConfig.MODEL_PROVIDER); + handleError(res, { status: 500, message: `Failed to count tokens: ${error.message}` }, currentConfig.MODEL_PROVIDER, null, req); return; } } @@ -254,10 +251,9 @@ export function createRequestHandler(config, providerPoolManager) { if (apiHandled) return; // Fallback for unmatched routes - res.writeHead(404, { 'Content-Type': 'application/json' }); - res.end(JSON.stringify({ error: { message: 'Not Found' } })); + handleError(res, { status: 404, message: 'Not Found' }, currentConfig.MODEL_PROVIDER, null, req); } catch (error) { - handleError(res, error, currentConfig.MODEL_PROVIDER); + handleError(res, error, currentConfig.MODEL_PROVIDER, null, req); } } finally { // Clear request context after request is complete diff --git a/src/providers/openai/codex-core.js b/src/providers/openai/codex-core.js index 6147767..88970be 100644 --- a/src/providers/openai/codex-core.js +++ b/src/providers/openai/codex-core.js @@ -14,7 +14,7 @@ import { getProviderModels } from '../provider-models.js'; const baseModels = getProviderModels(MODEL_PROVIDER.CODEX_API); const fastModels = baseModels.map(m => `${m}-fast`); const CODEX_MODELS = [...new Set([...baseModels, ...fastModels])]; -const CODEX_VERSION = '0.111.0'; +const CODEX_VERSION = '0.118.0'; /** * Codex API 服务类 @@ -319,8 +319,8 @@ export class CodexApiService { 'authorization': `Bearer ${this.accessToken}`, 'chatgpt-account-id': this.accountId, 'content-type': 'application/json', - 'user-agent': `codex_cli_rs/${CODEX_VERSION} (Windows 10.0.26100; x86_64) WindowsTerminal`, - 'originator': 'codex_cli_rs', + 'user-agent': `codex-tui/${CODEX_VERSION} (Windows 10.0.26100; x86_64) WindowsTerminal (codex-tui; ${CODEX_VERSION})`, + 'originator': 'codex-tui', 'host': 'chatgpt.com', 'Connection': 'Keep-Alive' }; @@ -365,6 +365,17 @@ export class CodexApiService { // 即使 originalRequestBody 中已经带了 model,这里也必须覆盖 cleanedBody.model = upstreamModel; + // 为所有 Codex 模型增加默认工具 + if (!cleanedBody.tools) { + cleanedBody.tools = []; + } + if (Array.isArray(cleanedBody.tools)) { + const hasWebSearch = cleanedBody.tools.some(t => t.type === 'web_search'); + if (!hasWebSearch) { + cleanedBody.tools.push({ type: 'web_search' }); + } + } + if (isFastModel) { logger.info(`[Codex] Detected -fast model: ${normalizedModel} -> ${upstreamModel}, service_tier: ${cleanedBody.service_tier || defaultServiceTier}`); } @@ -733,7 +744,7 @@ export class CodexApiService { try { const url = 'https://chatgpt.com/backend-api/wham/usage'; const headers = { - 'user-agent': `codex_cli_rs/${CODEX_VERSION} (Windows 10.0.26100; x86_64) WindowsTerminal`, + 'user-agent': `codex-tui/${CODEX_VERSION} (Windows 10.0.26100; x86_64) WindowsTerminal (codex-tui; ${CODEX_VERSION})`, 'authorization': `Bearer ${this.accessToken}`, 'chatgpt-account-id': this.accountId, 'accept': '*/*', diff --git a/src/providers/provider-pool-manager.js b/src/providers/provider-pool-manager.js index db68e9b..562fbc1 100644 --- a/src/providers/provider-pool-manager.js +++ b/src/providers/provider-pool-manager.js @@ -1301,12 +1301,21 @@ export class ProviderPoolManager { } // 如果硬编码的模型列表为空,或者该类型的提供商在号池中没有配置节点,尝试从服务获取 - if (models.length === 0) { + // 只有在非号池模式,或者号池中有节点时才尝试获取,避免无节点时读取全局默认配置 + if (models.length === 0 && (!this.providerStatus[providerType] || this.providerStatus[providerType].length > 0)) { try { // 确定使用的配置:优先使用号池中第一个节点的配置,否则使用全局配置 let targetConfig = this.globalConfig; - if (this.providerStatus[providerType] && this.providerStatus[providerType].length > 0) { + if (this.providerStatus[providerType] && this.providerStatus[providerType].length > 0) { targetConfig = this.providerStatus[providerType][0].config; + } else { + // 如果该提供商是属于号池类型的提供商(在 PROVIDER_MAPPINGS 中),且号池为空,则不应尝试读取全局配置 + const { PROVIDER_MAPPINGS } = await import('../utils/provider-utils.js'); + const isPoolable = PROVIDER_MAPPINGS.some(m => m.providerType === providerType); + if (isPoolable) { + this._log('debug', `Skipping model fetch for poolable provider ${providerType} with empty pool to avoid reading default config.`); + continue; + } } const tempConfig = { diff --git a/src/services/service-manager.js b/src/services/service-manager.js index 27ce3b7..315b460 100644 --- a/src/services/service-manager.js +++ b/src/services/service-manager.js @@ -411,8 +411,9 @@ export async function getApiService(config, requestedModel = null, options = {}) if (effectiveProvider === MODEL_PROVIDER.AUTO && !actualModelName) return null; let serviceConfig = config; - if (providerPoolManager && config.providerPools && config.providerPools[config.MODEL_PROVIDER]) { - // 如果有号池管理器,并且当前模型提供者类型有对应的号池,则从号池中选择一个提供者配置 + const isPoolable = PROVIDER_MAPPINGS.some(m => m.providerType === config.MODEL_PROVIDER); + if (providerPoolManager && ((config.providerPools && config.providerPools[config.MODEL_PROVIDER]) || isPoolable)) { + // 如果有号池管理器,并且当前模型提供者类型有对应的号池(或属于号池类型提供商),则从号池中选择一个提供者配置 // selectProvider 现在是异步的,使用链式锁确保并发安全 const selectedProviderConfig = await providerPoolManager.selectProvider(config.MODEL_PROVIDER, actualModelName, { ...options, skipUsageCount: true }); if (selectedProviderConfig) { @@ -458,7 +459,8 @@ export async function getApiServiceWithFallback(config, requestedModel = null, o let selectedUuid = null; let actualModel = actualModelName; - if (providerPoolManager && config.providerPools && config.providerPools[config.MODEL_PROVIDER]) { + const isPoolable = PROVIDER_MAPPINGS.some(m => m.providerType === config.MODEL_PROVIDER); + if (providerPoolManager && ((config.providerPools && config.providerPools[config.MODEL_PROVIDER]) || isPoolable)) { // selectProviderWithFallback 现在是异步的,使用链式锁确保并发安全 // 如果开启了并发限制,则使用 acquireSlot 进行选择和占位 const useAcquire = options.acquireSlot === true; @@ -496,7 +498,7 @@ export async function getApiServiceWithFallback(config, requestedModel = null, o serviceConfig.MODEL_PROVIDER = actualProviderType; } } else { - const errorMsg = `[API Service] No healthy provider found in pool (including fallback) for ${config.MODEL_PROVIDER}${actualModelName ? ` supporting model: ${actualModelName}` : ''}`; + const errorMsg = `[API Service] No healthy provider found in pool for ${config.MODEL_PROVIDER}${actualModelName ? ` supporting model: ${actualModelName}` : ''}`; logger.error(errorMsg); throw new Error(errorMsg); } diff --git a/src/ui-modules/update-api.js b/src/ui-modules/update-api.js index b112dc1..a4c657b 100644 --- a/src/ui-modules/update-api.js +++ b/src/ui-modules/update-api.js @@ -252,7 +252,7 @@ export async function checkForUpdates() { logger.info('[Update] Fetching remote tags...'); await execAsync('git fetch --tags'); } catch (error) { - logger.warn('[Update] Failed to fetch tags via git, falling back to GitHub API:', error.message); + logger.warn('[Update] Failed to fetch tags via git, falling back to GitHub API'); // 如果 git fetch 失败,回退到 GitHub API availableVersions = await getVersionsFromGitHub(10); latestTag = availableVersions.length > 0 ? availableVersions[0] : null; diff --git a/src/utils/common.js b/src/utils/common.js index e0375b0..e605d3f 100644 --- a/src/utils/common.js +++ b/src/utils/common.js @@ -275,11 +275,11 @@ export function isAuthorized(req, requestUrl, REQUIRED_API_KEY) { * @param {Object} responsePayload - The actual response payload (string for unary, object for stream chunks). * @param {boolean} isStream - Whether the response is a stream. */ -export async function handleUnifiedResponse(res, responsePayload, isStream) { +export async function handleUnifiedResponse(res, responsePayload, isStream, statusCode = 200) { if (isStream) { res.writeHead(200, { "Content-Type": "text/event-stream", "Cache-Control": "no-cache", "Connection": "keep-alive", "Transfer-Encoding": "chunked" }); } else { - res.writeHead(200, { 'Content-Type': 'application/json' }); + res.writeHead(statusCode, { 'Content-Type': 'application/json' }); } if (isStream) { @@ -783,7 +783,8 @@ export async function handleUnaryRequest(res, service, model, requestBody, fromP // 使用新方法创建符合 fromProvider 格式的错误响应 const errorResponse = createErrorResponse(error, fromProvider); - await handleUnifiedResponse(res, JSON.stringify(errorResponse), false); + const statusCode = error.status || error.code || (error.response && error.response.status) || 500; + await handleUnifiedResponse(res, JSON.stringify(errorResponse), false, statusCode); } finally { // 确保在请求结束或出错时释放插槽 if (providerPoolManager && pooluuid) { @@ -805,14 +806,14 @@ export async function handleUnaryRequest(res, service, model, requestBody, fromP * @param {string} pooluuid - The selected provider UUID. */ export async function handleModelListRequest(req, res, service, endpointType, CONFIG, providerPoolManager, pooluuid) { - try { - const clientProviderMap = { - [ENDPOINT_TYPE.OPENAI_MODEL_LIST]: MODEL_PROTOCOL_PREFIX.OPENAI, - [ENDPOINT_TYPE.GEMINI_MODEL_LIST]: MODEL_PROTOCOL_PREFIX.GEMINI, - }; + const clientProviderMap = { + [ENDPOINT_TYPE.OPENAI_MODEL_LIST]: MODEL_PROTOCOL_PREFIX.OPENAI, + [ENDPOINT_TYPE.GEMINI_MODEL_LIST]: MODEL_PROTOCOL_PREFIX.GEMINI, + }; - const fromProvider = clientProviderMap[endpointType]; - + const fromProvider = clientProviderMap[endpointType]; + + try { if (!fromProvider) { throw new Error(`Unsupported endpoint type for model list: ${endpointType}`); } @@ -901,7 +902,7 @@ export async function handleModelListRequest(req, res, service, endpointType, CO // uuid: pooluuid // }, error.message); // } - handleError(res, error, CONFIG.MODEL_PROVIDER); + handleError(res, error, CONFIG.MODEL_PROVIDER, fromProvider); } } @@ -1074,14 +1075,30 @@ export function extractPromptText(requestBody, provider) { return strategy.extractPromptText(requestBody); } -export function handleError(res, error, provider = null) { +export function handleError(res, error, provider = null, fromProvider = null, req = null) { const statusCode = error.response?.status || error.statusCode || error.status || error.code || 500; + + // 如果没有提供 fromProvider 但提供了 req,尝试从路径推断 + if (!fromProvider && req && req.url) { + if (req.url.includes('/v1/messages')) fromProvider = MODEL_PROTOCOL_PREFIX.CLAUDE; + else if (req.url.includes('/v1/chat/completions')) fromProvider = MODEL_PROTOCOL_PREFIX.OPENAI; + else if (req.url.includes('/v1beta/models')) fromProvider = MODEL_PROTOCOL_PREFIX.GEMINI; + } + + // 如果指定了客户端协议,则使用 createErrorResponse 创建符合该协议的错误响应 + if (fromProvider) { + const errorResponse = createErrorResponse(error, fromProvider); + if (!res.headersSent) { + res.writeHead(statusCode, { 'Content-Type': 'application/json' }); + } + res.end(JSON.stringify(errorResponse)); + return; + } + + const hasOriginalMessage = error.message && error.message.trim() !== ''; let errorMessage = error.message; let suggestions = []; - // 仅在没有传入错误信息时,才使用默认消息;否则只添加建议 - const hasOriginalMessage = error.message && error.message.trim() !== ''; - // 根据提供商获取适配的错误信息和建议 const providerSuggestions = _getProviderSpecificSuggestions(statusCode, provider);