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": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@anthropic-ai/tokenizer": "^0.0.4",
|
||||||
"axios": "^1.10.0",
|
"axios": "^1.10.0",
|
||||||
"deepmerge": "^4.3.1",
|
"deepmerge": "^4.3.1",
|
||||||
"dotenv": "^16.4.5",
|
"dotenv": "^16.4.5",
|
||||||
|
|
@ -39,6 +40,31 @@
|
||||||
"node": ">=6.0.0"
|
"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": {
|
"node_modules/@babel/code-frame": {
|
||||||
"version": "7.27.1",
|
"version": "7.27.1",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz",
|
||||||
|
|
@ -6217,6 +6243,12 @@
|
||||||
"node": ">=8"
|
"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": {
|
"node_modules/tmpl": {
|
||||||
"version": "1.0.5",
|
"version": "1.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz",
|
||||||
|
|
|
||||||
|
|
@ -76,9 +76,18 @@ async function getMacAddressSha256() {
|
||||||
return sha256Hash;
|
return sha256Hash;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper functions for tool calls
|
// Helper functions for tool calls and JSON parsing
|
||||||
function findMatchingBracket(text, startPos) {
|
|
||||||
if (!text || startPos >= text.length || text[startPos] !== '[') {
|
/**
|
||||||
|
* 通用的括号匹配函数 - 支持多种括号类型
|
||||||
|
* @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;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -105,9 +114,9 @@ function findMatchingBracket(text, startPos) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!inString) {
|
if (!inString) {
|
||||||
if (char === '[') {
|
if (char === openChar) {
|
||||||
bracketCount++;
|
bracketCount++;
|
||||||
} else if (char === ']') {
|
} else if (char === closeChar) {
|
||||||
bracketCount--;
|
bracketCount--;
|
||||||
if (bracketCount === 0) {
|
if (bracketCount === 0) {
|
||||||
return i;
|
return i;
|
||||||
|
|
@ -118,6 +127,28 @@ function findMatchingBracket(text, startPos) {
|
||||||
return -1;
|
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) {
|
function parseSingleToolCall(toolCallText) {
|
||||||
const namePattern = /\[Called\s+(\w+)\s+with\s+args:/i;
|
const namePattern = /\[Called\s+(\w+)\s+with\s+args:/i;
|
||||||
const nameMatch = toolCallText.match(namePattern);
|
const nameMatch = toolCallText.match(namePattern);
|
||||||
|
|
@ -144,16 +175,7 @@ function parseSingleToolCall(toolCallText) {
|
||||||
const jsonCandidate = toolCallText.substring(argsStart, argsEnd).trim();
|
const jsonCandidate = toolCallText.substring(argsStart, argsEnd).trim();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Simple repair for common issues like trailing commas or unquoted keys
|
const repairedJson = repairJson(jsonCandidate);
|
||||||
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 argumentsObj = JSON.parse(repairedJson);
|
const argumentsObj = JSON.parse(repairedJson);
|
||||||
|
|
||||||
if (typeof argumentsObj !== 'object' || argumentsObj === null) {
|
if (typeof argumentsObj !== 'object' || argumentsObj === null) {
|
||||||
|
|
@ -934,6 +956,9 @@ async initializeAuth(forceRefresh = false) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 调用 API 并处理错误重试
|
||||||
|
*/
|
||||||
async callApi(method, model, body, isRetry = false, retryCount = 0) {
|
async callApi(method, model, body, isRetry = false, retryCount = 0) {
|
||||||
if (!this.isInitialized) await this.initialize();
|
if (!this.isInitialized) await this.initialize();
|
||||||
const maxRetries = this.config.REQUEST_MAX_RETRIES || 3;
|
const maxRetries = this.config.REQUEST_MAX_RETRIES || 3;
|
||||||
|
|
@ -1135,7 +1160,8 @@ async initializeAuth(forceRefresh = false) {
|
||||||
if (parsed.content !== undefined && !parsed.followupPrompt) {
|
if (parsed.content !== undefined && !parsed.followupPrompt) {
|
||||||
// 处理转义字符
|
// 处理转义字符
|
||||||
let decodedContent = parsed.content;
|
let decodedContent = parsed.content;
|
||||||
decodedContent = decodedContent.replace(/(?<!\\)\\n/g, '\n');
|
// 无须处理转义的换行符,原来要处理是因为智能体返回的 content 需要通过换行符切割不同的json
|
||||||
|
// decodedContent = decodedContent.replace(/(?<!\\)\\n/g, '\n');
|
||||||
events.push({ type: 'content', data: decodedContent });
|
events.push({ type: 'content', data: decodedContent });
|
||||||
}
|
}
|
||||||
// 处理结构化工具调用事件 - 开始事件(包含 name 和 toolUseId)
|
// 处理结构化工具调用事件 - 开始事件(包含 name 和 toolUseId)
|
||||||
|
|
|
||||||
216
src/common.js
216
src/common.js
|
|
@ -243,12 +243,11 @@ export async function handleStreamRequest(res, service, model, requestBody, from
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!res.writableEnded) {
|
// 使用新方法创建符合 fromProvider 格式的流式错误响应
|
||||||
const errorPayload = { error: { message: "An error occurred during streaming.", details: error.message } };
|
const errorPayload = createStreamErrorResponse(error, fromProvider);
|
||||||
res.end(JSON.stringify(errorPayload));
|
res.write(errorPayload);
|
||||||
|
res.end();
|
||||||
responseClosed = true;
|
responseClosed = true;
|
||||||
}
|
|
||||||
|
|
||||||
} finally {
|
} finally {
|
||||||
if (!responseClosed) {
|
if (!responseClosed) {
|
||||||
res.end();
|
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) {
|
export async function handleUnaryRequest(res, service, model, requestBody, fromProvider, toProvider, PROMPT_LOG_MODE, PROMPT_LOG_FILENAME, providerPoolManager, pooluuid) {
|
||||||
try{
|
try{
|
||||||
// The service returns the response in its native format (toProvider).
|
// The service returns the response in its native format (toProvider).
|
||||||
|
|
@ -288,14 +288,8 @@ export async function handleUnaryRequest(res, service, model, requestBody, fromP
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 返回错误响应给客户端
|
// 使用新方法创建符合 fromProvider 格式的错误响应
|
||||||
const errorResponse = {
|
const errorResponse = createErrorResponse(error, fromProvider);
|
||||||
error: {
|
|
||||||
message: error.message || "An error occurred during processing.",
|
|
||||||
code: error.status || 500,
|
|
||||||
details: error.stack
|
|
||||||
}
|
|
||||||
};
|
|
||||||
await handleUnifiedResponse(res, JSON.stringify(errorResponse), false);
|
await handleUnifiedResponse(res, JSON.stringify(errorResponse), false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -402,24 +396,9 @@ export async function handleContentGenerationRequest(req, res, service, endpoint
|
||||||
|
|
||||||
// 2.5. 如果使用了提供商池,根据模型重新选择提供商
|
// 2.5. 如果使用了提供商池,根据模型重新选择提供商
|
||||||
if (providerPoolManager && CONFIG.providerPools && CONFIG.providerPools[CONFIG.MODEL_PROVIDER]) {
|
if (providerPoolManager && CONFIG.providerPools && CONFIG.providerPools[CONFIG.MODEL_PROVIDER]) {
|
||||||
const { getApiService, NoAvailableProviderError } = await import('./service-manager.js');
|
const { getApiService } = await import('./service-manager.js');
|
||||||
try {
|
|
||||||
service = await getApiService(CONFIG, model);
|
service = await getApiService(CONFIG, model);
|
||||||
console.log(`[Content Generation] Re-selected service adapter based on model: ${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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Apply system prompt from file if configured.
|
// 3. Apply system prompt from file if configured.
|
||||||
|
|
@ -619,3 +598,182 @@ export function getMD5Hash(obj) {
|
||||||
const jsonString = JSON.stringify(obj);
|
const jsonString = JSON.stringify(obj);
|
||||||
return crypto.createHash('md5').update(jsonString).digest('hex');
|
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,
|
prompt_tokens: claudeResponse.usage?.input_tokens || 0,
|
||||||
completion_tokens: claudeResponse.usage?.output_tokens || 0,
|
completion_tokens: claudeResponse.usage?.output_tokens || 0,
|
||||||
total_tokens: (claudeResponse.usage?.input_tokens || 0) + (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: {
|
usage: {
|
||||||
prompt_tokens: claudeChunk.message?.usage?.input_tokens || 0,
|
prompt_tokens: claudeChunk.message?.usage?.input_tokens || 0,
|
||||||
completion_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 ? {
|
usage: claudeChunk.usage ? {
|
||||||
prompt_tokens: claudeChunk.usage.input_tokens || 0,
|
prompt_tokens: claudeChunk.usage.input_tokens || 0,
|
||||||
completion_tokens: claudeChunk.usage.output_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
|
} : undefined
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -887,7 +896,16 @@ export class ClaudeConverter extends BaseConverter {
|
||||||
usageMetadata: claudeResponse.usage ? {
|
usageMetadata: claudeResponse.usage ? {
|
||||||
promptTokenCount: claudeResponse.usage.input_tokens || 0,
|
promptTokenCount: claudeResponse.usage.input_tokens || 0,
|
||||||
candidatesTokenCount: claudeResponse.usage.output_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 事件 - 流结束
|
// message_delta 事件 - 流结束
|
||||||
if (claudeChunk.type === 'message_delta') {
|
if (claudeChunk.type === 'message_delta') {
|
||||||
const stopReason = claudeChunk.delta?.stop_reason;
|
const stopReason = claudeChunk.delta?.stop_reason;
|
||||||
return {
|
const result = {
|
||||||
candidates: [{
|
candidates: [{
|
||||||
finishReason: stopReason === 'end_turn' ? 'STOP' :
|
finishReason: stopReason === 'end_turn' ? 'STOP' :
|
||||||
stopReason === 'max_tokens' ? 'MAX_TOKENS' :
|
stopReason === 'max_tokens' ? 'MAX_TOKENS' :
|
||||||
'OTHER'
|
'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: {
|
usage: {
|
||||||
input_tokens: claudeResponse.usage?.input_tokens || 0,
|
input_tokens: claudeResponse.usage?.input_tokens || 0,
|
||||||
input_tokens_details: {
|
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: claudeResponse.usage?.output_tokens || 0,
|
||||||
output_tokens_details: {
|
output_tokens_details: {
|
||||||
|
|
@ -1266,7 +1304,13 @@ export class ClaudeConverter extends BaseConverter {
|
||||||
if (lastEvent.response) {
|
if (lastEvent.response) {
|
||||||
lastEvent.response.usage = {
|
lastEvent.response.usage = {
|
||||||
input_tokens: claudeChunk.usage.input_tokens || 0,
|
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: claudeChunk.usage.output_tokens || 0,
|
||||||
|
output_tokens_details: {
|
||||||
|
reasoning_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)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -160,10 +160,24 @@ export class GeminiConverter extends BaseConverter {
|
||||||
prompt_tokens: geminiResponse.usageMetadata.promptTokenCount || 0,
|
prompt_tokens: geminiResponse.usageMetadata.promptTokenCount || 0,
|
||||||
completion_tokens: geminiResponse.usageMetadata.candidatesTokenCount || 0,
|
completion_tokens: geminiResponse.usageMetadata.candidatesTokenCount || 0,
|
||||||
total_tokens: geminiResponse.usageMetadata.totalTokenCount || 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,
|
prompt_tokens: 0,
|
||||||
completion_tokens: 0,
|
completion_tokens: 0,
|
||||||
total_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,
|
prompt_tokens: geminiChunk.usageMetadata.promptTokenCount || 0,
|
||||||
completion_tokens: geminiChunk.usageMetadata.candidatesTokenCount || 0,
|
completion_tokens: geminiChunk.usageMetadata.candidatesTokenCount || 0,
|
||||||
total_tokens: geminiChunk.usageMetadata.totalTokenCount || 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,
|
prompt_tokens: 0,
|
||||||
completion_tokens: 0,
|
completion_tokens: 0,
|
||||||
total_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,
|
stop_sequence: null,
|
||||||
usage: {
|
usage: {
|
||||||
input_tokens: geminiResponse.usageMetadata?.promptTokenCount || 0,
|
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
|
output_tokens: geminiResponse.usageMetadata?.candidatesTokenCount || 0
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -483,7 +513,7 @@ export class GeminiConverter extends BaseConverter {
|
||||||
|
|
||||||
// 处理finishReason
|
// 处理finishReason
|
||||||
if (candidate.finishReason) {
|
if (candidate.finishReason) {
|
||||||
return {
|
const result = {
|
||||||
type: "message_delta",
|
type: "message_delta",
|
||||||
delta: {
|
delta: {
|
||||||
stop_reason: candidate.finishReason === 'STOP' ? 'end_turn' :
|
stop_reason: candidate.finishReason === 'STOP' ? 'end_turn' :
|
||||||
|
|
@ -491,6 +521,22 @@ export class GeminiConverter extends BaseConverter {
|
||||||
candidate.finishReason.toLowerCase()
|
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: {
|
usage: {
|
||||||
input_tokens: geminiResponse.usageMetadata?.promptTokenCount || 0,
|
input_tokens: geminiResponse.usageMetadata?.promptTokenCount || 0,
|
||||||
input_tokens_details: {
|
input_tokens_details: {
|
||||||
cached_tokens: geminiResponse.usageMetadata?.cachedTokens || 0,
|
cached_tokens: geminiResponse.usageMetadata?.cachedContentTokenCount || 0
|
||||||
},
|
},
|
||||||
output_tokens: geminiResponse.usageMetadata?.candidatesTokenCount || 0,
|
output_tokens: geminiResponse.usageMetadata?.candidatesTokenCount || 0,
|
||||||
output_tokens_details: {
|
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
|
user: null
|
||||||
};
|
};
|
||||||
|
|
@ -791,7 +837,13 @@ export class GeminiConverter extends BaseConverter {
|
||||||
if (lastEvent.response) {
|
if (lastEvent.response) {
|
||||||
lastEvent.response.usage = {
|
lastEvent.response.usage = {
|
||||||
input_tokens: geminiChunk.usageMetadata.promptTokenCount || 0,
|
input_tokens: geminiChunk.usageMetadata.promptTokenCount || 0,
|
||||||
|
input_tokens_details: {
|
||||||
|
cached_tokens: geminiChunk.usageMetadata.cachedContentTokenCount || 0
|
||||||
|
},
|
||||||
output_tokens: geminiChunk.usageMetadata.candidatesTokenCount || 0,
|
output_tokens: geminiChunk.usageMetadata.candidatesTokenCount || 0,
|
||||||
|
output_tokens_details: {
|
||||||
|
reasoning_tokens: geminiChunk.usageMetadata.thoughtsTokenCount || 0
|
||||||
|
},
|
||||||
total_tokens: geminiChunk.usageMetadata.totalTokenCount || 0
|
total_tokens: geminiChunk.usageMetadata.totalTokenCount || 0
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -327,6 +327,8 @@ export class OpenAIConverter extends BaseConverter {
|
||||||
stop_sequence: null,
|
stop_sequence: null,
|
||||||
usage: {
|
usage: {
|
||||||
input_tokens: openaiResponse.usage?.prompt_tokens || 0,
|
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
|
output_tokens: openaiResponse.usage?.completion_tokens || 0
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -458,8 +460,10 @@ export class OpenAIConverter extends BaseConverter {
|
||||||
stop_sequence: null
|
stop_sequence: null
|
||||||
},
|
},
|
||||||
usage: {
|
usage: {
|
||||||
output_tokens: openaiChunk.usage?.completion_tokens || 0,
|
|
||||||
input_tokens: openaiChunk.usage?.prompt_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 ? {
|
usageMetadata: openaiResponse.usage ? {
|
||||||
promptTokenCount: openaiResponse.usage.prompt_tokens || 0,
|
promptTokenCount: openaiResponse.usage.prompt_tokens || 0,
|
||||||
candidatesTokenCount: openaiResponse.usage.completion_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 = {
|
result.usageMetadata = {
|
||||||
promptTokenCount: openaiChunk.usage.prompt_tokens || 0,
|
promptTokenCount: openaiChunk.usage.prompt_tokens || 0,
|
||||||
candidatesTokenCount: openaiChunk.usage.completion_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,
|
output: output,
|
||||||
usage: openaiResponse.usage ? {
|
usage: openaiResponse.usage ? {
|
||||||
input_tokens: openaiResponse.usage.prompt_tokens || 0,
|
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: 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
|
total_tokens: openaiResponse.usage.total_tokens || 0
|
||||||
} : {
|
} : {
|
||||||
input_tokens: 0,
|
input_tokens: 0,
|
||||||
|
input_tokens_details: {
|
||||||
|
cached_tokens: 0
|
||||||
|
},
|
||||||
output_tokens: 0,
|
output_tokens: 0,
|
||||||
|
output_tokens_details: {
|
||||||
|
reasoning_tokens: 0
|
||||||
|
},
|
||||||
total_tokens: 0
|
total_tokens: 0
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -1051,7 +1087,13 @@ export class OpenAIConverter extends BaseConverter {
|
||||||
if (lastEvent.response) {
|
if (lastEvent.response) {
|
||||||
lastEvent.response.usage = {
|
lastEvent.response.usage = {
|
||||||
input_tokens: openaiChunk.usage.prompt_tokens || 0,
|
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: 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
|
total_tokens: openaiChunk.usage.total_tokens || 0
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -173,10 +173,26 @@ export class OpenAIResponsesConverter extends BaseConverter {
|
||||||
},
|
},
|
||||||
finish_reason: responsesResponse.finish_reason || 'stop'
|
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,
|
prompt_tokens: 0,
|
||||||
completion_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,
|
model: model || responsesResponse.model,
|
||||||
stop_reason: responsesResponse.choices?.[0]?.finish_reason || 'end_turn',
|
stop_reason: responsesResponse.choices?.[0]?.finish_reason || 'end_turn',
|
||||||
usage: {
|
usage: {
|
||||||
input_tokens: responsesResponse.usage?.prompt_tokens || 0,
|
input_tokens: responsesResponse.usage?.input_tokens || responsesResponse.usage?.prompt_tokens || 0,
|
||||||
output_tokens: responsesResponse.usage?.completion_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
|
index: 0
|
||||||
}],
|
}],
|
||||||
usageMetadata: {
|
usageMetadata: {
|
||||||
promptTokenCount: responsesResponse.usage?.prompt_tokens || 0,
|
promptTokenCount: responsesResponse.usage?.input_tokens || responsesResponse.usage?.prompt_tokens || 0,
|
||||||
candidatesTokenCount: responsesResponse.usage?.completion_tokens || 0,
|
candidatesTokenCount: responsesResponse.usage?.output_tokens || responsesResponse.usage?.completion_tokens || 0,
|
||||||
totalTokenCount: responsesResponse.usage?.total_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
|
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
|
* Get API service adapter, considering provider pools
|
||||||
* @param {Object} config - The current request configuration
|
* @param {Object} config - The current request configuration
|
||||||
* @param {string} [requestedModel] - Optional. The model name to filter providers by.
|
* @param {string} [requestedModel] - Optional. The model name to filter providers by.
|
||||||
* @returns {Promise<Object>} The API service adapter
|
* @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) {
|
export async function getApiService(config, requestedModel = null) {
|
||||||
let serviceConfig = config;
|
let serviceConfig = config;
|
||||||
|
|
@ -93,17 +73,10 @@ export async function getApiService(config, requestedModel = null) {
|
||||||
delete serviceConfig.providerPools; // 移除 providerPools 属性
|
delete serviceConfig.providerPools; // 移除 providerPools 属性
|
||||||
config.uuid = serviceConfig.uuid;
|
config.uuid = serviceConfig.uuid;
|
||||||
console.log(`[API Service] Using pooled configuration for ${config.MODEL_PROVIDER}: ${serviceConfig.uuid}${requestedModel ? ` (model: ${requestedModel})` : ''}`);
|
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 {
|
} 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);
|
return getServiceAdapter(serviceConfig);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue