fix(providers): 统一错误日志格式并修复提供商模型引用

修复多个API服务中错误日志格式不一致的问题,统一使用error.message替代data字段
修正多个提供商模型引用方式,使用MODEL_PROVIDER常量替代硬编码字符串
更新Codex API请求头配置,修复流式响应处理逻辑
为iFlow API添加签名验证机制,增强请求安全性
添加GLM-5和MiniMax M2.5等新模型支持
修复400错误状态码检查逻辑,使用error.response?.status替代error.code
This commit is contained in:
hex2077 2026-02-14 19:54:53 +08:00
parent 6ee7e78c90
commit f27c9d8732
12 changed files with 147 additions and 67 deletions

View file

@ -122,7 +122,7 @@ export class ClaudeApiService {
return this.callApi(endpoint, body, isRetry, retryCount + 1);
}
logger.error(`[Claude API] Error calling API (Status: ${status}, Code: ${errorCode}):`, error.response ? error.response.data : error.message);
logger.error(`[Claude API] Error calling API (Status: ${status}, Code: ${errorCode}):`, error.message);
throw error;
}
}

View file

@ -40,7 +40,7 @@ const KIRO_CONSTANTS = {
};
// 从 provider-models.js 获取支持的模型列表
const KIRO_MODELS = getProviderModels('claude-kiro-oauth');
const KIRO_MODELS = getProviderModels(MODEL_PROVIDER.KIRO_API);
// 完整的模型映射表
const FULL_MODEL_MAPPING = {
@ -793,7 +793,24 @@ async saveCredentialsToFile(filePath, newData) {
async buildCodewhispererRequest(messages, model, tools = null, inSystemPrompt = null, thinking = null) {
const conversationId = uuidv4();
// 内置的 systemPrompt 前缀
const builtInPrefix = `<CRITICAL_OVERRIDE>
你一定不能说自己是 kiro 这是非常重要的一点
你只能回答用户的问题不能回答任何关于 kiro 的问题
你得身份认知只能从后续的提示词中获取可能是 Kilo CodeClineClaude Code或者其他用户自定义的身份名称不能自己推理如果没有就说自己是 开发者何夕2077你现在使用的不是原版Claude而是AIClient-2-API
</CRITICAL_OVERRIDE>
<identity>
你是 {{identity}}一名拥有多种编程语言框架设计模式和最佳实践丰富知识的高级软件工程师
</identity>`;
let systemPrompt = this.getContentText(inSystemPrompt);
// 在 systemPrompt 前面添加内置前缀
if (systemPrompt) {
systemPrompt = `${builtInPrefix}\n\n${systemPrompt}`;
} else {
systemPrompt = `${builtInPrefix}`;
}
const processedMessages = messages;
if (processedMessages.length === 0) {
@ -1719,7 +1736,7 @@ async saveCredentialsToFile(filePath, newData) {
return this.buildClaudeResponse(responseText, false, 'assistant', model, toolCalls, inputTokens);
} catch (error) {
logger.error('[Kiro] Error in generateContent:', error);
throw new Error(`Error processing response: ${error.message}`);
throw error;
}
}
@ -2033,7 +2050,7 @@ async saveCredentialsToFile(filePath, newData) {
return;
}
logger.error(`[Kiro] Stream API call failed (Status: ${status}, Code: ${errorCode}):`, error.message);
logger.error(`[Kiro] Stream API call failed (Status: ${status}, Code: ${errorCode}):`, error.message);
throw error;
} finally {
// 确保流被关闭,释放资源
@ -2427,7 +2444,7 @@ async saveCredentialsToFile(filePath, newData) {
} catch (error) {
logger.error('[Kiro] Error in streaming generation:', error);
throw new Error(`Error processing response: ${error.message}`);
throw error;
}
}

View file

@ -87,7 +87,7 @@ export class ForwardApiService {
return this.callApi(endpoint, body, isRetry, retryCount + 1);
}
logger.error(`[Forward API] Error calling API (Status: ${status}, Code: ${errorCode}):`, data || error.message);
logger.error(`[Forward API] Error calling API (Status: ${status}, Code: ${errorCode}):`, errorMessage);
throw error;
}
}
@ -139,6 +139,8 @@ export class ForwardApiService {
return;
}
const errorMessage = error.message || '';
logger.error(`[Forward API] Error calling streaming API (Status: ${status || errorCode}):`, errorMessage);
throw error;
}
}

