feat: 添加统一错误响应生成功能
refactor(service-manager): 移除NoAvailableProviderError和严格池检查 feat(converters): 增强token统计功能,支持缓存和推理token详情 refactor(claude-kiro): 改进JSON解析和工具调用处理 feat(common): 添加统一错误响应生成功能
This commit is contained in:
parent
0e9d0912cd
commit
bc3725e901
8 changed files with 463 additions and 100 deletions
32
package-lock.json
generated
32
package-lock.json
generated
|
|
@ -5,6 +5,7 @@
|
|||
"packages": {
|
||||
"": {
|
||||
"dependencies": {
|
||||
"@anthropic-ai/tokenizer": "^0.0.4",
|
||||
"axios": "^1.10.0",
|
||||
"deepmerge": "^4.3.1",
|
||||
"dotenv": "^16.4.5",
|
||||
|
|
@ -39,6 +40,31 @@
|
|||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@anthropic-ai/tokenizer": {
|
||||
"version": "0.0.4",
|
||||
"resolved": "https://registry.npmmirror.com/@anthropic-ai/tokenizer/-/tokenizer-0.0.4.tgz",
|
||||
"integrity": "sha512-EHRKbxlxlc8W4KCBEseByJ7YwyYCmgu9OyN59H9+IYIGPoKv8tXyQXinkeGDI+cI8Tiuz9wk2jZb/kK7AyvL7g==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@types/node": "^18.11.18",
|
||||
"tiktoken": "^1.0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/@anthropic-ai/tokenizer/node_modules/@types/node": {
|
||||
"version": "18.19.130",
|
||||
"resolved": "https://registry.npmmirror.com/@types/node/-/node-18.19.130.tgz",
|
||||
"integrity": "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~5.26.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@anthropic-ai/tokenizer/node_modules/undici-types": {
|
||||
"version": "5.26.5",
|
||||
"resolved": "https://registry.npmmirror.com/undici-types/-/undici-types-5.26.5.tgz",
|
||||
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@babel/code-frame": {
|
||||
"version": "7.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz",
|
||||
|
|
@ -6217,6 +6243,12 @@
|
|||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/tiktoken": {
|
||||
"version": "1.0.22",
|
||||
"resolved": "https://registry.npmmirror.com/tiktoken/-/tiktoken-1.0.22.tgz",
|
||||
"integrity": "sha512-PKvy1rVF1RibfF3JlXBSP0Jrcw2uq3yXdgcEXtKTYn3QJ/cBRBHDnrJ5jHky+MENZ6DIPwNUGWpkVx+7joCpNA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tmpl": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz",
|
||||
|
|
|
|||
|
|
@ -76,9 +76,18 @@ async function getMacAddressSha256() {
|
|||
return sha256Hash;
|
||||
}
|
||||
|
||||
// Helper functions for tool calls
|
||||
function findMatchingBracket(text, startPos) {
|
||||
if (!text || startPos >= text.length || text[startPos] !== '[') {
|
||||
// Helper functions for tool calls and JSON parsing
|
||||
|
||||
/**
|
||||
* 通用的括号匹配函数 - 支持多种括号类型
|
||||
* @param {string} text - 要搜索的文本
|
||||
* @param {number} startPos - 起始位置
|
||||
* @param {string} openChar - 开括号字符 (默认 '[')
|
||||
* @param {string} closeChar - 闭括号字符 (默认 ']')
|
||||
* @returns {number} 匹配的闭括号位置,未找到返回 -1
|
||||
*/
|
||||
function findMatchingBracket(text, startPos, openChar = '[', closeChar = ']') {
|
||||
if (!text || startPos >= text.length || text[startPos] !== openChar) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
|
|
@ -105,9 +114,9 @@ function findMatchingBracket(text, startPos) {
|
|||
}
|
||||
|
||||
if (!inString) {
|
||||
if (char === '[') {
|
||||
if (char === openChar) {
|
||||
bracketCount++;
|
||||
} else if (char === ']') {
|
||||
} else if (char === closeChar) {
|
||||
bracketCount--;
|
||||
if (bracketCount === 0) {
|
||||
return i;
|
||||
|
|
@ -118,6 +127,28 @@ function findMatchingBracket(text, startPos) {
|
|||
return -1;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 尝试修复常见的 JSON 格式问题
|
||||
* @param {string} jsonStr - 可能有问题的 JSON 字符串
|
||||
* @returns {string} 修复后的 JSON 字符串
|
||||
*/
|
||||
function repairJson(jsonStr) {
|
||||
let repaired = jsonStr;
|
||||
// 移除尾部逗号
|
||||
repaired = repaired.replace(/,\s*([}\]])/g, '$1');
|
||||
// 为未引用的键添加引号
|
||||
repaired = repaired.replace(/([{,]\s*)([a-zA-Z0-9_]+?)\s*:/g, '$1"$2":');
|
||||
// 确保字符串值被正确引用
|
||||
repaired = repaired.replace(/:\s*([a-zA-Z0-9_]+)(?=[,\}\]])/g, ':"$1"');
|
||||
return repaired;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析单个工具调用文本
|
||||
* @param {string} toolCallText - 工具调用文本
|
||||
* @returns {Object|null} 解析后的工具调用对象或 null
|
||||
*/
|
||||
function parseSingleToolCall(toolCallText) {
|
||||
const namePattern = /\[Called\s+(\w+)\s+with\s+args:/i;
|
||||
const nameMatch = toolCallText.match(namePattern);
|
||||
|
|
@ -144,16 +175,7 @@ function parseSingleToolCall(toolCallText) {
|
|||
const jsonCandidate = toolCallText.substring(argsStart, argsEnd).trim();
|
||||
|
||||
try {
|
||||
// Simple repair for common issues like trailing commas or unquoted keys
|
||||
let repairedJson = jsonCandidate;
|
||||
// Remove trailing comma before closing brace/bracket
|
||||
repairedJson = repairedJson.replace(/,\s*([}\]])/g, '$1');
|
||||
// Add quotes to unquoted keys (basic attempt)
|
||||
repairedJson = repairedJson.replace(/([{,]\s*)([a-zA-Z0-9_]+?)\s*:/g, '$1"$2":');
|
||||
// Ensure string values are properly quoted if they contain special characters and are not already quoted
|
||||
repairedJson = repairedJson.replace(/:\s*([a-zA-Z0-9_]+)(?=[,\}\]])/g, ':"$1"');
|
||||
|
||||
|
||||
const repairedJson = repairJson(jsonCandidate);
|
||||
const argumentsObj = JSON.parse(repairedJson);
|
||||
|
||||
if (typeof argumentsObj !== 'object' || argumentsObj === null) {
|
||||
|
|
@ -934,6 +956,9 @@ async initializeAuth(forceRefresh = false) {
|
|||
}
|
||||
|
||||
|
||||
/**
|
||||
* 调用 API 并处理错误重试
|
||||
*/
|
||||
async callApi(method, model, body, isRetry = false, retryCount = 0) {
|
||||
if (!this.isInitialized) await this.initialize();
|
||||
const maxRetries = this.config.REQUEST_MAX_RETRIES || 3;
|
||||
|
|
@ -963,13 +988,13 @@ async initializeAuth(forceRefresh = false) {
|
|||
throw refreshError;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Handle 429 (Too Many Requests) with exponential backoff
|
||||
if (error.response?.status === 429 && retryCount < maxRetries) {
|
||||
const delay = baseDelay * Math.pow(2, retryCount);
|
||||
console.log(`[Kiro] Received 429 (Too Many Requests). Retrying in ${delay}ms... (attempt ${retryCount + 1}/${maxRetries})`);
|
||||
await new Promise(resolve => setTimeout(resolve, delay));
|
||||
return this.callApi(method, model, body, isRetry, retryCount + 1);
|
||||
await new Promise(resolve => setTimeout(resolve, delay));
|
||||
return this.callApi(method, model, body, isRetry, retryCount + 1);
|
||||
}
|
||||
|
||||
// Handle other retryable errors (5xx server errors)
|
||||
|
|
@ -1135,7 +1160,8 @@ async initializeAuth(forceRefresh = false) {
|
|||
if (parsed.content !== undefined && !parsed.followupPrompt) {
|
||||
// 处理转义字符
|
||||
let decodedContent = parsed.content;
|
||||
decodedContent = decodedContent.replace(/(?<!\\)\\n/g, '\n');
|
||||
// 无须处理转义的换行符,原来要处理是因为智能体返回的 content 需要通过换行符切割不同的json
|
||||
// decodedContent = decodedContent.replace(/(?<!\\)\\n/g, '\n');
|
||||
events.push({ type: 'content', data: decodedContent });
|
||||
}
|
||||
// 处理结构化工具调用事件 - 开始事件(包含 name 和 toolUseId)
|
||||
|
|
@ -1258,8 +1284,8 @@ async initializeAuth(forceRefresh = false) {
|
|||
if (error.response?.status === 429 && retryCount < maxRetries) {
|
||||
const delay = baseDelay * Math.pow(2, retryCount);
|
||||
console.log(`[Kiro] Received 429 in stream. Retrying in ${delay}ms...`);
|
||||
await new Promise(resolve => setTimeout(resolve, delay));
|
||||
yield* this.streamApiReal(method, model, body, isRetry, retryCount + 1);
|
||||
await new Promise(resolve => setTimeout(resolve, delay));
|
||||
yield* this.streamApiReal(method, model, body, isRetry, retryCount + 1);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
|||
222
src/common.js
222
src/common.js
|
|
@ -243,12 +243,11 @@ export async function handleStreamRequest(res, service, model, requestBody, from
|
|||
});
|
||||
}
|
||||
|
||||
if (!res.writableEnded) {
|
||||
const errorPayload = { error: { message: "An error occurred during streaming.", details: error.message } };
|
||||
res.end(JSON.stringify(errorPayload));
|
||||
responseClosed = true;
|
||||
}
|
||||
|
||||
// 使用新方法创建符合 fromProvider 格式的流式错误响应
|
||||
const errorPayload = createStreamErrorResponse(error, fromProvider);
|
||||
res.write(errorPayload);
|
||||
res.end();
|
||||
responseClosed = true;
|
||||
} finally {
|
||||
if (!responseClosed) {
|
||||
res.end();
|
||||
|
|
@ -259,6 +258,7 @@ export async function handleStreamRequest(res, service, model, requestBody, from
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
export async function handleUnaryRequest(res, service, model, requestBody, fromProvider, toProvider, PROMPT_LOG_MODE, PROMPT_LOG_FILENAME, providerPoolManager, pooluuid) {
|
||||
try{
|
||||
// The service returns the response in its native format (toProvider).
|
||||
|
|
@ -288,14 +288,8 @@ export async function handleUnaryRequest(res, service, model, requestBody, fromP
|
|||
});
|
||||
}
|
||||
|
||||
// 返回错误响应给客户端
|
||||
const errorResponse = {
|
||||
error: {
|
||||
message: error.message || "An error occurred during processing.",
|
||||
code: error.status || 500,
|
||||
details: error.stack
|
||||
}
|
||||
};
|
||||
// 使用新方法创建符合 fromProvider 格式的错误响应
|
||||
const errorResponse = createErrorResponse(error, fromProvider);
|
||||
await handleUnifiedResponse(res, JSON.stringify(errorResponse), false);
|
||||
}
|
||||
}
|
||||
|
|
@ -402,24 +396,9 @@ export async function handleContentGenerationRequest(req, res, service, endpoint
|
|||
|
||||
// 2.5. 如果使用了提供商池,根据模型重新选择提供商
|
||||
if (providerPoolManager && CONFIG.providerPools && CONFIG.providerPools[CONFIG.MODEL_PROVIDER]) {
|
||||
const { getApiService, NoAvailableProviderError } = await import('./service-manager.js');
|
||||
try {
|
||||
service = await getApiService(CONFIG, model);
|
||||
console.log(`[Content Generation] Re-selected service adapter based on model: ${model}`);
|
||||
} catch (error) {
|
||||
if (error instanceof NoAvailableProviderError) {
|
||||
// 号池无可用账号,返回 400 错误
|
||||
res.writeHead(400, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({
|
||||
error: {
|
||||
type: 'no_available_provider',
|
||||
message: error.message
|
||||
}
|
||||
}));
|
||||
return;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
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.
|
||||
|
|
@ -619,3 +598,182 @@ export function getMD5Hash(obj) {
|
|||
const jsonString = JSON.stringify(obj);
|
||||
return crypto.createHash('md5').update(jsonString).digest('hex');
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 创建符合 fromProvider 格式的错误响应(非流式)
|
||||
* @param {Error} error - 错误对象
|
||||
* @param {string} fromProvider - 客户端期望的提供商格式
|
||||
* @returns {Object} 格式化的错误响应对象
|
||||
*/
|
||||
function createErrorResponse(error, fromProvider) {
|
||||
const protocolPrefix = getProtocolPrefix(fromProvider);
|
||||
const statusCode = error.status || error.code || 500;
|
||||
const errorMessage = error.message || "An error occurred during processing.";
|
||||
|
||||
// 根据 HTTP 状态码映射错误类型
|
||||
const getErrorType = (code) => {
|
||||
if (code === 401) return 'authentication_error';
|
||||
if (code === 403) return 'permission_error';
|
||||
if (code === 429) return 'rate_limit_error';
|
||||
if (code >= 500) return 'server_error';
|
||||
return 'invalid_request_error';
|
||||
};
|
||||
|
||||
// 根据 HTTP 状态码映射 Gemini 的 status
|
||||
const getGeminiStatus = (code) => {
|
||||
if (code === 400) return 'INVALID_ARGUMENT';
|
||||
if (code === 401) return 'UNAUTHENTICATED';
|
||||
if (code === 403) return 'PERMISSION_DENIED';
|
||||
if (code === 404) return 'NOT_FOUND';
|
||||
if (code === 429) return 'RESOURCE_EXHAUSTED';
|
||||
if (code >= 500) return 'INTERNAL';
|
||||
return 'UNKNOWN';
|
||||
};
|
||||
|
||||
switch (protocolPrefix) {
|
||||
case MODEL_PROTOCOL_PREFIX.OPENAI:
|
||||
// OpenAI 非流式错误格式
|
||||
return {
|
||||
error: {
|
||||
message: errorMessage,
|
||||
type: getErrorType(statusCode),
|
||||
code: getErrorType(statusCode) // OpenAI 使用 code 字段作为核心判断
|
||||
}
|
||||
};
|
||||
|
||||
case MODEL_PROTOCOL_PREFIX.OPENAI_RESPONSES:
|
||||
// OpenAI Responses API 非流式错误格式
|
||||
return {
|
||||
error: {
|
||||
type: getErrorType(statusCode),
|
||||
message: errorMessage,
|
||||
code: getErrorType(statusCode)
|
||||
}
|
||||
};
|
||||
|
||||
case MODEL_PROTOCOL_PREFIX.CLAUDE:
|
||||
// Claude 非流式错误格式(外层有 type 标记)
|
||||
return {
|
||||
type: "error", // 核心区分标记
|
||||
error: {
|
||||
type: getErrorType(statusCode), // Claude 使用 error.type 作为核心判断
|
||||
message: errorMessage
|
||||
}
|
||||
};
|
||||
|
||||
case MODEL_PROTOCOL_PREFIX.GEMINI:
|
||||
// Gemini 非流式错误格式(遵循 Google Cloud 标准)
|
||||
return {
|
||||
error: {
|
||||
code: statusCode,
|
||||
message: errorMessage,
|
||||
status: getGeminiStatus(statusCode) // Gemini 使用 status 作为核心判断
|
||||
}
|
||||
};
|
||||
|
||||
default:
|
||||
// 默认使用 OpenAI 格式
|
||||
return {
|
||||
error: {
|
||||
message: errorMessage,
|
||||
type: getErrorType(statusCode),
|
||||
code: getErrorType(statusCode)
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建符合 fromProvider 格式的流式错误响应
|
||||
* @param {Error} error - 错误对象
|
||||
* @param {string} fromProvider - 客户端期望的提供商格式
|
||||
* @returns {string} 格式化的流式错误响应字符串
|
||||
*/
|
||||
function createStreamErrorResponse(error, fromProvider) {
|
||||
const protocolPrefix = getProtocolPrefix(fromProvider);
|
||||
const statusCode = error.status || error.code || 500;
|
||||
const errorMessage = error.message || "An error occurred during streaming.";
|
||||
|
||||
// 根据 HTTP 状态码映射错误类型
|
||||
const getErrorType = (code) => {
|
||||
if (code === 401) return 'authentication_error';
|
||||
if (code === 403) return 'permission_error';
|
||||
if (code === 429) return 'rate_limit_error';
|
||||
if (code >= 500) return 'server_error';
|
||||
return 'invalid_request_error';
|
||||
};
|
||||
|
||||
// 根据 HTTP 状态码映射 Gemini 的 status
|
||||
const getGeminiStatus = (code) => {
|
||||
if (code === 400) return 'INVALID_ARGUMENT';
|
||||
if (code === 401) return 'UNAUTHENTICATED';
|
||||
if (code === 403) return 'PERMISSION_DENIED';
|
||||
if (code === 404) return 'NOT_FOUND';
|
||||
if (code === 429) return 'RESOURCE_EXHAUSTED';
|
||||
if (code >= 500) return 'INTERNAL';
|
||||
return 'UNKNOWN';
|
||||
};
|
||||
|
||||
switch (protocolPrefix) {
|
||||
case MODEL_PROTOCOL_PREFIX.OPENAI:
|
||||
// OpenAI 流式错误格式(SSE data 块)
|
||||
const openaiError = {
|
||||
error: {
|
||||
message: errorMessage,
|
||||
type: getErrorType(statusCode),
|
||||
code: null
|
||||
}
|
||||
};
|
||||
return `data: ${JSON.stringify(openaiError)}\n\n`;
|
||||
|
||||
case MODEL_PROTOCOL_PREFIX.OPENAI_RESPONSES:
|
||||
// OpenAI Responses API 流式错误格式(SSE event + data)
|
||||
const responsesError = {
|
||||
id: `resp_${Date.now()}`,
|
||||
object: "error",
|
||||
created: Math.floor(Date.now() / 1000),
|
||||
error: {
|
||||
type: getErrorType(statusCode),
|
||||
message: errorMessage,
|
||||
code: getErrorType(statusCode)
|
||||
}
|
||||
};
|
||||
return `event: error\ndata: ${JSON.stringify(responsesError)}\n\n`;
|
||||
|
||||
case MODEL_PROTOCOL_PREFIX.CLAUDE:
|
||||
// Claude 流式错误格式(SSE event + data)
|
||||
const claudeError = {
|
||||
type: "error",
|
||||
error: {
|
||||
type: getErrorType(statusCode),
|
||||
message: errorMessage
|
||||
}
|
||||
};
|
||||
return `event: error\ndata: ${JSON.stringify(claudeError)}\n\n`;
|
||||
|
||||
case MODEL_PROTOCOL_PREFIX.GEMINI:
|
||||
// Gemini 流式错误格式
|
||||
// 注意:虽然 Gemini 原生使用 JSON 数组,但在我们的实现中已经转换为 SSE 格式
|
||||
// 所以这里也需要使用 data: 前缀,保持与正常流式响应一致
|
||||
const geminiError = {
|
||||
error: {
|
||||
code: statusCode,
|
||||
message: errorMessage,
|
||||
status: getGeminiStatus(statusCode)
|
||||
}
|
||||
};
|
||||
return `data: ${JSON.stringify(geminiError)}\n\n`;
|
||||
|
||||
default:
|
||||
// 默认使用 OpenAI SSE 格式
|
||||
const defaultError = {
|
||||
error: {
|
||||
message: errorMessage,
|
||||
type: getErrorType(statusCode),
|
||||
code: null
|
||||
}
|
||||
};
|
||||
return `data: ${JSON.stringify(defaultError)}\n\n`;
|
||||
}
|
||||
}
|
||||
|
|
@ -355,6 +355,10 @@ export class ClaudeConverter extends BaseConverter {
|
|||
prompt_tokens: claudeResponse.usage?.input_tokens || 0,
|
||||
completion_tokens: claudeResponse.usage?.output_tokens || 0,
|
||||
total_tokens: (claudeResponse.usage?.input_tokens || 0) + (claudeResponse.usage?.output_tokens || 0),
|
||||
cached_tokens: claudeResponse.usage?.cache_read_input_tokens || 0,
|
||||
prompt_tokens_details: {
|
||||
cached_tokens: claudeResponse.usage?.cache_read_input_tokens || 0
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
@ -388,7 +392,8 @@ export class ClaudeConverter extends BaseConverter {
|
|||
usage: {
|
||||
prompt_tokens: claudeChunk.message?.usage?.input_tokens || 0,
|
||||
completion_tokens: 0,
|
||||
total_tokens: claudeChunk.message?.usage?.input_tokens || 0
|
||||
total_tokens: claudeChunk.message?.usage?.input_tokens || 0,
|
||||
cached_tokens: claudeChunk.message?.usage?.cache_read_input_tokens || 0
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
@ -542,7 +547,11 @@ export class ClaudeConverter extends BaseConverter {
|
|||
usage: claudeChunk.usage ? {
|
||||
prompt_tokens: claudeChunk.usage.input_tokens || 0,
|
||||
completion_tokens: claudeChunk.usage.output_tokens || 0,
|
||||
total_tokens: (claudeChunk.usage.input_tokens || 0) + (claudeChunk.usage.output_tokens || 0)
|
||||
total_tokens: (claudeChunk.usage.input_tokens || 0) + (claudeChunk.usage.output_tokens || 0),
|
||||
cached_tokens: claudeChunk.usage.cache_read_input_tokens || 0,
|
||||
prompt_tokens_details: {
|
||||
cached_tokens: claudeChunk.usage.cache_read_input_tokens || 0
|
||||
}
|
||||
} : undefined
|
||||
};
|
||||
}
|
||||
|
|
@ -887,7 +896,16 @@ export class ClaudeConverter extends BaseConverter {
|
|||
usageMetadata: claudeResponse.usage ? {
|
||||
promptTokenCount: claudeResponse.usage.input_tokens || 0,
|
||||
candidatesTokenCount: claudeResponse.usage.output_tokens || 0,
|
||||
totalTokenCount: (claudeResponse.usage.input_tokens || 0) + (claudeResponse.usage.output_tokens || 0)
|
||||
totalTokenCount: (claudeResponse.usage.input_tokens || 0) + (claudeResponse.usage.output_tokens || 0),
|
||||
cachedContentTokenCount: claudeResponse.usage.cache_read_input_tokens || 0,
|
||||
promptTokensDetails: [{
|
||||
modality: "TEXT",
|
||||
tokenCount: claudeResponse.usage.input_tokens || 0
|
||||
}],
|
||||
candidatesTokensDetails: [{
|
||||
modality: "TEXT",
|
||||
tokenCount: claudeResponse.usage.output_tokens || 0
|
||||
}]
|
||||
} : {}
|
||||
};
|
||||
}
|
||||
|
|
@ -936,13 +954,33 @@ export class ClaudeConverter extends BaseConverter {
|
|||
// message_delta 事件 - 流结束
|
||||
if (claudeChunk.type === 'message_delta') {
|
||||
const stopReason = claudeChunk.delta?.stop_reason;
|
||||
return {
|
||||
const result = {
|
||||
candidates: [{
|
||||
finishReason: stopReason === 'end_turn' ? 'STOP' :
|
||||
stopReason === 'max_tokens' ? 'MAX_TOKENS' :
|
||||
'OTHER'
|
||||
}]
|
||||
};
|
||||
|
||||
// 添加 usage 信息
|
||||
if (claudeChunk.usage) {
|
||||
result.usageMetadata = {
|
||||
promptTokenCount: claudeChunk.usage.input_tokens || 0,
|
||||
candidatesTokenCount: claudeChunk.usage.output_tokens || 0,
|
||||
totalTokenCount: (claudeChunk.usage.input_tokens || 0) + (claudeChunk.usage.output_tokens || 0),
|
||||
cachedContentTokenCount: claudeChunk.usage.cache_read_input_tokens || 0,
|
||||
promptTokensDetails: [{
|
||||
modality: "TEXT",
|
||||
tokenCount: claudeChunk.usage.input_tokens || 0
|
||||
}],
|
||||
candidatesTokensDetails: [{
|
||||
modality: "TEXT",
|
||||
tokenCount: claudeChunk.usage.output_tokens || 0
|
||||
}]
|
||||
};
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1153,7 +1191,7 @@ export class ClaudeConverter extends BaseConverter {
|
|||
usage: {
|
||||
input_tokens: claudeResponse.usage?.input_tokens || 0,
|
||||
input_tokens_details: {
|
||||
cached_tokens: claudeResponse.usage?.cache_creation_input_tokens || 0,
|
||||
cached_tokens: claudeResponse.usage?.cache_read_input_tokens || 0
|
||||
},
|
||||
output_tokens: claudeResponse.usage?.output_tokens || 0,
|
||||
output_tokens_details: {
|
||||
|
|
@ -1266,7 +1304,13 @@ export class ClaudeConverter extends BaseConverter {
|
|||
if (lastEvent.response) {
|
||||
lastEvent.response.usage = {
|
||||
input_tokens: claudeChunk.usage.input_tokens || 0,
|
||||
input_tokens_details: {
|
||||
cached_tokens: claudeChunk.usage.cache_read_input_tokens || 0
|
||||
},
|
||||
output_tokens: claudeChunk.usage.output_tokens || 0,
|
||||
output_tokens_details: {
|
||||
reasoning_tokens: 0
|
||||
},
|
||||
total_tokens: (claudeChunk.usage.input_tokens || 0) + (claudeChunk.usage.output_tokens || 0)
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -160,10 +160,24 @@ export class GeminiConverter extends BaseConverter {
|
|||
prompt_tokens: geminiResponse.usageMetadata.promptTokenCount || 0,
|
||||
completion_tokens: geminiResponse.usageMetadata.candidatesTokenCount || 0,
|
||||
total_tokens: geminiResponse.usageMetadata.totalTokenCount || 0,
|
||||
cached_tokens: geminiResponse.usageMetadata.cachedContentTokenCount || 0,
|
||||
prompt_tokens_details: {
|
||||
cached_tokens: geminiResponse.usageMetadata.cachedContentTokenCount || 0
|
||||
},
|
||||
completion_tokens_details: {
|
||||
reasoning_tokens: geminiResponse.usageMetadata.thoughtsTokenCount || 0
|
||||
}
|
||||
} : {
|
||||
prompt_tokens: 0,
|
||||
completion_tokens: 0,
|
||||
total_tokens: 0,
|
||||
cached_tokens: 0,
|
||||
prompt_tokens_details: {
|
||||
cached_tokens: 0
|
||||
},
|
||||
completion_tokens_details: {
|
||||
reasoning_tokens: 0
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
@ -235,10 +249,24 @@ export class GeminiConverter extends BaseConverter {
|
|||
prompt_tokens: geminiChunk.usageMetadata.promptTokenCount || 0,
|
||||
completion_tokens: geminiChunk.usageMetadata.candidatesTokenCount || 0,
|
||||
total_tokens: geminiChunk.usageMetadata.totalTokenCount || 0,
|
||||
cached_tokens: geminiChunk.usageMetadata.cachedContentTokenCount || 0,
|
||||
prompt_tokens_details: {
|
||||
cached_tokens: geminiChunk.usageMetadata.cachedContentTokenCount || 0
|
||||
},
|
||||
completion_tokens_details: {
|
||||
reasoning_tokens: geminiChunk.usageMetadata.thoughtsTokenCount || 0
|
||||
}
|
||||
} : {
|
||||
prompt_tokens: 0,
|
||||
completion_tokens: 0,
|
||||
total_tokens: 0,
|
||||
cached_tokens: 0,
|
||||
prompt_tokens_details: {
|
||||
cached_tokens: 0
|
||||
},
|
||||
completion_tokens_details: {
|
||||
reasoning_tokens: 0
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
@ -447,6 +475,8 @@ export class GeminiConverter extends BaseConverter {
|
|||
stop_sequence: null,
|
||||
usage: {
|
||||
input_tokens: geminiResponse.usageMetadata?.promptTokenCount || 0,
|
||||
cache_creation_input_tokens: 0,
|
||||
cache_read_input_tokens: geminiResponse.usageMetadata?.cachedContentTokenCount || 0,
|
||||
output_tokens: geminiResponse.usageMetadata?.candidatesTokenCount || 0
|
||||
}
|
||||
};
|
||||
|
|
@ -483,7 +513,7 @@ export class GeminiConverter extends BaseConverter {
|
|||
|
||||
// 处理finishReason
|
||||
if (candidate.finishReason) {
|
||||
return {
|
||||
const result = {
|
||||
type: "message_delta",
|
||||
delta: {
|
||||
stop_reason: candidate.finishReason === 'STOP' ? 'end_turn' :
|
||||
|
|
@ -491,6 +521,22 @@ export class GeminiConverter extends BaseConverter {
|
|||
candidate.finishReason.toLowerCase()
|
||||
}
|
||||
};
|
||||
|
||||
// 添加 usage 信息
|
||||
if (geminiChunk.usageMetadata) {
|
||||
result.usage = {
|
||||
input_tokens: geminiChunk.usageMetadata.promptTokenCount || 0,
|
||||
cache_creation_input_tokens: 0,
|
||||
cache_read_input_tokens: geminiChunk.usageMetadata.cachedContentTokenCount || 0,
|
||||
output_tokens: geminiChunk.usageMetadata.candidatesTokenCount || 0,
|
||||
prompt_tokens: geminiChunk.usageMetadata.promptTokenCount || 0,
|
||||
completion_tokens: geminiChunk.usageMetadata.candidatesTokenCount || 0,
|
||||
total_tokens: geminiChunk.usageMetadata.totalTokenCount || 0,
|
||||
cached_tokens: geminiChunk.usageMetadata.cachedContentTokenCount || 0
|
||||
};
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -719,13 +765,13 @@ export class GeminiConverter extends BaseConverter {
|
|||
usage: {
|
||||
input_tokens: geminiResponse.usageMetadata?.promptTokenCount || 0,
|
||||
input_tokens_details: {
|
||||
cached_tokens: geminiResponse.usageMetadata?.cachedTokens || 0,
|
||||
cached_tokens: geminiResponse.usageMetadata?.cachedContentTokenCount || 0
|
||||
},
|
||||
output_tokens: geminiResponse.usageMetadata?.candidatesTokenCount || 0,
|
||||
output_tokens_details: {
|
||||
reasoning_tokens: 0
|
||||
reasoning_tokens: geminiResponse.usageMetadata?.thoughtsTokenCount || 0
|
||||
},
|
||||
total_tokens: geminiResponse.usageMetadata?.totalTokenCount || 0,
|
||||
total_tokens: geminiResponse.usageMetadata?.totalTokenCount || 0
|
||||
},
|
||||
user: null
|
||||
};
|
||||
|
|
@ -791,7 +837,13 @@ export class GeminiConverter extends BaseConverter {
|
|||
if (lastEvent.response) {
|
||||
lastEvent.response.usage = {
|
||||
input_tokens: geminiChunk.usageMetadata.promptTokenCount || 0,
|
||||
input_tokens_details: {
|
||||
cached_tokens: geminiChunk.usageMetadata.cachedContentTokenCount || 0
|
||||
},
|
||||
output_tokens: geminiChunk.usageMetadata.candidatesTokenCount || 0,
|
||||
output_tokens_details: {
|
||||
reasoning_tokens: geminiChunk.usageMetadata.thoughtsTokenCount || 0
|
||||
},
|
||||
total_tokens: geminiChunk.usageMetadata.totalTokenCount || 0
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -327,6 +327,8 @@ export class OpenAIConverter extends BaseConverter {
|
|||
stop_sequence: null,
|
||||
usage: {
|
||||
input_tokens: openaiResponse.usage?.prompt_tokens || 0,
|
||||
cache_creation_input_tokens: 0,
|
||||
cache_read_input_tokens: openaiResponse.usage?.prompt_tokens_details?.cached_tokens || 0,
|
||||
output_tokens: openaiResponse.usage?.completion_tokens || 0
|
||||
}
|
||||
};
|
||||
|
|
@ -458,8 +460,10 @@ export class OpenAIConverter extends BaseConverter {
|
|||
stop_sequence: null
|
||||
},
|
||||
usage: {
|
||||
output_tokens: openaiChunk.usage?.completion_tokens || 0,
|
||||
input_tokens: openaiChunk.usage?.prompt_tokens || 0,
|
||||
cache_creation_input_tokens: 0,
|
||||
cache_read_input_tokens: openaiChunk.usage?.prompt_tokens_details?.cached_tokens || 0,
|
||||
output_tokens: openaiChunk.usage?.completion_tokens || 0
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -778,7 +782,17 @@ export class OpenAIConverter extends BaseConverter {
|
|||
usageMetadata: openaiResponse.usage ? {
|
||||
promptTokenCount: openaiResponse.usage.prompt_tokens || 0,
|
||||
candidatesTokenCount: openaiResponse.usage.completion_tokens || 0,
|
||||
totalTokenCount: openaiResponse.usage.total_tokens || 0
|
||||
totalTokenCount: openaiResponse.usage.total_tokens || 0,
|
||||
cachedContentTokenCount: openaiResponse.usage.prompt_tokens_details?.cached_tokens || 0,
|
||||
promptTokensDetails: [{
|
||||
modality: "TEXT",
|
||||
tokenCount: openaiResponse.usage.prompt_tokens || 0
|
||||
}],
|
||||
candidatesTokensDetails: [{
|
||||
modality: "TEXT",
|
||||
tokenCount: openaiResponse.usage.completion_tokens || 0
|
||||
}],
|
||||
thoughtsTokenCount: openaiResponse.usage.completion_tokens_details?.reasoning_tokens || 0
|
||||
} : {}
|
||||
};
|
||||
}
|
||||
|
|
@ -850,7 +864,17 @@ export class OpenAIConverter extends BaseConverter {
|
|||
result.usageMetadata = {
|
||||
promptTokenCount: openaiChunk.usage.prompt_tokens || 0,
|
||||
candidatesTokenCount: openaiChunk.usage.completion_tokens || 0,
|
||||
totalTokenCount: openaiChunk.usage.total_tokens || 0
|
||||
totalTokenCount: openaiChunk.usage.total_tokens || 0,
|
||||
cachedContentTokenCount: openaiChunk.usage.prompt_tokens_details?.cached_tokens || 0,
|
||||
promptTokensDetails: [{
|
||||
modality: "TEXT",
|
||||
tokenCount: openaiChunk.usage.prompt_tokens || 0
|
||||
}],
|
||||
candidatesTokensDetails: [{
|
||||
modality: "TEXT",
|
||||
tokenCount: openaiChunk.usage.completion_tokens || 0
|
||||
}],
|
||||
thoughtsTokenCount: openaiChunk.usage.completion_tokens_details?.reasoning_tokens || 0
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -946,11 +970,23 @@ export class OpenAIConverter extends BaseConverter {
|
|||
output: output,
|
||||
usage: openaiResponse.usage ? {
|
||||
input_tokens: openaiResponse.usage.prompt_tokens || 0,
|
||||
input_tokens_details: {
|
||||
cached_tokens: openaiResponse.usage.prompt_tokens_details?.cached_tokens || 0
|
||||
},
|
||||
output_tokens: openaiResponse.usage.completion_tokens || 0,
|
||||
output_tokens_details: {
|
||||
reasoning_tokens: openaiResponse.usage.completion_tokens_details?.reasoning_tokens || 0
|
||||
},
|
||||
total_tokens: openaiResponse.usage.total_tokens || 0
|
||||
} : {
|
||||
input_tokens: 0,
|
||||
input_tokens_details: {
|
||||
cached_tokens: 0
|
||||
},
|
||||
output_tokens: 0,
|
||||
output_tokens_details: {
|
||||
reasoning_tokens: 0
|
||||
},
|
||||
total_tokens: 0
|
||||
}
|
||||
};
|
||||
|
|
@ -1051,7 +1087,13 @@ export class OpenAIConverter extends BaseConverter {
|
|||
if (lastEvent.response) {
|
||||
lastEvent.response.usage = {
|
||||
input_tokens: openaiChunk.usage.prompt_tokens || 0,
|
||||
input_tokens_details: {
|
||||
cached_tokens: openaiChunk.usage.prompt_tokens_details?.cached_tokens || 0
|
||||
},
|
||||
output_tokens: openaiChunk.usage.completion_tokens || 0,
|
||||
output_tokens_details: {
|
||||
reasoning_tokens: openaiChunk.usage.completion_tokens_details?.reasoning_tokens || 0
|
||||
},
|
||||
total_tokens: openaiChunk.usage.total_tokens || 0
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -173,10 +173,26 @@ export class OpenAIResponsesConverter extends BaseConverter {
|
|||
},
|
||||
finish_reason: responsesResponse.finish_reason || 'stop'
|
||||
}],
|
||||
usage: responsesResponse.usage || {
|
||||
usage: responsesResponse.usage ? {
|
||||
prompt_tokens: responsesResponse.usage.input_tokens || 0,
|
||||
completion_tokens: responsesResponse.usage.output_tokens || 0,
|
||||
total_tokens: responsesResponse.usage.total_tokens || 0,
|
||||
prompt_tokens_details: {
|
||||
cached_tokens: responsesResponse.usage.input_tokens_details?.cached_tokens || 0
|
||||
},
|
||||
completion_tokens_details: {
|
||||
reasoning_tokens: responsesResponse.usage.output_tokens_details?.reasoning_tokens || 0
|
||||
}
|
||||
} : {
|
||||
prompt_tokens: 0,
|
||||
completion_tokens: 0,
|
||||
total_tokens: 0
|
||||
total_tokens: 0,
|
||||
prompt_tokens_details: {
|
||||
cached_tokens: 0
|
||||
},
|
||||
completion_tokens_details: {
|
||||
reasoning_tokens: 0
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
@ -283,8 +299,16 @@ export class OpenAIResponsesConverter extends BaseConverter {
|
|||
model: model || responsesResponse.model,
|
||||
stop_reason: responsesResponse.choices?.[0]?.finish_reason || 'end_turn',
|
||||
usage: {
|
||||
input_tokens: responsesResponse.usage?.prompt_tokens || 0,
|
||||
output_tokens: responsesResponse.usage?.completion_tokens || 0
|
||||
input_tokens: responsesResponse.usage?.input_tokens || responsesResponse.usage?.prompt_tokens || 0,
|
||||
cache_creation_input_tokens: 0,
|
||||
cache_read_input_tokens: responsesResponse.usage?.input_tokens_details?.cached_tokens || 0,
|
||||
output_tokens: responsesResponse.usage?.output_tokens || responsesResponse.usage?.completion_tokens || 0,
|
||||
prompt_tokens: responsesResponse.usage?.input_tokens || responsesResponse.usage?.prompt_tokens || 0,
|
||||
completion_tokens: responsesResponse.usage?.output_tokens || responsesResponse.usage?.completion_tokens || 0,
|
||||
total_tokens: responsesResponse.usage?.total_tokens ||
|
||||
((responsesResponse.usage?.input_tokens || responsesResponse.usage?.prompt_tokens || 0) +
|
||||
(responsesResponse.usage?.output_tokens || responsesResponse.usage?.completion_tokens || 0)),
|
||||
cached_tokens: responsesResponse.usage?.input_tokens_details?.cached_tokens || 0
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
@ -429,9 +453,21 @@ export class OpenAIResponsesConverter extends BaseConverter {
|
|||
index: 0
|
||||
}],
|
||||
usageMetadata: {
|
||||
promptTokenCount: responsesResponse.usage?.prompt_tokens || 0,
|
||||
candidatesTokenCount: responsesResponse.usage?.completion_tokens || 0,
|
||||
totalTokenCount: responsesResponse.usage?.total_tokens || 0
|
||||
promptTokenCount: responsesResponse.usage?.input_tokens || responsesResponse.usage?.prompt_tokens || 0,
|
||||
candidatesTokenCount: responsesResponse.usage?.output_tokens || responsesResponse.usage?.completion_tokens || 0,
|
||||
totalTokenCount: responsesResponse.usage?.total_tokens ||
|
||||
((responsesResponse.usage?.input_tokens || responsesResponse.usage?.prompt_tokens || 0) +
|
||||
(responsesResponse.usage?.output_tokens || responsesResponse.usage?.completion_tokens || 0)),
|
||||
cachedContentTokenCount: responsesResponse.usage?.input_tokens_details?.cached_tokens || 0,
|
||||
promptTokensDetails: [{
|
||||
modality: "TEXT",
|
||||
tokenCount: responsesResponse.usage?.input_tokens || responsesResponse.usage?.prompt_tokens || 0
|
||||
}],
|
||||
candidatesTokensDetails: [{
|
||||
modality: "TEXT",
|
||||
tokenCount: responsesResponse.usage?.output_tokens || responsesResponse.usage?.completion_tokens || 0
|
||||
}],
|
||||
thoughtsTokenCount: responsesResponse.usage?.output_tokens_details?.reasoning_tokens || 0
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -56,31 +56,11 @@ export async function initApiService(config) {
|
|||
return serviceInstances; // Return the collection of initialized service instances
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom error class for no available provider in pool
|
||||
*/
|
||||
export class NoAvailableProviderError extends Error {
|
||||
constructor(providerType, requestedModel = null) {
|
||||
const message = requestedModel
|
||||
? `号池无可用账号: ${providerType} (model: ${requestedModel})`
|
||||
: `号池无可用账号: ${providerType}`;
|
||||
super(message);
|
||||
this.name = 'NoAvailableProviderError';
|
||||
this.providerType = providerType;
|
||||
this.requestedModel = requestedModel;
|
||||
this.statusCode = 400;
|
||||
}
|
||||
}
|
||||
|
||||
// Provider types that should throw error when no available provider (instead of falling back to main config)
|
||||
const STRICT_POOL_PROVIDERS = ['claude-kiro-oauth'];
|
||||
|
||||
/**
|
||||
* 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
|
||||
* @throws {NoAvailableProviderError} If no healthy provider is available in the pool (only for strict pool providers)
|
||||
*/
|
||||
export async function getApiService(config, requestedModel = null) {
|
||||
let serviceConfig = config;
|
||||
|
|
@ -94,14 +74,7 @@ export async function getApiService(config, requestedModel = null) {
|
|||
config.uuid = serviceConfig.uuid;
|
||||
console.log(`[API Service] Using pooled configuration for ${config.MODEL_PROVIDER}: ${serviceConfig.uuid}${requestedModel ? ` (model: ${requestedModel})` : ''}`);
|
||||
} else {
|
||||
// 号池没有可用账号
|
||||
// 只有 Kiro 类型的 provider 抛出错误,其他 provider 回退到主配置
|
||||
if (STRICT_POOL_PROVIDERS.includes(config.MODEL_PROVIDER)) {
|
||||
console.error(`[API Service] 号池无可用账号: ${config.MODEL_PROVIDER}${requestedModel ? ` (model: ${requestedModel})` : ''}`);
|
||||
throw new NoAvailableProviderError(config.MODEL_PROVIDER, requestedModel);
|
||||
} else {
|
||||
console.warn(`[API Service] No healthy provider found in pool for ${config.MODEL_PROVIDER}${requestedModel ? ` supporting model: ${requestedModel}` : ''}. 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);
|
||||
|
|
|
|||
Loading…
Reference in a new issue