fix: 修复号池提供商空节点时错误读取默认配置的问题

- 在 provider-pool-manager.js 中,修复当号池类型提供商节点为空时错误读取全局默认配置的问题
- 在 service-manager.js 中,统一号池提供商的判断逻辑,确保号池类型提供商即使未显式配置也能正确使用号池
- 更新 Codex API 版本至 0.118.0,调整 user-agent 并增加默认 web_search 工具
- 优化错误处理,统一使用 handleError 函数返回符合客户端协议的错误响应
- 修复更新检查中日志记录错误信息的问题
This commit is contained in:
hex2077 2026-04-09 14:05:14 +08:00
parent 373ad4ee3b
commit 1754b6ce4e
6 changed files with 73 additions and 38 deletions

View file

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

View file

@ -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': '*/*',

View file

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

View file

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

View file

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

View file

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