View file

@ -55,7 +55,7 @@ const DEFAULT_THINKING_MIN = 1024;
const DEFAULT_THINKING_MAX = 100000;
// 获取 Antigravity 模型列表
const ANTIGRAVITY_MODELS = getProviderModels('gemini-antigravity');
const ANTIGRAVITY_MODELS = getProviderModels(MODEL_PROVIDER.ANTIGRAVITY);
// 模型别名映射 - 别名 -> 真实模型名
const MODEL_ALIAS_MAP = {
@ -1107,7 +1107,7 @@ export class AntigravityApiService {
// 检查是否为可重试的网络错误
const isNetworkError = isRetryableNetworkError(error);
logger.error(`[Antigravity API] Error calling ${method} on ${baseURL}:`, status, error.message);
logger.error(`[Antigravity API] Error calling (Status: ${status}, Code: ${errorCode}):`, error.message);
if ((status === 400 || status === 401) && !isRetry) {
logger.info('[Antigravity API] Received 401/400. Triggering background refresh via PoolManager...');
@ -1209,7 +1209,7 @@ export class AntigravityApiService {
// 检查是否为可重试的网络错误
const isNetworkError = isRetryableNetworkError(error);
logger.error(`[Antigravity API] Error during stream ${method} on ${baseURL}:`, status, error.message);
logger.error(`[Antigravity API] Error during stream (Status: ${status}, Code: ${errorCode}):`, error.message);
if ((status === 400 || status === 401) && !isRetry) {
logger.info('[Antigravity API] Received 401/400 during stream. Triggering background refresh via PoolManager...');

View file

@ -36,7 +36,7 @@ const DEFAULT_CODE_ASSIST_ENDPOINT = 'https://cloudcode-pa.googleapis.com';
const DEFAULT_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 = getProviderModels('gemini-cli-oauth');
const GEMINI_MODELS = getProviderModels(MODEL_PROVIDER.GEMINI_CLI);
const ANTI_TRUNCATION_MODELS = GEMINI_MODELS.map(model => `anti-${model}`);
function is_anti_truncation_model(model) {
@ -489,7 +489,7 @@ export class GeminiApiService {
// 检查是否为可重试的网络错误
const isNetworkError = isRetryableNetworkError(error);
logger.error(`[Gemini API] Error calling ${method}:`, status, error.message);
logger.error(`[Gemini API] Error calling (Status: ${status}, Code: ${errorCode}):`, errorMessage);
// Handle 401 (Unauthorized) - refresh auth and retry once
if ((status === 400 || status === 401) && !isRetry) {
@ -568,7 +568,7 @@ export class GeminiApiService {
// 检查是否为可重试的网络错误
const isNetworkError = isRetryableNetworkError(error);
logger.error(`[Gemini API] Error during stream ${method}:`, status, error.message);
logger.error(`[Gemini API] Error during stream (Status: ${status}, Code: ${errorCode}):`, errorMessage);
// Handle 401 (Unauthorized) - refresh auth and retry once
if ((status === 400 || status === 401) && !isRetry) {

View file

@ -148,11 +148,12 @@ export class CodexApiService {
const url = `${this.baseUrl}/responses`;
const body = this.prepareRequestBody(model, requestBody, true);
const headers = this.buildHeaders(body.prompt_cache_key);
const headers = this.buildHeaders(body.prompt_cache_key, true);
try {
const config = {
headers,
responseType: 'text', // 确保以文本形式接收 SSE 流
timeout: 120000 // 2 分钟超时
};
@ -184,8 +185,10 @@ export class CodexApiService {
error.shouldSwitchCredential = true;
error.skipErrorCount = true;
throw error;
} else {
logger.error(`[Codex] Error calling non-stream API (Status: ${error.response?.status}, Code: ${error.code || 'N/A'}):`, error.message);
throw error;
}
throw error;
}
}
@ -216,7 +219,7 @@ export class CodexApiService {
const url = `${this.baseUrl}/responses`;
const body = this.prepareRequestBody(model, requestBody, true);
const headers = this.buildHeaders(body.prompt_cache_key);
const headers = this.buildHeaders(body.prompt_cache_key, true);
try {
const config = {
@ -254,6 +257,7 @@ export class CodexApiService {
error.skipErrorCount = true;
throw error;
} else {
logger.error(`[Codex] Error calling streaming API (Status: ${error.response?.status}, Code: ${error.code || 'N/A'}):`, error.message);
throw error;
}
}
@ -262,21 +266,34 @@ export class CodexApiService {
/**
* 构建请求头
*/
buildHeaders(cacheId) {
return {
'version': '0.98.0',
buildHeaders(cacheId, stream = true) {
const headers = {
'version': '0.101.0',
'x-codex-beta-features': 'powershell_utf8',
'x-oai-web-search-eligible': 'true',
'session_id': cacheId,
'accept': 'text/event-stream',
'authorization': `Bearer ${this.accessToken}`,
'chatgpt-account-id': this.accountId,
'content-type': 'application/json',
'user-agent': 'codex_cli_rs/0.89.0 (Windows 10.0.26100; x86_64) WindowsTerminal',
'user-agent': 'codex_cli_rs/0.101.0 (Windows 10.0.26100; x86_64) WindowsTerminal',
'originator': 'codex_cli_rs',
'host': 'chatgpt.com',
'Connection': 'close'
'Connection': 'Keep-Alive'
};
// 设置 Conversation_id 和 Session_id
if (cacheId) {
headers['Conversation_id'] = cacheId;
headers['Session_id'] = cacheId;
}
// 根据是否流式设置 Accept 头
if (stream) {
headers['accept'] = 'text/event-stream';
} else {
headers['accept'] = 'application/json';
}
return headers;
}
/**
@ -438,22 +455,32 @@ export class CodexApiService {
* 解析非流式响应
*/
parseNonStreamResponse(data) {
// 确保 data 是字符串
const responseText = typeof data === 'string' ? data : String(data);
// 从 SSE 流中提取 response.completed 事件
const lines = data.split('\n');
const lines = responseText.split('\n');
for (const line of lines) {
if (line.startsWith('data: ')) {
const jsonData = line.slice(6).trim();
if (!jsonData || jsonData === '[DONE]') {
continue;
}
try {
const parsed = JSON.parse(jsonData);
if (parsed.type === 'response.completed') {
return parsed;
}
} catch (e) {
// 继续解析
// 继续解析下一行
logger.debug('[Codex] Failed to parse SSE line:', e.message);
}
}
}
throw new Error('No completed response found in Codex response');
// 如果没有找到 response.completed抛出错误
logger.error('[Codex] No completed response found in Codex response');
throw new Error('stream error: stream disconnected before completion: stream closed before response.completed');
}
/**
@ -472,7 +499,8 @@ export class CodexApiService {
{ id: 'gpt-5.1-codex-max', object: 'model', created: Math.floor(Date.now() / 1000), owned_by: 'openai' },
{ id: 'gpt-5.2', object: 'model', created: Math.floor(Date.now() / 1000), owned_by: 'openai' },
{ id: 'gpt-5.2-codex', object: 'model', created: Math.floor(Date.now() / 1000), owned_by: 'openai' },
{ id: 'gpt-5.3-codex', object: 'model', created: Math.floor(Date.now() / 1000), owned_by: 'openai' }
{ id: 'gpt-5.3-codex', object: 'model', created: Math.floor(Date.now() / 1000), owned_by: 'openai' },
{ id: 'gpt-5.3-codex-spark', object: 'model', created: Math.floor(Date.now() / 1000), owned_by: 'openai' }
]
};
}

View file

@ -23,9 +23,11 @@ import * as https from 'https';
import { promises as fs } from 'fs';
import * as path from 'path';
import * as os from 'os';
import * as crypto from 'crypto';
import { configureAxiosProxy } from '../../utils/proxy-utils.js';
import { isRetryableNetworkError, MODEL_PROVIDER, formatExpiryLog } from '../../utils/common.js';
import { getProviderPoolManager } from '../../services/service-manager.js';
import { getProviderModels } from '../provider-models.js';
// iFlow API 端点
const IFLOW_API_BASE_URL = 'https://apis.iflow.cn/v1';
@ -36,32 +38,10 @@ const IFLOW_OAUTH_CLIENT_ID = '10009311001';
const IFLOW_OAUTH_CLIENT_SECRET = '4Z3YjXycVsQvyGF1etiNlIBB4RsqSDtW';
// 默认模型列表
const IFLOW_MODELS = [
// iFlow 特有模型
'iflow-rome-30ba3b',
// Qwen 模型
'qwen3-coder-plus',
'qwen3-max',
'qwen3-vl-plus',
'qwen3-max-preview',
'qwen3-32b',
'qwen3-235b-a22b-thinking-2507',
'qwen3-235b-a22b-instruct',
'qwen3-235b',
// Kimi 模型
'kimi-k2-0905',
'kimi-k2',
// GLM 模型
'glm-4.6',
'glm-4.7',
// DeepSeek 模型
'deepseek-v3.2',
'deepseek-r1',
'deepseek-v3'
];
const IFLOW_MODELS = getProviderModels(MODEL_PROVIDER.IFLOW_API);
// 支持 thinking 的模型前缀
const THINKING_MODEL_PREFIXES = ['glm-4', 'qwen3-235b-a22b-thinking', 'deepseek-r1'];
const THINKING_MODEL_PREFIXES = ['glm-', 'qwen3-235b-a22b-thinking', 'deepseek-r1'];
// ==================== Token 管理 ====================
@ -294,6 +274,33 @@ async function fetchUserInfo(accessToken, axiosInstance = null) {
// ==================== 请求处理工具函数 ====================
/**
* 生成 UUID v4
* @returns {string} - UUID 字符串
*/
function generateUUID() {
return crypto.randomUUID();
}
/**
* 创建 iFlow 签名
* 签名格式: HMAC-SHA256(userAgent:sessionId:timestamp, apiKey)
* @param {string} userAgent - User-Agent
* @param {string} sessionID - Session ID
* @param {number} timestamp - 时间戳毫秒
* @param {string} apiKey - API Key
* @returns {string} - 十六进制签名
*/
function createIFlowSignature(userAgent, sessionID, timestamp, apiKey) {
if (!apiKey) {
return '';
}
const payload = `${userAgent}:${sessionID}:${timestamp}`;
const hmac = crypto.createHmac('sha256', apiKey);
hmac.update(payload);
return hmac.digest('hex');
}
/**
* 检查模型是否支持 thinking 配置
* @param {string} model - 模型名称
@ -359,6 +366,9 @@ function applyIFlowThinkingConfig(body, model) {
* 保留消息历史中的 reasoning_content
* 对于支持 thinking 的模型保留 assistant 消息中的 reasoning_content
*
* 对于 GLM-4.6/4.7 MiniMax M2/M2.1建议在消息历史中包含完整的 assistant
* 响应包括 reasoning_content以保持更好的上下文连续性
*
* @param {Object} body - 请求体
* @param {string} model - 模型名称
* @returns {Object} - 处理后的请求体
@ -368,11 +378,13 @@ function preserveReasoningContentInMessages(body, model) {
const lowerModel = model.toLowerCase();
// 只对支持 thinking 的模型应用
// 只对支持 thinking 且需要历史保留的模型应用
const needsPreservation = lowerModel.startsWith('glm-4') ||
lowerModel.includes('thinking') ||
lowerModel.startsWith('deepseek-r1');
if (!needsPreservation) return body;
lowerModel.startsWith('minimax-m2');
if (!needsPreservation) {
return body;
}
const messages = body.messages;
if (!Array.isArray(messages)) return body;
@ -382,8 +394,10 @@ function preserveReasoningContentInMessages(body, model) {
msg.role === 'assistant' && msg.reasoning_content && msg.reasoning_content !== ''
);
// 如果 reasoning content 已经存在,说明消息格式正确
// 客户端已经正确地在历史中保留了推理内容
if (hasReasoningContent) {
logger.info(`[iFlow] reasoning_content found in message history for ${model}`);
logger.debug(`[iFlow] reasoning_content found in message history for ${model}`);
}
return body;
@ -735,12 +749,28 @@ export class IFlowApiService {
* @returns {Object} - 请求头
*/
_getHeaders(stream = false) {
// 生成 session-id
const sessionID = 'session-' + generateUUID();
// 生成时间戳(毫秒)
const timestamp = Date.now();
// 生成签名
const signature = createIFlowSignature(IFLOW_USER_AGENT, sessionID, timestamp, this.apiKey);
const headers = {
'Content-Type': 'application/json',
'Authorization': `Bearer ${this.apiKey}`,
'User-Agent': IFLOW_USER_AGENT,
'session-id': sessionID,
'x-iflow-timestamp': timestamp.toString(),
};
// 只有在签名生成成功时才添加
if (signature) {
headers['x-iflow-signature'] = signature;
}
if (stream) {
headers['Accept'] = 'text/event-stream';
} else {
@ -824,7 +854,7 @@ export class IFlowApiService {
return this.callApi(endpoint, body, model, isRetry, retryCount + 1);
}
logger.error(`[iFlow] Error calling API (Status: ${status}, Code: ${errorCode}):`, data || error.message);
logger.error(`[iFlow] Error calling API (Status: ${status}, Code: ${errorCode}):`, errorMessage);
throw error;
}
}
@ -985,7 +1015,7 @@ export class IFlowApiService {
return;
}
logger.error(`[iFlow] Error calling streaming API (Status: ${status}, Code: ${errorCode}):`, data || error.message);
logger.error(`[iFlow] Error calling streaming API (Status: ${status}, Code: ${errorCode}):`, errorMessage);
throw error;
}
}
@ -1055,7 +1085,7 @@ export class IFlowApiService {
}
// 需要手动添加的模型列表
const manualModels = ['glm-4.7', 'kimi-k2.5', 'minimax-m2.1'];
const manualModels = ['glm-4.7', 'glm-5', 'kimi-k2.5', 'minimax-m2.1', 'minimax-m2.5'];
try {
const response = await this.axiosInstance.get('/models', {

View file

@ -98,7 +98,7 @@ export class OpenAIApiService {
return this.callApi(endpoint, body, isRetry, retryCount + 1);
}
logger.error(`[OpenAI API] Error calling API (Status: ${status}, Code: ${errorCode}):`, data || error.message);
logger.error(`[OpenAI API] Error calling API (Status: ${status}, Code: ${errorCode}):`, errorMessage);
throw error;
}
}
@ -183,7 +183,7 @@ export class OpenAIApiService {
return;
}
logger.error(`[OpenAI API] Error calling streaming API (Status: ${status}, Code: ${errorCode}):`, data || error.message);
logger.error(`[OpenAI API] Error calling streaming API (Status: ${status}, Code: ${errorCode}):`, errorMessage);
throw error;
}
}

View file

@ -82,7 +82,7 @@ export class OpenAIResponsesApiService {
return this.callApi(endpoint, body, isRetry, retryCount + 1);
}
logger.error(`Error calling OpenAI Responses API (Status: ${status}):`, data || error.message);
logger.error(`Error calling OpenAI Responses API (Status: ${status}):`, error.message);
throw error;
}
}
@ -151,7 +151,7 @@ export class OpenAIResponsesApiService {
return;
}
logger.error(`Error calling OpenAI Responses streaming API (Status: ${status}):`, data || error.message);
logger.error(`Error calling OpenAI Responses streaming API (Status: ${status}):`, error.message);
throw error;
}
}

View file

@ -19,7 +19,7 @@ import { getProviderPoolManager } from '../../services/service-manager.js';
const QWEN_DIR = '.qwen';
const QWEN_CREDENTIAL_FILENAME = 'oauth_creds.json';
// 从 provider-models.js 获取支持的模型列表
const QWEN_MODELS = getProviderModels('openai-qwen-oauth');
const QWEN_MODELS = getProviderModels(MODEL_PROVIDER.QWEN_API);
const QWEN_MODEL_LIST = QWEN_MODELS.map(id => ({
id: id,
name: id.split('-').map(word => word.charAt(0).toUpperCase() + word.slice(1)).join(' ')
@ -645,7 +645,7 @@ export class QwenApiService {
return this.callApiWithAuthAndRetry(endpoint, body, isStream, retryCount + 1);
}
logger.error(`[QwenApiService] Error calling API (Status: ${status}, Code: ${errorCode}):`, data);
logger.error(`[QwenApiService] Error calling API (Status: ${status}, Code: ${errorCode}):`, errorMessage);
throw error;
}
}

View file

@ -66,8 +66,10 @@ export const PROVIDER_MODELS = {
'deepseek-v3',
// 手动定义
'glm-4.7',
'glm-5',
'kimi-k2.5',
'minimax-m2.1',
'minimax-m2.5',
],
'openai-codex-oauth': [
'gpt-5',
@ -79,7 +81,8 @@ export const PROVIDER_MODELS = {
'gpt-5.1-codex-max',
'gpt-5.2',
'gpt-5.2-codex',
'gpt-5.3-codex'
'gpt-5.3-codex',
'gpt-5.3-codex-spark'
],
'forward-api': []
};

View file

@ -494,7 +494,7 @@ export async function handleStreamRequest(res, service, model, requestBody, from
// 如果底层未标记,且不跳过错误计数,则在此处标记
if (!credentialMarkedUnhealthy && !skipErrorCount && providerPoolManager && pooluuid) {
// 400 报错码通常是请求参数问题,不记录为提供商错误
if (error.code === 400) {
if (error.response?.status === 400) {
logger.info(`[Provider Pool] Skipping unhealthy marking for ${toProvider} (${pooluuid}) due to status 400 (client error)`);
} else {
logger.info(`[Provider Pool] Marking ${toProvider} as unhealthy due to stream error (status: ${status || 'unknown'})`);
@ -692,7 +692,7 @@ export async function handleUnaryRequest(res, service, model, requestBody, fromP
// 如果底层未标记,且不跳过错误计数,则在此处标记
if (!credentialMarkedUnhealthy && !skipErrorCount && providerPoolManager && pooluuid) {
// 400 报错码通常是请求参数问题,不记录为提供商错误
if (error.code === 400) {
if (error.response?.status === 400) {
logger.info(`[Provider Pool] Skipping unhealthy marking for ${toProvider} (${pooluuid}) due to status 400 (client error)`);
} else {
logger.info(`[Provider Pool] Marking ${toProvider} as unhealthy due to unary error (status: ${status || 'unknown'})`);