fix: 修复号池提供商空节点时错误读取默认配置的问题
- 在 provider-pool-manager.js 中,修复当号池类型提供商节点为空时错误读取全局默认配置的问题 - 在 service-manager.js 中,统一号池提供商的判断逻辑,确保号池类型提供商即使未显式配置也能正确使用号池 - 更新 Codex API 版本至 0.118.0,调整 user-agent 并增加默认 web_search 工具 - 优化错误处理,统一使用 handleError 函数返回符合客户端协议的错误响应 - 修复更新检查中日志记录错误信息的问题
This commit is contained in:
parent
373ad4ee3b
commit
1754b6ce4e
6 changed files with 73 additions and 38 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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': '*/*',
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue