From a212a71385cdb6cd0f13785dd9ec75a383bbba1a Mon Sep 17 00:00:00 2001 From: hex2077 Date: Sun, 9 Nov 2025 22:22:22 +0800 Subject: [PATCH] =?UTF-8?q?refactor(converters):=20=E9=87=8D=E6=9E=84?= =?UTF-8?q?=E5=8D=8F=E8=AE=AE=E8=BD=AC=E6=8D=A2=E5=99=A8=E6=9E=B6=E6=9E=84?= =?UTF-8?q?=E5=B9=B6=E8=BF=81=E7=A7=BB=E8=87=B3=E7=AD=96=E7=95=A5=E6=A8=A1?= =?UTF-8?q?=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 创建新的转换器基础架构,包括BaseConverter、ConverterFactory和策略模式实现 - 迁移OpenAI、Claude、Gemini和OpenAI Responses(仅对话,无工具)转换器到新的策略模式 - 移除旧的ensureRolesInContents函数,优化内容处理逻辑 - 添加注册转换器机制,支持动态协议转换 - 更新API服务器以使用新的转换器工厂 - 优化流式响应处理和协议映射逻辑 - 添加详细的转换器工具和实用函数 - 改进错误处理和调试日志记录 --- src/api-server.js | 11 +- src/common.js | 61 +- src/convert-old.js | 2571 ++++++++++++++++ src/convert.js | 2739 ++--------------- src/converters/BaseConverter.js | 115 + src/converters/ConverterFactory.js | 182 ++ src/converters/register-converters.js | 25 + src/converters/strategies/ClaudeConverter.js | 1290 ++++++++ src/converters/strategies/GeminiConverter.js | 820 +++++ src/converters/strategies/OpenAIConverter.js | 997 ++++++ .../strategies/OpenAIResponsesConverter.js | 532 ++++ src/converters/utils.js | 296 ++ .../claude/oldResponse1762501965597.json | 1 + .../claude/oldResponseChunk1762501803864.json | 19 + .../originalRequestBody1762501795790.json | 1 + .../originalRequestBody1762501964743.json | 1 + .../claude/responseChunk1762590717239.json | 58 + .../gemini/oldResponse1762502386432.json | 1 + .../gemini/oldResponseChunk1762502078640.json | 3 + .../originalRequestBody1762502075001.json | 1 + .../originalRequestBody1762502375803.json | 1 + .../openai/oldResponse1762501667708.json | 1 + .../openai/oldResponseChunk1762501528837.json | 71 + .../originalRequestBody1762501497434.json | 1 + .../originalRequestBody1762501665376.json | 1 + .../oldResponse1762502706486.json | 56 + .../oldResponseChunk1762500869734.json | 53 + .../originalRequestBody1762500856556.json | 1 + src/gemini/gemini-core.js | 129 +- src/openai/openai-responses-core.js | 2 +- src/provider-pool-manager.js | 16 +- 31 files changed, 7517 insertions(+), 2539 deletions(-) create mode 100644 src/convert-old.js create mode 100644 src/converters/BaseConverter.js create mode 100644 src/converters/ConverterFactory.js create mode 100644 src/converters/register-converters.js create mode 100644 src/converters/strategies/ClaudeConverter.js create mode 100644 src/converters/strategies/GeminiConverter.js create mode 100644 src/converters/strategies/OpenAIConverter.js create mode 100644 src/converters/strategies/OpenAIResponsesConverter.js create mode 100644 src/converters/utils.js create mode 100644 src/example/claude/oldResponse1762501965597.json create mode 100644 src/example/claude/oldResponseChunk1762501803864.json create mode 100644 src/example/claude/originalRequestBody1762501795790.json create mode 100644 src/example/claude/originalRequestBody1762501964743.json create mode 100644 src/example/claude/responseChunk1762590717239.json create mode 100644 src/example/gemini/oldResponse1762502386432.json create mode 100644 src/example/gemini/oldResponseChunk1762502078640.json create mode 100644 src/example/gemini/originalRequestBody1762502075001.json create mode 100644 src/example/gemini/originalRequestBody1762502375803.json create mode 100644 src/example/openai/oldResponse1762501667708.json create mode 100644 src/example/openai/oldResponseChunk1762501528837.json create mode 100644 src/example/openai/originalRequestBody1762501497434.json create mode 100644 src/example/openai/originalRequestBody1762501665376.json create mode 100644 src/example/openaiResponses/oldResponse1762502706486.json create mode 100644 src/example/openaiResponses/oldResponseChunk1762500869734.json create mode 100644 src/example/openaiResponses/originalRequestBody1762500856556.json diff --git a/src/api-server.js b/src/api-server.js index 697e94f..211da40 100644 --- a/src/api-server.js +++ b/src/api-server.js @@ -111,6 +111,7 @@ import { promises as pfs } from 'fs'; import 'dotenv/config'; // Import dotenv and configure it import deepmerge from 'deepmerge'; +import './converters/register-converters.js'; // 注册所有转换器 import { getServiceAdapter, serviceInstances } from './adapter.js'; import { ProviderPoolManager } from './provider-pool-manager.js'; import { @@ -449,8 +450,7 @@ async function initApiService(config) { if (config.providerPools && Object.keys(config.providerPools).length > 0) { providerPoolManager = new ProviderPoolManager(config.providerPools, { globalConfig: config }); console.log('[Initialization] ProviderPoolManager initialized with configured pools.'); - // 可以选择在这里触发一次健康检查 - providerPoolManager.performHealthChecks(true); + // 健康检查将在服务器完全启动后执行 } else { console.log('[Initialization] No provider pools configured. Using single provider mode.'); } @@ -505,7 +505,7 @@ function logProviderSpecificDetails(provider, config) { } else { console.log(` [gemini-cli-oauth] OAuth Creds: Default discovery`); } - console.log(` [gemini-cli-oauth] Project ID: ${config.PROJECT_ID || 'Auto-discovered'}`); + // console.log(` [gemini-cli-oauth] Project ID: ${config.PROJECT_ID || 'Auto-discovered'}`); break; case MODEL_PROVIDER.KIRO_API: if (config.KIRO_OAUTH_CREDS_FILE_PATH) { @@ -743,6 +743,11 @@ async function startServer() { // 每 CRON_NEAR_MINUTES 分钟执行一次心跳日志和令牌刷新 setInterval(heartbeatAndRefreshToken, CONFIG.CRON_NEAR_MINUTES * 60 * 1000); } + // 服务器完全启动后,执行初始健康检查 + if (providerPoolManager) { + console.log('[Initialization] Performing initial health checks for provider pools...'); + providerPoolManager.performHealthChecks(true); + } }); return server; // Return the server instance for testing purposes } diff --git a/src/common.js b/src/common.js index 2fb58c0..a53b2fd 100644 --- a/src/common.js +++ b/src/common.js @@ -68,33 +68,6 @@ export function formatExpiryTime(expiryTimestamp) { return `${pad(hours)}h ${pad(minutes)}m ${pad(seconds)}s`; } -/** - * Ensures that all content parts in a request body have a 'role' property. - * If 'systemInstruction' is present and lacks a role, it defaults to 'user'. - * If any 'contents' entry lacks a role, it defaults to 'user'. - * @param {Object} requestBody - The request body object. - * @returns {Object} The modified request body with roles ensured. - */ -export function ensureRolesInContents(requestBody) { - if (requestBody.system_instruction) { - requestBody.systemInstruction = requestBody.system_instruction; - delete requestBody.system_instruction; - } - - if (requestBody.systemInstruction && !requestBody.systemInstruction.role) { - requestBody.systemInstruction.role = 'user'; - } - - if (requestBody.contents && Array.isArray(requestBody.contents)) { - requestBody.contents.forEach(content => { - if (!content.role) { - content.role = 'user'; - } - }); - } - return requestBody; -} - /** * Reads the entire request body from an HTTP request. * @param {http.IncomingMessage} req - The HTTP request object. @@ -213,29 +186,23 @@ export async function handleStreamRequest(res, service, model, requestBody, from // fs.writeFile('request'+Date.now()+'.json', JSON.stringify(requestBody)); // The service returns a stream in its native format (toProvider). - const nativeStream = await service.generateContentStream(model, requestBody); const needsConversion = getProtocolPrefix(fromProvider) !== getProtocolPrefix(toProvider); + requestBody.model = model; + const nativeStream = await service.generateContentStream(model, requestBody); const addEvent = getProtocolPrefix(fromProvider) === MODEL_PROTOCOL_PREFIX.CLAUDE || getProtocolPrefix(fromProvider) === MODEL_PROTOCOL_PREFIX.OPENAI_RESPONSES; const openStop = getProtocolPrefix(fromProvider) === MODEL_PROTOCOL_PREFIX.OPENAI ; - const openResponses = getProtocolPrefix(fromProvider) === MODEL_PROTOCOL_PREFIX.OPENAI_RESPONSES ; try { - if (openResponses && needsConversion) { - const beginChunks = getOpenAIResponsesStreamChunkBegin(model); - for (const chunk of beginChunks) { - res.write(`event: ${chunk.type}\n`); - res.write(`data: ${JSON.stringify(chunk)}\n\n`); - } - } for await (const nativeChunk of nativeStream) { - // Convert chunk to the client's format (fromProvider), if necessary. + // Extract text for logging purposes const chunkText = extractResponseText(nativeChunk, toProvider); if (chunkText && !Array.isArray(chunkText)) { fullResponseText += chunkText; } + // Convert the complete chunk object to the client's format (fromProvider), if necessary. const chunkToSend = needsConversion - ? convertData(chunkText, 'streamChunk', toProvider, fromProvider, model) + ? convertData(nativeChunk, 'streamChunk', toProvider, fromProvider, model) : nativeChunk; if (!chunkToSend) { @@ -247,6 +214,7 @@ export async function handleStreamRequest(res, service, model, requestBody, from for (const chunk of chunksToSend) { if (addEvent) { + // fullOldResponseJson += chunk.type+"\n"; // fullResponseJson += chunk.type+"\n"; res.write(`event: ${chunk.type}\n`); // console.log(`event: ${chunk.type}\n`); @@ -258,13 +226,6 @@ export async function handleStreamRequest(res, service, model, requestBody, from // console.log(`data: ${JSON.stringify(chunk)}\n`); } } - if (openResponses && needsConversion) { - const endChunks = getOpenAIResponsesStreamChunkEnd(model); - for (const chunk of endChunks) { - res.write(`event: ${chunk.type}\n`); - res.write(`data: ${JSON.stringify(chunk)}\n\n`); - } - } if (openStop && needsConversion) { res.write(`data: ${JSON.stringify(getOpenAIStreamChunkStop(model))}\n\n`); // console.log(`data: ${JSON.stringify(getOpenAIStreamChunkStop(model))}\n`); @@ -291,20 +252,23 @@ export async function handleStreamRequest(res, service, model, requestBody, from res.end(); } await logConversation('output', fullResponseText, PROMPT_LOG_MODE, PROMPT_LOG_FILENAME); - // fs.writeFile('oldResponse'+Date.now()+'.json', fullOldResponseJson); - // fs.writeFile('response'+Date.now()+'.json', fullResponseJson); + // fs.writeFile('oldResponseChunk'+Date.now()+'.json', fullOldResponseJson); + // fs.writeFile('responseChunk'+Date.now()+'.json', fullResponseJson); } } 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). + const needsConversion = getProtocolPrefix(fromProvider) !== getProtocolPrefix(toProvider); + requestBody.model = model; + // fs.writeFile('oldRequest'+Date.now()+'.json', JSON.stringify(requestBody)); const nativeResponse = await service.generateContent(model, requestBody); const responseText = extractResponseText(nativeResponse, toProvider); // Convert the response back to the client's format (fromProvider), if necessary. let clientResponse = nativeResponse; - if (getProtocolPrefix(fromProvider) !== getProtocolPrefix(toProvider)) { + if (needsConversion) { console.log(`[Response Convert] Converting response from ${toProvider} to ${fromProvider}`); clientResponse = convertData(nativeResponse, 'response', toProvider, fromProvider, model); } @@ -312,6 +276,7 @@ export async function handleUnaryRequest(res, service, model, requestBody, fromP //console.log(`[Response] Sending response to client: ${JSON.stringify(clientResponse)}`); await handleUnifiedResponse(res, JSON.stringify(clientResponse), false); await logConversation('output', responseText, PROMPT_LOG_MODE, PROMPT_LOG_FILENAME); + // fs.writeFile('oldResponse'+Date.now()+'.json', JSON.stringify(clientResponse)); } catch (error) { console.error('\n[Server] Error during unary processing:', error.stack); if (providerPoolManager) { diff --git a/src/convert-old.js b/src/convert-old.js new file mode 100644 index 0000000..a4a4ae0 --- /dev/null +++ b/src/convert-old.js @@ -0,0 +1,2571 @@ +import { v4 as uuidv4 } from 'uuid'; +import { MODEL_PROTOCOL_PREFIX, getProtocolPrefix } from './common.js'; +import { + streamStateManager, + generateResponseCreated, + generateResponseInProgress, + generateOutputItemAdded, + generateContentPartAdded, + generateOutputTextDelta, + generateOutputTextDone, + generateContentPartDone, + generateOutputItemDone, + generateResponseCompleted +} from './openai/openai-responses-core.mjs'; + +// ============================================================================= +// 常量和辅助函数定义 +// ============================================================================= + +// 定义默认常量 +const DEFAULT_MAX_TOKENS = 8192; +const DEFAULT_GEMINI_MAX_TOKENS = 65535; +const DEFAULT_TEMPERATURE = 1; +const DEFAULT_TOP_P = 0.95; + +// 辅助函数:判断值是否为 undefined 或 0,并返回默认值 +function checkAndAssignOrDefault(value, defaultValue) { + if (value !== undefined && value !== 0) { + return value; + } + return defaultValue; +} + +/** + * 映射结束原因 + * @param {string} reason - 结束原因 + * @param {string} sourceFormat - 源格式 + * @param {string} targetFormat - 目标格式 + * @returns {string} 映射后的结束原因 + */ +function _mapFinishReason(reason, sourceFormat, targetFormat) { + const reasonMappings = { + openai: { + anthropic: { + stop: "end_turn", + length: "max_tokens", + content_filter: "stop_sequence", + tool_calls: "tool_use" + } + }, + gemini: { + anthropic: { + // 旧版本大写格式 + STOP: "end_turn", + MAX_TOKENS: "max_tokens", + SAFETY: "stop_sequence", + RECITATION: "stop_sequence", + // 新版本小写格式(v1beta/v1 API) + stop: "end_turn", + length: "max_tokens", + safety: "stop_sequence", + recitation: "stop_sequence", + other: "end_turn" + } + } + }; + + try { + return reasonMappings[sourceFormat][targetFormat][reason] || "end_turn"; + } catch (e) { + return "end_turn"; + } +} + +/** + * 递归清理Gemini不支持的JSON Schema属性 + * @param {Object} schema - JSON Schema + * @returns {Object} 清理后的JSON Schema + */ +function _cleanJsonSchemaProperties(schema) { + if (!schema || typeof schema !== 'object') { + return schema; + } + + // 移除所有非标准属性 + const sanitized = {}; + for (const [key, value] of Object.entries(schema)) { + if (["type", "description", "properties", "required", "enum", "items"].includes(key)) { + sanitized[key] = value; + } + } + + if (sanitized.properties && typeof sanitized.properties === 'object') { + const cleanProperties = {}; + for (const [propName, propSchema] of Object.entries(sanitized.properties)) { + cleanProperties[propName] = _cleanJsonSchemaProperties(propSchema); + } + sanitized.properties = cleanProperties; + } + + if (sanitized.items) { + sanitized.items = _cleanJsonSchemaProperties(sanitized.items); + } + + return sanitized; +} + +/** + * 根据budget_tokens智能判断OpenAI reasoning_effort等级 + * @param {number|null} budgetTokens - Anthropic thinking的budget_tokens值 + * @returns {string} OpenAI reasoning_effort等级 ("low", "medium", "high") + */ +function _determineReasoningEffortFromBudget(budgetTokens) { + // 如果没有提供budget_tokens,默认为high + if (budgetTokens === null || budgetTokens === undefined) { + console.info("No budget_tokens provided, defaulting to reasoning_effort='high'"); + return "high"; + } + + // 使用固定阈值替代环境变量 + const LOW_THRESHOLD = 50; // 低推理努力的阈值 + const HIGH_THRESHOLD = 200; // 高推理努力的阈值 + + console.debug(`Threshold configuration: low <= ${LOW_THRESHOLD}, medium <= ${HIGH_THRESHOLD}, high > ${HIGH_THRESHOLD}`); + + let effort; + if (budgetTokens <= LOW_THRESHOLD) { + effort = "low"; + } else if (budgetTokens <= HIGH_THRESHOLD) { + effort = "medium"; + } else { + effort = "high"; + } + + console.info(`🎯 Budget tokens ${budgetTokens} -> reasoning_effort '${effort}' (thresholds: low<=${LOW_THRESHOLD}, high<=${HIGH_THRESHOLD})`); + return effort; +} + +// 全局工具状态管理器 +class ToolStateManager { + constructor() { + if (ToolStateManager.instance) { + return ToolStateManager.instance; + } + ToolStateManager.instance = this; + this._toolMappings = {}; + return this; + } + + // 存储工具名到ID的映射 + storeToolMapping(funcName, toolId) { + this._toolMappings[funcName] = toolId; + } + + // 根据工具名获取ID + getToolId(funcName) { + return this._toolMappings[funcName] || null; + } + + // 清除所有映射 + clearMappings() { + this._toolMappings = {}; + } +} + +// 全局工具状态管理器实例 +const toolStateManager = new ToolStateManager(); + +// ============================================================================= +// 主转换函数 +// ============================================================================= + +/** + * Generic data conversion function. + * @param {object} data - The data to convert (request body or response). + * @param {string} type - The type of conversion: 'request', 'response', 'streamChunk', 'modelList'. + * @param {string} fromProvider - The source model provider (e.g., MODEL_PROVIDER.GEMINI_CLI). + * @param {string} toProvider - The target model provider (e.g., MODEL_PROVIDER.OPENAI_CUSTOM). + * @param {string} [model] - Optional model name for response conversions. + * @returns {object} The converted data. + * @throws {Error} If no suitable conversion function is found. + */ +export function convertData(data, type, fromProvider, toProvider, model) { + // Define a map of conversion functions using protocol prefixes + const conversionMap = { + request: { + [MODEL_PROTOCOL_PREFIX.OPENAI]: { // to OpenAI protocol + [MODEL_PROTOCOL_PREFIX.GEMINI]: toOpenAIRequestFromGemini, // from Gemini protocol + [MODEL_PROTOCOL_PREFIX.CLAUDE]: toOpenAIRequestFromClaude, // from Claude protocol + }, + [MODEL_PROTOCOL_PREFIX.CLAUDE]: { // to Claude protocol + [MODEL_PROTOCOL_PREFIX.OPENAI]: toClaudeRequestFromOpenAI, // from OpenAI protocol + [MODEL_PROTOCOL_PREFIX.OPENAI_RESPONSES]: toClaudeRequestFromOpenAIResponses, // from OpenAI protocol (Responses format) + }, + [MODEL_PROTOCOL_PREFIX.GEMINI]: { // to Gemini protocol + [MODEL_PROTOCOL_PREFIX.OPENAI]: toGeminiRequestFromOpenAI, // from OpenAI protocol + [MODEL_PROTOCOL_PREFIX.CLAUDE]: toGeminiRequestFromClaude, // from Claude protocol + [MODEL_PROTOCOL_PREFIX.OPENAI_RESPONSES]: toGeminiRequestFromOpenAIResponses, // from OpenAI protocol (Responses format) + }, + }, + response: { + [MODEL_PROTOCOL_PREFIX.OPENAI]: { // to OpenAI protocol + [MODEL_PROTOCOL_PREFIX.GEMINI]: toOpenAIChatCompletionFromGemini, // from Gemini protocol + [MODEL_PROTOCOL_PREFIX.CLAUDE]: toOpenAIChatCompletionFromClaude, // from Claude protocol + }, + [MODEL_PROTOCOL_PREFIX.CLAUDE]: { // to Claude protocol + [MODEL_PROTOCOL_PREFIX.GEMINI]: toClaudeChatCompletionFromGemini, // from Gemini protocol + [MODEL_PROTOCOL_PREFIX.OPENAI]: toClaudeChatCompletionFromOpenAI, // from OpenAI protocol + }, + [MODEL_PROTOCOL_PREFIX.OPENAI_RESPONSES]: { // to OpenAI protocol (Responses format) + [MODEL_PROTOCOL_PREFIX.GEMINI]: toOpenAIResponsesFromGemini, // from Gemini protocol + [MODEL_PROTOCOL_PREFIX.CLAUDE]: toOpenAIResponsesFromClaude, // from Claude protocol + }, + }, + streamChunk: { + [MODEL_PROTOCOL_PREFIX.OPENAI]: { // to OpenAI protocol + [MODEL_PROTOCOL_PREFIX.GEMINI]: toOpenAIStreamChunkFromGemini, // from Gemini protocol + [MODEL_PROTOCOL_PREFIX.CLAUDE]: toOpenAIStreamChunkFromClaude, // from Claude protocol + }, + [MODEL_PROTOCOL_PREFIX.CLAUDE]: { // to Claude protocol + [MODEL_PROTOCOL_PREFIX.GEMINI]: toClaudeStreamChunkFromGemini, // from Gemini protocol + [MODEL_PROTOCOL_PREFIX.OPENAI]: toClaudeStreamChunkFromOpenAI, // from OpenAI protocol + }, + [MODEL_PROTOCOL_PREFIX.OPENAI_RESPONSES]: { // to OpenAI protocol (Responses format) + [MODEL_PROTOCOL_PREFIX.GEMINI]: toOpenAIResponsesStreamChunkFromGemini, // from Gemini protocol + [MODEL_PROTOCOL_PREFIX.CLAUDE]: toOpenAIResponsesStreamChunkFromClaude, // from Claude protocol + }, + }, + modelList: { + [MODEL_PROTOCOL_PREFIX.OPENAI]: { // to OpenAI protocol + [MODEL_PROTOCOL_PREFIX.GEMINI]: toOpenAIModelListFromGemini, // from Gemini protocol + [MODEL_PROTOCOL_PREFIX.CLAUDE]: toOpenAIModelListFromClaude, // from Claude protocol + }, + [MODEL_PROTOCOL_PREFIX.CLAUDE]: { // to Claude protocol + [MODEL_PROTOCOL_PREFIX.GEMINI]: toClaudeModelListFromGemini, // from Gemini protocol + [MODEL_PROTOCOL_PREFIX.OPENAI]: toClaudeModelListFromOpenAI, // from OpenAI protocol + }, + } + }; + + const targetConversions = conversionMap[type]; + if (!targetConversions) { + throw new Error(`Unsupported conversion type: ${type}`); + } + + const toConversions = targetConversions[getProtocolPrefix(toProvider)]; + if (!toConversions) { + throw new Error(`No conversions defined for target protocol: ${getProtocolPrefix(toProvider)} for type: ${type}`); + } + + const conversionFunction = toConversions[getProtocolPrefix(fromProvider)]; + if (!conversionFunction) { + throw new Error(`No conversion function found from ${getProtocolPrefix(fromProvider)} to ${toProvider} for type: ${type}`); + } + + console.log(conversionFunction); + if (type === 'response' || type === 'streamChunk' || type === 'modelList') { + return conversionFunction(data, model); + } else { + return conversionFunction(data); + } +} + +// ============================================================================= +// OpenAI 相关转换函数 +// ============================================================================= + +/** + * Converts a Gemini API request body to an OpenAI chat completion request body. + * Handles system instructions and role mapping with multimodal support. + * @param {Object} geminiRequest - The request body from the Gemini API. + * @returns {Object} The formatted request body for the OpenAI API. + */ +export function toOpenAIRequestFromGemini(geminiRequest) { + const openaiRequest = { + messages: [], + model: geminiRequest.model, // Default model if not specified in Gemini request + max_tokens: checkAndAssignOrDefault(geminiRequest.max_tokens, DEFAULT_MAX_TOKENS), + temperature: checkAndAssignOrDefault(geminiRequest.temperature, DEFAULT_TEMPERATURE), + top_p: checkAndAssignOrDefault(geminiRequest.top_p, DEFAULT_TOP_P), + }; + + // Process system instruction + if (geminiRequest.systemInstruction && Array.isArray(geminiRequest.systemInstruction.parts)) { + const systemContent = processGeminiPartsToOpenAIContent(geminiRequest.systemInstruction.parts); + if (systemContent) { + openaiRequest.messages.push({ + role: 'system', + content: systemContent + }); + } + } + + // Process contents + if (geminiRequest.contents && Array.isArray(geminiRequest.contents)) { + geminiRequest.contents.forEach(content => { + if (content && Array.isArray(content.parts)) { + const openaiContent = processGeminiPartsToOpenAIContent(content.parts); + if (openaiContent && openaiContent.length > 0) { + const openaiRole = content.role === 'model' ? 'assistant' : content.role; + openaiRequest.messages.push({ + role: openaiRole, + content: openaiContent + }); + } + } + }); + } + + return openaiRequest; +} + + +/** + * Processes Gemini parts to OpenAI content format with multimodal support. + * @param {Array} parts - Array of Gemini parts. + * @returns {Array|string} OpenAI content format. + */ +function processGeminiPartsToOpenAIContent(parts) { + if (!parts || !Array.isArray(parts)) return ''; + + const contentArray = []; + + parts.forEach(part => { + if (!part) return; + + // Handle text content + if (typeof part.text === 'string') { + contentArray.push({ + type: 'text', + text: part.text + }); + } + + // Handle inline data (images, audio) + if (part.inlineData) { + const { mimeType, data } = part.inlineData; + if (mimeType && data) { + contentArray.push({ + type: 'image_url', + image_url: { + url: `data:${mimeType};base64,${data}` + } + }); + } + } + + // Handle file data + if (part.fileData) { + const { mimeType, fileUri } = part.fileData; + if (mimeType && fileUri) { + // For file URIs, we need to determine if it's an image or audio + if (mimeType.startsWith('image/')) { + contentArray.push({ + type: 'image_url', + image_url: { + url: fileUri + } + }); + } else if (mimeType.startsWith('audio/')) { + // For audio, we'll use a placeholder or handle as text description + contentArray.push({ + type: 'text', + text: `[Audio file: ${fileUri}]` + }); + } + } + } + }); + + // Return as array for multimodal, or string for simple text + return contentArray.length === 1 && contentArray[0].type === 'text' + ? contentArray[0].text + : contentArray; +} + +export function toOpenAIModelListFromGemini(geminiModels) { + return { + object: "list", + data: geminiModels.models.map(m => ({ + id: m.name.startsWith('models/') ? m.name.substring(7) : m.name, // 移除 'models/' 前缀作为 id + object: "model", + created: Math.floor(Date.now() / 1000), + owned_by: "google", + })), + }; +} + +export function toOpenAIChatCompletionFromGemini(geminiResponse, model) { + const content = processGeminiResponseContent(geminiResponse); + + return { + id: `chatcmpl-${uuidv4()}`, + object: "chat.completion", + created: Math.floor(Date.now() / 1000), + model: model, + choices: [{ + index: 0, + message: { + role: "assistant", + content: content + }, + finish_reason: "stop", + }], + usage: geminiResponse.usageMetadata ? { + prompt_tokens: geminiResponse.usageMetadata.promptTokenCount || 0, + completion_tokens: geminiResponse.usageMetadata.candidatesTokenCount || 0, + total_tokens: geminiResponse.usageMetadata.totalTokenCount || 0, + } : { + prompt_tokens: 0, + completion_tokens: 0, + total_tokens: 0, + }, + }; +} + +/** + * Processes Gemini response content to OpenAI format with multimodal support. + * @param {Object} geminiResponse - The Gemini API response. + * @returns {string|Array} Processed content. + */ +function processGeminiResponseContent(geminiResponse) { + if (!geminiResponse || !geminiResponse.candidates) return ''; + + const contents = []; + + geminiResponse.candidates.forEach(candidate => { + if (candidate.content && candidate.content.parts) { + candidate.content.parts.forEach(part => { + if (part.text) { + contents.push(part.text); + } + // Note: Gemini response typically doesn't include multimodal content in responses + // but we handle it for completeness + }); + } + }); + + return contents.join('\n'); +} + +export function toOpenAIStreamChunkFromGemini(geminiChunk, model) { + return { + id: `chatcmpl-${uuidv4()}`, // uuidv4 needs to be imported or handled + object: "chat.completion.chunk", + created: Math.floor(Date.now() / 1000), + model: model, + choices: [{ + index: 0, + delta: { content: geminiChunk }, + finish_reason: null, + }], + usage: geminiChunk.usageMetadata ? { + prompt_tokens: geminiChunk.usageMetadata.promptTokenCount || 0, + completion_tokens: geminiChunk.usageMetadata.candidatesTokenCount || 0, + total_tokens: geminiChunk.usageMetadata.totalTokenCount || 0, + } : { + prompt_tokens: 0, + completion_tokens: 0, + total_tokens: 0, + }, + }; +} + +/** + * Converts a Claude API messages response to an OpenAI chat completion response. + * @param {Object} claudeResponse - The Claude API messages response object. + * @param {string} model - The model name to include in the response. + * @returns {Object} The formatted OpenAI chat completion response. + */ +export function toOpenAIChatCompletionFromClaude(claudeResponse, model) { + if (!claudeResponse || !claudeResponse.content || claudeResponse.content.length === 0) { + return { + id: `chatcmpl-${uuidv4()}`, + object: "chat.completion", + created: Math.floor(Date.now() / 1000), + model: model, + choices: [{ + index: 0, + message: { + role: "assistant", + content: "", + }, + finish_reason: "stop", + }], + usage: { + 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), + }, + }; + } + + const content = processClaudeResponseContent(claudeResponse.content); + const finishReason = claudeResponse.stop_reason === 'end_turn' ? 'stop' : claudeResponse.stop_reason; + + return { + id: `chatcmpl-${uuidv4()}`, + object: "chat.completion", + created: Math.floor(Date.now() / 1000), + model: model, + choices: [{ + index: 0, + message: { + role: "assistant", + content: content + }, + finish_reason: finishReason, + }], + usage: { + 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), + }, + }; +} + +/** + * Processes Claude response content to OpenAI format with multimodal support. + * @param {Array} content - Array of Claude content blocks. + * @returns {string|Array} Processed content. + */ +function processClaudeResponseContent(content) { + if (!content || !Array.isArray(content)) return ''; + + const contentArray = []; + + content.forEach(block => { + if (!block) return; + + switch (block.type) { + case 'text': + contentArray.push({ + type: 'text', + text: block.text || '' + }); + break; + + case 'image': + // Handle image blocks from Claude + if (block.source && block.source.type === 'base64') { + contentArray.push({ + type: 'image_url', + image_url: { + url: `data:${block.source.media_type};base64,${block.source.data}` + } + }); + } + break; + + default: + // Handle other content types as text + if (block.text) { + contentArray.push({ + type: 'text', + text: block.text + }); + } + } + }); + + // Return as array for multimodal, or string for simple text + return contentArray.length === 1 && contentArray[0].type === 'text' + ? contentArray[0].text + : contentArray; +} + +/** + * Converts a Claude API messages stream chunk to an OpenAI chat completion stream chunk. + * Based on the official Claude Messages API stream events. + * @param {Object} claudeChunk - The Claude API messages stream chunk object. + * @param {string} [model] - Optional model name to include in the response. + * @returns {Object} The formatted OpenAI chat completion stream chunk, or an empty object for events that don't map. + */ +export function toOpenAIStreamChunkFromClaude(claudeChunk, model) { + if (!claudeChunk) { + return null; + } + return { + id: `chatcmpl-${uuidv4()}`, // uuidv4 needs to be imported or handled + object: "chat.completion.chunk", + created: Math.floor(Date.now() / 1000), + model: model, + system_fingerprint: "", + choices: [{ + index: 0, + delta: { + content: claudeChunk, + reasoning_content: "" + }, + finish_reason: !claudeChunk ? 'stop' : null, + message: { + content: claudeChunk, + reasoning_content: "" + } + }], + usage:{ + prompt_tokens: 0, + completion_tokens: 0, + total_tokens: 0, + }, + }; +} + +/** + * Converts a Claude API model list response to an OpenAI model list response. + * @param {Array} claudeModels - The array of model objects from Claude API. + * @returns {Object} The formatted OpenAI model list response. + */ +export function toOpenAIModelListFromClaude(claudeModels) { + return { + object: "list", + data: claudeModels.models.map(m => ({ + id: m.id || m.name, // Claude models might use 'name' instead of 'id' + object: "model", + created: Math.floor(Date.now() / 1000), // Claude may not provide 'created' timestamp + owned_by: "anthropic", + // You can add more properties here if they exist in Claude's model response + // and you want to map them to OpenAI's format, e.g., permissions. + })), + }; +} + +/** + * Converts an OpenAI chat completion response to a Claude API messages response. + * @param {Object} openaiResponse - The OpenAI API chat completion response object. + * @param {string} model - The model name to include in the response. + * @returns {Object} The formatted Claude API messages response. + */ +export function toClaudeChatCompletionFromOpenAI(openaiResponse, model) { + if (!openaiResponse || !openaiResponse.choices || openaiResponse.choices.length === 0) { + return { + id: `msg_${uuidv4()}`, + type: "message", + role: "assistant", + content: [], + model: model, + stop_reason: "end_turn", + stop_sequence: null, + usage: { + input_tokens: openaiResponse?.usage?.prompt_tokens || 0, + output_tokens: openaiResponse?.usage?.completion_tokens || 0 + } + }; + } + + const choice = openaiResponse.choices[0]; + const contentList = []; + + // Handle tool calls + const toolCalls = choice.message?.tool_calls || []; + for (const toolCall of toolCalls.filter(tc => tc && typeof tc === 'object')) { + if (toolCall.function) { + const func = toolCall.function; + const argStr = func.arguments || "{}"; + let argObj; + try { + argObj = typeof argStr === 'string' ? JSON.parse(argStr) : argStr; + } catch (e) { + argObj = {}; + } + contentList.push({ + type: "tool_use", + id: toolCall.id || "", + name: func.name || "", + input: argObj, + }); + } + } + + // Handle text content + const contentText = choice.message?.content || ""; + if (contentText) { + // 使用 _extractThinkingFromOpenAIText 提取 thinking 内容 + const extractedContent = _extractThinkingFromOpenAIText(contentText); + if (Array.isArray(extractedContent)) { + contentList.push(...extractedContent); + } else { + contentList.push({ type: "text", text: extractedContent }); + } + } + + // Map OpenAI finish reason to Claude stop reason + const stopReason = _mapFinishReason( + choice.finish_reason || "stop", + "openai", + "anthropic" + ); + + return { + id: `msg_${uuidv4()}`, + type: "message", + role: "assistant", + content: contentList, + model: model, + stop_reason: stopReason, + stop_sequence: null, + usage: { + input_tokens: openaiResponse.usage?.prompt_tokens || 0, + output_tokens: openaiResponse.usage?.completion_tokens || 0 + } + }; +} + +/** + * Converts a Claude API request body to an OpenAI chat completion request body. + * Handles system instructions and multimodal content. + * @param {Object} claudeRequest - The request body from the Claude API. + * @returns {Object} The formatted request body for the OpenAI API. + */ +export function toOpenAIRequestFromClaude(claudeRequest) { + const openaiMessages = []; + let systemMessageContent = ''; + + // Add system message if present + if (claudeRequest.system) { + systemMessageContent = claudeRequest.system; + } + + // Process messages + if (claudeRequest.messages && Array.isArray(claudeRequest.messages)) { + const tempOpenAIMessages = []; + for (const msg of claudeRequest.messages) { + const role = msg.role; + + // 处理用户的工具结果消息 + if (role === "user" && Array.isArray(msg.content)) { + const hasToolResult = msg.content.some( + item => item && typeof item === 'object' && item.type === "tool_result" + ); + + if (hasToolResult) { + for (const item of msg.content) { + if (item && typeof item === 'object' && item.type === "tool_result") { + const toolUseId = item.tool_use_id || item.id || ""; + const contentStr = String(item.content || ""); + tempOpenAIMessages.push({ + role: "tool", + tool_call_id: toolUseId, + content: contentStr, + }); + } + } + continue; // 已处理工具结果,跳过后续处理 + } + } + + // 处理 assistant 消息中的工具调用 + if (role === "assistant" && Array.isArray(msg.content) && msg.content.length > 0) { + const firstPart = msg.content[0]; + if (firstPart.type === "tool_use") { + const funcName = firstPart.name || ""; + const funcArgs = firstPart.input || {}; + tempOpenAIMessages.push({ + role: "assistant", + content: '', + tool_calls: [ + { + id: firstPart.id || `call_${funcName}_1`, + type: "function", + function: { + name: funcName, + arguments: JSON.stringify(funcArgs) + }, + index: firstPart.index || 0 + } + ] + }); + continue; // 已处理 + } + } + + // 普通文本消息 + const contentConverted = processClaudeContentToOpenAIContent(msg.content || ""); + // 跳过空消息,避免在历史中插入空字符串导致模型误判 + if (contentConverted && (Array.isArray(contentConverted) ? contentConverted.length > 0 : contentConverted.trim().length > 0)) { + tempOpenAIMessages.push({ + role: role, + content: contentConverted + }); + } + } + + // ---------------- OpenAI 兼容性校验 ---------------- + // 确保所有 assistant.tool_calls 均有后续 tool 响应消息;否则移除不匹配的 tool_call + const validatedMessages = []; + for (let idx = 0; idx < tempOpenAIMessages.length; idx++) { + const m = tempOpenAIMessages[idx]; + if (m.role === "assistant" && m.tool_calls) { + const callIds = m.tool_calls.map(tc => tc.id).filter(id => id); + // 统计后续是否有对应的 tool 消息 + let unmatched = new Set(callIds); + for (let laterIdx = idx + 1; laterIdx < tempOpenAIMessages.length; laterIdx++) { + const later = tempOpenAIMessages[laterIdx]; + if (later.role === "tool" && unmatched.has(later.tool_call_id)) { + unmatched.delete(later.tool_call_id); + } + if (unmatched.size === 0) { + break; + } + } + if (unmatched.size > 0) { + // 移除无匹配的 tool_call + m.tool_calls = m.tool_calls.filter(tc => !unmatched.has(tc.id)); + // 如果全部被移除,则降级为普通 assistant 文本消息 + if (m.tool_calls.length === 0) { + delete m.tool_calls; + if (m.content === null) { + m.content = ""; + } + } + } + } + validatedMessages.push(m); + } + openaiMessages.push(...validatedMessages); + } + + const openaiRequest = { + model: claudeRequest.model, // Default OpenAI model + messages: openaiMessages, + max_tokens: checkAndAssignOrDefault(claudeRequest.max_tokens, DEFAULT_MAX_TOKENS), + temperature: checkAndAssignOrDefault(claudeRequest.temperature, DEFAULT_TEMPERATURE), + top_p: checkAndAssignOrDefault(claudeRequest.top_p, DEFAULT_TOP_P), + stream: claudeRequest.stream, // Stream mode is handled by different endpoint + }; + + // Process tools + if (claudeRequest.tools) { + const openaiTools = []; + for (const tool of claudeRequest.tools) { + openaiTools.push({ + type: "function", + function: { + name: tool.name || "", + description: tool.description || "", + parameters: _cleanJsonSchemaProperties(tool.input_schema || {}) // 使用清理函数 + } + }); + } + openaiRequest.tools = openaiTools; + openaiRequest.tool_choice = "auto"; + } + + // 处理思考预算转换 (Anthropic thinking -> OpenAI reasoning_effort + max_completion_tokens) + if (claudeRequest.thinking && claudeRequest.thinking.type === "enabled") { + const budgetTokens = claudeRequest.thinking.budget_tokens; + // 根据budget_tokens智能判断reasoning_effort等级 + const reasoningEffort = _determineReasoningEffortFromBudget(budgetTokens); + openaiRequest.reasoning_effort = reasoningEffort; + + // 处理max_completion_tokens的优先级逻辑 + let maxCompletionTokens = null; + + // 优先级1:客户端传入的max_tokens + if (claudeRequest.max_tokens !== undefined) { + maxCompletionTokens = claudeRequest.max_tokens; + delete openaiRequest.max_tokens; // 移除max_tokens,使用max_completion_tokens + console.info(`Using client max_tokens as max_completion_tokens: ${maxCompletionTokens}`); + } else { + // 优先级2:环境变量OPENAI_REASONING_MAX_TOKENS + const envMaxTokens = process.env.OPENAI_REASONING_MAX_TOKENS; + if (envMaxTokens) { + try { + maxCompletionTokens = parseInt(envMaxTokens, 10); + console.info(`Using OPENAI_REASONING_MAX_TOKENS from environment: ${maxCompletionTokens}`); + } catch (e) { + console.warn(`Invalid OPENAI_REASONING_MAX_TOKENS value '${envMaxTokens}', must be integer`); + } + } + + if (!envMaxTokens) { + // 优先级3:都没有则报错 + throw new Error("For OpenAI reasoning models, max_completion_tokens is required. Please specify max_tokens in the request or set OPENAI_REASONING_MAX_TOKENS environment variable."); + } + } + openaiRequest.max_completion_tokens = maxCompletionTokens; + console.info(`Anthropic thinking enabled -> OpenAI reasoning_effort='${reasoningEffort}', max_completion_tokens=${maxCompletionTokens}`); + if (budgetTokens) { + console.info(`Budget tokens: ${budgetTokens} -> reasoning_effort: '${reasoningEffort}'`); + } + } + + // Add system message at the beginning if present + if (systemMessageContent) { + let stringifiedSystemMessageContent = systemMessageContent; + if(Array.isArray(systemMessageContent)){ + stringifiedSystemMessageContent = systemMessageContent.map(item => + typeof item === 'string' ? item : item.text).join('\n'); + } + openaiRequest.messages.unshift({ role: 'system', content: stringifiedSystemMessageContent }); + } + + return openaiRequest; +} + + +/** + * Processes Claude content to OpenAI content format with multimodal support. + * @param {Array} content - Array of Claude content blocks. + * @returns {Array} OpenAI content format. + */ +function processClaudeContentToOpenAIContent(content) { + if (!content || !Array.isArray(content)) return []; + + const contentArray = []; + + content.forEach(block => { + if (!block) return; + + switch (block.type) { + case 'text': + if (block.text) { + contentArray.push({ + type: 'text', + text: block.text + }); + } + break; + + case 'image': + // Handle image blocks from Claude + if (block.source && block.source.type === 'base64') { + contentArray.push({ + type: 'image_url', + image_url: { + url: `data:${block.source.media_type};base64,${block.source.data}` + } + }); + } + break; + + case 'tool_use': + // Handle tool use as text + contentArray.push({ + type: 'text', + text: `[Tool use: ${block.name}]` + }); + break; + + case 'tool_result': + // Handle tool results as text + contentArray.push({ + type: 'text', + text: typeof block.content === 'string' ? block.content : JSON.stringify(block.content) + }); + break; + + default: + // Handle any other content types as text + if (block.text) { + contentArray.push({ + type: 'text', + text: block.text + }); + } + } + }); + + return contentArray; +} + +// ============================================================================= +// Gemini 相关转换函数 +// ============================================================================= + +/** + * Converts an OpenAI chat completion request body to a Gemini API request body. + * Handles system instructions and merges consecutive messages of the same role with multimodal support. + * @param {Object} openaiRequest - The request body from the OpenAI API. + * @returns {Object} The formatted request body for the Gemini API. + */ +export function toGeminiRequestFromOpenAI(openaiRequest) { + const messages = openaiRequest.messages || []; + const { systemInstruction, nonSystemMessages } = extractAndProcessSystemMessages(messages); + + // Process messages with role conversion and multimodal support + const processedMessages = []; + let lastMessage = null; + + for (const message of nonSystemMessages) { + const geminiRole = message.role === 'assistant' ? 'model' : message.role; + + // Handle tool responses + if (geminiRole === 'tool') { + if (lastMessage) processedMessages.push(lastMessage); + processedMessages.push({ + role: 'function', + parts: [{ + functionResponse: { + name: message.name, + response: { content: safeParseJSON(message.content) } + } + }] + }); + lastMessage = null; + continue; + } + + // Process multimodal content + const processedContent = processOpenAIContentToGeminiParts(message.content); + + // Merge consecutive text messages + if (lastMessage && lastMessage.role === geminiRole && !message.tool_calls && + Array.isArray(processedContent) && processedContent.every(p => p.text) && + Array.isArray(lastMessage.parts) && lastMessage.parts.every(p => p.text)) { + lastMessage.parts.push(...processedContent); + continue; + } + + if (lastMessage) processedMessages.push(lastMessage); + lastMessage = { role: geminiRole, parts: processedContent }; + } + if (lastMessage) processedMessages.push(lastMessage); + + // Build Gemini request + const geminiRequest = { + contents: processedMessages.filter(item => item.parts && item.parts.length > 0) + }; + + if (systemInstruction) geminiRequest.systemInstruction = systemInstruction; + + // Handle tools + if (openaiRequest.tools?.length) { + geminiRequest.tools = [{ + functionDeclarations: openaiRequest.tools.map(t => { + // Ensure tool is a valid object and has function property + if (!t || typeof t !== 'object' || !t.function) { + console.warn("Skipping invalid tool declaration in openaiRequest.tools."); + return null; // Return null for invalid tools, filter out later + } + + const func = t.function; + // Clean parameters schema for Gemini compatibility + const parameters = _cleanJsonSchemaProperties(func.parameters || {}); + + return { + name: String(func.name || ''), // Ensure name is string + description: String(func.description || ''), // Ensure description is string + parameters: parameters // Use cleaned parameters + }; + }).filter(Boolean) // Filter out any nulls from invalid tool declarations + }]; + // If no valid functionDeclarations, remove the tools array + if (geminiRequest.tools[0].functionDeclarations.length === 0) { + delete geminiRequest.tools; + } + } + + if (openaiRequest.tool_choice) { + geminiRequest.toolConfig = buildToolConfig(openaiRequest.tool_choice); + } + + // Add generation config + const config = buildGenerationConfig(openaiRequest); + if (Object.keys(config).length) geminiRequest.generationConfig = config; + + // Validation + if (geminiRequest.contents[0]?.role !== 'user') { + console.warn(`[Request Conversion] Warning: Conversation does not start with a 'user' role.`); + } + + return geminiRequest; +} + +/** + * Processes OpenAI content to Gemini parts format with multimodal support. + * @param {string|Array} content - OpenAI message content. + * @returns {Array} Array of Gemini parts. + */ +function processOpenAIContentToGeminiParts(content) { + if (!content) return []; + + // Handle string content + if (typeof content === 'string') { + return [{ text: content }]; + } + + // Handle array content (multimodal) + if (Array.isArray(content)) { + const parts = []; + + content.forEach(item => { + if (!item) return; + + switch (item.type) { + case 'text': + if (item.text) { + parts.push({ text: item.text }); + } + break; + + case 'image_url': + if (item.image_url) { + const imageUrl = typeof item.image_url === 'string' + ? item.image_url + : item.image_url.url; + + if (imageUrl.startsWith('data:')) { + // Handle base64 data URL + const [header, data] = imageUrl.split(','); + const mimeType = header.match(/data:([^;]+)/)?.[1] || 'image/jpeg'; + parts.push({ + inlineData: { + mimeType, + data + } + }); + } else { + // Handle regular URL + parts.push({ + fileData: { + mimeType: 'image/jpeg', // Default MIME type + fileUri: imageUrl + } + }); + } + } + break; + + case 'audio': + // Handle audio content + if (item.audio_url) { + const audioUrl = typeof item.audio_url === 'string' + ? item.audio_url + : item.audio_url.url; + + if (audioUrl.startsWith('data:')) { + const [header, data] = audioUrl.split(','); + const mimeType = header.match(/data:([^;]+)/)?.[1] || 'audio/wav'; + parts.push({ + inlineData: { + mimeType, + data + } + }); + } else { + parts.push({ + fileData: { + mimeType: 'audio/wav', // Default MIME type + fileUri: audioUrl + } + }); + } + } + break; + } + }); + + return parts; + } + + return []; +} + +function safeParseJSON(str) { + if (!str) { + return str; + } + let cleanedStr = str; + + // 处理可能被截断的转义序列 + if (cleanedStr.endsWith('\\') && !cleanedStr.endsWith('\\\\')) { + cleanedStr = cleanedStr.substring(0, cleanedStr.length - 1); // 移除悬挂的反斜杠 + } else if (cleanedStr.endsWith('\\u') || cleanedStr.endsWith('\\u0') || cleanedStr.endsWith('\\u00')) { + // 不完整的Unicode转义序列 + const idx = cleanedStr.lastIndexOf('\\u'); + cleanedStr = cleanedStr.substring(0, idx); + } + + try { + return JSON.parse(cleanedStr || '{}'); + } catch (e) { + // 如果清理后仍然无法解析,则返回原始字符串或进行其他错误处理 + return str; + } +} + +function buildToolConfig(toolChoice) { + if (typeof toolChoice === 'string' && ['none', 'auto'].includes(toolChoice)) { + return { functionCallingConfig: { mode: toolChoice.toUpperCase() } }; + } + if (typeof toolChoice === 'object' && toolChoice.function) { + return { functionCallingConfig: { mode: 'ANY', allowedFunctionNames: [toolChoice.function.name] } }; + } + return null; +} + +/** + * 根据 tool_result 字段构造 Gemini functionResponse + * @param {Object} item - 工具结果项 + * @returns {Object|null} functionResponse 对象 + */ +function _buildFunctionResponse(item) { + if (!item || typeof item !== 'object') { + return null; + } + + // 判定是否为工具结果 + const isResult = ( + item.type === "tool_result" || + item.tool_use_id !== undefined || + item.tool_output !== undefined || + item.result !== undefined || + item.content !== undefined + ); + if (!isResult) { + return null; + } + + // 提取函数名 + let funcName = null; + + // 方法1:从映射表中获取(Anthropic格式) + const toolUseId = item.tool_use_id || item.id; + // 这里需要注意,AnthropicConverter内部维护的_toolUseMapping是类的私有属性,在convert.js中无法直接访问 + // 因此,这里需要依赖全局的toolStateManager + // if (toolUseId && this._toolUseMapping) { // 这行代码在convert.js中将无法使用 + // funcName = this._toolUseMapping[toolUseId]; + // } + + // 方法1.5:使用全局工具状态管理器 + if (!funcName && toolUseId) { + // 先尝试从ID中提取可能的函数名 + let potentialFuncName = null; + if (String(toolUseId).startsWith("call_")) { + const nameAndHash = toolUseId.substring(4); // 去掉 "call_" 前缀 + potentialFuncName = nameAndHash.substring(0, nameAndHash.lastIndexOf("_")); + } + + // 检查全局管理器中是否有对应的映射 + if (potentialFuncName) { + const storedId = toolStateManager.getToolId(potentialFuncName); + if (storedId === toolUseId) { + funcName = potentialFuncName; + } + } + } + + // 方法2:从 tool_use_id 中提取(OpenAI格式) + if (!funcName && toolUseId && String(toolUseId).startsWith("call_")) { + // 格式: call__ ,函数名可能包含多个下划线 + const nameAndHash = toolUseId.substring(4); // 去掉 "call_" 前缀 + funcName = nameAndHash.substring(0, nameAndHash.lastIndexOf("_")); // 去掉最后一个 hash 段 + } + + // 方法3:直接从字段获取 + if (!funcName) { + funcName = ( + item.tool_name || + item.name || + item.function_name + ); + } + + if (!funcName) { + return null; + } + + // 提取结果内容 + let funcResponse = null; + + // 尝试多个可能的结果字段 + for (const key of ["content", "tool_output", "output", "response", "result"]) { + if (item[key] !== undefined) { + funcResponse = item[key]; + break; + } + } + + // 如果 content 是列表,尝试提取文本 + if (Array.isArray(funcResponse) && funcResponse.length > 0) { + const textParts = funcResponse + .filter(p => p && typeof p === 'object' && p.type === "text") + .map(p => p.text || ""); + if (textParts.length > 0) { + funcResponse = textParts.join(""); + } + } + + // 确保有响应内容 + if (funcResponse === null || funcResponse === undefined) { + funcResponse = ""; + } + + // Gemini 要求 response 为 JSON 对象,若为原始字符串则包装 + if (typeof funcResponse !== 'object') { + funcResponse = { content: String(funcResponse) }; + } + + return { + functionResponse: { + name: funcName, + response: funcResponse + } + }; +} + +/** + * Converts a Gemini API model list response to a Claude API model list response. + * @param {Object} geminiModels - The Gemini API model list response object. + * @returns {Object} The formatted Claude API model list response. + */ +export function toClaudeModelListFromGemini(geminiModels) { + return { + models: geminiModels.models.map(m => ({ + name: m.name.startsWith('models/') ? m.name.substring(7) : m.name, // 移除 'models/' 前缀作为 name + // Claude models 可能包含其他字段,这里使用默认值 + description: "", // Gemini models 不提供描述 + // Claude API 可能需要其他字段,根据实际 API 文档调整 + })), + }; +} + +/** + * Converts an OpenAI API model list response to a Claude API model list response. + * @param {Object} openaiModels - The OpenAI API model list response object. + * @returns {Object} The formatted Claude API model list response. + */ +export function toClaudeModelListFromOpenAI(openaiModels) { + return { + models: openaiModels.data.map(m => ({ + name: m.id, // OpenAI 的 id 映射为 Claude 的 name + // Claude models 可能包含其他字段,这里使用默认值 + description: "", // OpenAI models 不提供描述 + // Claude API 可能需要其他字段,根据实际 API 文档调整 + })), + }; +} + +/** + * 从OpenAI文本中提取thinking内容,返回Anthropic格式的content blocks + * @param {string} text - 文本内容 + * @returns {string|Array} 提取后的内容 + */ +function _extractThinkingFromOpenAIText(text) { + // 匹配 ... 标签 + const thinkingPattern = /\s*(.*?)\s*<\/thinking>/gs; + const matches = [...text.matchAll(thinkingPattern)]; + + const contentBlocks = []; + let lastEnd = 0; + + for (const match of matches) { + // 添加thinking标签之前的文本(如果有) + const beforeText = text.substring(lastEnd, match.index).trim(); + if (beforeText) { + contentBlocks.push({ + type: "text", + text: beforeText + }); + } + + // 添加thinking内容 + const thinkingText = match[1].trim(); + if (thinkingText) { + contentBlocks.push({ + type: "thinking", + thinking: thinkingText + }); + } + + lastEnd = match.index + match[0].length; + } + + // 添加最后一个thinking标签之后的文本(如果有) + const afterText = text.substring(lastEnd).trim(); + if (afterText) { + contentBlocks.push({ + type: "text", + text: afterText + }); + } + + // 如果没有找到thinking标签,返回原文本 + if (contentBlocks.length === 0) { + return text; + } + + // 如果只有一个文本块,返回字符串 + if (contentBlocks.length === 1 && contentBlocks[0].type === "text") { + return contentBlocks[0].text; + } + + return contentBlocks; +} + +/** + * Converts an OpenAI chat completion stream chunk to a Claude API messages stream chunk. + * @param {Object} openaiChunk - The OpenAI API chat completion stream chunk object. + * @param {string} [model] - Optional model name to include in the response. + * @returns {Object} The formatted Claude API messages stream chunk. + */ +export function toClaudeStreamChunkFromOpenAI(openaiChunk, model) { + if (!openaiChunk) { + return null; + } + + // 工具调用 + if ( Array.isArray(openaiChunk)) { + const toolCall = openaiChunk[0]; // 假设每次只处理一个工具调用 + if (toolCall) { + if (toolCall.function && toolCall.function.name) { + const toolUseBlock = { + type: "tool_use", + id: toolCall.id || `call_${toolCall.function.name}_${Date.now()}`, + name: toolCall.function.name, + input: toolCall.function.arguments ? JSON.parse(toolCall.function.arguments) : {} + }; + return { type: "content_block_start", index: 1, content_block: toolUseBlock }; + } + } + } + + // 文本内容 + if (typeof openaiChunk === 'string') { + return { + type: "content_block_delta", + index: 0, + delta: { + type: "text_delta", + text: openaiChunk + } + }; + } + return null; +} + +function buildGenerationConfig({ temperature, max_tokens, top_p, stop }) { + const config = {}; + config.temperature = checkAndAssignOrDefault(temperature, DEFAULT_TEMPERATURE); + config.maxOutputTokens = checkAndAssignOrDefault(max_tokens, DEFAULT_GEMINI_MAX_TOKENS); + config.topP = checkAndAssignOrDefault(top_p, DEFAULT_TOP_P); + if (stop !== undefined) config.stopSequences = Array.isArray(stop) ? stop : [stop]; + return config; +} + +/** + * Converts an OpenAI chat completion request body to a Claude API request body. + * Handles system instructions, tool calls, and multimodal content. + * @param {Object} openaiRequest - The request body from the OpenAI API. + * @returns {Object} The formatted request body for the Claude API. + */ +export function toClaudeRequestFromOpenAI(openaiRequest) { + const messages = openaiRequest.messages || []; + const { systemInstruction, nonSystemMessages } = extractAndProcessSystemMessages(messages); + + const claudeMessages = []; + + for (const message of nonSystemMessages) { + const role = message.role === 'assistant' ? 'assistant' : 'user'; + let content = []; + + if (message.role === 'tool') { + // Claude expects tool_result to be in a 'user' message + // The content of a tool message is a single tool_result block + content.push({ + type: 'tool_result', + tool_use_id: message.tool_call_id, // Use tool_call_id from OpenAI tool message + content: safeParseJSON(message.content) // Parse content as JSON if possible + }); + claudeMessages.push({ role: 'user', content: content }); + } else if (message.role === 'assistant' && message.tool_calls?.length) { + // Assistant message with tool calls - properly format as tool_use blocks + // Claude expects tool_use to be in an 'assistant' message + const toolUseBlocks = message.tool_calls.map(tc => ({ + type: 'tool_use', + id: tc.id, + name: tc.function.name, + input: safeParseJSON(tc.function.arguments) + })); + claudeMessages.push({ role: 'assistant', content: toolUseBlocks }); + } else { + // Regular user or assistant message (text and multimodal) + if (typeof message.content === 'string') { + if (message.content) { + content.push({ type: 'text', text: message.content }); + } + } else if (Array.isArray(message.content)) { + message.content.forEach(item => { + if (!item) return; + switch (item.type) { + case 'text': + if (item.text) { + content.push({ type: 'text', text: item.text }); + } + break; + case 'image_url': + if (item.image_url) { + const imageUrl = typeof item.image_url === 'string' + ? item.image_url + : item.image_url.url; + if (imageUrl.startsWith('data:')) { + const [header, data] = imageUrl.split(','); + const mediaType = header.match(/data:([^;]+)/)?.[1] || 'image/jpeg'; + content.push({ + type: 'image', + source: { + type: 'base64', + media_type: mediaType, + data: data + } + }); + } else { + // Claude requires base64 for images, so for URLs, we'll represent as text + content.push({ type: 'text', text: `[Image: ${imageUrl}]` }); + } + } + break; + case 'audio': + // Handle audio content as text placeholder + if (item.audio_url) { + const audioUrl = typeof item.audio_url === 'string' + ? item.audio_url + : item.audio_url.url; + content.push({ type: 'text', text: `[Audio: ${audioUrl}]` }); + } + break; + } + }); + } + // Only add message if content is not empty + if (content.length > 0) { + claudeMessages.push({ role: role, content: content }); + } + } + } + + const claudeRequest = { + model: openaiRequest.model, + messages: claudeMessages, + max_tokens: checkAndAssignOrDefault(openaiRequest.max_tokens, DEFAULT_MAX_TOKENS), + temperature: checkAndAssignOrDefault(openaiRequest.temperature, DEFAULT_TEMPERATURE), + top_p: checkAndAssignOrDefault(openaiRequest.top_p, DEFAULT_TOP_P), + }; + + if (systemInstruction) { + claudeRequest.system = extractTextFromMessageContent(systemInstruction.parts[0].text); + } + + if (openaiRequest.tools?.length) { + claudeRequest.tools = openaiRequest.tools.map(t => ({ + name: t.function.name, + description: t.function.description || '', + input_schema: t.function.parameters || { type: 'object', properties: {} } + })); + claudeRequest.tool_choice = buildClaudeToolChoice(openaiRequest.tool_choice); + } + + return claudeRequest; +} + +function buildClaudeToolChoice(toolChoice) { + if (typeof toolChoice === 'string') { + const mapping = { auto: 'auto', none: 'none', required: 'any' }; + return { type: mapping[toolChoice] }; + } + if (typeof toolChoice === 'object' && toolChoice.function) { + return { type: 'tool', name: toolChoice.function.name }; + } + return undefined; +} + +/** + * Extracts and combines all 'system' role messages into a single system instruction. + * Filters out system messages and returns the remaining non-system messages. + * @param {Array} messages - Array of message objects from OpenAI request. + * @returns {{systemInstruction: Object|null, nonSystemMessages: Array}} + * An object containing the system instruction and an array of non-system messages. + */ +export function extractAndProcessSystemMessages(messages) { + const systemContents = []; + const nonSystemMessages = []; + + for (const message of messages) { + if (message.role === 'system') { + systemContents.push(extractTextFromMessageContent(message.content)); + } else { + nonSystemMessages.push(message); + } + } + + let systemInstruction = null; + if (systemContents.length > 0) { + systemInstruction = { + parts: [{ + text: systemContents.join('\n') + }] + }; + } + return { systemInstruction, nonSystemMessages }; +} + +/** + * Extracts text from various forms of message content. + * @param {string|Array} content - The content from a message object. + * @returns {string} The extracted text. + */ +export function extractTextFromMessageContent(content) { + if (typeof content === 'string') { + return content; + } + if (Array.isArray(content)) { + return content + .filter(part => part.type === 'text' && part.text) + .map(part => part.text) + .join('\n'); + } + return ''; +} + +/** + * Converts a Claude API request body to a Gemini API request body. + * Handles system instructions and multimodal content. + * @param {Object} claudeRequest - The request body from the Claude API. + * @returns {Object} The formatted request body for the Gemini API. + */ +export function toGeminiRequestFromClaude(claudeRequest) { + // Ensure claudeRequest is a valid object + if (!claudeRequest || typeof claudeRequest !== 'object') { + console.warn("Invalid claudeRequest provided to toGeminiRequestFromClaude."); + return { contents: [] }; + } + + const geminiRequest = { + contents: [] + }; + + // Handle system instruction + if (claudeRequest.system) { + let incomingSystemText = null; + if (typeof claudeRequest.system === 'string') { + incomingSystemText = claudeRequest.system; + } else if (typeof claudeRequest.system === 'object') { + incomingSystemText = JSON.stringify(claudeRequest.system); + } else if (claudeRequest.messages?.length > 0) { + // Fallback to first user message if no system property + const userMessage = claudeRequest.messages.find(m => m.role === 'user'); + if (userMessage) { + if (Array.isArray(userMessage.content)) { + incomingSystemText = userMessage.content.map(block => block.text).join(''); + } else { + incomingSystemText = userMessage.content; + } + } + } + geminiRequest.systemInstruction = { + parts: [{ text: incomingSystemText}] // Ensure system is string + }; + } + + // Process messages + if (Array.isArray(claudeRequest.messages)) { + claudeRequest.messages.forEach(message => { + // Ensure message is a valid object and has a role and content + if (!message || typeof message !== 'object' || !message.role || !message.content) { + console.warn("Skipping invalid message in claudeRequest.messages."); + return; + } + + const geminiRole = message.role === 'assistant' ? 'model' : 'user'; + const processedParts = processClaudeContentToGeminiParts(message.content); + + // If the processed parts contain a function response, it should be a 'function' role message + // Claude's tool_result block does not contain the function name, only tool_use_id. + // We need to infer the function name from the previous tool_use message. + // For simplicity in this conversion, we'll assume the tool_use_id is the function name + // or that the tool_result is always preceded by a tool_use with the correct name. + // A more robust solution would involve tracking tool_use_ids to function names. + const functionResponsePart = processedParts.find(part => part.functionResponse); + if (functionResponsePart) { + geminiRequest.contents.push({ + role: 'function', + parts: [functionResponsePart] + }); + } else if (processedParts.length > 0) { // Only push if there are actual parts + geminiRequest.contents.push({ + role: geminiRole, + parts: processedParts + }); + } + }); + } + + // Add generation config + const generationConfig = {}; + generationConfig.maxOutputTokens = checkAndAssignOrDefault(claudeRequest.max_tokens, DEFAULT_GEMINI_MAX_TOKENS); + generationConfig.temperature = checkAndAssignOrDefault(claudeRequest.temperature, DEFAULT_TEMPERATURE); + generationConfig.topP = checkAndAssignOrDefault(claudeRequest.top_p, DEFAULT_TOP_P); + + if (Object.keys(generationConfig).length > 0) { + geminiRequest.generationConfig = generationConfig; + } + + // Handle tools + if (Array.isArray(claudeRequest.tools)) { + geminiRequest.tools = [{ + functionDeclarations: claudeRequest.tools.map(tool => { + // Ensure tool is a valid object and has a name + if (!tool || typeof tool !== 'object' || !tool.name) { + console.warn("Skipping invalid tool declaration in claudeRequest.tools."); + return null; // Return null for invalid tools, filter out later + } + + // Filter out TodoWrite tool + // if (tool.name === 'TodoWrite') { + // console.log("Filtering out TodoWrite tool"); + // return null; + // } + + delete tool.input_schema.$schema; + return { + name: String(tool.name), // Ensure name is string + description: String(tool.description || ''), // Ensure description is string + parameters: tool.input_schema && typeof tool.input_schema === 'object' ? tool.input_schema : { type: 'object', properties: {} } + }; + }).filter(Boolean) // Filter out any nulls from invalid tool declarations + }]; + // If no valid functionDeclarations, remove the tools array + if (geminiRequest.tools[0].functionDeclarations.length === 0) { + delete geminiRequest.tools; + } + } + + // Handle tool_choice + if (claudeRequest.tool_choice) { + geminiRequest.toolConfig = buildGeminiToolConfigFromClaude(claudeRequest.tool_choice); + } + + return geminiRequest; +} + +/** + * Builds Gemini toolConfig from Claude tool_choice. + * @param {Object} claudeToolChoice - The tool_choice object from Claude API. + * @returns {Object|undefined} The formatted toolConfig for Gemini API, or undefined if invalid. + */ +function buildGeminiToolConfigFromClaude(claudeToolChoice) { + if (!claudeToolChoice || typeof claudeToolChoice !== 'object' || !claudeToolChoice.type) { + console.warn("Invalid claudeToolChoice provided to buildGeminiToolConfigFromClaude."); + return undefined; + } + + switch (claudeToolChoice.type) { + case 'auto': + return { functionCallingConfig: { mode: 'AUTO' } }; + case 'none': + return { functionCallingConfig: { mode: 'NONE' } }; + case 'tool': + if (claudeToolChoice.name && typeof claudeToolChoice.name === 'string') { + return { functionCallingConfig: { mode: 'ANY', allowedFunctionNames: [claudeToolChoice.name] } }; + } + console.warn("Invalid tool name in claudeToolChoice of type 'tool'."); + return undefined; + default: + console.warn(`Unsupported claudeToolChoice type: ${claudeToolChoice.type}`); + return undefined; + } +} + +/** + * Processes Claude content to Gemini parts format with multimodal support. + * @param {string|Array} content - Claude message content. + * @returns {Array} Array of Gemini parts. + */ +function processClaudeContentToGeminiParts(content) { + if (!content) return []; + + // Handle string content + if (typeof content === 'string') { + return [{ text: content }]; + } + + // Handle array content (multimodal) + if (Array.isArray(content)) { + const parts = []; + + content.forEach(block => { + // Ensure block is a valid object and has a type + if (!block || typeof block !== 'object' || !block.type) { + console.warn("Skipping invalid content block in processClaudeContentToGeminiParts."); + return; + } + + switch (block.type) { + case 'text': + if (typeof block.text === 'string') { + parts.push({ text: block.text }); + } else { + console.warn("Invalid text content in Claude text block."); + } + break; + + case 'image': + if (block.source && typeof block.source === 'object' && block.source.type === 'base64' && + typeof block.source.media_type === 'string' && typeof block.source.data === 'string') { + parts.push({ + inlineData: { + mimeType: block.source.media_type, + data: block.source.data + } + }); + } else { + console.warn("Invalid image source in Claude image block."); + } + break; + + case 'tool_use': + if (typeof block.name === 'string' && block.input && typeof block.input === 'object') { + // Filter out TodoWrite tool use + // if (block.name === 'TodoWrite') { + // console.log("Filtering out TodoWrite tool use"); + // break; // Skip adding this tool to parts + // } + parts.push({ + functionCall: { + name: block.name, + args: block.input + } + }); + } else { + console.warn("Invalid tool_use block in Claude content."); + } + break; + + case 'tool_result': + // Claude's tool_result block does not contain the function name, only tool_use_id. + // Gemini's functionResponse requires a function name. + // For now, we'll use the tool_use_id as the name, but this is a potential point of failure + // if the tool_use_id is not the actual function name in Gemini's context. + // A more robust solution would involve tracking the function name from the tool_use block. + if (typeof block.tool_use_id === 'string') { + parts.push({ + functionResponse: { + name: block.tool_use_id, // This might need to be the actual function name + response: { content: block.content } // content can be any JSON-serializable value + } + }); + } else { + console.warn("Invalid tool_result block in Claude content: missing tool_use_id."); + } + break; + + default: + // Handle any other content types as text if they have a text property + if (typeof block.text === 'string') { + parts.push({ text: block.text }); + } else { + console.warn(`Unsupported Claude content block type: ${block.type}. Skipping.`); + } + } + }); + + return parts; + } + + return []; +} + +/** + * Converts a Gemini API response to a Claude API messages response. + * @param {Object} geminiResponse - The Gemini API response object. + * @param {string} model - The model name to include in the response. + * @returns {Object} The formatted Claude API messages response. + */ +export function toClaudeChatCompletionFromGemini(geminiResponse, model) { + // Handle cases where geminiResponse or candidates are missing or empty + if (!geminiResponse || !geminiResponse.candidates || geminiResponse.candidates.length === 0) { + return { + id: `msg_${uuidv4()}`, + type: "message", + role: "assistant", + content: [], // Empty content for no candidates + model: model, + stop_reason: "end_turn", // Default stop reason + stop_sequence: null, + usage: { + input_tokens: geminiResponse?.usageMetadata?.promptTokenCount || 0, + output_tokens: geminiResponse?.usageMetadata?.candidatesTokenCount || 0 + } + }; + } + + const candidate = geminiResponse.candidates[0]; + const content = processGeminiResponseToClaudeContent(geminiResponse); + const finishReason = candidate.finishReason; + let stopReason = "end_turn"; // Default stop reason + + if (finishReason) { + switch (finishReason) { + case 'STOP': + stopReason = 'end_turn'; + break; + case 'MAX_TOKENS': + stopReason = 'max_tokens'; + break; + case 'SAFETY': + stopReason = 'safety'; + break; + case 'RECITATION': + stopReason = 'recitation'; + break; + case 'OTHER': + stopReason = 'other'; + break; + default: + stopReason = 'end_turn'; + } + } + + return { + id: `msg_${uuidv4()}`, + type: "message", + role: "assistant", + content: content, + model: model, + stop_reason: stopReason, + stop_sequence: null, + usage: { + input_tokens: geminiResponse.usageMetadata?.promptTokenCount || 0, + output_tokens: geminiResponse.usageMetadata?.candidatesTokenCount || 0 + } + }; +} + +/** + * Processes Gemini response content to Claude format. + * @param {Object} geminiResponse - The Gemini API response. + * @returns {Array} Array of Claude content blocks. + */ +function processGeminiResponseToClaudeContent(geminiResponse) { + if (!geminiResponse || !geminiResponse.candidates || geminiResponse.candidates.length === 0) return []; + + const content = []; + + for (const candidate of geminiResponse.candidates) { + // 检查完成原因是否为错误类型 + if (candidate.finishReason && candidate.finishReason !== 'STOP') { + // console.log('Gemini response finishReason:', JSON.stringify(candidate)); + // console.warn('Gemini response contains malformed function call:', candidate.finishMessage || 'No finish message'); + + // 将错误信息作为文本内容返回 + if (candidate.finishMessage) { + content.push({ + type: 'text', + text: `Error: ${candidate.finishMessage}` + }); + } + // console.log("Processed content:", content); + continue; // 跳过当前候选的进一步处理 + } + + if (candidate.content && candidate.content.parts) { + for (const part of candidate.content.parts) { + if (part.text) { + content.push({ + type: 'text', + text: part.text + }); + } else if (part.inlineData) { + content.push({ + type: 'image', + source: { + type: 'base64', + media_type: part.inlineData.mimeType, + data: part.inlineData.data + } + }); + } else if (part.functionCall) { + // Convert Gemini functionCall to Claude tool_use + content.push({ + type: 'tool_use', + id: uuidv4(), // Generate a new ID for the tool use + name: part.functionCall.name, + input: part.functionCall.args || {} + }); + } + } + } + } + + return content; +} + +/** + * Converts a Gemini API stream chunk to a Claude API messages stream chunk. + * @param {Object} geminiChunk - The Gemini API stream chunk object. + * @param {string} [model] - Optional model name to include in the response. + * @returns {Object} The formatted Claude API messages stream chunk. + */ +export function toClaudeStreamChunkFromGemini(geminiChunk, model) { + if (!geminiChunk) { + return null; + } + + if (typeof geminiChunk === 'string') { + return { + type: "content_block_delta", + index: 0, + delta: { + type: "text_delta", + text: geminiChunk + } + }; + } + + return null; +} + + +/** + * Converts a Claude API response to an OpenAI Responses API response. + * @param {Object} claudeResponse - The Claude API response object. + * @param {string} model - The model name to include in the response. + * @returns {Object} The formatted OpenAI Responses API response. + */ +export function toOpenAIResponsesFromClaude(claudeResponse, model) { + // 根据参考示例重构响应结构 + const content = processClaudeResponseContent(claudeResponse.content); + const textContent = typeof content === 'string' ? content : JSON.stringify(content); + + // 将claude的内容转换为OpenAI Responses输出格式 + let output = []; + + // 添加文本内容 + output.push({ + type: "message", + id: `msg_${uuidv4().replace(/-/g, '')}`, + summary: [], + type: "message", + role: "assistant", + status: "completed", + content: [{ + annotations: [], + logprobs: [], + text: textContent, + type: "output_text" + }] + }); + + return { + background: false, + created_at: Math.floor(Date.now() / 1000), + error: null, + id: `resp_${uuidv4().replace(/-/g, '')}`, + incomplete_details: null, + max_output_tokens: null, + max_tool_calls: null, + metadata: {}, + model: model || claudeResponse.model, + object: "response", + output: output, + parallel_tool_calls: true, + previous_response_id: null, + prompt_cache_key: null, + reasoning: { + // effort: "minimal", + // summary: "detailed" + }, + safety_identifier: "user-"+uuidv4().replace(/-/g, ''), // 示例值 + service_tier: "default", + status: "completed", + store: false, + temperature: 1, + text: { + format: {type: "text"}, + // verbosity: "medium" + }, + tool_choice: "auto", + tools: [], + top_logprobs: 0, + top_p: 1, + truncation: "disabled", + usage: { + input_tokens: claudeResponse.usage?.input_tokens || 0, // 示例值 + input_tokens_details: { + cached_tokens: claudeResponse.usage?.cache_creation_input_tokens || 0, // 如果有缓存相关数据则使用 + }, + output_tokens: claudeResponse.usage?.output_tokens || 0, // 示例值 + output_tokens_details: { + reasoning_tokens: 0 + }, + total_tokens: (claudeResponse.usage?.input_tokens || 0) + (claudeResponse.usage?.output_tokens || 0) // 示例值 + }, + user: null + }; +} + +/** + * Converts a Gemini API response to an OpenAI Responses API response. + * @param {Object} geminiResponse - The Gemini API response object. + * @param {string} model - The model name to include in the response. + * @returns {Object} The formatted OpenAI Responses API response. + */ +export function toOpenAIResponsesFromGemini(geminiResponse, model) { + // 根据参考示例重构响应结构 + const content = processGeminiResponseContent(geminiResponse); + const textContent = typeof content === 'string' ? content : JSON.stringify(content); + + // 将gemini的内容转换为OpenAI Responses输出格式 + let output = []; + + // 添加文本内容 + output.push({ + id: `msg_${uuidv4().replace(/-/g, '')}`, + summary: [], + type: "message", + role: "assistant", + status: "completed", + content: [{ + annotations: [], + logprobs: [], + text: textContent, + type: "output_text" + }] + }); + + return { + background: false, + created_at: Math.floor(Date.now() / 1000), + error: null, + id: `resp_${uuidv4().replace(/-/g, '')}`, + incomplete_details: null, + max_output_tokens: null, + max_tool_calls: null, + metadata: {}, + model: model, + object: "response", + output: output, + parallel_tool_calls: true, + previous_response_id: null, + prompt_cache_key: null, + reasoning: { + // effort: "minimal", + // summary: "detailed" + }, + safety_identifier: "user-"+uuidv4().replace(/-/g, ''), // 示例值 + service_tier: "default", + status: "completed", + store: false, + temperature: 1, + text: { + format: {type: "text"}, + // verbosity: "medium" + }, + tool_choice: "auto", + tools: [], + top_logprobs: 0, + top_p: 1, + truncation: "disabled", + usage: { + input_tokens: geminiResponse.usageMetadata?.promptTokenCount || 0, // 示例值 + input_tokens_details: { + cached_tokens: geminiResponse.usageMetadata?.cachedTokens || 0, // 使用正确的Gemini缓存字段 + }, + output_tokens: geminiResponse.usageMetadata?.candidatesTokenCount || 0, // 示例值 + output_tokens_details: { + reasoning_tokens: 0 + }, + total_tokens: geminiResponse.usageMetadata?.totalTokenCount || 0, // 示例值 + }, + user: null + }; +} + + +/** + * Converts an OpenAI Responses API request body to a Claude API request body. + * @param {Object} responsesRequest - The request body from the OpenAI Responses API. + * @returns {Object} The formatted request body for the Claude API. + */ +export function toClaudeRequestFromOpenAIResponses(responsesRequest) { + // The OpenAI Responses API uses input and instructions instead of messages + const claudeRequest = { + model: responsesRequest.model, + max_tokens: checkAndAssignOrDefault(responsesRequest.max_tokens, DEFAULT_MAX_TOKENS), + temperature: checkAndAssignOrDefault(responsesRequest.temperature, DEFAULT_TEMPERATURE), + top_p: checkAndAssignOrDefault(responsesRequest.top_p, DEFAULT_TOP_P), + }; + + // Process instructions as system message + if (responsesRequest.instructions) { + claudeRequest.system = []; + claudeRequest.system.push({ + text: typeof responsesRequest.instructions === 'string' ? responsesRequest.instructions : JSON.stringify(responsesRequest.instructions) + }); + + } + + const claudeMessages = []; + // Process input as user message content + if (responsesRequest.input) { + if (typeof responsesRequest.input === 'string') { + // Create user message with the string content + claudeMessages.push({ + role: 'user', + content: [{ + type: 'text', + text: responsesRequest.input + }] + }); + } else { + // Handle array of messages or items - process the entire array + for (const message of responsesRequest.input) { + const role = message.role === 'assistant' ? 'assistant' : 'user'; + let content = []; + + if (message.role === 'tool') { + // Claude expects tool_result to be in a 'user' message + // The content of a tool message is a single tool_result block + content.push({ + type: 'tool_result', + tool_use_id: message.tool_call_id, // Use tool_call_id from OpenAI tool message + content: safeParseJSON(message.content) // Parse content as JSON if possible + }); + claudeMessages.push({ role: 'user', content: content }); + } else if (message.role === 'assistant' && message.tool_calls?.length) { + // Assistant message with tool calls - properly format as tool_use blocks + // Claude expects tool_use to be in an 'assistant' message + const toolUseBlocks = message.tool_calls.map(tc => ({ + type: 'tool_use', + id: tc.id, + name: tc.function.name, + input: safeParseJSON(tc.function.arguments) + })); + claudeMessages.push({ role: 'assistant', content: toolUseBlocks }); + } else { + // Regular user or assistant message (text and multimodal) + if (typeof message.content === 'string') { + if (message.content) { + content.push({ type: 'text', text: message.content }); + } + } else if (Array.isArray(message.content)) { + message.content.forEach(item => { + if (!item) return; + switch (item.type) { + case 'input_text': + if (item.text) { + content.push({ type: 'text', text: item.text }); + } + break; + case 'output_text': + if (item.text) { + content.push({ type: 'text', text: item.text }); + } + break; + case 'image_url': + if (item.image_url) { + const imageUrl = typeof item.image_url === 'string' + ? item.image_url + : item.image_url.url; + if (imageUrl.startsWith('data:')) { + const [header, data] = imageUrl.split(','); + const mediaType = header.match(/data:([^;]+)/)?.[1] || 'image/jpeg'; + content.push({ + type: 'image', + source: { + type: 'base64', + media_type: mediaType, + data: data + } + }); + } else { + // Claude requires base64 for images, so for URLs, we'll represent as text + content.push({ type: 'text', text: `[Image: ${imageUrl}]` }); + } + } + break; + case 'audio': + // Handle audio content as text placeholder + if (item.audio_url) { + const audioUrl = typeof item.audio_url === 'string' + ? item.audio_url + : item.audio_url.url; + content.push({ type: 'text', text: `[Audio: ${audioUrl}]` }); + } + break; + } + }); + } + // Only add message if content is not empty + if (content.length > 0) { + claudeMessages.push({ role: role, content: content }); + } + } + } + } + } + + // Process tools if present + // if (responsesRequest.tools && Array.isArray(responsesRequest.tools)) { + // claudeRequest.tools = responsesRequest.tools.map(tool => ({ + // name: tool.name, + // description: tool.description || '', + // input_schema: tool.parameters || { type: 'object', properties: {} } + // })); + // claudeRequest.tool_choice = buildClaudeToolChoice(responsesRequest.tool_choice); + // } + + // Process messages + claudeRequest.messages = claudeMessages; + claudeRequest.stream = responsesRequest.stream || false; + return claudeRequest; +} + +/** + * Converts an OpenAI Responses API request body to a Gemini API request body. + * @param {Object} responsesRequest - The request body from the OpenAI Responses API. + * @returns {Object} The formatted request body for the Gemini API. + */ +export function toGeminiRequestFromOpenAIResponses(responsesRequest) { + // The OpenAI Responses API uses input and instructions instead of messages + const geminiRequest = { + contents: [] + }; + + // Process instructions as system instruction + if (responsesRequest.instructions) { + let instructionsText = ''; + if (typeof responsesRequest.instructions === 'string') { + instructionsText = responsesRequest.instructions; + } else { + instructionsText = JSON.stringify(responsesRequest.instructions); + } + geminiRequest.systemInstruction = { + parts: [{ text: instructionsText }] + }; + } + + // Process input as user content + if (responsesRequest.input) { + let inputContent = ''; + if (typeof responsesRequest.input === 'string') { + inputContent = responsesRequest.input; + } else if (Array.isArray(responsesRequest.input)) { + // Handle array of messages or items + if (responsesRequest.input.length > 0) { + // For compatibility, take the content of the last item with text content + const lastInputItem = [...responsesRequest.input].reverse().find(item => + item && ( + (item.content && typeof item.content === 'string') || + (item.content && Array.isArray(item.content) && item.content.some(c => c && c.text)) || + (item.role === 'user' && item.content) + ) + ); + + if (lastInputItem) { + if (typeof lastInputItem.content === 'string') { + inputContent = lastInputItem.content; + } else if (Array.isArray(lastInputItem.content)) { + // Process array of content blocks + inputContent = lastInputItem.content + .filter(block => block && block.text) + .map(block => block.text) + .join(' '); + } else { + // General fallback + inputContent = JSON.stringify(lastInputItem.content || lastInputItem); + } + } + } + } + + if (inputContent) { + // Add user message with the input content + geminiRequest.contents.push({ + role: 'user', + parts: [{ text: inputContent }] + }); + } + } else { + // If no input is provided, ensure we have at least one user message for Gemini + geminiRequest.contents.push({ + role: 'user', + parts: [{ text: 'Hello' }] // Default content to satisfy Gemini API requirement + }); + } + + // Add generation config + const generationConfig = {}; + generationConfig.maxOutputTokens = checkAndAssignOrDefault(responsesRequest.max_tokens, DEFAULT_GEMINI_MAX_TOKENS); + generationConfig.temperature = checkAndAssignOrDefault(responsesRequest.temperature, DEFAULT_TEMPERATURE); + generationConfig.topP = checkAndAssignOrDefault(responsesRequest.top_p, DEFAULT_TOP_P); + + if (Object.keys(generationConfig).length > 0) { + geminiRequest.generationConfig = generationConfig; + } + + // Process tools if present + if (responsesRequest.tools && Array.isArray(responsesRequest.tools)) { + geminiRequest.tools = [{ + functionDeclarations: responsesRequest.tools + .filter(tool => tool && (tool.type === 'function' || tool.function)) + .map(tool => { + const func = tool.function || tool; + return { + name: String(func.name || tool.name || ''), + description: String(func.description || tool.description || ''), + parameters: func.parameters || tool.parameters || { type: 'object', properties: {} } + }; + }).filter(Boolean) // Filter out any invalid tools + }]; + + // If no valid functionDeclarations, remove the tools array + if (geminiRequest.tools[0].functionDeclarations.length === 0) { + delete geminiRequest.tools; + } + } + + return geminiRequest; +} + +/** + * Converts a Claude API stream chunk to an OpenAI Responses API stream chunk. + * @param {Object} claudeChunk - The Claude API stream chunk object. + * @param {string} [model] - Optional model name to include in the response. + * @param {string} [requestId] - Optional request ID to maintain stream state across chunks. + * @returns {Array} The formatted OpenAI Responses API stream chunks as an array of events. + */ +export function toOpenAIResponsesStreamChunkFromClaude(claudeChunk, model, requestId = null) { + if (!claudeChunk) { + return []; + } + + // 如果没有提供requestId,则生成一个(首次调用时) + const id = requestId || Date.now().toString(); + + // 设置模型信息(仅在新请求时设置) + if (!requestId) { + streamStateManager.setModel(id, model); + } + + // Handle text content from Claude stream + let content = ''; + if (typeof claudeChunk === 'string') { + content = claudeChunk; + } else if (claudeChunk && typeof claudeChunk === 'object' && claudeChunk.delta?.text) { + content = claudeChunk.delta.text; + } else if (claudeChunk && typeof claudeChunk === 'object') { + content = claudeChunk; + } + + // 对于第一个数据块(fullText为空),生成开始事件 + const state = streamStateManager.getOrCreateState(id); + if (state.fullText === '' && !requestId) { // 只在首次调用时(未指定requestId时)生成开始事件 + // 在这种情况下,我们需要先添加内容到状态 + state.fullText = content; + return [ + // ...getOpenAIResponsesStreamChunkBegin(id, model), + generateOutputTextDelta(id, content), + // ...getOpenAIResponsesStreamChunkEnd(id) + ]; + } else if (content === '') { + // 如果是结束块,生成结束事件 + const doneEvents = getOpenAIResponsesStreamChunkEnd(id); + + // 清理状态 + streamStateManager.cleanup(id); + + return doneEvents; + } else { + // 中间数据块,只返回delta事件,但也要更新状态 + streamStateManager.updateText(id, content); + return [ + generateOutputTextDelta(id, content) + ]; + } +} + +/** + * Converts a Gemini API stream chunk to an OpenAI Responses API stream chunk. + * @param {Object} geminiChunk - The Gemini API stream chunk object. + * @param {string} [model] - Optional model name to include in the response. + * @param {string} [requestId] - Optional request ID to maintain stream state across chunks. + * @returns {Array} The formatted OpenAI Responses API stream chunks as an array of events. + */ +export function toOpenAIResponsesStreamChunkFromGemini(geminiChunk, model, requestId = null) { + if (!geminiChunk) { + return []; + } + + // 如果没有提供requestId,则生成一个(首次调用时) + const id = requestId || Date.now().toString(); + + // 设置模型信息(仅在新请求时设置) + if (!requestId) { + streamStateManager.setModel(id, model); + } + + // Handle text content in stream + let content = ''; + if (typeof geminiChunk === 'string') { + content = geminiChunk; + } else if (geminiChunk && typeof geminiChunk === 'object') { + // Extract content from Gemini chunk if it's an object + content = geminiChunk.content || geminiChunk.text || geminiChunk; + } + + // 对于第一个数据块(fullText为空),生成开始事件 + const state = streamStateManager.getOrCreateState(id); + if (state.fullText === '' && !requestId) { // 只在首次调用时(未指定requestId时)生成开始事件 + // 在这种情况下,我们需要先添加内容到状态 + state.fullText = content; + return [ + // ...getOpenAIResponsesStreamChunkBegin(id, model), + generateOutputTextDelta(id, content), + // ...getOpenAIResponsesStreamChunkEnd(id) + ]; + } else if (content === '') { + // 如果是结束块,生成结束事件 + const doneEvents = getOpenAIResponsesStreamChunkEnd(id); + + // 清理状态 + streamStateManager.cleanup(id); + + return doneEvents; + } else { + // 中间数据块,只返回delta事件,但也要更新状态 + streamStateManager.updateText(id, content); + return [ + generateOutputTextDelta(id, content) + ]; + } +} + +export function getOpenAIStreamChunkStop(model) { + return { + id: `chatcmpl-${uuidv4()}`, // uuidv4 needs to be imported or handled + object: "chat.completion.chunk", + created: Math.floor(Date.now() / 1000), + model: model, + system_fingerprint: "", + choices: [{ + index: 0, + delta: { + content: "", + reasoning_content: "" + }, + finish_reason: 'stop', + message: { + content: "", + reasoning_content: "" + } + }], + usage:{ + prompt_tokens: 0, + completion_tokens: 0, + total_tokens: 0, + }, + }; +} + +export function getOpenAIResponsesStreamChunkBegin(id, model){ + + return [ + generateResponseCreated(id, model), + generateResponseInProgress(id), + generateOutputItemAdded(id), + generateContentPartAdded(id) + ]; +} + +export function getOpenAIResponsesStreamChunkEnd(id){ + + return [ + generateOutputTextDone(id), + generateContentPartDone(id), + generateOutputItemDone(id), + generateResponseCompleted(id) + ]; +} \ No newline at end of file diff --git a/src/convert.js b/src/convert.js index a4a4ae0..b1c56b8 100644 --- a/src/convert.js +++ b/src/convert.js @@ -1,2538 +1,301 @@ +/** + * 协议转换模块 - 新架构版本 + * 使用重构后的转换器架构 + * + * 这个文件展示了如何使用新的转换器架构 + * 可以逐步替换原有的 convert.js + */ + import { v4 as uuidv4 } from 'uuid'; -import { MODEL_PROTOCOL_PREFIX, getProtocolPrefix } from './common.js'; +import { MODEL_PROTOCOL_PREFIX } from './common.js'; +import { getProtocolPrefix } from './common.js'; +import { ConverterFactory } from './converters/ConverterFactory.js'; import { - streamStateManager, - generateResponseCreated, - generateResponseInProgress, - generateOutputItemAdded, - generateContentPartAdded, - generateOutputTextDelta, - generateOutputTextDone, - generateContentPartDone, - generateOutputItemDone, - generateResponseCompleted + generateResponseCreated, + generateResponseInProgress, + generateOutputItemAdded, + generateContentPartAdded, + generateOutputTextDone, + generateContentPartDone, + generateOutputItemDone, + generateResponseCompleted } from './openai/openai-responses-core.mjs'; // ============================================================================= -// 常量和辅助函数定义 +// 初始化:注册所有转换器 // ============================================================================= -// 定义默认常量 -const DEFAULT_MAX_TOKENS = 8192; -const DEFAULT_GEMINI_MAX_TOKENS = 65535; -const DEFAULT_TEMPERATURE = 1; -const DEFAULT_TOP_P = 0.95; - -// 辅助函数:判断值是否为 undefined 或 0,并返回默认值 -function checkAndAssignOrDefault(value, defaultValue) { - if (value !== undefined && value !== 0) { - return value; - } - return defaultValue; -} - -/** - * 映射结束原因 - * @param {string} reason - 结束原因 - * @param {string} sourceFormat - 源格式 - * @param {string} targetFormat - 目标格式 - * @returns {string} 映射后的结束原因 - */ -function _mapFinishReason(reason, sourceFormat, targetFormat) { - const reasonMappings = { - openai: { - anthropic: { - stop: "end_turn", - length: "max_tokens", - content_filter: "stop_sequence", - tool_calls: "tool_use" - } - }, - gemini: { - anthropic: { - // 旧版本大写格式 - STOP: "end_turn", - MAX_TOKENS: "max_tokens", - SAFETY: "stop_sequence", - RECITATION: "stop_sequence", - // 新版本小写格式(v1beta/v1 API) - stop: "end_turn", - length: "max_tokens", - safety: "stop_sequence", - recitation: "stop_sequence", - other: "end_turn" - } - } - }; - - try { - return reasonMappings[sourceFormat][targetFormat][reason] || "end_turn"; - } catch (e) { - return "end_turn"; - } -} - -/** - * 递归清理Gemini不支持的JSON Schema属性 - * @param {Object} schema - JSON Schema - * @returns {Object} 清理后的JSON Schema - */ -function _cleanJsonSchemaProperties(schema) { - if (!schema || typeof schema !== 'object') { - return schema; - } - - // 移除所有非标准属性 - const sanitized = {}; - for (const [key, value] of Object.entries(schema)) { - if (["type", "description", "properties", "required", "enum", "items"].includes(key)) { - sanitized[key] = value; - } - } - - if (sanitized.properties && typeof sanitized.properties === 'object') { - const cleanProperties = {}; - for (const [propName, propSchema] of Object.entries(sanitized.properties)) { - cleanProperties[propName] = _cleanJsonSchemaProperties(propSchema); - } - sanitized.properties = cleanProperties; - } - - if (sanitized.items) { - sanitized.items = _cleanJsonSchemaProperties(sanitized.items); - } - - return sanitized; -} - -/** - * 根据budget_tokens智能判断OpenAI reasoning_effort等级 - * @param {number|null} budgetTokens - Anthropic thinking的budget_tokens值 - * @returns {string} OpenAI reasoning_effort等级 ("low", "medium", "high") - */ -function _determineReasoningEffortFromBudget(budgetTokens) { - // 如果没有提供budget_tokens,默认为high - if (budgetTokens === null || budgetTokens === undefined) { - console.info("No budget_tokens provided, defaulting to reasoning_effort='high'"); - return "high"; - } - - // 使用固定阈值替代环境变量 - const LOW_THRESHOLD = 50; // 低推理努力的阈值 - const HIGH_THRESHOLD = 200; // 高推理努力的阈值 - - console.debug(`Threshold configuration: low <= ${LOW_THRESHOLD}, medium <= ${HIGH_THRESHOLD}, high > ${HIGH_THRESHOLD}`); - - let effort; - if (budgetTokens <= LOW_THRESHOLD) { - effort = "low"; - } else if (budgetTokens <= HIGH_THRESHOLD) { - effort = "medium"; - } else { - effort = "high"; - } - - console.info(`🎯 Budget tokens ${budgetTokens} -> reasoning_effort '${effort}' (thresholds: low<=${LOW_THRESHOLD}, high<=${HIGH_THRESHOLD})`); - return effort; -} - -// 全局工具状态管理器 -class ToolStateManager { - constructor() { - if (ToolStateManager.instance) { - return ToolStateManager.instance; - } - ToolStateManager.instance = this; - this._toolMappings = {}; - return this; - } - - // 存储工具名到ID的映射 - storeToolMapping(funcName, toolId) { - this._toolMappings[funcName] = toolId; - } - - // 根据工具名获取ID - getToolId(funcName) { - return this._toolMappings[funcName] || null; - } - - // 清除所有映射 - clearMappings() { - this._toolMappings = {}; - } -} - -// 全局工具状态管理器实例 -const toolStateManager = new ToolStateManager(); - // ============================================================================= // 主转换函数 // ============================================================================= /** - * Generic data conversion function. - * @param {object} data - The data to convert (request body or response). - * @param {string} type - The type of conversion: 'request', 'response', 'streamChunk', 'modelList'. - * @param {string} fromProvider - The source model provider (e.g., MODEL_PROVIDER.GEMINI_CLI). - * @param {string} toProvider - The target model provider (e.g., MODEL_PROVIDER.OPENAI_CUSTOM). - * @param {string} [model] - Optional model name for response conversions. - * @returns {object} The converted data. - * @throws {Error} If no suitable conversion function is found. + * 通用数据转换函数(新架构版本) + * @param {object} data - 要转换的数据(请求体或响应) + * @param {string} type - 转换类型:'request', 'response', 'streamChunk', 'modelList' + * @param {string} fromProvider - 源模型提供商 + * @param {string} toProvider - 目标模型提供商 + * @param {string} [model] - 可选的模型名称(用于响应转换) + * @returns {object} 转换后的数据 + * @throws {Error} 如果找不到合适的转换函数 */ export function convertData(data, type, fromProvider, toProvider, model) { - // Define a map of conversion functions using protocol prefixes - const conversionMap = { - request: { - [MODEL_PROTOCOL_PREFIX.OPENAI]: { // to OpenAI protocol - [MODEL_PROTOCOL_PREFIX.GEMINI]: toOpenAIRequestFromGemini, // from Gemini protocol - [MODEL_PROTOCOL_PREFIX.CLAUDE]: toOpenAIRequestFromClaude, // from Claude protocol - }, - [MODEL_PROTOCOL_PREFIX.CLAUDE]: { // to Claude protocol - [MODEL_PROTOCOL_PREFIX.OPENAI]: toClaudeRequestFromOpenAI, // from OpenAI protocol - [MODEL_PROTOCOL_PREFIX.OPENAI_RESPONSES]: toClaudeRequestFromOpenAIResponses, // from OpenAI protocol (Responses format) - }, - [MODEL_PROTOCOL_PREFIX.GEMINI]: { // to Gemini protocol - [MODEL_PROTOCOL_PREFIX.OPENAI]: toGeminiRequestFromOpenAI, // from OpenAI protocol - [MODEL_PROTOCOL_PREFIX.CLAUDE]: toGeminiRequestFromClaude, // from Claude protocol - [MODEL_PROTOCOL_PREFIX.OPENAI_RESPONSES]: toGeminiRequestFromOpenAIResponses, // from OpenAI protocol (Responses format) - }, - }, - response: { - [MODEL_PROTOCOL_PREFIX.OPENAI]: { // to OpenAI protocol - [MODEL_PROTOCOL_PREFIX.GEMINI]: toOpenAIChatCompletionFromGemini, // from Gemini protocol - [MODEL_PROTOCOL_PREFIX.CLAUDE]: toOpenAIChatCompletionFromClaude, // from Claude protocol - }, - [MODEL_PROTOCOL_PREFIX.CLAUDE]: { // to Claude protocol - [MODEL_PROTOCOL_PREFIX.GEMINI]: toClaudeChatCompletionFromGemini, // from Gemini protocol - [MODEL_PROTOCOL_PREFIX.OPENAI]: toClaudeChatCompletionFromOpenAI, // from OpenAI protocol - }, - [MODEL_PROTOCOL_PREFIX.OPENAI_RESPONSES]: { // to OpenAI protocol (Responses format) - [MODEL_PROTOCOL_PREFIX.GEMINI]: toOpenAIResponsesFromGemini, // from Gemini protocol - [MODEL_PROTOCOL_PREFIX.CLAUDE]: toOpenAIResponsesFromClaude, // from Claude protocol - }, - }, - streamChunk: { - [MODEL_PROTOCOL_PREFIX.OPENAI]: { // to OpenAI protocol - [MODEL_PROTOCOL_PREFIX.GEMINI]: toOpenAIStreamChunkFromGemini, // from Gemini protocol - [MODEL_PROTOCOL_PREFIX.CLAUDE]: toOpenAIStreamChunkFromClaude, // from Claude protocol - }, - [MODEL_PROTOCOL_PREFIX.CLAUDE]: { // to Claude protocol - [MODEL_PROTOCOL_PREFIX.GEMINI]: toClaudeStreamChunkFromGemini, // from Gemini protocol - [MODEL_PROTOCOL_PREFIX.OPENAI]: toClaudeStreamChunkFromOpenAI, // from OpenAI protocol - }, - [MODEL_PROTOCOL_PREFIX.OPENAI_RESPONSES]: { // to OpenAI protocol (Responses format) - [MODEL_PROTOCOL_PREFIX.GEMINI]: toOpenAIResponsesStreamChunkFromGemini, // from Gemini protocol - [MODEL_PROTOCOL_PREFIX.CLAUDE]: toOpenAIResponsesStreamChunkFromClaude, // from Claude protocol - }, - }, - modelList: { - [MODEL_PROTOCOL_PREFIX.OPENAI]: { // to OpenAI protocol - [MODEL_PROTOCOL_PREFIX.GEMINI]: toOpenAIModelListFromGemini, // from Gemini protocol - [MODEL_PROTOCOL_PREFIX.CLAUDE]: toOpenAIModelListFromClaude, // from Claude protocol - }, - [MODEL_PROTOCOL_PREFIX.CLAUDE]: { // to Claude protocol - [MODEL_PROTOCOL_PREFIX.GEMINI]: toClaudeModelListFromGemini, // from Gemini protocol - [MODEL_PROTOCOL_PREFIX.OPENAI]: toClaudeModelListFromOpenAI, // from OpenAI protocol - }, + try { + // 获取协议前缀 + const fromProtocol = getProtocolPrefix(fromProvider); + const toProtocol = getProtocolPrefix(toProvider); + + // 从工厂获取转换器 + const converter = ConverterFactory.getConverter(fromProtocol); + + if (!converter) { + throw new Error(`No converter found for protocol: ${fromProtocol}`); } - }; - const targetConversions = conversionMap[type]; - if (!targetConversions) { - throw new Error(`Unsupported conversion type: ${type}`); - } - - const toConversions = targetConversions[getProtocolPrefix(toProvider)]; - if (!toConversions) { - throw new Error(`No conversions defined for target protocol: ${getProtocolPrefix(toProvider)} for type: ${type}`); - } - - const conversionFunction = toConversions[getProtocolPrefix(fromProvider)]; - if (!conversionFunction) { - throw new Error(`No conversion function found from ${getProtocolPrefix(fromProvider)} to ${toProvider} for type: ${type}`); - } - - console.log(conversionFunction); - if (type === 'response' || type === 'streamChunk' || type === 'modelList') { - return conversionFunction(data, model); - } else { - return conversionFunction(data); + // 根据类型调用相应的转换方法 + switch (type) { + case 'request': + return converter.convertRequest(data, toProtocol); + + case 'response': + return converter.convertResponse(data, toProtocol, model); + + case 'streamChunk': + return converter.convertStreamChunk(data, toProtocol, model); + + case 'modelList': + return converter.convertModelList(data, toProtocol); + + default: + throw new Error(`Unsupported conversion type: ${type}`); + } + } catch (error) { + console.error(`Conversion error: ${error.message}`); + throw error; } } // ============================================================================= -// OpenAI 相关转换函数 +// 向后兼容的导出函数 // ============================================================================= /** - * Converts a Gemini API request body to an OpenAI chat completion request body. - * Handles system instructions and role mapping with multimodal support. - * @param {Object} geminiRequest - The request body from the Gemini API. - * @returns {Object} The formatted request body for the OpenAI API. + * 以下函数保持与原有API的兼容性 + * 内部使用新的转换器架构 */ + +// OpenAI 相关转换 export function toOpenAIRequestFromGemini(geminiRequest) { - const openaiRequest = { - messages: [], - model: geminiRequest.model, // Default model if not specified in Gemini request - max_tokens: checkAndAssignOrDefault(geminiRequest.max_tokens, DEFAULT_MAX_TOKENS), - temperature: checkAndAssignOrDefault(geminiRequest.temperature, DEFAULT_TEMPERATURE), - top_p: checkAndAssignOrDefault(geminiRequest.top_p, DEFAULT_TOP_P), - }; - - // Process system instruction - if (geminiRequest.systemInstruction && Array.isArray(geminiRequest.systemInstruction.parts)) { - const systemContent = processGeminiPartsToOpenAIContent(geminiRequest.systemInstruction.parts); - if (systemContent) { - openaiRequest.messages.push({ - role: 'system', - content: systemContent - }); - } - } - - // Process contents - if (geminiRequest.contents && Array.isArray(geminiRequest.contents)) { - geminiRequest.contents.forEach(content => { - if (content && Array.isArray(content.parts)) { - const openaiContent = processGeminiPartsToOpenAIContent(content.parts); - if (openaiContent && openaiContent.length > 0) { - const openaiRole = content.role === 'model' ? 'assistant' : content.role; - openaiRequest.messages.push({ - role: openaiRole, - content: openaiContent - }); - } - } - }); - } - - return openaiRequest; + const converter = ConverterFactory.getConverter(MODEL_PROTOCOL_PREFIX.GEMINI); + return converter.toOpenAIRequest(geminiRequest); } - -/** - * Processes Gemini parts to OpenAI content format with multimodal support. - * @param {Array} parts - Array of Gemini parts. - * @returns {Array|string} OpenAI content format. - */ -function processGeminiPartsToOpenAIContent(parts) { - if (!parts || !Array.isArray(parts)) return ''; - - const contentArray = []; - - parts.forEach(part => { - if (!part) return; - - // Handle text content - if (typeof part.text === 'string') { - contentArray.push({ - type: 'text', - text: part.text - }); - } - - // Handle inline data (images, audio) - if (part.inlineData) { - const { mimeType, data } = part.inlineData; - if (mimeType && data) { - contentArray.push({ - type: 'image_url', - image_url: { - url: `data:${mimeType};base64,${data}` - } - }); - } - } - - // Handle file data - if (part.fileData) { - const { mimeType, fileUri } = part.fileData; - if (mimeType && fileUri) { - // For file URIs, we need to determine if it's an image or audio - if (mimeType.startsWith('image/')) { - contentArray.push({ - type: 'image_url', - image_url: { - url: fileUri - } - }); - } else if (mimeType.startsWith('audio/')) { - // For audio, we'll use a placeholder or handle as text description - contentArray.push({ - type: 'text', - text: `[Audio file: ${fileUri}]` - }); - } - } - } - }); - - // Return as array for multimodal, or string for simple text - return contentArray.length === 1 && contentArray[0].type === 'text' - ? contentArray[0].text - : contentArray; -} - -export function toOpenAIModelListFromGemini(geminiModels) { - return { - object: "list", - data: geminiModels.models.map(m => ({ - id: m.name.startsWith('models/') ? m.name.substring(7) : m.name, // 移除 'models/' 前缀作为 id - object: "model", - created: Math.floor(Date.now() / 1000), - owned_by: "google", - })), - }; +export function toOpenAIRequestFromClaude(claudeRequest) { + const converter = ConverterFactory.getConverter(MODEL_PROTOCOL_PREFIX.CLAUDE); + return converter.toOpenAIRequest(claudeRequest); } export function toOpenAIChatCompletionFromGemini(geminiResponse, model) { - const content = processGeminiResponseContent(geminiResponse); - - return { - id: `chatcmpl-${uuidv4()}`, - object: "chat.completion", - created: Math.floor(Date.now() / 1000), - model: model, - choices: [{ - index: 0, - message: { - role: "assistant", - content: content - }, - finish_reason: "stop", - }], - usage: geminiResponse.usageMetadata ? { - prompt_tokens: geminiResponse.usageMetadata.promptTokenCount || 0, - completion_tokens: geminiResponse.usageMetadata.candidatesTokenCount || 0, - total_tokens: geminiResponse.usageMetadata.totalTokenCount || 0, - } : { - prompt_tokens: 0, - completion_tokens: 0, - total_tokens: 0, - }, - }; + const converter = ConverterFactory.getConverter(MODEL_PROTOCOL_PREFIX.GEMINI); + return converter.toOpenAIResponse(geminiResponse, model); } -/** - * Processes Gemini response content to OpenAI format with multimodal support. - * @param {Object} geminiResponse - The Gemini API response. - * @returns {string|Array} Processed content. - */ -function processGeminiResponseContent(geminiResponse) { - if (!geminiResponse || !geminiResponse.candidates) return ''; - - const contents = []; - - geminiResponse.candidates.forEach(candidate => { - if (candidate.content && candidate.content.parts) { - candidate.content.parts.forEach(part => { - if (part.text) { - contents.push(part.text); - } - // Note: Gemini response typically doesn't include multimodal content in responses - // but we handle it for completeness - }); - } - }); - - return contents.join('\n'); +export function toOpenAIChatCompletionFromClaude(claudeResponse, model) { + const converter = ConverterFactory.getConverter(MODEL_PROTOCOL_PREFIX.CLAUDE); + return converter.toOpenAIResponse(claudeResponse, model); } export function toOpenAIStreamChunkFromGemini(geminiChunk, model) { - return { - id: `chatcmpl-${uuidv4()}`, // uuidv4 needs to be imported or handled - object: "chat.completion.chunk", - created: Math.floor(Date.now() / 1000), - model: model, - choices: [{ - index: 0, - delta: { content: geminiChunk }, - finish_reason: null, - }], - usage: geminiChunk.usageMetadata ? { - prompt_tokens: geminiChunk.usageMetadata.promptTokenCount || 0, - completion_tokens: geminiChunk.usageMetadata.candidatesTokenCount || 0, - total_tokens: geminiChunk.usageMetadata.totalTokenCount || 0, - } : { - prompt_tokens: 0, - completion_tokens: 0, - total_tokens: 0, - }, - }; + const converter = ConverterFactory.getConverter(MODEL_PROTOCOL_PREFIX.GEMINI); + return converter.toOpenAIStreamChunk(geminiChunk, model); } -/** - * Converts a Claude API messages response to an OpenAI chat completion response. - * @param {Object} claudeResponse - The Claude API messages response object. - * @param {string} model - The model name to include in the response. - * @returns {Object} The formatted OpenAI chat completion response. - */ -export function toOpenAIChatCompletionFromClaude(claudeResponse, model) { - if (!claudeResponse || !claudeResponse.content || claudeResponse.content.length === 0) { - return { - id: `chatcmpl-${uuidv4()}`, - object: "chat.completion", - created: Math.floor(Date.now() / 1000), - model: model, - choices: [{ - index: 0, - message: { - role: "assistant", - content: "", - }, - finish_reason: "stop", - }], - usage: { - 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), - }, - }; - } - - const content = processClaudeResponseContent(claudeResponse.content); - const finishReason = claudeResponse.stop_reason === 'end_turn' ? 'stop' : claudeResponse.stop_reason; - - return { - id: `chatcmpl-${uuidv4()}`, - object: "chat.completion", - created: Math.floor(Date.now() / 1000), - model: model, - choices: [{ - index: 0, - message: { - role: "assistant", - content: content - }, - finish_reason: finishReason, - }], - usage: { - 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), - }, - }; -} - -/** - * Processes Claude response content to OpenAI format with multimodal support. - * @param {Array} content - Array of Claude content blocks. - * @returns {string|Array} Processed content. - */ -function processClaudeResponseContent(content) { - if (!content || !Array.isArray(content)) return ''; - - const contentArray = []; - - content.forEach(block => { - if (!block) return; - - switch (block.type) { - case 'text': - contentArray.push({ - type: 'text', - text: block.text || '' - }); - break; - - case 'image': - // Handle image blocks from Claude - if (block.source && block.source.type === 'base64') { - contentArray.push({ - type: 'image_url', - image_url: { - url: `data:${block.source.media_type};base64,${block.source.data}` - } - }); - } - break; - - default: - // Handle other content types as text - if (block.text) { - contentArray.push({ - type: 'text', - text: block.text - }); - } - } - }); - - // Return as array for multimodal, or string for simple text - return contentArray.length === 1 && contentArray[0].type === 'text' - ? contentArray[0].text - : contentArray; -} - -/** - * Converts a Claude API messages stream chunk to an OpenAI chat completion stream chunk. - * Based on the official Claude Messages API stream events. - * @param {Object} claudeChunk - The Claude API messages stream chunk object. - * @param {string} [model] - Optional model name to include in the response. - * @returns {Object} The formatted OpenAI chat completion stream chunk, or an empty object for events that don't map. - */ export function toOpenAIStreamChunkFromClaude(claudeChunk, model) { - if (!claudeChunk) { - return null; - } - return { - id: `chatcmpl-${uuidv4()}`, // uuidv4 needs to be imported or handled - object: "chat.completion.chunk", - created: Math.floor(Date.now() / 1000), - model: model, - system_fingerprint: "", - choices: [{ - index: 0, - delta: { - content: claudeChunk, - reasoning_content: "" - }, - finish_reason: !claudeChunk ? 'stop' : null, - message: { - content: claudeChunk, - reasoning_content: "" - } - }], - usage:{ - prompt_tokens: 0, - completion_tokens: 0, - total_tokens: 0, - }, - }; + const converter = ConverterFactory.getConverter(MODEL_PROTOCOL_PREFIX.CLAUDE); + return converter.toOpenAIStreamChunk(claudeChunk, model); +} + +export function toOpenAIModelListFromGemini(geminiModels) { + const converter = ConverterFactory.getConverter(MODEL_PROTOCOL_PREFIX.GEMINI); + return converter.toOpenAIModelList(geminiModels); } -/** - * Converts a Claude API model list response to an OpenAI model list response. - * @param {Array} claudeModels - The array of model objects from Claude API. - * @returns {Object} The formatted OpenAI model list response. - */ export function toOpenAIModelListFromClaude(claudeModels) { - return { - object: "list", - data: claudeModels.models.map(m => ({ - id: m.id || m.name, // Claude models might use 'name' instead of 'id' - object: "model", - created: Math.floor(Date.now() / 1000), // Claude may not provide 'created' timestamp - owned_by: "anthropic", - // You can add more properties here if they exist in Claude's model response - // and you want to map them to OpenAI's format, e.g., permissions. - })), - }; + const converter = ConverterFactory.getConverter(MODEL_PROTOCOL_PREFIX.CLAUDE); + return converter.toOpenAIModelList(claudeModels); } -/** - * Converts an OpenAI chat completion response to a Claude API messages response. - * @param {Object} openaiResponse - The OpenAI API chat completion response object. - * @param {string} model - The model name to include in the response. - * @returns {Object} The formatted Claude API messages response. - */ -export function toClaudeChatCompletionFromOpenAI(openaiResponse, model) { - if (!openaiResponse || !openaiResponse.choices || openaiResponse.choices.length === 0) { - return { - id: `msg_${uuidv4()}`, - type: "message", - role: "assistant", - content: [], - model: model, - stop_reason: "end_turn", - stop_sequence: null, - usage: { - input_tokens: openaiResponse?.usage?.prompt_tokens || 0, - output_tokens: openaiResponse?.usage?.completion_tokens || 0 - } - }; - } - - const choice = openaiResponse.choices[0]; - const contentList = []; - - // Handle tool calls - const toolCalls = choice.message?.tool_calls || []; - for (const toolCall of toolCalls.filter(tc => tc && typeof tc === 'object')) { - if (toolCall.function) { - const func = toolCall.function; - const argStr = func.arguments || "{}"; - let argObj; - try { - argObj = typeof argStr === 'string' ? JSON.parse(argStr) : argStr; - } catch (e) { - argObj = {}; - } - contentList.push({ - type: "tool_use", - id: toolCall.id || "", - name: func.name || "", - input: argObj, - }); - } - } - - // Handle text content - const contentText = choice.message?.content || ""; - if (contentText) { - // 使用 _extractThinkingFromOpenAIText 提取 thinking 内容 - const extractedContent = _extractThinkingFromOpenAIText(contentText); - if (Array.isArray(extractedContent)) { - contentList.push(...extractedContent); - } else { - contentList.push({ type: "text", text: extractedContent }); - } - } - - // Map OpenAI finish reason to Claude stop reason - const stopReason = _mapFinishReason( - choice.finish_reason || "stop", - "openai", - "anthropic" - ); - - return { - id: `msg_${uuidv4()}`, - type: "message", - role: "assistant", - content: contentList, - model: model, - stop_reason: stopReason, - stop_sequence: null, - usage: { - input_tokens: openaiResponse.usage?.prompt_tokens || 0, - output_tokens: openaiResponse.usage?.completion_tokens || 0 - } - }; -} - -/** - * Converts a Claude API request body to an OpenAI chat completion request body. - * Handles system instructions and multimodal content. - * @param {Object} claudeRequest - The request body from the Claude API. - * @returns {Object} The formatted request body for the OpenAI API. - */ -export function toOpenAIRequestFromClaude(claudeRequest) { - const openaiMessages = []; - let systemMessageContent = ''; - - // Add system message if present - if (claudeRequest.system) { - systemMessageContent = claudeRequest.system; - } - - // Process messages - if (claudeRequest.messages && Array.isArray(claudeRequest.messages)) { - const tempOpenAIMessages = []; - for (const msg of claudeRequest.messages) { - const role = msg.role; - - // 处理用户的工具结果消息 - if (role === "user" && Array.isArray(msg.content)) { - const hasToolResult = msg.content.some( - item => item && typeof item === 'object' && item.type === "tool_result" - ); - - if (hasToolResult) { - for (const item of msg.content) { - if (item && typeof item === 'object' && item.type === "tool_result") { - const toolUseId = item.tool_use_id || item.id || ""; - const contentStr = String(item.content || ""); - tempOpenAIMessages.push({ - role: "tool", - tool_call_id: toolUseId, - content: contentStr, - }); - } - } - continue; // 已处理工具结果,跳过后续处理 - } - } - - // 处理 assistant 消息中的工具调用 - if (role === "assistant" && Array.isArray(msg.content) && msg.content.length > 0) { - const firstPart = msg.content[0]; - if (firstPart.type === "tool_use") { - const funcName = firstPart.name || ""; - const funcArgs = firstPart.input || {}; - tempOpenAIMessages.push({ - role: "assistant", - content: '', - tool_calls: [ - { - id: firstPart.id || `call_${funcName}_1`, - type: "function", - function: { - name: funcName, - arguments: JSON.stringify(funcArgs) - }, - index: firstPart.index || 0 - } - ] - }); - continue; // 已处理 - } - } - - // 普通文本消息 - const contentConverted = processClaudeContentToOpenAIContent(msg.content || ""); - // 跳过空消息,避免在历史中插入空字符串导致模型误判 - if (contentConverted && (Array.isArray(contentConverted) ? contentConverted.length > 0 : contentConverted.trim().length > 0)) { - tempOpenAIMessages.push({ - role: role, - content: contentConverted - }); - } - } - - // ---------------- OpenAI 兼容性校验 ---------------- - // 确保所有 assistant.tool_calls 均有后续 tool 响应消息;否则移除不匹配的 tool_call - const validatedMessages = []; - for (let idx = 0; idx < tempOpenAIMessages.length; idx++) { - const m = tempOpenAIMessages[idx]; - if (m.role === "assistant" && m.tool_calls) { - const callIds = m.tool_calls.map(tc => tc.id).filter(id => id); - // 统计后续是否有对应的 tool 消息 - let unmatched = new Set(callIds); - for (let laterIdx = idx + 1; laterIdx < tempOpenAIMessages.length; laterIdx++) { - const later = tempOpenAIMessages[laterIdx]; - if (later.role === "tool" && unmatched.has(later.tool_call_id)) { - unmatched.delete(later.tool_call_id); - } - if (unmatched.size === 0) { - break; - } - } - if (unmatched.size > 0) { - // 移除无匹配的 tool_call - m.tool_calls = m.tool_calls.filter(tc => !unmatched.has(tc.id)); - // 如果全部被移除,则降级为普通 assistant 文本消息 - if (m.tool_calls.length === 0) { - delete m.tool_calls; - if (m.content === null) { - m.content = ""; - } - } - } - } - validatedMessages.push(m); - } - openaiMessages.push(...validatedMessages); - } - - const openaiRequest = { - model: claudeRequest.model, // Default OpenAI model - messages: openaiMessages, - max_tokens: checkAndAssignOrDefault(claudeRequest.max_tokens, DEFAULT_MAX_TOKENS), - temperature: checkAndAssignOrDefault(claudeRequest.temperature, DEFAULT_TEMPERATURE), - top_p: checkAndAssignOrDefault(claudeRequest.top_p, DEFAULT_TOP_P), - stream: claudeRequest.stream, // Stream mode is handled by different endpoint - }; - - // Process tools - if (claudeRequest.tools) { - const openaiTools = []; - for (const tool of claudeRequest.tools) { - openaiTools.push({ - type: "function", - function: { - name: tool.name || "", - description: tool.description || "", - parameters: _cleanJsonSchemaProperties(tool.input_schema || {}) // 使用清理函数 - } - }); - } - openaiRequest.tools = openaiTools; - openaiRequest.tool_choice = "auto"; - } - - // 处理思考预算转换 (Anthropic thinking -> OpenAI reasoning_effort + max_completion_tokens) - if (claudeRequest.thinking && claudeRequest.thinking.type === "enabled") { - const budgetTokens = claudeRequest.thinking.budget_tokens; - // 根据budget_tokens智能判断reasoning_effort等级 - const reasoningEffort = _determineReasoningEffortFromBudget(budgetTokens); - openaiRequest.reasoning_effort = reasoningEffort; - - // 处理max_completion_tokens的优先级逻辑 - let maxCompletionTokens = null; - - // 优先级1:客户端传入的max_tokens - if (claudeRequest.max_tokens !== undefined) { - maxCompletionTokens = claudeRequest.max_tokens; - delete openaiRequest.max_tokens; // 移除max_tokens,使用max_completion_tokens - console.info(`Using client max_tokens as max_completion_tokens: ${maxCompletionTokens}`); - } else { - // 优先级2:环境变量OPENAI_REASONING_MAX_TOKENS - const envMaxTokens = process.env.OPENAI_REASONING_MAX_TOKENS; - if (envMaxTokens) { - try { - maxCompletionTokens = parseInt(envMaxTokens, 10); - console.info(`Using OPENAI_REASONING_MAX_TOKENS from environment: ${maxCompletionTokens}`); - } catch (e) { - console.warn(`Invalid OPENAI_REASONING_MAX_TOKENS value '${envMaxTokens}', must be integer`); - } - } - - if (!envMaxTokens) { - // 优先级3:都没有则报错 - throw new Error("For OpenAI reasoning models, max_completion_tokens is required. Please specify max_tokens in the request or set OPENAI_REASONING_MAX_TOKENS environment variable."); - } - } - openaiRequest.max_completion_tokens = maxCompletionTokens; - console.info(`Anthropic thinking enabled -> OpenAI reasoning_effort='${reasoningEffort}', max_completion_tokens=${maxCompletionTokens}`); - if (budgetTokens) { - console.info(`Budget tokens: ${budgetTokens} -> reasoning_effort: '${reasoningEffort}'`); - } - } - - // Add system message at the beginning if present - if (systemMessageContent) { - let stringifiedSystemMessageContent = systemMessageContent; - if(Array.isArray(systemMessageContent)){ - stringifiedSystemMessageContent = systemMessageContent.map(item => - typeof item === 'string' ? item : item.text).join('\n'); - } - openaiRequest.messages.unshift({ role: 'system', content: stringifiedSystemMessageContent }); - } - - return openaiRequest; -} - - -/** - * Processes Claude content to OpenAI content format with multimodal support. - * @param {Array} content - Array of Claude content blocks. - * @returns {Array} OpenAI content format. - */ -function processClaudeContentToOpenAIContent(content) { - if (!content || !Array.isArray(content)) return []; - - const contentArray = []; - - content.forEach(block => { - if (!block) return; - - switch (block.type) { - case 'text': - if (block.text) { - contentArray.push({ - type: 'text', - text: block.text - }); - } - break; - - case 'image': - // Handle image blocks from Claude - if (block.source && block.source.type === 'base64') { - contentArray.push({ - type: 'image_url', - image_url: { - url: `data:${block.source.media_type};base64,${block.source.data}` - } - }); - } - break; - - case 'tool_use': - // Handle tool use as text - contentArray.push({ - type: 'text', - text: `[Tool use: ${block.name}]` - }); - break; - - case 'tool_result': - // Handle tool results as text - contentArray.push({ - type: 'text', - text: typeof block.content === 'string' ? block.content : JSON.stringify(block.content) - }); - break; - - default: - // Handle any other content types as text - if (block.text) { - contentArray.push({ - type: 'text', - text: block.text - }); - } - } - }); - - return contentArray; -} - -// ============================================================================= -// Gemini 相关转换函数 -// ============================================================================= - -/** - * Converts an OpenAI chat completion request body to a Gemini API request body. - * Handles system instructions and merges consecutive messages of the same role with multimodal support. - * @param {Object} openaiRequest - The request body from the OpenAI API. - * @returns {Object} The formatted request body for the Gemini API. - */ -export function toGeminiRequestFromOpenAI(openaiRequest) { - const messages = openaiRequest.messages || []; - const { systemInstruction, nonSystemMessages } = extractAndProcessSystemMessages(messages); - - // Process messages with role conversion and multimodal support - const processedMessages = []; - let lastMessage = null; - - for (const message of nonSystemMessages) { - const geminiRole = message.role === 'assistant' ? 'model' : message.role; - - // Handle tool responses - if (geminiRole === 'tool') { - if (lastMessage) processedMessages.push(lastMessage); - processedMessages.push({ - role: 'function', - parts: [{ - functionResponse: { - name: message.name, - response: { content: safeParseJSON(message.content) } - } - }] - }); - lastMessage = null; - continue; - } - - // Process multimodal content - const processedContent = processOpenAIContentToGeminiParts(message.content); - - // Merge consecutive text messages - if (lastMessage && lastMessage.role === geminiRole && !message.tool_calls && - Array.isArray(processedContent) && processedContent.every(p => p.text) && - Array.isArray(lastMessage.parts) && lastMessage.parts.every(p => p.text)) { - lastMessage.parts.push(...processedContent); - continue; - } - - if (lastMessage) processedMessages.push(lastMessage); - lastMessage = { role: geminiRole, parts: processedContent }; - } - if (lastMessage) processedMessages.push(lastMessage); - - // Build Gemini request - const geminiRequest = { - contents: processedMessages.filter(item => item.parts && item.parts.length > 0) - }; - - if (systemInstruction) geminiRequest.systemInstruction = systemInstruction; - - // Handle tools - if (openaiRequest.tools?.length) { - geminiRequest.tools = [{ - functionDeclarations: openaiRequest.tools.map(t => { - // Ensure tool is a valid object and has function property - if (!t || typeof t !== 'object' || !t.function) { - console.warn("Skipping invalid tool declaration in openaiRequest.tools."); - return null; // Return null for invalid tools, filter out later - } - - const func = t.function; - // Clean parameters schema for Gemini compatibility - const parameters = _cleanJsonSchemaProperties(func.parameters || {}); - - return { - name: String(func.name || ''), // Ensure name is string - description: String(func.description || ''), // Ensure description is string - parameters: parameters // Use cleaned parameters - }; - }).filter(Boolean) // Filter out any nulls from invalid tool declarations - }]; - // If no valid functionDeclarations, remove the tools array - if (geminiRequest.tools[0].functionDeclarations.length === 0) { - delete geminiRequest.tools; - } - } - - if (openaiRequest.tool_choice) { - geminiRequest.toolConfig = buildToolConfig(openaiRequest.tool_choice); - } - - // Add generation config - const config = buildGenerationConfig(openaiRequest); - if (Object.keys(config).length) geminiRequest.generationConfig = config; - - // Validation - if (geminiRequest.contents[0]?.role !== 'user') { - console.warn(`[Request Conversion] Warning: Conversation does not start with a 'user' role.`); - } - - return geminiRequest; -} - -/** - * Processes OpenAI content to Gemini parts format with multimodal support. - * @param {string|Array} content - OpenAI message content. - * @returns {Array} Array of Gemini parts. - */ -function processOpenAIContentToGeminiParts(content) { - if (!content) return []; - - // Handle string content - if (typeof content === 'string') { - return [{ text: content }]; - } - - // Handle array content (multimodal) - if (Array.isArray(content)) { - const parts = []; - - content.forEach(item => { - if (!item) return; - - switch (item.type) { - case 'text': - if (item.text) { - parts.push({ text: item.text }); - } - break; - - case 'image_url': - if (item.image_url) { - const imageUrl = typeof item.image_url === 'string' - ? item.image_url - : item.image_url.url; - - if (imageUrl.startsWith('data:')) { - // Handle base64 data URL - const [header, data] = imageUrl.split(','); - const mimeType = header.match(/data:([^;]+)/)?.[1] || 'image/jpeg'; - parts.push({ - inlineData: { - mimeType, - data - } - }); - } else { - // Handle regular URL - parts.push({ - fileData: { - mimeType: 'image/jpeg', // Default MIME type - fileUri: imageUrl - } - }); - } - } - break; - - case 'audio': - // Handle audio content - if (item.audio_url) { - const audioUrl = typeof item.audio_url === 'string' - ? item.audio_url - : item.audio_url.url; - - if (audioUrl.startsWith('data:')) { - const [header, data] = audioUrl.split(','); - const mimeType = header.match(/data:([^;]+)/)?.[1] || 'audio/wav'; - parts.push({ - inlineData: { - mimeType, - data - } - }); - } else { - parts.push({ - fileData: { - mimeType: 'audio/wav', // Default MIME type - fileUri: audioUrl - } - }); - } - } - break; - } - }); - - return parts; - } - - return []; -} - -function safeParseJSON(str) { - if (!str) { - return str; - } - let cleanedStr = str; - - // 处理可能被截断的转义序列 - if (cleanedStr.endsWith('\\') && !cleanedStr.endsWith('\\\\')) { - cleanedStr = cleanedStr.substring(0, cleanedStr.length - 1); // 移除悬挂的反斜杠 - } else if (cleanedStr.endsWith('\\u') || cleanedStr.endsWith('\\u0') || cleanedStr.endsWith('\\u00')) { - // 不完整的Unicode转义序列 - const idx = cleanedStr.lastIndexOf('\\u'); - cleanedStr = cleanedStr.substring(0, idx); - } - - try { - return JSON.parse(cleanedStr || '{}'); - } catch (e) { - // 如果清理后仍然无法解析,则返回原始字符串或进行其他错误处理 - return str; - } -} - -function buildToolConfig(toolChoice) { - if (typeof toolChoice === 'string' && ['none', 'auto'].includes(toolChoice)) { - return { functionCallingConfig: { mode: toolChoice.toUpperCase() } }; - } - if (typeof toolChoice === 'object' && toolChoice.function) { - return { functionCallingConfig: { mode: 'ANY', allowedFunctionNames: [toolChoice.function.name] } }; - } - return null; -} - -/** - * 根据 tool_result 字段构造 Gemini functionResponse - * @param {Object} item - 工具结果项 - * @returns {Object|null} functionResponse 对象 - */ -function _buildFunctionResponse(item) { - if (!item || typeof item !== 'object') { - return null; - } - - // 判定是否为工具结果 - const isResult = ( - item.type === "tool_result" || - item.tool_use_id !== undefined || - item.tool_output !== undefined || - item.result !== undefined || - item.content !== undefined - ); - if (!isResult) { - return null; - } - - // 提取函数名 - let funcName = null; - - // 方法1:从映射表中获取(Anthropic格式) - const toolUseId = item.tool_use_id || item.id; - // 这里需要注意,AnthropicConverter内部维护的_toolUseMapping是类的私有属性,在convert.js中无法直接访问 - // 因此,这里需要依赖全局的toolStateManager - // if (toolUseId && this._toolUseMapping) { // 这行代码在convert.js中将无法使用 - // funcName = this._toolUseMapping[toolUseId]; - // } - - // 方法1.5:使用全局工具状态管理器 - if (!funcName && toolUseId) { - // 先尝试从ID中提取可能的函数名 - let potentialFuncName = null; - if (String(toolUseId).startsWith("call_")) { - const nameAndHash = toolUseId.substring(4); // 去掉 "call_" 前缀 - potentialFuncName = nameAndHash.substring(0, nameAndHash.lastIndexOf("_")); - } - - // 检查全局管理器中是否有对应的映射 - if (potentialFuncName) { - const storedId = toolStateManager.getToolId(potentialFuncName); - if (storedId === toolUseId) { - funcName = potentialFuncName; - } - } - } - - // 方法2:从 tool_use_id 中提取(OpenAI格式) - if (!funcName && toolUseId && String(toolUseId).startsWith("call_")) { - // 格式: call__ ,函数名可能包含多个下划线 - const nameAndHash = toolUseId.substring(4); // 去掉 "call_" 前缀 - funcName = nameAndHash.substring(0, nameAndHash.lastIndexOf("_")); // 去掉最后一个 hash 段 - } - - // 方法3:直接从字段获取 - if (!funcName) { - funcName = ( - item.tool_name || - item.name || - item.function_name - ); - } - - if (!funcName) { - return null; - } - - // 提取结果内容 - let funcResponse = null; - - // 尝试多个可能的结果字段 - for (const key of ["content", "tool_output", "output", "response", "result"]) { - if (item[key] !== undefined) { - funcResponse = item[key]; - break; - } - } - - // 如果 content 是列表,尝试提取文本 - if (Array.isArray(funcResponse) && funcResponse.length > 0) { - const textParts = funcResponse - .filter(p => p && typeof p === 'object' && p.type === "text") - .map(p => p.text || ""); - if (textParts.length > 0) { - funcResponse = textParts.join(""); - } - } - - // 确保有响应内容 - if (funcResponse === null || funcResponse === undefined) { - funcResponse = ""; - } - - // Gemini 要求 response 为 JSON 对象,若为原始字符串则包装 - if (typeof funcResponse !== 'object') { - funcResponse = { content: String(funcResponse) }; - } - - return { - functionResponse: { - name: funcName, - response: funcResponse - } - }; -} - -/** - * Converts a Gemini API model list response to a Claude API model list response. - * @param {Object} geminiModels - The Gemini API model list response object. - * @returns {Object} The formatted Claude API model list response. - */ -export function toClaudeModelListFromGemini(geminiModels) { - return { - models: geminiModels.models.map(m => ({ - name: m.name.startsWith('models/') ? m.name.substring(7) : m.name, // 移除 'models/' 前缀作为 name - // Claude models 可能包含其他字段,这里使用默认值 - description: "", // Gemini models 不提供描述 - // Claude API 可能需要其他字段,根据实际 API 文档调整 - })), - }; -} - -/** - * Converts an OpenAI API model list response to a Claude API model list response. - * @param {Object} openaiModels - The OpenAI API model list response object. - * @returns {Object} The formatted Claude API model list response. - */ -export function toClaudeModelListFromOpenAI(openaiModels) { - return { - models: openaiModels.data.map(m => ({ - name: m.id, // OpenAI 的 id 映射为 Claude 的 name - // Claude models 可能包含其他字段,这里使用默认值 - description: "", // OpenAI models 不提供描述 - // Claude API 可能需要其他字段,根据实际 API 文档调整 - })), - }; -} - -/** - * 从OpenAI文本中提取thinking内容,返回Anthropic格式的content blocks - * @param {string} text - 文本内容 - * @returns {string|Array} 提取后的内容 - */ -function _extractThinkingFromOpenAIText(text) { - // 匹配 ... 标签 - const thinkingPattern = /\s*(.*?)\s*<\/thinking>/gs; - const matches = [...text.matchAll(thinkingPattern)]; - - const contentBlocks = []; - let lastEnd = 0; - - for (const match of matches) { - // 添加thinking标签之前的文本(如果有) - const beforeText = text.substring(lastEnd, match.index).trim(); - if (beforeText) { - contentBlocks.push({ - type: "text", - text: beforeText - }); - } - - // 添加thinking内容 - const thinkingText = match[1].trim(); - if (thinkingText) { - contentBlocks.push({ - type: "thinking", - thinking: thinkingText - }); - } - - lastEnd = match.index + match[0].length; - } - - // 添加最后一个thinking标签之后的文本(如果有) - const afterText = text.substring(lastEnd).trim(); - if (afterText) { - contentBlocks.push({ - type: "text", - text: afterText - }); - } - - // 如果没有找到thinking标签,返回原文本 - if (contentBlocks.length === 0) { - return text; - } - - // 如果只有一个文本块,返回字符串 - if (contentBlocks.length === 1 && contentBlocks[0].type === "text") { - return contentBlocks[0].text; - } - - return contentBlocks; -} - -/** - * Converts an OpenAI chat completion stream chunk to a Claude API messages stream chunk. - * @param {Object} openaiChunk - The OpenAI API chat completion stream chunk object. - * @param {string} [model] - Optional model name to include in the response. - * @returns {Object} The formatted Claude API messages stream chunk. - */ -export function toClaudeStreamChunkFromOpenAI(openaiChunk, model) { - if (!openaiChunk) { - return null; - } - - // 工具调用 - if ( Array.isArray(openaiChunk)) { - const toolCall = openaiChunk[0]; // 假设每次只处理一个工具调用 - if (toolCall) { - if (toolCall.function && toolCall.function.name) { - const toolUseBlock = { - type: "tool_use", - id: toolCall.id || `call_${toolCall.function.name}_${Date.now()}`, - name: toolCall.function.name, - input: toolCall.function.arguments ? JSON.parse(toolCall.function.arguments) : {} - }; - return { type: "content_block_start", index: 1, content_block: toolUseBlock }; - } - } - } - - // 文本内容 - if (typeof openaiChunk === 'string') { - return { - type: "content_block_delta", - index: 0, - delta: { - type: "text_delta", - text: openaiChunk - } - }; - } - return null; -} - -function buildGenerationConfig({ temperature, max_tokens, top_p, stop }) { - const config = {}; - config.temperature = checkAndAssignOrDefault(temperature, DEFAULT_TEMPERATURE); - config.maxOutputTokens = checkAndAssignOrDefault(max_tokens, DEFAULT_GEMINI_MAX_TOKENS); - config.topP = checkAndAssignOrDefault(top_p, DEFAULT_TOP_P); - if (stop !== undefined) config.stopSequences = Array.isArray(stop) ? stop : [stop]; - return config; -} - -/** - * Converts an OpenAI chat completion request body to a Claude API request body. - * Handles system instructions, tool calls, and multimodal content. - * @param {Object} openaiRequest - The request body from the OpenAI API. - * @returns {Object} The formatted request body for the Claude API. - */ +// Claude 相关转换 export function toClaudeRequestFromOpenAI(openaiRequest) { - const messages = openaiRequest.messages || []; - const { systemInstruction, nonSystemMessages } = extractAndProcessSystemMessages(messages); - - const claudeMessages = []; - - for (const message of nonSystemMessages) { - const role = message.role === 'assistant' ? 'assistant' : 'user'; - let content = []; - - if (message.role === 'tool') { - // Claude expects tool_result to be in a 'user' message - // The content of a tool message is a single tool_result block - content.push({ - type: 'tool_result', - tool_use_id: message.tool_call_id, // Use tool_call_id from OpenAI tool message - content: safeParseJSON(message.content) // Parse content as JSON if possible - }); - claudeMessages.push({ role: 'user', content: content }); - } else if (message.role === 'assistant' && message.tool_calls?.length) { - // Assistant message with tool calls - properly format as tool_use blocks - // Claude expects tool_use to be in an 'assistant' message - const toolUseBlocks = message.tool_calls.map(tc => ({ - type: 'tool_use', - id: tc.id, - name: tc.function.name, - input: safeParseJSON(tc.function.arguments) - })); - claudeMessages.push({ role: 'assistant', content: toolUseBlocks }); - } else { - // Regular user or assistant message (text and multimodal) - if (typeof message.content === 'string') { - if (message.content) { - content.push({ type: 'text', text: message.content }); - } - } else if (Array.isArray(message.content)) { - message.content.forEach(item => { - if (!item) return; - switch (item.type) { - case 'text': - if (item.text) { - content.push({ type: 'text', text: item.text }); - } - break; - case 'image_url': - if (item.image_url) { - const imageUrl = typeof item.image_url === 'string' - ? item.image_url - : item.image_url.url; - if (imageUrl.startsWith('data:')) { - const [header, data] = imageUrl.split(','); - const mediaType = header.match(/data:([^;]+)/)?.[1] || 'image/jpeg'; - content.push({ - type: 'image', - source: { - type: 'base64', - media_type: mediaType, - data: data - } - }); - } else { - // Claude requires base64 for images, so for URLs, we'll represent as text - content.push({ type: 'text', text: `[Image: ${imageUrl}]` }); - } - } - break; - case 'audio': - // Handle audio content as text placeholder - if (item.audio_url) { - const audioUrl = typeof item.audio_url === 'string' - ? item.audio_url - : item.audio_url.url; - content.push({ type: 'text', text: `[Audio: ${audioUrl}]` }); - } - break; - } - }); - } - // Only add message if content is not empty - if (content.length > 0) { - claudeMessages.push({ role: role, content: content }); - } - } - } - - const claudeRequest = { - model: openaiRequest.model, - messages: claudeMessages, - max_tokens: checkAndAssignOrDefault(openaiRequest.max_tokens, DEFAULT_MAX_TOKENS), - temperature: checkAndAssignOrDefault(openaiRequest.temperature, DEFAULT_TEMPERATURE), - top_p: checkAndAssignOrDefault(openaiRequest.top_p, DEFAULT_TOP_P), - }; - - if (systemInstruction) { - claudeRequest.system = extractTextFromMessageContent(systemInstruction.parts[0].text); - } - - if (openaiRequest.tools?.length) { - claudeRequest.tools = openaiRequest.tools.map(t => ({ - name: t.function.name, - description: t.function.description || '', - input_schema: t.function.parameters || { type: 'object', properties: {} } - })); - claudeRequest.tool_choice = buildClaudeToolChoice(openaiRequest.tool_choice); - } - - return claudeRequest; + const converter = ConverterFactory.getConverter(MODEL_PROTOCOL_PREFIX.OPENAI); + return converter.toClaudeRequest(openaiRequest); } -function buildClaudeToolChoice(toolChoice) { - if (typeof toolChoice === 'string') { - const mapping = { auto: 'auto', none: 'none', required: 'any' }; - return { type: mapping[toolChoice] }; - } - if (typeof toolChoice === 'object' && toolChoice.function) { - return { type: 'tool', name: toolChoice.function.name }; - } - return undefined; -} - -/** - * Extracts and combines all 'system' role messages into a single system instruction. - * Filters out system messages and returns the remaining non-system messages. - * @param {Array} messages - Array of message objects from OpenAI request. - * @returns {{systemInstruction: Object|null, nonSystemMessages: Array}} - * An object containing the system instruction and an array of non-system messages. - */ -export function extractAndProcessSystemMessages(messages) { - const systemContents = []; - const nonSystemMessages = []; - - for (const message of messages) { - if (message.role === 'system') { - systemContents.push(extractTextFromMessageContent(message.content)); - } else { - nonSystemMessages.push(message); - } - } - - let systemInstruction = null; - if (systemContents.length > 0) { - systemInstruction = { - parts: [{ - text: systemContents.join('\n') - }] - }; - } - return { systemInstruction, nonSystemMessages }; -} - -/** - * Extracts text from various forms of message content. - * @param {string|Array} content - The content from a message object. - * @returns {string} The extracted text. - */ -export function extractTextFromMessageContent(content) { - if (typeof content === 'string') { - return content; - } - if (Array.isArray(content)) { - return content - .filter(part => part.type === 'text' && part.text) - .map(part => part.text) - .join('\n'); - } - return ''; -} - -/** - * Converts a Claude API request body to a Gemini API request body. - * Handles system instructions and multimodal content. - * @param {Object} claudeRequest - The request body from the Claude API. - * @returns {Object} The formatted request body for the Gemini API. - */ -export function toGeminiRequestFromClaude(claudeRequest) { - // Ensure claudeRequest is a valid object - if (!claudeRequest || typeof claudeRequest !== 'object') { - console.warn("Invalid claudeRequest provided to toGeminiRequestFromClaude."); - return { contents: [] }; - } - - const geminiRequest = { - contents: [] - }; - - // Handle system instruction - if (claudeRequest.system) { - let incomingSystemText = null; - if (typeof claudeRequest.system === 'string') { - incomingSystemText = claudeRequest.system; - } else if (typeof claudeRequest.system === 'object') { - incomingSystemText = JSON.stringify(claudeRequest.system); - } else if (claudeRequest.messages?.length > 0) { - // Fallback to first user message if no system property - const userMessage = claudeRequest.messages.find(m => m.role === 'user'); - if (userMessage) { - if (Array.isArray(userMessage.content)) { - incomingSystemText = userMessage.content.map(block => block.text).join(''); - } else { - incomingSystemText = userMessage.content; - } - } - } - geminiRequest.systemInstruction = { - parts: [{ text: incomingSystemText}] // Ensure system is string - }; - } - - // Process messages - if (Array.isArray(claudeRequest.messages)) { - claudeRequest.messages.forEach(message => { - // Ensure message is a valid object and has a role and content - if (!message || typeof message !== 'object' || !message.role || !message.content) { - console.warn("Skipping invalid message in claudeRequest.messages."); - return; - } - - const geminiRole = message.role === 'assistant' ? 'model' : 'user'; - const processedParts = processClaudeContentToGeminiParts(message.content); - - // If the processed parts contain a function response, it should be a 'function' role message - // Claude's tool_result block does not contain the function name, only tool_use_id. - // We need to infer the function name from the previous tool_use message. - // For simplicity in this conversion, we'll assume the tool_use_id is the function name - // or that the tool_result is always preceded by a tool_use with the correct name. - // A more robust solution would involve tracking tool_use_ids to function names. - const functionResponsePart = processedParts.find(part => part.functionResponse); - if (functionResponsePart) { - geminiRequest.contents.push({ - role: 'function', - parts: [functionResponsePart] - }); - } else if (processedParts.length > 0) { // Only push if there are actual parts - geminiRequest.contents.push({ - role: geminiRole, - parts: processedParts - }); - } - }); - } - - // Add generation config - const generationConfig = {}; - generationConfig.maxOutputTokens = checkAndAssignOrDefault(claudeRequest.max_tokens, DEFAULT_GEMINI_MAX_TOKENS); - generationConfig.temperature = checkAndAssignOrDefault(claudeRequest.temperature, DEFAULT_TEMPERATURE); - generationConfig.topP = checkAndAssignOrDefault(claudeRequest.top_p, DEFAULT_TOP_P); - - if (Object.keys(generationConfig).length > 0) { - geminiRequest.generationConfig = generationConfig; - } - - // Handle tools - if (Array.isArray(claudeRequest.tools)) { - geminiRequest.tools = [{ - functionDeclarations: claudeRequest.tools.map(tool => { - // Ensure tool is a valid object and has a name - if (!tool || typeof tool !== 'object' || !tool.name) { - console.warn("Skipping invalid tool declaration in claudeRequest.tools."); - return null; // Return null for invalid tools, filter out later - } - - // Filter out TodoWrite tool - // if (tool.name === 'TodoWrite') { - // console.log("Filtering out TodoWrite tool"); - // return null; - // } - - delete tool.input_schema.$schema; - return { - name: String(tool.name), // Ensure name is string - description: String(tool.description || ''), // Ensure description is string - parameters: tool.input_schema && typeof tool.input_schema === 'object' ? tool.input_schema : { type: 'object', properties: {} } - }; - }).filter(Boolean) // Filter out any nulls from invalid tool declarations - }]; - // If no valid functionDeclarations, remove the tools array - if (geminiRequest.tools[0].functionDeclarations.length === 0) { - delete geminiRequest.tools; - } - } - - // Handle tool_choice - if (claudeRequest.tool_choice) { - geminiRequest.toolConfig = buildGeminiToolConfigFromClaude(claudeRequest.tool_choice); - } - - return geminiRequest; -} - -/** - * Builds Gemini toolConfig from Claude tool_choice. - * @param {Object} claudeToolChoice - The tool_choice object from Claude API. - * @returns {Object|undefined} The formatted toolConfig for Gemini API, or undefined if invalid. - */ -function buildGeminiToolConfigFromClaude(claudeToolChoice) { - if (!claudeToolChoice || typeof claudeToolChoice !== 'object' || !claudeToolChoice.type) { - console.warn("Invalid claudeToolChoice provided to buildGeminiToolConfigFromClaude."); - return undefined; - } - - switch (claudeToolChoice.type) { - case 'auto': - return { functionCallingConfig: { mode: 'AUTO' } }; - case 'none': - return { functionCallingConfig: { mode: 'NONE' } }; - case 'tool': - if (claudeToolChoice.name && typeof claudeToolChoice.name === 'string') { - return { functionCallingConfig: { mode: 'ANY', allowedFunctionNames: [claudeToolChoice.name] } }; - } - console.warn("Invalid tool name in claudeToolChoice of type 'tool'."); - return undefined; - default: - console.warn(`Unsupported claudeToolChoice type: ${claudeToolChoice.type}`); - return undefined; - } -} - -/** - * Processes Claude content to Gemini parts format with multimodal support. - * @param {string|Array} content - Claude message content. - * @returns {Array} Array of Gemini parts. - */ -function processClaudeContentToGeminiParts(content) { - if (!content) return []; - - // Handle string content - if (typeof content === 'string') { - return [{ text: content }]; - } - - // Handle array content (multimodal) - if (Array.isArray(content)) { - const parts = []; - - content.forEach(block => { - // Ensure block is a valid object and has a type - if (!block || typeof block !== 'object' || !block.type) { - console.warn("Skipping invalid content block in processClaudeContentToGeminiParts."); - return; - } - - switch (block.type) { - case 'text': - if (typeof block.text === 'string') { - parts.push({ text: block.text }); - } else { - console.warn("Invalid text content in Claude text block."); - } - break; - - case 'image': - if (block.source && typeof block.source === 'object' && block.source.type === 'base64' && - typeof block.source.media_type === 'string' && typeof block.source.data === 'string') { - parts.push({ - inlineData: { - mimeType: block.source.media_type, - data: block.source.data - } - }); - } else { - console.warn("Invalid image source in Claude image block."); - } - break; - - case 'tool_use': - if (typeof block.name === 'string' && block.input && typeof block.input === 'object') { - // Filter out TodoWrite tool use - // if (block.name === 'TodoWrite') { - // console.log("Filtering out TodoWrite tool use"); - // break; // Skip adding this tool to parts - // } - parts.push({ - functionCall: { - name: block.name, - args: block.input - } - }); - } else { - console.warn("Invalid tool_use block in Claude content."); - } - break; - - case 'tool_result': - // Claude's tool_result block does not contain the function name, only tool_use_id. - // Gemini's functionResponse requires a function name. - // For now, we'll use the tool_use_id as the name, but this is a potential point of failure - // if the tool_use_id is not the actual function name in Gemini's context. - // A more robust solution would involve tracking the function name from the tool_use block. - if (typeof block.tool_use_id === 'string') { - parts.push({ - functionResponse: { - name: block.tool_use_id, // This might need to be the actual function name - response: { content: block.content } // content can be any JSON-serializable value - } - }); - } else { - console.warn("Invalid tool_result block in Claude content: missing tool_use_id."); - } - break; - - default: - // Handle any other content types as text if they have a text property - if (typeof block.text === 'string') { - parts.push({ text: block.text }); - } else { - console.warn(`Unsupported Claude content block type: ${block.type}. Skipping.`); - } - } - }); - - return parts; - } - - return []; -} - -/** - * Converts a Gemini API response to a Claude API messages response. - * @param {Object} geminiResponse - The Gemini API response object. - * @param {string} model - The model name to include in the response. - * @returns {Object} The formatted Claude API messages response. - */ -export function toClaudeChatCompletionFromGemini(geminiResponse, model) { - // Handle cases where geminiResponse or candidates are missing or empty - if (!geminiResponse || !geminiResponse.candidates || geminiResponse.candidates.length === 0) { - return { - id: `msg_${uuidv4()}`, - type: "message", - role: "assistant", - content: [], // Empty content for no candidates - model: model, - stop_reason: "end_turn", // Default stop reason - stop_sequence: null, - usage: { - input_tokens: geminiResponse?.usageMetadata?.promptTokenCount || 0, - output_tokens: geminiResponse?.usageMetadata?.candidatesTokenCount || 0 - } - }; - } - - const candidate = geminiResponse.candidates[0]; - const content = processGeminiResponseToClaudeContent(geminiResponse); - const finishReason = candidate.finishReason; - let stopReason = "end_turn"; // Default stop reason - - if (finishReason) { - switch (finishReason) { - case 'STOP': - stopReason = 'end_turn'; - break; - case 'MAX_TOKENS': - stopReason = 'max_tokens'; - break; - case 'SAFETY': - stopReason = 'safety'; - break; - case 'RECITATION': - stopReason = 'recitation'; - break; - case 'OTHER': - stopReason = 'other'; - break; - default: - stopReason = 'end_turn'; - } - } - - return { - id: `msg_${uuidv4()}`, - type: "message", - role: "assistant", - content: content, - model: model, - stop_reason: stopReason, - stop_sequence: null, - usage: { - input_tokens: geminiResponse.usageMetadata?.promptTokenCount || 0, - output_tokens: geminiResponse.usageMetadata?.candidatesTokenCount || 0 - } - }; -} - -/** - * Processes Gemini response content to Claude format. - * @param {Object} geminiResponse - The Gemini API response. - * @returns {Array} Array of Claude content blocks. - */ -function processGeminiResponseToClaudeContent(geminiResponse) { - if (!geminiResponse || !geminiResponse.candidates || geminiResponse.candidates.length === 0) return []; - - const content = []; - - for (const candidate of geminiResponse.candidates) { - // 检查完成原因是否为错误类型 - if (candidate.finishReason && candidate.finishReason !== 'STOP') { - // console.log('Gemini response finishReason:', JSON.stringify(candidate)); - // console.warn('Gemini response contains malformed function call:', candidate.finishMessage || 'No finish message'); - - // 将错误信息作为文本内容返回 - if (candidate.finishMessage) { - content.push({ - type: 'text', - text: `Error: ${candidate.finishMessage}` - }); - } - // console.log("Processed content:", content); - continue; // 跳过当前候选的进一步处理 - } - - if (candidate.content && candidate.content.parts) { - for (const part of candidate.content.parts) { - if (part.text) { - content.push({ - type: 'text', - text: part.text - }); - } else if (part.inlineData) { - content.push({ - type: 'image', - source: { - type: 'base64', - media_type: part.inlineData.mimeType, - data: part.inlineData.data - } - }); - } else if (part.functionCall) { - // Convert Gemini functionCall to Claude tool_use - content.push({ - type: 'tool_use', - id: uuidv4(), // Generate a new ID for the tool use - name: part.functionCall.name, - input: part.functionCall.args || {} - }); - } - } - } - } - - return content; -} - -/** - * Converts a Gemini API stream chunk to a Claude API messages stream chunk. - * @param {Object} geminiChunk - The Gemini API stream chunk object. - * @param {string} [model] - Optional model name to include in the response. - * @returns {Object} The formatted Claude API messages stream chunk. - */ -export function toClaudeStreamChunkFromGemini(geminiChunk, model) { - if (!geminiChunk) { - return null; - } - - if (typeof geminiChunk === 'string') { - return { - type: "content_block_delta", - index: 0, - delta: { - type: "text_delta", - text: geminiChunk - } - }; - } - - return null; -} - - -/** - * Converts a Claude API response to an OpenAI Responses API response. - * @param {Object} claudeResponse - The Claude API response object. - * @param {string} model - The model name to include in the response. - * @returns {Object} The formatted OpenAI Responses API response. - */ -export function toOpenAIResponsesFromClaude(claudeResponse, model) { - // 根据参考示例重构响应结构 - const content = processClaudeResponseContent(claudeResponse.content); - const textContent = typeof content === 'string' ? content : JSON.stringify(content); - - // 将claude的内容转换为OpenAI Responses输出格式 - let output = []; - - // 添加文本内容 - output.push({ - type: "message", - id: `msg_${uuidv4().replace(/-/g, '')}`, - summary: [], - type: "message", - role: "assistant", - status: "completed", - content: [{ - annotations: [], - logprobs: [], - text: textContent, - type: "output_text" - }] - }); - - return { - background: false, - created_at: Math.floor(Date.now() / 1000), - error: null, - id: `resp_${uuidv4().replace(/-/g, '')}`, - incomplete_details: null, - max_output_tokens: null, - max_tool_calls: null, - metadata: {}, - model: model || claudeResponse.model, - object: "response", - output: output, - parallel_tool_calls: true, - previous_response_id: null, - prompt_cache_key: null, - reasoning: { - // effort: "minimal", - // summary: "detailed" - }, - safety_identifier: "user-"+uuidv4().replace(/-/g, ''), // 示例值 - service_tier: "default", - status: "completed", - store: false, - temperature: 1, - text: { - format: {type: "text"}, - // verbosity: "medium" - }, - tool_choice: "auto", - tools: [], - top_logprobs: 0, - top_p: 1, - truncation: "disabled", - usage: { - input_tokens: claudeResponse.usage?.input_tokens || 0, // 示例值 - input_tokens_details: { - cached_tokens: claudeResponse.usage?.cache_creation_input_tokens || 0, // 如果有缓存相关数据则使用 - }, - output_tokens: claudeResponse.usage?.output_tokens || 0, // 示例值 - output_tokens_details: { - reasoning_tokens: 0 - }, - total_tokens: (claudeResponse.usage?.input_tokens || 0) + (claudeResponse.usage?.output_tokens || 0) // 示例值 - }, - user: null - }; -} - -/** - * Converts a Gemini API response to an OpenAI Responses API response. - * @param {Object} geminiResponse - The Gemini API response object. - * @param {string} model - The model name to include in the response. - * @returns {Object} The formatted OpenAI Responses API response. - */ -export function toOpenAIResponsesFromGemini(geminiResponse, model) { - // 根据参考示例重构响应结构 - const content = processGeminiResponseContent(geminiResponse); - const textContent = typeof content === 'string' ? content : JSON.stringify(content); - - // 将gemini的内容转换为OpenAI Responses输出格式 - let output = []; - - // 添加文本内容 - output.push({ - id: `msg_${uuidv4().replace(/-/g, '')}`, - summary: [], - type: "message", - role: "assistant", - status: "completed", - content: [{ - annotations: [], - logprobs: [], - text: textContent, - type: "output_text" - }] - }); - - return { - background: false, - created_at: Math.floor(Date.now() / 1000), - error: null, - id: `resp_${uuidv4().replace(/-/g, '')}`, - incomplete_details: null, - max_output_tokens: null, - max_tool_calls: null, - metadata: {}, - model: model, - object: "response", - output: output, - parallel_tool_calls: true, - previous_response_id: null, - prompt_cache_key: null, - reasoning: { - // effort: "minimal", - // summary: "detailed" - }, - safety_identifier: "user-"+uuidv4().replace(/-/g, ''), // 示例值 - service_tier: "default", - status: "completed", - store: false, - temperature: 1, - text: { - format: {type: "text"}, - // verbosity: "medium" - }, - tool_choice: "auto", - tools: [], - top_logprobs: 0, - top_p: 1, - truncation: "disabled", - usage: { - input_tokens: geminiResponse.usageMetadata?.promptTokenCount || 0, // 示例值 - input_tokens_details: { - cached_tokens: geminiResponse.usageMetadata?.cachedTokens || 0, // 使用正确的Gemini缓存字段 - }, - output_tokens: geminiResponse.usageMetadata?.candidatesTokenCount || 0, // 示例值 - output_tokens_details: { - reasoning_tokens: 0 - }, - total_tokens: geminiResponse.usageMetadata?.totalTokenCount || 0, // 示例值 - }, - user: null - }; -} - - -/** - * Converts an OpenAI Responses API request body to a Claude API request body. - * @param {Object} responsesRequest - The request body from the OpenAI Responses API. - * @returns {Object} The formatted request body for the Claude API. - */ export function toClaudeRequestFromOpenAIResponses(responsesRequest) { - // The OpenAI Responses API uses input and instructions instead of messages - const claudeRequest = { - model: responsesRequest.model, - max_tokens: checkAndAssignOrDefault(responsesRequest.max_tokens, DEFAULT_MAX_TOKENS), - temperature: checkAndAssignOrDefault(responsesRequest.temperature, DEFAULT_TEMPERATURE), - top_p: checkAndAssignOrDefault(responsesRequest.top_p, DEFAULT_TOP_P), - }; - - // Process instructions as system message - if (responsesRequest.instructions) { - claudeRequest.system = []; - claudeRequest.system.push({ - text: typeof responsesRequest.instructions === 'string' ? responsesRequest.instructions : JSON.stringify(responsesRequest.instructions) - }); - - } - - const claudeMessages = []; - // Process input as user message content - if (responsesRequest.input) { - if (typeof responsesRequest.input === 'string') { - // Create user message with the string content - claudeMessages.push({ - role: 'user', - content: [{ - type: 'text', - text: responsesRequest.input - }] - }); - } else { - // Handle array of messages or items - process the entire array - for (const message of responsesRequest.input) { - const role = message.role === 'assistant' ? 'assistant' : 'user'; - let content = []; - - if (message.role === 'tool') { - // Claude expects tool_result to be in a 'user' message - // The content of a tool message is a single tool_result block - content.push({ - type: 'tool_result', - tool_use_id: message.tool_call_id, // Use tool_call_id from OpenAI tool message - content: safeParseJSON(message.content) // Parse content as JSON if possible - }); - claudeMessages.push({ role: 'user', content: content }); - } else if (message.role === 'assistant' && message.tool_calls?.length) { - // Assistant message with tool calls - properly format as tool_use blocks - // Claude expects tool_use to be in an 'assistant' message - const toolUseBlocks = message.tool_calls.map(tc => ({ - type: 'tool_use', - id: tc.id, - name: tc.function.name, - input: safeParseJSON(tc.function.arguments) - })); - claudeMessages.push({ role: 'assistant', content: toolUseBlocks }); - } else { - // Regular user or assistant message (text and multimodal) - if (typeof message.content === 'string') { - if (message.content) { - content.push({ type: 'text', text: message.content }); - } - } else if (Array.isArray(message.content)) { - message.content.forEach(item => { - if (!item) return; - switch (item.type) { - case 'input_text': - if (item.text) { - content.push({ type: 'text', text: item.text }); - } - break; - case 'output_text': - if (item.text) { - content.push({ type: 'text', text: item.text }); - } - break; - case 'image_url': - if (item.image_url) { - const imageUrl = typeof item.image_url === 'string' - ? item.image_url - : item.image_url.url; - if (imageUrl.startsWith('data:')) { - const [header, data] = imageUrl.split(','); - const mediaType = header.match(/data:([^;]+)/)?.[1] || 'image/jpeg'; - content.push({ - type: 'image', - source: { - type: 'base64', - media_type: mediaType, - data: data - } - }); - } else { - // Claude requires base64 for images, so for URLs, we'll represent as text - content.push({ type: 'text', text: `[Image: ${imageUrl}]` }); - } - } - break; - case 'audio': - // Handle audio content as text placeholder - if (item.audio_url) { - const audioUrl = typeof item.audio_url === 'string' - ? item.audio_url - : item.audio_url.url; - content.push({ type: 'text', text: `[Audio: ${audioUrl}]` }); - } - break; - } - }); - } - // Only add message if content is not empty - if (content.length > 0) { - claudeMessages.push({ role: role, content: content }); - } - } - } - } - } - - // Process tools if present - // if (responsesRequest.tools && Array.isArray(responsesRequest.tools)) { - // claudeRequest.tools = responsesRequest.tools.map(tool => ({ - // name: tool.name, - // description: tool.description || '', - // input_schema: tool.parameters || { type: 'object', properties: {} } - // })); - // claudeRequest.tool_choice = buildClaudeToolChoice(responsesRequest.tool_choice); - // } - - // Process messages - claudeRequest.messages = claudeMessages; - claudeRequest.stream = responsesRequest.stream || false; - return claudeRequest; + const converter = ConverterFactory.getConverter(MODEL_PROTOCOL_PREFIX.OPENAI_RESPONSES); + return converter.toClaudeRequest(responsesRequest); +} + +export function toClaudeChatCompletionFromOpenAI(openaiResponse, model) { + const converter = ConverterFactory.getConverter(MODEL_PROTOCOL_PREFIX.OPENAI); + return converter.toClaudeResponse(openaiResponse, model); +} + +export function toClaudeChatCompletionFromGemini(geminiResponse, model) { + const converter = ConverterFactory.getConverter(MODEL_PROTOCOL_PREFIX.GEMINI); + return converter.toClaudeResponse(geminiResponse, model); +} + +export function toClaudeStreamChunkFromOpenAI(openaiChunk, model) { + const converter = ConverterFactory.getConverter(MODEL_PROTOCOL_PREFIX.OPENAI); + return converter.toClaudeStreamChunk(openaiChunk, model); +} + +export function toClaudeStreamChunkFromGemini(geminiChunk, model) { + const converter = ConverterFactory.getConverter(MODEL_PROTOCOL_PREFIX.GEMINI); + return converter.toClaudeStreamChunk(geminiChunk, model); +} + +export function toClaudeModelListFromOpenAI(openaiModels) { + const converter = ConverterFactory.getConverter(MODEL_PROTOCOL_PREFIX.OPENAI); + return converter.toClaudeModelList(openaiModels); +} + +export function toClaudeModelListFromGemini(geminiModels) { + const converter = ConverterFactory.getConverter(MODEL_PROTOCOL_PREFIX.GEMINI); + return converter.toClaudeModelList(geminiModels); +} + +// Gemini 相关转换 +export function toGeminiRequestFromOpenAI(openaiRequest) { + const converter = ConverterFactory.getConverter(MODEL_PROTOCOL_PREFIX.OPENAI); + return converter.toGeminiRequest(openaiRequest); +} + +export function toGeminiRequestFromClaude(claudeRequest) { + const converter = ConverterFactory.getConverter(MODEL_PROTOCOL_PREFIX.CLAUDE); + return converter.toGeminiRequest(claudeRequest); } -/** - * Converts an OpenAI Responses API request body to a Gemini API request body. - * @param {Object} responsesRequest - The request body from the OpenAI Responses API. - * @returns {Object} The formatted request body for the Gemini API. - */ export function toGeminiRequestFromOpenAIResponses(responsesRequest) { - // The OpenAI Responses API uses input and instructions instead of messages - const geminiRequest = { - contents: [] - }; + const converter = ConverterFactory.getConverter(MODEL_PROTOCOL_PREFIX.OPENAI_RESPONSES); + return converter.toGeminiRequest(responsesRequest); +} - // Process instructions as system instruction - if (responsesRequest.instructions) { - let instructionsText = ''; - if (typeof responsesRequest.instructions === 'string') { - instructionsText = responsesRequest.instructions; - } else { - instructionsText = JSON.stringify(responsesRequest.instructions); - } - geminiRequest.systemInstruction = { - parts: [{ text: instructionsText }] - }; - } +// OpenAI Responses 相关转换 +export function toOpenAIResponsesFromOpenAI(openaiResponse, model) { + const converter = ConverterFactory.getConverter(MODEL_PROTOCOL_PREFIX.OPENAI); + return converter.toOpenAIResponsesResponse(openaiResponse, model); +} - // Process input as user content - if (responsesRequest.input) { - let inputContent = ''; - if (typeof responsesRequest.input === 'string') { - inputContent = responsesRequest.input; - } else if (Array.isArray(responsesRequest.input)) { - // Handle array of messages or items - if (responsesRequest.input.length > 0) { - // For compatibility, take the content of the last item with text content - const lastInputItem = [...responsesRequest.input].reverse().find(item => - item && ( - (item.content && typeof item.content === 'string') || - (item.content && Array.isArray(item.content) && item.content.some(c => c && c.text)) || - (item.role === 'user' && item.content) - ) - ); +export function toOpenAIResponsesFromClaude(claudeResponse, model) { + const converter = ConverterFactory.getConverter(MODEL_PROTOCOL_PREFIX.CLAUDE); + return converter.toOpenAIResponsesResponse(claudeResponse, model); +} - if (lastInputItem) { - if (typeof lastInputItem.content === 'string') { - inputContent = lastInputItem.content; - } else if (Array.isArray(lastInputItem.content)) { - // Process array of content blocks - inputContent = lastInputItem.content - .filter(block => block && block.text) - .map(block => block.text) - .join(' '); - } else { - // General fallback - inputContent = JSON.stringify(lastInputItem.content || lastInputItem); - } - } - } - } +export function toOpenAIResponsesFromGemini(geminiResponse, model) { + const converter = ConverterFactory.getConverter(MODEL_PROTOCOL_PREFIX.GEMINI); + return converter.toOpenAIResponsesResponse(geminiResponse, model); +} - if (inputContent) { - // Add user message with the input content - geminiRequest.contents.push({ - role: 'user', - parts: [{ text: inputContent }] - }); - } - } else { - // If no input is provided, ensure we have at least one user message for Gemini - geminiRequest.contents.push({ - role: 'user', - parts: [{ text: 'Hello' }] // Default content to satisfy Gemini API requirement - }); - } +export function toOpenAIResponsesStreamChunkFromOpenAI(openaiChunk, model, requestId) { + const converter = ConverterFactory.getConverter(MODEL_PROTOCOL_PREFIX.OPENAI); + return converter.toOpenAIResponsesStreamChunk(openaiChunk, model, requestId); +} - // Add generation config - const generationConfig = {}; - generationConfig.maxOutputTokens = checkAndAssignOrDefault(responsesRequest.max_tokens, DEFAULT_GEMINI_MAX_TOKENS); - generationConfig.temperature = checkAndAssignOrDefault(responsesRequest.temperature, DEFAULT_TEMPERATURE); - generationConfig.topP = checkAndAssignOrDefault(responsesRequest.top_p, DEFAULT_TOP_P); +export function toOpenAIResponsesStreamChunkFromClaude(claudeChunk, model, requestId) { + const converter = ConverterFactory.getConverter(MODEL_PROTOCOL_PREFIX.CLAUDE); + return converter.toOpenAIResponsesStreamChunk(claudeChunk, model, requestId); +} - if (Object.keys(generationConfig).length > 0) { - geminiRequest.generationConfig = generationConfig; - } +export function toOpenAIResponsesStreamChunkFromGemini(geminiChunk, model, requestId) { + const converter = ConverterFactory.getConverter(MODEL_PROTOCOL_PREFIX.GEMINI); + return converter.toOpenAIResponsesStreamChunk(geminiChunk, model, requestId); +} - // Process tools if present - if (responsesRequest.tools && Array.isArray(responsesRequest.tools)) { - geminiRequest.tools = [{ - functionDeclarations: responsesRequest.tools - .filter(tool => tool && (tool.type === 'function' || tool.function)) - .map(tool => { - const func = tool.function || tool; - return { - name: String(func.name || tool.name || ''), - description: String(func.description || tool.description || ''), - parameters: func.parameters || tool.parameters || { type: 'object', properties: {} } - }; - }).filter(Boolean) // Filter out any invalid tools - }]; +// 从 OpenAI Responses 转换到其他格式 +export function toOpenAIRequestFromOpenAIResponses(responsesRequest) { + const converter = ConverterFactory.getConverter(MODEL_PROTOCOL_PREFIX.OPENAI_RESPONSES); + return converter.toOpenAIRequest(responsesRequest); +} - // If no valid functionDeclarations, remove the tools array - if (geminiRequest.tools[0].functionDeclarations.length === 0) { - delete geminiRequest.tools; - } - } +export function toOpenAIChatCompletionFromOpenAIResponses(responsesResponse, model) { + const converter = ConverterFactory.getConverter(MODEL_PROTOCOL_PREFIX.OPENAI_RESPONSES); + return converter.toOpenAIResponse(responsesResponse, model); +} - return geminiRequest; +export function toOpenAIStreamChunkFromOpenAIResponses(responsesChunk, model) { + const converter = ConverterFactory.getConverter(MODEL_PROTOCOL_PREFIX.OPENAI_RESPONSES); + return converter.toOpenAIStreamChunk(responsesChunk, model); +} + +// 辅助函数导出 +export async function extractAndProcessSystemMessages(messages) { + const { Utils } = await import('./converters/utils.js'); + return Utils.extractSystemMessages(messages); +} + +export async function extractTextFromMessageContent(content) { + const { Utils } = await import('./converters/utils.js'); + return Utils.extractText(content); +} + +// ============================================================================= +// 工具函数 +// ============================================================================= + +/** + * 获取所有已注册的协议 + * @returns {Array} 协议前缀数组 + */ +export function getRegisteredProtocols() { + return ConverterFactory.getRegisteredProtocols(); } /** - * Converts a Claude API stream chunk to an OpenAI Responses API stream chunk. - * @param {Object} claudeChunk - The Claude API stream chunk object. - * @param {string} [model] - Optional model name to include in the response. - * @param {string} [requestId] - Optional request ID to maintain stream state across chunks. - * @returns {Array} The formatted OpenAI Responses API stream chunks as an array of events. + * 检查协议是否已注册 + * @param {string} protocol - 协议前缀 + * @returns {boolean} 是否已注册 */ -export function toOpenAIResponsesStreamChunkFromClaude(claudeChunk, model, requestId = null) { - if (!claudeChunk) { - return []; - } - - // 如果没有提供requestId,则生成一个(首次调用时) - const id = requestId || Date.now().toString(); - - // 设置模型信息(仅在新请求时设置) - if (!requestId) { - streamStateManager.setModel(id, model); - } - - // Handle text content from Claude stream - let content = ''; - if (typeof claudeChunk === 'string') { - content = claudeChunk; - } else if (claudeChunk && typeof claudeChunk === 'object' && claudeChunk.delta?.text) { - content = claudeChunk.delta.text; - } else if (claudeChunk && typeof claudeChunk === 'object') { - content = claudeChunk; - } - - // 对于第一个数据块(fullText为空),生成开始事件 - const state = streamStateManager.getOrCreateState(id); - if (state.fullText === '' && !requestId) { // 只在首次调用时(未指定requestId时)生成开始事件 - // 在这种情况下,我们需要先添加内容到状态 - state.fullText = content; - return [ - // ...getOpenAIResponsesStreamChunkBegin(id, model), - generateOutputTextDelta(id, content), - // ...getOpenAIResponsesStreamChunkEnd(id) - ]; - } else if (content === '') { - // 如果是结束块,生成结束事件 - const doneEvents = getOpenAIResponsesStreamChunkEnd(id); - - // 清理状态 - streamStateManager.cleanup(id); - - return doneEvents; - } else { - // 中间数据块,只返回delta事件,但也要更新状态 - streamStateManager.updateText(id, content); - return [ - generateOutputTextDelta(id, content) - ]; - } +export function isProtocolRegistered(protocol) { + return ConverterFactory.isProtocolRegistered(protocol); } /** - * Converts a Gemini API stream chunk to an OpenAI Responses API stream chunk. - * @param {Object} geminiChunk - The Gemini API stream chunk object. - * @param {string} [model] - Optional model name to include in the response. - * @param {string} [requestId] - Optional request ID to maintain stream state across chunks. - * @returns {Array} The formatted OpenAI Responses API stream chunks as an array of events. + * 清除所有转换器缓存 */ -export function toOpenAIResponsesStreamChunkFromGemini(geminiChunk, model, requestId = null) { - if (!geminiChunk) { - return []; - } - - // 如果没有提供requestId,则生成一个(首次调用时) - const id = requestId || Date.now().toString(); - - // 设置模型信息(仅在新请求时设置) - if (!requestId) { - streamStateManager.setModel(id, model); - } - - // Handle text content in stream - let content = ''; - if (typeof geminiChunk === 'string') { - content = geminiChunk; - } else if (geminiChunk && typeof geminiChunk === 'object') { - // Extract content from Gemini chunk if it's an object - content = geminiChunk.content || geminiChunk.text || geminiChunk; - } - - // 对于第一个数据块(fullText为空),生成开始事件 - const state = streamStateManager.getOrCreateState(id); - if (state.fullText === '' && !requestId) { // 只在首次调用时(未指定requestId时)生成开始事件 - // 在这种情况下,我们需要先添加内容到状态 - state.fullText = content; - return [ - // ...getOpenAIResponsesStreamChunkBegin(id, model), - generateOutputTextDelta(id, content), - // ...getOpenAIResponsesStreamChunkEnd(id) - ]; - } else if (content === '') { - // 如果是结束块,生成结束事件 - const doneEvents = getOpenAIResponsesStreamChunkEnd(id); - - // 清理状态 - streamStateManager.cleanup(id); - - return doneEvents; - } else { - // 中间数据块,只返回delta事件,但也要更新状态 - streamStateManager.updateText(id, content); - return [ - generateOutputTextDelta(id, content) - ]; - } +export function clearConverterCache() { + ConverterFactory.clearCache(); } +/** + * 获取转换器实例(用于高级用法) + * @param {string} protocol - 协议前缀 + * @returns {BaseConverter} 转换器实例 + */ +export function getConverter(protocol) { + return ConverterFactory.getConverter(protocol); +} + +// ============================================================================= +// 辅助函数 - 从原 convert.js 迁移 +// ============================================================================= + +/** + * 生成 OpenAI 流式响应的停止块 + * @param {string} model - 模型名称 + * @returns {Object} OpenAI 流式停止块 + */ export function getOpenAIStreamChunkStop(model) { return { - id: `chatcmpl-${uuidv4()}`, // uuidv4 needs to be imported or handled + id: `chatcmpl-${uuidv4()}`, object: "chat.completion.chunk", created: Math.floor(Date.now() / 1000), model: model, system_fingerprint: "", choices: [{ index: 0, - delta: { + delta: { content: "", reasoning_content: "" }, @@ -2550,8 +313,13 @@ export function getOpenAIStreamChunkStop(model) { }; } -export function getOpenAIResponsesStreamChunkBegin(id, model){ - +/** + * 生成 OpenAI Responses 流式响应的开始事件 + * @param {string} id - 响应 ID + * @param {string} model - 模型名称 + * @returns {Array} 开始事件数组 + */ +export function getOpenAIResponsesStreamChunkBegin(id, model) { return [ generateResponseCreated(id, model), generateResponseInProgress(id), @@ -2560,12 +328,57 @@ export function getOpenAIResponsesStreamChunkBegin(id, model){ ]; } -export function getOpenAIResponsesStreamChunkEnd(id){ - +/** + * 生成 OpenAI Responses 流式响应的结束事件 + * @param {string} id - 响应 ID + * @returns {Array} 结束事件数组 + */ +export function getOpenAIResponsesStreamChunkEnd(id) { return [ generateOutputTextDone(id), generateContentPartDone(id), generateOutputItemDone(id), generateResponseCompleted(id) ]; -} \ No newline at end of file +} + +// ============================================================================= +// 默认导出 +// ============================================================================= + +export default { + convertData, + getRegisteredProtocols, + isProtocolRegistered, + clearConverterCache, + getConverter, + // 向后兼容的函数 + toOpenAIRequestFromGemini, + toOpenAIRequestFromClaude, + toOpenAIChatCompletionFromGemini, + toOpenAIChatCompletionFromClaude, + toOpenAIStreamChunkFromGemini, + toOpenAIStreamChunkFromClaude, + toOpenAIModelListFromGemini, + toOpenAIModelListFromClaude, + toClaudeRequestFromOpenAI, + toClaudeChatCompletionFromOpenAI, + toClaudeChatCompletionFromGemini, + toClaudeStreamChunkFromOpenAI, + toClaudeStreamChunkFromGemini, + toClaudeModelListFromOpenAI, + toClaudeModelListFromGemini, + toGeminiRequestFromOpenAI, + toGeminiRequestFromClaude, + toOpenAIResponsesFromOpenAI, + toOpenAIResponsesFromClaude, + toOpenAIResponsesFromGemini, + toOpenAIResponsesStreamChunkFromOpenAI, + toOpenAIResponsesStreamChunkFromClaude, + toOpenAIResponsesStreamChunkFromGemini, + toOpenAIRequestFromOpenAIResponses, + toOpenAIChatCompletionFromOpenAIResponses, + toOpenAIStreamChunkFromOpenAIResponses, + toClaudeRequestFromOpenAIResponses, + toGeminiRequestFromOpenAIResponses, +}; \ No newline at end of file diff --git a/src/converters/BaseConverter.js b/src/converters/BaseConverter.js new file mode 100644 index 0000000..e75735b --- /dev/null +++ b/src/converters/BaseConverter.js @@ -0,0 +1,115 @@ +/** + * 转换器基类 + * 使用策略模式定义转换器的通用接口 + */ + +/** + * 抽象转换器基类 + * 所有具体的协议转换器都应继承此类 + */ +export class BaseConverter { + constructor(protocolName) { + if (new.target === BaseConverter) { + throw new Error('BaseConverter是抽象类,不能直接实例化'); + } + this.protocolName = protocolName; + } + + /** + * 转换请求 + * @param {Object} data - 请求数据 + * @param {string} targetProtocol - 目标协议 + * @returns {Object} 转换后的请求 + */ + convertRequest(data, targetProtocol) { + throw new Error('convertRequest方法必须被子类实现'); + } + + /** + * 转换响应 + * @param {Object} data - 响应数据 + * @param {string} targetProtocol - 目标协议 + * @param {string} model - 模型名称 + * @returns {Object} 转换后的响应 + */ + convertResponse(data, targetProtocol, model) { + throw new Error('convertResponse方法必须被子类实现'); + } + + /** + * 转换流式响应块 + * @param {Object} chunk - 流式响应块 + * @param {string} targetProtocol - 目标协议 + * @param {string} model - 模型名称 + * @returns {Object} 转换后的流式响应块 + */ + convertStreamChunk(chunk, targetProtocol, model) { + throw new Error('convertStreamChunk方法必须被子类实现'); + } + + /** + * 转换模型列表 + * @param {Object} data - 模型列表数据 + * @param {string} targetProtocol - 目标协议 + * @returns {Object} 转换后的模型列表 + */ + convertModelList(data, targetProtocol) { + throw new Error('convertModelList方法必须被子类实现'); + } + + /** + * 获取协议名称 + * @returns {string} 协议名称 + */ + getProtocolName() { + return this.protocolName; + } +} + +/** + * 内容处理器接口 + * 用于处理不同类型的内容(文本、图片、音频等) + */ +export class ContentProcessor { + /** + * 处理内容 + * @param {*} content - 内容数据 + * @returns {*} 处理后的内容 + */ + process(content) { + throw new Error('process方法必须被子类实现'); + } +} + +/** + * 工具处理器接口 + * 用于处理工具调用相关的转换 + */ +export class ToolProcessor { + /** + * 处理工具定义 + * @param {Array} tools - 工具定义数组 + * @returns {Array} 处理后的工具定义 + */ + processToolDefinitions(tools) { + throw new Error('processToolDefinitions方法必须被子类实现'); + } + + /** + * 处理工具调用 + * @param {Object} toolCall - 工具调用数据 + * @returns {Object} 处理后的工具调用 + */ + processToolCall(toolCall) { + throw new Error('processToolCall方法必须被子类实现'); + } + + /** + * 处理工具结果 + * @param {Object} toolResult - 工具结果数据 + * @returns {Object} 处理后的工具结果 + */ + processToolResult(toolResult) { + throw new Error('processToolResult方法必须被子类实现'); + } +} \ No newline at end of file diff --git a/src/converters/ConverterFactory.js b/src/converters/ConverterFactory.js new file mode 100644 index 0000000..58d13cb --- /dev/null +++ b/src/converters/ConverterFactory.js @@ -0,0 +1,182 @@ +/** + * 转换器工厂类 + * 使用工厂模式管理转换器实例的创建和缓存 + */ + +import { MODEL_PROTOCOL_PREFIX } from '../common.js'; + +/** + * 转换器工厂(单例模式 + 工厂模式) + */ +export class ConverterFactory { + // 私有静态属性:存储转换器实例 + static #converters = new Map(); + + // 私有静态属性:存储转换器类 + static #converterClasses = new Map(); + + /** + * 注册转换器类 + * @param {string} protocolPrefix - 协议前缀 + * @param {Class} ConverterClass - 转换器类 + */ + static registerConverter(protocolPrefix, ConverterClass) { + this.#converterClasses.set(protocolPrefix, ConverterClass); + } + + /** + * 获取转换器实例(带缓存) + * @param {string} protocolPrefix - 协议前缀 + * @returns {BaseConverter} 转换器实例 + */ + static getConverter(protocolPrefix) { + // 检查缓存 + if (this.#converters.has(protocolPrefix)) { + return this.#converters.get(protocolPrefix); + } + + // 创建新实例 + const converter = this.createConverter(protocolPrefix); + + // 缓存实例 + if (converter) { + this.#converters.set(protocolPrefix, converter); + } + + return converter; + } + + /** + * 创建转换器实例 + * @param {string} protocolPrefix - 协议前缀 + * @returns {BaseConverter} 转换器实例 + */ + static createConverter(protocolPrefix) { + const ConverterClass = this.#converterClasses.get(protocolPrefix); + + if (!ConverterClass) { + throw new Error(`No converter registered for protocol: ${protocolPrefix}`); + } + + return new ConverterClass(); + } + + /** + * 清除所有缓存的转换器 + */ + static clearCache() { + this.#converters.clear(); + } + + /** + * 清除特定协议的转换器缓存 + * @param {string} protocolPrefix - 协议前缀 + */ + static clearConverterCache(protocolPrefix) { + this.#converters.delete(protocolPrefix); + } + + /** + * 获取所有已注册的协议 + * @returns {Array} 协议前缀数组 + */ + static getRegisteredProtocols() { + return Array.from(this.#converterClasses.keys()); + } + + /** + * 检查协议是否已注册 + * @param {string} protocolPrefix - 协议前缀 + * @returns {boolean} 是否已注册 + */ + static isProtocolRegistered(protocolPrefix) { + return this.#converterClasses.has(protocolPrefix); + } +} + +/** + * 内容处理器工厂 + */ +export class ContentProcessorFactory { + static #processors = new Map(); + + /** + * 获取内容处理器 + * @param {string} sourceFormat - 源格式 + * @param {string} targetFormat - 目标格式 + * @returns {ContentProcessor} 内容处理器实例 + */ + static getProcessor(sourceFormat, targetFormat) { + const key = `${sourceFormat}_to_${targetFormat}`; + + if (!this.#processors.has(key)) { + this.#processors.set(key, this.createProcessor(sourceFormat, targetFormat)); + } + + return this.#processors.get(key); + } + + /** + * 创建内容处理器 + * @param {string} sourceFormat - 源格式 + * @param {string} targetFormat - 目标格式 + * @returns {ContentProcessor} 内容处理器实例 + */ + static createProcessor(sourceFormat, targetFormat) { + // 这里返回null,实际使用时需要导入具体的处理器类 + // 为了避免循环依赖,处理器类应该在使用时动态导入 + console.warn(`Content processor for ${sourceFormat} to ${targetFormat} not yet implemented`); + return null; + } + + /** + * 清除所有缓存的处理器 + */ + static clearCache() { + this.#processors.clear(); + } +} + +/** + * 工具处理器工厂 + */ +export class ToolProcessorFactory { + static #processors = new Map(); + + /** + * 获取工具处理器 + * @param {string} sourceFormat - 源格式 + * @param {string} targetFormat - 目标格式 + * @returns {ToolProcessor} 工具处理器实例 + */ + static getProcessor(sourceFormat, targetFormat) { + const key = `${sourceFormat}_to_${targetFormat}`; + + if (!this.#processors.has(key)) { + this.#processors.set(key, this.createProcessor(sourceFormat, targetFormat)); + } + + return this.#processors.get(key); + } + + /** + * 创建工具处理器 + * @param {string} sourceFormat - 源格式 + * @param {string} targetFormat - 目标格式 + * @returns {ToolProcessor} 工具处理器实例 + */ + static createProcessor(sourceFormat, targetFormat) { + console.warn(`Tool processor for ${sourceFormat} to ${targetFormat} not yet implemented`); + return null; + } + + /** + * 清除所有缓存的处理器 + */ + static clearCache() { + this.#processors.clear(); + } +} + +// 导出工厂类 +export default ConverterFactory; \ No newline at end of file diff --git a/src/converters/register-converters.js b/src/converters/register-converters.js new file mode 100644 index 0000000..95278e8 --- /dev/null +++ b/src/converters/register-converters.js @@ -0,0 +1,25 @@ +/** + * 转换器注册模块 + * 用于注册所有转换器到工厂,避免循环依赖问题 + */ + +import { MODEL_PROTOCOL_PREFIX } from '../common.js'; +import { ConverterFactory } from './ConverterFactory.js'; +import { OpenAIConverter } from './strategies/OpenAIConverter.js'; +import { OpenAIResponsesConverter } from './strategies/OpenAIResponsesConverter.js'; +import { ClaudeConverter } from './strategies/ClaudeConverter.js'; +import { GeminiConverter } from './strategies/GeminiConverter.js'; + +/** + * 注册所有转换器到工厂 + * 此函数应在应用启动时调用一次 + */ +export function registerAllConverters() { + ConverterFactory.registerConverter(MODEL_PROTOCOL_PREFIX.OPENAI, OpenAIConverter); + ConverterFactory.registerConverter(MODEL_PROTOCOL_PREFIX.OPENAI_RESPONSES, OpenAIResponsesConverter); + ConverterFactory.registerConverter(MODEL_PROTOCOL_PREFIX.CLAUDE, ClaudeConverter); + ConverterFactory.registerConverter(MODEL_PROTOCOL_PREFIX.GEMINI, GeminiConverter); +} + +// 自动注册所有转换器 +registerAllConverters(); \ No newline at end of file diff --git a/src/converters/strategies/ClaudeConverter.js b/src/converters/strategies/ClaudeConverter.js new file mode 100644 index 0000000..a0b62bc --- /dev/null +++ b/src/converters/strategies/ClaudeConverter.js @@ -0,0 +1,1290 @@ +/** + * Claude转换器 + * 处理Claude(Anthropic)协议与其他协议之间的转换 + */ + +import { v4 as uuidv4 } from 'uuid'; +import { BaseConverter } from '../BaseConverter.js'; +import { + checkAndAssignOrDefault, + cleanJsonSchemaProperties as cleanJsonSchema, + determineReasoningEffortFromBudget +} from '../utils.js'; +import { MODEL_PROTOCOL_PREFIX } from '../../common.js'; +import { + generateResponseCreated, + generateResponseInProgress, + generateOutputItemAdded, + generateContentPartAdded, + generateOutputTextDone, + generateContentPartDone, + generateOutputItemDone, + generateResponseCompleted +} from '../../openai/openai-responses-core.mjs'; + +/** + * Claude转换器类 + * 实现Claude协议到其他协议的转换 + */ +export class ClaudeConverter extends BaseConverter { + constructor() { + super('claude'); + } + + /** + * 转换请求 + */ + convertRequest(data, targetProtocol) { + switch (targetProtocol) { + case MODEL_PROTOCOL_PREFIX.OPENAI: + return this.toOpenAIRequest(data); + case MODEL_PROTOCOL_PREFIX.GEMINI: + return this.toGeminiRequest(data); + case MODEL_PROTOCOL_PREFIX.OPENAI_RESPONSES: + return this.toOpenAIResponsesRequest(data); + default: + throw new Error(`Unsupported target protocol: ${targetProtocol}`); + } + } + + /** + * 转换响应 + */ + convertResponse(data, targetProtocol, model) { + switch (targetProtocol) { + case MODEL_PROTOCOL_PREFIX.OPENAI: + return this.toOpenAIResponse(data, model); + case MODEL_PROTOCOL_PREFIX.GEMINI: + return this.toGeminiResponse(data, model); + case MODEL_PROTOCOL_PREFIX.OPENAI_RESPONSES: + return this.toOpenAIResponsesResponse(data, model); + default: + throw new Error(`Unsupported target protocol: ${targetProtocol}`); + } + } + + /** + * 转换流式响应块 + */ + convertStreamChunk(chunk, targetProtocol, model) { + switch (targetProtocol) { + case MODEL_PROTOCOL_PREFIX.OPENAI: + return this.toOpenAIStreamChunk(chunk, model); + case MODEL_PROTOCOL_PREFIX.GEMINI: + return this.toGeminiStreamChunk(chunk, model); + case MODEL_PROTOCOL_PREFIX.OPENAI_RESPONSES: + return this.toOpenAIResponsesStreamChunk(chunk, model); + default: + throw new Error(`Unsupported target protocol: ${targetProtocol}`); + } + } + + /** + * 转换模型列表 + */ + convertModelList(data, targetProtocol) { + switch (targetProtocol) { + case MODEL_PROTOCOL_PREFIX.OPENAI: + return this.toOpenAIModelList(data); + case MODEL_PROTOCOL_PREFIX.GEMINI: + return this.toGeminiModelList(data); + default: + return data; + } + } + + // ========================================================================= + // Claude -> OpenAI 转换 + // ========================================================================= + + /** + * Claude请求 -> OpenAI请求 + */ + toOpenAIRequest(claudeRequest) { + const openaiMessages = []; + let systemMessageContent = ''; + + // 添加系统消息 + if (claudeRequest.system) { + systemMessageContent = claudeRequest.system; + } + + // 处理消息 + if (claudeRequest.messages && Array.isArray(claudeRequest.messages)) { + const tempOpenAIMessages = []; + for (const msg of claudeRequest.messages) { + const role = msg.role; + + // 处理用户的工具结果消息 + if (role === "user" && Array.isArray(msg.content)) { + const hasToolResult = msg.content.some( + item => item && typeof item === 'object' && item.type === "tool_result" + ); + + if (hasToolResult) { + for (const item of msg.content) { + if (item && typeof item === 'object' && item.type === "tool_result") { + const toolUseId = item.tool_use_id || item.id || ""; + const contentStr = String(item.content || ""); + tempOpenAIMessages.push({ + role: "tool", + tool_call_id: toolUseId, + content: contentStr, + }); + } + } + continue; + } + } + + // 处理assistant消息中的工具调用 + if (role === "assistant" && Array.isArray(msg.content) && msg.content.length > 0) { + const firstPart = msg.content[0]; + if (firstPart.type === "tool_use") { + const funcName = firstPart.name || ""; + const funcArgs = firstPart.input || {}; + tempOpenAIMessages.push({ + role: "assistant", + content: '', + tool_calls: [ + { + id: firstPart.id || `call_${funcName}_1`, + type: "function", + function: { + name: funcName, + arguments: JSON.stringify(funcArgs) + }, + index: firstPart.index || 0 + } + ] + }); + continue; + } + } + + // 普通文本消息 + const contentConverted = this.processClaudeContentToOpenAIContent(msg.content || ""); + if (contentConverted && (Array.isArray(contentConverted) ? contentConverted.length > 0 : contentConverted.trim().length > 0)) { + tempOpenAIMessages.push({ + role: role, + content: contentConverted + }); + } + } + + // OpenAI兼容性校验 + const validatedMessages = []; + for (let idx = 0; idx < tempOpenAIMessages.length; idx++) { + const m = tempOpenAIMessages[idx]; + if (m.role === "assistant" && m.tool_calls) { + const callIds = m.tool_calls.map(tc => tc.id).filter(id => id); + let unmatched = new Set(callIds); + for (let laterIdx = idx + 1; laterIdx < tempOpenAIMessages.length; laterIdx++) { + const later = tempOpenAIMessages[laterIdx]; + if (later.role === "tool" && unmatched.has(later.tool_call_id)) { + unmatched.delete(later.tool_call_id); + } + if (unmatched.size === 0) break; + } + if (unmatched.size > 0) { + m.tool_calls = m.tool_calls.filter(tc => !unmatched.has(tc.id)); + if (m.tool_calls.length === 0) { + delete m.tool_calls; + if (m.content === null) m.content = ""; + } + } + } + validatedMessages.push(m); + } + openaiMessages.push(...validatedMessages); + } + + const openaiRequest = { + model: claudeRequest.model, + messages: openaiMessages, + max_tokens: checkAndAssignOrDefault(claudeRequest.max_tokens, 8192), + temperature: checkAndAssignOrDefault(claudeRequest.temperature, 1), + top_p: checkAndAssignOrDefault(claudeRequest.top_p, 0.95), + stream: claudeRequest.stream, + }; + + // 处理工具 + if (claudeRequest.tools) { + const openaiTools = []; + for (const tool of claudeRequest.tools) { + openaiTools.push({ + type: "function", + function: { + name: tool.name || "", + description: tool.description || "", + parameters: cleanJsonSchema(tool.input_schema || {}) + } + }); + } + openaiRequest.tools = openaiTools; + openaiRequest.tool_choice = "auto"; + } + + // 处理thinking转换 + if (claudeRequest.thinking && claudeRequest.thinking.type === "enabled") { + const budgetTokens = claudeRequest.thinking.budget_tokens; + const reasoningEffort = determineReasoningEffortFromBudget(budgetTokens); + openaiRequest.reasoning_effort = reasoningEffort; + + let maxCompletionTokens = null; + if (claudeRequest.max_tokens !== undefined) { + maxCompletionTokens = claudeRequest.max_tokens; + delete openaiRequest.max_tokens; + } else { + const envMaxTokens = process.env.OPENAI_REASONING_MAX_TOKENS; + if (envMaxTokens) { + try { + maxCompletionTokens = parseInt(envMaxTokens, 10); + } catch (e) { + console.warn(`Invalid OPENAI_REASONING_MAX_TOKENS value '${envMaxTokens}'`); + } + } + if (!envMaxTokens) { + throw new Error("For OpenAI reasoning models, max_completion_tokens is required."); + } + } + openaiRequest.max_completion_tokens = maxCompletionTokens; + } + + // 添加系统消息 + if (systemMessageContent) { + let stringifiedSystemMessageContent = systemMessageContent; + if (Array.isArray(systemMessageContent)) { + stringifiedSystemMessageContent = systemMessageContent.map(item => + typeof item === 'string' ? item : item.text).join('\n'); + } + openaiRequest.messages.unshift({ role: 'system', content: stringifiedSystemMessageContent }); + } + + return openaiRequest; + } + + /** + * Claude响应 -> OpenAI响应 + */ + toOpenAIResponse(claudeResponse, model) { + if (!claudeResponse || !claudeResponse.content || claudeResponse.content.length === 0) { + return { + id: `chatcmpl-${uuidv4()}`, + object: "chat.completion", + created: Math.floor(Date.now() / 1000), + model: model, + choices: [{ + index: 0, + message: { + role: "assistant", + content: "", + }, + finish_reason: "stop", + }], + usage: { + 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), + }, + }; + } + + // 检查是否包含 tool_use + const hasToolUse = claudeResponse.content.some(block => block && block.type === 'tool_use'); + + let message = { + role: "assistant", + content: null + }; + + if (hasToolUse) { + // 处理包含工具调用的响应 + const toolCalls = []; + let textContent = ''; + + for (const block of claudeResponse.content) { + if (!block) continue; + + if (block.type === 'text') { + textContent += block.text || ''; + } else if (block.type === 'tool_use') { + toolCalls.push({ + id: block.id || `call_${block.name}_${Date.now()}`, + type: "function", + function: { + name: block.name || '', + arguments: JSON.stringify(block.input || {}) + } + }); + } + } + + message.content = textContent || null; + if (toolCalls.length > 0) { + message.tool_calls = toolCalls; + } + } else { + // 处理普通文本响应 + message.content = this.processClaudeResponseContent(claudeResponse.content); + } + + // 处理 finish_reason + let finishReason = 'stop'; + if (claudeResponse.stop_reason === 'end_turn') { + finishReason = 'stop'; + } else if (claudeResponse.stop_reason === 'max_tokens') { + finishReason = 'length'; + } else if (claudeResponse.stop_reason === 'tool_use') { + finishReason = 'tool_calls'; + } else if (claudeResponse.stop_reason) { + finishReason = claudeResponse.stop_reason; + } + + return { + id: `chatcmpl-${uuidv4()}`, + object: "chat.completion", + created: Math.floor(Date.now() / 1000), + model: model, + choices: [{ + index: 0, + message: message, + finish_reason: finishReason, + }], + usage: { + 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), + }, + }; + } + + /** + * Claude流式响应 -> OpenAI流式响应 + */ + toOpenAIStreamChunk(claudeChunk, model) { + if (!claudeChunk) return null; + + // 处理 Claude 流式事件 + const chunkId = `chatcmpl-${uuidv4()}`; + const timestamp = Math.floor(Date.now() / 1000); + + // message_start 事件 + if (claudeChunk.type === 'message_start') { + return { + id: chunkId, + object: "chat.completion.chunk", + created: timestamp, + model: model, + system_fingerprint: "", + choices: [{ + index: 0, + delta: { + role: "assistant", + content: "" + }, + finish_reason: null + }], + usage: { + prompt_tokens: claudeChunk.message?.usage?.input_tokens || 0, + completion_tokens: 0, + total_tokens: claudeChunk.message?.usage?.input_tokens || 0 + } + }; + } + + // content_block_start 事件 + if (claudeChunk.type === 'content_block_start') { + const contentBlock = claudeChunk.content_block; + + // 处理 tool_use 类型 + if (contentBlock && contentBlock.type === 'tool_use') { + return { + id: chunkId, + object: "chat.completion.chunk", + created: timestamp, + model: model, + system_fingerprint: "", + choices: [{ + index: 0, + delta: { + tool_calls: [{ + index: claudeChunk.index || 0, + id: contentBlock.id, + type: "function", + function: { + name: contentBlock.name, + arguments: "" + } + }] + }, + finish_reason: null + }] + }; + } + + // 处理 text 类型 + return { + id: chunkId, + object: "chat.completion.chunk", + created: timestamp, + model: model, + system_fingerprint: "", + choices: [{ + index: 0, + delta: { + content: "" + }, + finish_reason: null + }] + }; + } + + // content_block_delta 事件 + if (claudeChunk.type === 'content_block_delta') { + const delta = claudeChunk.delta; + + // 处理 text_delta + if (delta && delta.type === 'text_delta') { + return { + id: chunkId, + object: "chat.completion.chunk", + created: timestamp, + model: model, + system_fingerprint: "", + choices: [{ + index: 0, + delta: { + content: delta.text || "" + }, + finish_reason: null + }] + }; + } + + // 处理 thinking_delta (推理内容) + if (delta && delta.type === 'thinking_delta') { + return { + id: chunkId, + object: "chat.completion.chunk", + created: timestamp, + model: model, + system_fingerprint: "", + choices: [{ + index: 0, + delta: { + reasoning_content: delta.thinking || "" + }, + finish_reason: null + }] + }; + } + + // 处理 input_json_delta (tool arguments) + if (delta && delta.type === 'input_json_delta') { + return { + id: chunkId, + object: "chat.completion.chunk", + created: timestamp, + model: model, + system_fingerprint: "", + choices: [{ + index: 0, + delta: { + tool_calls: [{ + index: claudeChunk.index || 0, + function: { + arguments: delta.partial_json || "" + } + }] + }, + finish_reason: null + }] + }; + } + } + + // content_block_stop 事件 + if (claudeChunk.type === 'content_block_stop') { + return { + id: chunkId, + object: "chat.completion.chunk", + created: timestamp, + model: model, + system_fingerprint: "", + choices: [{ + index: 0, + delta: {}, + finish_reason: null + }] + }; + } + + // message_delta 事件 + if (claudeChunk.type === 'message_delta') { + const stopReason = claudeChunk.delta?.stop_reason; + const finishReason = stopReason === 'end_turn' ? 'stop' : + stopReason === 'max_tokens' ? 'length' : + stopReason === 'tool_use' ? 'tool_calls' : + stopReason || 'stop'; + + return { + id: chunkId, + object: "chat.completion.chunk", + created: timestamp, + model: model, + system_fingerprint: "", + choices: [{ + index: 0, + delta: {}, + finish_reason: finishReason + }], + 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) + } : undefined + }; + } + + // message_stop 事件 + if (claudeChunk.type === 'message_stop') { + return { + id: chunkId, + object: "chat.completion.chunk", + created: timestamp, + model: model, + system_fingerprint: "", + choices: [{ + index: 0, + delta: {}, + finish_reason: 'stop' + }] + }; + } + + // 兼容旧格式:如果是字符串,直接作为文本内容 + if (typeof claudeChunk === 'string') { + return { + id: chunkId, + object: "chat.completion.chunk", + created: timestamp, + model: model, + system_fingerprint: "", + choices: [{ + index: 0, + delta: { + content: claudeChunk + }, + finish_reason: null + }] + }; + } + + return null; + } + + /** + * Claude模型列表 -> OpenAI模型列表 + */ + toOpenAIModelList(claudeModels) { + return { + object: "list", + data: claudeModels.models.map(m => ({ + id: m.id || m.name, + object: "model", + created: Math.floor(Date.now() / 1000), + owned_by: "anthropic", + })), + }; + } + + /** + * 将 Claude 模型列表转换为 Gemini 模型列表 + */ + toGeminiModelList(claudeModels) { + const models = claudeModels.models || []; + return { + models: models.map(m => ({ + name: `models/${m.id || m.name}`, + version: m.version || "1.0.0", + displayName: m.displayName || m.id || m.name, + description: m.description || `A generative model for text and chat generation. ID: ${m.id || m.name}`, + inputTokenLimit: m.inputTokenLimit || 32768, + outputTokenLimit: m.outputTokenLimit || 8192, + supportedGenerationMethods: m.supportedGenerationMethods || ["generateContent", "streamGenerateContent"] + })) + }; + } + + /** + * 处理Claude内容到OpenAI格式 + */ + processClaudeContentToOpenAIContent(content) { + if (!content || !Array.isArray(content)) return []; + + const contentArray = []; + + content.forEach(block => { + if (!block) return; + + switch (block.type) { + case 'text': + if (block.text) { + contentArray.push({ + type: 'text', + text: block.text + }); + } + break; + + case 'image': + if (block.source && block.source.type === 'base64') { + contentArray.push({ + type: 'image_url', + image_url: { + url: `data:${block.source.media_type};base64,${block.source.data}` + } + }); + } + break; + + case 'tool_use': + contentArray.push({ + type: 'text', + text: `[Tool use: ${block.name}]` + }); + break; + + case 'tool_result': + contentArray.push({ + type: 'text', + text: typeof block.content === 'string' ? block.content : JSON.stringify(block.content) + }); + break; + + default: + if (block.text) { + contentArray.push({ + type: 'text', + text: block.text + }); + } + } + }); + + return contentArray; + } + + /** + * 处理Claude响应内容 + */ + processClaudeResponseContent(content) { + if (!content || !Array.isArray(content)) return ''; + + const contentArray = []; + + content.forEach(block => { + if (!block) return; + + switch (block.type) { + case 'text': + contentArray.push({ + type: 'text', + text: block.text || '' + }); + break; + + case 'image': + if (block.source && block.source.type === 'base64') { + contentArray.push({ + type: 'image_url', + image_url: { + url: `data:${block.source.media_type};base64,${block.source.data}` + } + }); + } + break; + + default: + if (block.text) { + contentArray.push({ + type: 'text', + text: block.text + }); + } + } + }); + + return contentArray.length === 1 && contentArray[0].type === 'text' + ? contentArray[0].text + : contentArray; + } + + // ========================================================================= + // Claude -> Gemini 转换 + // ========================================================================= + + /** + * Claude请求 -> Gemini请求 + */ + toGeminiRequest(claudeRequest) { + if (!claudeRequest || typeof claudeRequest !== 'object') { + console.warn("Invalid claudeRequest provided to toGeminiRequest."); + return { contents: [] }; + } + + const geminiRequest = { + contents: [] + }; + + // 处理系统指令 + if (claudeRequest.system) { + let incomingSystemText = null; + if (typeof claudeRequest.system === 'string') { + incomingSystemText = claudeRequest.system; + } else if (typeof claudeRequest.system === 'object') { + incomingSystemText = JSON.stringify(claudeRequest.system); + } + geminiRequest.systemInstruction = { + parts: [{ text: incomingSystemText }] + }; + } + + // 处理消息 + if (Array.isArray(claudeRequest.messages)) { + claudeRequest.messages.forEach(message => { + if (!message || typeof message !== 'object' || !message.role || !message.content) { + console.warn("Skipping invalid message in claudeRequest.messages."); + return; + } + + const geminiRole = message.role === 'assistant' ? 'model' : 'user'; + const processedParts = this.processClaudeContentToGeminiParts(message.content); + + const functionResponsePart = processedParts.find(part => part.functionResponse); + if (functionResponsePart) { + geminiRequest.contents.push({ + role: 'function', + parts: [functionResponsePart] + }); + } else if (processedParts.length > 0) { + geminiRequest.contents.push({ + role: geminiRole, + parts: processedParts + }); + } + }); + } + + // 添加生成配置 + const generationConfig = {}; + generationConfig.maxOutputTokens = checkAndAssignOrDefault(claudeRequest.max_tokens, 65535); + generationConfig.temperature = checkAndAssignOrDefault(claudeRequest.temperature, 1); + generationConfig.topP = checkAndAssignOrDefault(claudeRequest.top_p, 0.95); + + if (Object.keys(generationConfig).length > 0) { + geminiRequest.generationConfig = generationConfig; + } + + // 处理工具 + if (Array.isArray(claudeRequest.tools)) { + geminiRequest.tools = [{ + functionDeclarations: claudeRequest.tools.map(tool => { + if (!tool || typeof tool !== 'object' || !tool.name) { + console.warn("Skipping invalid tool declaration in claudeRequest.tools."); + return null; + } + + delete tool.input_schema.$schema; + return { + name: String(tool.name), + description: String(tool.description || ''), + parameters: tool.input_schema && typeof tool.input_schema === 'object' + ? tool.input_schema + : { type: 'object', properties: {} } + }; + }).filter(Boolean) + }]; + + if (geminiRequest.tools[0].functionDeclarations.length === 0) { + delete geminiRequest.tools; + } + } + + // 处理tool_choice + if (claudeRequest.tool_choice) { + geminiRequest.toolConfig = this.buildGeminiToolConfigFromClaude(claudeRequest.tool_choice); + } + + return geminiRequest; + } + + /** + * Claude响应 -> Gemini响应 + */ + toGeminiResponse(claudeResponse, model) { + if (!claudeResponse || !claudeResponse.content || claudeResponse.content.length === 0) { + return { candidates: [], usageMetadata: {} }; + } + + const parts = []; + + // 处理内容块 + for (const block of claudeResponse.content) { + if (!block) continue; + + switch (block.type) { + case 'text': + if (block.text) { + parts.push({ text: block.text }); + } + break; + + case 'tool_use': + parts.push({ + functionCall: { + name: block.name, + args: block.input || {} + } + }); + break; + + case 'image': + if (block.source && block.source.type === 'base64') { + parts.push({ + inlineData: { + mimeType: block.source.media_type, + data: block.source.data + } + }); + } + break; + + default: + if (block.text) { + parts.push({ text: block.text }); + } + } + } + + // 映射finish_reason + const finishReasonMap = { + 'end_turn': 'STOP', + 'max_tokens': 'MAX_TOKENS', + 'tool_use': 'STOP', + 'stop_sequence': 'STOP' + }; + + return { + candidates: [{ + content: { + role: 'model', + parts: parts + }, + finishReason: finishReasonMap[claudeResponse.stop_reason] || 'STOP' + }], + 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) + } : {} + }; + } + + /** + * Claude流式响应 -> Gemini流式响应 + */ + toGeminiStreamChunk(claudeChunk, model) { + if (!claudeChunk) return null; + + // 处理Claude流式事件 + if (typeof claudeChunk === 'object' && !Array.isArray(claudeChunk)) { + // content_block_delta 事件 + if (claudeChunk.type === 'content_block_delta') { + const delta = claudeChunk.delta; + + // 处理 text_delta + if (delta && delta.type === 'text_delta') { + return { + candidates: [{ + content: { + role: "model", + parts: [{ + text: delta.text || "" + }] + } + }] + }; + } + + // 处理 thinking_delta - 映射为文本 + if (delta && delta.type === 'thinking_delta') { + return { + candidates: [{ + content: { + role: "model", + parts: [{ + text: delta.thinking || "" + }] + } + }] + }; + } + } + + // message_delta 事件 - 流结束 + if (claudeChunk.type === 'message_delta') { + const stopReason = claudeChunk.delta?.stop_reason; + return { + candidates: [{ + finishReason: stopReason === 'end_turn' ? 'STOP' : + stopReason === 'max_tokens' ? 'MAX_TOKENS' : + 'OTHER' + }] + }; + } + } + + // 向后兼容:处理字符串格式 + if (typeof claudeChunk === 'string') { + return { + candidates: [{ + content: { + role: "model", + parts: [{ + text: claudeChunk + }] + } + }] + }; + } + + return null; + } + + /** + * 处理Claude内容到Gemini parts + */ + processClaudeContentToGeminiParts(content) { + if (!content) return []; + + if (typeof content === 'string') { + return [{ text: content }]; + } + + if (Array.isArray(content)) { + const parts = []; + + content.forEach(block => { + if (!block || typeof block !== 'object' || !block.type) { + console.warn("Skipping invalid content block."); + return; + } + + switch (block.type) { + case 'text': + if (typeof block.text === 'string') { + parts.push({ text: block.text }); + } + break; + + case 'image': + if (block.source && typeof block.source === 'object' && + block.source.type === 'base64' && + typeof block.source.media_type === 'string' && + typeof block.source.data === 'string') { + parts.push({ + inlineData: { + mimeType: block.source.media_type, + data: block.source.data + } + }); + } + break; + + case 'tool_use': + if (typeof block.name === 'string' && + block.input && typeof block.input === 'object') { + parts.push({ + functionCall: { + name: block.name, + args: block.input + } + }); + } + break; + + case 'tool_result': + if (typeof block.tool_use_id === 'string') { + parts.push({ + functionResponse: { + name: block.tool_use_id, + response: { content: block.content } + } + }); + } + break; + + default: + if (typeof block.text === 'string') { + parts.push({ text: block.text }); + } + } + }); + + return parts; + } + + return []; + } + + /** + * 构建Gemini工具配置 + */ + buildGeminiToolConfigFromClaude(claudeToolChoice) { + if (!claudeToolChoice || typeof claudeToolChoice !== 'object' || !claudeToolChoice.type) { + console.warn("Invalid claudeToolChoice provided."); + return undefined; + } + + switch (claudeToolChoice.type) { + case 'auto': + return { functionCallingConfig: { mode: 'AUTO' } }; + case 'none': + return { functionCallingConfig: { mode: 'NONE' } }; + case 'tool': + if (claudeToolChoice.name && typeof claudeToolChoice.name === 'string') { + return { + functionCallingConfig: { + mode: 'ANY', + allowedFunctionNames: [claudeToolChoice.name] + } + }; + } + console.warn("Invalid tool name in claudeToolChoice of type 'tool'."); + return undefined; + default: + console.warn(`Unsupported claudeToolChoice type: ${claudeToolChoice.type}`); + return undefined; + } + } + + // ========================================================================= + // Claude -> OpenAI Responses 转换 + // ========================================================================= + + /** + * Claude请求 -> OpenAI Responses请求 + */ + toOpenAIResponsesRequest(claudeRequest) { + // 转换为OpenAI Responses格式 + const responsesRequest = { + model: claudeRequest.model, + max_tokens: checkAndAssignOrDefault(claudeRequest.max_tokens, 8192), + temperature: checkAndAssignOrDefault(claudeRequest.temperature, 1), + top_p: checkAndAssignOrDefault(claudeRequest.top_p, 0.95), + }; + + // 处理系统指令 + if (claudeRequest.system) { + responsesRequest.instructions = claudeRequest.system; + } + + // 处理消息 + if (claudeRequest.messages && Array.isArray(claudeRequest.messages)) { + responsesRequest.input = claudeRequest.messages; + } + + return responsesRequest; + } + + /** + * Claude响应 -> OpenAI Responses响应 + */ + toOpenAIResponsesResponse(claudeResponse, model) { + const content = this.processClaudeResponseContent(claudeResponse.content); + const textContent = typeof content === 'string' ? content : JSON.stringify(content); + + let output = []; + output.push({ + type: "message", + id: `msg_${uuidv4().replace(/-/g, '')}`, + summary: [], + role: "assistant", + status: "completed", + content: [{ + annotations: [], + logprobs: [], + text: textContent, + type: "output_text" + }] + }); + + return { + background: false, + created_at: Math.floor(Date.now() / 1000), + error: null, + id: `resp_${uuidv4().replace(/-/g, '')}`, + incomplete_details: null, + max_output_tokens: null, + max_tool_calls: null, + metadata: {}, + model: model || claudeResponse.model, + object: "response", + output: output, + parallel_tool_calls: true, + previous_response_id: null, + prompt_cache_key: null, + reasoning: {}, + safety_identifier: "user-" + uuidv4().replace(/-/g, ''), + service_tier: "default", + status: "completed", + store: false, + temperature: 1, + text: { + format: { type: "text" }, + }, + tool_choice: "auto", + tools: [], + top_logprobs: 0, + top_p: 1, + truncation: "disabled", + usage: { + input_tokens: claudeResponse.usage?.input_tokens || 0, + input_tokens_details: { + cached_tokens: claudeResponse.usage?.cache_creation_input_tokens || 0, + }, + output_tokens: claudeResponse.usage?.output_tokens || 0, + output_tokens_details: { + reasoning_tokens: 0 + }, + total_tokens: (claudeResponse.usage?.input_tokens || 0) + (claudeResponse.usage?.output_tokens || 0) + }, + user: null + }; + } + + /** + * Claude流式响应 -> OpenAI Responses流式响应 + */ + toOpenAIResponsesStreamChunk(claudeChunk, model, requestId = null) { + if (!claudeChunk) return []; + + const responseId = requestId || `resp_${uuidv4().replace(/-/g, '')}`; + const events = []; + + // message_start 事件 - 流开始 + if (claudeChunk.type === 'message_start') { + events.push( + generateResponseCreated(responseId, model || 'unknown'), + generateResponseInProgress(responseId), + generateOutputItemAdded(responseId), + generateContentPartAdded(responseId) + ); + } + + // content_block_start 事件 + if (claudeChunk.type === 'content_block_start') { + const contentBlock = claudeChunk.content_block; + + // 对于 tool_use 类型,添加工具调用项 + if (contentBlock && contentBlock.type === 'tool_use') { + events.push({ + item: { + id: contentBlock.id, + type: "function_call", + name: contentBlock.name, + arguments: "", + status: "in_progress" + }, + output_index: claudeChunk.index || 0, + sequence_number: 2, + type: "response.output_item.added" + }); + } + } + + // content_block_delta 事件 + if (claudeChunk.type === 'content_block_delta') { + const delta = claudeChunk.delta; + + // 处理文本增量 + if (delta && delta.type === 'text_delta') { + events.push({ + delta: delta.text || "", + item_id: `msg_${uuidv4().replace(/-/g, '')}`, + output_index: claudeChunk.index || 0, + sequence_number: 3, + type: "response.output_text.delta" + }); + } + // 处理推理内容增量 + else if (delta && delta.type === 'thinking_delta') { + events.push({ + delta: delta.thinking || "", + item_id: `thinking_${uuidv4().replace(/-/g, '')}`, + output_index: claudeChunk.index || 0, + sequence_number: 3, + type: "response.reasoning_summary_text.delta" + }); + } + // 处理工具调用参数增量 + else if (delta && delta.type === 'input_json_delta') { + events.push({ + delta: delta.partial_json || "", + item_id: `call_${uuidv4().replace(/-/g, '')}`, + output_index: claudeChunk.index || 0, + sequence_number: 3, + type: "response.custom_tool_call_input.delta" + }); + } + } + + // content_block_stop 事件 + if (claudeChunk.type === 'content_block_stop') { + events.push({ + item_id: `msg_${uuidv4().replace(/-/g, '')}`, + output_index: claudeChunk.index || 0, + sequence_number: 4, + type: "response.output_item.done" + }); + } + + // message_delta 事件 - 流结束 + if (claudeChunk.type === 'message_delta') { + // events.push( + // generateOutputTextDone(responseId), + // generateContentPartDone(responseId), + // generateOutputItemDone(responseId), + // generateResponseCompleted(responseId) + // ); + + // 如果有 usage 信息,更新最后一个事件 + if (claudeChunk.usage && events.length > 0) { + const lastEvent = events[events.length - 1]; + if (lastEvent.response) { + lastEvent.response.usage = { + input_tokens: claudeChunk.usage.input_tokens || 0, + output_tokens: claudeChunk.usage.output_tokens || 0, + total_tokens: (claudeChunk.usage.input_tokens || 0) + (claudeChunk.usage.output_tokens || 0) + }; + } + } + } + + // message_stop 事件 + if (claudeChunk.type === 'message_stop') { + events.push( + generateOutputTextDone(responseId), + generateContentPartDone(responseId), + generateOutputItemDone(responseId), + generateResponseCompleted(responseId) + ); + } + + return events; + } +} + +export default ClaudeConverter; \ No newline at end of file diff --git a/src/converters/strategies/GeminiConverter.js b/src/converters/strategies/GeminiConverter.js new file mode 100644 index 0000000..438bbe0 --- /dev/null +++ b/src/converters/strategies/GeminiConverter.js @@ -0,0 +1,820 @@ +/** + * Gemini转换器 + * 处理Gemini(Google)协议与其他协议之间的转换 + */ + +import { v4 as uuidv4 } from 'uuid'; +import { BaseConverter } from '../BaseConverter.js'; +import { + checkAndAssignOrDefault +} from '../utils.js'; +import { MODEL_PROTOCOL_PREFIX } from '../../common.js'; +import { + generateResponseCreated, + generateResponseInProgress, + generateOutputItemAdded, + generateContentPartAdded, + generateOutputTextDone, + generateContentPartDone, + generateOutputItemDone, + generateResponseCompleted +} from '../../openai/openai-responses-core.mjs'; + +/** + * Gemini转换器类 + * 实现Gemini协议到其他协议的转换 + */ +export class GeminiConverter extends BaseConverter { + constructor() { + super('gemini'); + } + + /** + * 转换请求 + */ + convertRequest(data, targetProtocol) { + switch (targetProtocol) { + case MODEL_PROTOCOL_PREFIX.OPENAI: + return this.toOpenAIRequest(data); + case MODEL_PROTOCOL_PREFIX.CLAUDE: + return this.toClaudeRequest(data); + case MODEL_PROTOCOL_PREFIX.OPENAI_RESPONSES: + return this.toOpenAIResponsesRequest(data); + default: + throw new Error(`Unsupported target protocol: ${targetProtocol}`); + } + } + + /** + * 转换响应 + */ + convertResponse(data, targetProtocol, model) { + switch (targetProtocol) { + case MODEL_PROTOCOL_PREFIX.OPENAI: + return this.toOpenAIResponse(data, model); + case MODEL_PROTOCOL_PREFIX.CLAUDE: + return this.toClaudeResponse(data, model); + case MODEL_PROTOCOL_PREFIX.OPENAI_RESPONSES: + return this.toOpenAIResponsesResponse(data, model); + default: + throw new Error(`Unsupported target protocol: ${targetProtocol}`); + } + } + + /** + * 转换流式响应块 + */ + convertStreamChunk(chunk, targetProtocol, model) { + switch (targetProtocol) { + case MODEL_PROTOCOL_PREFIX.OPENAI: + return this.toOpenAIStreamChunk(chunk, model); + case MODEL_PROTOCOL_PREFIX.CLAUDE: + return this.toClaudeStreamChunk(chunk, model); + case MODEL_PROTOCOL_PREFIX.OPENAI_RESPONSES: + return this.toOpenAIResponsesStreamChunk(chunk, model); + default: + throw new Error(`Unsupported target protocol: ${targetProtocol}`); + } + } + + /** + * 转换模型列表 + */ + convertModelList(data, targetProtocol) { + switch (targetProtocol) { + case MODEL_PROTOCOL_PREFIX.OPENAI: + return this.toOpenAIModelList(data); + case MODEL_PROTOCOL_PREFIX.CLAUDE: + return this.toClaudeModelList(data); + default: + return data; + } + } + + // ========================================================================= + // Gemini -> OpenAI 转换 + // ========================================================================= + + /** + * Gemini请求 -> OpenAI请求 + */ + toOpenAIRequest(geminiRequest) { + const openaiRequest = { + messages: [], + model: geminiRequest.model, + max_tokens: checkAndAssignOrDefault(geminiRequest.max_tokens, 8192), + temperature: checkAndAssignOrDefault(geminiRequest.temperature, 1), + top_p: checkAndAssignOrDefault(geminiRequest.top_p, 0.95), + }; + + // 处理系统指令 + if (geminiRequest.systemInstruction && Array.isArray(geminiRequest.systemInstruction.parts)) { + const systemContent = this.processGeminiPartsToOpenAIContent(geminiRequest.systemInstruction.parts); + if (systemContent) { + openaiRequest.messages.push({ + role: 'system', + content: systemContent + }); + } + } + + // 处理内容 + if (geminiRequest.contents && Array.isArray(geminiRequest.contents)) { + geminiRequest.contents.forEach(content => { + if (content && Array.isArray(content.parts)) { + const openaiContent = this.processGeminiPartsToOpenAIContent(content.parts); + if (openaiContent && openaiContent.length > 0) { + const openaiRole = content.role === 'model' ? 'assistant' : content.role; + openaiRequest.messages.push({ + role: openaiRole, + content: openaiContent + }); + } + } + }); + } + + return openaiRequest; + } + + /** + * Gemini响应 -> OpenAI响应 + */ + toOpenAIResponse(geminiResponse, model) { + const content = this.processGeminiResponseContent(geminiResponse); + + return { + id: `chatcmpl-${uuidv4()}`, + object: "chat.completion", + created: Math.floor(Date.now() / 1000), + model: model, + choices: [{ + index: 0, + message: { + role: "assistant", + content: content + }, + finish_reason: "stop", + }], + usage: geminiResponse.usageMetadata ? { + prompt_tokens: geminiResponse.usageMetadata.promptTokenCount || 0, + completion_tokens: geminiResponse.usageMetadata.candidatesTokenCount || 0, + total_tokens: geminiResponse.usageMetadata.totalTokenCount || 0, + } : { + prompt_tokens: 0, + completion_tokens: 0, + total_tokens: 0, + }, + }; + } + + /** + * Gemini流式响应 -> OpenAI流式响应 + */ + toOpenAIStreamChunk(geminiChunk, model) { + if (!geminiChunk) return null; + + // 处理完整的Gemini chunk对象 + if (typeof geminiChunk === 'object' && !Array.isArray(geminiChunk)) { + const candidate = geminiChunk.candidates?.[0]; + + // 提取文本内容 + let content = ''; + let finishReason = null; + + if (candidate) { + // 从parts中提取文本 + const parts = candidate.content?.parts; + if (parts && Array.isArray(parts)) { + content = parts + .filter(part => part && typeof part.text === 'string') + .map(part => part.text) + .join(''); + } + + // 处理finishReason + if (candidate.finishReason) { + finishReason = candidate.finishReason === 'STOP' ? 'stop' : + candidate.finishReason === 'MAX_TOKENS' ? 'length' : + candidate.finishReason.toLowerCase(); + } + } + + return { + id: `chatcmpl-${uuidv4()}`, + object: "chat.completion.chunk", + created: Math.floor(Date.now() / 1000), + model: model, + choices: [{ + index: 0, + delta: content ? { content: content } : {}, + finish_reason: finishReason, + }], + usage: geminiChunk.usageMetadata ? { + prompt_tokens: geminiChunk.usageMetadata.promptTokenCount || 0, + completion_tokens: geminiChunk.usageMetadata.candidatesTokenCount || 0, + total_tokens: geminiChunk.usageMetadata.totalTokenCount || 0, + } : { + prompt_tokens: 0, + completion_tokens: 0, + total_tokens: 0, + }, + }; + } + + // 向后兼容:处理字符串格式 + if (typeof geminiChunk === 'string') { + return { + id: `chatcmpl-${uuidv4()}`, + object: "chat.completion.chunk", + created: Math.floor(Date.now() / 1000), + model: model, + choices: [{ + index: 0, + delta: { content: geminiChunk }, + finish_reason: null, + }], + usage: { + prompt_tokens: 0, + completion_tokens: 0, + total_tokens: 0, + }, + }; + } + + return null; + } + + /** + * Gemini模型列表 -> OpenAI模型列表 + */ + toOpenAIModelList(geminiModels) { + return { + object: "list", + data: geminiModels.models.map(m => ({ + id: m.name.startsWith('models/') ? m.name.substring(7) : m.name, + object: "model", + created: Math.floor(Date.now() / 1000), + owned_by: "google", + })), + }; + } + + /** + * 处理Gemini parts到OpenAI内容 + */ + processGeminiPartsToOpenAIContent(parts) { + if (!parts || !Array.isArray(parts)) return ''; + + const contentArray = []; + + parts.forEach(part => { + if (!part) return; + + if (typeof part.text === 'string') { + contentArray.push({ + type: 'text', + text: part.text + }); + } + + if (part.inlineData) { + const { mimeType, data } = part.inlineData; + if (mimeType && data) { + contentArray.push({ + type: 'image_url', + image_url: { + url: `data:${mimeType};base64,${data}` + } + }); + } + } + + if (part.fileData) { + const { mimeType, fileUri } = part.fileData; + if (mimeType && fileUri) { + if (mimeType.startsWith('image/')) { + contentArray.push({ + type: 'image_url', + image_url: { + url: fileUri + } + }); + } else if (mimeType.startsWith('audio/')) { + contentArray.push({ + type: 'text', + text: `[Audio file: ${fileUri}]` + }); + } + } + } + }); + + return contentArray.length === 1 && contentArray[0].type === 'text' + ? contentArray[0].text + : contentArray; + } + + /** + * 处理Gemini响应内容 + */ + processGeminiResponseContent(geminiResponse) { + if (!geminiResponse || !geminiResponse.candidates) return ''; + + const contents = []; + + geminiResponse.candidates.forEach(candidate => { + if (candidate.content && candidate.content.parts) { + candidate.content.parts.forEach(part => { + if (part.text) { + contents.push(part.text); + } + }); + } + }); + + return contents.join('\n'); + } + + // ========================================================================= + // Gemini -> Claude 转换 + // ========================================================================= + + /** + * Gemini请求 -> Claude请求 + */ + toClaudeRequest(geminiRequest) { + const claudeRequest = { + model: geminiRequest.model || 'claude-3-opus', + messages: [], + max_tokens: checkAndAssignOrDefault(geminiRequest.generationConfig?.maxOutputTokens, 8192), + temperature: checkAndAssignOrDefault(geminiRequest.generationConfig?.temperature, 1), + top_p: checkAndAssignOrDefault(geminiRequest.generationConfig?.topP, 0.95), + }; + + // 处理系统指令 + if (geminiRequest.systemInstruction && geminiRequest.systemInstruction.parts) { + const systemText = geminiRequest.systemInstruction.parts + .filter(p => p.text) + .map(p => p.text) + .join('\n'); + if (systemText) { + claudeRequest.system = systemText; + } + } + + // 处理内容 + if (geminiRequest.contents && Array.isArray(geminiRequest.contents)) { + geminiRequest.contents.forEach(content => { + if (!content || !content.parts) return; + + const role = content.role === 'model' ? 'assistant' : 'user'; + const claudeContent = this.processGeminiPartsToClaudeContent(content.parts); + + if (claudeContent.length > 0) { + claudeRequest.messages.push({ + role: role, + content: claudeContent + }); + } + }); + } + + // 处理工具 + if (geminiRequest.tools && geminiRequest.tools[0]?.functionDeclarations) { + claudeRequest.tools = geminiRequest.tools[0].functionDeclarations.map(func => ({ + name: func.name, + description: func.description || '', + input_schema: func.parameters || { type: 'object', properties: {} } + })); + } + + return claudeRequest; + } + + /** + * Gemini响应 -> Claude响应 + */ + toClaudeResponse(geminiResponse, model) { + if (!geminiResponse || !geminiResponse.candidates || geminiResponse.candidates.length === 0) { + return { + id: `msg_${uuidv4()}`, + type: "message", + role: "assistant", + content: [], + model: model, + stop_reason: "end_turn", + stop_sequence: null, + usage: { + input_tokens: geminiResponse?.usageMetadata?.promptTokenCount || 0, + output_tokens: geminiResponse?.usageMetadata?.candidatesTokenCount || 0 + } + }; + } + + const candidate = geminiResponse.candidates[0]; + const content = this.processGeminiResponseToClaudeContent(geminiResponse); + const finishReason = candidate.finishReason; + let stopReason = "end_turn"; + + if (finishReason) { + switch (finishReason) { + case 'STOP': + stopReason = 'end_turn'; + break; + case 'MAX_TOKENS': + stopReason = 'max_tokens'; + break; + case 'SAFETY': + stopReason = 'safety'; + break; + case 'RECITATION': + stopReason = 'recitation'; + break; + case 'OTHER': + stopReason = 'other'; + break; + default: + stopReason = 'end_turn'; + } + } + + return { + id: `msg_${uuidv4()}`, + type: "message", + role: "assistant", + content: content, + model: model, + stop_reason: stopReason, + stop_sequence: null, + usage: { + input_tokens: geminiResponse.usageMetadata?.promptTokenCount || 0, + output_tokens: geminiResponse.usageMetadata?.candidatesTokenCount || 0 + } + }; + } + + /** + * Gemini流式响应 -> Claude流式响应 + */ + toClaudeStreamChunk(geminiChunk, model) { + if (!geminiChunk) return null; + + // 处理完整的Gemini chunk对象 + if (typeof geminiChunk === 'object' && !Array.isArray(geminiChunk)) { + const candidate = geminiChunk.candidates?.[0]; + + if (candidate) { + const parts = candidate.content?.parts; + + // 提取文本内容 + if (parts && Array.isArray(parts)) { + const textParts = parts.filter(part => part && typeof part.text === 'string'); + if (textParts.length > 0) { + const text = textParts.map(part => part.text).join(''); + return { + type: "content_block_delta", + index: 0, + delta: { + type: "text_delta", + text: text + } + }; + } + } + + // 处理finishReason + if (candidate.finishReason) { + return { + type: "message_delta", + delta: { + stop_reason: candidate.finishReason === 'STOP' ? 'end_turn' : + candidate.finishReason === 'MAX_TOKENS' ? 'max_tokens' : + candidate.finishReason.toLowerCase() + } + }; + } + } + } + + // 向后兼容:处理字符串格式 + if (typeof geminiChunk === 'string') { + return { + type: "content_block_delta", + index: 0, + delta: { + type: "text_delta", + text: geminiChunk + } + }; + } + + return null; + } + + /** + * Gemini模型列表 -> Claude模型列表 + */ + toClaudeModelList(geminiModels) { + return { + models: geminiModels.models.map(m => ({ + name: m.name.startsWith('models/') ? m.name.substring(7) : m.name, + description: "", + })), + }; + } + + /** + * 处理Gemini parts到Claude内容 + */ + processGeminiPartsToClaudeContent(parts) { + if (!parts || !Array.isArray(parts)) return []; + + const content = []; + + parts.forEach(part => { + if (!part) return; + + if (part.text) { + content.push({ + type: 'text', + text: part.text + }); + } + + if (part.inlineData) { + content.push({ + type: 'image', + source: { + type: 'base64', + media_type: part.inlineData.mimeType, + data: part.inlineData.data + } + }); + } + + if (part.functionCall) { + content.push({ + type: 'tool_use', + id: uuidv4(), + name: part.functionCall.name, + input: part.functionCall.args || {} + }); + } + + if (part.functionResponse) { + content.push({ + type: 'tool_result', + tool_use_id: part.functionResponse.name, + content: part.functionResponse.response + }); + } + }); + + return content; + } + + /** + * 处理Gemini响应到Claude内容 + */ + processGeminiResponseToClaudeContent(geminiResponse) { + if (!geminiResponse || !geminiResponse.candidates || geminiResponse.candidates.length === 0) return []; + + const content = []; + + for (const candidate of geminiResponse.candidates) { + if (candidate.finishReason && candidate.finishReason !== 'STOP') { + if (candidate.finishMessage) { + content.push({ + type: 'text', + text: `Error: ${candidate.finishMessage}` + }); + } + continue; + } + + if (candidate.content && candidate.content.parts) { + for (const part of candidate.content.parts) { + if (part.text) { + content.push({ + type: 'text', + text: part.text + }); + } else if (part.inlineData) { + content.push({ + type: 'image', + source: { + type: 'base64', + media_type: part.inlineData.mimeType, + data: part.inlineData.data + } + }); + } else if (part.functionCall) { + content.push({ + type: 'tool_use', + id: uuidv4(), + name: part.functionCall.name, + input: part.functionCall.args || {} + }); + } + } + } + } + + return content; + } + + // ========================================================================= + // Gemini -> OpenAI Responses 转换 + // ========================================================================= + + /** + * Gemini请求 -> OpenAI Responses请求 + */ + toOpenAIResponsesRequest(geminiRequest) { + const responsesRequest = { + model: geminiRequest.model, + max_tokens: checkAndAssignOrDefault(geminiRequest.generationConfig?.maxOutputTokens, 8192), + temperature: checkAndAssignOrDefault(geminiRequest.generationConfig?.temperature, 1), + top_p: checkAndAssignOrDefault(geminiRequest.generationConfig?.topP, 0.95), + }; + + // 处理系统指令 + if (geminiRequest.systemInstruction && geminiRequest.systemInstruction.parts) { + const instructionsText = geminiRequest.systemInstruction.parts + .filter(p => p.text) + .map(p => p.text) + .join('\n'); + if (instructionsText) { + responsesRequest.instructions = instructionsText; + } + } + + // 处理输入 + if (geminiRequest.contents && Array.isArray(geminiRequest.contents)) { + const lastContent = geminiRequest.contents[geminiRequest.contents.length - 1]; + if (lastContent && lastContent.parts) { + const inputText = lastContent.parts + .filter(p => p.text) + .map(p => p.text) + .join(' '); + if (inputText) { + responsesRequest.input = inputText; + } + } + } + + return responsesRequest; + } + + /** + * Gemini响应 -> OpenAI Responses响应 + */ + toOpenAIResponsesResponse(geminiResponse, model) { + const content = this.processGeminiResponseContent(geminiResponse); + const textContent = typeof content === 'string' ? content : JSON.stringify(content); + + let output = []; + output.push({ + id: `msg_${uuidv4().replace(/-/g, '')}`, + summary: [], + type: "message", + role: "assistant", + status: "completed", + content: [{ + annotations: [], + logprobs: [], + text: textContent, + type: "output_text" + }] + }); + + return { + background: false, + created_at: Math.floor(Date.now() / 1000), + error: null, + id: `resp_${uuidv4().replace(/-/g, '')}`, + incomplete_details: null, + max_output_tokens: null, + max_tool_calls: null, + metadata: {}, + model: model, + object: "response", + output: output, + parallel_tool_calls: true, + previous_response_id: null, + prompt_cache_key: null, + reasoning: {}, + safety_identifier: "user-" + uuidv4().replace(/-/g, ''), + service_tier: "default", + status: "completed", + store: false, + temperature: 1, + text: { + format: { type: "text" }, + }, + tool_choice: "auto", + tools: [], + top_logprobs: 0, + top_p: 1, + truncation: "disabled", + usage: { + input_tokens: geminiResponse.usageMetadata?.promptTokenCount || 0, + input_tokens_details: { + cached_tokens: geminiResponse.usageMetadata?.cachedTokens || 0, + }, + output_tokens: geminiResponse.usageMetadata?.candidatesTokenCount || 0, + output_tokens_details: { + reasoning_tokens: 0 + }, + total_tokens: geminiResponse.usageMetadata?.totalTokenCount || 0, + }, + user: null + }; + } + + /** + * Gemini流式响应 -> OpenAI Responses流式响应 + */ + toOpenAIResponsesStreamChunk(geminiChunk, model, requestId = null) { + if (!geminiChunk) return []; + + const responseId = requestId || `resp_${uuidv4().replace(/-/g, '')}`; + const events = []; + + // 处理完整的Gemini chunk对象 + if (typeof geminiChunk === 'object' && !Array.isArray(geminiChunk)) { + const candidate = geminiChunk.candidates?.[0]; + + if (candidate) { + const parts = candidate.content?.parts; + + // 第一个chunk - 检测是否是开始(有role) + if (candidate.content?.role === 'model' && parts && parts.length > 0) { + // 只在第一次有内容时发送开始事件 + const hasContent = parts.some(part => part && typeof part.text === 'string' && part.text.length > 0); + if (hasContent) { + events.push( + generateResponseCreated(responseId, model || 'unknown'), + generateResponseInProgress(responseId), + generateOutputItemAdded(responseId), + generateContentPartAdded(responseId) + ); + } + } + + // 提取文本内容 + if (parts && Array.isArray(parts)) { + const textParts = parts.filter(part => part && typeof part.text === 'string'); + if (textParts.length > 0) { + const text = textParts.map(part => part.text).join(''); + events.push({ + delta: text, + item_id: `msg_${uuidv4().replace(/-/g, '')}`, + output_index: 0, + sequence_number: 3, + type: "response.output_text.delta" + }); + } + } + + // 处理finishReason + if (candidate.finishReason) { + events.push( + generateOutputTextDone(responseId), + generateContentPartDone(responseId), + generateOutputItemDone(responseId), + generateResponseCompleted(responseId) + ); + + // 如果有 usage 信息,更新最后一个事件 + if (geminiChunk.usageMetadata && events.length > 0) { + const lastEvent = events[events.length - 1]; + if (lastEvent.response) { + lastEvent.response.usage = { + input_tokens: geminiChunk.usageMetadata.promptTokenCount || 0, + output_tokens: geminiChunk.usageMetadata.candidatesTokenCount || 0, + total_tokens: geminiChunk.usageMetadata.totalTokenCount || 0 + }; + } + } + } + } + } + + // 向后兼容:处理字符串格式 + if (typeof geminiChunk === 'string') { + events.push({ + delta: geminiChunk, + item_id: `msg_${uuidv4().replace(/-/g, '')}`, + output_index: 0, + sequence_number: 3, + type: "response.output_text.delta" + }); + } + + return events; + } +} + +export default GeminiConverter; \ No newline at end of file diff --git a/src/converters/strategies/OpenAIConverter.js b/src/converters/strategies/OpenAIConverter.js new file mode 100644 index 0000000..b2796c6 --- /dev/null +++ b/src/converters/strategies/OpenAIConverter.js @@ -0,0 +1,997 @@ +/** + * OpenAI转换器 + * 处理OpenAI协议与其他协议之间的转换 + */ + +import { v4 as uuidv4 } from 'uuid'; +import { BaseConverter } from '../BaseConverter.js'; +import { + extractAndProcessSystemMessages as extractSystemMessages, + extractTextFromMessageContent as extractText, + safeParseJSON, + checkAndAssignOrDefault, + extractThinkingFromOpenAIText, + mapFinishReason, + cleanJsonSchemaProperties as cleanJsonSchema +} from '../utils.js'; +import { MODEL_PROTOCOL_PREFIX } from '../../common.js'; +import { + generateResponseCreated, + generateResponseInProgress, + generateOutputItemAdded, + generateContentPartAdded, + generateOutputTextDone, + generateContentPartDone, + generateOutputItemDone, + generateResponseCompleted +} from '../../openai/openai-responses-core.mjs'; + +/** + * OpenAI转换器类 + * 实现OpenAI协议到其他协议的转换 + */ +export class OpenAIConverter extends BaseConverter { + constructor() { + super('openai'); + } + + /** + * 转换请求 + */ + convertRequest(data, targetProtocol) { + switch (targetProtocol) { + case MODEL_PROTOCOL_PREFIX.CLAUDE: + return this.toClaudeRequest(data); + case MODEL_PROTOCOL_PREFIX.GEMINI: + return this.toGeminiRequest(data); + case MODEL_PROTOCOL_PREFIX.OPENAI_RESPONSES: + return this.toOpenAIResponsesRequest(data); + default: + throw new Error(`Unsupported target protocol: ${targetProtocol}`); + } + } + + /** + * 转换响应 + */ + convertResponse(data, targetProtocol, model) { + // OpenAI作为源格式时,通常不需要转换响应 + // 因为其他协议会转换到OpenAI格式 + switch (targetProtocol) { + case MODEL_PROTOCOL_PREFIX.CLAUDE: + return this.toClaudeResponse(data, model); + case MODEL_PROTOCOL_PREFIX.GEMINI: + return this.toGeminiResponse(data, model); + case MODEL_PROTOCOL_PREFIX.OPENAI_RESPONSES: + return this.toOpenAIResponsesResponse(data, model); + default: + throw new Error(`Unsupported target protocol: ${targetProtocol}`); + } + } + + /** + * 转换流式响应块 + */ + convertStreamChunk(chunk, targetProtocol, model) { + switch (targetProtocol) { + case MODEL_PROTOCOL_PREFIX.CLAUDE: + return this.toClaudeStreamChunk(chunk, model); + case MODEL_PROTOCOL_PREFIX.GEMINI: + return this.toGeminiStreamChunk(chunk, model); + case MODEL_PROTOCOL_PREFIX.OPENAI_RESPONSES: + return this.toOpenAIResponsesStreamChunk(chunk, model); + default: + throw new Error(`Unsupported target protocol: ${targetProtocol}`); + } + } + + /** + * 转换模型列表 + */ + convertModelList(data, targetProtocol) { + switch (targetProtocol) { + case MODEL_PROTOCOL_PREFIX.CLAUDE: + return this.toClaudeModelList(data); + case MODEL_PROTOCOL_PREFIX.GEMINI: + return this.toGeminiModelList(data); + default: + return data; + } + } + + // ========================================================================= + // OpenAI -> Claude 转换 + // ========================================================================= + + /** + * OpenAI请求 -> Claude请求 + */ + toClaudeRequest(openaiRequest) { + const messages = openaiRequest.messages || []; + const { systemInstruction, nonSystemMessages } = extractSystemMessages(messages); + + const claudeMessages = []; + + for (const message of nonSystemMessages) { + const role = message.role === 'assistant' ? 'assistant' : 'user'; + let content = []; + + if (message.role === 'tool') { + // 工具结果消息 + content.push({ + type: 'tool_result', + tool_use_id: message.tool_call_id, + content: safeParseJSON(message.content) + }); + claudeMessages.push({ role: 'user', content: content }); + } else if (message.role === 'assistant' && (message.tool_calls?.length || message.function_calls?.length)) { + // 助手工具调用消息 - 支持tool_calls和function_calls + const calls = message.tool_calls || message.function_calls || []; + const toolUseBlocks = calls.map(tc => ({ + type: 'tool_use', + id: tc.id, + name: tc.function.name, + input: safeParseJSON(tc.function.arguments) + })); + claudeMessages.push({ role: 'assistant', content: toolUseBlocks }); + } else { + // 普通消息 + if (typeof message.content === 'string') { + if (message.content) { + content.push({ type: 'text', text: message.content }); + } + } else if (Array.isArray(message.content)) { + message.content.forEach(item => { + if (!item) return; + switch (item.type) { + case 'text': + if (item.text) { + content.push({ type: 'text', text: item.text }); + } + break; + case 'image_url': + if (item.image_url) { + const imageUrl = typeof item.image_url === 'string' + ? item.image_url + : item.image_url.url; + if (imageUrl.startsWith('data:')) { + const [header, data] = imageUrl.split(','); + const mediaType = header.match(/data:([^;]+)/)?.[1] || 'image/jpeg'; + content.push({ + type: 'image', + source: { + type: 'base64', + media_type: mediaType, + data: data + } + }); + } else { + content.push({ type: 'text', text: `[Image: ${imageUrl}]` }); + } + } + break; + case 'audio': + if (item.audio_url) { + const audioUrl = typeof item.audio_url === 'string' + ? item.audio_url + : item.audio_url.url; + content.push({ type: 'text', text: `[Audio: ${audioUrl}]` }); + } + break; + } + }); + } + if (content.length > 0) { + claudeMessages.push({ role: role, content: content }); + } + } + } + // 合并相邻相同 role 的消息 + const mergedClaudeMessages = []; + for (let i = 0; i < claudeMessages.length; i++) { + const currentMessage = claudeMessages[i]; + + if (mergedClaudeMessages.length === 0) { + mergedClaudeMessages.push(currentMessage); + } else { + const lastMessage = mergedClaudeMessages[mergedClaudeMessages.length - 1]; + + // 如果当前消息的 role 与上一条消息的 role 相同,则合并 content 数组 + if (lastMessage.role === currentMessage.role) { + lastMessage.content = lastMessage.content.concat(currentMessage.content); + } else { + mergedClaudeMessages.push(currentMessage); + } + } + } + + + const claudeRequest = { + model: openaiRequest.model, + messages: mergedClaudeMessages, + max_tokens: checkAndAssignOrDefault(openaiRequest.max_tokens, 8192), + temperature: checkAndAssignOrDefault(openaiRequest.temperature, 1), + top_p: checkAndAssignOrDefault(openaiRequest.top_p, 0.95), + }; + + if (systemInstruction) { + claudeRequest.system = extractText(systemInstruction.parts[0].text); + } + + if (openaiRequest.tools?.length) { + claudeRequest.tools = openaiRequest.tools.map(t => ({ + name: t.function.name, + description: t.function.description || '', + input_schema: t.function.parameters || { type: 'object', properties: {} } + })); + claudeRequest.tool_choice = this.buildClaudeToolChoice(openaiRequest.tool_choice); + } + + return claudeRequest; + } + + /** + * OpenAI响应 -> Claude响应 + */ + toClaudeResponse(openaiResponse, model) { + if (!openaiResponse || !openaiResponse.choices || openaiResponse.choices.length === 0) { + return { + id: `msg_${uuidv4()}`, + type: "message", + role: "assistant", + content: [], + model: model, + stop_reason: "end_turn", + stop_sequence: null, + usage: { + input_tokens: openaiResponse?.usage?.prompt_tokens || 0, + output_tokens: openaiResponse?.usage?.completion_tokens || 0 + } + }; + } + + const choice = openaiResponse.choices[0]; + const contentList = []; + + // 处理工具调用 - 支持tool_calls和function_calls + const toolCalls = choice.message?.tool_calls || choice.message?.function_calls || []; + for (const toolCall of toolCalls.filter(tc => tc && typeof tc === 'object')) { + if (toolCall.function) { + const func = toolCall.function; + const argStr = func.arguments || "{}"; + let argObj; + try { + argObj = typeof argStr === 'string' ? JSON.parse(argStr) : argStr; + } catch (e) { + argObj = {}; + } + contentList.push({ + type: "tool_use", + id: toolCall.id || "", + name: func.name || "", + input: argObj, + }); + } + } + + // 处理reasoning_content(推理内容) + const reasoningContent = choice.message?.reasoning_content || ""; + if (reasoningContent) { + contentList.push({ + type: "thinking", + thinking: reasoningContent + }); + } + + // 处理文本内容 + const contentText = choice.message?.content || ""; + if (contentText) { + const extractedContent = extractThinkingFromOpenAIText(contentText); + if (Array.isArray(extractedContent)) { + contentList.push(...extractedContent); + } else { + contentList.push({ type: "text", text: extractedContent }); + } + } + + // 映射结束原因 + const stopReason = mapFinishReason( + choice.finish_reason || "stop", + "openai", + "anthropic" + ); + + return { + id: `msg_${uuidv4()}`, + type: "message", + role: "assistant", + content: contentList, + model: model, + stop_reason: stopReason, + stop_sequence: null, + usage: { + input_tokens: openaiResponse.usage?.prompt_tokens || 0, + output_tokens: openaiResponse.usage?.completion_tokens || 0 + } + }; + } + + /** + * OpenAI流式响应 -> Claude流式响应 + * + * 这个方法实现了与 ClaudeConverter.toOpenAIStreamChunk 相反的转换逻辑 + * 将 OpenAI 的流式 chunk 转换为 Claude 的流式事件 + */ + toClaudeStreamChunk(openaiChunk, model) { + if (!openaiChunk) return null; + + // 处理 OpenAI chunk 对象 + if (typeof openaiChunk === 'object' && !Array.isArray(openaiChunk)) { + const choice = openaiChunk.choices?.[0]; + if (!choice){ + return null; + } + + const delta = choice.delta; + const finishReason = choice.finish_reason; + const events = []; + + // 注释部分是为了兼容claude code,但是不兼容cherry studio + // 1. 处理 role (对应 message_start) + // if (delta?.role === "assistant") { + // events.push({ + // type: "message_start", + // message: { + // id: openaiChunk.id || `msg_${uuidv4()}`, + // type: "message", + // role: "assistant", + // content: [], + // model: model || openaiChunk.model || "unknown", + // stop_reason: null, + // stop_sequence: null, + // usage: { + // input_tokens: openaiChunk.usage?.prompt_tokens || 0, + // output_tokens: 0 + // } + // } + // }); + // events.push({ + // type: "content_block_start", + // index: 0, + // content_block: { + // type: "text", + // text: "" + // } + // }); + // } + + // 2. 处理 tool_calls (对应 content_block_start 和 content_block_delta) + // if (delta?.tool_calls) { + // const toolCalls = delta.tool_calls; + // for (const toolCall of toolCalls) { + // // 如果有 function.name,说明是工具调用开始 + // if (toolCall.function?.name) { + // events.push({ + // type: "content_block_start", + // index: toolCall.index || 0, + // content_block: { + // type: "tool_use", + // id: toolCall.id || `tool_${uuidv4()}`, + // name: toolCall.function.name, + // input: {} + // } + // }); + // } + + // // 如果有 function.arguments,说明是参数增量 + // if (toolCall.function?.arguments) { + // events.push({ + // type: "content_block_delta", + // index: toolCall.index || 0, + // delta: { + // type: "input_json_delta", + // partial_json: toolCall.function.arguments + // } + // }); + // } + // } + // } + + // 3. 处理 reasoning_content (对应 thinking 类型的 content_block) + if (delta?.reasoning_content) { + // 注意:这里可能需要先发送 content_block_start,但由于状态管理复杂, + // 我们假设调用方会处理这个逻辑 + events.push({ + type: "content_block_delta", + index: 0, + delta: { + type: "thinking_delta", + thinking: delta.reasoning_content + } + }); + } + + // 4. 处理普通文本 content (对应 text 类型的 content_block) + if (delta?.content) { + events.push({ + type: "content_block_delta", + index: 0, + delta: { + type: "text_delta", + text: delta.content + } + }); + } + + // 5. 处理 finish_reason (对应 message_delta 和 message_stop) + if (finishReason) { + // 映射 finish_reason + const stopReason = finishReason === "stop" ? "end_turn" : + finishReason === "length" ? "max_tokens" : + "end_turn"; + + events.push({ + type: "content_block_stop", + index: 0 + }); + // 发送 message_delta + events.push({ + type: "message_delta", + delta: { + stop_reason: stopReason, + stop_sequence: null + }, + usage: { + output_tokens: openaiChunk.usage?.completion_tokens || 0, + input_tokens: openaiChunk.usage?.prompt_tokens || 0, + } + }); + + // 发送 message_stop + events.push({ + type: "message_stop" + }); + } + + return events.length > 0 ? events : null; + } + + // 向后兼容:处理字符串格式 + if (typeof openaiChunk === 'string') { + return { + type: "content_block_delta", + index: 0, + delta: { + type: "text_delta", + text: openaiChunk + } + }; + } + + return null; + } + + /** + * OpenAI模型列表 -> Claude模型列表 + */ + toClaudeModelList(openaiModels) { + return { + models: openaiModels.data.map(m => ({ + name: m.id, + description: "", + })), + }; + } + + /** + * 将 OpenAI 模型列表转换为 Gemini 模型列表 + */ + toGeminiModelList(openaiModels) { + const models = openaiModels.data || []; + return { + models: models.map(m => ({ + name: `models/${m.id}`, + version: m.version || "1.0.0", + displayName: m.displayName || m.id, + description: m.description || `A generative model for text and chat generation. ID: ${m.id}`, + inputTokenLimit: m.inputTokenLimit || 32768, + outputTokenLimit: m.outputTokenLimit || 8192, + supportedGenerationMethods: m.supportedGenerationMethods || ["generateContent", "streamGenerateContent"] + })) + }; + } + + /** + * 构建Claude工具选择 + */ + buildClaudeToolChoice(toolChoice) { + if (typeof toolChoice === 'string') { + const mapping = { auto: 'auto', none: 'none', required: 'any' }; + return { type: mapping[toolChoice] }; + } + if (typeof toolChoice === 'object' && toolChoice.function) { + return { type: 'tool', name: toolChoice.function.name }; + } + return undefined; + } + + // ========================================================================= + // OpenAI -> Gemini 转换 + // ========================================================================= + + /** + * OpenAI请求 -> Gemini请求 + */ + toGeminiRequest(openaiRequest) { + const messages = openaiRequest.messages || []; + const { systemInstruction, nonSystemMessages } = extractSystemMessages(messages); + + const processedMessages = []; + let lastMessage = null; + + for (const message of nonSystemMessages) { + const geminiRole = message.role === 'assistant' ? 'model' : message.role; + + if (geminiRole === 'tool') { + if (lastMessage) processedMessages.push(lastMessage); + processedMessages.push({ + role: 'function', + parts: [{ + functionResponse: { + name: message.name, + response: { content: safeParseJSON(message.content) } + } + }] + }); + lastMessage = null; + continue; + } + + const processedContent = this.processOpenAIContentToGeminiParts(message.content); + + if (lastMessage && lastMessage.role === geminiRole && !message.tool_calls && + Array.isArray(processedContent) && processedContent.every(p => p.text) && + Array.isArray(lastMessage.parts) && lastMessage.parts.every(p => p.text)) { + lastMessage.parts.push(...processedContent); + continue; + } + + if (lastMessage) processedMessages.push(lastMessage); + lastMessage = { role: geminiRole, parts: processedContent }; + } + if (lastMessage) processedMessages.push(lastMessage); + + const geminiRequest = { + contents: processedMessages.filter(item => item.parts && item.parts.length > 0) + }; + + if (systemInstruction) geminiRequest.systemInstruction = systemInstruction; + + if (openaiRequest.tools?.length) { + geminiRequest.tools = [{ + functionDeclarations: openaiRequest.tools.map(t => { + if (!t || typeof t !== 'object' || !t.function) return null; + const func = t.function; + const parameters = cleanJsonSchema(func.parameters || {}); + return { + name: String(func.name || ''), + description: String(func.description || ''), + parameters: parameters + }; + }).filter(Boolean) + }]; + if (geminiRequest.tools[0].functionDeclarations.length === 0) { + delete geminiRequest.tools; + } + } + + if (openaiRequest.tool_choice) { + geminiRequest.toolConfig = this.buildGeminiToolConfig(openaiRequest.tool_choice); + } + + const config = this.buildGeminiGenerationConfig(openaiRequest); + if (Object.keys(config).length) geminiRequest.generationConfig = config; + + return geminiRequest; + } + + /** + * 处理OpenAI内容到Gemini parts + */ + processOpenAIContentToGeminiParts(content) { + if (!content) return []; + if (typeof content === 'string') return [{ text: content }]; + + if (Array.isArray(content)) { + const parts = []; + + for (const item of content) { + if (!item) continue; + + if (item.type === 'text' && item.text) { + parts.push({ text: item.text }); + } else if (item.type === 'image_url' && item.image_url) { + const imageUrl = typeof item.image_url === 'string' + ? item.image_url + : item.image_url.url; + + if (imageUrl.startsWith('data:')) { + const [header, data] = imageUrl.split(','); + const mimeType = header.match(/data:([^;]+)/)?.[1] || 'image/jpeg'; + parts.push({ inlineData: { mimeType, data } }); + } else { + parts.push({ + fileData: { mimeType: 'image/jpeg', fileUri: imageUrl } + }); + } + } + } + + return parts; + } + + return []; + } + + /** + * 构建Gemini工具配置 + */ + buildGeminiToolConfig(toolChoice) { + if (typeof toolChoice === 'string' && ['none', 'auto'].includes(toolChoice)) { + return { functionCallingConfig: { mode: toolChoice.toUpperCase() } }; + } + if (typeof toolChoice === 'object' && toolChoice.function) { + return { functionCallingConfig: { mode: 'ANY', allowedFunctionNames: [toolChoice.function.name] } }; + } + return null; + } + + /** + * 构建Gemini生成配置 + */ + buildGeminiGenerationConfig({ temperature, max_tokens, top_p, stop }) { + const config = {}; + config.temperature = checkAndAssignOrDefault(temperature, 1); + config.maxOutputTokens = checkAndAssignOrDefault(max_tokens, 65535); + config.topP = checkAndAssignOrDefault(top_p, 0.95); + if (stop !== undefined) config.stopSequences = Array.isArray(stop) ? stop : [stop]; + return config; + } + /** + * 将OpenAI响应转换为Gemini响应格式 + */ + toGeminiResponse(openaiResponse, model) { + if (!openaiResponse || !openaiResponse.choices || !openaiResponse.choices[0]) { + return { candidates: [], usageMetadata: {} }; + } + + const choice = openaiResponse.choices[0]; + const message = choice.message || {}; + const parts = []; + + // 处理文本内容 + if (message.content) { + parts.push({ text: message.content }); + } + + // 处理工具调用 + if (message.tool_calls && message.tool_calls.length > 0) { + for (const toolCall of message.tool_calls) { + if (toolCall.type === 'function') { + parts.push({ + functionCall: { + name: toolCall.function.name, + args: typeof toolCall.function.arguments === 'string' + ? JSON.parse(toolCall.function.arguments) + : toolCall.function.arguments + } + }); + } + } + } + + // 映射finish_reason + const finishReasonMap = { + 'stop': 'STOP', + 'length': 'MAX_TOKENS', + 'tool_calls': 'STOP', + 'content_filter': 'SAFETY' + }; + + return { + candidates: [{ + content: { + role: 'model', + parts: parts + }, + finishReason: finishReasonMap[choice.finish_reason] || 'STOP' + }], + usageMetadata: openaiResponse.usage ? { + promptTokenCount: openaiResponse.usage.prompt_tokens || 0, + candidatesTokenCount: openaiResponse.usage.completion_tokens || 0, + totalTokenCount: openaiResponse.usage.total_tokens || 0 + } : {} + }; + } + + /** + * 将OpenAI流式响应块转换为Gemini流式响应格式 + */ + toGeminiStreamChunk(openaiChunk, model) { + if (!openaiChunk || !openaiChunk.choices || !openaiChunk.choices[0]) { + return null; + } + + const choice = openaiChunk.choices[0]; + const delta = choice.delta || {}; + const parts = []; + + // 处理文本内容 + if (delta.content) { + parts.push({ text: delta.content }); + } + + // 处理工具调用 + if (delta.tool_calls && delta.tool_calls.length > 0) { + for (const toolCall of delta.tool_calls) { + if (toolCall.function) { + const functionCall = { + name: toolCall.function.name || '', + args: {} + }; + + if (toolCall.function.arguments) { + try { + functionCall.args = typeof toolCall.function.arguments === 'string' + ? JSON.parse(toolCall.function.arguments) + : toolCall.function.arguments; + } catch (e) { + // 部分参数,保持为字符串 + functionCall.args = { partial: toolCall.function.arguments }; + } + } + + parts.push({ functionCall }); + } + } + } + + const result = { + candidates: [{ + content: { + role: 'model', + parts: parts + } + }] + }; + + // 添加finish_reason(如果存在) + if (choice.finish_reason) { + const finishReasonMap = { + 'stop': 'STOP', + 'length': 'MAX_TOKENS', + 'tool_calls': 'STOP', + 'content_filter': 'SAFETY' + }; + result.candidates[0].finishReason = finishReasonMap[choice.finish_reason] || 'STOP'; + } + + // 添加usage信息(如果存在) + if (openaiChunk.usage) { + result.usageMetadata = { + promptTokenCount: openaiChunk.usage.prompt_tokens || 0, + candidatesTokenCount: openaiChunk.usage.completion_tokens || 0, + totalTokenCount: openaiChunk.usage.total_tokens || 0 + }; + } + + return result; + } + + /** + * 将OpenAI请求转换为OpenAI Responses格式 + */ + toOpenAIResponsesRequest(openaiRequest) { + const responsesRequest = { + model: openaiRequest.model, + messages: [] + }; + + // 转换messages + if (openaiRequest.messages && openaiRequest.messages.length > 0) { + responsesRequest.messages = openaiRequest.messages.map(msg => ({ + role: msg.role, + content: typeof msg.content === 'string' + ? [{ type: 'input_text', text: msg.content }] + : msg.content + })); + } + + // 转换其他参数 + if (openaiRequest.temperature !== undefined) { + responsesRequest.temperature = openaiRequest.temperature; + } + if (openaiRequest.max_tokens !== undefined) { + responsesRequest.max_output_tokens = openaiRequest.max_tokens; + } + if (openaiRequest.top_p !== undefined) { + responsesRequest.top_p = openaiRequest.top_p; + } + if (openaiRequest.tools) { + responsesRequest.tools = openaiRequest.tools; + } + if (openaiRequest.tool_choice) { + responsesRequest.tool_choice = openaiRequest.tool_choice; + } + + return responsesRequest; + } + + /** + * 将OpenAI响应转换为OpenAI Responses格式 + */ + toOpenAIResponsesResponse(openaiResponse, model) { + if (!openaiResponse || !openaiResponse.choices || !openaiResponse.choices[0]) { + return { + id: `resp_${Date.now()}`, + object: 'response', + created_at: Math.floor(Date.now() / 1000), + status: 'completed', + model: model || 'unknown', + output: [], + usage: { + input_tokens: 0, + output_tokens: 0, + total_tokens: 0 + } + }; + } + + const choice = openaiResponse.choices[0]; + const message = choice.message || {}; + const output = []; + + // 构建message输出 + const messageContent = []; + if (message.content) { + messageContent.push({ + type: 'output_text', + text: message.content + }); + } + + output.push({ + type: 'message', + id: `msg_${Date.now()}`, + status: 'completed', + role: 'assistant', + content: messageContent + }); + + return { + id: openaiResponse.id || `resp_${Date.now()}`, + object: 'response', + created_at: openaiResponse.created || Math.floor(Date.now() / 1000), + status: choice.finish_reason === 'stop' ? 'completed' : 'in_progress', + model: model || openaiResponse.model || 'unknown', + output: output, + usage: openaiResponse.usage ? { + input_tokens: openaiResponse.usage.prompt_tokens || 0, + output_tokens: openaiResponse.usage.completion_tokens || 0, + total_tokens: openaiResponse.usage.total_tokens || 0 + } : { + input_tokens: 0, + output_tokens: 0, + total_tokens: 0 + } + }; + } + + /** + * 将OpenAI流式响应转换为OpenAI Responses流式格式 + * 参考 ClaudeConverter.toOpenAIResponsesStreamChunk 的实现逻辑 + */ + toOpenAIResponsesStreamChunk(openaiChunk, model, requestId = null) { + if (!openaiChunk || !openaiChunk.choices || !openaiChunk.choices[0]) { + return []; + } + + const responseId = requestId || `resp_${uuidv4().replace(/-/g, '')}`; + const choice = openaiChunk.choices[0]; + const delta = choice.delta || {}; + const events = []; + + // 第一个chunk - role为assistant时调用 getOpenAIResponsesStreamChunkBegin + if (delta.role === 'assistant') { + events.push( + generateResponseCreated(responseId, model || openaiChunk.model || 'unknown'), + generateResponseInProgress(responseId), + generateOutputItemAdded(responseId), + generateContentPartAdded(responseId) + ); + } + + // 处理 reasoning_content(推理内容) + if (delta.reasoning_content) { + events.push({ + delta: delta.reasoning_content, + item_id: `thinking_${uuidv4().replace(/-/g, '')}`, + output_index: 0, + sequence_number: 3, + type: "response.reasoning_summary_text.delta" + }); + } + + // 处理 tool_calls(工具调用) + if (delta.tool_calls && delta.tool_calls.length > 0) { + for (const toolCall of delta.tool_calls) { + const outputIndex = toolCall.index || 0; + + // 如果有 function.name,说明是工具调用开始 + if (toolCall.function && toolCall.function.name) { + events.push({ + item: { + id: toolCall.id || `call_${uuidv4().replace(/-/g, '')}`, + type: "function_call", + name: toolCall.function.name, + arguments: "", + status: "in_progress" + }, + output_index: outputIndex, + sequence_number: 2, + type: "response.output_item.added" + }); + } + + // 如果有 function.arguments,说明是参数增量 + if (toolCall.function && toolCall.function.arguments) { + events.push({ + delta: toolCall.function.arguments, + item_id: toolCall.id || `call_${uuidv4().replace(/-/g, '')}`, + output_index: outputIndex, + sequence_number: 3, + type: "response.custom_tool_call_input.delta" + }); + } + } + } + + // 处理普通文本内容 + if (delta.content) { + events.push({ + delta: delta.content, + item_id: `msg_${uuidv4().replace(/-/g, '')}`, + output_index: 0, + sequence_number: 3, + type: "response.output_text.delta" + }); + } + + // 处理完成状态 - 调用 getOpenAIResponsesStreamChunkEnd + if (choice.finish_reason) { + events.push( + generateOutputTextDone(responseId), + generateContentPartDone(responseId), + generateOutputItemDone(responseId), + generateResponseCompleted(responseId) + ); + + // 如果有 usage 信息,更新最后一个事件 + if (openaiChunk.usage && events.length > 0) { + const lastEvent = events[events.length - 1]; + if (lastEvent.response) { + lastEvent.response.usage = { + input_tokens: openaiChunk.usage.prompt_tokens || 0, + output_tokens: openaiChunk.usage.completion_tokens || 0, + total_tokens: openaiChunk.usage.total_tokens || 0 + }; + } + } + } + + return events; + } + +} + +export default OpenAIConverter; \ No newline at end of file diff --git a/src/converters/strategies/OpenAIResponsesConverter.js b/src/converters/strategies/OpenAIResponsesConverter.js new file mode 100644 index 0000000..50ce3c5 --- /dev/null +++ b/src/converters/strategies/OpenAIResponsesConverter.js @@ -0,0 +1,532 @@ +/** + * OpenAI Responses API 转换器 + * 处理 OpenAI Responses API 格式与其他协议之间的转换 + */ + +import { BaseConverter } from '../BaseConverter.js'; +import { MODEL_PROTOCOL_PREFIX } from '../../common.js'; +import { + extractAndProcessSystemMessages as extractSystemMessages, + extractTextFromMessageContent as extractText +} from '../utils.js'; + +/** + * OpenAI Responses API 转换器类 + * 支持 OpenAI Responses 格式与 OpenAI、Claude、Gemini 之间的转换 + */ +export class OpenAIResponsesConverter extends BaseConverter { + constructor() { + super(MODEL_PROTOCOL_PREFIX.OPENAI_RESPONSES); + } + + // ============================================================================= + // 请求转换 + // ============================================================================= + + /** + * 转换请求到目标协议 + */ + convertRequest(data, toProtocol) { + switch (toProtocol) { + case MODEL_PROTOCOL_PREFIX.OPENAI: + return this.toOpenAIRequest(data); + case MODEL_PROTOCOL_PREFIX.CLAUDE: + return this.toClaudeRequest(data); + case MODEL_PROTOCOL_PREFIX.GEMINI: + return this.toGeminiRequest(data); + default: + throw new Error(`Unsupported target protocol: ${toProtocol}`); + } + } + + /** + * 转换响应到目标协议 + */ + convertResponse(data, toProtocol, model) { + switch (toProtocol) { + case MODEL_PROTOCOL_PREFIX.OPENAI: + return this.toOpenAIResponse(data, model); + case MODEL_PROTOCOL_PREFIX.CLAUDE: + return this.toClaudeResponse(data, model); + case MODEL_PROTOCOL_PREFIX.GEMINI: + return this.toGeminiResponse(data, model); + default: + throw new Error(`Unsupported target protocol: ${toProtocol}`); + } + } + + /** + * 转换流式响应块到目标协议 + */ + convertStreamChunk(chunk, toProtocol, model) { + switch (toProtocol) { + case MODEL_PROTOCOL_PREFIX.OPENAI: + return this.toOpenAIStreamChunk(chunk, model); + case MODEL_PROTOCOL_PREFIX.CLAUDE: + return this.toClaudeStreamChunk(chunk, model); + case MODEL_PROTOCOL_PREFIX.GEMINI: + return this.toGeminiStreamChunk(chunk, model); + default: + throw new Error(`Unsupported target protocol: ${toProtocol}`); + } + } + + /** + * 转换模型列表到目标协议 + */ + convertModelList(data, targetProtocol) { + switch (targetProtocol) { + case MODEL_PROTOCOL_PREFIX.OPENAI: + return this.toOpenAIModelList(data); + case MODEL_PROTOCOL_PREFIX.CLAUDE: + return this.toClaudeModelList(data); + case MODEL_PROTOCOL_PREFIX.GEMINI: + return this.toGeminiModelList(data); + default: + return data; + } + } + + // ============================================================================= + // 转换到 OpenAI 格式 + // ============================================================================= + + /** + * 将 OpenAI Responses 请求转换为标准 OpenAI 请求 + */ + toOpenAIRequest(responsesRequest) { + const openaiRequest = { + model: responsesRequest.model, + messages: [], + stream: responsesRequest.stream || false + }; + + // OpenAI Responses API 使用 instructions 和 input 字段 + // 需要转换为标准的 messages 格式 + if (responsesRequest.instructions) { + // instructions 作为系统消息 + openaiRequest.messages.push({ + role: 'system', + content: responsesRequest.instructions + }); + } + + // input 包含用户消息和历史对话 + if (responsesRequest.input && Array.isArray(responsesRequest.input)) { + responsesRequest.input.forEach(item => { + if (item.type === 'message') { + // 提取消息内容 + const content = item.content + .filter(c => c.type === 'input_text') + .map(c => c.text) + .join('\n'); + + if (content) { + openaiRequest.messages.push({ + role: item.role, + content: content + }); + } + } + }); + } + + // 如果有标准的 messages 字段,也支持 + if (responsesRequest.messages && Array.isArray(responsesRequest.messages)) { + responsesRequest.messages.forEach(msg => { + openaiRequest.messages.push({ + role: msg.role, + content: msg.content + }); + }); + } + + // 复制其他参数 + if (responsesRequest.temperature !== undefined) { + openaiRequest.temperature = responsesRequest.temperature; + } + if (responsesRequest.max_tokens !== undefined) { + openaiRequest.max_tokens = responsesRequest.max_tokens; + } + if (responsesRequest.top_p !== undefined) { + openaiRequest.top_p = responsesRequest.top_p; + } + + return openaiRequest; + } + + /** + * 将 OpenAI Responses 响应转换为标准 OpenAI 响应 + */ + toOpenAIResponse(responsesResponse, model) { + // OpenAI Responses 格式已经很接近标准 OpenAI 格式 + return { + id: responsesResponse.id || `chatcmpl-${Date.now()}`, + object: 'chat.completion', + created: responsesResponse.created || Math.floor(Date.now() / 1000), + model: model || responsesResponse.model, + choices: responsesResponse.choices || [{ + index: 0, + message: { + role: 'assistant', + content: responsesResponse.content || '' + }, + finish_reason: responsesResponse.finish_reason || 'stop' + }], + usage: responsesResponse.usage || { + prompt_tokens: 0, + completion_tokens: 0, + total_tokens: 0 + } + }; + } + + /** + * 将 OpenAI Responses 流式块转换为标准 OpenAI 流式块 + */ + toOpenAIStreamChunk(responsesChunk, model) { + return { + id: responsesChunk.id || `chatcmpl-${Date.now()}`, + object: 'chat.completion.chunk', + created: responsesChunk.created || Math.floor(Date.now() / 1000), + model: model || responsesChunk.model, + choices: responsesChunk.choices || [{ + index: 0, + delta: { + content: responsesChunk.delta?.content || '' + }, + finish_reason: responsesChunk.finish_reason || null + }] + }; + } + + // ============================================================================= + // 转换到 Claude 格式 + // ============================================================================= + + /** + * 将 OpenAI Responses 请求转换为 Claude 请求 + */ + toClaudeRequest(responsesRequest) { + const claudeRequest = { + model: responsesRequest.model, + messages: [], + max_tokens: responsesRequest.max_tokens || 4096, + stream: responsesRequest.stream || false + }; + + // 处理 instructions 作为系统消息 + if (responsesRequest.instructions) { + claudeRequest.system = responsesRequest.instructions; + } + + // 处理 input 数组中的消息 + if (responsesRequest.input && Array.isArray(responsesRequest.input)) { + responsesRequest.input.forEach(item => { + if (item.type === 'message') { + const content = item.content + .filter(c => c.type === 'input_text') + .map(c => c.text) + .join('\n'); + + if (content) { + claudeRequest.messages.push({ + role: item.role === 'assistant' ? 'assistant' : 'user', + content: content + }); + } + } + }); + } + + // 如果有标准的 messages 字段,也支持 + if (responsesRequest.messages && Array.isArray(responsesRequest.messages)) { + if (!claudeRequest.system && systemMessages.length > 0) { + const systemTexts = systemMessages.map(msg => extractText(msg.content)); + claudeRequest.system = systemTexts.join('\n'); + } + + otherMessages.forEach(msg => { + claudeRequest.messages.push({ + role: msg.role === 'assistant' ? 'assistant' : 'user', + content: typeof msg.content === 'string' ? msg.content : extractText(msg.content) + }); + }); + } + + // 复制其他参数 + if (responsesRequest.temperature !== undefined) { + claudeRequest.temperature = responsesRequest.temperature; + } + if (responsesRequest.top_p !== undefined) { + claudeRequest.top_p = responsesRequest.top_p; + } + + return claudeRequest; + } + + /** + * 将 OpenAI Responses 响应转换为 Claude 响应 + */ + toClaudeResponse(responsesResponse, model) { + const content = responsesResponse.choices?.[0]?.message?.content || + responsesResponse.content || ''; + + return { + id: responsesResponse.id || `msg_${Date.now()}`, + type: 'message', + role: 'assistant', + content: [{ + type: 'text', + text: content + }], + 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 + } + }; + } + + /** + * 将 OpenAI Responses 流式块转换为 Claude 流式块 + */ + toClaudeStreamChunk(responsesChunk, model) { + const delta = responsesChunk.choices?.[0]?.delta || responsesChunk.delta || {}; + const finishReason = responsesChunk.choices?.[0]?.finish_reason || + responsesChunk.finish_reason; + + if (finishReason) { + return { + type: 'message_stop' + }; + } + + if (delta.content) { + return { + type: 'content_block_delta', + index: 0, + delta: { + type: 'text_delta', + text: delta.content + } + }; + } + + return { + type: 'message_start', + message: { + id: responsesChunk.id || `msg_${Date.now()}`, + type: 'message', + role: 'assistant', + content: [], + model: model || responsesChunk.model + } + }; + } + + // ============================================================================= + // 转换到 Gemini 格式 + // ============================================================================= + + /** + * 将 OpenAI Responses 请求转换为 Gemini 请求 + */ + toGeminiRequest(responsesRequest) { + const geminiRequest = { + contents: [], + generationConfig: {} + }; + + // 处理 instructions 作为系统指令 + if (responsesRequest.instructions) { + geminiRequest.systemInstruction = { + parts: [{ + text: responsesRequest.instructions + }] + }; + } + + // 处理 input 数组中的消息 + if (responsesRequest.input && Array.isArray(responsesRequest.input)) { + responsesRequest.input.forEach(item => { + if (item.type === 'message') { + const content = item.content + .filter(c => c.type === 'input_text') + .map(c => c.text) + .join('\n'); + + if (content) { + geminiRequest.contents.push({ + role: item.role === 'assistant' ? 'model' : 'user', + parts: [{ + text: content + }] + }); + } + } + }); + } + + // 如果有标准的 messages 字段,也支持 + if (responsesRequest.messages && Array.isArray(responsesRequest.messages)) { + const { systemMessages, otherMessages } = extractSystemMessages( + responsesRequest.messages + ); + + if (!geminiRequest.systemInstruction && systemMessages.length > 0) { + const systemTexts = systemMessages.map(msg => extractText(msg.content)); + geminiRequest.systemInstruction = { + parts: [{ + text: systemTexts.join('\n') + }] + }; + } + + otherMessages.forEach(msg => { + geminiRequest.contents.push({ + role: msg.role === 'assistant' ? 'model' : 'user', + parts: [{ + text: typeof msg.content === 'string' ? msg.content : extractText(msg.content) + }] + }); + }); + } + + // 设置生成配置 + if (responsesRequest.temperature !== undefined) { + geminiRequest.generationConfig.temperature = responsesRequest.temperature; + } + if (responsesRequest.max_tokens !== undefined) { + geminiRequest.generationConfig.maxOutputTokens = responsesRequest.max_tokens; + } + if (responsesRequest.top_p !== undefined) { + geminiRequest.generationConfig.topP = responsesRequest.top_p; + } + + return geminiRequest; + } + + /** + * 将 OpenAI Responses 响应转换为 Gemini 响应 + */ + toGeminiResponse(responsesResponse, model) { + const content = responsesResponse.choices?.[0]?.message?.content || + responsesResponse.content || ''; + + return { + candidates: [{ + content: { + parts: [{ + text: content + }], + role: 'model' + }, + finishReason: this.mapFinishReason( + responsesResponse.choices?.[0]?.finish_reason || 'STOP' + ), + index: 0 + }], + usageMetadata: { + promptTokenCount: responsesResponse.usage?.prompt_tokens || 0, + candidatesTokenCount: responsesResponse.usage?.completion_tokens || 0, + totalTokenCount: responsesResponse.usage?.total_tokens || 0 + } + }; + } + + /** + * 将 OpenAI Responses 流式块转换为 Gemini 流式块 + */ + toGeminiStreamChunk(responsesChunk, model) { + const delta = responsesChunk.choices?.[0]?.delta || responsesChunk.delta || {}; + const finishReason = responsesChunk.choices?.[0]?.finish_reason || + responsesChunk.finish_reason; + + return { + candidates: [{ + content: { + parts: delta.content ? [{ + text: delta.content + }] : [], + role: 'model' + }, + finishReason: finishReason ? this.mapFinishReason(finishReason) : null, + index: 0 + }] + }; + } + + // ============================================================================= + // 辅助方法 + // ============================================================================= + + /** + * 映射完成原因 + */ + mapFinishReason(reason) { + const reasonMap = { + 'stop': 'STOP', + 'length': 'MAX_TOKENS', + 'content_filter': 'SAFETY', + 'end_turn': 'STOP' + }; + return reasonMap[reason] || 'STOP'; + } + + /** + * 将 OpenAI Responses 模型列表转换为标准 OpenAI 模型列表 + */ + toOpenAIModelList(responsesModels) { + // OpenAI Responses 格式的模型列表已经是标准 OpenAI 格式 + // 如果输入已经是标准格式,直接返回 + if (responsesModels.object === 'list' && responsesModels.data) { + return responsesModels; + } + + // 如果是其他格式,转换为标准格式 + return { + object: "list", + data: (responsesModels.models || responsesModels.data || []).map(m => ({ + id: m.id || m.name, + object: "model", + created: m.created || Math.floor(Date.now() / 1000), + owned_by: m.owned_by || "openai", + })), + }; + } + + /** + * 将 OpenAI Responses 模型列表转换为 Claude 模型列表 + */ + toClaudeModelList(responsesModels) { + const models = responsesModels.data || responsesModels.models || []; + return { + models: models.map(m => ({ + name: m.id || m.name, + description: m.description || "", + })), + }; + } + + /** + * 将 OpenAI Responses 模型列表转换为 Gemini 模型列表 + */ + toGeminiModelList(responsesModels) { + const models = responsesModels.data || responsesModels.models || []; + return { + models: models.map(m => ({ + name: `models/${m.id || m.name}`, + version: m.version || "1.0.0", + displayName: m.displayName || m.id || m.name, + description: m.description || `A generative model for text and chat generation. ID: ${m.id || m.name}`, + inputTokenLimit: m.inputTokenLimit || 32768, + outputTokenLimit: m.outputTokenLimit || 8192, + supportedGenerationMethods: m.supportedGenerationMethods || ["generateContent", "streamGenerateContent"] + })) + }; + } + +} + diff --git a/src/converters/utils.js b/src/converters/utils.js new file mode 100644 index 0000000..4a621f9 --- /dev/null +++ b/src/converters/utils.js @@ -0,0 +1,296 @@ +/** + * 转换器公共工具函数模块 + * 提供各种协议转换所需的通用辅助函数 + */ + +import { v4 as uuidv4 } from 'uuid'; + +// ============================================================================= +// 常量定义 +// ============================================================================= + +export const DEFAULT_MAX_TOKENS = 8192; +export const DEFAULT_GEMINI_MAX_TOKENS = 65535; +export const DEFAULT_TEMPERATURE = 1; +export const DEFAULT_TOP_P = 0.95; + +// ============================================================================= +// 通用辅助函数 +// ============================================================================= + +/** + * 判断值是否为 undefined 或 0,并返回默认值 + * @param {*} value - 要检查的值 + * @param {*} defaultValue - 默认值 + * @returns {*} 处理后的值 + */ +export function checkAndAssignOrDefault(value, defaultValue) { + if (value !== undefined && value !== 0) { + return value; + } + return defaultValue; +} + +/** + * 生成唯一ID + * @param {string} prefix - ID前缀 + * @returns {string} 生成的ID + */ +export function generateId(prefix = '') { + return prefix ? `${prefix}_${uuidv4()}` : uuidv4(); +} + +/** + * 安全解析JSON字符串 + * @param {string} str - JSON字符串 + * @returns {*} 解析后的对象或原始字符串 + */ +export function safeParseJSON(str) { + if (!str) { + return str; + } + let cleanedStr = str; + + // 处理可能被截断的转义序列 + if (cleanedStr.endsWith('\\') && !cleanedStr.endsWith('\\\\')) { + cleanedStr = cleanedStr.substring(0, cleanedStr.length - 1); + } else if (cleanedStr.endsWith('\\u') || cleanedStr.endsWith('\\u0') || cleanedStr.endsWith('\\u00')) { + const idx = cleanedStr.lastIndexOf('\\u'); + cleanedStr = cleanedStr.substring(0, idx); + } + + try { + return JSON.parse(cleanedStr || '{}'); + } catch (e) { + return str; + } +} + +/** + * 提取消息内容中的文本 + * @param {string|Array} content - 消息内容 + * @returns {string} 提取的文本 + */ +export function extractTextFromMessageContent(content) { + if (typeof content === 'string') { + return content; + } + if (Array.isArray(content)) { + return content + .filter(part => part.type === 'text' && part.text) + .map(part => part.text) + .join('\n'); + } + return ''; +} + +/** + * 提取并处理系统消息 + * @param {Array} messages - 消息数组 + * @returns {{systemInstruction: Object|null, nonSystemMessages: Array}} + */ +export function extractAndProcessSystemMessages(messages) { + const systemContents = []; + const nonSystemMessages = []; + + for (const message of messages) { + if (message.role === 'system') { + systemContents.push(extractTextFromMessageContent(message.content)); + } else { + nonSystemMessages.push(message); + } + } + + let systemInstruction = null; + if (systemContents.length > 0) { + systemInstruction = { + parts: [{ + text: systemContents.join('\n') + }] + }; + } + return { systemInstruction, nonSystemMessages }; +} + +/** + * 清理JSON Schema属性(移除Gemini不支持的属性) + * @param {Object} schema - JSON Schema + * @returns {Object} 清理后的JSON Schema + */ +export function cleanJsonSchemaProperties(schema) { + if (!schema || typeof schema !== 'object') { + return schema; + } + + const sanitized = {}; + for (const [key, value] of Object.entries(schema)) { + if (["type", "description", "properties", "required", "enum", "items"].includes(key)) { + sanitized[key] = value; + } + } + + if (sanitized.properties && typeof sanitized.properties === 'object') { + const cleanProperties = {}; + for (const [propName, propSchema] of Object.entries(sanitized.properties)) { + cleanProperties[propName] = cleanJsonSchemaProperties(propSchema); + } + sanitized.properties = cleanProperties; + } + + if (sanitized.items) { + sanitized.items = cleanJsonSchemaProperties(sanitized.items); + } + + return sanitized; +} + +/** + * 映射结束原因 + * @param {string} reason - 结束原因 + * @param {string} sourceFormat - 源格式 + * @param {string} targetFormat - 目标格式 + * @returns {string} 映射后的结束原因 + */ +export function mapFinishReason(reason, sourceFormat, targetFormat) { + const reasonMappings = { + openai: { + anthropic: { + stop: "end_turn", + length: "max_tokens", + content_filter: "stop_sequence", + tool_calls: "tool_use" + } + }, + gemini: { + anthropic: { + STOP: "end_turn", + MAX_TOKENS: "max_tokens", + SAFETY: "stop_sequence", + RECITATION: "stop_sequence", + stop: "end_turn", + length: "max_tokens", + safety: "stop_sequence", + recitation: "stop_sequence", + other: "end_turn" + } + } + }; + + try { + return reasonMappings[sourceFormat][targetFormat][reason] || "end_turn"; + } catch (e) { + return "end_turn"; + } +} + +/** + * 根据budget_tokens智能判断OpenAI reasoning_effort等级 + * @param {number|null} budgetTokens - Anthropic thinking的budget_tokens值 + * @returns {string} OpenAI reasoning_effort等级 + */ +export function determineReasoningEffortFromBudget(budgetTokens) { + if (budgetTokens === null || budgetTokens === undefined) { + console.info("No budget_tokens provided, defaulting to reasoning_effort='high'"); + return "high"; + } + + const LOW_THRESHOLD = 50; + const HIGH_THRESHOLD = 200; + + console.debug(`Threshold configuration: low <= ${LOW_THRESHOLD}, medium <= ${HIGH_THRESHOLD}, high > ${HIGH_THRESHOLD}`); + + let effort; + if (budgetTokens <= LOW_THRESHOLD) { + effort = "low"; + } else if (budgetTokens <= HIGH_THRESHOLD) { + effort = "medium"; + } else { + effort = "high"; + } + + console.info(`🎯 Budget tokens ${budgetTokens} -> reasoning_effort '${effort}' (thresholds: low<=${LOW_THRESHOLD}, high<=${HIGH_THRESHOLD})`); + return effort; +} + +/** + * 从OpenAI文本中提取thinking内容 + * @param {string} text - 文本内容 + * @returns {string|Array} 提取后的内容 + */ +export function extractThinkingFromOpenAIText(text) { + const thinkingPattern = /\s*(.*?)\s*<\/thinking>/gs; + const matches = [...text.matchAll(thinkingPattern)]; + + const contentBlocks = []; + let lastEnd = 0; + + for (const match of matches) { + const beforeText = text.substring(lastEnd, match.index).trim(); + if (beforeText) { + contentBlocks.push({ + type: "text", + text: beforeText + }); + } + + const thinkingText = match[1].trim(); + if (thinkingText) { + contentBlocks.push({ + type: "thinking", + thinking: thinkingText + }); + } + + lastEnd = match.index + match[0].length; + } + + const afterText = text.substring(lastEnd).trim(); + if (afterText) { + contentBlocks.push({ + type: "text", + text: afterText + }); + } + + if (contentBlocks.length === 0) { + return text; + } + + if (contentBlocks.length === 1 && contentBlocks[0].type === "text") { + return contentBlocks[0].text; + } + + return contentBlocks; +} + +// ============================================================================= +// 工具状态管理器(单例模式) +// ============================================================================= + +/** + * 全局工具状态管理器 + */ +class ToolStateManager { + constructor() { + if (ToolStateManager.instance) { + return ToolStateManager.instance; + } + ToolStateManager.instance = this; + this._toolMappings = {}; + return this; + } + + storeToolMapping(funcName, toolId) { + this._toolMappings[funcName] = toolId; + } + + getToolId(funcName) { + return this._toolMappings[funcName] || null; + } + + clearMappings() { + this._toolMappings = {}; + } +} + +export const toolStateManager = new ToolStateManager(); \ No newline at end of file diff --git a/src/example/claude/oldResponse1762501965597.json b/src/example/claude/oldResponse1762501965597.json new file mode 100644 index 0000000..286bf9a --- /dev/null +++ b/src/example/claude/oldResponse1762501965597.json @@ -0,0 +1 @@ +{"id":"msg_chatcmpl-690da54ca66df0e03c78831b","type":"message","role":"assistant","content":[{"type":"text","text":"I"}],"model":"claude-haiku-4-5-20251001","stop_reason":"max_tokens","stop_sequence":null,"usage":{"input_tokens":10855,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"output_tokens":1,"prompt_tokens":10855,"cached_tokens":10855,"completion_tokens":1,"total_tokens":10856}} \ No newline at end of file diff --git a/src/example/claude/oldResponseChunk1762501803864.json b/src/example/claude/oldResponseChunk1762501803864.json new file mode 100644 index 0000000..0b6b8e1 --- /dev/null +++ b/src/example/claude/oldResponseChunk1762501803864.json @@ -0,0 +1,19 @@ +{"type":"message_start","message":{"id":"msg_chatcmpl-690da4a34b300f384a5563b3","type":"message","role":"assistant","content":[],"model":"claude-sonnet-4-5-20250929","stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":22969,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"output_tokens":0,"service_tier":"standard","prompt_tokens":22969,"cached_tokens":0}}} +{"type":"content_block_start","index":0,"content_block":{"type":"tool_use","id":"TodoWrite_3","name":"TodoWrite","input":{}}} +{"type":"content_block_delta","index":0,"delta":{"type":"input_json_delta","partial_json":"{\""}} +{"type":"content_block_delta","index":0,"delta":{"type":"input_json_delta","partial_json":"t"}} +{"type":"content_block_delta","index":0,"delta":{"type":"input_json_delta","partial_json":"odos"}} +{"type":"content_block_delta","index":0,"delta":{"type":"input_json_delta","partial_json":"\":[{\""}} +{"type":"content_block_delta","index":0,"delta":{"type":"input_json_delta","partial_json":"content"}} +{"type":"content_block_delta","index":0,"delta":{"type":"input_json_delta","partial_json":"\":\""}} +{"type":"content_block_delta","index":0,"delta":{"type":"input_json_delta","partial_json":"读取"}} +{"type":"content_block_delta","index":0,"delta":{"type":"input_json_delta","partial_json":"当前的"}} +{"type":"content_block_delta","index":0,"delta":{"type":"input_json_delta","partial_json":"README"}} +{"type":"content_block_delta","index":0,"delta":{"type":"input_json_delta","partial_json":"文件"}} +{"type":"content_block_delta","index":0,"delta":{"type":"input_json_delta","partial_json":"\",\""}} +{"type":"content_block_delta","index":0,"delta":{"type":"input_json_delta","partial_json":"status"}} +{"type":"content_block_delta","index":0,"delta":{"type":"input_json_delta","partial_json":"\"}"}} +{"type":"content_block_delta","index":0,"delta":{"type":"input_json_delta","partial_json":"]}"}} +{"type":"content_block_stop","index":0} +{"type":"message_delta","delta":{"stop_reason":"tool_use","stop_sequence":null},"usage":{"input_tokens":22969,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"output_tokens":80,"prompt_tokens":22969,"completion_tokens":80,"total_tokens":23049,"cached_tokens":15872}} +{"type":"message_stop"} \ No newline at end of file diff --git a/src/example/claude/originalRequestBody1762501795790.json b/src/example/claude/originalRequestBody1762501795790.json new file mode 100644 index 0000000..d04c024 --- /dev/null +++ b/src/example/claude/originalRequestBody1762501795790.json @@ -0,0 +1 @@ +{"model":"claude-sonnet-4-5-20250929","messages":[{"role":"user","content":[{"type":"text","text":"The user opened the file d:\\ai\\code\\AIClient2API\\src\\example\\openai\\oldResponse1762501667708.json in the IDE. This may or may not be related to the current task."},{"type":"text","text":"更新readme,写入一行随机数"}]},{"role":"assistant","content":[{"type":"text","text":"我将帮您更新README文件,添加一行随机数。"},{"type":"tool_use","id":"TodoWrite_0","name":"TodoWrite","input":{"todos":[{"content":"读取当前的README文件","status":"pending","activeForm":"读取当前的README文件"},{"content":"在README文件中添加一行随机数","status":"pending","activeForm":"在README文件中添加一行随机数"},{"content":"保存更新后的README文件","status":"pending","activeForm":"保存更新后的README文件"}]}}]},{"role":"user","content":[{"tool_use_id":"TodoWrite_0","type":"tool_result","content":"Todos have been modified successfully. Ensure that you continue to use the todo list to track your progress. Please proceed with the current tasks if applicable"}]},{"role":"assistant","content":[{"type":"tool_use","id":"TodoWrite_1","name":"TodoWrite","input":{"todos":[{"content":"读取当前的README文件","status":"in_progress","activeForm":"正在读取README文件"},{"content":"在README文件中添加一行随机数","status":"pending","activeForm":"在README文件中添加一行随机数"},{"content":"保存更新后的README文件","status":"pending","activeForm":"保存更新后的README文件"}]}}]},{"role":"user","content":[{"tool_use_id":"TodoWrite_1","type":"tool_result","content":"Todos have been modified successfully. Ensure that you continue to use the todo list to track your progress. Please proceed with the current tasks if applicable"}]},{"role":"assistant","content":[{"type":"tool_use","id":"Read_2","name":"Read","input":{"file_path":"D:\\ai\\code\\AIClient2API\\README.md"},"cache_control":{"type":"ephemeral"}}]},{"role":"user","content":[{"tool_use_id":"Read_2","type":"tool_result","content":" 1→
\n 2→\n 3→![logo](src/img/logo-min.webp)\n 4→\n 5→# AIClient-2-API 🚀\n 6→\n 7→**A powerful proxy that can unify the requests of various large model APIs (Gemini CLI, Qwen Code Plus, Kiro Claude...) that are only used within the client into a local OpenAI compatible interface.**\n 8→\n 9→
\n 10→\n 11→
\n 12→\n 13→\"Ask\n 14→\n 15→[![License: GPL v3](https://img.shields.io/badge/License-GPLv3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0)\n 16→[![Node.js](https://img.shields.io/badge/Node.js-≥20.0.0-green.svg)](https://nodejs.org/)\n 17→[![Docker](https://img.shields.io/badge/docker-≥20.0.0-blue.svg)](https://aiproxy.justlikemaki.vip/en/docs/installation/docker-deployment.html)\n 18→\n 19→\n 20→[**中文**](./README-ZH.md) | [**English**](./README.md) | [**日本語**](./README-JA.md) | [**📚 Complete Documentation**](https://aiproxy.justlikemaki.vip/en/)\n 21→\n 22→
\n 23→\n 24→`AIClient2API` is an API proxy service that breaks through client limitations, converting free large models originally restricted to client use only (such as Gemini CLI, Qwen Code Plus, Kiro Claude) into standard OpenAI-compatible interfaces that can be called by any application. Built on Node.js, it supports intelligent conversion between three major protocols (OpenAI, Claude, Gemini), enabling tools like Cherry-Studio, NextChat, and Cline to freely use advanced models such as Claude Sonnet 4.5, Gemini 2.5 Flash, and Qwen3 Coder Plus at scale. The project adopts a modular architecture based on strategy and adapter patterns, with built-in account pool management, intelligent polling, automatic failover, and health check mechanisms, ensuring 99.9% service availability.\n 25→\n 26→> [!NOTE]\n 27→> **🎉 Important Milestone**\n 28→>\n 29→> - Thanks to Ruan Yifeng for the recommendation in [Weekly Issue 359](https://www.ruanyifeng.com/blog/2025/08/weekly-issue-359.html)\n 30→>\n 31→> **📅 Version Update Log**\n 32→>\n 33→> - **2025.11.06** - Added support for Gemini 3 Preview, enhanced model compatibility and performance optimization\n 34→> - **2025.10.18** - Kiro open registration, new accounts get 500 credits, full support for Claude Sonnet 4.5\n 35→> - **2025.09.01** - Integrated Qwen Code CLI, added `qwen3-coder-plus` model support\n 36→> - **2025.08.29** - Released account pool management feature, supporting multi-account polling, intelligent failover, and automatic degradation strategies\n 37→> - Configuration: Add `PROVIDER_POOLS_FILE_PATH` parameter in config.json\n 38→> - Reference configuration: [provider_pools.json](./provider_pools.json.example)\n 39→\n 40→---\n 41→\n 42→## 💡 Core Advantages\n 43→\n 44→### 🎯 Unified Access, One-Stop Management\n 45→* **Multi-Model Unified Interface**: Through standard OpenAI-compatible protocol, configure once to access mainstream large models including Gemini, Claude, GPT, Qwen Code, Kimi K2, GLM-4.6\n 46→* **Flexible Switching Mechanism**: Support dynamic model switching via startup parameters, Path routing, or environment variables to meet different scenario requirements\n 47→* **Zero-Cost Migration**: Fully compatible with OpenAI API specifications, tools like Cherry-Studio, NextChat, Cline can be used without modification\n 48→* **Multi-Protocol Intelligent Conversion**: Support intelligent conversion between OpenAI, Claude, and Gemini protocols for cross-protocol model invocation\n 49→ * Call Claude models using OpenAI protocol: Use `claude-custom` or `claude-kiro-oauth` providers\n 50→ * Call Gemini models using OpenAI protocol: Use `gemini-cli-oauth` provider\n 51→ * Call Gemini models using Claude protocol: Use `gemini-cli-oauth` provider\n 52→ * Call OpenAI models using Claude protocol: Use `openai-custom` or `openai-qwen-oauth` providers\n 53→\n 54→### 🚀 Break Through Limitations, Improve Efficiency\n 55→* **Bypass Official Restrictions**: Utilize OAuth authorization mechanism to effectively break through rate and quota limits of free APIs like Gemini\n 56→* **Free Advanced Models**: Use Claude Sonnet 4.5 for free via Kiro API mode, use Qwen3 Coder Plus via Qwen OAuth mode, reducing usage costs\n 57→* **Intelligent Account Pool Scheduling**: Support multi-account polling, automatic failover, and configuration degradation, ensuring 99.9% service availability\n 58→\n 59→### 🛡️ Secure and Controllable, Data Transparent\n 60→* **Full-Chain Log Recording**: Capture all request and response data, supporting auditing and debugging\n 61→* **Private Dataset Construction**: Quickly build proprietary training datasets based on log data\n 62→* **System Prompt Management**: Support override and append modes, achieving perfect combination of unified base instructions and personalized extensions\n 63→\n 64→### 🔧 Developer-Friendly, Easy to Extend\n 65→* **Modular Architecture**: Based on strategy and adapter patterns, adding new model providers requires only 3 steps\n 66→* **Complete Test Coverage**: Integration and unit test coverage 90%+, ensuring code quality\n 67→* **Containerized Deployment**: Provides Docker support, one-click deployment, cross-platform operation\n 68→* **MCP Protocol Support**: Perfectly compatible with Model Context Protocol, easily extend functionality\n 69→\n 70→---\n 71→\n 72→## 📑 Quick Navigation\n 73→\n 74→- [🐳 Docker Deployment](https://aiproxy.justlikemaki.vip/en/docs/installation/docker-deployment.html)\n 75→- [🎨 Model Protocol and Provider Relationship Diagram](#-model-protocol-and-provider-relationship-diagram)\n 76→- [🔧 Usage Instructions](#-usage-instructions)\n 77→- [🚀 Project Startup Parameters](#-project-startup-parameters)\n 78→- [📄 Open Source License](#-open-source-license)\n 79→- [🙏 Acknowledgements](#-acknowledgements)\n 80→- [⚠️ Disclaimer](#-disclaimer)\n 81→\n 82→---\n 83→\n 84→## 🎨 Model Protocol and Provider Relationship Diagram\n 85→\n 86→This project supports multiple model providers through different protocols. The following is an overview of their relationships:\n 87→\n 88→* **OpenAI Protocol (P_OPENAI)**: Implemented by `openai-custom`, `gemini-cli-oauth`, `claude-custom`, `claude-kiro-oauth`, and `openai-qwen-oauth` model providers.\n 89→* **Claude Protocol (P_CLAUDE)**: Implemented by `claude-custom`, `claude-kiro-oauth`, `gemini-cli-oauth`, `openai-custom`, and `openai-qwen-oauth` model providers.\n 90→* **Gemini Protocol (P_GEMINI)**: Implemented by `gemini-cli-oauth` model provider.\n 91→\n 92→Detailed relationship diagram:\n 93→\n 94→ ```mermaid\n 95→ \n 96→ graph TD\n 97→ subgraph Core_Protocols[\"Core Protocols\"]\n 98→ P_OPENAI[OpenAI Protocol]\n 99→ P_GEMINI[Gemini Protocol]\n 100→ P_CLAUDE[Claude Protocol]\n 101→ end\n 102→ \n 103→ subgraph Supported_Model_Providers[\"Supported Model Providers\"]\n 104→ MP_OPENAI[openai-custom]\n 105→ MP_GEMINI[gemini-cli-oauth]\n 106→ MP_CLAUDE_C[claude-custom]\n 107→ MP_CLAUDE_K[claude-kiro-oauth]\n 108→ MP_QWEN[openai-qwen-oauth]\n 109→ end\n 110→ \n 111→ P_OPENAI ---|Support| MP_OPENAI\n 112→ P_OPENAI ---|Support| MP_QWEN\n 113→ P_OPENAI ---|Support| MP_GEMINI\n 114→ P_OPENAI ---|Support| MP_CLAUDE_C\n 115→ P_OPENAI ---|Support| MP_CLAUDE_K\n 116→ \n 117→ P_GEMINI ---|Support| MP_GEMINI\n 118→ \n 119→ P_CLAUDE ---|Support| MP_CLAUDE_C\n 120→ P_CLAUDE ---|Support| MP_CLAUDE_K\n 121→ P_CLAUDE ---|Support| MP_GEMINI\n 122→ P_CLAUDE ---|Support| MP_OPENAI\n 123→ P_CLAUDE ---|Support| MP_QWEN\n 124→ \n 125→ style P_OPENAI fill:#f9f,stroke:#333,stroke-width:2px\n 126→ style P_GEMINI fill:#ccf,stroke:#333,stroke-width:2px\n 127→ style P_CLAUDE fill:#cfc,stroke:#333,stroke-width:2px\n 128→\n 129→ ```\n 130→\n 131→---\n 132→\n 133→## 🔧 Usage Instructions\n 134→\n 135→### 📋 Core Features\n 136→\n 137→#### MCP Protocol Support\n 138→This project is fully compatible with **Model Context Protocol (MCP)**, enabling seamless integration with MCP-supporting clients for powerful functional extensions.\n 139→\n 140→#### Multimodal Input Capabilities\n 141→Supports various input types including images and documents, providing richer interactive experiences and more powerful application scenarios.\n 142→\n 143→#### Latest Model Support\n 144→Seamlessly supports the following latest large models, simply configure the corresponding OpenAI or Claude compatible interface in [`config.json`](./config.json):\n 145→* **Kimi K2** - Moonshot AI's latest flagship model\n 146→* **GLM-4.5** - Zhipu AI's latest version\n 147→* **Qwen Code** - Alibaba Tongyi Qianwen code-specific model\n 148→\n 149→---\n 150→\n 151→### 🔐 Authorization Configuration Guide\n 152→\n 153→#### Gemini CLI OAuth Configuration\n 154→1. **Obtain OAuth Credentials**: Visit [Google Cloud Console](https://console.cloud.google.com/) to create a project and enable Gemini API\n 155→2. **First Authorization**: After using Gemini service, the command line will print Google authorization page, copy the page to browser for authorization, then return to command line\n 156→3. **Credential Storage**: After successful authorization, `oauth_creds.json` file will be automatically generated and saved to `~/.gemini` directory\n 157→4. **Project Configuration**: Need to provide a valid Google Cloud project ID, can be specified via startup parameter `--project-id`\n 158→\n 159→#### Qwen Code OAuth Configuration\n 160→1. **First Authorization**: After starting the service, the system will automatically open the authorization page in the browser\n 161→2. **Credential Storage**: After successful authorization, `oauth_creds.json` file will be automatically generated and saved to `~/.qwen` directory\n 162→3. **Recommended Parameters**: Use official default parameters for best results\n 163→ ```json\n 164→ {\n 165→ \"temperature\": 0,\n 166→ \"top_p\": 1\n 167→ }\n 168→ ```\n 169→\n 170→#### Kiro API Configuration\n 171→1. **Environment Preparation**: [Download and install Kiro client](https://aibook.ren/archives/kiro-install)\n 172→2. **Complete Authorization**: Log in to your account in the client to generate `kiro-auth-token.json` credential file\n 173→3. **Best Practice**: Recommended to use with **Claude Code** for optimal experience\n 174→4. **Important Notice**: Kiro service usage policy has been updated, please visit the official website for the latest usage restrictions and terms\n 175→\n 176→#### OpenAI Responses API\n 177→* **Application Scenario**: Suitable for scenarios requiring structured dialogue using OpenAI Responses API, such as Codex\n 178→* **Configuration Method**:\n 179→ * Method 1: Set `MODEL_PROVIDER` to `openaiResponses-custom` in [`config.json`](./config.json)\n 180→ * Method 2: Use startup parameter `--model-provider openaiResponses-custom`\n 181→ * Method 3: Use path routing `/openaiResponses-custom`\n 182→* **Required Parameters**: Provide valid API key and base URL\n 183→\n 184→---\n 185→\n 186→### 🔄 Model Provider Switching\n 187→\n 188→This project provides two flexible model switching methods to meet different usage scenario requirements.\n 189→\n 190→#### Method 1: Startup Parameter Switching\n 191→\n 192→Specify the default model provider via command line parameters:\n 193→\n 194→```bash\n 195→# Use Gemini provider\n 196→node src/api-server.js --model-provider gemini-cli-oauth --project-id your-project-id\n 197→\n 198→# Use Claude Kiro provider\n 199→node src/api-server.js --model-provider claude-kiro-oauth\n 200→\n 201→# Use Qwen provider\n 202→node src/api-server.js --model-provider openai-qwen-oauth\n 203→```\n 204→\n 205→**Available Model Provider Identifiers**:\n 206→- `openai-custom` - Standard OpenAI API\n 207→- `claude-custom` - Official Claude API\n 208→- `gemini-cli-oauth` - Gemini CLI OAuth\n 209→- `claude-kiro-oauth` - Kiro Claude OAuth\n 210→- `openai-qwen-oauth` - Qwen Code OAuth\n 211→- `openaiResponses-custom` - OpenAI Responses API\n 212→\n 213→#### Method 2: Path Routing Switching (Recommended)\n 214→\n 215→Achieve instant switching by specifying provider identifier in API request path:\n 216→\n 217→| Route Path | Description | Use Case |\n 218→|---------|------|---------|\n 219→| `/claude-custom` | Use Claude API from config file | Official Claude API calls |\n 220→| `/claude-kiro-oauth` | Access Claude via Kiro OAuth | Free use of Claude Sonnet 4.5 |\n 221→| `/openai-custom` | Use OpenAI provider to handle requests | Standard OpenAI API calls |\n 222→| `/gemini-cli-oauth` | Access via Gemini CLI OAuth | Break through Gemini free limits |\n 223→| `/openai-qwen-oauth` | Access via Qwen OAuth | Use Qwen Code Plus |\n 224→| `/openaiResponses-custom` | OpenAI Responses API | Structured dialogue scenarios |\n 225→\n 226→**Usage Examples**:\n 227→```bash\n 228→# Configure in programming agents like Cline, Kilo\n 229→API_ENDPOINT=http://localhost:3000/claude-kiro-oauth\n 230→\n 231→# Direct API call\n 232→curl http://localhost:3000/gemini-cli-oauth/v1/chat/completions \\\n 233→ -H \"Content-Type: application/json\" \\\n 234→ -d '{\"model\":\"gemini-2.0-flash-exp\",\"messages\":[...]}'\n 235→```\n 236→\n 237→---\n 238→\n 239→### 📁 Authorization File Storage Paths\n 240→\n 241→Default storage locations for authorization credential files of each service:\n 242→\n 243→| Service | Default Path | Description |\n 244→|------|---------|------|\n 245→| **Gemini** | `~/.gemini/oauth_creds.json` | OAuth authentication credentials |\n 246→| **Kiro** | `~/.aws/sso/cache/kiro-auth-token.json` | Kiro authentication token |\n 247→| **Qwen** | `~/.qwen/oauth_creds.json` | Qwen OAuth credentials |\n 248→\n 249→> **Note**: `~` represents the user home directory (Windows: `C:\\Users\\username`, Linux/macOS: `/home/username` or `/Users/username`)\n 250→>\n 251→> **Custom Path**: Can specify custom storage location via relevant parameters in configuration file or environment variables\n 252→\n 253→---\n 254→\n 255→## 🚀 Project Startup Parameters\n 256→\n 257→This project supports rich command-line parameter configuration, allowing flexible adjustment of service behavior as needed. The following is a detailed explanation of all startup parameters, displayed in functional groups:\n 258→\n 259→### 🔧 Server Configuration Parameters\n 260→\n 261→| Parameter | Type | Default Value | Description |\n 262→|------|------|--------|------|\n 263→| `--host` | string | localhost | Server listening address |\n 264→| `--port` | number | 3000 | Server listening port |\n 265→| `--api-key` | string | 123456 | API key for authentication |\n 266→\n 267→### 🤖 Model Provider Configuration Parameters\n 268→\n 269→| Parameter | Type | Default Value | Description |\n 270→|------|------|--------|------|\n 271→| `--model-provider` | string | gemini-cli-oauth | AI model provider, optional values: openai-custom, claude-custom, gemini-cli-oauth, claude-kiro-oauth, openai-qwen-oauth |\n 272→\n 273→### 🧠 OpenAI Compatible Provider Parameters\n 274→\n 275→| Parameter | Type | Default Value | Description |\n 276→|------|------|--------|------|\n 277→| `--openai-api-key` | string | null | OpenAI API key (required when `model-provider` is `openai-custom`) |\n 278→| `--openai-base-url` | string | null | OpenAI API base URL (required when `model-provider` is `openai-custom`) |\n 279→\n 280→### 🖥️ Claude Compatible Provider Parameters\n 281→\n 282→| Parameter | Type | Default Value | Description |\n 283→|------|------|--------|------|\n 284→| `--claude-api-key` | string | null | Claude API key (required when `model-provider` is `claude-custom`) |\n 285→| `--claude-base-url` | string | null | Claude API base URL (required when `model-provider` is `claude-custom`) |\n 286→\n 287→### 🔐 Gemini OAuth Authentication Parameters\n 288→\n 289→| Parameter | Type | Default Value | Description |\n 290→|------|------|--------|------|\n 291→| `--gemini-oauth-creds-base64` | string | null | Base64 string of Gemini OAuth credentials (optional when `model-provider` is `gemini-cli-oauth`, choose one with `--gemini-oauth-creds-file`) |\n 292→| `--gemini-oauth-creds-file` | string | null | Gemini OAuth credentials JSON file path (optional when `model-provider` is `gemini-cli-oauth`, choose one with `--gemini-oauth-creds-base64`) |\n 293→| `--project-id` | string | null | Google Cloud project ID (required when `model-provider` is `gemini-cli-oauth`) |\n 294→\n 295→### 🎮 Kiro OAuth Authentication Parameters\n 296→\n 297→| Parameter | Type | Default Value | Description |\n 298→|------|------|--------|------|\n 299→| `--kiro-oauth-creds-base64` | string | null | Base64 string of Kiro OAuth credentials (optional when `model-provider` is `claude-kiro-oauth`, choose one with `--kiro-oauth-creds-file`) |\n 300→| `--kiro-oauth-creds-file` | string | null | Kiro OAuth credentials JSON file path (optional when `model-provider` is `claude-kiro-oauth`, choose one with `--kiro-oauth-creds-base64`) |\n 301→\n 302→### 🐼 Qwen OAuth Authentication Parameters\n 303→\n 304→| Parameter | Type | Default Value | Description |\n 305→|------|------|--------|------|\n 306→| `--qwen-oauth-creds-file` | string | null | Qwen OAuth credentials JSON file path (required when `model-provider` is `openai-qwen-oauth`) |\n 307→\n 308→### 🔄 OpenAI Responses API Parameters\n 309→\n 310→| Parameter | Type | Default Value | Description |\n 311→|------|------|--------|------|\n 312→| `--model-provider` | string | openaiResponses-custom | Model provider, set to `openaiResponses-custom` when using OpenAI Responses API |\n 313→| `--openai-api-key` | string | null | OpenAI API key (required when `model-provider` is `openaiResponses-custom`) |\n 314→| `--openai-base-url` | string | null | OpenAI API base URL (required when `model-provider` is `openaiResponses-custom`) |\n 315→\n 316→### 📝 System Prompt Configuration Parameters\n 317→\n 318→| Parameter | Type | Default Value | Description |\n 319→|------|------|--------|------|\n 320→| `--system-prompt-file` | string | input_system_prompt.txt | System prompt file path |\n 321→| `--system-prompt-mode` | string | overwrite | System prompt mode, optional values: overwrite (override), append (append) |\n 322→\n 323→### 📊 Log Configuration Parameters\n 324→\n 325→| Parameter | Type | Default Value | Description |\n 326→|------|------|--------|------|\n 327→| `--log-prompts` | string | none | Prompt log mode, optional values: console (console), file (file), none (none) |\n 328→| `--prompt-log-base-name` | string | prompt_log | Prompt log file base name |\n 329→\n 330→### 🔄 Retry Mechanism Parameters\n 331→\n 332→| Parameter | Type | Default Value | Description |\n 333→|------|------|--------|------|\n 334→| `--request-max-retries` | number | 3 | Maximum number of automatic retries when API requests fail |\n 335→| `--request-base-delay` | number | 1000 | Base delay time (milliseconds) between automatic retries, delay increases after each retry |\n 336→\n 337→### ⏰ Scheduled Task Parameters\n 338→\n 339→| Parameter | Type | Default Value | Description |\n 340→|------|------|--------|------|\n 341→| `--cron-near-minutes` | number | 15 | Interval time (minutes) for OAuth token refresh task schedule |\n 342→| `--cron-refresh-token` | boolean | true | Whether to enable automatic OAuth token refresh task |\n 343→\n 344→### 🎯 Account Pool Configuration Parameters\n 345→\n 346→| Parameter | Type | Default Value | Description |\n 347→|------|------|--------|------|\n 348→| `--provider-pools-file` | string | null | Provider account pool configuration file path |\n 349→\n 350→### Usage Examples\n 351→\n 352→```bash\n 353→# Basic usage\n 354→node src/api-server.js\n 355→\n 356→# Specify port and API key\n 357→node src/api-server.js --port 8080 --api-key my-secret-key\n 358→\n 359→# Use OpenAI provider\n 360→node src/api-server.js --model-provider openai-custom --openai-api-key sk-xxx --openai-base-url https://api.openai.com/v1\n 361→\n 362→# Use Claude provider\n 363→node src/api-server.js --model-provider claude-custom --claude-api-key sk-ant-xxx --claude-base-url https://api.anthropic.com\n 364→\n 365→# Use OpenAI Responses API provider\n 366→node src/api-server.js --model-provider openaiResponses-custom --openai-api-key sk-xxx --openai-base-url https://api.openai.com/v1\n 367→\n 368→# Use Gemini provider (Base64 credentials)\n 369→node src/api-server.js --model-provider gemini-cli-oauth --gemini-oauth-creds-base64 eyJ0eXBlIjoi... --project-id your-project-id\n 370→\n 371→# Use Gemini provider (credentials file)\n 372→node src/api-server.js --model-provider gemini-cli-oauth --gemini-oauth-creds-file /path/to/credentials.json --project-id your-project-id\n 373→\n 374→# Configure system prompt\n 375→node src/api-server.js --system-prompt-file custom-prompt.txt --system-prompt-mode append\n 376→\n 377→# Configure logging\n 378→node src/api-server.js --log-prompts console\n 379→node src/api-server.js --log-prompts file --prompt-log-base-name my-logs\n 380→\n 381→# Complete example\n 382→node src/api-server.js \\\n 383→ --host 0.0.0.0 \\\n 384→ --port 3000 \\\n 385→ --api-key my-secret-key \\\n 386→ --model-provider gemini-cli-oauth \\\n 387→ --project-id my-gcp-project \\\n 388→ --gemini-oauth-creds-file ./credentials.json \\\n 389→ --system-prompt-file ./custom-system-prompt.txt \\\n 390→ --system-prompt-mode overwrite \\\n 391→ --log-prompts file \\\n 392→ --prompt-log-base-name api-logs\n 393→```\n 394→\n 395→---\n 396→\n 397→## 📄 Open Source License\n 398→\n 399→This project operates under the [**GNU General Public License v3 (GPLv3)**](https://www.gnu.org/licenses/gpl-3.0). For complete details, please refer to the `LICENSE` file located in the root directory.\n 400→\n 401→## 🙏 Acknowledgements\n 402→\n 403→The development of this project was significantly inspired by the official Google Gemini CLI and incorporated some code implementations from Cline 3.18.0's `gemini-cli.ts`. We extend our sincere gratitude to the official Google team and the Cline development team for their exceptional work!\n 404→\n 405→## 🌟 Star History\n 406→\n 407→[![Star History Chart](https://api.star-history.com/svg?repos=justlovemaki/AIClient-2-API&type=Timeline)](https://www.star-history.com/#justlovemaki/AIClient-2-API&Timeline)\n 408→\n 409→---\n 410→\n 411→## ⚠️ Disclaimer\n 412→\n 413→### Usage Risk Warning\n 414→This project (AIClient-2-API) is for learning and research purposes only. Users assume all risks when using this project. The author is not responsible for any direct, indirect, or consequential losses resulting from the use of this project.\n 415→\n 416→### Third-Party Service Responsibility Statement\n 417→This project is an API proxy tool and does not provide any AI model services. All AI model services are provided by their respective third-party providers (such as Google, OpenAI, Anthropic, etc.). Users should comply with the terms of service and policies of each third-party service when accessing them through this project. The author is not responsible for the availability, quality, security, or legality of third-party services.\n 418→\n 419→### Data Privacy Statement\n 420→This project runs locally and does not collect or upload any user data. However, users should protect their API keys and other sensitive information when using this project. It is recommended that users regularly check and update their API keys and avoid using this project in insecure network environments.\n 421→\n 422→### Legal Compliance Reminder\n 423→Users should comply with the laws and regulations of their country/region when using this project. It is strictly prohibited to use this project for any illegal purposes. Any consequences resulting from users' violation of laws and regulations shall be borne by the users themselves.\n 424→\n\n\nWhenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.\n\n","cache_control":{"type":"ephemeral"}}]}],"system":[{"type":"text","text":"You are Claude Code, Anthropic's official CLI for Claude, running within the Claude Agent SDK.","cache_control":{"type":"ephemeral"}},{"type":"text","text":"\nYou are an interactive CLI tool that helps users with software engineering tasks. Use the instructions below and the tools available to you to assist the user.\n\nIMPORTANT: Assist with authorized security testing, defensive security, CTF challenges, and educational contexts. Refuse requests for destructive techniques, DoS attacks, mass targeting, supply chain compromise, or detection evasion for malicious purposes. Dual-use security tools (C2 frameworks, credential testing, exploit development) require clear authorization context: pentesting engagements, CTF competitions, security research, or defensive use cases.\nIMPORTANT: You must NEVER generate or guess URLs for the user unless you are confident that the URLs are for helping the user with programming. You may use URLs provided by the user in their messages or local files.\n\nIf the user asks for help or wants to give feedback inform them of the following:\n- /help: Get help with using Claude Code\n- To give feedback, users should report the issue at https://github.com/anthropics/claude-code/issues\n\nWhen the user directly asks about Claude Code (eg. \"can Claude Code do...\", \"does Claude Code have...\"), or asks in second person (eg. \"are you able...\", \"can you do...\"), or asks how to use a specific Claude Code feature (eg. implement a hook, write a slash command, or install an MCP server), use the WebFetch tool to gather information to answer the question from Claude Code docs. The list of available docs is available at https://docs.claude.com/en/docs/claude-code/claude_code_docs_map.md.\n\n# Tone and style\n- Only use emojis if the user explicitly requests it. Avoid using emojis in all communication unless asked.\n- Your output will be displayed on a command line interface. Your responses should be short and concise. You can use Github-flavored markdown for formatting, and will be rendered in a monospace font using the CommonMark specification.\n- Output text to communicate with the user; all text you output outside of tool use is displayed to the user. Only use tools to complete tasks. Never use tools like Bash or code comments as means to communicate with the user during the session.\n- NEVER create files unless they're absolutely necessary for achieving your goal. ALWAYS prefer editing an existing file to creating a new one. This includes markdown files.\n\n# Professional objectivity\nPrioritize technical accuracy and truthfulness over validating the user's beliefs. Focus on facts and problem-solving, providing direct, objective technical info without any unnecessary superlatives, praise, or emotional validation. It is best for the user if Claude honestly applies the same rigorous standards to all ideas and disagrees when necessary, even if it may not be what the user wants to hear. Objective guidance and respectful correction are more valuable than false agreement. Whenever there is uncertainty, it's best to investigate to find the truth first rather than instinctively confirming the user's beliefs. Avoid using over-the-top validation or excessive praise when responding to users such as \"You're absolutely right\" or similar phrases.\n\n# Task Management\nYou have access to the TodoWrite tools to help you manage and plan tasks. Use these tools VERY frequently to ensure that you are tracking your tasks and giving the user visibility into your progress.\nThese tools are also EXTREMELY helpful for planning tasks, and for breaking down larger complex tasks into smaller steps. If you do not use this tool when planning, you may forget to do important tasks - and that is unacceptable.\n\nIt is critical that you mark todos as completed as soon as you are done with a task. Do not batch up multiple tasks before marking them as completed.\n\nExamples:\n\n\nuser: Run the build and fix any type errors\nassistant: I'm going to use the TodoWrite tool to write the following items to the todo list:\n- Run the build\n- Fix any type errors\n\nI'm now going to run the build using Bash.\n\nLooks like I found 10 type errors. I'm going to use the TodoWrite tool to write 10 items to the todo list.\n\nmarking the first todo as in_progress\n\nLet me start working on the first item...\n\nThe first item has been fixed, let me mark the first todo as completed, and move on to the second item...\n..\n..\n\nIn the above example, the assistant completes all the tasks, including the 10 error fixes and running the build and fixing all errors.\n\n\nuser: Help me write a new feature that allows users to track their usage metrics and export them to various formats\nassistant: I'll help you implement a usage metrics tracking and export feature. Let me first use the TodoWrite tool to plan this task.\nAdding the following todos to the todo list:\n1. Research existing metrics tracking in the codebase\n2. Design the metrics collection system\n3. Implement core metrics tracking functionality\n4. Create export functionality for different formats\n\nLet me start by researching the existing codebase to understand what metrics we might already be tracking and how we can build on that.\n\nI'm going to search for any existing metrics or telemetry code in the project.\n\nI've found some existing telemetry code. Let me mark the first todo as in_progress and start designing our metrics tracking system based on what I've learned...\n\n[Assistant continues implementing the feature step by step, marking todos as in_progress and completed as they go]\n\n\n\n\n\nUsers may configure 'hooks', shell commands that execute in response to events like tool calls, in settings. Treat feedback from hooks, including , as coming from the user. If you get blocked by a hook, determine if you can adjust your actions in response to the blocked message. If not, ask the user to check their hooks configuration.\n\n# Doing tasks\nThe user will primarily request you perform software engineering tasks. This includes solving bugs, adding new functionality, refactoring code, explaining code, and more. For these tasks the following steps are recommended:\n- \n- Use the TodoWrite tool to plan the task if required\n- \n- Be careful not to introduce security vulnerabilities such as command injection, XSS, SQL injection, and other OWASP top 10 vulnerabilities. If you notice that you wrote insecure code, immediately fix it.\n\n- Tool results and user messages may include tags. tags contain useful information and reminders. They are automatically added by the system, and bear no direct relation to the specific tool results or user messages in which they appear.\n\n\n# Tool usage policy\n- When doing file search, prefer to use the Task tool in order to reduce context usage.\n- You should proactively use the Task tool with specialized agents when the task at hand matches the agent's description.\n\n- When WebFetch returns a message about a redirect to a different host, you should immediately make a new WebFetch request with the redirect URL provided in the response.\n- You can call multiple tools in a single response. If you intend to call multiple tools and there are no dependencies between them, make all independent tool calls in parallel. Maximize use of parallel tool calls where possible to increase efficiency. However, if some tool calls depend on previous calls to inform dependent values, do NOT call these tools in parallel and instead call them sequentially. For instance, if one operation must complete before another starts, run these operations sequentially instead. Never use placeholders or guess missing parameters in tool calls.\n- If the user specifies that they want you to run tools \"in parallel\", you MUST send a single message with multiple tool use content blocks. For example, if you need to launch multiple agents in parallel, send a single message with multiple Task tool calls.\n- Use specialized tools instead of bash commands when possible, as this provides a better user experience. For file operations, use dedicated tools: Read for reading files instead of cat/head/tail, Edit for editing instead of sed/awk, and Write for creating files instead of cat with heredoc or echo redirection. Reserve bash tools exclusively for actual system commands and terminal operations that require shell execution. NEVER use bash echo or other command-line tools to communicate thoughts, explanations, or instructions to the user. Output all communication directly in your response text instead.\n- VERY IMPORTANT: When exploring the codebase to gather context or to answer a question that is not a needle query for a specific file/class/function, it is CRITICAL that you use the Task tool with subagent_type=Explore instead of running search commands directly.\n\nuser: Where are errors from the client handled?\nassistant: [Uses the Task tool with subagent_type=Explore to find the files that handle client errors instead of using Glob or Grep directly]\n\n\nuser: What is the codebase structure?\nassistant: [Uses the Task tool with subagent_type=Explore]\n\n\n\nYou can use the following tools without requiring user approval: Bash(node -e \"\n// 测试 toClaudeRequestFromOpenAIResponses\nconst { toClaudeRequestFromOpenAIResponses } = require(''./convert.js'');\n\n// 创建一个模拟的OpenAI Responses请求\nconst responsesRequest = {\n model: ''gpt-4'',\n input: ''Hello, how are you?'',\n instructions: ''You are a helpful assistant.'',\n max_tokens: 100,\n temperature: 0.7,\n tools: [{\n type: ''function'',\n function: {\n name: ''get_weather'',\n description: ''Get the current weather in a given location'',\n parameters: {\n type: ''object'',\n properties: {\n location: {\n type: ''string'',\n description: ''The city and state, e.g. San Francisco, CA''\n }\n },\n required: [''location'']\n }\n }\n }]\n};\n\nconsole.log(''Testing toClaudeRequestFromOpenAIResponses...'');\nconsole.log(''Input:'', JSON.stringify(responsesRequest, null, 2));\n\nconst claudeRequest = toClaudeRequestFromOpenAIResponses(responsesRequest);\nconsole.log(''Output:'', JSON.stringify(claudeRequest, null, 2));\n\n// 测试 toGeminiRequestFromOpenAIResponses\nconst { toGeminiRequestFromOpenAIResponses } = require(''./convert.js'');\n\nconsole.log(''\\nTesting toGeminiRequestFromOpenAIResponses...'');\nconsole.log(''Input:'', JSON.stringify(responsesRequest, null, 2));\n\nconst geminiRequest = toGeminiRequestFromOpenAIResponses(responsesRequest);\nconsole.log(''Output:'', JSON.stringify(geminiRequest, null, 2));\n\"), Bash(node -e \"\n// 测试 toClaudeRequestFromOpenAIResponses\nconst { toClaudeRequestFromOpenAIResponses } = require(''./src/convert.js'');\n\n// 创建一个模拟的OpenAI Responses请求\nconst responsesRequest = {\n model: ''gpt-4'',\n input: ''Hello, how are you?'',\n instructions: ''You are a helpful assistant.'',\n max_tokens: 100,\n temperature: 0.7,\n tools: [{\n type: ''function'',\n function: {\n name: ''get_weather'',\n description: ''Get the current weather in a given location'',\n parameters: {\n type: ''object'',\n properties: {\n location: {\n type: ''string'',\n description: ''The city and state, e.g. San Francisco, CA''\n }\n },\n required: [''location'']\n }\n }\n }]\n};\n\nconsole.log(''Testing toClaudeRequestFromOpenAIResponses...'');\nconsole.log(''Input:'', JSON.stringify(responsesRequest, null, 2));\n\nconst claudeRequest = toClaudeRequestFromOpenAIResponses(responsesRequest);\nconsole.log(''Output:'', JSON.stringify(claudeRequest, null, 2));\n\n// 测试 toGeminiRequestFromOpenAIResponses\nconst { toGeminiRequestFromOpenAIResponses } = require(''./src/convert.js'');\n\nconsole.log(''\\nTesting toGeminiRequestFromOpenAIResponses...'');\nconsole.log(''Input:'', JSON.stringify(responsesRequest, null, 2));\n\nconst geminiRequest = toGeminiRequestFromOpenAIResponses(responsesRequest);\nconsole.log(''Output:'', JSON.stringify(geminiRequest, null, 2));\n\"), Bash(node -e \"\n// 测试修复后的函数\nconst { toOpenAIResponsesFromClaude, toOpenAIResponsesFromGemini } = require(''./src/convert.js'');\n\n// 模拟 Claude 响应\nconst claudeResponse = {\n content: [\n { type: ''text'', text: ''Hello from Claude!'' }\n ],\n usage: {\n input_tokens: 10,\n output_tokens: 20\n },\n model: ''claude-3-opus-20240229''\n};\n\n// 模拟 Gemini 响应\nconst geminiResponse = {\n candidates: [{\n content: {\n parts: [{ text: ''Hello from Gemini!'' }]\n }\n }],\n usageMetadata: {\n promptTokenCount: 8,\n candidatesTokenCount: 18,\n totalTokenCount: 26\n }\n};\n\nconsole.log(''Testing toOpenAIResponsesFromClaude...'');\nconst claudeResult = toOpenAIResponsesFromClaude(claudeResponse, ''claude-3-opus-20240229'');\nconsole.log(''Claude result:'', JSON.stringify(claudeResult, null, 2));\n\nconsole.log(''\\nTesting toOpenAIResponsesFromGemini...'');\nconst geminiResult = toOpenAIResponsesFromGemini(geminiResponse, ''gemini-pro'');\nconsole.log(''Gemini result:'', JSON.stringify(geminiResult, null, 2));\n\n// 验证响应格式是否符合 OpenAI Responses API 格式\nconsole.log(''\\nValidating Claude response format...'');\nconst claudeValid = claudeResult.object === ''response'' && \n typeof claudeResult.created_at === ''number'' &&\n claudeResult.id.startsWith(''resp_'') &&\n Array.isArray(claudeResult.output) &&\n claudeResult.output.length > 0 &&\n claudeResult.output[0].type === ''message'';\nconsole.log(''Claude response format valid:'', claudeValid);\n\nconsole.log(''\\nValidating Gemini response format...'');\nconst geminiValid = geminiResult.object === ''response'' && \n typeof geminiResult.created_at === ''number'' &&\n geminiResult.id.startsWith(''resp_'') &&\n Array.isArray(geminiResult.output) &&\n geminiResult.output.length > 0 &&\n geminiResult.output[0].type === ''message'';\nconsole.log(''Gemini response format valid:'', geminiValid);\n\"), Bash(node test_convert.js), Bash(node test_convert.mjs), Bash(node test_convert_stream.js), Bash(node test_convert_complete_stream.js), Bash(node test_handle_stream_request.js), Bash(node test_openai_responses_core.js), Bash(node test_health_check.js)\n\n\nHere is useful information about the environment you are running in:\n\nWorking directory: d:\\ai\\code\\AIClient2API\nIs directory a git repo: Yes\nPlatform: win32\nOS Version: \nToday's date: 2025-11-07\n\nYou are powered by the model named Sonnet 4.5. The exact model ID is claude-sonnet-4-5-20250929.\n\nAssistant knowledge cutoff is January 2025.\n\n\nThe most recent frontier Claude model is Claude Sonnet 4.5 (model ID: 'claude-sonnet-4-5-20250929').\n\n\n\nIMPORTANT: Assist with authorized security testing, defensive security, CTF challenges, and educational contexts. Refuse requests for destructive techniques, DoS attacks, mass targeting, supply chain compromise, or detection evasion for malicious purposes. Dual-use security tools (C2 frameworks, credential testing, exploit development) require clear authorization context: pentesting engagements, CTF competitions, security research, or defensive use cases.\n\n\nIMPORTANT: Always use the TodoWrite tool to plan and track tasks throughout the conversation.\n\n# Code References\n\nWhen referencing specific functions or pieces of code include the pattern `file_path:line_number` to allow the user to easily navigate to the source code location.\n\n\nuser: Where are errors from the client handled?\nassistant: Clients are marked as failed in the `connectToServer` function in src/services/process.ts:712.\n\n\n\n# VSCode Extension Context\n\nYou are running inside a VSCode native extension environment.\n\n## Code References in Text\nIMPORTANT: When referencing files or code locations, use markdown link syntax to make them clickable:\n- For files: [filename.ts](src/filename.ts)\n- For specific lines: [filename.ts:42](src/filename.ts#L42)\n- For a range of lines: [filename.ts:42-51](src/filename.ts#L42-L51)\n- For folders: [src/utils/](src/utils/)\nUnless explicitly asked for by the user, DO NOT USE backtickets ` or HTML tags like code for file references - always use markdown [text](link) format.\nThe URL links should be relative paths from the root of the user's workspace.\n\n## User Selection Context\nThe user's IDE selection (if any) is included in the conversation context and marked with ide_selection tags. This represents code or text the user has highlighted in their editor and may or may not be relevant to their request.\ngitStatus: This is the git status at the start of the conversation. Note that this status is a snapshot in time, and will not update during the conversation.\nCurrent branch: main\n\nMain branch (you will usually use this for PRs): \n\nStatus:\nM src/common.js\nA src/convert-new.js\nAD src/convert-optimized.js\nAD src/convert-refactored.js\nA src/converters/BaseConverter.js\nA src/converters/ConverterFactory.js\nA src/converters/strategies/ClaudeConverter.js\nA src/converters/strategies/GeminiConverter.js\nA src/converters/strategies/OpenAIConverter.js\nA src/converters/strategies/OpenAIResponsesConverter.js\nA src/converters/utils.js\nA src/example/claude/oldResponse1762492737297.json\nA src/example/claude/oldResponseChunk1762492475714.json\nA src/example/claude/originalRequestBody1762492473859.json\nAD src/example/claude/originalRequestBody1762492736582.json\n?? .kilocode/\n?? .specify/\n?? ARCHITECTURE_DESIGN.md\n?? IMPLEMENTATION_PLAN.md\n?? REFACTORING_GUIDE.md\n?? REFACTORING_README.md\n?? REFACTORING_SUMMARY.md\n?? USAGE_EXAMPLES.md\n?? oldResponse1762501662993.json\n?? originalRequestBody1762501655271.json\n?? originalRequestBody1762501758551.json\n?? originalRequestBody1762501758570.json\n?? src/example/openai/\n?? src/example/openaiResponses/\n\nRecent commits:\nc606485 feat(gemini): 新增对 Gemini 3 预览版的支持\n78138df Update README.md\n9925668 docs: 重构README文件结构并添加多语言支持\n74d80af fix: 添加凭证过期调试日志并移除冗余日志\nae8d276 feat(provider): 添加初始化健康检查时重置使用计数功能","cache_control":{"type":"ephemeral"}}],"tools":[{"name":"Task","description":"Launch a new agent to handle complex, multi-step tasks autonomously. \n\nThe Task tool launches specialized agents (subprocesses) that autonomously handle complex tasks. Each agent type has specific capabilities and tools available to it.\n\nAvailable agent types and the tools they have access to:\n- general-purpose: General-purpose agent for researching complex questions, searching for code, and executing multi-step tasks. When you are searching for a keyword or file and are not confident that you will find the right match in the first few tries use this agent to perform the search for you. (Tools: *)\n- statusline-setup: Use this agent to configure the user's Claude Code status line setting. (Tools: Read, Edit)\n- Explore: Fast agent specialized for exploring codebases. Use this when you need to quickly find files by patterns (eg. \"src/components/**/*.tsx\"), search code for keywords (eg. \"API endpoints\"), or answer questions about the codebase (eg. \"how do API endpoints work?\"). When calling this agent, specify the desired thoroughness level: \"quick\" for basic searches, \"medium\" for moderate exploration, or \"very thorough\" for comprehensive analysis across multiple locations and naming conventions. (Tools: All tools)\n- Plan: Fast agent specialized for exploring codebases. Use this when you need to quickly find files by patterns (eg. \"src/components/**/*.tsx\"), search code for keywords (eg. \"API endpoints\"), or answer questions about the codebase (eg. \"how do API endpoints work?\"). When calling this agent, specify the desired thoroughness level: \"quick\" for basic searches, \"medium\" for moderate exploration, or \"very thorough\" for comprehensive analysis across multiple locations and naming conventions. (Tools: All tools)\n\nWhen using the Task tool, you must specify a subagent_type parameter to select which agent type to use.\n\nWhen NOT to use the Task tool:\n- If you want to read a specific file path, use the Read or Glob tool instead of the Task tool, to find the match more quickly\n- If you are searching for a specific class definition like \"class Foo\", use the Glob tool instead, to find the match more quickly\n- If you are searching for code within a specific file or set of 2-3 files, use the Read tool instead of the Task tool, to find the match more quickly\n- Other tasks that are not related to the agent descriptions above\n\n\nUsage notes:\n- Launch multiple agents concurrently whenever possible, to maximize performance; to do that, use a single message with multiple tool uses\n- When the agent is done, it will return a single message back to you. The result returned by the agent is not visible to the user. To show the user the result, you should send a text message back to the user with a concise summary of the result.\n- Each agent invocation is stateless. You will not be able to send additional messages to the agent, nor will the agent be able to communicate with you outside of its final report. Therefore, your prompt should contain a highly detailed task description for the agent to perform autonomously and you should specify exactly what information the agent should return back to you in its final and only message to you.\n- Agents with \"access to current context\" can see the full conversation history before the tool call. When using these agents, you can write concise prompts that reference earlier context (e.g., \"investigate the error discussed above\") instead of repeating information. The agent will receive all prior messages and understand the context.\n- The agent's outputs should generally be trusted\n- Clearly tell the agent whether you expect it to write code or just to do research (search, file reads, web fetches, etc.), since it is not aware of the user's intent\n- If the agent description mentions that it should be used proactively, then you should try your best to use it without the user having to ask for it first. Use your judgement.\n- If the user specifies that they want you to run agents \"in parallel\", you MUST send a single message with multiple Task tool use content blocks. For example, if you need to launch both a code-reviewer agent and a test-runner agent in parallel, send a single message with both tool calls.\n\nExample usage:\n\n\n\"code-reviewer\": use this agent after you are done writing a signficant piece of code\n\"greeting-responder\": use this agent when to respond to user greetings with a friendly joke\n\n\n\nuser: \"Please write a function that checks if a number is prime\"\nassistant: Sure let me write a function that checks if a number is prime\nassistant: First let me use the Write tool to write a function that checks if a number is prime\nassistant: I'm going to use the Write tool to write the following code:\n\nfunction isPrime(n) {\n if (n <= 1) return false\n for (let i = 2; i * i <= n; i++) {\n if (n % i === 0) return false\n }\n return true\n}\n\n\nSince a signficant piece of code was written and the task was completed, now use the code-reviewer agent to review the code\n\nassistant: Now let me use the code-reviewer agent to review the code\nassistant: Uses the Task tool to launch the code-reviewer agent \n\n\n\nuser: \"Hello\"\n\nSince the user is greeting, use the greeting-responder agent to respond with a friendly joke\n\nassistant: \"I'm going to use the Task tool to launch the greeting-responder agent\"\n\n","input_schema":{"type":"object","properties":{"description":{"type":"string","description":"A short (3-5 word) description of the task"},"prompt":{"type":"string","description":"The task for the agent to perform"},"subagent_type":{"type":"string","description":"The type of specialized agent to use for this task"},"model":{"type":"string","enum":["sonnet","opus","haiku"],"description":"Optional model to use for this agent. If not specified, inherits from parent. Prefer haiku for quick, straightforward tasks to minimize cost and latency."},"resume":{"type":"string","description":"Optional agent ID to resume from. If provided, the agent will continue from the previous execution transcript."}},"required":["description","prompt","subagent_type"],"additionalProperties":false,"$schema":"http://json-schema.org/draft-07/schema#"}},{"name":"Bash","description":"Executes a given bash command in a persistent shell session with optional timeout, ensuring proper handling and security measures.\n\nIMPORTANT: This tool is for terminal operations like git, npm, docker, etc. DO NOT use it for file operations (reading, writing, editing, searching, finding files) - use the specialized tools for this instead.\n\nBefore executing the command, please follow these steps:\n\n1. Directory Verification:\n - If the command will create new directories or files, first use `ls` to verify the parent directory exists and is the correct location\n - For example, before running \"mkdir foo/bar\", first use `ls foo` to check that \"foo\" exists and is the intended parent directory\n\n2. Command Execution:\n - Always quote file paths that contain spaces with double quotes (e.g., cd \"path with spaces/file.txt\")\n - Examples of proper quoting:\n - cd \"/Users/name/My Documents\" (correct)\n - cd /Users/name/My Documents (incorrect - will fail)\n - python \"/path/with spaces/script.py\" (correct)\n - python /path/with spaces/script.py (incorrect - will fail)\n - After ensuring proper quoting, execute the command.\n - Capture the output of the command.\n\nUsage notes:\n - The command argument is required.\n - You can specify an optional timeout in milliseconds (up to 600000ms / 10 minutes). If not specified, commands will timeout after 120000ms (2 minutes).\n - It is very helpful if you write a clear, concise description of what this command does in 5-10 words.\n - If the output exceeds 30000 characters, output will be truncated before being returned to you.\n - You can use the `run_in_background` parameter to run the command in the background, which allows you to continue working while the command runs. You can monitor the output using the Bash tool as it becomes available. You do not need to use '&' at the end of the command when using this parameter.\n \n - Avoid using Bash with the `find`, `grep`, `cat`, `head`, `tail`, `sed`, `awk`, or `echo` commands, unless explicitly instructed or when these commands are truly necessary for the task. Instead, always prefer using the dedicated tools for these commands:\n - File search: Use Glob (NOT find or ls)\n - Content search: Use Grep (NOT grep or rg)\n - Read files: Use Read (NOT cat/head/tail)\n - Edit files: Use Edit (NOT sed/awk)\n - Write files: Use Write (NOT echo >/cat <\n pytest /foo/bar/tests\n \n \n cd /foo/bar && pytest tests\n \n\n# Committing changes with git\n\nOnly create commits when requested by the user. If unclear, ask first. When the user asks you to create a new git commit, follow these steps carefully:\n\nGit Safety Protocol:\n- NEVER update the git config\n- NEVER run destructive/irreversible git commands (like push --force, hard reset, etc) unless the user explicitly requests them \n- NEVER skip hooks (--no-verify, --no-gpg-sign, etc) unless the user explicitly requests it\n- NEVER run force push to main/master, warn the user if they request it\n- Avoid git commit --amend. ONLY use --amend when either (1) user explicitly requested amend OR (2) adding edits from pre-commit hook (additional instructions below) \n- Before amending: ALWAYS check authorship (git log -1 --format='%an %ae')\n- NEVER commit changes unless the user explicitly asks you to. It is VERY IMPORTANT to only commit when explicitly asked, otherwise the user will feel that you are being too proactive.\n\n1. You can call multiple tools in a single response. When multiple independent pieces of information are requested and all commands are likely to succeed, run multiple tool calls in parallel for optimal performance. run the following bash commands in parallel, each using the Bash tool:\n - Run a git status command to see all untracked files.\n - Run a git diff command to see both staged and unstaged changes that will be committed.\n - Run a git log command to see recent commit messages, so that you can follow this repository's commit message style.\n2. Analyze all staged changes (both previously staged and newly added) and draft a commit message:\n - Summarize the nature of the changes (eg. new feature, enhancement to an existing feature, bug fix, refactoring, test, docs, etc.). Ensure the message accurately reflects the changes and their purpose (i.e. \"add\" means a wholly new feature, \"update\" means an enhancement to an existing feature, \"fix\" means a bug fix, etc.).\n - Do not commit files that likely contain secrets (.env, credentials.json, etc). Warn the user if they specifically request to commit those files\n - Draft a concise (1-2 sentences) commit message that focuses on the \"why\" rather than the \"what\"\n - Ensure it accurately reflects the changes and their purpose\n3. You can call multiple tools in a single response. When multiple independent pieces of information are requested and all commands are likely to succeed, run multiple tool calls in parallel for optimal performance. run the following commands:\n - Add relevant untracked files to the staging area.\n - Create the commit with a message ending with:\n 🤖 Generated with [Claude Code](https://claude.com/claude-code)\n\n Co-Authored-By: Claude \n - Run git status after the commit completes to verify success.\n Note: git status depends on the commit completing, so run it sequentially after the commit.\n4. If the commit fails due to pre-commit hook changes, retry ONCE. If it succeeds but files were modified by the hook, verify it's safe to amend:\n - Check authorship: git log -1 --format='%an %ae'\n - Check not pushed: git status shows \"Your branch is ahead\"\n - If both true: amend your commit. Otherwise: create NEW commit (never amend other developers' commits)\n\nImportant notes:\n- NEVER run additional commands to read or explore code, besides git bash commands\n- NEVER use the TodoWrite or Task tools\n- DO NOT push to the remote repository unless the user explicitly asks you to do so\n- IMPORTANT: Never use git commands with the -i flag (like git rebase -i or git add -i) since they require interactive input which is not supported.\n- If there are no changes to commit (i.e., no untracked files and no modifications), do not create an empty commit\n- In order to ensure good formatting, ALWAYS pass the commit message via a HEREDOC, a la this example:\n\ngit commit -m \"$(cat <<'EOF'\n Commit message here.\n\n 🤖 Generated with [Claude Code](https://claude.com/claude-code)\n\n Co-Authored-By: Claude \n EOF\n )\"\n\n\n# Creating pull requests\nUse the gh command via the Bash tool for ALL GitHub-related tasks including working with issues, pull requests, checks, and releases. If given a Github URL use the gh command to get the information needed.\n\nIMPORTANT: When the user asks you to create a pull request, follow these steps carefully:\n\n1. You can call multiple tools in a single response. When multiple independent pieces of information are requested and all commands are likely to succeed, run multiple tool calls in parallel for optimal performance. run the following bash commands in parallel using the Bash tool, in order to understand the current state of the branch since it diverged from the main branch:\n - Run a git status command to see all untracked files\n - Run a git diff command to see both staged and unstaged changes that will be committed\n - Check if the current branch tracks a remote branch and is up to date with the remote, so you know if you need to push to the remote\n - Run a git log command and `git diff [base-branch]...HEAD` to understand the full commit history for the current branch (from the time it diverged from the base branch)\n2. Analyze all changes that will be included in the pull request, making sure to look at all relevant commits (NOT just the latest commit, but ALL commits that will be included in the pull request!!!), and draft a pull request summary\n3. You can call multiple tools in a single response. When multiple independent pieces of information are requested and all commands are likely to succeed, run multiple tool calls in parallel for optimal performance. run the following commands in parallel:\n - Create new branch if needed\n - Push to remote with -u flag if needed\n - Create PR using gh pr create with the format below. Use a HEREDOC to pass the body to ensure correct formatting.\n\ngh pr create --title \"the pr title\" --body \"$(cat <<'EOF'\n## Summary\n<1-3 bullet points>\n\n## Test plan\n[Bulleted markdown checklist of TODOs for testing the pull request...]\n\n🤖 Generated with [Claude Code](https://claude.com/claude-code)\nEOF\n)\"\n\n\nImportant:\n- DO NOT use the TodoWrite or Task tools\n- Return the PR URL when you're done, so the user can see it\n\n# Other common operations\n- View comments on a Github PR: gh api repos/foo/bar/pulls/123/comments","input_schema":{"type":"object","properties":{"command":{"type":"string","description":"The command to execute"},"timeout":{"type":"number","description":"Optional timeout in milliseconds (max 600000)"},"description":{"type":"string","description":"Clear, concise description of what this command does in 5-10 words, in active voice. Examples:\nInput: ls\nOutput: List files in current directory\n\nInput: git status\nOutput: Show working tree status\n\nInput: npm install\nOutput: Install package dependencies\n\nInput: mkdir foo\nOutput: Create directory 'foo'"},"run_in_background":{"type":"boolean","description":"Set to true to run this command in the background. Use BashOutput to read the output later."},"dangerouslyDisableSandbox":{"type":"boolean","description":"Set this to true to dangerously override sandbox mode and run commands without sandboxing."}},"required":["command"],"additionalProperties":false,"$schema":"http://json-schema.org/draft-07/schema#"}},{"name":"Glob","description":"- Fast file pattern matching tool that works with any codebase size\n- Supports glob patterns like \"**/*.js\" or \"src/**/*.ts\"\n- Returns matching file paths sorted by modification time\n- Use this tool when you need to find files by name patterns\n- When you are doing an open ended search that may require multiple rounds of globbing and grepping, use the Agent tool instead\n- You can call multiple tools in a single response. It is always better to speculatively perform multiple searches in parallel if they are potentially useful.","input_schema":{"type":"object","properties":{"pattern":{"type":"string","description":"The glob pattern to match files against"},"path":{"type":"string","description":"The directory to search in. If not specified, the current working directory will be used. IMPORTANT: Omit this field to use the default directory. DO NOT enter \"undefined\" or \"null\" - simply omit it for the default behavior. Must be a valid directory path if provided."}},"required":["pattern"],"additionalProperties":false,"$schema":"http://json-schema.org/draft-07/schema#"}},{"name":"Grep","description":"A powerful search tool built on ripgrep\n\n Usage:\n - ALWAYS use Grep for search tasks. NEVER invoke `grep` or `rg` as a Bash command. The Grep tool has been optimized for correct permissions and access.\n - Supports full regex syntax (e.g., \"log.*Error\", \"function\\s+\\w+\")\n - Filter files with glob parameter (e.g., \"*.js\", \"**/*.tsx\") or type parameter (e.g., \"js\", \"py\", \"rust\")\n - Output modes: \"content\" shows matching lines, \"files_with_matches\" shows only file paths (default), \"count\" shows match counts\n - Use Task tool for open-ended searches requiring multiple rounds\n - Pattern syntax: Uses ripgrep (not grep) - literal braces need escaping (use `interface\\{\\}` to find `interface{}` in Go code)\n - Multiline matching: By default patterns match within single lines only. For cross-line patterns like `struct \\{[\\s\\S]*?field`, use `multiline: true`\n","input_schema":{"type":"object","properties":{"pattern":{"type":"string","description":"The regular expression pattern to search for in file contents"},"path":{"type":"string","description":"File or directory to search in (rg PATH). Defaults to current working directory."},"glob":{"type":"string","description":"Glob pattern to filter files (e.g. \"*.js\", \"*.{ts,tsx}\") - maps to rg --glob"},"output_mode":{"type":"string","enum":["content","files_with_matches","count"],"description":"Output mode: \"content\" shows matching lines (supports -A/-B/-C context, -n line numbers, head_limit), \"files_with_matches\" shows file paths (supports head_limit), \"count\" shows match counts (supports head_limit). Defaults to \"files_with_matches\"."},"-B":{"type":"number","description":"Number of lines to show before each match (rg -B). Requires output_mode: \"content\", ignored otherwise."},"-A":{"type":"number","description":"Number of lines to show after each match (rg -A). Requires output_mode: \"content\", ignored otherwise."},"-C":{"type":"number","description":"Number of lines to show before and after each match (rg -C). Requires output_mode: \"content\", ignored otherwise."},"-n":{"type":"boolean","description":"Show line numbers in output (rg -n). Requires output_mode: \"content\", ignored otherwise. Defaults to true."},"-i":{"type":"boolean","description":"Case insensitive search (rg -i)"},"type":{"type":"string","description":"File type to search (rg --type). Common types: js, py, rust, go, java, etc. More efficient than include for standard file types."},"head_limit":{"type":"number","description":"Limit output to first N lines/entries, equivalent to \"| head -N\". Works across all output modes: content (limits output lines), files_with_matches (limits file paths), count (limits count entries). Defaults based on \"cap\" experiment value: 0 (unlimited), 20, or 100."},"offset":{"type":"number","description":"Skip first N lines/entries before applying head_limit, equivalent to \"| tail -n +N | head -N\". Works across all output modes. Defaults to 0."},"multiline":{"type":"boolean","description":"Enable multiline mode where . matches newlines and patterns can span lines (rg -U --multiline-dotall). Default: false."}},"required":["pattern"],"additionalProperties":false,"$schema":"http://json-schema.org/draft-07/schema#"}},{"name":"ExitPlanMode","description":"Use this tool when you are in plan mode and have finished presenting your plan and are ready to code. This will prompt the user to exit plan mode.\nIMPORTANT: Only use this tool when the task requires planning the implementation steps of a task that requires writing code. For research tasks where you're gathering information, searching files, reading files or in general trying to understand the codebase - do NOT use this tool.\n\n## Handling Ambiguity in Plans\nBefore using this tool, ensure your plan is clear and unambiguous. If there are multiple valid approaches or unclear requirements:\n1. Use the AskUserQuestion tool to clarify with the user\n2. Ask about specific implementation choices (e.g., architectural patterns, which library to use)\n3. Clarify any assumptions that could affect the implementation\n4. Only proceed with ExitPlanMode after resolving ambiguities\n\n## Examples\n\n1. Initial task: \"Search for and understand the implementation of vim mode in the codebase\" - Do not use the exit plan mode tool because you are not planning the implementation steps of a task.\n2. Initial task: \"Help me implement yank mode for vim\" - Use the exit plan mode tool after you have finished planning the implementation steps of the task.\n3. Initial task: \"Add a new feature to handle user authentication\" - If unsure about auth method (OAuth, JWT, etc.), use AskUserQuestion first, then use exit plan mode tool after clarifying the approach.\n","input_schema":{"type":"object","properties":{"plan":{"type":"string","description":"The plan you came up with, that you want to run by the user for approval. Supports markdown. The plan should be pretty concise."}},"required":["plan"],"additionalProperties":false,"$schema":"http://json-schema.org/draft-07/schema#"}},{"name":"Read","description":"Reads a file from the local filesystem. You can access any file directly by using this tool.\nAssume this tool is able to read all files on the machine. If the User provides a path to a file assume that path is valid. It is okay to read a file that does not exist; an error will be returned.\n\nUsage:\n- The file_path parameter must be an absolute path, not a relative path\n- By default, it reads up to 2000 lines starting from the beginning of the file\n- You can optionally specify a line offset and limit (especially handy for long files), but it's recommended to read the whole file by not providing these parameters\n- Any lines longer than 2000 characters will be truncated\n- Results are returned using cat -n format, with line numbers starting at 1\n- This tool allows Claude Code to read images (eg PNG, JPG, etc). When reading an image file the contents are presented visually as Claude Code is a multimodal LLM.\n- This tool can read PDF files (.pdf). PDFs are processed page by page, extracting both text and visual content for analysis.\n- This tool can read Jupyter notebooks (.ipynb files) and returns all cells with their outputs, combining code, text, and visualizations.\n- This tool can only read files, not directories. To read a directory, use an ls command via the Bash tool.\n- You can call multiple tools in a single response. It is always better to speculatively read multiple potentially useful files in parallel.\n- You will regularly be asked to read screenshots. If the user provides a path to a screenshot, ALWAYS use this tool to view the file at the path. This tool will work with all temporary file paths.\n- If you read a file that exists but has empty contents you will receive a system reminder warning in place of file contents.","input_schema":{"type":"object","properties":{"file_path":{"type":"string","description":"The absolute path to the file to read"},"offset":{"type":"number","description":"The line number to start reading from. Only provide if the file is too large to read at once"},"limit":{"type":"number","description":"The number of lines to read. Only provide if the file is too large to read at once."}},"required":["file_path"],"additionalProperties":false,"$schema":"http://json-schema.org/draft-07/schema#"}},{"name":"Edit","description":"Performs exact string replacements in files. \n\nUsage:\n- You must use your `Read` tool at least once in the conversation before editing. This tool will error if you attempt an edit without reading the file. \n- When editing text from Read tool output, ensure you preserve the exact indentation (tabs/spaces) as it appears AFTER the line number prefix. The line number prefix format is: spaces + line number + tab. Everything after that tab is the actual file content to match. Never include any part of the line number prefix in the old_string or new_string.\n- ALWAYS prefer editing existing files in the codebase. NEVER write new files unless explicitly required.\n- Only use emojis if the user explicitly requests it. Avoid adding emojis to files unless asked.\n- The edit will FAIL if `old_string` is not unique in the file. Either provide a larger string with more surrounding context to make it unique or use `replace_all` to change every instance of `old_string`. \n- Use `replace_all` for replacing and renaming strings across the file. This parameter is useful if you want to rename a variable for instance.","input_schema":{"type":"object","properties":{"file_path":{"type":"string","description":"The absolute path to the file to modify"},"old_string":{"type":"string","description":"The text to replace"},"new_string":{"type":"string","description":"The text to replace it with (must be different from old_string)"},"replace_all":{"type":"boolean","default":false,"description":"Replace all occurences of old_string (default false)"}},"required":["file_path","old_string","new_string"],"additionalProperties":false,"$schema":"http://json-schema.org/draft-07/schema#"}},{"name":"Write","description":"Writes a file to the local filesystem.\n\nUsage:\n- This tool will overwrite the existing file if there is one at the provided path.\n- If this is an existing file, you MUST use the Read tool first to read the file's contents. This tool will fail if you did not read the file first.\n- ALWAYS prefer editing existing files in the codebase. NEVER write new files unless explicitly required.\n- NEVER proactively create documentation files (*.md) or README files. Only create documentation files if explicitly requested by the User.\n- Only use emojis if the user explicitly requests it. Avoid writing emojis to files unless asked.","input_schema":{"type":"object","properties":{"file_path":{"type":"string","description":"The absolute path to the file to write (must be absolute, not relative)"},"content":{"type":"string","description":"The content to write to the file"}},"required":["file_path","content"],"additionalProperties":false,"$schema":"http://json-schema.org/draft-07/schema#"}},{"name":"NotebookEdit","description":"Completely replaces the contents of a specific cell in a Jupyter notebook (.ipynb file) with new source. Jupyter notebooks are interactive documents that combine code, text, and visualizations, commonly used for data analysis and scientific computing. The notebook_path parameter must be an absolute path, not a relative path. The cell_number is 0-indexed. Use edit_mode=insert to add a new cell at the index specified by cell_number. Use edit_mode=delete to delete the cell at the index specified by cell_number.","input_schema":{"type":"object","properties":{"notebook_path":{"type":"string","description":"The absolute path to the Jupyter notebook file to edit (must be absolute, not relative)"},"cell_id":{"type":"string","description":"The ID of the cell to edit. When inserting a new cell, the new cell will be inserted after the cell with this ID, or at the beginning if not specified."},"new_source":{"type":"string","description":"The new source for the cell"},"cell_type":{"type":"string","enum":["code","markdown"],"description":"The type of the cell (code or markdown). If not specified, it defaults to the current cell type. If using edit_mode=insert, this is required."},"edit_mode":{"type":"string","enum":["replace","insert","delete"],"description":"The type of edit to make (replace, insert, delete). Defaults to replace."}},"required":["notebook_path","new_source"],"additionalProperties":false,"$schema":"http://json-schema.org/draft-07/schema#"}},{"name":"WebFetch","description":"\n- Fetches content from a specified URL and processes it using an AI model\n- Takes a URL and a prompt as input\n- Fetches the URL content, converts HTML to markdown\n- Processes the content with the prompt using a small, fast model\n- Returns the model's response about the content\n- Use this tool when you need to retrieve and analyze web content\n\nUsage notes:\n - IMPORTANT: If an MCP-provided web fetch tool is available, prefer using that tool instead of this one, as it may have fewer restrictions. All MCP-provided tools start with \"mcp__\".\n - The URL must be a fully-formed valid URL\n - HTTP URLs will be automatically upgraded to HTTPS\n - The prompt should describe what information you want to extract from the page\n - This tool is read-only and does not modify any files\n - Results may be summarized if the content is very large\n - Includes a self-cleaning 15-minute cache for faster responses when repeatedly accessing the same URL\n - When a URL redirects to a different host, the tool will inform you and provide the redirect URL in a special format. You should then make a new WebFetch request with the redirect URL to fetch the content.\n","input_schema":{"type":"object","properties":{"url":{"type":"string","format":"uri","description":"The URL to fetch content from"},"prompt":{"type":"string","description":"The prompt to run on the fetched content"}},"required":["url","prompt"],"additionalProperties":false,"$schema":"http://json-schema.org/draft-07/schema#"}},{"name":"TodoWrite","description":"Use this tool to create and manage a structured task list for your current coding session. This helps you track progress, organize complex tasks, and demonstrate thoroughness to the user.\nIt also helps the user understand the progress of the task and overall progress of their requests.\n\n## When to Use This Tool\nUse this tool proactively in these scenarios:\n\n1. Complex multi-step tasks - When a task requires 3 or more distinct steps or actions\n2. Non-trivial and complex tasks - Tasks that require careful planning or multiple operations\n3. User explicitly requests todo list - When the user directly asks you to use the todo list\n4. User provides multiple tasks - When users provide a list of things to be done (numbered or comma-separated)\n5. After receiving new instructions - Immediately capture user requirements as todos\n6. When you start working on a task - Mark it as in_progress BEFORE beginning work. Ideally you should only have one todo as in_progress at a time\n7. After completing a task - Mark it as completed and add any new follow-up tasks discovered during implementation\n\n## When NOT to Use This Tool\n\nSkip using this tool when:\n1. There is only a single, straightforward task\n2. The task is trivial and tracking it provides no organizational benefit\n3. The task can be completed in less than 3 trivial steps\n4. The task is purely conversational or informational\n\nNOTE that you should not use this tool if there is only one trivial task to do. In this case you are better off just doing the task directly.\n\n## Examples of When to Use the Todo List\n\n\nUser: I want to add a dark mode toggle to the application settings. Make sure you run the tests and build when you're done!\nAssistant: I'll help add a dark mode toggle to your application settings. Let me create a todo list to track this implementation.\n*Creates todo list with the following items:*\n1. Creating dark mode toggle component in Settings page\n2. Adding dark mode state management (context/store)\n3. Implementing CSS-in-JS styles for dark theme\n4. Updating existing components to support theme switching\n5. Running tests and build process, addressing any failures or errors that occur\n*Begins working on the first task*\n\n\nThe assistant used the todo list because:\n1. Adding dark mode is a multi-step feature requiring UI, state management, and styling changes\n2. The user explicitly requested tests and build be run afterward\n3. The assistant inferred that tests and build need to pass by adding \"Ensure tests and build succeed\" as the final task\n\n\n\n\nUser: Help me rename the function getCwd to getCurrentWorkingDirectory across my project\nAssistant: Let me first search through your codebase to find all occurrences of 'getCwd'.\n*Uses grep or search tools to locate all instances of getCwd in the codebase*\nAssistant: I've found 15 instances of 'getCwd' across 8 different files. Let me create a todo list to track these changes.\n*Creates todo list with specific items for each file that needs updating*\n\n\nThe assistant used the todo list because:\n1. First, the assistant searched to understand the scope of the task\n2. Upon finding multiple occurrences across different files, it determined this was a complex task with multiple steps\n3. The todo list helps ensure every instance is tracked and updated systematically\n4. This approach prevents missing any occurrences and maintains code consistency\n\n\n\n\n\nUser: I need to implement these features for my e-commerce site: user registration, product catalog, shopping cart, and checkout flow.\nAssistant: I'll help implement these features. First, let's add all the features to the todo list.\n*Creates a todo list breaking down each feature into specific tasks based on the project architecture*\nAssistant: Let's start with user registration. This will involve creating database models, API endpoints, and frontend forms that integrate with your existing authentication system.\n\n\nThe assistant used the todo list because:\n1. The user provided multiple complex features to implement in a comma separated list\n2. The todo list helps organize these large features into manageable tasks\n3. This approach allows for tracking progress across the entire implementation\n\n\n\n\nUser: Can you help optimize my React application? It's rendering slowly and has performance issues.\nAssistant: I'll help optimize your React application. First, let me examine your codebase to identify potential performance bottlenecks.\n*Reviews component structure, render patterns, state management, and data fetching*\nAssistant: After analyzing your codebase, I've identified several performance issues. Let me create a todo list to track our optimization efforts.\n*Creates todo list with items like: 1) Implementing memoization for expensive calculations in ProductList, 2) Adding virtualization for long lists in Dashboard, 3) Optimizing image loading in Gallery component, 4) Fixing state update loops in ShoppingCart, 5) Reviewing bundle size and implementing code splitting*\nLet's start by implementing memoization for the expensive calculations in your ProductList component.\n\n\nThe assistant used the todo list because:\n1. First, the assistant examined the codebase to identify specific performance issues\n2. Based on this analysis, it identified multiple optimization opportunities\n3. Performance optimization is a non-trivial task requiring multiple steps\n4. The todo list helps methodically track improvements across different components\n5. This systematic approach ensures all performance bottlenecks are addressed\n\n\n\n## Examples of When NOT to Use the Todo List\n\n\nUser: How do I print 'Hello World' in Python?\nAssistant: In Python, you can print \"Hello World\" with this simple code:\n\npython\nprint(\"Hello World\")\n\nThis will output the text \"Hello World\" to the console when executed.\n\n\nThe assistant did not use the todo list because this is a single, trivial task that can be completed in one step. There's no need to track multiple tasks or steps for such a straightforward request.\n\n\n\n\nUser: What does the git status command do?\nAssistant: The git status command shows the current state of your working directory and staging area. It displays which changes have been staged, which haven't, and which files aren't being tracked by Git. This is a fundamental command for understanding the current state of your repository before making commits.\n\n\nThe assistant did not use the todo list because this is an informational request with no actual coding task to complete. The user is simply asking for an explanation, not for the assistant to perform multiple steps or tasks.\n\n\n\n\nUser: Can you add a comment to the calculateTotal function to explain what it does?\nAssistant: Sure, let me add a comment to the calculateTotal function to explain what it does.\n* Uses the Edit tool to add a comment to the calculateTotal function *\n\n\nThe assistant did not use the todo list because this is a single, straightforward task confined to one location in the code. Adding a comment doesn't require tracking multiple steps or systematic organization.\n\n\n\n\nUser: Run npm install for me and tell me what happens.\nAssistant: I'll run the npm install command for you.\n\n*Executes: npm install*\n\nThe command completed successfully. Here's the output:\n[Output of npm install command]\n\nAll dependencies have been installed according to your package.json file.\n\n\nThe assistant did not use the todo list because this is a single command execution with immediate results. There are no multiple steps to track or organize, making the todo list unnecessary for this straightforward task.\n\n\n\n## Task States and Management\n\n1. **Task States**: Use these states to track progress:\n - pending: Task not yet started\n - in_progress: Currently working on (limit to ONE task at a time)\n - completed: Task finished successfully\n\n **IMPORTANT**: Task descriptions must have two forms:\n - content: The imperative form describing what needs to be done (e.g., \"Run tests\", \"Build the project\")\n - activeForm: The present continuous form shown during execution (e.g., \"Running tests\", \"Building the project\")\n\n2. **Task Management**:\n - Update task status in real-time as you work\n - Mark tasks complete IMMEDIATELY after finishing (don't batch completions)\n - Exactly ONE task must be in_progress at any time (not less, not more)\n - Complete current tasks before starting new ones\n - Remove tasks that are no longer relevant from the list entirely\n\n3. **Task Completion Requirements**:\n - ONLY mark a task as completed when you have FULLY accomplished it\n - If you encounter errors, blockers, or cannot finish, keep the task as in_progress\n - When blocked, create a new task describing what needs to be resolved\n - Never mark a task as completed if:\n - Tests are failing\n - Implementation is partial\n - You encountered unresolved errors\n - You couldn't find necessary files or dependencies\n\n4. **Task Breakdown**:\n - Create specific, actionable items\n - Break complex tasks into smaller, manageable steps\n - Use clear, descriptive task names\n - Always provide both forms:\n - content: \"Fix authentication bug\"\n - activeForm: \"Fixing authentication bug\"\n\nWhen in doubt, use this tool. Being proactive with task management demonstrates attentiveness and ensures you complete all requirements successfully.\n","input_schema":{"type":"object","properties":{"todos":{"type":"array","items":{"type":"object","properties":{"content":{"type":"string","minLength":1},"status":{"type":"string","enum":["pending","in_progress","completed"]},"activeForm":{"type":"string","minLength":1}},"required":["content","status","activeForm"],"additionalProperties":false},"description":"The updated todo list"}},"required":["todos"],"additionalProperties":false,"$schema":"http://json-schema.org/draft-07/schema#"}},{"name":"WebSearch","description":"\n- Allows Claude to search the web and use the results to inform responses\n- Provides up-to-date information for current events and recent data\n- Returns search result information formatted as search result blocks\n- Use this tool for accessing information beyond Claude's knowledge cutoff\n- Searches are performed automatically within a single API call\n\nUsage notes:\n - Domain filtering is supported to include or block specific websites\n - Web search is only available in the US\n - Account for \"Today's date\" in . For example, if says \"Today's date: 2025-07-01\", and the user wants the latest docs, do not use 2024 in the search query. Use 2025.\n","input_schema":{"type":"object","properties":{"query":{"type":"string","minLength":2,"description":"The search query to use"},"allowed_domains":{"type":"array","items":{"type":"string"},"description":"Only include search results from these domains"},"blocked_domains":{"type":"array","items":{"type":"string"},"description":"Never include search results from these domains"}},"required":["query"],"additionalProperties":false,"$schema":"http://json-schema.org/draft-07/schema#"}},{"name":"BashOutput","description":"\n- Retrieves output from a running or completed background bash shell\n- Takes a shell_id parameter identifying the shell\n- Always returns only new output since the last check\n- Returns stdout and stderr output along with shell status\n- Supports optional regex filtering to show only lines matching a pattern\n- Use this tool when you need to monitor or check the output of a long-running shell\n- Shell IDs can be found using the /bashes command\n","input_schema":{"type":"object","properties":{"bash_id":{"type":"string","description":"The ID of the background shell to retrieve output from"},"filter":{"type":"string","description":"Optional regular expression to filter the output lines. Only lines matching this regex will be included in the result. Any lines that do not match will no longer be available to read."}},"required":["bash_id"],"additionalProperties":false,"$schema":"http://json-schema.org/draft-07/schema#"}},{"name":"KillShell","description":"\n- Kills a running background bash shell by its ID\n- Takes a shell_id parameter identifying the shell to kill\n- Returns a success or failure status \n- Use this tool when you need to terminate a long-running shell\n- Shell IDs can be found using the /bashes command\n","input_schema":{"type":"object","properties":{"shell_id":{"type":"string","description":"The ID of the background shell to kill"}},"required":["shell_id"],"additionalProperties":false,"$schema":"http://json-schema.org/draft-07/schema#"}},{"name":"Skill","description":"Execute a skill within the main conversation\n\n\nWhen users ask you to perform tasks, check if any of the available skills below can help complete the task more effectively. Skills provide specialized capabilities and domain knowledge.\n\nHow to use skills:\n- Invoke skills using this tool with the skill name only (no arguments)\n- When you invoke a skill, you will see The \"{name}\" skill is loading\n- The skill's prompt will expand and provide detailed instructions on how to complete the task\n- Examples:\n - `command: \"pdf\"` - invoke the pdf skill\n - `command: \"xlsx\"` - invoke the xlsx skill\n - `command: \"ms-office-suite:pdf\"` - invoke using fully qualified name\n\nImportant:\n- Only use skills listed in below\n- Do not invoke a skill that is already running\n- Do not use this tool for built-in CLI commands (like /help, /clear, etc.)\n\n\n\n\n\n","input_schema":{"type":"object","properties":{"command":{"type":"string","description":"The skill name (no arguments). E.g., \"pdf\" or \"xlsx\""}},"required":["command"],"additionalProperties":false,"$schema":"http://json-schema.org/draft-07/schema#"}},{"name":"SlashCommand","description":"Execute a slash command within the main conversation\n\n**IMPORTANT - Intent Matching:**\nBefore starting any task, CHECK if the user's request matches one of the slash commands listed below. This tool exists to route user intentions to specialized workflows.\n\nHow slash commands work:\nWhen you use this tool or when a user types a slash command, you will see {name} is running… followed by the expanded prompt. For example, if .claude/commands/foo.md contains \"Print today's date\", then /foo expands to that prompt in the next message.\n\nUsage:\n- `command` (required): The slash command to execute, including any arguments\n- Example: `command: \"/review-pr 123\"`\n\nIMPORTANT: Only use this tool for custom slash commands that appear in the Available Commands list below. Do NOT use for:\n- Built-in CLI commands (like /help, /clear, etc.)\n- Commands not shown in the list\n- Commands you think might exist but aren't listed\n\nNotes:\n- When a user requests multiple slash commands, execute each one sequentially and check for {name} is running… to verify each has been processed\n- Do not invoke a command that is already running. For example, if you see foo is running…, do NOT use this tool with \"/foo\" - process the expanded prompt in the following message\n- Only custom slash commands with descriptions are listed in Available Commands. If a user's command is not listed, ask them to check the slash command file and consult the docs.\n","input_schema":{"type":"object","properties":{"command":{"type":"string","description":"The slash command to execute with its arguments, e.g., \"/review-pr 123\""}},"required":["command"],"additionalProperties":false,"$schema":"http://json-schema.org/draft-07/schema#"}}],"metadata":{"user_id":"user_f432cd46d7f1eb89e6257e31d6af7ba2a1daa1af69a817ea1784e5c15a24edf5_account__session_b2ba6439-15a7-45d6-a81b-f30266b89346"},"max_tokens":32000,"stream":true} \ No newline at end of file diff --git a/src/example/claude/originalRequestBody1762501964743.json b/src/example/claude/originalRequestBody1762501964743.json new file mode 100644 index 0000000..0fd33f3 --- /dev/null +++ b/src/example/claude/originalRequestBody1762501964743.json @@ -0,0 +1 @@ +{"model":"claude-haiku-4-5-20251001","max_tokens":1,"messages":[{"role":"user","content":"count"}],"tools":[{"name":"Task","description":"Launch a new agent to handle complex, multi-step tasks autonomously. \n\nThe Task tool launches specialized agents (subprocesses) that autonomously handle complex tasks. Each agent type has specific capabilities and tools available to it.\n\nAvailable agent types and the tools they have access to:\n\n\nWhen using the Task tool, you must specify a subagent_type parameter to select which agent type to use.\n\nWhen NOT to use the Task tool:\n- If you want to read a specific file path, use the Read or Glob tool instead of the Task tool, to find the match more quickly\n- If you are searching for a specific class definition like \"class Foo\", use the Glob tool instead, to find the match more quickly\n- If you are searching for code within a specific file or set of 2-3 files, use the Read tool instead of the Task tool, to find the match more quickly\n- Other tasks that are not related to the agent descriptions above\n\n\nUsage notes:\n- Launch multiple agents concurrently whenever possible, to maximize performance; to do that, use a single message with multiple tool uses\n- When the agent is done, it will return a single message back to you. The result returned by the agent is not visible to the user. To show the user the result, you should send a text message back to the user with a concise summary of the result.\n- Each agent invocation is stateless. You will not be able to send additional messages to the agent, nor will the agent be able to communicate with you outside of its final report. Therefore, your prompt should contain a highly detailed task description for the agent to perform autonomously and you should specify exactly what information the agent should return back to you in its final and only message to you.\n- Agents with \"access to current context\" can see the full conversation history before the tool call. When using these agents, you can write concise prompts that reference earlier context (e.g., \"investigate the error discussed above\") instead of repeating information. The agent will receive all prior messages and understand the context.\n- The agent's outputs should generally be trusted\n- Clearly tell the agent whether you expect it to write code or just to do research (search, file reads, web fetches, etc.), since it is not aware of the user's intent\n- If the agent description mentions that it should be used proactively, then you should try your best to use it without the user having to ask for it first. Use your judgement.\n- If the user specifies that they want you to run agents \"in parallel\", you MUST send a single message with multiple Task tool use content blocks. For example, if you need to launch both a code-reviewer agent and a test-runner agent in parallel, send a single message with both tool calls.\n\nExample usage:\n\n\n\"code-reviewer\": use this agent after you are done writing a signficant piece of code\n\"greeting-responder\": use this agent when to respond to user greetings with a friendly joke\n\n\n\nuser: \"Please write a function that checks if a number is prime\"\nassistant: Sure let me write a function that checks if a number is prime\nassistant: First let me use the Write tool to write a function that checks if a number is prime\nassistant: I'm going to use the Write tool to write the following code:\n\nfunction isPrime(n) {\n if (n <= 1) return false\n for (let i = 2; i * i <= n; i++) {\n if (n % i === 0) return false\n }\n return true\n}\n\n\nSince a signficant piece of code was written and the task was completed, now use the code-reviewer agent to review the code\n\nassistant: Now let me use the code-reviewer agent to review the code\nassistant: Uses the Task tool to launch the code-reviewer agent \n\n\n\nuser: \"Hello\"\n\nSince the user is greeting, use the greeting-responder agent to respond with a friendly joke\n\nassistant: \"I'm going to use the Task tool to launch the greeting-responder agent\"\n\n","input_schema":{"type":"object","properties":{"description":{"type":"string","description":"A short (3-5 word) description of the task"},"prompt":{"type":"string","description":"The task for the agent to perform"},"subagent_type":{"type":"string","description":"The type of specialized agent to use for this task"},"model":{"type":"string","enum":["sonnet","opus","haiku"],"description":"Optional model to use for this agent. If not specified, inherits from parent. Prefer haiku for quick, straightforward tasks to minimize cost and latency."},"resume":{"type":"string","description":"Optional agent ID to resume from. If provided, the agent will continue from the previous execution transcript."}},"required":["description","prompt","subagent_type"],"additionalProperties":false,"$schema":"http://json-schema.org/draft-07/schema#"}},{"name":"Bash","description":"Executes a given bash command in a persistent shell session with optional timeout, ensuring proper handling and security measures.\n\nIMPORTANT: This tool is for terminal operations like git, npm, docker, etc. DO NOT use it for file operations (reading, writing, editing, searching, finding files) - use the specialized tools for this instead.\n\nBefore executing the command, please follow these steps:\n\n1. Directory Verification:\n - If the command will create new directories or files, first use `ls` to verify the parent directory exists and is the correct location\n - For example, before running \"mkdir foo/bar\", first use `ls foo` to check that \"foo\" exists and is the intended parent directory\n\n2. Command Execution:\n - Always quote file paths that contain spaces with double quotes (e.g., cd \"path with spaces/file.txt\")\n - Examples of proper quoting:\n - cd \"/Users/name/My Documents\" (correct)\n - cd /Users/name/My Documents (incorrect - will fail)\n - python \"/path/with spaces/script.py\" (correct)\n - python /path/with spaces/script.py (incorrect - will fail)\n - After ensuring proper quoting, execute the command.\n - Capture the output of the command.\n\nUsage notes:\n - The command argument is required.\n - You can specify an optional timeout in milliseconds (up to 600000ms / 10 minutes). If not specified, commands will timeout after 120000ms (2 minutes).\n - It is very helpful if you write a clear, concise description of what this command does in 5-10 words.\n - If the output exceeds 30000 characters, output will be truncated before being returned to you.\n - You can use the `run_in_background` parameter to run the command in the background, which allows you to continue working while the command runs. You can monitor the output using the Bash tool as it becomes available. You do not need to use '&' at the end of the command when using this parameter.\n \n - Avoid using Bash with the `find`, `grep`, `cat`, `head`, `tail`, `sed`, `awk`, or `echo` commands, unless explicitly instructed or when these commands are truly necessary for the task. Instead, always prefer using the dedicated tools for these commands:\n - File search: Use Glob (NOT find or ls)\n - Content search: Use Grep (NOT grep or rg)\n - Read files: Use Read (NOT cat/head/tail)\n - Edit files: Use Edit (NOT sed/awk)\n - Write files: Use Write (NOT echo >/cat <\n pytest /foo/bar/tests\n \n \n cd /foo/bar && pytest tests\n \n\n# Committing changes with git\n\nOnly create commits when requested by the user. If unclear, ask first. When the user asks you to create a new git commit, follow these steps carefully:\n\nGit Safety Protocol:\n- NEVER update the git config\n- NEVER run destructive/irreversible git commands (like push --force, hard reset, etc) unless the user explicitly requests them \n- NEVER skip hooks (--no-verify, --no-gpg-sign, etc) unless the user explicitly requests it\n- NEVER run force push to main/master, warn the user if they request it\n- Avoid git commit --amend. ONLY use --amend when either (1) user explicitly requested amend OR (2) adding edits from pre-commit hook (additional instructions below) \n- Before amending: ALWAYS check authorship (git log -1 --format='%an %ae')\n- NEVER commit changes unless the user explicitly asks you to. It is VERY IMPORTANT to only commit when explicitly asked, otherwise the user will feel that you are being too proactive.\n\n1. You can call multiple tools in a single response. When multiple independent pieces of information are requested and all commands are likely to succeed, run multiple tool calls in parallel for optimal performance. run the following bash commands in parallel, each using the Bash tool:\n - Run a git status command to see all untracked files.\n - Run a git diff command to see both staged and unstaged changes that will be committed.\n - Run a git log command to see recent commit messages, so that you can follow this repository's commit message style.\n2. Analyze all staged changes (both previously staged and newly added) and draft a commit message:\n - Summarize the nature of the changes (eg. new feature, enhancement to an existing feature, bug fix, refactoring, test, docs, etc.). Ensure the message accurately reflects the changes and their purpose (i.e. \"add\" means a wholly new feature, \"update\" means an enhancement to an existing feature, \"fix\" means a bug fix, etc.).\n - Do not commit files that likely contain secrets (.env, credentials.json, etc). Warn the user if they specifically request to commit those files\n - Draft a concise (1-2 sentences) commit message that focuses on the \"why\" rather than the \"what\"\n - Ensure it accurately reflects the changes and their purpose\n3. You can call multiple tools in a single response. When multiple independent pieces of information are requested and all commands are likely to succeed, run multiple tool calls in parallel for optimal performance. run the following commands:\n - Add relevant untracked files to the staging area.\n - Create the commit with a message ending with:\n 🤖 Generated with [Claude Code](https://claude.com/claude-code)\n\n Co-Authored-By: Claude \n - Run git status after the commit completes to verify success.\n Note: git status depends on the commit completing, so run it sequentially after the commit.\n4. If the commit fails due to pre-commit hook changes, retry ONCE. If it succeeds but files were modified by the hook, verify it's safe to amend:\n - Check authorship: git log -1 --format='%an %ae'\n - Check not pushed: git status shows \"Your branch is ahead\"\n - If both true: amend your commit. Otherwise: create NEW commit (never amend other developers' commits)\n\nImportant notes:\n- NEVER run additional commands to read or explore code, besides git bash commands\n- NEVER use the TodoWrite or Task tools\n- DO NOT push to the remote repository unless the user explicitly asks you to do so\n- IMPORTANT: Never use git commands with the -i flag (like git rebase -i or git add -i) since they require interactive input which is not supported.\n- If there are no changes to commit (i.e., no untracked files and no modifications), do not create an empty commit\n- In order to ensure good formatting, ALWAYS pass the commit message via a HEREDOC, a la this example:\n\ngit commit -m \"$(cat <<'EOF'\n Commit message here.\n\n 🤖 Generated with [Claude Code](https://claude.com/claude-code)\n\n Co-Authored-By: Claude \n EOF\n )\"\n\n\n# Creating pull requests\nUse the gh command via the Bash tool for ALL GitHub-related tasks including working with issues, pull requests, checks, and releases. If given a Github URL use the gh command to get the information needed.\n\nIMPORTANT: When the user asks you to create a pull request, follow these steps carefully:\n\n1. You can call multiple tools in a single response. When multiple independent pieces of information are requested and all commands are likely to succeed, run multiple tool calls in parallel for optimal performance. run the following bash commands in parallel using the Bash tool, in order to understand the current state of the branch since it diverged from the main branch:\n - Run a git status command to see all untracked files\n - Run a git diff command to see both staged and unstaged changes that will be committed\n - Check if the current branch tracks a remote branch and is up to date with the remote, so you know if you need to push to the remote\n - Run a git log command and `git diff [base-branch]...HEAD` to understand the full commit history for the current branch (from the time it diverged from the base branch)\n2. Analyze all changes that will be included in the pull request, making sure to look at all relevant commits (NOT just the latest commit, but ALL commits that will be included in the pull request!!!), and draft a pull request summary\n3. You can call multiple tools in a single response. When multiple independent pieces of information are requested and all commands are likely to succeed, run multiple tool calls in parallel for optimal performance. run the following commands in parallel:\n - Create new branch if needed\n - Push to remote with -u flag if needed\n - Create PR using gh pr create with the format below. Use a HEREDOC to pass the body to ensure correct formatting.\n\ngh pr create --title \"the pr title\" --body \"$(cat <<'EOF'\n## Summary\n<1-3 bullet points>\n\n## Test plan\n[Bulleted markdown checklist of TODOs for testing the pull request...]\n\n🤖 Generated with [Claude Code](https://claude.com/claude-code)\nEOF\n)\"\n\n\nImportant:\n- DO NOT use the TodoWrite or Task tools\n- Return the PR URL when you're done, so the user can see it\n\n# Other common operations\n- View comments on a Github PR: gh api repos/foo/bar/pulls/123/comments","input_schema":{"type":"object","properties":{"command":{"type":"string","description":"The command to execute"},"timeout":{"type":"number","description":"Optional timeout in milliseconds (max 600000)"},"description":{"type":"string","description":"Clear, concise description of what this command does in 5-10 words, in active voice. Examples:\nInput: ls\nOutput: List files in current directory\n\nInput: git status\nOutput: Show working tree status\n\nInput: npm install\nOutput: Install package dependencies\n\nInput: mkdir foo\nOutput: Create directory 'foo'"},"run_in_background":{"type":"boolean","description":"Set to true to run this command in the background. Use BashOutput to read the output later."},"dangerouslyDisableSandbox":{"type":"boolean","description":"Set this to true to dangerously override sandbox mode and run commands without sandboxing."}},"required":["command"],"additionalProperties":false,"$schema":"http://json-schema.org/draft-07/schema#"}},{"name":"Glob","description":"- Fast file pattern matching tool that works with any codebase size\n- Supports glob patterns like \"**/*.js\" or \"src/**/*.ts\"\n- Returns matching file paths sorted by modification time\n- Use this tool when you need to find files by name patterns\n- When you are doing an open ended search that may require multiple rounds of globbing and grepping, use the Agent tool instead\n- You can call multiple tools in a single response. It is always better to speculatively perform multiple searches in parallel if they are potentially useful.","input_schema":{"type":"object","properties":{"pattern":{"type":"string","description":"The glob pattern to match files against"},"path":{"type":"string","description":"The directory to search in. If not specified, the current working directory will be used. IMPORTANT: Omit this field to use the default directory. DO NOT enter \"undefined\" or \"null\" - simply omit it for the default behavior. Must be a valid directory path if provided."}},"required":["pattern"],"additionalProperties":false,"$schema":"http://json-schema.org/draft-07/schema#"}},{"name":"Grep","description":"A powerful search tool built on ripgrep\n\n Usage:\n - ALWAYS use Grep for search tasks. NEVER invoke `grep` or `rg` as a Bash command. The Grep tool has been optimized for correct permissions and access.\n - Supports full regex syntax (e.g., \"log.*Error\", \"function\\s+\\w+\")\n - Filter files with glob parameter (e.g., \"*.js\", \"**/*.tsx\") or type parameter (e.g., \"js\", \"py\", \"rust\")\n - Output modes: \"content\" shows matching lines, \"files_with_matches\" shows only file paths (default), \"count\" shows match counts\n - Use Task tool for open-ended searches requiring multiple rounds\n - Pattern syntax: Uses ripgrep (not grep) - literal braces need escaping (use `interface\\{\\}` to find `interface{}` in Go code)\n - Multiline matching: By default patterns match within single lines only. For cross-line patterns like `struct \\{[\\s\\S]*?field`, use `multiline: true`\n","input_schema":{"type":"object","properties":{"pattern":{"type":"string","description":"The regular expression pattern to search for in file contents"},"path":{"type":"string","description":"File or directory to search in (rg PATH). Defaults to current working directory."},"glob":{"type":"string","description":"Glob pattern to filter files (e.g. \"*.js\", \"*.{ts,tsx}\") - maps to rg --glob"},"output_mode":{"type":"string","enum":["content","files_with_matches","count"],"description":"Output mode: \"content\" shows matching lines (supports -A/-B/-C context, -n line numbers, head_limit), \"files_with_matches\" shows file paths (supports head_limit), \"count\" shows match counts (supports head_limit). Defaults to \"files_with_matches\"."},"-B":{"type":"number","description":"Number of lines to show before each match (rg -B). Requires output_mode: \"content\", ignored otherwise."},"-A":{"type":"number","description":"Number of lines to show after each match (rg -A). Requires output_mode: \"content\", ignored otherwise."},"-C":{"type":"number","description":"Number of lines to show before and after each match (rg -C). Requires output_mode: \"content\", ignored otherwise."},"-n":{"type":"boolean","description":"Show line numbers in output (rg -n). Requires output_mode: \"content\", ignored otherwise. Defaults to true."},"-i":{"type":"boolean","description":"Case insensitive search (rg -i)"},"type":{"type":"string","description":"File type to search (rg --type). Common types: js, py, rust, go, java, etc. More efficient than include for standard file types."},"head_limit":{"type":"number","description":"Limit output to first N lines/entries, equivalent to \"| head -N\". Works across all output modes: content (limits output lines), files_with_matches (limits file paths), count (limits count entries). Defaults based on \"cap\" experiment value: 0 (unlimited), 20, or 100."},"offset":{"type":"number","description":"Skip first N lines/entries before applying head_limit, equivalent to \"| tail -n +N | head -N\". Works across all output modes. Defaults to 0."},"multiline":{"type":"boolean","description":"Enable multiline mode where . matches newlines and patterns can span lines (rg -U --multiline-dotall). Default: false."}},"required":["pattern"],"additionalProperties":false,"$schema":"http://json-schema.org/draft-07/schema#"}},{"name":"ExitPlanMode","description":"Use this tool when you are in plan mode and have finished presenting your plan and are ready to code. This will prompt the user to exit plan mode.\nIMPORTANT: Only use this tool when the task requires planning the implementation steps of a task that requires writing code. For research tasks where you're gathering information, searching files, reading files or in general trying to understand the codebase - do NOT use this tool.\n\n## Handling Ambiguity in Plans\nBefore using this tool, ensure your plan is clear and unambiguous. If there are multiple valid approaches or unclear requirements:\n1. Use the AskUserQuestion tool to clarify with the user\n2. Ask about specific implementation choices (e.g., architectural patterns, which library to use)\n3. Clarify any assumptions that could affect the implementation\n4. Only proceed with ExitPlanMode after resolving ambiguities\n\n## Examples\n\n1. Initial task: \"Search for and understand the implementation of vim mode in the codebase\" - Do not use the exit plan mode tool because you are not planning the implementation steps of a task.\n2. Initial task: \"Help me implement yank mode for vim\" - Use the exit plan mode tool after you have finished planning the implementation steps of the task.\n3. Initial task: \"Add a new feature to handle user authentication\" - If unsure about auth method (OAuth, JWT, etc.), use AskUserQuestion first, then use exit plan mode tool after clarifying the approach.\n","input_schema":{"type":"object","properties":{"plan":{"type":"string","description":"The plan you came up with, that you want to run by the user for approval. Supports markdown. The plan should be pretty concise."}},"required":["plan"],"additionalProperties":false,"$schema":"http://json-schema.org/draft-07/schema#"}},{"name":"Read","description":"Reads a file from the local filesystem. You can access any file directly by using this tool.\nAssume this tool is able to read all files on the machine. If the User provides a path to a file assume that path is valid. It is okay to read a file that does not exist; an error will be returned.\n\nUsage:\n- The file_path parameter must be an absolute path, not a relative path\n- By default, it reads up to 2000 lines starting from the beginning of the file\n- You can optionally specify a line offset and limit (especially handy for long files), but it's recommended to read the whole file by not providing these parameters\n- Any lines longer than 2000 characters will be truncated\n- Results are returned using cat -n format, with line numbers starting at 1\n- This tool allows Claude Code to read images (eg PNG, JPG, etc). When reading an image file the contents are presented visually as Claude Code is a multimodal LLM.\n- This tool can read PDF files (.pdf). PDFs are processed page by page, extracting both text and visual content for analysis.\n- This tool can read Jupyter notebooks (.ipynb files) and returns all cells with their outputs, combining code, text, and visualizations.\n- This tool can only read files, not directories. To read a directory, use an ls command via the Bash tool.\n- You can call multiple tools in a single response. It is always better to speculatively read multiple potentially useful files in parallel.\n- You will regularly be asked to read screenshots. If the user provides a path to a screenshot, ALWAYS use this tool to view the file at the path. This tool will work with all temporary file paths.\n- If you read a file that exists but has empty contents you will receive a system reminder warning in place of file contents.","input_schema":{"type":"object","properties":{"file_path":{"type":"string","description":"The absolute path to the file to read"},"offset":{"type":"number","description":"The line number to start reading from. Only provide if the file is too large to read at once"},"limit":{"type":"number","description":"The number of lines to read. Only provide if the file is too large to read at once."}},"required":["file_path"],"additionalProperties":false,"$schema":"http://json-schema.org/draft-07/schema#"}},{"name":"Edit","description":"Performs exact string replacements in files. \n\nUsage:\n- You must use your `Read` tool at least once in the conversation before editing. This tool will error if you attempt an edit without reading the file. \n- When editing text from Read tool output, ensure you preserve the exact indentation (tabs/spaces) as it appears AFTER the line number prefix. The line number prefix format is: spaces + line number + tab. Everything after that tab is the actual file content to match. Never include any part of the line number prefix in the old_string or new_string.\n- ALWAYS prefer editing existing files in the codebase. NEVER write new files unless explicitly required.\n- Only use emojis if the user explicitly requests it. Avoid adding emojis to files unless asked.\n- The edit will FAIL if `old_string` is not unique in the file. Either provide a larger string with more surrounding context to make it unique or use `replace_all` to change every instance of `old_string`. \n- Use `replace_all` for replacing and renaming strings across the file. This parameter is useful if you want to rename a variable for instance.","input_schema":{"type":"object","properties":{"file_path":{"type":"string","description":"The absolute path to the file to modify"},"old_string":{"type":"string","description":"The text to replace"},"new_string":{"type":"string","description":"The text to replace it with (must be different from old_string)"},"replace_all":{"type":"boolean","default":false,"description":"Replace all occurences of old_string (default false)"}},"required":["file_path","old_string","new_string"],"additionalProperties":false,"$schema":"http://json-schema.org/draft-07/schema#"}},{"name":"Write","description":"Writes a file to the local filesystem.\n\nUsage:\n- This tool will overwrite the existing file if there is one at the provided path.\n- If this is an existing file, you MUST use the Read tool first to read the file's contents. This tool will fail if you did not read the file first.\n- ALWAYS prefer editing existing files in the codebase. NEVER write new files unless explicitly required.\n- NEVER proactively create documentation files (*.md) or README files. Only create documentation files if explicitly requested by the User.\n- Only use emojis if the user explicitly requests it. Avoid writing emojis to files unless asked.","input_schema":{"type":"object","properties":{"file_path":{"type":"string","description":"The absolute path to the file to write (must be absolute, not relative)"},"content":{"type":"string","description":"The content to write to the file"}},"required":["file_path","content"],"additionalProperties":false,"$schema":"http://json-schema.org/draft-07/schema#"}},{"name":"NotebookEdit","description":"Completely replaces the contents of a specific cell in a Jupyter notebook (.ipynb file) with new source. Jupyter notebooks are interactive documents that combine code, text, and visualizations, commonly used for data analysis and scientific computing. The notebook_path parameter must be an absolute path, not a relative path. The cell_number is 0-indexed. Use edit_mode=insert to add a new cell at the index specified by cell_number. Use edit_mode=delete to delete the cell at the index specified by cell_number.","input_schema":{"type":"object","properties":{"notebook_path":{"type":"string","description":"The absolute path to the Jupyter notebook file to edit (must be absolute, not relative)"},"cell_id":{"type":"string","description":"The ID of the cell to edit. When inserting a new cell, the new cell will be inserted after the cell with this ID, or at the beginning if not specified."},"new_source":{"type":"string","description":"The new source for the cell"},"cell_type":{"type":"string","enum":["code","markdown"],"description":"The type of the cell (code or markdown). If not specified, it defaults to the current cell type. If using edit_mode=insert, this is required."},"edit_mode":{"type":"string","enum":["replace","insert","delete"],"description":"The type of edit to make (replace, insert, delete). Defaults to replace."}},"required":["notebook_path","new_source"],"additionalProperties":false,"$schema":"http://json-schema.org/draft-07/schema#"}},{"name":"WebFetch","description":"\n- Fetches content from a specified URL and processes it using an AI model\n- Takes a URL and a prompt as input\n- Fetches the URL content, converts HTML to markdown\n- Processes the content with the prompt using a small, fast model\n- Returns the model's response about the content\n- Use this tool when you need to retrieve and analyze web content\n\nUsage notes:\n - IMPORTANT: If an MCP-provided web fetch tool is available, prefer using that tool instead of this one, as it may have fewer restrictions. All MCP-provided tools start with \"mcp__\".\n - The URL must be a fully-formed valid URL\n - HTTP URLs will be automatically upgraded to HTTPS\n - The prompt should describe what information you want to extract from the page\n - This tool is read-only and does not modify any files\n - Results may be summarized if the content is very large\n - Includes a self-cleaning 15-minute cache for faster responses when repeatedly accessing the same URL\n - When a URL redirects to a different host, the tool will inform you and provide the redirect URL in a special format. You should then make a new WebFetch request with the redirect URL to fetch the content.\n","input_schema":{"type":"object","properties":{"url":{"type":"string","format":"uri","description":"The URL to fetch content from"},"prompt":{"type":"string","description":"The prompt to run on the fetched content"}},"required":["url","prompt"],"additionalProperties":false,"$schema":"http://json-schema.org/draft-07/schema#"}},{"name":"TodoWrite","description":"Use this tool to create and manage a structured task list for your current coding session. This helps you track progress, organize complex tasks, and demonstrate thoroughness to the user.\nIt also helps the user understand the progress of the task and overall progress of their requests.\n\n## When to Use This Tool\nUse this tool proactively in these scenarios:\n\n1. Complex multi-step tasks - When a task requires 3 or more distinct steps or actions\n2. Non-trivial and complex tasks - Tasks that require careful planning or multiple operations\n3. User explicitly requests todo list - When the user directly asks you to use the todo list\n4. User provides multiple tasks - When users provide a list of things to be done (numbered or comma-separated)\n5. After receiving new instructions - Immediately capture user requirements as todos\n6. When you start working on a task - Mark it as in_progress BEFORE beginning work. Ideally you should only have one todo as in_progress at a time\n7. After completing a task - Mark it as completed and add any new follow-up tasks discovered during implementation\n\n## When NOT to Use This Tool\n\nSkip using this tool when:\n1. There is only a single, straightforward task\n2. The task is trivial and tracking it provides no organizational benefit\n3. The task can be completed in less than 3 trivial steps\n4. The task is purely conversational or informational\n\nNOTE that you should not use this tool if there is only one trivial task to do. In this case you are better off just doing the task directly.\n\n## Examples of When to Use the Todo List\n\n\nUser: I want to add a dark mode toggle to the application settings. Make sure you run the tests and build when you're done!\nAssistant: I'll help add a dark mode toggle to your application settings. Let me create a todo list to track this implementation.\n*Creates todo list with the following items:*\n1. Creating dark mode toggle component in Settings page\n2. Adding dark mode state management (context/store)\n3. Implementing CSS-in-JS styles for dark theme\n4. Updating existing components to support theme switching\n5. Running tests and build process, addressing any failures or errors that occur\n*Begins working on the first task*\n\n\nThe assistant used the todo list because:\n1. Adding dark mode is a multi-step feature requiring UI, state management, and styling changes\n2. The user explicitly requested tests and build be run afterward\n3. The assistant inferred that tests and build need to pass by adding \"Ensure tests and build succeed\" as the final task\n\n\n\n\nUser: Help me rename the function getCwd to getCurrentWorkingDirectory across my project\nAssistant: Let me first search through your codebase to find all occurrences of 'getCwd'.\n*Uses grep or search tools to locate all instances of getCwd in the codebase*\nAssistant: I've found 15 instances of 'getCwd' across 8 different files. Let me create a todo list to track these changes.\n*Creates todo list with specific items for each file that needs updating*\n\n\nThe assistant used the todo list because:\n1. First, the assistant searched to understand the scope of the task\n2. Upon finding multiple occurrences across different files, it determined this was a complex task with multiple steps\n3. The todo list helps ensure every instance is tracked and updated systematically\n4. This approach prevents missing any occurrences and maintains code consistency\n\n\n\n\n\nUser: I need to implement these features for my e-commerce site: user registration, product catalog, shopping cart, and checkout flow.\nAssistant: I'll help implement these features. First, let's add all the features to the todo list.\n*Creates a todo list breaking down each feature into specific tasks based on the project architecture*\nAssistant: Let's start with user registration. This will involve creating database models, API endpoints, and frontend forms that integrate with your existing authentication system.\n\n\nThe assistant used the todo list because:\n1. The user provided multiple complex features to implement in a comma separated list\n2. The todo list helps organize these large features into manageable tasks\n3. This approach allows for tracking progress across the entire implementation\n\n\n\n\nUser: Can you help optimize my React application? It's rendering slowly and has performance issues.\nAssistant: I'll help optimize your React application. First, let me examine your codebase to identify potential performance bottlenecks.\n*Reviews component structure, render patterns, state management, and data fetching*\nAssistant: After analyzing your codebase, I've identified several performance issues. Let me create a todo list to track our optimization efforts.\n*Creates todo list with items like: 1) Implementing memoization for expensive calculations in ProductList, 2) Adding virtualization for long lists in Dashboard, 3) Optimizing image loading in Gallery component, 4) Fixing state update loops in ShoppingCart, 5) Reviewing bundle size and implementing code splitting*\nLet's start by implementing memoization for the expensive calculations in your ProductList component.\n\n\nThe assistant used the todo list because:\n1. First, the assistant examined the codebase to identify specific performance issues\n2. Based on this analysis, it identified multiple optimization opportunities\n3. Performance optimization is a non-trivial task requiring multiple steps\n4. The todo list helps methodically track improvements across different components\n5. This systematic approach ensures all performance bottlenecks are addressed\n\n\n\n## Examples of When NOT to Use the Todo List\n\n\nUser: How do I print 'Hello World' in Python?\nAssistant: In Python, you can print \"Hello World\" with this simple code:\n\npython\nprint(\"Hello World\")\n\nThis will output the text \"Hello World\" to the console when executed.\n\n\nThe assistant did not use the todo list because this is a single, trivial task that can be completed in one step. There's no need to track multiple tasks or steps for such a straightforward request.\n\n\n\n\nUser: What does the git status command do?\nAssistant: The git status command shows the current state of your working directory and staging area. It displays which changes have been staged, which haven't, and which files aren't being tracked by Git. This is a fundamental command for understanding the current state of your repository before making commits.\n\n\nThe assistant did not use the todo list because this is an informational request with no actual coding task to complete. The user is simply asking for an explanation, not for the assistant to perform multiple steps or tasks.\n\n\n\n\nUser: Can you add a comment to the calculateTotal function to explain what it does?\nAssistant: Sure, let me add a comment to the calculateTotal function to explain what it does.\n* Uses the Edit tool to add a comment to the calculateTotal function *\n\n\nThe assistant did not use the todo list because this is a single, straightforward task confined to one location in the code. Adding a comment doesn't require tracking multiple steps or systematic organization.\n\n\n\n\nUser: Run npm install for me and tell me what happens.\nAssistant: I'll run the npm install command for you.\n\n*Executes: npm install*\n\nThe command completed successfully. Here's the output:\n[Output of npm install command]\n\nAll dependencies have been installed according to your package.json file.\n\n\nThe assistant did not use the todo list because this is a single command execution with immediate results. There are no multiple steps to track or organize, making the todo list unnecessary for this straightforward task.\n\n\n\n## Task States and Management\n\n1. **Task States**: Use these states to track progress:\n - pending: Task not yet started\n - in_progress: Currently working on (limit to ONE task at a time)\n - completed: Task finished successfully\n\n **IMPORTANT**: Task descriptions must have two forms:\n - content: The imperative form describing what needs to be done (e.g., \"Run tests\", \"Build the project\")\n - activeForm: The present continuous form shown during execution (e.g., \"Running tests\", \"Building the project\")\n\n2. **Task Management**:\n - Update task status in real-time as you work\n - Mark tasks complete IMMEDIATELY after finishing (don't batch completions)\n - Exactly ONE task must be in_progress at any time (not less, not more)\n - Complete current tasks before starting new ones\n - Remove tasks that are no longer relevant from the list entirely\n\n3. **Task Completion Requirements**:\n - ONLY mark a task as completed when you have FULLY accomplished it\n - If you encounter errors, blockers, or cannot finish, keep the task as in_progress\n - When blocked, create a new task describing what needs to be resolved\n - Never mark a task as completed if:\n - Tests are failing\n - Implementation is partial\n - You encountered unresolved errors\n - You couldn't find necessary files or dependencies\n\n4. **Task Breakdown**:\n - Create specific, actionable items\n - Break complex tasks into smaller, manageable steps\n - Use clear, descriptive task names\n - Always provide both forms:\n - content: \"Fix authentication bug\"\n - activeForm: \"Fixing authentication bug\"\n\nWhen in doubt, use this tool. Being proactive with task management demonstrates attentiveness and ensures you complete all requirements successfully.\n","input_schema":{"type":"object","properties":{"todos":{"type":"array","items":{"type":"object","properties":{"content":{"type":"string","minLength":1},"status":{"type":"string","enum":["pending","in_progress","completed"]},"activeForm":{"type":"string","minLength":1}},"required":["content","status","activeForm"],"additionalProperties":false},"description":"The updated todo list"}},"required":["todos"],"additionalProperties":false,"$schema":"http://json-schema.org/draft-07/schema#"}},{"name":"WebSearch","description":"\n- Allows Claude to search the web and use the results to inform responses\n- Provides up-to-date information for current events and recent data\n- Returns search result information formatted as search result blocks\n- Use this tool for accessing information beyond Claude's knowledge cutoff\n- Searches are performed automatically within a single API call\n\nUsage notes:\n - Domain filtering is supported to include or block specific websites\n - Web search is only available in the US\n - Account for \"Today's date\" in . For example, if says \"Today's date: 2025-07-01\", and the user wants the latest docs, do not use 2024 in the search query. Use 2025.\n","input_schema":{"type":"object","properties":{"query":{"type":"string","minLength":2,"description":"The search query to use"},"allowed_domains":{"type":"array","items":{"type":"string"},"description":"Only include search results from these domains"},"blocked_domains":{"type":"array","items":{"type":"string"},"description":"Never include search results from these domains"}},"required":["query"],"additionalProperties":false,"$schema":"http://json-schema.org/draft-07/schema#"}},{"name":"BashOutput","description":"\n- Retrieves output from a running or completed background bash shell\n- Takes a shell_id parameter identifying the shell\n- Always returns only new output since the last check\n- Returns stdout and stderr output along with shell status\n- Supports optional regex filtering to show only lines matching a pattern\n- Use this tool when you need to monitor or check the output of a long-running shell\n- Shell IDs can be found using the /bashes command\n","input_schema":{"type":"object","properties":{"bash_id":{"type":"string","description":"The ID of the background shell to retrieve output from"},"filter":{"type":"string","description":"Optional regular expression to filter the output lines. Only lines matching this regex will be included in the result. Any lines that do not match will no longer be available to read."}},"required":["bash_id"],"additionalProperties":false,"$schema":"http://json-schema.org/draft-07/schema#"}},{"name":"KillShell","description":"\n- Kills a running background bash shell by its ID\n- Takes a shell_id parameter identifying the shell to kill\n- Returns a success or failure status \n- Use this tool when you need to terminate a long-running shell\n- Shell IDs can be found using the /bashes command\n","input_schema":{"type":"object","properties":{"shell_id":{"type":"string","description":"The ID of the background shell to kill"}},"required":["shell_id"],"additionalProperties":false,"$schema":"http://json-schema.org/draft-07/schema#"}},{"name":"Skill","description":"Execute a skill within the main conversation\n\n\nWhen users ask you to perform tasks, check if any of the available skills below can help complete the task more effectively. Skills provide specialized capabilities and domain knowledge.\n\nHow to use skills:\n- Invoke skills using this tool with the skill name only (no arguments)\n- When you invoke a skill, you will see The \"{name}\" skill is loading\n- The skill's prompt will expand and provide detailed instructions on how to complete the task\n- Examples:\n - `command: \"pdf\"` - invoke the pdf skill\n - `command: \"xlsx\"` - invoke the xlsx skill\n - `command: \"ms-office-suite:pdf\"` - invoke using fully qualified name\n\nImportant:\n- Only use skills listed in below\n- Do not invoke a skill that is already running\n- Do not use this tool for built-in CLI commands (like /help, /clear, etc.)\n\n\n\n\n\n","input_schema":{"type":"object","properties":{"command":{"type":"string","description":"The skill name (no arguments). E.g., \"pdf\" or \"xlsx\""}},"required":["command"],"additionalProperties":false,"$schema":"http://json-schema.org/draft-07/schema#"}},{"name":"SlashCommand","description":"Execute a slash command within the main conversation\n\n**IMPORTANT - Intent Matching:**\nBefore starting any task, CHECK if the user's request matches one of the slash commands listed below. This tool exists to route user intentions to specialized workflows.\n\nHow slash commands work:\nWhen you use this tool or when a user types a slash command, you will see {name} is running… followed by the expanded prompt. For example, if .claude/commands/foo.md contains \"Print today's date\", then /foo expands to that prompt in the next message.\n\nUsage:\n- `command` (required): The slash command to execute, including any arguments\n- Example: `command: \"/review-pr 123\"`\n\nIMPORTANT: Only use this tool for custom slash commands that appear in the Available Commands list below. Do NOT use for:\n- Built-in CLI commands (like /help, /clear, etc.)\n- Commands not shown in the list\n- Commands you think might exist but aren't listed\n\nNotes:\n- When a user requests multiple slash commands, execute each one sequentially and check for {name} is running… to verify each has been processed\n- Do not invoke a command that is already running. For example, if you see foo is running…, do NOT use this tool with \"/foo\" - process the expanded prompt in the following message\n- Only custom slash commands with descriptions are listed in Available Commands. If a user's command is not listed, ask them to check the slash command file and consult the docs.\n","input_schema":{"type":"object","properties":{"command":{"type":"string","description":"The slash command to execute with its arguments, e.g., \"/review-pr 123\""}},"required":["command"],"additionalProperties":false,"$schema":"http://json-schema.org/draft-07/schema#"}}],"metadata":{"user_id":"user_f432cd46d7f1eb89e6257e31d6af7ba2a1daa1af69a817ea1784e5c15a24edf5_account__session_ed2d8ac7-3aba-4db0-a755-ee9ce121461e"}} \ No newline at end of file diff --git a/src/example/claude/responseChunk1762590717239.json b/src/example/claude/responseChunk1762590717239.json new file mode 100644 index 0000000..e0f1a53 --- /dev/null +++ b/src/example/claude/responseChunk1762590717239.json @@ -0,0 +1,58 @@ +{"response":{"background":false,"created_at":1762590715,"error":null,"id":"resp_07c6fd8a69b421a201690efffb224481929d66a0b705232d92","incomplete_details":null,"max_output_tokens":null,"max_tool_calls":null,"metadata":{},"model":"gpt-5-codex-high","object":"response","output":[],"parallel_tool_calls":false,"previous_response_id":null,"prompt_cache_key":"5665999a-8a9a-46d0-ac6d-8fb343e44e78","prompt_cache_retention":null,"reasoning":{"effort":"high","summary":"detailed"},"safety_identifier":"user-GsmbkQMEAtzSBhl7dDHLAZjA","service_tier":"auto","status":"in_progress","store":false,"temperature":1,"text":{"format":{"type":"text"},"verbosity":"medium"},"tool_choice":"auto","tools":[{"description":"Runs a shell command and returns its output.","name":"shell","parameters":{"additionalProperties":false,"properties":{"command":{"description":"The command to execute","items":{"type":"string"},"type":"array"},"justification":{"description":"Only set if with_escalated_permissions is true. 1-sentence explanation of why we want to run this command.","type":"string"},"timeout_ms":{"description":"The timeout for the command in milliseconds","type":"number"},"with_escalated_permissions":{"description":"Whether to request escalated permissions. Set to true if command needs to be run without sandbox restrictions","type":"boolean"},"workdir":{"description":"The working directory to execute the command in","type":"string"}},"required":["command"],"type":"object"},"strict":false,"type":"function"},{"description":"Lists resources provided by MCP servers. Resources allow servers to share data that provides context to language models, such as files, database schemas, or application-specific information. Prefer resources over web search when possible.","name":"list_mcp_resources","parameters":{"additionalProperties":false,"properties":{"cursor":{"description":"Opaque cursor returned by a previous list_mcp_resources call for the same server.","type":"string"},"server":{"description":"Optional MCP server name. When omitted, lists resources from every configured server.","type":"string"}},"type":"object"},"strict":false,"type":"function"},{"description":"Lists resource templates provided by MCP servers. Parameterized resource templates allow servers to share data that takes parameters and provides context to language models, such as files, database schemas, or application-specific information. Prefer resource templates over web search when possible.","name":"list_mcp_resource_templates","parameters":{"additionalProperties":false,"properties":{"cursor":{"description":"Opaque cursor returned by a previous list_mcp_resource_templates call for the same server.","type":"string"},"server":{"description":"Optional MCP server name. When omitted, lists resource templates from all configured servers.","type":"string"}},"type":"object"},"strict":false,"type":"function"},{"description":"Read a specific resource from an MCP server given the server name and resource URI.","name":"read_mcp_resource","parameters":{"additionalProperties":false,"properties":{"server":{"description":"MCP server name exactly as configured. Must match the 'server' field returned by list_mcp_resources.","type":"string"},"uri":{"description":"Resource URI to read. Must be one of the URIs returned by list_mcp_resources.","type":"string"}},"required":["server","uri"],"type":"object"},"strict":false,"type":"function"},{"description":"Updates the task plan.\nProvide an optional explanation and a list of plan items, each with a step and status.\nAt most one step can be in_progress at a time.\n","name":"update_plan","parameters":{"additionalProperties":false,"properties":{"explanation":{"type":"string"},"plan":{"description":"The list of steps","items":{"additionalProperties":false,"properties":{"status":{"description":"One of: pending, in_progress, completed","type":"string"},"step":{"type":"string"}},"required":["step","status"],"type":"object"},"type":"array"}},"required":["plan"],"type":"object"},"strict":false,"type":"function"},{"description":"Attach a local image (by filesystem path) to the conversation context for this turn.","name":"view_image","parameters":{"additionalProperties":false,"properties":{"path":{"description":"Local filesystem path to an image file","type":"string"}},"required":["path"],"type":"object"},"strict":false,"type":"function"},{"description":"Use the `apply_patch` tool to edit files. This is a FREEFORM tool, so do not wrap the patch in JSON.","format":{"definition":"start: begin_patch hunk+ end_patch\r\nbegin_patch: \"*** Begin Patch\" LF\r\nend_patch: \"*** End Patch\" LF?\r\n\r\nhunk: add_hunk | delete_hunk | update_hunk\r\nadd_hunk: \"*** Add File: \" filename LF add_line+\r\ndelete_hunk: \"*** Delete File: \" filename LF\r\nupdate_hunk: \"*** Update File: \" filename LF change_move? change?\r\n\r\nfilename: /(.+)/\r\nadd_line: \"+\" /(.*)/ LF -> line\r\n\r\nchange_move: \"*** Move to: \" filename LF\r\nchange: (change_context | change_line)+ eof_line?\r\nchange_context: (\"@@\" | \"@@ \" /(.+)/) LF\r\nchange_line: (\"+\" | \"-\" | \" \") /(.*)/ LF\r\neof_line: \"*** End of File\" LF\r\n\r\n%import common.LF\r\n","syntax":"lark","type":"grammar"},"name":"apply_patch","type":"custom"}],"top_logprobs":0,"top_p":1,"truncation":"disabled","usage":null,"user":null},"sequence_number":0,"type":"response.created"} + +{"response":{"background":false,"created_at":1762590715,"error":null,"id":"resp_07c6fd8a69b421a201690efffb224481929d66a0b705232d92","incomplete_details":null,"max_output_tokens":null,"max_tool_calls":null,"metadata":{},"model":"gpt-5-codex-high","object":"response","output":[],"parallel_tool_calls":false,"previous_response_id":null,"prompt_cache_key":"5665999a-8a9a-46d0-ac6d-8fb343e44e78","prompt_cache_retention":null,"reasoning":{"effort":"high","summary":"detailed"},"safety_identifier":"user-GsmbkQMEAtzSBhl7dDHLAZjA","service_tier":"auto","status":"in_progress","store":false,"temperature":1,"text":{"format":{"type":"text"},"verbosity":"medium"},"tool_choice":"auto","tools":[{"description":"Runs a shell command and returns its output.","name":"shell","parameters":{"additionalProperties":false,"properties":{"command":{"description":"The command to execute","items":{"type":"string"},"type":"array"},"justification":{"description":"Only set if with_escalated_permissions is true. 1-sentence explanation of why we want to run this command.","type":"string"},"timeout_ms":{"description":"The timeout for the command in milliseconds","type":"number"},"with_escalated_permissions":{"description":"Whether to request escalated permissions. Set to true if command needs to be run without sandbox restrictions","type":"boolean"},"workdir":{"description":"The working directory to execute the command in","type":"string"}},"required":["command"],"type":"object"},"strict":false,"type":"function"},{"description":"Lists resources provided by MCP servers. Resources allow servers to share data that provides context to language models, such as files, database schemas, or application-specific information. Prefer resources over web search when possible.","name":"list_mcp_resources","parameters":{"additionalProperties":false,"properties":{"cursor":{"description":"Opaque cursor returned by a previous list_mcp_resources call for the same server.","type":"string"},"server":{"description":"Optional MCP server name. When omitted, lists resources from every configured server.","type":"string"}},"type":"object"},"strict":false,"type":"function"},{"description":"Lists resource templates provided by MCP servers. Parameterized resource templates allow servers to share data that takes parameters and provides context to language models, such as files, database schemas, or application-specific information. Prefer resource templates over web search when possible.","name":"list_mcp_resource_templates","parameters":{"additionalProperties":false,"properties":{"cursor":{"description":"Opaque cursor returned by a previous list_mcp_resource_templates call for the same server.","type":"string"},"server":{"description":"Optional MCP server name. When omitted, lists resource templates from all configured servers.","type":"string"}},"type":"object"},"strict":false,"type":"function"},{"description":"Read a specific resource from an MCP server given the server name and resource URI.","name":"read_mcp_resource","parameters":{"additionalProperties":false,"properties":{"server":{"description":"MCP server name exactly as configured. Must match the 'server' field returned by list_mcp_resources.","type":"string"},"uri":{"description":"Resource URI to read. Must be one of the URIs returned by list_mcp_resources.","type":"string"}},"required":["server","uri"],"type":"object"},"strict":false,"type":"function"},{"description":"Updates the task plan.\nProvide an optional explanation and a list of plan items, each with a step and status.\nAt most one step can be in_progress at a time.\n","name":"update_plan","parameters":{"additionalProperties":false,"properties":{"explanation":{"type":"string"},"plan":{"description":"The list of steps","items":{"additionalProperties":false,"properties":{"status":{"description":"One of: pending, in_progress, completed","type":"string"},"step":{"type":"string"}},"required":["step","status"],"type":"object"},"type":"array"}},"required":["plan"],"type":"object"},"strict":false,"type":"function"},{"description":"Attach a local image (by filesystem path) to the conversation context for this turn.","name":"view_image","parameters":{"additionalProperties":false,"properties":{"path":{"description":"Local filesystem path to an image file","type":"string"}},"required":["path"],"type":"object"},"strict":false,"type":"function"},{"description":"Use the `apply_patch` tool to edit files. This is a FREEFORM tool, so do not wrap the patch in JSON.","format":{"definition":"start: begin_patch hunk+ end_patch\r\nbegin_patch: \"*** Begin Patch\" LF\r\nend_patch: \"*** End Patch\" LF?\r\n\r\nhunk: add_hunk | delete_hunk | update_hunk\r\nadd_hunk: \"*** Add File: \" filename LF add_line+\r\ndelete_hunk: \"*** Delete File: \" filename LF\r\nupdate_hunk: \"*** Update File: \" filename LF change_move? change?\r\n\r\nfilename: /(.+)/\r\nadd_line: \"+\" /(.*)/ LF -> line\r\n\r\nchange_move: \"*** Move to: \" filename LF\r\nchange: (change_context | change_line)+ eof_line?\r\nchange_context: (\"@@\" | \"@@ \" /(.+)/) LF\r\nchange_line: (\"+\" | \"-\" | \" \") /(.*)/ LF\r\neof_line: \"*** End of File\" LF\r\n\r\n%import common.LF\r\n","syntax":"lark","type":"grammar"},"name":"apply_patch","type":"custom"}],"top_logprobs":0,"top_p":1,"truncation":"disabled","usage":null,"user":null},"sequence_number":1,"type":"response.in_progress"} + +{"item":{"encrypted_content":"gAAAAABpDv_7B0ZQOi7w5hs2rsMvjtlSC8JtM7o6_z0uQkHNsjHydfiJs5YXI2UfG1WNpjjZlyVhoA5vjp2CzOSXPak9Cbi7tOiR_5jYB5VgUugkTprqCkL2rF_OX1EnSwo6QxbZOsDpw950M9__eepLatbBBXQNAsGqUTIUENgMACtwqaP5WEenxriDOFc3M11eEL_KhKlGEH_JBEP-abUYnEqgtMBAGCb7oZtfsmtY2BHEOq0moy8HaNnGkp2hm105Cyiqh2mtBooo6IEw1Iadi31smpHAC12siqS09YLV_jPqhj5_wr7YH0ebcHJ9xh5VwwGmSeHoFdVtsGlbBg_9fTLHIQHit5YbZD2SqaPx56Bg4BfqgyARnvNB5H1i7jFxV_dVwGcj_dR_ahG8HmiNUHK4YtbtHux8p_ZQVCgXCUKWxq0v0fWaNnSsrrCxGgQhoB80KDaU5TrilvolF93UAi6-zl7AO2mf-Xo5mAdNh31-ZYeug6zREgoUAVd4ANMu8sa0zhEjZYh-CHvk5N1iDzddZFar5HrxXWT0lOqckllYLa_rPssih-e1eMnK8vzuWAt6eQpZQMcTe3jegPKZHFkU-W_u_xvee7LKZQTKIm3KXkYk0KrJ_vxr4Tmy5DN483vqhBUV8IL3qUgWjj_3H0C0__JDWnjbaUvHNpcObzJ9dE-_CIFjvAkuSkT67kfzA1SmfwmsQTfVCACysDFpaMyQmiBy2pnUWQmZM9VtijazZrIOHoLZSQN78idcPy4TcBHqq4TK7DLMzkAIYr0MBhxWAPnFaLqC8vqL1nwj8FoWp8zW4oSGkKN-0CgKK1mJij2hfED37qEo7OqGiVPHUh6Q4YBpbA==","id":"rs_07c6fd8a69b421a201690efffb9b008192baf1597d72b14a80","summary":[],"type":"reasoning"},"output_index":0,"sequence_number":2,"type":"response.output_item.added"} + +{"item_id":"rs_07c6fd8a69b421a201690efffb9b008192baf1597d72b14a80","output_index":0,"part":{"text":"","type":"summary_text"},"sequence_number":3,"summary_index":0,"type":"response.reasoning_summary_part.added"} + +{"delta":"**Preparing","item_id":"rs_07c6fd8a69b421a201690efffb9b008192baf1597d72b14a80","obfuscation":"LuwVs","output_index":0,"sequence_number":4,"summary_index":0,"type":"response.reasoning_summary_text.delta"} + +{"delta":" simple","item_id":"rs_07c6fd8a69b421a201690efffb9b008192baf1597d72b14a80","obfuscation":"CX3qlxPy3","output_index":0,"sequence_number":5,"summary_index":0,"type":"response.reasoning_summary_text.delta"} + +{"delta":" Chinese","item_id":"rs_07c6fd8a69b421a201690efffb9b008192baf1597d72b14a80","obfuscation":"LR9VamPT","output_index":0,"sequence_number":6,"summary_index":0,"type":"response.reasoning_summary_text.delta"} + +{"delta":" greeting","item_id":"rs_07c6fd8a69b421a201690efffb9b008192baf1597d72b14a80","obfuscation":"Fef9s1V","output_index":0,"sequence_number":7,"summary_index":0,"type":"response.reasoning_summary_text.delta"} + +{"delta":"**","item_id":"rs_07c6fd8a69b421a201690efffb9b008192baf1597d72b14a80","obfuscation":"PKbE7Wmk8E7zSi","output_index":0,"sequence_number":8,"summary_index":0,"type":"response.reasoning_summary_text.delta"} + +{"item_id":"rs_07c6fd8a69b421a201690efffb9b008192baf1597d72b14a80","output_index":0,"sequence_number":9,"summary_index":0,"text":"**Preparing simple Chinese greeting**","type":"response.reasoning_summary_text.done"} + +{"item_id":"rs_07c6fd8a69b421a201690efffb9b008192baf1597d72b14a80","output_index":0,"part":{"text":"**Preparing simple Chinese greeting**","type":"summary_text"},"sequence_number":10,"summary_index":0,"type":"response.reasoning_summary_part.done"} + +{"item":{"encrypted_content":"gAAAAABpDv_71uf0D8TS-yl4_IL1N3dEWfTsTcShwDWuoEEtNjQDcjdJdIXvVPXDvSTOciNae6soMwAmDZ3C8JMeFPIMVFzL5oq8FQEBZ8-Ao19hJMgAcyEm2eMb42JLMfwpD-HsedAWxGSCeghjeaOFZMBW1QDA69QlyL10q1ud8gnT8bxcFTJ5Pl4lEIaQf8efeHSCOpiZSyS8NRv0qijWZql-qWOAbtEgwA51x7gJFsMqvX1ju0hCnFzCweGBrelpZfytdtDUbLCUrZseuAnJE38t6LWOyWfHd22SpjF2_xwlNaMoC38gsP3pw9ZbLm4s4dNALspwkHysL2Kmng1A6j88VfkS6ENmxzfsGX9afJPyH1JoJBLkkFRmwDVL03KIe6ucOGwQk9nkBCvQVPWbjs0yH6U4MHbpxpthGuECZcYIfv2CQ0ehYJOXZgqPfg7w_gX7vnrQpwqACpDFYHw_RFjvQvuoOn7QkWZh6WkuQsbLr5G1fY07_vUYAIPVtMIIT9_RQ48JeE6keXIXPlifG1ZqJSXlIz2oBBNkty8JsgCtW4P8ShZIVHoUXB1r6XvbvA_3Cdd08aAN6l1h08qzYfSyErkWsalTs38A5NVaCJ238UbIWG4KaSnaUo6Jb2Ls1T43jGCJ22bI-YL2Me67m3lhMjInAUTrIt4HpE8jEWrOIwCwRymdTidmGZZkKcZ8XGNw9x6EpVUQKMuOgK8_UTueHhcjxfue2XKqaR-FCKgJE5r_fu0pCtKvXwf00ngL208bQWqFe8XsH_LbzkWpWSS30ILW9oVkP6S1nH7IByLzAh3h2BtSMafjwB5ipLwBNANOdKoOh5TvSc8sQxw8wzDauv93tSq6CV7D-aPhqA3uzW-fFDIOxQY-eSf-mId9sdTMbSVaUTNBZOyUJM0_GwnztC79SA==","id":"rs_07c6fd8a69b421a201690efffb9b008192baf1597d72b14a80","summary":[{"text":"**Preparing simple Chinese greeting**","type":"summary_text"}],"type":"reasoning"},"output_index":0,"sequence_number":11,"type":"response.output_item.done"} + +{"item":{"content":[],"id":"msg_07c6fd8a69b421a201690efffbdd9c8192b17e299d924a5b03","role":"assistant","status":"in_progress","type":"message"},"output_index":1,"sequence_number":12,"type":"response.output_item.added"} + +{"content_index":0,"item_id":"msg_07c6fd8a69b421a201690efffbdd9c8192b17e299d924a5b03","output_index":1,"part":{"annotations":[],"logprobs":[],"text":"","type":"output_text"},"sequence_number":13,"type":"response.content_part.added"} + +{"content_index":0,"delta":"你好","item_id":"msg_07c6fd8a69b421a201690efffbdd9c8192b17e299d924a5b03","logprobs":[],"obfuscation":"UeZmbDWcbgXcbF","output_index":1,"sequence_number":14,"type":"response.output_text.delta"} + +{"content_index":0,"delta":"!","item_id":"msg_07c6fd8a69b421a201690efffbdd9c8192b17e299d924a5b03","logprobs":[],"obfuscation":"PE85zXGyeIek9Bw","output_index":1,"sequence_number":15,"type":"response.output_text.delta"} + +{"content_index":0,"delta":"请","item_id":"msg_07c6fd8a69b421a201690efffbdd9c8192b17e299d924a5b03","logprobs":[],"obfuscation":"BvfTZ0adAZ7L3WU","output_index":1,"sequence_number":16,"type":"response.output_text.delta"} + +{"content_index":0,"delta":"问","item_id":"msg_07c6fd8a69b421a201690efffbdd9c8192b17e299d924a5b03","logprobs":[],"obfuscation":"AHlWs1aZGtagu6k","output_index":1,"sequence_number":17,"type":"response.output_text.delta"} + +{"content_index":0,"delta":"需要","item_id":"msg_07c6fd8a69b421a201690efffbdd9c8192b17e299d924a5b03","logprobs":[],"obfuscation":"FhglDnF827PG8n","output_index":1,"sequence_number":18,"type":"response.output_text.delta"} + +{"content_index":0,"delta":"我","item_id":"msg_07c6fd8a69b421a201690efffbdd9c8192b17e299d924a5b03","logprobs":[],"obfuscation":"CwICNtLo9B9elR4","output_index":1,"sequence_number":19,"type":"response.output_text.delta"} + +{"content_index":0,"delta":"帮","item_id":"msg_07c6fd8a69b421a201690efffbdd9c8192b17e299d924a5b03","logprobs":[],"obfuscation":"sXLo60L4IbenRYU","output_index":1,"sequence_number":20,"type":"response.output_text.delta"} + +{"content_index":0,"delta":"你","item_id":"msg_07c6fd8a69b421a201690efffbdd9c8192b17e299d924a5b03","logprobs":[],"obfuscation":"ssJWzZEJH1GEIaN","output_index":1,"sequence_number":21,"type":"response.output_text.delta"} + +{"content_index":0,"delta":"做","item_id":"msg_07c6fd8a69b421a201690efffbdd9c8192b17e299d924a5b03","logprobs":[],"obfuscation":"6bHDbgbj7afe4SR","output_index":1,"sequence_number":22,"type":"response.output_text.delta"} + +{"content_index":0,"delta":"什么","item_id":"msg_07c6fd8a69b421a201690efffbdd9c8192b17e299d924a5b03","logprobs":[],"obfuscation":"L6u1Cy1U8TyCpK","output_index":1,"sequence_number":23,"type":"response.output_text.delta"} + +{"content_index":0,"delta":"?","item_id":"msg_07c6fd8a69b421a201690efffbdd9c8192b17e299d924a5b03","logprobs":[],"obfuscation":"XaR7CwKxHDWWIRY","output_index":1,"sequence_number":24,"type":"response.output_text.delta"} + +{"content_index":0,"item_id":"msg_07c6fd8a69b421a201690efffbdd9c8192b17e299d924a5b03","logprobs":[],"output_index":1,"sequence_number":25,"text":"你好!请问需要我帮你做什么?","type":"response.output_text.done"} + +{"content_index":0,"item_id":"msg_07c6fd8a69b421a201690efffbdd9c8192b17e299d924a5b03","output_index":1,"part":{"annotations":[],"logprobs":[],"text":"你好!请问需要我帮你做什么?","type":"output_text"},"sequence_number":26,"type":"response.content_part.done"} + +{"item":{"content":[{"annotations":[],"logprobs":[],"text":"你好!请问需要我帮你做什么?","type":"output_text"}],"id":"msg_07c6fd8a69b421a201690efffbdd9c8192b17e299d924a5b03","role":"assistant","status":"completed","type":"message"},"output_index":1,"sequence_number":27,"type":"response.output_item.done"} + +{"response":{"background":false,"created_at":1762590715,"error":null,"id":"resp_07c6fd8a69b421a201690efffb224481929d66a0b705232d92","incomplete_details":null,"max_output_tokens":null,"max_tool_calls":null,"metadata":{},"model":"gpt-5-codex-high","object":"response","output":[{"encrypted_content":"gAAAAABpDv_7QuROvJ12LAYWxiL53VCslmPDAYdJ0aDGVYwCKbWAYZifAaTzrA1kkIE5r3YiUxQegU7TXzh06BBZvyRvt3Lggf3V1TUyi3RZRbZYhb7ZTUvMrsg-F-0OSIdxt4frDhrN5PtTRJjs8JrXsXZ6fhrwq5LnnSX7N--9tEgt4mWwU5gImyGFROY1Qjr-W093zgE8DJu-QsHEMuWCa-TDSaYPRXKnzQ7AydTqnEyVORLF92NpJSGhHyhwvN6r2npdxj1nUfYQWxneROhXMwFjwGOiV7bxnd3AG6RgI_31RfEy68fzy5yPOxJ4suJodmcAOmGx5olsc4mlKh9rtLjv_PRj1b-P5vpPE2HT-Miiy8Gc2aqs-mgfpk9sqJ-MrCA2HyYeMDfdszu6oP41UFKJsY6R2IncoU8hTQ7_xSEtq4kF-B5egJ6zCQWuA5FZNYwOswkZ1FxAX4w9rbgcPla1iiv5SFpj9__SJha79AW7tZndgKWoTIXTlGZf6lZsp_FFZuzAAPRE54f4ZxowmUEaM4xDw1Iosdbxh0QtMpYxnkKxxYc4zjq9zjxTYOIpZSq1mwEKHSBYtzkWLoZO3xd8jCJPv5dLBBEhSpvuDCMmsWKBz6WfasIUyzJl4Q4hIp0p6aCo_VcXfKIlhPlKBzvpvYk0fr3NNf18sa6MQEtjVCRE5csicsfCZ1-0Gw4Wl-a4F1Ph6ghCi6m9Q7Qp1oM9NsiTmqWxKC0V95xyeXHrM8ozK8BGcQ6HbXL8XWCMIhGq3_yMLKCF9ySMwOwpVwnqn2tCX3y87MmliOLzsESgVhgdH4g_a-0Wa8OEq46YcslBiasvOsCibT30q_Vl8AvVdrsBPTtG8vztNLkMjNmsfbomF8fTfPl03Z1NvKD7dUbJAMjr-Iexzr3cNb6dJEPMiHNmXA==","id":"rs_07c6fd8a69b421a201690efffb9b008192baf1597d72b14a80","summary":[{"text":"**Preparing simple Chinese greeting**","type":"summary_text"}],"type":"reasoning"},{"content":[{"annotations":[],"logprobs":[],"text":"你好!请问需要我帮你做什么?","type":"output_text"}],"id":"msg_07c6fd8a69b421a201690efffbdd9c8192b17e299d924a5b03","role":"assistant","status":"completed","type":"message"}],"parallel_tool_calls":false,"previous_response_id":null,"prompt_cache_key":"5665999a-8a9a-46d0-ac6d-8fb343e44e78","prompt_cache_retention":null,"reasoning":{"effort":"high","summary":"detailed"},"safety_identifier":"user-GsmbkQMEAtzSBhl7dDHLAZjA","service_tier":"default","status":"completed","store":false,"temperature":1,"text":{"format":{"type":"text"},"verbosity":"medium"},"tool_choice":"auto","tools":[{"description":"Runs a shell command and returns its output.","name":"shell","parameters":{"additionalProperties":false,"properties":{"command":{"description":"The command to execute","items":{"type":"string"},"type":"array"},"justification":{"description":"Only set if with_escalated_permissions is true. 1-sentence explanation of why we want to run this command.","type":"string"},"timeout_ms":{"description":"The timeout for the command in milliseconds","type":"number"},"with_escalated_permissions":{"description":"Whether to request escalated permissions. Set to true if command needs to be run without sandbox restrictions","type":"boolean"},"workdir":{"description":"The working directory to execute the command in","type":"string"}},"required":["command"],"type":"object"},"strict":false,"type":"function"},{"description":"Lists resources provided by MCP servers. Resources allow servers to share data that provides context to language models, such as files, database schemas, or application-specific information. Prefer resources over web search when possible.","name":"list_mcp_resources","parameters":{"additionalProperties":false,"properties":{"cursor":{"description":"Opaque cursor returned by a previous list_mcp_resources call for the same server.","type":"string"},"server":{"description":"Optional MCP server name. When omitted, lists resources from every configured server.","type":"string"}},"type":"object"},"strict":false,"type":"function"},{"description":"Lists resource templates provided by MCP servers. Parameterized resource templates allow servers to share data that takes parameters and provides context to language models, such as files, database schemas, or application-specific information. Prefer resource templates over web search when possible.","name":"list_mcp_resource_templates","parameters":{"additionalProperties":false,"properties":{"cursor":{"description":"Opaque cursor returned by a previous list_mcp_resource_templates call for the same server.","type":"string"},"server":{"description":"Optional MCP server name. When omitted, lists resource templates from all configured servers.","type":"string"}},"type":"object"},"strict":false,"type":"function"},{"description":"Read a specific resource from an MCP server given the server name and resource URI.","name":"read_mcp_resource","parameters":{"additionalProperties":false,"properties":{"server":{"description":"MCP server name exactly as configured. Must match the 'server' field returned by list_mcp_resources.","type":"string"},"uri":{"description":"Resource URI to read. Must be one of the URIs returned by list_mcp_resources.","type":"string"}},"required":["server","uri"],"type":"object"},"strict":false,"type":"function"},{"description":"Updates the task plan.\nProvide an optional explanation and a list of plan items, each with a step and status.\nAt most one step can be in_progress at a time.\n","name":"update_plan","parameters":{"additionalProperties":false,"properties":{"explanation":{"type":"string"},"plan":{"description":"The list of steps","items":{"additionalProperties":false,"properties":{"status":{"description":"One of: pending, in_progress, completed","type":"string"},"step":{"type":"string"}},"required":["step","status"],"type":"object"},"type":"array"}},"required":["plan"],"type":"object"},"strict":false,"type":"function"},{"description":"Attach a local image (by filesystem path) to the conversation context for this turn.","name":"view_image","parameters":{"additionalProperties":false,"properties":{"path":{"description":"Local filesystem path to an image file","type":"string"}},"required":["path"],"type":"object"},"strict":false,"type":"function"},{"description":"Use the `apply_patch` tool to edit files. This is a FREEFORM tool, so do not wrap the patch in JSON.","format":{"definition":"start: begin_patch hunk+ end_patch\r\nbegin_patch: \"*** Begin Patch\" LF\r\nend_patch: \"*** End Patch\" LF?\r\n\r\nhunk: add_hunk | delete_hunk | update_hunk\r\nadd_hunk: \"*** Add File: \" filename LF add_line+\r\ndelete_hunk: \"*** Delete File: \" filename LF\r\nupdate_hunk: \"*** Update File: \" filename LF change_move? change?\r\n\r\nfilename: /(.+)/\r\nadd_line: \"+\" /(.*)/ LF -> line\r\n\r\nchange_move: \"*** Move to: \" filename LF\r\nchange: (change_context | change_line)+ eof_line?\r\nchange_context: (\"@@\" | \"@@ \" /(.+)/) LF\r\nchange_line: (\"+\" | \"-\" | \" \") /(.*)/ LF\r\neof_line: \"*** End of File\" LF\r\n\r\n%import common.LF\r\n","syntax":"lark","type":"grammar"},"name":"apply_patch","type":"custom"}],"top_logprobs":0,"top_p":1,"truncation":"disabled","usage":{"input_tokens":3051,"input_tokens_details":{"cached_tokens":2048},"output_tokens":17,"output_tokens_details":{"reasoning_tokens":0},"total_tokens":3068},"user":null},"sequence_number":28,"type":"response.completed"} + diff --git a/src/example/gemini/oldResponse1762502386432.json b/src/example/gemini/oldResponse1762502386432.json new file mode 100644 index 0000000..8d9a782 --- /dev/null +++ b/src/example/gemini/oldResponse1762502386432.json @@ -0,0 +1 @@ +{"candidates":[{"content":{"role":"model","parts":[{"text":"好的,我为您整理并优化了一份可读性强的 Markdown 格式项目文档,文案风格力求专业、平易近人,并已将随机数纳入其中。\n\n---\n\n# 项目名称:[请在此处填写您的项目名称]\n\n## 概览\n\n本项目致力于提供一套[此处简要描述项目核心功能或解决的问题,例如:高效、稳定的数据处理方案 / 创新的前端组件库 / 自动化的部署工具]。我们注重性能、可靠性与用户体验,旨在为您简化工作流程,提升开发效率。\n\n## 核心特性\n\n* **简洁明了的架构**: 采用模块化设计,易于理解与维护。\n* **高效可靠的运算**: 核心算法经过优化,确保在多种场景下均能稳定快速响应。\n* **灵活的配置选项**: 提供丰富的配置接口,允许用户根据具体需求进行定制。\n* **[其他具体特性,例如:跨平台兼容性 / 友好的用户界面 / 完善的API文档]**\n\n## 快速上手\n\n### 1. 环境准备\n\n在开始使用本项目之前,请确保您的系统满足以下最低要求:\n\n* **操作系统**: Windows 10+, macOS 10.15+, Ubuntu 20.04+\n* **依赖软件**: [例如:Python 3.8+ / Node.js 14+ / Docker 20.10+]\n\n### 2. 安装指南\n\n请按照以下步骤安装本项目:\n\n1. **克隆仓库**:\n ```bash\n git clone https://github.com/your-username/your-project-name.git\n cd your-project-name\n ```\n2. **安装依赖**:\n ```bash\n # 例如,如果是Python项目\n pip install -r requirements.txt\n # 如果是Node.js项目\n npm install\n # 或者,如果需要构建\n npm run build\n ```\n\n### 3. 使用示例\n\n安装完成后,您可以尝试运行以下示例来验证项目功能:\n\n```bash\n# 例如,如果是命令行工具\npython main.py --help\n# 如果是启动服务\nnpm start\n# 或者调用SDK接口\n# import your_project\n# result = your_project.process_data(...)\n```\n[在此处添加更具体的代码示例或使用场景]\n\n## 贡献指南\n\n我们非常欢迎社区的贡献!如果您有兴趣改进本项目,请遵循以下步骤:\n\n1. **Fork 本仓库**: 将项目 Fork 到您的 GitHub 账户。\n2. **创建功能分支**: 从 `main` 分支拉出新分支,例如 `feature/add-new-function` 或 `bugfix/resolve-issue-123`。\n3. **提交代码**: 确保代码风格与项目保持一致,并编写清晰的提交信息。\n4. **提交 Pull Request**: 详细描述您所做的更改及原因。\n\n在提交 Pull Request 前,请务必运行测试以确保没有任何回归。\n\n## 许可证\n\n本项目采用 [请在此处填写许可证类型,例如:MIT License / Apache 2.0 License] 开放源代码许可。有关详细信息,请参阅项目根目录下的 `LICENSE` 文件。\n\n## 联系方式\n\n如果您在使用过程中遇到任何问题,欢迎通过以下方式与我们联系:\n\n* **GitHub Issues**: [https://github.com/your-username/your-project-name/issues](https://github.com/your-username/your-project-name/issues)\n* **电子邮件**: [your.email@example.com]\n\n## 附注随机标识\n\n本项目的特定生成标识为:`349187`\n\n---"}]},"finishReason":"STOP","avgLogprobs":-0.9484239548045448}],"usageMetadata":{"promptTokenCount":106,"candidatesTokenCount":821,"totalTokenCount":1674,"trafficType":"ON_DEMAND","promptTokensDetails":[{"modality":"TEXT","tokenCount":106}],"candidatesTokensDetails":[{"modality":"TEXT","tokenCount":821}],"thoughtsTokenCount":747}} \ No newline at end of file diff --git a/src/example/gemini/oldResponseChunk1762502078640.json b/src/example/gemini/oldResponseChunk1762502078640.json new file mode 100644 index 0000000..6d89a0d --- /dev/null +++ b/src/example/gemini/oldResponseChunk1762502078640.json @@ -0,0 +1,3 @@ +{"candidates":[{"content":{"role":"model","parts":[{"text":"\n请问有什么具体任务需要我协助完成吗?例如,您是需要我编写代码、修复错误、重构代码、回答问题,还是其他操作?\n<"}]}}],"usageMetadata":{"trafficType":"PROVISIONED_THROUGHPUT"}} +{"candidates":[{"content":{"role":"model","parts":[{"text":"follow_up>\n我需要你编写一个新功能。\n我需要你帮助我修复一个bug。\n我有一个关于代码的问题想问你。\n我"}]}}],"usageMetadata":{"trafficType":"PROVISIONED_THROUGHPUT"}} +{"candidates":[{"content":{"role":"model","parts":[{"text":"需要你检查一下我的代码。\n\n"}]},"finishReason":"STOP"}],"usageMetadata":{"promptTokenCount":19131,"candidatesTokenCount":127,"totalTokenCount":19351,"trafficType":"PROVISIONED_THROUGHPUT","promptTokensDetails":[{"modality":"TEXT","tokenCount":19131}],"candidatesTokensDetails":[{"modality":"TEXT","tokenCount":127}],"thoughtsTokenCount":93}} diff --git a/src/example/gemini/originalRequestBody1762502075001.json b/src/example/gemini/originalRequestBody1762502075001.json new file mode 100644 index 0000000..c7fcd51 --- /dev/null +++ b/src/example/gemini/originalRequestBody1762502075001.json @@ -0,0 +1 @@ +{"contents":[{"parts":[{"text":"\n你好\n"},{"text":"\n# VSCode Visible Files\nsrc\\converters\\ConverterFactory.js\n\n# VSCode Open Tabs\nsrc/claude/claude-kiro.js,src/openai/qwen-core.js,config.json,provider_pools.json,src/converters/ConverterFactory.js,src/gemini/gemini-core.js,src/provider-pool-manager.js\n\n# Current Time\nCurrent time in ISO 8601 UTC format: 2025-11-07T07:54:28.167Z\nUser time zone: Asia/Shanghai, UTC+8:00\n\n# Current Cost\n$0.00\n\n# Current Mode\ncode\nCode\ngemini-exp-1206\n\n\n# Current Workspace Directory (d:/ai/code/AIClient2API) Files\n.babelrc\n.dockerignore\n.gitignore\nARCHITECTURE_DESIGN.md\nconfig.json.example\nDockerfile\nhealthcheck.js\nIMPLEMENTATION_PLAN.md\njest.config.js\nLICENSE\npackage-lock.json\npackage.json\nprovider_pools.json.example\nREADME-JA.md\nREADME-ZH.md\nREADME.md\nREFACTORING_GUIDE.md\nREFACTORING_README.md\nREFACTORING_SUMMARY.md\nrun-docker.bat\nrun-docker.sh\nUSAGE_EXAMPLES.md\n.kilocode/\n.specify/\nsrc/\nsrc/adapter.js\nsrc/api-server.js\nsrc/common.js\nsrc/convert-new.js\nsrc/convert.js\nsrc/provider-pool-manager.js\nsrc/provider-strategies.js\nsrc/provider-strategy.js\nsrc/claude/\nsrc/claude/claude-core.js\nsrc/claude/claude-kiro.js\nsrc/claude/claude-strategy.js\nsrc/converters/\nsrc/converters/BaseConverter.js\nsrc/converters/ConverterFactory.js\nsrc/converters/utils.js\nsrc/converters/strategies/\nsrc/converters/strategies/ClaudeConverter.js\nsrc/converters/strategies/GeminiConverter.js\nsrc/converters/strategies/OpenAIConverter.js\nsrc/converters/strategies/OpenAIResponsesConverter.js\nsrc/example/\nsrc/example/claude/\nsrc/example/claude/oldResponse1762501965597.json\nsrc/example/claude/oldResponseChunk1762501803864.json\nsrc/example/claude/originalRequestBody1762501795790.json\nsrc/example/claude/originalRequestBody1762501964743.json\nsrc/example/gemini/\nsrc/example/openai/\nsrc/example/openai/oldResponse1762501667708.json\nsrc/example/openai/oldResponseChunk1762501528837.json\nsrc/example/openai/originalRequestBody1762501497434.json\nsrc/example/openai/originalRequestBody1762501665376.json\nsrc/example/openaiResponses/\nsrc/example/openaiResponses/oldResponseChunk1762500869734.json\nsrc/example/openaiResponses/originalRequestBody1762500856556.json\nsrc/gemini/\nsrc/gemini/gemini-core.js\nsrc/gemini/gemini-strategy.js\nsrc/img/\nsrc/img/logo-min.webp\nsrc/img/logo.webp\nsrc/openai/\nsrc/openai/openai-core.js\nsrc/openai/openai-responses-core.js\nsrc/openai/openai-responses-core.mjs\nsrc/openai/openai-responses-strategy.js\nsrc/openai/openai-strategy.js\nsrc/openai/qwen-core.js\ntests/\ntests/api-integration.test.js\nYou have not created a todo list yet. Create one with `update_todo_list` if your task is complicated or involves multiple steps.\n"}],"role":"user"},{"parts":[{"text":"你好!有什么我能帮助你的吗?"}],"role":"model"},{"parts":[{"text":"[ERROR] You did not use a tool in your previous response! Please retry with a tool use.\n\n# Reminder: Instructions for Tool Use\n\nTool uses are formatted using XML-style tags. The tool name itself becomes the XML tag name. Each parameter is enclosed within its own set of tags. Here's the structure:\n\n\nvalue1\nvalue2\n...\n\n\nFor example, to use the attempt_completion tool:\n\n\n\nI have completed the task...\n\n\n\nAlways use the actual tool name as the XML tag name for proper parsing and execution.\n\n# Next Steps\n\nIf you have completed the user's task, use the attempt_completion tool.\nIf you require additional information from the user, use the ask_followup_question tool.\nOtherwise, if you have not completed the task and do not need additional information, then proceed with the next step of the task.\n(This is an automated message, so do not respond to it conversationally.)"},{"text":"\n# VSCode Visible Files\nsrc\\converters\\ConverterFactory.js\n\n# VSCode Open Tabs\nsrc/claude/claude-kiro.js,src/openai/qwen-core.js,config.json,provider_pools.json,src/converters/ConverterFactory.js,src/gemini/gemini-core.js,src/provider-pool-manager.js\n\n# Current Time\nCurrent time in ISO 8601 UTC format: 2025-11-07T07:54:32.611Z\nUser time zone: Asia/Shanghai, UTC+8:00\n\n# Current Cost\n$0.00\n\n# Current Mode\ncode\nCode\ngemini-exp-1206\nYou have not created a todo list yet. Create one with `update_todo_list` if your task is complicated or involves multiple steps.\n"}],"role":"user"}],"systemInstruction":{"parts":[{"text":"You are Kilo Code, a highly skilled software engineer with extensive knowledge in many programming languages, frameworks, design patterns, and best practices.\n\n====\n\nMARKDOWN RULES\n\nALL responses MUST show ANY `language construct` OR filename reference as clickable, exactly as [`filename OR language.declaration()`](relative/file/path.ext:line); line is required for `syntax` and optional for filename links. This applies to ALL markdown responses and ALSO those in \n\n====\n\nTOOL USE\n\nYou have access to a set of tools that are executed upon the user's approval. You can use one tool per message, and will receive the result of that tool use in the user's response. You use tools step-by-step to accomplish a given task, with each tool use informed by the result of the previous tool use.\n\n# Tool Use Formatting\n\nTool uses are formatted using XML-style tags. The tool name itself becomes the XML tag name. Each parameter is enclosed within its own set of tags. Here's the structure:\n\n\nvalue1\nvalue2\n...\n\n\nAlways use the actual tool name as the XML tag name for proper parsing and execution.\n\n# Tools\n\n## read_file\nDescription: Request to read the contents of one or more files. The tool outputs line-numbered content (e.g. \"1 | const x = 1\") for easy reference when creating diffs or discussing code. Supports text extraction from .pdf and .docx and .ipynb and .xlsx files, but may not handle other binary files properly.\n\n**IMPORTANT: You can read a maximum of 5 files in a single request.** If you need to read more files, use multiple sequential read_file requests.\n\n\nParameters:\n- args: Contains one or more file elements, where each file contains:\n - path: (required) File path (relative to workspace directory d:\\ai\\code\\AIClient2API)\n \n\nUsage:\n\n\n \n path/to/file\n \n \n\n\n\nExamples:\n\n1. Reading a single file:\n\n\n \n src/app.ts\n \n \n\n\n\n2. Reading multiple files (within the 5-file limit):\n\n\n \n src/app.ts\n \n \n \n src/utils.ts\n \n \n\n\n\n3. Reading an entire file:\n\n\n \n config.json\n \n\n\n\nIMPORTANT: You MUST use this Efficient Reading Strategy:\n- You MUST read all related files and implementations together in a single operation (up to 5 files at once)\n- You MUST obtain all necessary context before proceeding with changes\n\n- When you need to read more than 5 files, prioritize the most critical files first, then use subsequent read_file requests for additional files\n\n## fetch_instructions\nDescription: Request to fetch instructions to perform a task\nParameters:\n- task: (required) The task to get instructions for. This can take the following values:\n create_mcp_server\n create_mode\n\nExample: Requesting instructions to create an MCP Server\n\n\ncreate_mcp_server\n\n\n## search_files\nDescription: Request to perform a regex search across files in a specified directory, providing context-rich results. This tool searches for patterns or specific content across multiple files, displaying each match with encapsulating context.\nParameters:\n- path: (required) The path of the directory to search in (relative to the current workspace directory d:\\ai\\code\\AIClient2API). This directory will be recursively searched.\n- regex: (required) The regular expression pattern to search for. Uses Rust regex syntax.\n- file_pattern: (optional) Glob pattern to filter files (e.g., '*.ts' for TypeScript files). If not provided, it will search all files (*).\nUsage:\n\nDirectory path here\nYour regex pattern here\nfile pattern here (optional)\n\n\nExample: Requesting to search for all .ts files in the current directory\n\n.\n.*\n*.ts\n\n\n## list_files\nDescription: Request to list files and directories within the specified directory. If recursive is true, it will list all files and directories recursively. If recursive is false or not provided, it will only list the top-level contents. Do not use this tool to confirm the existence of files you may have created, as the user will let you know if the files were created successfully or not.\nParameters:\n- path: (required) The path of the directory to list contents for (relative to the current workspace directory d:\\ai\\code\\AIClient2API)\n- recursive: (optional) Whether to list files recursively. Use true for recursive listing, false or omit for top-level only.\nUsage:\n\nDirectory path here\ntrue or false (optional)\n\n\nExample: Requesting to list all files in the current directory\n\n.\nfalse\n\n\n## list_code_definition_names\nDescription: Request to list definition names (classes, functions, methods, etc.) from source code. This tool can analyze either a single file or all files at the top level of a specified directory. It provides insights into the codebase structure and important constructs, encapsulating high-level concepts and relationships that are crucial for understanding the overall architecture.\nParameters:\n- path: (required) The path of the file or directory (relative to the current working directory d:\\ai\\code\\AIClient2API) to analyze. When given a directory, it lists definitions from all top-level source files.\nUsage:\n\nDirectory path here\n\n\nExamples:\n\n1. List definitions from a specific file:\n\nsrc/main.ts\n\n\n2. List definitions from all files in a directory:\n\nsrc/\n\n\n## apply_diff\nDescription: Request to apply PRECISE, TARGETED modifications to an existing file by searching for specific sections of content and replacing them. This tool is for SURGICAL EDITS ONLY - specific changes to existing code.\nYou can perform multiple distinct search and replace operations within a single `apply_diff` call by providing multiple SEARCH/REPLACE blocks in the `diff` parameter. This is the preferred way to make several targeted changes efficiently.\nThe SEARCH section must exactly match existing content including whitespace and indentation.\nIf you're not confident in the exact content to search for, use the read_file tool first to get the exact content.\nWhen applying the diffs, be extra careful to remember to change any closing brackets or other syntax that may be affected by the diff farther down in the file.\nALWAYS make as many changes in a single 'apply_diff' request as possible using multiple SEARCH/REPLACE blocks\n\nParameters:\n- path: (required) The path of the file to modify (relative to the current workspace directory d:\\ai\\code\\AIClient2API)\n- diff: (required) The search/replace block defining the changes.\n\nDiff format:\n```\n<<<<<<< SEARCH\n:start_line: (required) The line number of original content where the search block starts.\n-------\n[exact content to find including whitespace]\n=======\n[new content to replace with]\n>>>>>>> REPLACE\n\n```\n\n\nExample:\n\nOriginal file:\n```\n1 | def calculate_total(items):\n2 | total = 0\n3 | for item in items:\n4 | total += item\n5 | return total\n```\n\nSearch/Replace content:\n```\n<<<<<<< SEARCH\n:start_line:1\n-------\ndef calculate_total(items):\n total = 0\n for item in items:\n total += item\n return total\n=======\ndef calculate_total(items):\n \"\"\"Calculate total with 10% markup\"\"\"\n return sum(item * 1.1 for item in items)\n>>>>>>> REPLACE\n\n```\n\nSearch/Replace content with multiple edits:\n```\n<<<<<<< SEARCH\n:start_line:1\n-------\ndef calculate_total(items):\n sum = 0\n=======\ndef calculate_sum(items):\n sum = 0\n>>>>>>> REPLACE\n\n<<<<<<< SEARCH\n:start_line:4\n-------\n total += item\n return total\n=======\n sum += item\n return sum \n>>>>>>> REPLACE\n```\n\n\nUsage:\n\nFile path here\n\nYour search/replace content here\nYou can use multi search/replace block in one diff block, but make sure to include the line numbers for each block.\nOnly use a single line of '=======' between search and replacement content, because multiple '=======' will corrupt the file.\n\n\n\n## write_to_file\nDescription: Request to write content to a file. This tool is primarily used for **creating new files** or for scenarios where a **complete rewrite of an existing file is intentionally required**. If the file exists, it will be overwritten. If it doesn't exist, it will be created. This tool will automatically create any directories needed to write the file.\nParameters:\n- path: (required) The path of the file to write to (relative to the current workspace directory d:\\ai\\code\\AIClient2API)\n- content: (required) The content to write to the file. When performing a full rewrite of an existing file or creating a new one, ALWAYS provide the COMPLETE intended content of the file, without any truncation or omissions. You MUST include ALL parts of the file, even if they haven't been modified. Do NOT include the line numbers in the content though, just the actual content of the file.\n- line_count: (required) The number of lines in the file. Make sure to compute this based on the actual content of the file, not the number of lines in the content you're providing.\nUsage:\n\nFile path here\n\nYour file content here\n\ntotal number of lines in the file, including empty lines\n\n\nExample: Requesting to write to frontend-config.json\n\nfrontend-config.json\n\n{\n \"apiEndpoint\": \"https://api.example.com\",\n \"theme\": {\n \"primaryColor\": \"#007bff\",\n \"secondaryColor\": \"#6c757d\",\n \"fontFamily\": \"Arial, sans-serif\"\n },\n \"features\": {\n \"darkMode\": true,\n \"notifications\": true,\n \"analytics\": false\n },\n \"version\": \"1.0.0\"\n}\n\n14\n\n\n## insert_content\nDescription: Use this tool specifically for adding new lines of content into a file without modifying existing content. Specify the line number to insert before, or use line 0 to append to the end. Ideal for adding imports, functions, configuration blocks, log entries, or any multi-line text block.\n\nParameters:\n- path: (required) File path relative to workspace directory d:/ai/code/AIClient2API\n- line: (required) Line number where content will be inserted (1-based)\n\t Use 0 to append at end of file\n\t Use any positive number to insert before that line\n- content: (required) The content to insert at the specified line\n\nExample for inserting imports at start of file:\n\nsrc/utils.ts\n1\n\n// Add imports at start of file\nimport { sum } from './math';\n\n\n\nExample for appending to the end of file:\n\nsrc/utils.ts\n0\n\n// This is the end of the file\n\n\n\n\n## search_and_replace\nDescription: Use this tool to find and replace specific text strings or patterns (using regex) within a file. It's suitable for targeted replacements across multiple locations within the file. Supports literal text and regex patterns, case sensitivity options, and optional line ranges. Shows a diff preview before applying changes.\n\nRequired Parameters:\n- path: The path of the file to modify (relative to the current workspace directory d:/ai/code/AIClient2API)\n- search: The text or pattern to search for\n- replace: The text to replace matches with\n\nOptional Parameters:\n- start_line: Starting line number for restricted replacement (1-based)\n- end_line: Ending line number for restricted replacement (1-based)\n- use_regex: Set to \"true\" to treat search as a regex pattern (default: false)\n- ignore_case: Set to \"true\" to ignore case when matching (default: false)\n\nNotes:\n- When use_regex is true, the search parameter is treated as a regular expression pattern\n- When ignore_case is true, the search is case-insensitive regardless of regex mode\n\nExamples:\n\n1. Simple text replacement:\n\nexample.ts\noldText\nnewText\n\n\n2. Case-insensitive regex pattern:\n\nexample.ts\noldw+\nnew$&\ntrue\ntrue\n\n\n## execute_command\nDescription: Request to execute a CLI command on the system. Use this when you need to perform system operations or run specific commands to accomplish any step in the user's task. You must tailor your command to the user's system and provide a clear explanation of what the command does. For command chaining, use the appropriate chaining syntax for the user's shell. Prefer to execute complex CLI commands over creating executable scripts, as they are more flexible and easier to run. Prefer relative commands and paths that avoid location sensitivity for terminal consistency, e.g: `touch ./testdata/example.file`, `dir ./examples/model1/data/yaml`, or `go test ./cmd/front --config ./cmd/front/config.yml`. If directed by the user, you may open a terminal in a different directory by using the `cwd` parameter.\nParameters:\n- command: (required) The CLI command to execute. This should be valid for the current operating system. Ensure the command is properly formatted and does not contain any harmful instructions.\n- cwd: (optional) The working directory to execute the command in (default: d:\\ai\\code\\AIClient2API)\nUsage:\n\nYour command here\nWorking directory path (optional)\n\n\nExample: Requesting to execute npm run dev\n\nnpm run dev\n\n\nExample: Requesting to execute ls in a specific directory if directed\n\nls -la\n/home/user/projects\n\n\n## use_mcp_tool\nDescription: Request to use a tool provided by a connected MCP server. Each MCP server can provide multiple tools with different capabilities. Tools have defined input schemas that specify required and optional parameters.\nParameters:\n- server_name: (required) The name of the MCP server providing the tool\n- tool_name: (required) The name of the tool to execute\n- arguments: (required) A JSON object containing the tool's input parameters, following the tool's input schema\nUsage:\n\nserver name here\ntool name here\n\n{\n \"param1\": \"value1\",\n \"param2\": \"value2\"\n}\n\n\n\nExample: Requesting to use an MCP tool\n\n\nweather-server\nget_forecast\n\n{\n \"city\": \"San Francisco\",\n \"days\": 5\n}\n\n\n\n## access_mcp_resource\nDescription: Request to access a resource provided by a connected MCP server. Resources represent data sources that can be used as context, such as files, API responses, or system information.\nParameters:\n- server_name: (required) The name of the MCP server providing the resource\n- uri: (required) The URI identifying the specific resource to access\nUsage:\n\nserver name here\nresource URI here\n\n\nExample: Requesting to access an MCP resource\n\n\nweather-server\nweather://san-francisco/current\n\n\n## ask_followup_question\nDescription: Ask the user a question to gather additional information needed to complete the task. Use when you need clarification or more details to proceed effectively.\n\nParameters:\n- question: (required) A clear, specific question addressing the information needed\n- follow_up: (optional) A list of 2-4 suggested answers, each in its own tag. Suggestions must be complete, actionable answers without placeholders. Optionally include mode attribute to switch modes (code/architect/etc.)\n\nUsage:\n\nYour question here\n\nFirst suggestion\nAction with mode switch\n\n\n\nExample:\n\nWhat is the path to the frontend-config.json file?\n\n./src/frontend-config.json\n./config/frontend-config.json\n./frontend-config.json\n\n\n\n## attempt_completion\nDescription: After each tool use, the user will respond with the result of that tool use, i.e. if it succeeded or failed, along with any reasons for failure. Once you've received the results of tool uses and can confirm that the task is complete, use this tool to present the result of your work to the user. The user may respond with feedback if they are not satisfied with the result, which you can use to make improvements and try again.\nIMPORTANT NOTE: This tool CANNOT be used until you've confirmed from the user that any previous tool uses were successful. Failure to do so will result in code corruption and system failure. Before using this tool, you must ask yourself in tags if you've confirmed from the user that any previous tool uses were successful. If not, then DO NOT use this tool.\nParameters:\n- result: (required) The result of the task. Formulate this result in a way that is final and does not require further input from the user. Don't end your result with questions or offers for further assistance.\nUsage:\n\n\nYour final result description here\n\n\n\nExample: Requesting to attempt completion with a result\n\n\nI've updated the CSS\n\n\n\n## switch_mode\nDescription: Request to switch to a different mode. This tool allows modes to request switching to another mode when needed, such as switching to Code mode to make code changes. The user must approve the mode switch.\nParameters:\n- mode_slug: (required) The slug of the mode to switch to (e.g., \"code\", \"ask\", \"architect\")\n- reason: (optional) The reason for switching modes\nUsage:\n\nMode slug here\nReason for switching here\n\n\nExample: Requesting to switch to code mode\n\ncode\nNeed to make code changes\n\n\n## new_task\nDescription: This will let you create a new task instance in the chosen mode using your provided message.\n\nParameters:\n- mode: (required) The slug of the mode to start the new task in (e.g., \"code\", \"debug\", \"architect\").\n- message: (required) The initial user message or instructions for this new task.\n\nUsage:\n\nyour-mode-slug-here\nYour initial instructions here\n\n\nExample:\n\ncode\nImplement a new feature for the application\n\n\n\n## update_todo_list\n\n**Description:**\nReplace the entire TODO list with an updated checklist reflecting the current state. Always provide the full list; the system will overwrite the previous one. This tool is designed for step-by-step task tracking, allowing you to confirm completion of each step before updating, update multiple task statuses at once (e.g., mark one as completed and start the next), and dynamically add new todos discovered during long or complex tasks.\n\n**Checklist Format:**\n- Use a single-level markdown checklist (no nesting or subtasks).\n- List todos in the intended execution order.\n- Status options:\n\t - [ ] Task description (pending)\n\t - [x] Task description (completed)\n\t - [-] Task description (in progress)\n\n**Status Rules:**\n- [ ] = pending (not started)\n- [x] = completed (fully finished, no unresolved issues)\n- [-] = in_progress (currently being worked on)\n\n**Core Principles:**\n- Before updating, always confirm which todos have been completed since the last update.\n- You may update multiple statuses in a single update (e.g., mark the previous as completed and the next as in progress).\n- When a new actionable item is discovered during a long or complex task, add it to the todo list immediately.\n- Do not remove any unfinished todos unless explicitly instructed.\n- Always retain all unfinished tasks, updating their status as needed.\n- Only mark a task as completed when it is fully accomplished (no partials, no unresolved dependencies).\n- If a task is blocked, keep it as in_progress and add a new todo describing what needs to be resolved.\n- Remove tasks only if they are no longer relevant or if the user requests deletion.\n\n**Usage Example:**\n\n\n[x] Analyze requirements\n[x] Design architecture\n[-] Implement core logic\n[ ] Write tests\n[ ] Update documentation\n\n\n\n*After completing \"Implement core logic\" and starting \"Write tests\":*\n\n\n[x] Analyze requirements\n[x] Design architecture\n[x] Implement core logic\n[-] Write tests\n[ ] Update documentation\n[ ] Add performance benchmarks\n\n\n\n**When to Use:**\n- The task is complicated or involves multiple steps or requires ongoing tracking.\n- You need to update the status of several todos at once.\n- New actionable items are discovered during task execution.\n- The user requests a todo list or provides multiple tasks.\n- The task is complex and benefits from clear, stepwise progress tracking.\n\n**When NOT to Use:**\n- There is only a single, trivial task.\n- The task can be completed in one or two simple steps.\n- The request is purely conversational or informational.\n\n**Task Management Guidelines:**\n- Mark task as completed immediately after all work of the current task is done.\n- Start the next task by marking it as in_progress.\n- Add new todos as soon as they are identified.\n- Use clear, descriptive task names.\n\n\n# Tool Use Guidelines\n\n1. In tags, assess what information you already have and what information you need to proceed with the task.\n2. Choose the most appropriate tool based on the task and the tool descriptions provided. Assess if you need additional information to proceed, and which of the available tools would be most effective for gathering this information. For example using the list_files tool is more effective than running a command like `ls` in the terminal. It's critical that you think about each available tool and use the one that best fits the current step in the task.\n3. If multiple actions are needed, use one tool at a time per message to accomplish the task iteratively, with each tool use being informed by the result of the previous tool use. Do not assume the outcome of any tool use. Each step must be informed by the previous step's result.\n4. Formulate your tool use using the XML format specified for each tool.\n5. After each tool use, the user will respond with the result of that tool use. This result will provide you with the necessary information to continue your task or make further decisions. This response may include:\n - Information about whether the tool succeeded or failed, along with any reasons for failure.\n - Linter errors that may have arisen due to the changes you made, which you'll need to address.\n - New terminal output in reaction to the changes, which you may need to consider or act upon.\n - Any other relevant feedback or information related to the tool use.\n6. ALWAYS wait for user confirmation after each tool use before proceeding. Never assume the success of a tool use without explicit confirmation of the result from the user.\n\nIt is crucial to proceed step-by-step, waiting for the user's message after each tool use before moving forward with the task. This approach allows you to:\n1. Confirm the success of each step before proceeding.\n2. Address any issues or errors that arise immediately.\n3. Adapt your approach based on new information or unexpected results.\n4. Ensure that each action builds correctly on the previous ones.\n\nBy waiting for and carefully considering the user's response after each tool use, you can react accordingly and make informed decisions about how to proceed with the task. This iterative process helps ensure the overall success and accuracy of your work.\n\nMCP SERVERS\n\nThe Model Context Protocol (MCP) enables communication between the system and MCP servers that provide additional tools and resources to extend your capabilities. MCP servers can be one of two types:\n\n1. Local (Stdio-based) servers: These run locally on the user's machine and communicate via standard input/output\n2. Remote (SSE-based) servers: These run on remote machines and communicate via Server-Sent Events (SSE) over HTTP/HTTPS\n\n# Connected MCP Servers\n\nWhen a server is connected, you can use the server's tools via the `use_mcp_tool` tool, and access the server's resources via the `access_mcp_resource` tool.\n\n## Memory (`npx -y @modelcontextprotocol/server-memory`)\n\n### Available Tools\n- create_entities: Create multiple new entities in the knowledge graph\n Input Schema:\n\t\t{\n \"type\": \"object\",\n \"properties\": {\n \"entities\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"properties\": {\n \"name\": {\n \"type\": \"string\",\n \"description\": \"The name of the entity\"\n },\n \"entityType\": {\n \"type\": \"string\",\n \"description\": \"The type of the entity\"\n },\n \"observations\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\"\n },\n \"description\": \"An array of observation contents associated with the entity\"\n }\n },\n \"required\": [\n \"name\",\n \"entityType\",\n \"observations\"\n ],\n \"additionalProperties\": false\n }\n }\n },\n \"required\": [\n \"entities\"\n ],\n \"additionalProperties\": false\n }\n\n- create_relations: Create multiple new relations between entities in the knowledge graph. Relations should be in active voice\n Input Schema:\n\t\t{\n \"type\": \"object\",\n \"properties\": {\n \"relations\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"properties\": {\n \"from\": {\n \"type\": \"string\",\n \"description\": \"The name of the entity where the relation starts\"\n },\n \"to\": {\n \"type\": \"string\",\n \"description\": \"The name of the entity where the relation ends\"\n },\n \"relationType\": {\n \"type\": \"string\",\n \"description\": \"The type of the relation\"\n }\n },\n \"required\": [\n \"from\",\n \"to\",\n \"relationType\"\n ],\n \"additionalProperties\": false\n }\n }\n },\n \"required\": [\n \"relations\"\n ],\n \"additionalProperties\": false\n }\n\n- add_observations: Add new observations to existing entities in the knowledge graph\n Input Schema:\n\t\t{\n \"type\": \"object\",\n \"properties\": {\n \"observations\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"properties\": {\n \"entityName\": {\n \"type\": \"string\",\n \"description\": \"The name of the entity to add the observations to\"\n },\n \"contents\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\"\n },\n \"description\": \"An array of observation contents to add\"\n }\n },\n \"required\": [\n \"entityName\",\n \"contents\"\n ],\n \"additionalProperties\": false\n }\n }\n },\n \"required\": [\n \"observations\"\n ],\n \"additionalProperties\": false\n }\n\n- delete_entities: Delete multiple entities and their associated relations from the knowledge graph\n Input Schema:\n\t\t{\n \"type\": \"object\",\n \"properties\": {\n \"entityNames\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\"\n },\n \"description\": \"An array of entity names to delete\"\n }\n },\n \"required\": [\n \"entityNames\"\n ],\n \"additionalProperties\": false\n }\n\n- delete_observations: Delete specific observations from entities in the knowledge graph\n Input Schema:\n\t\t{\n \"type\": \"object\",\n \"properties\": {\n \"deletions\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"properties\": {\n \"entityName\": {\n \"type\": \"string\",\n \"description\": \"The name of the entity containing the observations\"\n },\n \"observations\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\"\n },\n \"description\": \"An array of observations to delete\"\n }\n },\n \"required\": [\n \"entityName\",\n \"observations\"\n ],\n \"additionalProperties\": false\n }\n }\n },\n \"required\": [\n \"deletions\"\n ],\n \"additionalProperties\": false\n }\n\n- delete_relations: Delete multiple relations from the knowledge graph\n Input Schema:\n\t\t{\n \"type\": \"object\",\n \"properties\": {\n \"relations\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"properties\": {\n \"from\": {\n \"type\": \"string\",\n \"description\": \"The name of the entity where the relation starts\"\n },\n \"to\": {\n \"type\": \"string\",\n \"description\": \"The name of the entity where the relation ends\"\n },\n \"relationType\": {\n \"type\": \"string\",\n \"description\": \"The type of the relation\"\n }\n },\n \"required\": [\n \"from\",\n \"to\",\n \"relationType\"\n ],\n \"additionalProperties\": false\n },\n \"description\": \"An array of relations to delete\"\n }\n },\n \"required\": [\n \"relations\"\n ],\n \"additionalProperties\": false\n }\n\n- read_graph: Read the entire knowledge graph\n Input Schema:\n\t\t{\n \"type\": \"object\",\n \"properties\": {},\n \"additionalProperties\": false\n }\n\n- search_nodes: Search for nodes in the knowledge graph based on a query\n Input Schema:\n\t\t{\n \"type\": \"object\",\n \"properties\": {\n \"query\": {\n \"type\": \"string\",\n \"description\": \"The search query to match against entity names, types, and observation content\"\n }\n },\n \"required\": [\n \"query\"\n ],\n \"additionalProperties\": false\n }\n\n- open_nodes: Open specific nodes in the knowledge graph by their names\n Input Schema:\n\t\t{\n \"type\": \"object\",\n \"properties\": {\n \"names\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\"\n },\n \"description\": \"An array of entity names to retrieve\"\n }\n },\n \"required\": [\n \"names\"\n ],\n \"additionalProperties\": false\n }\n\n## exa\n\n### Available Tools\n- web_search_exa: Search the web using Exa AI - performs real-time web searches and can scrape content from specific URLs. Supports configurable result counts and returns the content from the most relevant websites.\n Input Schema:\n\t\t{\n \"type\": \"object\",\n \"properties\": {\n \"query\": {\n \"type\": \"string\",\n \"description\": \"Websearch query\"\n },\n \"numResults\": {\n \"type\": \"number\",\n \"description\": \"Number of search results to return (default: 8)\"\n },\n \"livecrawl\": {\n \"type\": \"string\",\n \"enum\": [\n \"fallback\",\n \"preferred\"\n ],\n \"description\": \"Live crawl mode - 'fallback': use live crawling as backup if cached content unavailable, 'preferred': prioritize live crawling (default: 'fallback')\"\n },\n \"type\": {\n \"type\": \"string\",\n \"enum\": [\n \"auto\",\n \"fast\",\n \"deep\"\n ],\n \"description\": \"Search type - 'auto': balanced search (default), 'fast': quick results, 'deep': comprehensive search\"\n },\n \"contextMaxCharacters\": {\n \"type\": \"number\",\n \"description\": \"Maximum characters for context string optimized for LLMs (default: 10000)\"\n }\n },\n \"required\": [\n \"query\"\n ],\n \"additionalProperties\": false,\n \"$schema\": \"http://json-schema.org/draft-07/schema#\"\n }\n\n- get_code_context_exa: Search and get relevant context for any programming task. Exa-code has the highest quality and freshest context for libraries, SDKs, and APIs. Use this tool for ANY question or task for related to programming. RULE: when the user's query contains exa-code or anything related to code, you MUST use this tool.\n Input Schema:\n\t\t{\n \"type\": \"object\",\n \"properties\": {\n \"query\": {\n \"type\": \"string\",\n \"description\": \"Search query to find relevant context for APIs, Libraries, and SDKs. For example, 'React useState hook examples', 'Python pandas dataframe filtering', 'Express.js middleware', 'Next js partial prerendering configuration'\"\n },\n \"tokensNum\": {\n \"type\": \"number\",\n \"minimum\": 1000,\n \"maximum\": 50000,\n \"default\": 5000,\n \"description\": \"Number of tokens to return (1000-50000). Default is 5000 tokens. Adjust this value based on how much context you need - use lower values for focused queries and higher values for comprehensive documentation.\"\n }\n },\n \"required\": [\n \"query\"\n ],\n \"additionalProperties\": false,\n \"$schema\": \"http://json-schema.org/draft-07/schema#\"\n }\n\n### Direct Resources\n- exa://tools/list (tools_list): List of available Exa tools and their descriptions\n\n## chrome-devtools (`cmd /c npx -y chrome-devtools-mcp@latest -e=C:/VMDP/tools/MyChromeX64/chrome/chrome.exe`)\n\n### Available Tools\n- click: Clicks on the provided element\n Input Schema:\n\t\t{\n \"type\": \"object\",\n \"properties\": {\n \"uid\": {\n \"type\": \"string\",\n \"description\": \"The uid of an element on the page from the page content snapshot\"\n },\n \"dblClick\": {\n \"type\": \"boolean\",\n \"description\": \"Set to true for double clicks. Default is false.\"\n }\n },\n \"required\": [\n \"uid\"\n ],\n \"additionalProperties\": false,\n \"$schema\": \"http://json-schema.org/draft-07/schema#\"\n }\n\n- close_page: Closes the page by its index. The last open page cannot be closed.\n Input Schema:\n\t\t{\n \"type\": \"object\",\n \"properties\": {\n \"pageIdx\": {\n \"type\": \"number\",\n \"description\": \"The index of the page to close. Call list_pages to list pages.\"\n }\n },\n \"required\": [\n \"pageIdx\"\n ],\n \"additionalProperties\": false,\n \"$schema\": \"http://json-schema.org/draft-07/schema#\"\n }\n\n- drag: Drag an element onto another element\n Input Schema:\n\t\t{\n \"type\": \"object\",\n \"properties\": {\n \"from_uid\": {\n \"type\": \"string\",\n \"description\": \"The uid of the element to drag\"\n },\n \"to_uid\": {\n \"type\": \"string\",\n \"description\": \"The uid of the element to drop into\"\n }\n },\n \"required\": [\n \"from_uid\",\n \"to_uid\"\n ],\n \"additionalProperties\": false,\n \"$schema\": \"http://json-schema.org/draft-07/schema#\"\n }\n\n- emulate: Emulates various features on the selected page.\n Input Schema:\n\t\t{\n \"type\": \"object\",\n \"properties\": {\n \"networkConditions\": {\n \"type\": \"string\",\n \"enum\": [\n \"No emulation\",\n \"Offline\",\n \"Slow 3G\",\n \"Fast 3G\",\n \"Slow 4G\",\n \"Fast 4G\"\n ],\n \"description\": \"Throttle network. Set to \\\"No emulation\\\" to disable. If omitted, conditions remain unchanged.\"\n },\n \"cpuThrottlingRate\": {\n \"type\": \"number\",\n \"minimum\": 1,\n \"maximum\": 20,\n \"description\": \"Represents the CPU slowdown factor. Set the rate to 1 to disable throttling. If omitted, throttling remains unchanged.\"\n }\n },\n \"additionalProperties\": false,\n \"$schema\": \"http://json-schema.org/draft-07/schema#\"\n }\n\n- evaluate_script: Evaluate a JavaScript function inside the currently selected page. Returns the response as JSON\nso returned values have to JSON-serializable.\n Input Schema:\n\t\t{\n \"type\": \"object\",\n \"properties\": {\n \"function\": {\n \"type\": \"string\",\n \"description\": \"A JavaScript function declaration to be executed by the tool in the currently selected page.\\nExample without arguments: `() => {\\n return document.title\\n}` or `async () => {\\n return await fetch(\\\"example.com\\\")\\n}`.\\nExample with arguments: `(el) => {\\n return el.innerText;\\n}`\\n\"\n },\n \"args\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"properties\": {\n \"uid\": {\n \"type\": \"string\",\n \"description\": \"The uid of an element on the page from the page content snapshot\"\n }\n },\n \"required\": [\n \"uid\"\n ],\n \"additionalProperties\": false\n },\n \"description\": \"An optional list of arguments to pass to the function.\"\n }\n },\n \"required\": [\n \"function\"\n ],\n \"additionalProperties\": false,\n \"$schema\": \"http://json-schema.org/draft-07/schema#\"\n }\n\n- fill: Type text into a input, text area or select an option from a element.\n Input Schema:\n\t\t{\n \"type\": \"object\",\n \"properties\": {\n \"uid\": {\n \"type\": \"string\",\n \"description\": \"The uid of an element on the page from the page content snapshot\"\n },\n \"value\": {\n \"type\": \"string\",\n \"description\": \"The value to fill in\"\n }\n },\n \"required\": [\n \"uid\",\n \"value\"\n ],\n \"additionalProperties\": false,\n \"$schema\": \"http://json-schema.org/draft-07/schema#\"\n }\n\n- fill_form: Fill out multiple form elements at once\n Input Schema:\n\t\t{\n \"type\": \"object\",\n \"properties\": {\n \"elements\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"properties\": {\n \"uid\": {\n \"type\": \"string\",\n \"description\": \"The uid of the element to fill out\"\n },\n \"value\": {\n \"type\": \"string\",\n \"description\": \"Value for the element\"\n }\n },\n \"required\": [\n \"uid\",\n \"value\"\n ],\n \"additionalProperties\": false\n },\n \"description\": \"Elements from snapshot to fill out.\"\n }\n },\n \"required\": [\n \"elements\"\n ],\n \"additionalProperties\": false,\n \"$schema\": \"http://json-schema.org/draft-07/schema#\"\n }\n\n- get_console_message: Gets a console message by its ID. You can get all messages by calling list_console_messages.\n Input Schema:\n\t\t{\n \"type\": \"object\",\n \"properties\": {\n \"msgid\": {\n \"type\": \"number\",\n \"description\": \"The msgid of a console message on the page from the listed console messages\"\n }\n },\n \"required\": [\n \"msgid\"\n ],\n \"additionalProperties\": false,\n \"$schema\": \"http://json-schema.org/draft-07/schema#\"\n }\n\n- get_network_request: Gets a network request by an optional reqid, if omitted returns the currently selected request in the DevTools Network panel.\n Input Schema:\n\t\t{\n \"type\": \"object\",\n \"properties\": {\n \"reqid\": {\n \"type\": \"number\",\n \"description\": \"The reqid of the network request. If omitted returns the currently selected request in the DevTools Network panel.\"\n }\n },\n \"additionalProperties\": false,\n \"$schema\": \"http://json-schema.org/draft-07/schema#\"\n }\n\n- handle_dialog: If a browser dialog was opened, use this command to handle it\n Input Schema:\n\t\t{\n \"type\": \"object\",\n \"properties\": {\n \"action\": {\n \"type\": \"string\",\n \"enum\": [\n \"accept\",\n \"dismiss\"\n ],\n \"description\": \"Whether to dismiss or accept the dialog\"\n },\n \"promptText\": {\n \"type\": \"string\",\n \"description\": \"Optional prompt text to enter into the dialog.\"\n }\n },\n \"required\": [\n \"action\"\n ],\n \"additionalProperties\": false,\n \"$schema\": \"http://json-schema.org/draft-07/schema#\"\n }\n\n- hover: Hover over the provided element\n Input Schema:\n\t\t{\n \"type\": \"object\",\n \"properties\": {\n \"uid\": {\n \"type\": \"string\",\n \"description\": \"The uid of an element on the page from the page content snapshot\"\n }\n },\n \"required\": [\n \"uid\"\n ],\n \"additionalProperties\": false,\n \"$schema\": \"http://json-schema.org/draft-07/schema#\"\n }\n\n- list_console_messages: List all console messages for the currently selected page since the last navigation.\n Input Schema:\n\t\t{\n \"type\": \"object\",\n \"properties\": {\n \"pageSize\": {\n \"type\": \"integer\",\n \"exclusiveMinimum\": 0,\n \"description\": \"Maximum number of messages to return. When omitted, returns all requests.\"\n },\n \"pageIdx\": {\n \"type\": \"integer\",\n \"minimum\": 0,\n \"description\": \"Page number to return (0-based). When omitted, returns the first page.\"\n },\n \"types\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\",\n \"enum\": [\n \"log\",\n \"debug\",\n \"info\",\n \"error\",\n \"warn\",\n \"dir\",\n \"dirxml\",\n \"table\",\n \"trace\",\n \"clear\",\n \"startGroup\",\n \"startGroupCollapsed\",\n \"endGroup\",\n \"assert\",\n \"profile\",\n \"profileEnd\",\n \"count\",\n \"timeEnd\",\n \"verbose\"\n ]\n },\n \"description\": \"Filter messages to only return messages of the specified resource types. When omitted or empty, returns all messages.\"\n },\n \"includePreservedMessages\": {\n \"type\": \"boolean\",\n \"default\": false,\n \"description\": \"Set to true to return the preserved messages over the last 3 navigations.\"\n }\n },\n \"additionalProperties\": false,\n \"$schema\": \"http://json-schema.org/draft-07/schema#\"\n }\n\n- list_network_requests: List all requests for the currently selected page since the last navigation.\n Input Schema:\n\t\t{\n \"type\": \"object\",\n \"properties\": {\n \"pageSize\": {\n \"type\": \"integer\",\n \"exclusiveMinimum\": 0,\n \"description\": \"Maximum number of requests to return. When omitted, returns all requests.\"\n },\n \"pageIdx\": {\n \"type\": \"integer\",\n \"minimum\": 0,\n \"description\": \"Page number to return (0-based). When omitted, returns the first page.\"\n },\n \"resourceTypes\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\",\n \"enum\": [\n \"document\",\n \"stylesheet\",\n \"image\",\n \"media\",\n \"font\",\n \"script\",\n \"texttrack\",\n \"xhr\",\n \"fetch\",\n \"prefetch\",\n \"eventsource\",\n \"websocket\",\n \"manifest\",\n \"signedexchange\",\n \"ping\",\n \"cspviolationreport\",\n \"preflight\",\n \"fedcm\",\n \"other\"\n ]\n },\n \"description\": \"Filter requests to only return requests of the specified resource types. When omitted or empty, returns all requests.\"\n },\n \"includePreservedRequests\": {\n \"type\": \"boolean\",\n \"default\": false,\n \"description\": \"Set to true to return the preserved requests over the last 3 navigations.\"\n }\n },\n \"additionalProperties\": false,\n \"$schema\": \"http://json-schema.org/draft-07/schema#\"\n }\n\n- list_pages: Get a list of pages open in the browser.\n Input Schema:\n\t\t{\n \"type\": \"object\",\n \"properties\": {},\n \"additionalProperties\": false,\n \"$schema\": \"http://json-schema.org/draft-07/schema#\"\n }\n\n- navigate_page: Navigates the currently selected page to a URL.\n Input Schema:\n\t\t{\n \"type\": \"object\",\n \"properties\": {\n \"type\": {\n \"type\": \"string\",\n \"enum\": [\n \"url\",\n \"back\",\n \"forward\",\n \"reload\"\n ],\n \"description\": \"Navigate the page by URL, back or forward in history, or reload.\"\n },\n \"url\": {\n \"type\": \"string\",\n \"description\": \"Target URL (only type=url)\"\n },\n \"ignoreCache\": {\n \"type\": \"boolean\",\n \"description\": \"Whether to ignore cache on reload.\"\n },\n \"timeout\": {\n \"type\": \"integer\",\n \"description\": \"Maximum wait time in milliseconds. If set to 0, the default timeout will be used.\"\n }\n },\n \"additionalProperties\": false,\n \"$schema\": \"http://json-schema.org/draft-07/schema#\"\n }\n\n- new_page: Creates a new page\n Input Schema:\n\t\t{\n \"type\": \"object\",\n \"properties\": {\n \"url\": {\n \"type\": \"string\",\n \"description\": \"URL to load in a new page.\"\n },\n \"timeout\": {\n \"type\": \"integer\",\n \"description\": \"Maximum wait time in milliseconds. If set to 0, the default timeout will be used.\"\n }\n },\n \"required\": [\n \"url\"\n ],\n \"additionalProperties\": false,\n \"$schema\": \"http://json-schema.org/draft-07/schema#\"\n }\n\n- performance_analyze_insight: Provides more detailed information on a specific Performance Insight of an insight set that was highlighted in the results of a trace recording.\n Input Schema:\n\t\t{\n \"type\": \"object\",\n \"properties\": {\n \"insightSetId\": {\n \"type\": \"string\",\n \"description\": \"The id for the specific insight set. Only use the ids given in the \\\"Available insight sets\\\" list.\"\n },\n \"insightName\": {\n \"type\": \"string\",\n \"description\": \"The name of the Insight you want more information on. For example: \\\"DocumentLatency\\\" or \\\"LCPBreakdown\\\"\"\n }\n },\n \"required\": [\n \"insightSetId\",\n \"insightName\"\n ],\n \"additionalProperties\": false,\n \"$schema\": \"http://json-schema.org/draft-07/schema#\"\n }\n\n- performance_start_trace: Starts a performance trace recording on the selected page. This can be used to look for performance problems and insights to improve the performance of the page. It will also report Core Web Vital (CWV) scores for the page.\n Input Schema:\n\t\t{\n \"type\": \"object\",\n \"properties\": {\n \"reload\": {\n \"type\": \"boolean\",\n \"description\": \"Determines if, once tracing has started, the page should be automatically reloaded.\"\n },\n \"autoStop\": {\n \"type\": \"boolean\",\n \"description\": \"Determines if the trace recording should be automatically stopped.\"\n }\n },\n \"required\": [\n \"reload\",\n \"autoStop\"\n ],\n \"additionalProperties\": false,\n \"$schema\": \"http://json-schema.org/draft-07/schema#\"\n }\n\n- performance_stop_trace: Stops the active performance trace recording on the selected page.\n Input Schema:\n\t\t{\n \"type\": \"object\",\n \"properties\": {},\n \"additionalProperties\": false,\n \"$schema\": \"http://json-schema.org/draft-07/schema#\"\n }\n\n- press_key: Press a key or key combination. Use this when other input methods like fill() cannot be used (e.g., keyboard shortcuts, navigation keys, or special key combinations).\n Input Schema:\n\t\t{\n \"type\": \"object\",\n \"properties\": {\n \"key\": {\n \"type\": \"string\",\n \"description\": \"A key or a combination (e.g., \\\"Enter\\\", \\\"Control+A\\\", \\\"Control++\\\", \\\"Control+Shift+R\\\"). Modifiers: Control, Shift, Alt, Meta\"\n }\n },\n \"required\": [\n \"key\"\n ],\n \"additionalProperties\": false,\n \"$schema\": \"http://json-schema.org/draft-07/schema#\"\n }\n\n- resize_page: Resizes the selected page's window so that the page has specified dimension\n Input Schema:\n\t\t{\n \"type\": \"object\",\n \"properties\": {\n \"width\": {\n \"type\": \"number\",\n \"description\": \"Page width\"\n },\n \"height\": {\n \"type\": \"number\",\n \"description\": \"Page height\"\n }\n },\n \"required\": [\n \"width\",\n \"height\"\n ],\n \"additionalProperties\": false,\n \"$schema\": \"http://json-schema.org/draft-07/schema#\"\n }\n\n- select_page: Select a page as a context for future tool calls.\n Input Schema:\n\t\t{\n \"type\": \"object\",\n \"properties\": {\n \"pageIdx\": {\n \"type\": \"number\",\n \"description\": \"The index of the page to select. Call list_pages to list pages.\"\n }\n },\n \"required\": [\n \"pageIdx\"\n ],\n \"additionalProperties\": false,\n \"$schema\": \"http://json-schema.org/draft-07/schema#\"\n }\n\n- take_screenshot: Take a screenshot of the page or element.\n Input Schema:\n\t\t{\n \"type\": \"object\",\n \"properties\": {\n \"format\": {\n \"type\": \"string\",\n \"enum\": [\n \"png\",\n \"jpeg\",\n \"webp\"\n ],\n \"default\": \"png\",\n \"description\": \"Type of format to save the screenshot as. Default is \\\"png\\\"\"\n },\n \"quality\": {\n \"type\": \"number\",\n \"minimum\": 0,\n \"maximum\": 100,\n \"description\": \"Compression quality for JPEG and WebP formats (0-100). Higher values mean better quality but larger file sizes. Ignored for PNG format.\"\n },\n \"uid\": {\n \"type\": \"string\",\n \"description\": \"The uid of an element on the page from the page content snapshot. If omitted takes a pages screenshot.\"\n },\n \"fullPage\": {\n \"type\": \"boolean\",\n \"description\": \"If set to true takes a screenshot of the full page instead of the currently visible viewport. Incompatible with uid.\"\n },\n \"filePath\": {\n \"type\": \"string\",\n \"description\": \"The absolute path, or a path relative to the current working directory, to save the screenshot to instead of attaching it to the response.\"\n }\n },\n \"additionalProperties\": false,\n \"$schema\": \"http://json-schema.org/draft-07/schema#\"\n }\n\n- take_snapshot: Take a text snapshot of the currently selected page based on the a11y tree. The snapshot lists page elements along with a unique\nidentifier (uid). Always use the latest snapshot. Prefer taking a snapshot over taking a screenshot. The snapshot indicates the element selected\nin the DevTools Elements panel (if any).\n Input Schema:\n\t\t{\n \"type\": \"object\",\n \"properties\": {\n \"verbose\": {\n \"type\": \"boolean\",\n \"description\": \"Whether to include all possible information available in the full a11y tree. Default is false.\"\n },\n \"filePath\": {\n \"type\": \"string\",\n \"description\": \"The absolute path, or a path relative to the current working directory, to save the snapshot to instead of attaching it to the response.\"\n }\n },\n \"additionalProperties\": false,\n \"$schema\": \"http://json-schema.org/draft-07/schema#\"\n }\n\n- upload_file: Upload a file through a provided element.\n Input Schema:\n\t\t{\n \"type\": \"object\",\n \"properties\": {\n \"uid\": {\n \"type\": \"string\",\n \"description\": \"The uid of the file input element or an element that will open file chooser on the page from the page content snapshot\"\n },\n \"filePath\": {\n \"type\": \"string\",\n \"description\": \"The local path of the file to upload\"\n }\n },\n \"required\": [\n \"uid\",\n \"filePath\"\n ],\n \"additionalProperties\": false,\n \"$schema\": \"http://json-schema.org/draft-07/schema#\"\n }\n\n- wait_for: Wait for the specified text to appear on the selected page.\n Input Schema:\n\t\t{\n \"type\": \"object\",\n \"properties\": {\n \"text\": {\n \"type\": \"string\",\n \"description\": \"Text to appear on the page\"\n },\n \"timeout\": {\n \"type\": \"integer\",\n \"description\": \"Maximum wait time in milliseconds. If set to 0, the default timeout will be used.\"\n }\n },\n \"required\": [\n \"text\"\n ],\n \"additionalProperties\": false,\n \"$schema\": \"http://json-schema.org/draft-07/schema#\"\n }\n## Creating an MCP Server\n\nThe user may ask you something along the lines of \"add a tool\" that does some function, in other words to create an MCP server that provides tools and resources that may connect to external APIs for example. If they do, you should obtain detailed instructions on this topic using the fetch_instructions tool, like this:\n\ncreate_mcp_server\n\n\n====\n\nCAPABILITIES\n\n- You have access to tools that let you execute CLI commands on the user's computer, list files, view source code definitions, regex search, read and write files, and ask follow-up questions. These tools help you effectively accomplish a wide range of tasks, such as writing code, making edits or improvements to existing files, understanding the current state of a project, performing system operations, and much more.\n- When the user initially gives you a task, a recursive list of all filepaths in the current workspace directory ('d:\\ai\\code\\AIClient2API') will be included in environment_details. This provides an overview of the project's file structure, offering key insights into the project from directory/file names (how developers conceptualize and organize their code) and file extensions (the language used). This can also guide decision-making on which files to explore further. If you need to further explore directories such as outside the current workspace directory, you can use the list_files tool. If you pass 'true' for the recursive parameter, it will list files recursively. Otherwise, it will list files at the top level, which is better suited for generic directories where you don't necessarily need the nested structure, like the Desktop.\n- You can use search_files to perform regex searches across files in a specified directory, outputting context-rich results that include surrounding lines. This is particularly useful for understanding code patterns, finding specific implementations, or identifying areas that need refactoring.\n- You can use the list_code_definition_names tool to get an overview of source code definitions for all files at the top level of a specified directory. This can be particularly useful when you need to understand the broader context and relationships between certain parts of the code. You may need to call this tool multiple times to understand various parts of the codebase related to the task.\n - For example, when asked to make edits or improvements you might analyze the file structure in the initial environment_details to get an overview of the project, then use list_code_definition_names to get further insight using source code definitions for files located in relevant directories, then read_file to examine the contents of relevant files, analyze the code and suggest improvements or make necessary edits, then use the apply_diff or write_to_file tool to apply the changes. If you refactored code that could affect other parts of the codebase, you could use search_files to ensure you update other files as needed.\n- You can use the execute_command tool to run commands on the user's computer whenever you feel it can help accomplish the user's task. When you need to execute a CLI command, you must provide a clear explanation of what the command does. Prefer to execute complex CLI commands over creating executable scripts, since they are more flexible and easier to run. Interactive and long-running commands are allowed, since the commands are run in the user's VSCode terminal. The user may keep commands running in the background and you will be kept updated on their status along the way. Each command you execute is run in a new terminal instance.\n- You have access to MCP servers that may provide additional tools and resources. Each server may provide different capabilities that you can use to accomplish tasks more effectively.\n\n\n====\n\nMODES\n\n- These are the currently available modes:\n * \"Architect\" mode (architect) - Use this mode when you need to plan, design, or strategize before implementation. Perfect for breaking down complex problems, creating technical specifications, designing system architecture, or brainstorming solutions before coding.\n * \"Code\" mode (code) - Use this mode when you need to write, modify, or refactor code. Ideal for implementing features, fixing bugs, creating new files, or making code improvements across any programming language or framework.\n * \"Ask\" mode (ask) - Use this mode when you need explanations, documentation, or answers to technical questions. Best for understanding concepts, analyzing existing code, getting recommendations, or learning about technologies without making changes.\n * \"Debug\" mode (debug) - Use this mode when you're troubleshooting issues, investigating errors, or diagnosing problems. Specialized in systematic debugging, adding logging, analyzing stack traces, and identifying root causes before applying fixes.\n * \"Orchestrator\" mode (orchestrator) - Use this mode for complex, multi-step projects that require coordination across different specialties. Ideal when you need to break down large tasks into subtasks, manage workflows, or coordinate work that spans multiple domains or expertise areas.\nIf the user asks you to create or edit a new mode for this project, you should read the instructions by using the fetch_instructions tool, like this:\n\ncreate_mode\n\n\n\n====\n\nRULES\n\n- The project base directory is: d:/ai/code/AIClient2API\n- All file paths must be relative to this directory. However, commands may change directories in terminals, so respect working directory specified by the response to .\n- You cannot `cd` into a different directory to complete a task. You are stuck operating from 'd:/ai/code/AIClient2API', so be sure to pass in the correct 'path' parameter when using tools that require a path.\n- Do not use the ~ character or $HOME to refer to the home directory.\n- Before using the execute_command tool, you must first think about the SYSTEM INFORMATION context provided to understand the user's environment and tailor your commands to ensure they are compatible with their system. You must also consider if the command you need to run should be executed in a specific directory outside of the current working directory 'd:/ai/code/AIClient2API', and if so prepend with `cd`'ing into that directory && then executing the command (as one command since you are stuck operating from 'd:/ai/code/AIClient2API'). For example, if you needed to run `npm install` in a project outside of 'd:/ai/code/AIClient2API', you would need to prepend with a `cd` i.e. pseudocode for this would be `cd (path to project) && (command, in this case npm install)`.\n- When using the search_files tool, craft your regex patterns carefully to balance specificity and flexibility. Based on the user's task you may use it to find code patterns, TODO comments, function definitions, or any text-based information across the project. The results include context, so analyze the surrounding code to better understand the matches. Leverage the search_files tool in combination with other tools for more comprehensive analysis. For example, use it to find specific code patterns, then use read_file to examine the full context of interesting matches before using apply_diff or write_to_file to make informed changes.\n- When creating a new project (such as an app, website, or any software project), organize all new files within a dedicated project directory unless the user specifies otherwise. Use appropriate file paths when writing files, as the write_to_file tool will automatically create any necessary directories. Structure the project logically, adhering to best practices for the specific type of project being created. Unless otherwise specified, new projects should be easily run without additional setup, for example most projects can be built in HTML, CSS, and JavaScript - which you can open in a browser.\n- For editing files, you have access to these tools: apply_diff (for surgical edits - targeted changes to specific lines or functions), write_to_file (for creating new files or complete file rewrites), insert_content (for adding lines to files), search_and_replace (for finding and replacing individual pieces of text).\n- The insert_content tool adds lines of text to files at a specific line number, such as adding a new function to a JavaScript file or inserting a new route in a Python file. Use line number 0 to append at the end of the file, or any positive number to insert before that line.\n- The search_and_replace tool finds and replaces text or regex in files. This tool allows you to search for a specific regex pattern or text and replace it with another value. Be cautious when using this tool to ensure you are replacing the correct text. It can support multiple operations at once.\n- You should always prefer using other editing tools over write_to_file when making changes to existing files since write_to_file is much slower and cannot handle large files.\n- When using the write_to_file tool to modify a file, use the tool directly with the desired content. You do not need to display the content before using the tool. ALWAYS provide the COMPLETE file content in your response. This is NON-NEGOTIABLE. Partial updates or placeholders like '// rest of code unchanged' are STRICTLY FORBIDDEN. You MUST include ALL parts of the file, even if they haven't been modified. Failure to do so will result in incomplete or broken code, severely impacting the user's project.\n- Some modes have restrictions on which files they can edit. If you attempt to edit a restricted file, the operation will be rejected with a FileRestrictionError that will specify which file patterns are allowed for the current mode.\n- Be sure to consider the type of project (e.g. Python, JavaScript, web application) when determining the appropriate structure and files to include. Also consider what files may be most relevant to accomplishing the task, for example looking at a project's manifest file would help you understand the project's dependencies, which you could incorporate into any code you write.\n * For example, in architect mode trying to edit app.js would be rejected because architect mode can only edit files matching \"\\.md$\"\n- When making changes to code, always consider the context in which the code is being used. Ensure that your changes are compatible with the existing codebase and that they follow the project's coding standards and best practices.\n- Do not ask for more information than necessary. Use the tools provided to accomplish the user's request efficiently and effectively. When you've completed your task, you must use the attempt_completion tool to present the result to the user. The user may provide feedback, which you can use to make improvements and try again.\n- You are only allowed to ask the user questions using the ask_followup_question tool. Use this tool only when you need additional details to complete a task, and be sure to use a clear and concise question that will help you move forward with the task. When you ask a question, provide the user with 2-4 suggested answers based on your question so they don't need to do so much typing. The suggestions should be specific, actionable, and directly related to the completed task. They should be ordered by priority or logical sequence. However if you can use the available tools to avoid having to ask the user questions, you should do so. For example, if the user mentions a file that may be in an outside directory like the Desktop, you should use the list_files tool to list the files in the Desktop and check if the file they are talking about is there, rather than asking the user to provide the file path themselves.\n- When executing commands, if you don't see the expected output, assume the terminal executed the command successfully and proceed with the task. The user's terminal may be unable to stream the output back properly. If you absolutely need to see the actual terminal output, use the ask_followup_question tool to request the user to copy and paste it back to you.\n- The user may provide a file's contents directly in their message, in which case you shouldn't use the read_file tool to get the file contents again since you already have it.\n- Your goal is to try to accomplish the user's task, NOT engage in a back and forth conversation.\n- NEVER end attempt_completion result with a question or request to engage in further conversation! Formulate the end of your result in a way that is final and does not require further input from the user.\n- You are STRICTLY FORBIDDEN from starting your messages with \"Great\", \"Certainly\", \"Okay\", \"Sure\". You should NOT be conversational in your responses, but rather direct and to the point. For example you should NOT say \"Great, I've updated the CSS\" but instead something like \"I've updated the CSS\". It is important you be clear and technical in your messages.\n- When presented with images, utilize your vision capabilities to thoroughly examine them and extract meaningful information. Incorporate these insights into your thought process as you accomplish the user's task.\n- At the end of each user message, you will automatically receive environment_details. This information is not written by the user themselves, but is auto-generated to provide potentially relevant context about the project structure and environment. While this information can be valuable for understanding the project context, do not treat it as a direct part of the user's request or response. Use it to inform your actions and decisions, but don't assume the user is explicitly asking about or referring to this information unless they clearly do so in their message. When using environment_details, explain your actions clearly to ensure the user understands, as they may not be aware of these details.\n- Before executing commands, check the \"Actively Running Terminals\" section in environment_details. If present, consider how these active processes might impact your task. For example, if a local development server is already running, you wouldn't need to start it again. If no active terminals are listed, proceed with command execution as normal.\n- MCP operations should be used one at a time, similar to other tool usage. Wait for confirmation of success before proceeding with additional operations.\n- It is critical you wait for the user's response after each tool use, in order to confirm the success of the tool use. For example, if asked to make a todo app, you would create a file, wait for the user's response it was created successfully, then create another file if needed, wait for the user's response it was created successfully, etc.\n\n====\n\nSYSTEM INFORMATION\n\nOperating System: Windows 11\nDefault Shell: C:\\Windows\\System32\\cmd.exe\nHome Directory: C:/Users/home\nCurrent Workspace Directory: d:/ai/code/AIClient2API\n\nThe Current Workspace Directory is the active VS Code project directory, and is therefore the default directory for all tool operations. New terminals will be created in the current workspace directory, however if you change directories in a terminal it will then have a different working directory; changing directories in a terminal does not modify the workspace directory, because you do not have access to change the workspace directory. When the user initially gives you a task, a recursive list of all filepaths in the current workspace directory ('/test/path') will be included in environment_details. This provides an overview of the project's file structure, offering key insights into the project from directory/file names (how developers conceptualize and organize their code) and file extensions (the language used). This can also guide decision-making on which files to explore further. If you need to further explore directories such as outside the current workspace directory, you can use the list_files tool. If you pass 'true' for the recursive parameter, it will list files recursively. Otherwise, it will list files at the top level, which is better suited for generic directories where you don't necessarily need the nested structure, like the Desktop.\n\n====\n\nOBJECTIVE\n\nYou accomplish a given task iteratively, breaking it down into clear steps and working through them methodically.\n\n1. Analyze the user's task and set clear, achievable goals to accomplish it. Prioritize these goals in a logical order.\n2. Work through these goals sequentially, utilizing available tools one at a time as necessary. Each goal should correspond to a distinct step in your problem-solving process. You will be informed on the work completed and what's remaining as you go.\n3. Remember, you have extensive capabilities with access to a wide range of tools that can be used in powerful and clever ways as necessary to accomplish each goal. Before calling a tool, do some analysis within tags. First, analyze the file structure provided in environment_details to gain context and insights for proceeding effectively. Next, think about which of the provided tools is the most relevant tool to accomplish the user's task. Go through each of the required parameters of the relevant tool and determine if the user has directly provided or given enough information to infer a value. When deciding if the parameter can be inferred, carefully consider all the context to see if it supports a specific value. If all of the required parameters are present or can be reasonably inferred, close the thinking tag and proceed with the tool use. BUT, if one of the values for a required parameter is missing, DO NOT invoke the tool (not even with fillers for the missing params) and instead, ask the user to provide the missing parameters using the ask_followup_question tool. DO NOT ask for more information on optional parameters if it is not provided.\n4. Once you've completed the user's task, you must use the attempt_completion tool to present the result of the task to the user.\n5. The user may provide feedback, which you can use to make improvements and try again. But DO NOT continue in pointless back and forth conversations, i.e. don't end your responses with questions or offers for further assistance.\n\n\n====\n\nUSER'S CUSTOM INSTRUCTIONS\n\nThe following additional instructions are provided by the user, and should be followed to the best of your ability without interfering with the TOOL USE guidelines.\n\nLanguage Preference:\nYou should always speak and think in the \"简体中文\" (zh-CN) language unless the user gives you instructions below to do otherwise."},{"role":"user","content":[{"type":"text","text":"\n更新readme,写入一行随机数\n"},{"type":"text","text":"\n# VSCode Visible Files\nprovider_pools.json,src\\example\\openai\\oldResponseChunk1762501528837.json\n\n# VSCode Open Tabs\nsrc/claude/claude-kiro.js,src/openai/qwen-core.js,config.json,provider_pools.json,src/converters/ConverterFactory.js,src/gemini/gemini-core.js,src/provider-pool-manager.js,src/example/openai/oldResponseChunk1762501528837.json\n\n# Current Time\nCurrent time in ISO 8601 UTC format: 2025-11-07T07:46:41.969Z\nUser time zone: Asia/Shanghai, UTC+8:00\n\n# Current Cost\n$0.00\n\n# Current Mode\ncode\nCode\nZhipuAI/GLM-4.6\n\n\n# Current Workspace Directory (d:/ai/code/AIClient2API) Files\n.babelrc\n.dockerignore\n.gitignore\nARCHITECTURE_DESIGN.md\nconfig.json.example\nDockerfile\nhealthcheck.js\nIMPLEMENTATION_PLAN.md\njest.config.js\nLICENSE\noldResponseChunk1762501485510.json\noldResponseChunk1762501490049.json\noldResponseChunk1762501490213.json\noldResponseChunk1762501530854.json\noldResponseChunk1762501531523.json\noldResponseChunk1762501531679.json\noriginalRequestBody1762501468953.json\noriginalRequestBody1762501487965.json\noriginalRequestBody1762501490052.json\noriginalRequestBody1762501528840.json\noriginalRequestBody1762501530857.json\noriginalRequestBody1762501531527.json\npackage-lock.json\npackage.json\nprovider_pools.json.example\nREADME-JA.md\nREADME-ZH.md\nREADME.md\nREFACTORING_GUIDE.md\nREFACTORING_README.md\nREFACTORING_SUMMARY.md\nrun-docker.bat\nrun-docker.sh\nUSAGE_EXAMPLES.md\n.kilocode/\n.specify/\nsrc/\nsrc/adapter.js\nsrc/api-server.js\nsrc/common.js\nsrc/convert-new.js\nsrc/convert.js\nsrc/provider-pool-manager.js\nsrc/provider-strategies.js\nsrc/provider-strategy.js\nsrc/claude/\nsrc/claude/claude-core.js\nsrc/claude/claude-kiro.js\nsrc/claude/claude-strategy.js\nsrc/converters/\nsrc/converters/BaseConverter.js\nsrc/converters/ConverterFactory.js\nsrc/converters/utils.js\nsrc/converters/strategies/\nsrc/converters/strategies/ClaudeConverter.js\nsrc/converters/strategies/GeminiConverter.js\nsrc/converters/strategies/OpenAIConverter.js\nsrc/converters/strategies/OpenAIResponsesConverter.js\nsrc/example/\nsrc/example/claude/\nsrc/example/claude/oldResponse1762492737297.json\nsrc/example/claude/oldResponseChunk1762492475714.json\nsrc/example/claude/originalRequestBody1762492473859.json\nsrc/example/gemini/\nsrc/example/openai/\nsrc/example/openai/oldResponseChunk1762501528837.json\nsrc/example/openai/originalRequestBody1762501497434.json\nsrc/example/openaiResponses/\nsrc/example/openaiResponses/oldResponseChunk1762500869734.json\nsrc/example/openaiResponses/originalRequestBody1762500856556.json\nsrc/gemini/\nsrc/gemini/gemini-core.js\nsrc/gemini/gemini-strategy.js\nsrc/img/\nsrc/img/logo-min.webp\nsrc/img/logo.webp\nsrc/openai/\nsrc/openai/openai-core.js\nsrc/openai/openai-responses-core.js\nsrc/openai/openai-responses-core.mjs\nsrc/openai/openai-responses-strategy.js\nsrc/openai/openai-strategy.js\nsrc/openai/qwen-core.js\ntests/\ntests/api-integration.test.js\nYou have not created a todo list yet. Create one with `update_todo_list` if your task is complicated or involves multiple steps.\n"},{"type":"text","text":"\n# VSCode Visible Files\nsrc\\example\\openai\\oldResponseChunk1762501528837.json,provider_pools.json\n\n# VSCode Open Tabs\nsrc/claude/claude-kiro.js,src/openai/qwen-core.js,config.json,provider_pools.json,src/converters/ConverterFactory.js,src/gemini/gemini-core.js,src/provider-pool-manager.js,src/example/openai/oldResponseChunk1762501528837.json\n\n# Current Time\nCurrent time in ISO 8601 UTC format: 2025-11-07T07:47:33.020Z\nUser time zone: Asia/Shanghai, UTC+8:00\n\n# Current Cost\n$0.00\n\n# Current Mode\ncode\nCode\nZhipuAI/GLM-4.6\n\n\n# Current Workspace Directory (d:/ai/code/AIClient2API) Files\n.babelrc\n.dockerignore\n.gitignore\nARCHITECTURE_DESIGN.md\nconfig.json.example\nDockerfile\nhealthcheck.js\nIMPLEMENTATION_PLAN.md\njest.config.js\nLICENSE\npackage-lock.json\npackage.json\nprovider_pools.json.example\nREADME-JA.md\nREADME-ZH.md\nREADME.md\nREFACTORING_GUIDE.md\nREFACTORING_README.md\nREFACTORING_SUMMARY.md\nrun-docker.bat\nrun-docker.sh\nUSAGE_EXAMPLES.md\n.kilocode/\n.specify/\nsrc/\nsrc/adapter.js\nsrc/api-server.js\nsrc/common.js\nsrc/convert-new.js\nsrc/convert.js\nsrc/provider-pool-manager.js\nsrc/provider-strategies.js\nsrc/provider-strategy.js\nsrc/claude/\nsrc/claude/claude-core.js\nsrc/claude/claude-kiro.js\nsrc/claude/claude-strategy.js\nsrc/converters/\nsrc/converters/BaseConverter.js\nsrc/converters/ConverterFactory.js\nsrc/converters/utils.js\nsrc/converters/strategies/\nsrc/converters/strategies/ClaudeConverter.js\nsrc/converters/strategies/GeminiConverter.js\nsrc/converters/strategies/OpenAIConverter.js\nsrc/converters/strategies/OpenAIResponsesConverter.js\nsrc/example/\nsrc/example/claude/\nsrc/example/claude/oldResponse1762492737297.json\nsrc/example/claude/oldResponseChunk1762492475714.json\nsrc/example/claude/originalRequestBody1762492473859.json\nsrc/example/gemini/\nsrc/example/openai/\nsrc/example/openai/oldResponseChunk1762501528837.json\nsrc/example/openai/originalRequestBody1762501497434.json\nsrc/example/openaiResponses/\nsrc/example/openaiResponses/oldResponseChunk1762500869734.json\nsrc/example/openaiResponses/originalRequestBody1762500856556.json\nsrc/gemini/\nsrc/gemini/gemini-core.js\nsrc/gemini/gemini-strategy.js\nsrc/img/\nsrc/img/logo-min.webp\nsrc/img/logo.webp\nsrc/openai/\nsrc/openai/openai-core.js\nsrc/openai/openai-responses-core.js\nsrc/openai/openai-responses-core.mjs\nsrc/openai/openai-responses-strategy.js\nsrc/openai/openai-strategy.js\nsrc/openai/qwen-core.js\ntests/\ntests/api-integration.test.js\nYou have not created a todo list yet. Create one with `update_todo_list` if your task is complicated or involves multiple steps.\n"}]},{"role":"assistant","content":"\n\n用户要求更新 README 文件,并写入一行随机数。我需要:\n\n1. 首先读取现有的 README.md 文件,了解其当前内容\n2. 生成一个随机数\n3. 将随机数添加到 README 文件中\n\n让我先读取 README.md 文件来了解其结构和内容。\n\n\n我需要更新 README 文件并添加一行随机数。首先让我读取当前的 README.md 文件内容。\n\n\n\n \n README.md\n \n\n"},{"role":"user","content":[{"type":"text","text":"[read_file for 'README.md'. Reading multiple files at once is more efficient for the LLM. If other files are relevant to your current task, please read them simultaneously.] Result:"},{"type":"text","text":"\nREADME.md\n\n 1 |
\r\n 2 | \r\n 3 | ![logo](src/img/logo-min.webp)\r\n 4 | \r\n 5 | # AIClient-2-API 🚀\r\n 6 | \r\n 7 | **A powerful proxy that can unify the requests of various large model APIs (Gemini CLI, Qwen Code Plus, Kiro Claude...) that are only used within the client into a local OpenAI compatible interface.**\r\n 8 | \r\n 9 |
\r\n 10 | \r\n 11 |
\r\n 12 | \r\n 13 | \"Ask\r\n 14 | \r\n 15 | [![License: GPL v3](https://img.shields.io/badge/License-GPLv3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0)\r\n 16 | [![Node.js](https://img.shields.io/badge/Node.js-≥20.0.0-green.svg)](https://nodejs.org/)\r\n 17 | [![Docker](https://img.shields.io/badge/docker-≥20.0.0-blue.svg)](https://aiproxy.justlikemaki.vip/en/docs/installation/docker-deployment.html)\r\n 18 | \r\n 19 | \r\n 20 | [**中文**](./README-ZH.md) | [**English**](./README.md) | [**日本語**](./README-JA.md) | [**📚 Complete Documentation**](https://aiproxy.justlikemaki.vip/en/)\r\n 21 | \r\n 22 |
\r\n 23 | \r\n 24 | `AIClient2API` is an API proxy service that breaks through client limitations, converting free large models originally restricted to client use only (such as Gemini CLI, Qwen Code Plus, Kiro Claude) into standard OpenAI-compatible interfaces that can be called by any application. Built on Node.js, it supports intelligent conversion between three major protocols (OpenAI, Claude, Gemini), enabling tools like Cherry-Studio, NextChat, and Cline to freely use advanced models such as Claude Sonnet 4.5, Gemini 2.5 Flash, and Qwen3 Coder Plus at scale. The project adopts a modular architecture based on strategy and adapter patterns, with built-in account pool management, intelligent polling, automatic failover, and health check mechanisms, ensuring 99.9% service availability.\r\n 25 | \r\n 26 | > [!NOTE]\r\n 27 | > **🎉 Important Milestone**\r\n 28 | >\r\n 29 | > - Thanks to Ruan Yifeng for the recommendation in [Weekly Issue 359](https://www.ruanyifeng.com/blog/2025/08/weekly-issue-359.html)\r\n 30 | >\r\n 31 | > **📅 Version Update Log**\r\n 32 | >\r\n 33 | > - **2025.11.06** - Added support for Gemini 3 Preview, enhanced model compatibility and performance optimization\r\n 34 | > - **2025.10.18** - Kiro open registration, new accounts get 500 credits, full support for Claude Sonnet 4.5\r\n 35 | > - **2025.09.01** - Integrated Qwen Code CLI, added `qwen3-coder-plus` model support\r\n 36 | > - **2025.08.29** - Released account pool management feature, supporting multi-account polling, intelligent failover, and automatic degradation strategies\r\n 37 | > - Configuration: Add `PROVIDER_POOLS_FILE_PATH` parameter in config.json\r\n 38 | > - Reference configuration: [provider_pools.json](./provider_pools.json.example)\r\n 39 | \r\n 40 | ---\r\n 41 | \r\n 42 | ## 💡 Core Advantages\r\n 43 | \r\n 44 | ### 🎯 Unified Access, One-Stop Management\r\n 45 | * **Multi-Model Unified Interface**: Through standard OpenAI-compatible protocol, configure once to access mainstream large models including Gemini, Claude, GPT, Qwen Code, Kimi K2, GLM-4.6\r\n 46 | * **Flexible Switching Mechanism**: Support dynamic model switching via startup parameters, Path routing, or environment variables to meet different scenario requirements\r\n 47 | * **Zero-Cost Migration**: Fully compatible with OpenAI API specifications, tools like Cherry-Studio, NextChat, Cline can be used without modification\r\n 48 | * **Multi-Protocol Intelligent Conversion**: Support intelligent conversion between OpenAI, Claude, and Gemini protocols for cross-protocol model invocation\r\n 49 | * Call Claude models using OpenAI protocol: Use `claude-custom` or `claude-kiro-oauth` providers\r\n 50 | * Call Gemini models using OpenAI protocol: Use `gemini-cli-oauth` provider\r\n 51 | * Call Gemini models using Claude protocol: Use `gemini-cli-oauth` provider\r\n 52 | * Call OpenAI models using Claude protocol: Use `openai-custom` or `openai-qwen-oauth` providers\r\n 53 | \r\n 54 | ### 🚀 Break Through Limitations, Improve Efficiency\r\n 55 | * **Bypass Official Restrictions**: Utilize OAuth authorization mechanism to effectively break through rate and quota limits of free APIs like Gemini\r\n 56 | * **Free Advanced Models**: Use Claude Sonnet 4.5 for free via Kiro API mode, use Qwen3 Coder Plus via Qwen OAuth mode, reducing usage costs\r\n 57 | * **Intelligent Account Pool Scheduling**: Support multi-account polling, automatic failover, and configuration degradation, ensuring 99.9% service availability\r\n 58 | \r\n 59 | ### 🛡️ Secure and Controllable, Data Transparent\r\n 60 | * **Full-Chain Log Recording**: Capture all request and response data, supporting auditing and debugging\r\n 61 | * **Private Dataset Construction**: Quickly build proprietary training datasets based on log data\r\n 62 | * **System Prompt Management**: Support override and append modes, achieving perfect combination of unified base instructions and personalized extensions\r\n 63 | \r\n 64 | ### 🔧 Developer-Friendly, Easy to Extend\r\n 65 | * **Modular Architecture**: Based on strategy and adapter patterns, adding new model providers requires only 3 steps\r\n 66 | * **Complete Test Coverage**: Integration and unit test coverage 90%+, ensuring code quality\r\n 67 | * **Containerized Deployment**: Provides Docker support, one-click deployment, cross-platform operation\r\n 68 | * **MCP Protocol Support**: Perfectly compatible with Model Context Protocol, easily extend functionality\r\n 69 | \r\n 70 | ---\r\n 71 | \r\n 72 | ## 📑 Quick Navigation\r\n 73 | \r\n 74 | - [🐳 Docker Deployment](https://aiproxy.justlikemaki.vip/en/docs/installation/docker-deployment.html)\r\n 75 | - [🎨 Model Protocol and Provider Relationship Diagram](#-model-protocol-and-provider-relationship-diagram)\r\n 76 | - [🔧 Usage Instructions](#-usage-instructions)\r\n 77 | - [🚀 Project Startup Parameters](#-project-startup-parameters)\r\n 78 | - [📄 Open Source License](#-open-source-license)\r\n 79 | - [🙏 Acknowledgements](#-acknowledgements)\r\n 80 | - [⚠️ Disclaimer](#-disclaimer)\r\n 81 | \r\n 82 | ---\r\n 83 | \r\n 84 | ## 🎨 Model Protocol and Provider Relationship Diagram\r\n 85 | \r\n 86 | This project supports multiple model providers through different protocols. The following is an overview of their relationships:\r\n 87 | \r\n 88 | * **OpenAI Protocol (P_OPENAI)**: Implemented by `openai-custom`, `gemini-cli-oauth`, `claude-custom`, `claude-kiro-oauth`, and `openai-qwen-oauth` model providers.\r\n 89 | * **Claude Protocol (P_CLAUDE)**: Implemented by `claude-custom`, `claude-kiro-oauth`, `gemini-cli-oauth`, `openai-custom`, and `openai-qwen-oauth` model providers.\r\n 90 | * **Gemini Protocol (P_GEMINI)**: Implemented by `gemini-cli-oauth` model provider.\r\n 91 | \r\n 92 | Detailed relationship diagram:\r\n 93 | \r\n 94 | ```mermaid\r\n 95 | \r\n 96 | graph TD\r\n 97 | subgraph Core_Protocols[\"Core Protocols\"]\r\n 98 | P_OPENAI[OpenAI Protocol]\r\n 99 | P_GEMINI[Gemini Protocol]\r\n100 | P_CLAUDE[Claude Protocol]\r\n101 | end\r\n102 | \r\n103 | subgraph Supported_Model_Providers[\"Supported Model Providers\"]\r\n104 | MP_OPENAI[openai-custom]\r\n105 | MP_GEMINI[gemini-cli-oauth]\r\n106 | MP_CLAUDE_C[claude-custom]\r\n107 | MP_CLAUDE_K[claude-kiro-oauth]\r\n108 | MP_QWEN[openai-qwen-oauth]\r\n109 | end\r\n110 | \r\n111 | P_OPENAI ---|Support| MP_OPENAI\r\n112 | P_OPENAI ---|Support| MP_QWEN\r\n113 | P_OPENAI ---|Support| MP_GEMINI\r\n114 | P_OPENAI ---|Support| MP_CLAUDE_C\r\n115 | P_OPENAI ---|Support| MP_CLAUDE_K\r\n116 | \r\n117 | P_GEMINI ---|Support| MP_GEMINI\r\n118 | \r\n119 | P_CLAUDE ---|Support| MP_CLAUDE_C\r\n120 | P_CLAUDE ---|Support| MP_CLAUDE_K\r\n121 | P_CLAUDE ---|Support| MP_GEMINI\r\n122 | P_CLAUDE ---|Support| MP_OPENAI\r\n123 | P_CLAUDE ---|Support| MP_QWEN\r\n124 | \r\n125 | style P_OPENAI fill:#f9f,stroke:#333,stroke-width:2px\r\n126 | style P_GEMINI fill:#ccf,stroke:#333,stroke-width:2px\r\n127 | style P_CLAUDE fill:#cfc,stroke:#333,stroke-width:2px\r\n128 | \r\n129 | ```\r\n130 | \r\n131 | ---\r\n132 | \r\n133 | ## 🔧 Usage Instructions\r\n134 | \r\n135 | ### 📋 Core Features\r\n136 | \r\n137 | #### MCP Protocol Support\r\n138 | This project is fully compatible with **Model Context Protocol (MCP)**, enabling seamless integration with MCP-supporting clients for powerful functional extensions.\r\n139 | \r\n140 | #### Multimodal Input Capabilities\r\n141 | Supports various input types including images and documents, providing richer interactive experiences and more powerful application scenarios.\r\n142 | \r\n143 | #### Latest Model Support\r\n144 | Seamlessly supports the following latest large models, simply configure the corresponding OpenAI or Claude compatible interface in [`config.json`](./config.json):\r\n145 | * **Kimi K2** - Moonshot AI's latest flagship model\r\n146 | * **GLM-4.5** - Zhipu AI's latest version\r\n147 | * **Qwen Code** - Alibaba Tongyi Qianwen code-specific model\r\n148 | \r\n149 | ---\r\n150 | \r\n151 | ### 🔐 Authorization Configuration Guide\r\n152 | \r\n153 | #### Gemini CLI OAuth Configuration\r\n154 | 1. **Obtain OAuth Credentials**: Visit [Google Cloud Console](https://console.cloud.google.com/) to create a project and enable Gemini API\r\n155 | 2. **First Authorization**: After using Gemini service, the command line will print Google authorization page, copy the page to browser for authorization, then return to command line\r\n156 | 3. **Credential Storage**: After successful authorization, `oauth_creds.json` file will be automatically generated and saved to `~/.gemini` directory\r\n157 | 4. **Project Configuration**: Need to provide a valid Google Cloud project ID, can be specified via startup parameter `--project-id`\r\n158 | \r\n159 | #### Qwen Code OAuth Configuration\r\n160 | 1. **First Authorization**: After starting the service, the system will automatically open the authorization page in the browser\r\n161 | 2. **Credential Storage**: After successful authorization, `oauth_creds.json` file will be automatically generated and saved to `~/.qwen` directory\r\n162 | 3. **Recommended Parameters**: Use official default parameters for best results\r\n163 | ```json\r\n164 | {\r\n165 | \"temperature\": 0,\r\n166 | \"top_p\": 1\r\n167 | }\r\n168 | ```\r\n169 | \r\n170 | #### Kiro API Configuration\r\n171 | 1. **Environment Preparation**: [Download and install Kiro client](https://aibook.ren/archives/kiro-install)\r\n172 | 2. **Complete Authorization**: Log in to your account in the client to generate `kiro-auth-token.json` credential file\r\n173 | 3. **Best Practice**: Recommended to use with **Claude Code** for optimal experience\r\n174 | 4. **Important Notice**: Kiro service usage policy has been updated, please visit the official website for the latest usage restrictions and terms\r\n175 | \r\n176 | #### OpenAI Responses API\r\n177 | * **Application Scenario**: Suitable for scenarios requiring structured dialogue using OpenAI Responses API, such as Codex\r\n178 | * **Configuration Method**:\r\n179 | * Method 1: Set `MODEL_PROVIDER` to `openaiResponses-custom` in [`config.json`](./config.json)\r\n180 | * Method 2: Use startup parameter `--model-provider openaiResponses-custom`\r\n181 | * Method 3: Use path routing `/openaiResponses-custom`\r\n182 | * **Required Parameters**: Provide valid API key and base URL\r\n183 | \r\n184 | ---\r\n185 | \r\n186 | ### 🔄 Model Provider Switching\r\n187 | \r\n188 | This project provides two flexible model switching methods to meet different usage scenario requirements.\r\n189 | \r\n190 | #### Method 1: Startup Parameter Switching\r\n191 | \r\n192 | Specify the default model provider via command line parameters:\r\n193 | \r\n194 | ```bash\r\n195 | # Use Gemini provider\r\n196 | node src/api-server.js --model-provider gemini-cli-oauth --project-id your-project-id\r\n197 | \r\n198 | # Use Claude Kiro provider\r\n199 | node src/api-server.js --model-provider claude-kiro-oauth\r\n200 | \r\n201 | # Use Qwen provider\r\n202 | node src/api-server.js --model-provider openai-qwen-oauth\r\n203 | ```\r\n204 | \r\n205 | **Available Model Provider Identifiers**:\r\n206 | - `openai-custom` - Standard OpenAI API\r\n207 | - `claude-custom` - Official Claude API\r\n208 | - `gemini-cli-oauth` - Gemini CLI OAuth\r\n209 | - `claude-kiro-oauth` - Kiro Claude OAuth\r\n210 | - `openai-qwen-oauth` - Qwen Code OAuth\r\n211 | - `openaiResponses-custom` - OpenAI Responses API\r\n212 | \r\n213 | #### Method 2: Path Routing Switching (Recommended)\r\n214 | \r\n215 | Achieve instant switching by specifying provider identifier in API request path:\r\n216 | \r\n217 | | Route Path | Description | Use Case |\r\n218 | |---------|------|---------|\r\n219 | | `/claude-custom` | Use Claude API from config file | Official Claude API calls |\r\n220 | | `/claude-kiro-oauth` | Access Claude via Kiro OAuth | Free use of Claude Sonnet 4.5 |\r\n221 | | `/openai-custom` | Use OpenAI provider to handle requests | Standard OpenAI API calls |\r\n222 | | `/gemini-cli-oauth` | Access via Gemini CLI OAuth | Break through Gemini free limits |\r\n223 | | `/openai-qwen-oauth` | Access via Qwen OAuth | Use Qwen Code Plus |\r\n224 | | `/openaiResponses-custom` | OpenAI Responses API | Structured dialogue scenarios |\r\n225 | \r\n226 | **Usage Examples**:\r\n227 | ```bash\r\n228 | # Configure in programming agents like Cline, Kilo\r\n229 | API_ENDPOINT=http://localhost:3000/claude-kiro-oauth\r\n230 | \r\n231 | # Direct API call\r\n232 | curl http://localhost:3000/gemini-cli-oauth/v1/chat/completions \\\r\n233 | -H \"Content-Type: application/json\" \\\r\n234 | -d '{\"model\":\"gemini-2.0-flash-exp\",\"messages\":[...]}'\r\n235 | ```\r\n236 | \r\n237 | ---\r\n238 | \r\n239 | ### 📁 Authorization File Storage Paths\r\n240 | \r\n241 | Default storage locations for authorization credential files of each service:\r\n242 | \r\n243 | | Service | Default Path | Description |\r\n244 | |------|---------|------|\r\n245 | | **Gemini** | `~/.gemini/oauth_creds.json` | OAuth authentication credentials |\r\n246 | | **Kiro** | `~/.aws/sso/cache/kiro-auth-token.json` | Kiro authentication token |\r\n247 | | **Qwen** | `~/.qwen/oauth_creds.json` | Qwen OAuth credentials |\r\n248 | \r\n249 | > **Note**: `~` represents the user home directory (Windows: `C:\\Users\\username`, Linux/macOS: `/home/username` or `/Users/username`)\r\n250 | >\r\n251 | > **Custom Path**: Can specify custom storage location via relevant parameters in configuration file or environment variables\r\n252 | \r\n253 | ---\r\n254 | \r\n255 | ## 🚀 Project Startup Parameters\r\n256 | \r\n257 | This project supports rich command-line parameter configuration, allowing flexible adjustment of service behavior as needed. The following is a detailed explanation of all startup parameters, displayed in functional groups:\r\n258 | \r\n259 | ### 🔧 Server Configuration Parameters\r\n260 | \r\n261 | | Parameter | Type | Default Value | Description |\r\n262 | |------|------|--------|------|\r\n263 | | `--host` | string | localhost | Server listening address |\r\n264 | | `--port` | number | 3000 | Server listening port |\r\n265 | | `--api-key` | string | 123456 | API key for authentication |\r\n266 | \r\n267 | ### 🤖 Model Provider Configuration Parameters\r\n268 | \r\n269 | | Parameter | Type | Default Value | Description |\r\n270 | |------|------|--------|------|\r\n271 | | `--model-provider` | string | gemini-cli-oauth | AI model provider, optional values: openai-custom, claude-custom, gemini-cli-oauth, claude-kiro-oauth, openai-qwen-oauth |\r\n272 | \r\n273 | ### 🧠 OpenAI Compatible Provider Parameters\r\n274 | \r\n275 | | Parameter | Type | Default Value | Description |\r\n276 | |------|------|--------|------|\r\n277 | | `--openai-api-key` | string | null | OpenAI API key (required when `model-provider` is `openai-custom`) |\r\n278 | | `--openai-base-url` | string | null | OpenAI API base URL (required when `model-provider` is `openai-custom`) |\r\n279 | \r\n280 | ### 🖥️ Claude Compatible Provider Parameters\r\n281 | \r\n282 | | Parameter | Type | Default Value | Description |\r\n283 | |------|------|--------|------|\r\n284 | | `--claude-api-key` | string | null | Claude API key (required when `model-provider` is `claude-custom`) |\r\n285 | | `--claude-base-url` | string | null | Claude API base URL (required when `model-provider` is `claude-custom`) |\r\n286 | \r\n287 | ### 🔐 Gemini OAuth Authentication Parameters\r\n288 | \r\n289 | | Parameter | Type | Default Value | Description |\r\n290 | |------|------|--------|------|\r\n291 | | `--gemini-oauth-creds-base64` | string | null | Base64 string of Gemini OAuth credentials (optional when `model-provider` is `gemini-cli-oauth`, choose one with `--gemini-oauth-creds-file`) |\r\n292 | | `--gemini-oauth-creds-file` | string | null | Gemini OAuth credentials JSON file path (optional when `model-provider` is `gemini-cli-oauth`, choose one with `--gemini-oauth-creds-base64`) |\r\n293 | | `--project-id` | string | null | Google Cloud project ID (required when `model-provider` is `gemini-cli-oauth`) |\r\n294 | \r\n295 | ### 🎮 Kiro OAuth Authentication Parameters\r\n296 | \r\n297 | | Parameter | Type | Default Value | Description |\r\n298 | |------|------|--------|------|\r\n299 | | `--kiro-oauth-creds-base64` | string | null | Base64 string of Kiro OAuth credentials (optional when `model-provider` is `claude-kiro-oauth`, choose one with `--kiro-oauth-creds-file`) |\r\n300 | | `--kiro-oauth-creds-file` | string | null | Kiro OAuth credentials JSON file path (optional when `model-provider` is `claude-kiro-oauth`, choose one with `--kiro-oauth-creds-base64`) |\r\n301 | \r\n302 | ### 🐼 Qwen OAuth Authentication Parameters\r\n303 | \r\n304 | | Parameter | Type | Default Value | Description |\r\n305 | |------|------|--------|------|\r\n306 | | `--qwen-oauth-creds-file` | string | null | Qwen OAuth credentials JSON file path (required when `model-provider` is `openai-qwen-oauth`) |\r\n307 | \r\n308 | ### 🔄 OpenAI Responses API Parameters\r\n309 | \r\n310 | | Parameter | Type | Default Value | Description |\r\n311 | |------|------|--------|------|\r\n312 | | `--model-provider` | string | openaiResponses-custom | Model provider, set to `openaiResponses-custom` when using OpenAI Responses API |\r\n313 | | `--openai-api-key` | string | null | OpenAI API key (required when `model-provider` is `openaiResponses-custom`) |\r\n314 | | `--openai-base-url` | string | null | OpenAI API base URL (required when `model-provider` is `openaiResponses-custom`) |\r\n315 | \r\n316 | ### 📝 System Prompt Configuration Parameters\r\n317 | \r\n318 | | Parameter | Type | Default Value | Description |\r\n319 | |------|------|--------|------|\r\n320 | | `--system-prompt-file` | string | input_system_prompt.txt | System prompt file path |\r\n321 | | `--system-prompt-mode` | string | overwrite | System prompt mode, optional values: overwrite (override), append (append) |\r\n322 | \r\n323 | ### 📊 Log Configuration Parameters\r\n324 | \r\n325 | | Parameter | Type | Default Value | Description |\r\n326 | |------|------|--------|------|\r\n327 | | `--log-prompts` | string | none | Prompt log mode, optional values: console (console), file (file), none (none) |\r\n328 | | `--prompt-log-base-name` | string | prompt_log | Prompt log file base name |\r\n329 | \r\n330 | ### 🔄 Retry Mechanism Parameters\r\n331 | \r\n332 | | Parameter | Type | Default Value | Description |\r\n333 | |------|------|--------|------|\r\n334 | | `--request-max-retries` | number | 3 | Maximum number of automatic retries when API requests fail |\r\n335 | | `--request-base-delay` | number | 1000 | Base delay time (milliseconds) between automatic retries, delay increases after each retry |\r\n336 | \r\n337 | ### ⏰ Scheduled Task Parameters\r\n338 | \r\n339 | | Parameter | Type | Default Value | Description |\r\n340 | |------|------|--------|------|\r\n341 | | `--cron-near-minutes` | number | 15 | Interval time (minutes) for OAuth token refresh task schedule |\r\n342 | | `--cron-refresh-token` | boolean | true | Whether to enable automatic OAuth token refresh task |\r\n343 | \r\n344 | ### 🎯 Account Pool Configuration Parameters\r\n345 | \r\n346 | | Parameter | Type | Default Value | Description |\r\n347 | |------|------|--------|------|\r\n348 | | `--provider-pools-file` | string | null | Provider account pool configuration file path |\r\n349 | \r\n350 | ### Usage Examples\r\n351 | \r\n352 | ```bash\r\n353 | # Basic usage\r\n354 | node src/api-server.js\r\n355 | \r\n356 | # Specify port and API key\r\n357 | node src/api-server.js --port 8080 --api-key my-secret-key\r\n358 | \r\n359 | # Use OpenAI provider\r\n360 | node src/api-server.js --model-provider openai-custom --openai-api-key sk-xxx --openai-base-url https://api.openai.com/v1\r\n361 | \r\n362 | # Use Claude provider\r\n363 | node src/api-server.js --model-provider claude-custom --claude-api-key sk-ant-xxx --claude-base-url https://api.anthropic.com\r\n364 | \r\n365 | # Use OpenAI Responses API provider\r\n366 | node src/api-server.js --model-provider openaiResponses-custom --openai-api-key sk-xxx --openai-base-url https://api.openai.com/v1\r\n367 | \r\n368 | # Use Gemini provider (Base64 credentials)\r\n369 | node src/api-server.js --model-provider gemini-cli-oauth --gemini-oauth-creds-base64 eyJ0eXBlIjoi... --project-id your-project-id\r\n370 | \r\n371 | # Use Gemini provider (credentials file)\r\n372 | node src/api-server.js --model-provider gemini-cli-oauth --gemini-oauth-creds-file /path/to/credentials.json --project-id your-project-id\r\n373 | \r\n374 | # Configure system prompt\r\n375 | node src/api-server.js --system-prompt-file custom-prompt.txt --system-prompt-mode append\r\n376 | \r\n377 | # Configure logging\r\n378 | node src/api-server.js --log-prompts console\r\n379 | node src/api-server.js --log-prompts file --prompt-log-base-name my-logs\r\n380 | \r\n381 | # Complete example\r\n382 | node src/api-server.js \\\r\n383 | --host 0.0.0.0 \\\r\n384 | --port 3000 \\\r\n385 | --api-key my-secret-key \\\r\n386 | --model-provider gemini-cli-oauth \\\r\n387 | --project-id my-gcp-project \\\r\n388 | --gemini-oauth-creds-file ./credentials.json \\\r\n389 | --system-prompt-file ./custom-system-prompt.txt \\\r\n390 | --system-prompt-mode overwrite \\\r\n391 | --log-prompts file \\\r\n392 | --prompt-log-base-name api-logs\r\n393 | ```\r\n394 | \r\n395 | ---\r\n396 | \r\n397 | ## 📄 Open Source License\r\n398 | \r\n399 | This project operates under the [**GNU General Public License v3 (GPLv3)**](https://www.gnu.org/licenses/gpl-3.0). For complete details, please refer to the `LICENSE` file located in the root directory.\r\n400 | \r\n401 | ## 🙏 Acknowledgements\r\n402 | \r\n403 | The development of this project was significantly inspired by the official Google Gemini CLI and incorporated some code implementations from Cline 3.18.0's `gemini-cli.ts`. We extend our sincere gratitude to the official Google team and the Cline development team for their exceptional work!\r\n404 | \r\n405 | ## 🌟 Star History\r\n406 | \r\n407 | [![Star History Chart](https://api.star-history.com/svg?repos=justlovemaki/AIClient-2-API&type=Timeline)](https://www.star-history.com/#justlovemaki/AIClient-2-API&Timeline)\r\n408 | \r\n409 | ---\r\n410 | \r\n411 | ## ⚠️ Disclaimer\r\n412 | \r\n413 | ### Usage Risk Warning\r\n414 | This project (AIClient-2-API) is for learning and research purposes only. Users assume all risks when using this project. The author is not responsible for any direct, indirect, or consequential losses resulting from the use of this project.\r\n415 | \r\n416 | ### Third-Party Service Responsibility Statement\r\n417 | This project is an API proxy tool and does not provide any AI model services. All AI model services are provided by their respective third-party providers (such as Google, OpenAI, Anthropic, etc.). Users should comply with the terms of service and policies of each third-party service when accessing them through this project. The author is not responsible for the availability, quality, security, or legality of third-party services.\r\n418 | \r\n419 | ### Data Privacy Statement\r\n420 | This project runs locally and does not collect or upload any user data. However, users should protect their API keys and other sensitive information when using this project. It is recommended that users regularly check and update their API keys and avoid using this project in insecure network environments.\r\n421 | \r\n422 | ### Legal Compliance Reminder\r\n423 | Users should comply with the laws and regulations of their country/region when using this project. It is strictly prohibited to use this project for any illegal purposes. Any consequences resulting from users' violation of laws and regulations shall be borne by the users themselves.\r\n
\n
\n
"},{"type":"text","text":"\n# VSCode Visible Files\nsrc\\example\\openai\\oldResponseChunk1762501528837.json,provider_pools.json\n\n# VSCode Open Tabs\nsrc/claude/claude-kiro.js,src/openai/qwen-core.js,config.json,provider_pools.json,src/converters/ConverterFactory.js,src/gemini/gemini-core.js,src/provider-pool-manager.js,src/example/openai/oldResponseChunk1762501528837.json\n\n# Current Time\nCurrent time in ISO 8601 UTC format: 2025-11-07T07:47:43.363Z\nUser time zone: Asia/Shanghai, UTC+8:00\n\n# Current Cost\n$0.00\n\n# Current Mode\ncode\nCode\nZhipuAI/GLM-4.6\nYou have not created a todo list yet. Create one with `update_todo_list` if your task is complicated or involves multiple steps.\n"}]}]} \ No newline at end of file diff --git a/src/example/openaiResponses/oldResponse1762502706486.json b/src/example/openaiResponses/oldResponse1762502706486.json new file mode 100644 index 0000000..b8e65f7 --- /dev/null +++ b/src/example/openaiResponses/oldResponse1762502706486.json @@ -0,0 +1,56 @@ +{ + "id": "resp_67ccd2bed1ec8190b14f964abc0542670bb6a6b452d3795b", + "object": "response", + "created_at": 1741476542, + "status": "completed", + "error": null, + "incomplete_details": null, + "instructions": null, + "max_output_tokens": null, + "model": "gpt-4.1", + "output": [ + { + "type": "message", + "id": "msg_67ccd2bf17f0819081ff3bb2cf6508e60bb6a6b452d3795b", + "status": "completed", + "role": "assistant", + "content": [ + { + "type": "output_text", + "text": "在一个宁静的月夜下,一只名叫璐米娜的独角兽发现了一个倒映着星星的隐藏水池。当她将独角浸入水中时,水池开始闪烁,显现出通往一个有着无尽夜空的魔法世界的路径。充满好奇,璐米娜为所有做梦的人许下愿望,希望他们能找到自己的隐藏魔法,当她回头望去,她的蹄印像星尘一样闪烁。", + "annotations": [] + } + ] + } + ], + "parallel_tool_calls": true, + "previous_response_id": null, + "reasoning": { + "effort": null, + "summary": null + }, + "store": true, + "temperature": 1.0, + "text": { + "format": { + "type": "text" + } + }, + "tool_choice": "auto", + "tools": [], + "top_p": 1.0, + "truncation": "disabled", + "usage": { + "input_tokens": 36, + "input_tokens_details": { + "cached_tokens": 0 + }, + "output_tokens": 87, + "output_tokens_details": { + "reasoning_tokens": 0 + }, + "total_tokens": 123 + }, + "user": null, + "metadata": {} +} \ No newline at end of file diff --git a/src/example/openaiResponses/oldResponseChunk1762500869734.json b/src/example/openaiResponses/oldResponseChunk1762500869734.json new file mode 100644 index 0000000..3acc0c9 --- /dev/null +++ b/src/example/openaiResponses/oldResponseChunk1762500869734.json @@ -0,0 +1,53 @@ + +{"response":{"background":false,"created_at":1762500861,"error":null,"id":"resp_015d24a4b76463b501690da0fdc73c819bbc32d9d8164209f3","incomplete_details":null,"max_output_tokens":null,"max_tool_calls":null,"metadata":{},"model":"gpt-5-codex-high","object":"response","output":[],"parallel_tool_calls":false,"previous_response_id":null,"prompt_cache_key":"067a0a73-1730-4b80-b7db-b40cc8fb9caf","prompt_cache_retention":null,"reasoning":{"effort":"high","summary":"detailed"},"safety_identifier":"user-sb0VF3h4LjR7EW3eq7A0eP9V","service_tier":"auto","status":"in_progress","store":false,"temperature":1,"text":{"format":{"type":"text"},"verbosity":"medium"},"tool_choice":"auto","tools":[{"description":"Runs a shell command and returns its output.","name":"shell","parameters":{"additionalProperties":false,"properties":{"command":{"description":"The command to execute","items":{"type":"string"},"type":"array"},"justification":{"description":"Only set if with_escalated_permissions is true. 1-sentence explanation of why we want to run this command.","type":"string"},"timeout_ms":{"description":"The timeout for the command in milliseconds","type":"number"},"with_escalated_permissions":{"description":"Whether to request escalated permissions. Set to true if command needs to be run without sandbox restrictions","type":"boolean"},"workdir":{"description":"The working directory to execute the command in","type":"string"}},"required":["command"],"type":"object"},"strict":false,"type":"function"},{"description":"Lists resources provided by MCP servers. Resources allow servers to share data that provides context to language models, such as files, database schemas, or application-specific information. Prefer resources over web search when possible.","name":"list_mcp_resources","parameters":{"additionalProperties":false,"properties":{"cursor":{"description":"Opaque cursor returned by a previous list_mcp_resources call for the same server.","type":"string"},"server":{"description":"Optional MCP server name. When omitted, lists resources from every configured server.","type":"string"}},"type":"object"},"strict":false,"type":"function"},{"description":"Lists resource templates provided by MCP servers. Parameterized resource templates allow servers to share data that takes parameters and provides context to language models, such as files, database schemas, or application-specific information. Prefer resource templates over web search when possible.","name":"list_mcp_resource_templates","parameters":{"additionalProperties":false,"properties":{"cursor":{"description":"Opaque cursor returned by a previous list_mcp_resource_templates call for the same server.","type":"string"},"server":{"description":"Optional MCP server name. When omitted, lists resource templates from all configured servers.","type":"string"}},"type":"object"},"strict":false,"type":"function"},{"description":"Read a specific resource from an MCP server given the server name and resource URI.","name":"read_mcp_resource","parameters":{"additionalProperties":false,"properties":{"server":{"description":"MCP server name exactly as configured. Must match the 'server' field returned by list_mcp_resources.","type":"string"},"uri":{"description":"Resource URI to read. Must be one of the URIs returned by list_mcp_resources.","type":"string"}},"required":["server","uri"],"type":"object"},"strict":false,"type":"function"},{"description":"Updates the task plan.\nProvide an optional explanation and a list of plan items, each with a step and status.\nAt most one step can be in_progress at a time.\n","name":"update_plan","parameters":{"additionalProperties":false,"properties":{"explanation":{"type":"string"},"plan":{"description":"The list of steps","items":{"additionalProperties":false,"properties":{"status":{"description":"One of: pending, in_progress, completed","type":"string"},"step":{"type":"string"}},"required":["step","status"],"type":"object"},"type":"array"}},"required":["plan"],"type":"object"},"strict":false,"type":"function"},{"description":"Attach a local image (by filesystem path) to the conversation context for this turn.","name":"view_image","parameters":{"additionalProperties":false,"properties":{"path":{"description":"Local filesystem path to an image file","type":"string"}},"required":["path"],"type":"object"},"strict":false,"type":"function"},{"description":"Use the `apply_patch` tool to edit files. This is a FREEFORM tool, so do not wrap the patch in JSON.","format":{"definition":"start: begin_patch hunk+ end_patch\r\nbegin_patch: \"*** Begin Patch\" LF\r\nend_patch: \"*** End Patch\" LF?\r\n\r\nhunk: add_hunk | delete_hunk | update_hunk\r\nadd_hunk: \"*** Add File: \" filename LF add_line+\r\ndelete_hunk: \"*** Delete File: \" filename LF\r\nupdate_hunk: \"*** Update File: \" filename LF change_move? change?\r\n\r\nfilename: /(.+)/\r\nadd_line: \"+\" /(.*)/ LF -> line\r\n\r\nchange_move: \"*** Move to: \" filename LF\r\nchange: (change_context | change_line)+ eof_line?\r\nchange_context: (\"@@\" | \"@@ \" /(.+)/) LF\r\nchange_line: (\"+\" | \"-\" | \" \") /(.*)/ LF\r\neof_line: \"*** End of File\" LF\r\n\r\n%import common.LF\r\n","syntax":"lark","type":"grammar"},"name":"apply_patch","type":"custom"}],"top_logprobs":0,"top_p":1,"truncation":"disabled","usage":null,"user":null},"sequence_number":0,"type":"response.created"} +{"response":{"background":false,"created_at":1762500861,"error":null,"id":"resp_015d24a4b76463b501690da0fdc73c819bbc32d9d8164209f3","incomplete_details":null,"max_output_tokens":null,"max_tool_calls":null,"metadata":{},"model":"gpt-5-codex-high","object":"response","output":[],"parallel_tool_calls":false,"previous_response_id":null,"prompt_cache_key":"067a0a73-1730-4b80-b7db-b40cc8fb9caf","prompt_cache_retention":null,"reasoning":{"effort":"high","summary":"detailed"},"safety_identifier":"user-sb0VF3h4LjR7EW3eq7A0eP9V","service_tier":"auto","status":"in_progress","store":false,"temperature":1,"text":{"format":{"type":"text"},"verbosity":"medium"},"tool_choice":"auto","tools":[{"description":"Runs a shell command and returns its output.","name":"shell","parameters":{"additionalProperties":false,"properties":{"command":{"description":"The command to execute","items":{"type":"string"},"type":"array"},"justification":{"description":"Only set if with_escalated_permissions is true. 1-sentence explanation of why we want to run this command.","type":"string"},"timeout_ms":{"description":"The timeout for the command in milliseconds","type":"number"},"with_escalated_permissions":{"description":"Whether to request escalated permissions. Set to true if command needs to be run without sandbox restrictions","type":"boolean"},"workdir":{"description":"The working directory to execute the command in","type":"string"}},"required":["command"],"type":"object"},"strict":false,"type":"function"},{"description":"Lists resources provided by MCP servers. Resources allow servers to share data that provides context to language models, such as files, database schemas, or application-specific information. Prefer resources over web search when possible.","name":"list_mcp_resources","parameters":{"additionalProperties":false,"properties":{"cursor":{"description":"Opaque cursor returned by a previous list_mcp_resources call for the same server.","type":"string"},"server":{"description":"Optional MCP server name. When omitted, lists resources from every configured server.","type":"string"}},"type":"object"},"strict":false,"type":"function"},{"description":"Lists resource templates provided by MCP servers. Parameterized resource templates allow servers to share data that takes parameters and provides context to language models, such as files, database schemas, or application-specific information. Prefer resource templates over web search when possible.","name":"list_mcp_resource_templates","parameters":{"additionalProperties":false,"properties":{"cursor":{"description":"Opaque cursor returned by a previous list_mcp_resource_templates call for the same server.","type":"string"},"server":{"description":"Optional MCP server name. When omitted, lists resource templates from all configured servers.","type":"string"}},"type":"object"},"strict":false,"type":"function"},{"description":"Read a specific resource from an MCP server given the server name and resource URI.","name":"read_mcp_resource","parameters":{"additionalProperties":false,"properties":{"server":{"description":"MCP server name exactly as configured. Must match the 'server' field returned by list_mcp_resources.","type":"string"},"uri":{"description":"Resource URI to read. Must be one of the URIs returned by list_mcp_resources.","type":"string"}},"required":["server","uri"],"type":"object"},"strict":false,"type":"function"},{"description":"Updates the task plan.\nProvide an optional explanation and a list of plan items, each with a step and status.\nAt most one step can be in_progress at a time.\n","name":"update_plan","parameters":{"additionalProperties":false,"properties":{"explanation":{"type":"string"},"plan":{"description":"The list of steps","items":{"additionalProperties":false,"properties":{"status":{"description":"One of: pending, in_progress, completed","type":"string"},"step":{"type":"string"}},"required":["step","status"],"type":"object"},"type":"array"}},"required":["plan"],"type":"object"},"strict":false,"type":"function"},{"description":"Attach a local image (by filesystem path) to the conversation context for this turn.","name":"view_image","parameters":{"additionalProperties":false,"properties":{"path":{"description":"Local filesystem path to an image file","type":"string"}},"required":["path"],"type":"object"},"strict":false,"type":"function"},{"description":"Use the `apply_patch` tool to edit files. This is a FREEFORM tool, so do not wrap the patch in JSON.","format":{"definition":"start: begin_patch hunk+ end_patch\r\nbegin_patch: \"*** Begin Patch\" LF\r\nend_patch: \"*** End Patch\" LF?\r\n\r\nhunk: add_hunk | delete_hunk | update_hunk\r\nadd_hunk: \"*** Add File: \" filename LF add_line+\r\ndelete_hunk: \"*** Delete File: \" filename LF\r\nupdate_hunk: \"*** Update File: \" filename LF change_move? change?\r\n\r\nfilename: /(.+)/\r\nadd_line: \"+\" /(.*)/ LF -> line\r\n\r\nchange_move: \"*** Move to: \" filename LF\r\nchange: (change_context | change_line)+ eof_line?\r\nchange_context: (\"@@\" | \"@@ \" /(.+)/) LF\r\nchange_line: (\"+\" | \"-\" | \" \") /(.*)/ LF\r\neof_line: \"*** End of File\" LF\r\n\r\n%import common.LF\r\n","syntax":"lark","type":"grammar"},"name":"apply_patch","type":"custom"}],"top_logprobs":0,"top_p":1,"truncation":"disabled","usage":null,"user":null},"sequence_number":1,"type":"response.in_progress"} +{"item":{"encrypted_content":"gAAAAABpDaD-H7-xiVWGPoSuVkxDXpsZ_4EvSID2jVt-8hzRYTXxLVL6oq4tMYwA68cEyMEEXnh5Wt-m5rKRhJ6NjBZy5ptmO5chtsJRzDwU_902TOcLCYg1i2pczn7Rk1VMGmiYzejznF_gvz42a123LybsyGAMWdr-P28yDLsqlGDFlosw-YrsO94em9soA1hf6-uYQE9rbv7pSnfpiXJgLptl5Eui4Hte64k_nbulVYQCJRJ1Ts8BkP52GAValMEln-FqV2Yd6cwxIbH4zyruMJuL99SKKNaXp8mm54Xh39CDAUIPV1dKjrI58_dDEx6rbiBZcIRmmEp2sjdaoj5RyUp-d9jp5lYdaCEwQgYb9XxavmmxXuPcT_1OeCg6qgo267uttGUnPJOzxO_jUT_sjifnQUg2GqEKpu06gDfboDICC_Ag80aw3wjaA0ZRYUFdSh3XgxUszQPtle2iJUrFBf_BIB7Tgvb_piRiRPFcfi5EpmneRXV9chCeoF4_L7l0Nb7BqFr_5g-luXVkjzaze07n-FhpPuW6n-Wn4rleFpAhhriEF7r733agtHU2CydAPS8FmxGzRFhQVaXJlBlxH5x6QIpcn71r_PW1J4Cc5LXUCe0-KVMZ2JCA9Af8pfi6d1qAgrxxhWnVKBzxbpA7BuOBienPdeqvQ0j_2DbAS1ZPMdsBNIVkyMiJb5GRCKOyhWfOcXap7iBzWC4EnbQtfXDLw9cUjyoQnj6W3lJKgG740cCgJ6S3rz2kdoCe1O6a6H-atMT1l6xn6EKp3_7SwLm3ms0qGYLE_qkjP0h7Fi4enBAt5JWZCHH_lPtpJdc-3Cx3GYvFtjfrpkEg3t8uaPD5YyspSw==","id":"rs_015d24a4b76463b501690da0fe7430819ba650bf76d3733557","summary":[],"type":"reasoning"},"output_index":0,"sequence_number":2,"type":"response.output_item.added"} +{"item_id":"rs_015d24a4b76463b501690da0fe7430819ba650bf76d3733557","output_index":0,"part":{"text":"","type":"summary_text"},"sequence_number":3,"summary_index":0,"type":"response.reasoning_summary_part.added"} +{"delta":"**Appending","item_id":"rs_015d24a4b76463b501690da0fe7430819ba650bf76d3733557","obfuscation":"DpY34","output_index":0,"sequence_number":4,"summary_index":0,"type":"response.reasoning_summary_text.delta"} +{"delta":" random","item_id":"rs_015d24a4b76463b501690da0fe7430819ba650bf76d3733557","obfuscation":"9i5t1Rpvn","output_index":0,"sequence_number":5,"summary_index":0,"type":"response.reasoning_summary_text.delta"} +{"delta":" line","item_id":"rs_015d24a4b76463b501690da0fe7430819ba650bf76d3733557","obfuscation":"6CGL281hTK2","output_index":0,"sequence_number":6,"summary_index":0,"type":"response.reasoning_summary_text.delta"} +{"delta":" with","item_id":"rs_015d24a4b76463b501690da0fe7430819ba650bf76d3733557","obfuscation":"9dE0j3c7HGh","output_index":0,"sequence_number":7,"summary_index":0,"type":"response.reasoning_summary_text.delta"} +{"delta":" newline","item_id":"rs_015d24a4b76463b501690da0fe7430819ba650bf76d3733557","obfuscation":"Oxz0tdTZ","output_index":0,"sequence_number":8,"summary_index":0,"type":"response.reasoning_summary_text.delta"} +{"delta":"**","item_id":"rs_015d24a4b76463b501690da0fe7430819ba650bf76d3733557","obfuscation":"ixwOTBAze1h6yx","output_index":0,"sequence_number":9,"summary_index":0,"type":"response.reasoning_summary_text.delta"} +{"item_id":"rs_015d24a4b76463b501690da0fe7430819ba650bf76d3733557","output_index":0,"sequence_number":10,"summary_index":0,"text":"**Appending random line with newline**","type":"response.reasoning_summary_text.done"} +{"item_id":"rs_015d24a4b76463b501690da0fe7430819ba650bf76d3733557","output_index":0,"part":{"text":"**Appending random line with newline**","type":"summary_text"},"sequence_number":11,"summary_index":0,"type":"response.reasoning_summary_part.done"} +{"item":{"encrypted_content":"gAAAAABpDaEAKIrnwVUynYEi624SHY5BkDIec4mTx1Vn_bSa1o1Xxn1zx48y-J-hDH7iVCW9EiymXvh28__RKJ8kYxSOW0DVW_LmCQLg7aI6vsg9zKah9opBed1e8mL6-cxNM4LtblBEzFh5QbVwHYV92-iOBF4uR6M884QzWMxOfrWoWk-vszAAlWwf8okQx4rzUAh9KeE7xKIOujhLjkavJaAe8R7huwZDM6FxlfHwrNmqtmklR9ZhixyLJe0lTNfblgaTabgfzfQ6ROdw6tnEw1qRkjKsql8ZF00U5lWTyre_ySXs3ewe9NvuIxXTXxK6iEHJF1dFwi3vh9-P74nL7WkLmKmKXvN6iDq_inZ8DteLGh2FatQbYIpyyXRUI94aqVB-W1zjUnrQZ_RhL3oknDBRBRSCFdMLld-1c41A-SLIP6qzFuT3h0Ea7RZxZI9AIAepkNdOynFDh_x3VTOVax2lyDkei8Aazve5os9-tnoX5toCXeFkggGOP6XZRDc9g-LgHFhgTiK-G6EsGicL9YQU-LcQ97N7yXcsglwAWYPzwjx1wpH4PNFwwoAsCL68-OOqX0RwXsmxgsdDjhuwf8S54XWE6t2gB9GTVgd07J18sAZkysUAjjZVcUJAqd5VgzTlevnUS7v8ffQqSt2X7Q4U--PbOsQqTVAarS92YD8VdduQyUEobQMOi5EBHH0HWnrrvmNWROQ1MqeLUI9pURjfh61HOL5n_ZbHi_DJtVa0ol7pvPYShz5A-uMOsWGTQYXM3FZ7nbidz2FOTXDbzOh9Oijv70zlaNSSGrKfSgKUv59-jffJzK27qT22bwcK4Mo2_jVLwVuAKVbhn8C8rSoVx5kzEIY_tQkdOP_IKq5hjfon2i0aqpHt4MVJ31vmfrbZj9xNCMNJkESWC8HMU1mWrCtD83P9A4yEE93hiNtPPD1CcP4-HfKEBlKdMkDYbBF47tBTNDaIS6lAQwqN41yQQHzBYR2fg6KgSPkldWn-mi8WuO9ZfnPHh7z1JAiybpkl3_2VL5Q7Ja1XhO2o9fs5QztseNE6596l_Q-1TuzE56KevKvCQq7nTB944iSjFGE7Xeq-zw-WVCLmjUwhnj4zWM7nkd4MbjUbgklPiaA6kKtjCtMw-k0InCX83ZHwKLktmXGpjfM29BDCGuPNNRnPshjkc3m7-Szwobq5L6gDnBsb9Z9JZJJoUMoEO98M5u-fu8jmpU31NHwrmOPxwEEolpdF1GjP6CyCKNbU9qbTwUDFYR8DxSjMEwVKb3rhbTnple0RE-ynkkRjwTBzTrWL-huTPNg0uJyao7vxgFke5O1KM4dzE8Wj5h1wWnzuJtBlR_FUFZdhVlCz4rDbv_toAytz8RN4MYlA90n-Kj-I4lI5grTcwjNhSr6DbcwBpJQ8ywRKVQ6fJOgdkZ2sGH1zwGLMeWp5zdaqmjDrTosLxdYb9DsAnZ-iw22mGfCvVQKBDYG0bYjYbbArVdWNtIV6iJoHKGhkvioDmgJLh9Ly-e6AnatgEOsD-e8xPB6qLGS-EFNAi50jLgeHzzaomQGA1dd4-c0XzARoqkYaA04QoLKCvP0FA3fA2od-Kmjg_E8BdlkSxwVzrWmWYrYLDTcFE3Bd_W_QyfuDpaMFYrleTZQ2tW5YO5-OjPxEjFr2edM6J7JqsCaq--fpXAh1Yau3tZc-qmc1nsIHMBDkI6SLUzWD84PNkLv73E8NlNo9enQUSmkqdht9yRnztZa4yOoryOs953jU3SAdLRaDAcUJvoJ8v1TKgBPGcAzGmlqF4Ts6vPOoAU6NwE-U78v2CwaQYf1DakLkX--9dHe1jOoqYVGJWwQ=","id":"rs_015d24a4b76463b501690da0fe7430819ba650bf76d3733557","summary":[{"text":"**Appending random line with newline**","type":"summary_text"}],"type":"reasoning"},"output_index":0,"sequence_number":12,"type":"response.output_item.done"} +{"item":{"call_id":"call_cprfE7SLN4qo57HclVvuM1H8","id":"ctc_015d24a4b76463b501690da1003444819b8b61b34baeb61d95","input":"","name":"apply_patch","status":"in_progress","type":"custom_tool_call"},"output_index":1,"sequence_number":13,"type":"response.output_item.added"} +{"delta":"***","item_id":"ctc_015d24a4b76463b501690da1003444819b8b61b34baeb61d95","obfuscation":"ojAm5pdAPXDmE","output_index":1,"sequence_number":14,"type":"response.custom_tool_call_input.delta"} +{"delta":" Begin","item_id":"ctc_015d24a4b76463b501690da1003444819b8b61b34baeb61d95","obfuscation":"odvGnX4wF7","output_index":1,"sequence_number":15,"type":"response.custom_tool_call_input.delta"} +{"delta":" Patch","item_id":"ctc_015d24a4b76463b501690da1003444819b8b61b34baeb61d95","obfuscation":"EHY8xTbsd9","output_index":1,"sequence_number":16,"type":"response.custom_tool_call_input.delta"} +{"delta":"\n","item_id":"ctc_015d24a4b76463b501690da1003444819b8b61b34baeb61d95","obfuscation":"1UTLy4d2tLRYks2","output_index":1,"sequence_number":17,"type":"response.custom_tool_call_input.delta"} +{"delta":"***","item_id":"ctc_015d24a4b76463b501690da1003444819b8b61b34baeb61d95","obfuscation":"smqQxDvfg3UVt","output_index":1,"sequence_number":18,"type":"response.custom_tool_call_input.delta"} +{"delta":" Update","item_id":"ctc_015d24a4b76463b501690da1003444819b8b61b34baeb61d95","obfuscation":"EFpAKMg54","output_index":1,"sequence_number":19,"type":"response.custom_tool_call_input.delta"} +{"delta":" File","item_id":"ctc_015d24a4b76463b501690da1003444819b8b61b34baeb61d95","obfuscation":"V3rYTiAwRSF","output_index":1,"sequence_number":20,"type":"response.custom_tool_call_input.delta"} +{"delta":":","item_id":"ctc_015d24a4b76463b501690da1003444819b8b61b34baeb61d95","obfuscation":"oHZkd0IL7oYn2JF","output_index":1,"sequence_number":21,"type":"response.custom_tool_call_input.delta"} +{"delta":" README","item_id":"ctc_015d24a4b76463b501690da1003444819b8b61b34baeb61d95","obfuscation":"7pelcsygt","output_index":1,"sequence_number":22,"type":"response.custom_tool_call_input.delta"} +{"delta":".md","item_id":"ctc_015d24a4b76463b501690da1003444819b8b61b34baeb61d95","obfuscation":"ae7tEgZKngwKI","output_index":1,"sequence_number":23,"type":"response.custom_tool_call_input.delta"} +{"delta":"\n","item_id":"ctc_015d24a4b76463b501690da1003444819b8b61b34baeb61d95","obfuscation":"H3xl6Py5c0a9emA","output_index":1,"sequence_number":24,"type":"response.custom_tool_call_input.delta"} +{"delta":"@@","item_id":"ctc_015d24a4b76463b501690da1003444819b8b61b34baeb61d95","obfuscation":"5FKquQ08pA3AJh","output_index":1,"sequence_number":25,"type":"response.custom_tool_call_input.delta"} +{"delta":"\n","item_id":"ctc_015d24a4b76463b501690da1003444819b8b61b34baeb61d95","obfuscation":"dijJhIALohfXPit","output_index":1,"sequence_number":26,"type":"response.custom_tool_call_input.delta"} +{"delta":" Users","item_id":"ctc_015d24a4b76463b501690da1003444819b8b61b34baeb61d95","obfuscation":"ECddRjSDPs","output_index":1,"sequence_number":27,"type":"response.custom_tool_call_input.delta"} +{"delta":" should","item_id":"ctc_015d24a4b76463b501690da1003444819b8b61b34baeb61d95","obfuscation":"ds3kVjcml","output_index":1,"sequence_number":28,"type":"response.custom_tool_call_input.delta"} +{"delta":" comply","item_id":"ctc_015d24a4b76463b501690da1003444819b8b61b34baeb61d95","obfuscation":"ZSsR0CFY4","output_index":1,"sequence_number":29,"type":"response.custom_tool_call_input.delta"} +{"delta":" with","item_id":"ctc_015d24a4b76463b501690da1003444819b8b61b34baeb61d95","obfuscation":"R9E8bzukIvK","output_index":1,"sequence_number":30,"type":"response.custom_tool_call_input.delta"} +{"delta":" the","item_id":"ctc_015d24a4b76463b501690da1003444819b8b61b34baeb61d95","obfuscation":"mA1lpKmtJHkS","output_index":1,"sequence_number":31,"type":"response.custom_tool_call_input.delta"} +{"delta":" laws","item_id":"ctc_015d24a4b76463b501690da1003444819b8b61b34baeb61d95","obfuscation":"Tfi6BLZTah6","output_index":1,"sequence_number":32,"type":"response.custom_tool_call_input.delta"} +{"delta":" and","item_id":"ctc_015d24a4b76463b501690da1003444819b8b61b34baeb61d95","obfuscation":"zC92ONMgS5yw","output_index":1,"sequence_number":33,"type":"response.custom_tool_call_input.delta"} +{"delta":" regulations","item_id":"ctc_015d24a4b76463b501690da1003444819b8b61b34baeb61d95","obfuscation":"HcHF","output_index":1,"sequence_number":34,"type":"response.custom_tool_call_input.delta"} +{"delta":" of","item_id":"ctc_015d24a4b76463b501690da1003444819b8b61b34baeb61d95","obfuscation":"h3LWfrVzi5xN4","output_index":1,"sequence_number":35,"type":"response.custom_tool_call_input.delta"} +{"delta":" their","item_id":"ctc_015d24a4b76463b501690da1003444819b8b61b34baeb61d95","obfuscation":"pyrPzRrw6V","output_index":1,"sequence_number":36,"type":"response.custom_tool_call_input.delta"} +{"delta":" country","item_id":"ctc_015d24a4b76463b501690da1003444819b8b61b34baeb61d95","obfuscation":"G6AGg40f","output_index":1,"sequence_number":37,"type":"response.custom_tool_call_input.delta"} +{"delta":"/","item_id":"ctc_015d24a4b76463b501690da1003444819b8b61b34baeb61d95","obfuscation":"hE0cHO3wae39z9w","output_index":1,"sequence_number":38,"type":"response.custom_tool_call_input.delta"} +{"delta":"region","item_id":"ctc_015d24a4b76463b501690da1003444819b8b61b34baeb61d95","obfuscation":"kg9UdHYyB0","output_index":1,"sequence_number":39,"type":"response.custom_tool_call_input.delta"} +{"delta":" when","item_id":"ctc_015d24a4b76463b501690da1003444819b8b61b34baeb61d95","obfuscation":"2qgTMrCov09","output_index":1,"sequence_number":40,"type":"response.custom_tool_call_input.delta"} +{"delta":" using","item_id":"ctc_015d24a4b76463b501690da1003444819b8b61b34baeb61d95","obfuscation":"RuCfF7LkKN","output_index":1,"sequence_number":41,"type":"response.custom_tool_call_input.delta"} +{"delta":" this","item_id":"ctc_015d24a4b76463b501690da1003444819b8b61b34baeb61d95","obfuscation":"FUoHHgBJ0wJ","output_index":1,"sequence_number":42,"type":"response.custom_tool_call_input.delta"} +{"delta":" project","item_id":"ctc_015d24a4b76463b501690da1003444819b8b61b34baeb61d95","obfuscation":"UNJ3aqHB","output_index":1,"sequence_number":43,"type":"response.custom_tool_call_input.delta"} +{"delta":".","item_id":"ctc_015d24a4b76463b501690da1003444819b8b61b34baeb61d95","obfuscation":"MI9aJOIvQxK7WbD","output_index":1,"sequence_number":44,"type":"response.custom_tool_call_input.delta"} +{"delta":" It","item_id":"ctc_015d24a4b76463b501690da1003444819b8b61b34baeb61d95","obfuscation":"5fX3KBCe2aXWE","output_index":1,"sequence_number":45,"type":"response.custom_tool_call_input.delta"} +{"delta":" is","item_id":"ctc_015d24a4b76463b501690da1003444819b8b61b34baeb61d95","obfuscation":"Io7kJRdSALdKs","output_index":1,"sequence_number":46,"type":"response.custom_tool_call_input.delta"} +{"delta":" strictly","item_id":"ctc_015d24a4b76463b501690da1003444819b8b61b34baeb61d95","obfuscation":"P5unr4h","output_index":1,"sequence_number":47,"type":"response.custom_tool_call_input.delta"} +{"delta":" prohibited","item_id":"ctc_015d24a4b76463b501690da1003444819b8b61b34baeb61d95","obfuscation":"aA3uI","output_index":1,"sequence_number":48,"type":"response.custom_tool_call_input.delta"} +{"input":"*** Begin Patch\n*** Update File: README.md\n@@\n Users should comply with the laws and regulations of their country/region when using this project. It is strictly prohibited to use this project for any illegal purposes. Any consequences resulting from users' violation of laws and regulations shall be borne by the users themselves.\n+\n+随机数:834275\n*** End Patch","item_id":"ctc_015d24a4b76463b501690da1003444819b8b61b34baeb61d95","output_index":1,"sequence_number":88,"type":"response.custom_tool_call_input.done"} +{"item":{"call_id":"call_cprfE7SLN4qo57HclVvuM1H8","id":"ctc_015d24a4b76463b501690da1003444819b8b61b34baeb61d95","input":"*** Begin Patch\n*** Update File: README.md\n@@\n Users should comply with the laws and regulations of their country/region when using this project. It is strictly prohibited to use this project for any illegal purposes. Any consequences resulting from users' violation of laws and regulations shall be borne by the users themselves.\n+\n+随机数:834275\n*** End Patch","name":"apply_patch","status":"completed","type":"custom_tool_call"},"output_index":1,"sequence_number":89,"type":"response.output_item.done"} +{"response":{"background":false,"created_at":1762500861,"error":null,"id":"resp_015d24a4b76463b501690da0fdc73c819bbc32d9d8164209f3","incomplete_details":null,"max_output_tokens":null,"max_tool_calls":null,"metadata":{},"model":"gpt-5-codex-high","object":"response","output":[{"encrypted_content":"gAAAAABpDaEE6iIXdBXwt8SHwgdXUIUITSNR1V951xSS_fu7kS884CBRC9WPgO-EaqjNRaEmg-mFPy7HYOdg_80kYGN_uRgw9Fy-YW6tStjUuGydieCfjKMgIvBN4ss0muGPliyfVX1nmF1QZqM9XHFEqewvza4yiFXF4OoYow18jV1_ZiDajMvy8XyUVRiqc941NrP0C1OAnQl063MPRo3fHA1Bm3UGhkBC2vGOwAHNIPg6S2IEDn-T-AaRwNcll_ulSKXkNxoRhwqy9EJidhhS8I2lhbkhMVXPRaznB4FQOg8srxnYUQu8fmWH8vt9YmiMQN1T0XgNTuLq6fqtQk8dlJHKCkmw9IG19fwyhGATNEujK0XrPx-c7Dwfps6OjaK17Qw5C4bjhY0EwO4Ea2mpNkAmgQByAE9jMrE0NoKi1MBasmSqG9zNdH3f_7ZJIoLXBGqzxwbObRJ6AELM0J3m-2ajiMbcJ_Rj2yJkuZhkPKX0jZonJIOKicntF8I5fgBp4FPmE61wxzLlQ0ffs42HWmgFx2Lu1zLpbYbuv-bfT9CW_KIu0V4aLDV3GWJM9UhJrXgbjixw5blxHbapUR9pDEEfk1BsmndfbZcgEktc8NYxe3L-lT9QicI_Nnyr-cExSeBmve_r5-gIueMIbbB8tfTlecanO45Hkw3XA80zw9iHYx7dfJsk93JTq2D2sghJQfgspG1wuFokDWu_fs9rTHlIjCGRT17dTvTJ9h_m7ZP6FjwHVkaZ_a7y7NI_3_YBMQSxnk7TS2fNzTpBxsS4uaABZy0JZYQzooJPiE-VazPgvpq9_ejJuj2Fp6r62I4Rjipyw61QRbhy3CmdA1rtKQNyJtKmbr3o3puTlUUwx9eU2BNy9Flu1UC5tjTqeAYNNavkPVhGHohrGFk2pQPlQPLymYhdWDwKQzqV8kPPHOwq0WAfGTpQAEVRWtXH_y6RFz7QNfV_yZ7pDouiy68axpOpijd2zWuacKOJkpp44aXe4-c-ULfVUrminfMC-W9EGX3Qb2aqqd3GTyAJTgmDfTA6thDd0LgxYVTJpL5mca1J_9tcQLaUWm6UL1wam7Ip94YjuYbVW6CEVah-RilPiGjnA43SZErp38GkxcaM1VqHJqIZE1rHvpuSRDZ2EyC_VAJJgC3TsDNVvD1h1iOSqOvQ8xepfRDSnyUD43pPcVd5WTO09Tq9ebIbEjnUDSjoKXRF9NvCVD-UAAyME0nZ8eGHgHIW61euaQh2AmotoSwNXE3ywfnOKf0ikezKdRiKCxg-Fbja39pj1DOws9-zamJgs3mCNTV5kHPWapHic9m-wjii6ywE49E5BM2OAKly8slU4OPr6-ILS7Ru2eHux_wfmcPZHDzInEr20lcQbvUar2rDtqIM0FACTwUmNs-TDSkqGyxu-1OqiMy4F-TO3UdJ6NOhI-5T5-3tvtGWFmlXUf6pnZ5PIjnahnuT_0e5JG9ygw_O046uPWqBshqEiWugg_gU22LX2wZwSvezv2-jLs16VJl2LswH0M-pmHnWSHAczcvTAwyzHvCpitbIVXrJQ9Jb3Igf9vgZSfMUIMWwx9gEJkW6F43oTSYGVgOvfqVxzRHrZ14A0DHoN922OIR5QkBG7xGSnlsszptzvoAVDo6fjP6DUqBiWBew2DPtwE4vz8EK1IuZzapazCQJbnKY6DLIJILDZrfEDixOWP74lUDobsk02orjseXU_xoKDSFcU-4ArGTZdF9yxolwn_ZjrlM2FaTGtb0KNkVpi11ieG4cd3_8b7FT_HNIHgPUyAXI0lKU6SWu3SsSc4Vsf3-tv4FrqVdtyHY0W1xinD8_l129UF4=","id":"rs_015d24a4b76463b501690da0fe7430819ba650bf76d3733557","summary":[{"text":"**Appending random line with newline**","type":"summary_text"}],"type":"reasoning"},{"call_id":"call_cprfE7SLN4qo57HclVvuM1H8","id":"ctc_015d24a4b76463b501690da1003444819b8b61b34baeb61d95","input":"*** Begin Patch\n*** Update File: README.md\n@@\n Users should comply with the laws and regulations of their country/region when using this project. It is strictly prohibited to use this project for any illegal purposes. Any consequences resulting from users' violation of laws and regulations shall be borne by the users themselves.\n+\n+随机数:834275\n*** End Patch","name":"apply_patch","status":"completed","type":"custom_tool_call"}],"parallel_tool_calls":false,"previous_response_id":null,"prompt_cache_key":"067a0a73-1730-4b80-b7db-b40cc8fb9caf","prompt_cache_retention":null,"reasoning":{"effort":"high","summary":"detailed"},"safety_identifier":"user-sb0VF3h4LjR7EW3eq7A0eP9V","service_tier":"default","status":"completed","store":false,"temperature":1,"text":{"format":{"type":"text"},"verbosity":"medium"},"tool_choice":"auto","tools":[{"description":"Runs a shell command and returns its output.","name":"shell","parameters":{"additionalProperties":false,"properties":{"command":{"description":"The command to execute","items":{"type":"string"},"type":"array"},"justification":{"description":"Only set if with_escalated_permissions is true. 1-sentence explanation of why we want to run this command.","type":"string"},"timeout_ms":{"description":"The timeout for the command in milliseconds","type":"number"},"with_escalated_permissions":{"description":"Whether to request escalated permissions. Set to true if command needs to be run without sandbox restrictions","type":"boolean"},"workdir":{"description":"The working directory to execute the command in","type":"string"}},"required":["command"],"type":"object"},"strict":false,"type":"function"},{"description":"Lists resources provided by MCP servers. Resources allow servers to share data that provides context to language models, such as files, database schemas, or application-specific information. Prefer resources over web search when possible.","name":"list_mcp_resources","parameters":{"additionalProperties":false,"properties":{"cursor":{"description":"Opaque cursor returned by a previous list_mcp_resources call for the same server.","type":"string"},"server":{"description":"Optional MCP server name. When omitted, lists resources from every configured server.","type":"string"}},"type":"object"},"strict":false,"type":"function"},{"description":"Lists resource templates provided by MCP servers. Parameterized resource templates allow servers to share data that takes parameters and provides context to language models, such as files, database schemas, or application-specific information. Prefer resource templates over web search when possible.","name":"list_mcp_resource_templates","parameters":{"additionalProperties":false,"properties":{"cursor":{"description":"Opaque cursor returned by a previous list_mcp_resource_templates call for the same server.","type":"string"},"server":{"description":"Optional MCP server name. When omitted, lists resource templates from all configured servers.","type":"string"}},"type":"object"},"strict":false,"type":"function"},{"description":"Read a specific resource from an MCP server given the server name and resource URI.","name":"read_mcp_resource","parameters":{"additionalProperties":false,"properties":{"server":{"description":"MCP server name exactly as configured. Must match the 'server' field returned by list_mcp_resources.","type":"string"},"uri":{"description":"Resource URI to read. Must be one of the URIs returned by list_mcp_resources.","type":"string"}},"required":["server","uri"],"type":"object"},"strict":false,"type":"function"},{"description":"Updates the task plan.\nProvide an optional explanation and a list of plan items, each with a step and status.\nAt most one step can be in_progress at a time.\n","name":"update_plan","parameters":{"additionalProperties":false,"properties":{"explanation":{"type":"string"},"plan":{"description":"The list of steps","items":{"additionalProperties":false,"properties":{"status":{"description":"One of: pending, in_progress, completed","type":"string"},"step":{"type":"string"}},"required":["step","status"],"type":"object"},"type":"array"}},"required":["plan"],"type":"object"},"strict":false,"type":"function"},{"description":"Attach a local image (by filesystem path) to the conversation context for this turn.","name":"view_image","parameters":{"additionalProperties":false,"properties":{"path":{"description":"Local filesystem path to an image file","type":"string"}},"required":["path"],"type":"object"},"strict":false,"type":"function"},{"description":"Use the `apply_patch` tool to edit files. This is a FREEFORM tool, so do not wrap the patch in JSON.","format":{"definition":"start: begin_patch hunk+ end_patch\r\nbegin_patch: \"*** Begin Patch\" LF\r\nend_patch: \"*** End Patch\" LF?\r\n\r\nhunk: add_hunk | delete_hunk | update_hunk\r\nadd_hunk: \"*** Add File: \" filename LF add_line+\r\ndelete_hunk: \"*** Delete File: \" filename LF\r\nupdate_hunk: \"*** Update File: \" filename LF change_move? change?\r\n\r\nfilename: /(.+)/\r\nadd_line: \"+\" /(.*)/ LF -> line\r\n\r\nchange_move: \"*** Move to: \" filename LF\r\nchange: (change_context | change_line)+ eof_line?\r\nchange_context: (\"@@\" | \"@@ \" /(.+)/) LF\r\nchange_line: (\"+\" | \"-\" | \" \") /(.*)/ LF\r\neof_line: \"*** End of File\" LF\r\n\r\n%import common.LF\r\n","syntax":"lark","type":"grammar"},"name":"apply_patch","type":"custom"}],"top_logprobs":0,"top_p":1,"truncation":"disabled","usage":{"input_tokens":8585,"input_tokens_details":{"cached_tokens":1920},"output_tokens":217,"output_tokens_details":{"reasoning_tokens":128},"total_tokens":8802},"user":null},"sequence_number":90,"type":"response.completed"} \ No newline at end of file diff --git a/src/example/openaiResponses/originalRequestBody1762500856556.json b/src/example/openaiResponses/originalRequestBody1762500856556.json new file mode 100644 index 0000000..157286c --- /dev/null +++ b/src/example/openaiResponses/originalRequestBody1762500856556.json @@ -0,0 +1 @@ +{"model":"gpt-5-codex","instructions":"You are Codex, based on GPT-5. You are running as a coding agent in the Codex CLI on a user's computer.\r\n\r\n## General\r\n\r\n- The arguments to `shell` will be passed to execvp(). Most terminal commands should be prefixed with [\"bash\", \"-lc\"].\r\n- Always set the `workdir` param when using the shell function. Do not use `cd` unless absolutely necessary.\r\n- When searching for text or files, prefer using `rg` or `rg --files` respectively because `rg` is much faster than alternatives like `grep`. (If the `rg` command is not found, then use alternatives.)\r\n\r\n## Editing constraints\r\n\r\n- Default to ASCII when editing or creating files. Only introduce non-ASCII or other Unicode characters when there is a clear justification and the file already uses them.\r\n- Add succinct code comments that explain what is going on if code is not self-explanatory. You should not add comments like \"Assigns the value to the variable\", but a brief comment might be useful ahead of a complex code block that the user would otherwise have to spend time parsing out. Usage of these comments should be rare.\r\n- Try to use apply_patch for single file edits, but it is fine to explore other options to make the edit if it does not work well. Do not use apply_patch for changes that are auto-generated (i.e. generating package.json or running a lint or format command like gofmt) or when scripting is more efficient (such as search and replacing a string across a codebase).\r\n- You may be in a dirty git worktree.\r\n * NEVER revert existing changes you did not make unless explicitly requested, since these changes were made by the user.\r\n * If asked to make a commit or code edits and there are unrelated changes to your work or changes that you didn't make in those files, don't revert those changes.\r\n * If the changes are in files you've touched recently, you should read carefully and understand how you can work with the changes rather than reverting them.\r\n * If the changes are in unrelated files, just ignore them and don't revert them.\r\n- While you are working, you might notice unexpected changes that you didn't make. If this happens, STOP IMMEDIATELY and ask the user how they would like to proceed.\r\n- **NEVER** use destructive commands like `git reset --hard` or `git checkout --` unless specifically requested or approved by the user.\r\n\r\n## Plan tool\r\n\r\nWhen using the planning tool:\r\n- Skip using the planning tool for straightforward tasks (roughly the easiest 25%).\r\n- Do not make single-step plans.\r\n- When you made a plan, update it after having performed one of the sub-tasks that you shared on the plan.\r\n\r\n## Codex CLI harness, sandboxing, and approvals\r\n\r\nThe Codex CLI harness supports several different configurations for sandboxing and escalation approvals that the user can choose from.\r\n\r\nFilesystem sandboxing defines which files can be read or written. The options for `sandbox_mode` are:\r\n- **read-only**: The sandbox only permits reading files.\r\n- **workspace-write**: The sandbox permits reading files, and editing files in `cwd` and `writable_roots`. Editing files in other directories requires approval.\r\n- **danger-full-access**: No filesystem sandboxing - all commands are permitted.\r\n\r\nNetwork sandboxing defines whether network can be accessed without approval. Options for `network_access` are:\r\n- **restricted**: Requires approval\r\n- **enabled**: No approval needed\r\n\r\nApprovals are your mechanism to get user consent to run shell commands without the sandbox. Possible configuration options for `approval_policy` are\r\n- **untrusted**: The harness will escalate most commands for user approval, apart from a limited allowlist of safe \"read\" commands.\r\n- **on-failure**: The harness will allow all commands to run in the sandbox (if enabled), and failures will be escalated to the user for approval to run again without the sandbox.\r\n- **on-request**: Commands will be run in the sandbox by default, and you can specify in your tool call if you want to escalate a command to run without sandboxing. (Note that this mode is not always available. If it is, you'll see parameters for it in the `shell` command description.)\r\n- **never**: This is a non-interactive mode where you may NEVER ask the user for approval to run commands. Instead, you must always persist and work around constraints to solve the task for the user. You MUST do your utmost best to finish the task and validate your work before yielding. If this mode is paired with `danger-full-access`, take advantage of it to deliver the best outcome for the user. Further, in this mode, your default testing philosophy is overridden: Even if you don't see local patterns for testing, you may add tests and scripts to validate your work. Just remove them before yielding.\r\n\r\nWhen you are running with `approval_policy == on-request`, and sandboxing enabled, here are scenarios where you'll need to request approval:\r\n- You need to run a command that writes to a directory that requires it (e.g. running tests that write to /var)\r\n- You need to run a GUI app (e.g., open/xdg-open/osascript) to open browsers or files.\r\n- You are running sandboxed and need to run a command that requires network access (e.g. installing packages)\r\n- If you run a command that is important to solving the user's query, but it fails because of sandboxing, rerun the command with approval. ALWAYS proceed to use the `with_escalated_permissions` and `justification` parameters - do not message the user before requesting approval for the command.\r\n- You are about to take a potentially destructive action such as an `rm` or `git reset` that the user did not explicitly ask for\r\n- (for all of these, you should weigh alternative paths that do not require approval)\r\n\r\nWhen `sandbox_mode` is set to read-only, you'll need to request approval for any command that isn't a read.\r\n\r\nYou will be told what filesystem sandboxing, network sandboxing, and approval mode are active in a developer or user message. If you are not told about this, assume that you are running with workspace-write, network sandboxing enabled, and approval on-failure.\r\n\r\nAlthough they introduce friction to the user because your work is paused until the user responds, you should leverage them when necessary to accomplish important work. If the completing the task requires escalated permissions, Do not let these settings or the sandbox deter you from attempting to accomplish the user's task unless it is set to \"never\", in which case never ask for approvals.\r\n\r\nWhen requesting approval to execute a command that will require escalated privileges:\r\n - Provide the `with_escalated_permissions` parameter with the boolean value true\r\n - Include a short, 1 sentence explanation for why you need to enable `with_escalated_permissions` in the justification parameter\r\n\r\n## Special user requests\r\n\r\n- If the user makes a simple request (such as asking for the time) which you can fulfill by running a terminal command (such as `date`), you should do so.\r\n- If the user asks for a \"review\", default to a code review mindset: prioritise identifying bugs, risks, behavioural regressions, and missing tests. Findings must be the primary focus of the response - keep summaries or overviews brief and only after enumerating the issues. Present findings first (ordered by severity with file/line references), follow with open questions or assumptions, and offer a change-summary only as a secondary detail. If no findings are discovered, state that explicitly and mention any residual risks or testing gaps.\r\n\r\n## Presenting your work and final message\r\n\r\nYou are producing plain text that will later be styled by the CLI. Follow these rules exactly. Formatting should make results easy to scan, but not feel mechanical. Use judgment to decide how much structure adds value.\r\n\r\n- Default: be very concise; friendly coding teammate tone.\r\n- Ask only when needed; suggest ideas; mirror the user's style.\r\n- For substantial work, summarize clearly; follow final‑answer formatting.\r\n- Skip heavy formatting for simple confirmations.\r\n- Don't dump large files you've written; reference paths only.\r\n- No \"save/copy this file\" - User is on the same machine.\r\n- Offer logical next steps (tests, commits, build) briefly; add verify steps if you couldn't do something.\r\n- For code changes:\r\n * Lead with a quick explanation of the change, and then give more details on the context covering where and why a change was made. Do not start this explanation with \"summary\", just jump right in.\r\n * If there are natural next steps the user may want to take, suggest them at the end of your response. Do not make suggestions if there are no natural next steps.\r\n * When suggesting multiple options, use numeric lists for the suggestions so the user can quickly respond with a single number.\r\n- The user does not command execution outputs. When asked to show the output of a command (e.g. `git show`), relay the important details in your answer or summarize the key lines so the user understands the result.\r\n\r\n### Final answer structure and style guidelines\r\n\r\n- Plain text; CLI handles styling. Use structure only when it helps scanability.\r\n- Headers: optional; short Title Case (1-3 words) wrapped in **…**; no blank line before the first bullet; add only if they truly help.\r\n- Bullets: use - ; merge related points; keep to one line when possible; 4–6 per list ordered by importance; keep phrasing consistent.\r\n- Monospace: backticks for commands/paths/env vars/code ids and inline examples; use for literal keyword bullets; never combine with **.\r\n- Code samples or multi-line snippets should be wrapped in fenced code blocks; include an info string as often as possible.\r\n- Structure: group related bullets; order sections general → specific → supporting; for subsections, start with a bolded keyword bullet, then items; match complexity to the task.\r\n- Tone: collaborative, concise, factual; present tense, active voice; self‑contained; no \"above/below\"; parallel wording.\r\n- Don'ts: no nested bullets/hierarchies; no ANSI codes; don't cram unrelated keywords; keep keyword lists short—wrap/reformat if long; avoid naming formatting styles in answers.\r\n- Adaptation: code explanations → precise, structured with code refs; simple tasks → lead with outcome; big changes → logical walkthrough + rationale + next actions; casual one-offs → plain sentences, no headers/bullets.\r\n- File References: When referencing files in your response, make sure to include the relevant start line and always follow the below rules:\r\n * Use inline code to make file paths clickable.\r\n * Each reference should have a stand alone path. Even if it's the same file.\r\n * Accepted: absolute, workspace‑relative, a/ or b/ diff prefixes, or bare filename/suffix.\r\n * Line/column (1‑based, optional): :line[:column] or #Lline[Ccolumn] (column defaults to 1).\r\n * Do not use URIs like file://, vscode://, or https://.\r\n * Do not provide range of lines\r\n * Examples: src/app.ts, src/app.ts:42, b/server/index.js#L10, C:\\repo\\project\\main.rs:12:5\r\n","input":[{"type":"message","role":"user","content":[{"type":"input_text","text":"\n D:\\ai\\code\\AIClient2API\n on-request\n read-only\n restricted\n powershell.exe\n"}]},{"type":"message","role":"user","content":[{"type":"input_text","text":"你好"}]},{"type":"reasoning","summary":[{"type":"summary_text","text":"**Preparing simple Chinese response**"}],"content":null,"encrypted_content":"gAAAAABpDaCZsHl-znjq8FZSZRAHazaofH0Ez92Qor5jOcVeHMtutgynAUmu70RdFT8LJsfdUibNZjOhXhYKh8UcddmmH2_wcJVrFsqqLX6uBYlG9QVbURAZ0F7zI6qzgLDvKywV3UiG5UYtO1u2wCq_kJxMJMEWQd0ooc5QGBmpOn-dd4A9YI5NqqDak3gEUoWyqbZ6-L_GjfHHMO9uGPnkkAO_tE_8QJ24GFqAwlRfaopsGTcA6pi8lcCncI8v7CGx5sGz_Z_y4oWBFwuJY_LStrrQKfEvIznLBx-gmznOLN1LE-Um01YzVx9Ql1CAehgCm2e-mtLfxTpeXwA8Fg6dtn5Y8UGAqPusJI6UhB_GYST5_TEh3_xytfzBLWqqTB8Asc4bcAZNLom59-mcB1gUlcRlchUNkLegckx9hN5Zy32YjWSd6plkPq8Szn8l77YWzrii4yhL0tt_P8CxkryJGR9ZNClLv4aBtSQUvWDUsfKnIiaiaDCcaJ3-CydalJ3kPAJFpoZ43pDOd5r_AlIr0NcUepaGVi5PVGY6geJOUpbRqwalyTPUPhi9qdR40kTcmfHfXcLQ-Ds5Luz0wGzBFlQ-cEnQUoKGZKBKvU4xoxxRK9ICvIr1hoF8_GIPhYCzrvaBs9B5FmPHCYBAWiZqbi_Ocf4Fho9LOXbxcNyxbPWbd0TC_wjFwhobcWb2YnDLGrl7xbWAqXgTtuiIQBdRVpIWDROH0Jngten5e9jdUj86vGlbo2fKZYtlLElo9EMRvRSZbqWwdmuR7Mgpn7U9erlwghjfkuxY0-YlyKIKJE8AF-zzT_1sahOkABPSIGeVB0PwCJYGhLDF9aeodOW-Y0wDWOasjjMk-QNIuGXYibXJpb8PtjR9-JCVw4Qu8ofTG4hjHqZ3Y8nAeeA3fBoXpqWSuU8nPpTIi5uArmvhbj-q_ohvGXGM1KTpZoiQOYFzPRVCgjh-"},{"type":"message","role":"assistant","content":[{"type":"output_text","text":"你好!有什么可以帮你的?"}]},{"type":"message","role":"user","content":[{"type":"input_text","text":"在readme增加一行随机数"}]},{"type":"reasoning","summary":[{"type":"summary_text","text":"**Handling write restrictions and planning read commands**"}],"content":null,"encrypted_content":"gAAAAABpDaDHv5UNGpZT2KBH3-3u05eLjqP5XVydt00k5_Tb21e1wfvBBBAjT9X6K9DOXYDXRNJXOz1mgKi4RjMovH1l10fPnzmKg8JaEqE3ac1bklvmnKVVbDOa2m8l_86bfHT4JYLVuxLK0obhl79fgnmoKojXbWToDpLB0DC7EEB-v6zo8PiI0hYgigftff3BGR_8nCF48zum6syn-BmyaJNdd7WZYiHGzEd_HfqwJHjx9SO1XSemtGsAuB2H6yZEJLsfeKoue0YpaAyawV5SN7YmyyanYfEmKUyNrC5PLkkqSX2T4ggZLju4qBNc4wBa7AfuHcfCpi-pODemGUFJWJDrhM7afxdvvGwBDdx2voV7jlQKr84UOAxeh3TO273BCOd6BcefBJMoOeyhu8miAtAnbX-78yBzvHwKSfythnKewCTQn9w_P_6xZ_TyekGY3Xeteh5UFxwSsXzZ1R0qUszZAvvaNGSS2Hd7xcgB-tYYxV387-Tc5SVO_Fiwk6_JiBH7_PyJ4u4dUDKWYEDS6IhnZj07W3fs6VBNa38WeBr4Av2mYk8BMQ6my1i8D_hPhFpH7oRW1UUz1aNcJPdqtofppbMOMSJwImlWRJuAiFzEp8weIkacMMW6BfVkkIZ6AO_6MjawDRuv37w9dNsW92VjUl4SBIPleje0aH6fRy3OKxXTAuSpNuLPs77BFHAhFa3uI4wKmZyFbWLcZFv491ij3lXSqqoek7OwdMxFlnw6rFM4d3IE-owaIFqQT8OVNksoWCX9d5ASdgh-GJpko0ERyB3FBxMWs4D7SWOf9855hz3xzIn_mAdm-wUkwgBMACy0xxDzAx7eLG_fjQBlMRTRessE5eZ0i0NVAFpi_TvnPTaYKQjVJk0W1PA-rVwgU08kVcuvaBwErMjzbg8bYRNztKEchgbVIFJ36aYIo9xoQoxQestPOfYXiNq4aT8IhTuVrQK_cdv-WHPKL7BzUEHky3CL8LrsJJ8Ts5UCk2G4T16bIwN5vlgLANki79qdshfFLCS2n4vMmu43HlPNWcn9ROfCSXbt6Z8T9E4BnoIt5mOpFOq0hfjHpSgZaTLTIxF3CLAOtx7NtHPRIW0fTtXITV3B1TAXq-epGr1CmGtyFf-QzgfI5emsOAyj9kbZAdtIhdAv5Ry2SJdSWEwrs-1-9_Tvv4Hhrj3nqWIeaR79CS9SAu0RHg8JSGL4h-9GPsPYpvSxEj2MqdqsZndThEEdww420_RyNlHE1GzxhZMTgdNvVZpu4knX3kPL67MUCqeJhOuVhqn1OFimDGjPwJTXXUbhxn0S4yjzfRub1FoMaZSL060tCUO-YY1q-fFK6X3OXnyQf3TgIZb-yVI9m5YO6o73ORwv9d6zKmyw4laF6lSxyFDoAw7FHCQ_riTIrfsjRfRQBt5WiqmgFG9Ni7jk2_kBt-CSfHYdFj_tv-nlVhf2k8gt-Fzza6amGyteFRQKpAZ3CZIDBId3Lap3ovnL7TOH3cU66QGrum4cndrcYPMckqJi38N_4RuaH4qzT8tGZm-hM7iqAn7fQg7Tx30KkuDknqiqWDvJvfLoqQG-dgWY8tEzSSx-1uZ6Qf3YMrwkMFBcYOAGuBO2smusSYw68n-mFkwNEhxRm2DLEhEmr4rUtAnnIkIRagMWS6_zf0cqD0CtMvpAWcIcNqMVsUkL8iKJtmENcemFdXkJwfncN_HQfmGnMpUbYv7n0gG1ZMbQED14LQJ3fi3Lb0-dg1PYFMj4803YG8fyOB3KGKtU4dDvO8GnSx9lawpdiaVJPRdGwk60o0toUaAPf9nh3vRNXfZSpVeCKMc0vddVMkByVUnmNtGKC08vY3MStDcYj2b41snRuyBDMQlzXCdsUC79j6MxPhjER4eN2p5UyBjw-SbuvnY0k5rXtbdJcqKi6hOculh1zDAA3SyhKL_VMInvWonaJbRUCez6hw81hU2NUvgMXqj4YB7vIsZ0v8xQFt1XJuCQttWR5cctclkYDF5gUcquz5CWYx7wtsc413ili5a6TmTvLugvUsvhiYenENuqOTKBlYE9tkkWv4dmyXllZqurRLy3QViNrrArdwWhgyYHpBJRHcDILxKO9qoFCRMk9Z9GDjy-XLLOydITUrL__BBiYiWyZG7DOpu_0MOpyNG6wiiI4Gj6WKDpCwR2f63fETUHhhGt0x8CTjDIL8k4cT2cFetMgVgDrbiArnr4bUhcwSUPuQB7RPTQf-wG2AysIB0-j02Y35-sMWwzqxmNZTICLmutSd-n1NhFmiZqQ6g4KmcYf0bYde8iGgFyyf4Fug5vdeuGZjKzBjJOc76FJKtVkn-y7EIvvwBuiqod5mn8xwPwivajMYE8W7gwuUSLd2kt-XG7MUlz7F7h9qc-E8Gz84i_Z5KqgKGv1wENO0ISHdotxw--ogoCTzg_cwu2my6BvnWwY7AQ278TwytHcaJ-I4dVynlLM_r5uQvUjL2uOLS5MqFbz34MKYR1R2CCUV5R0D4F584NZ7StSXnsx5QMt44FxPs2kgKW1UjHBea2EXkW_SZPc7skcm4KO2CZpXxp-vEv2mnhLENDhL09Fkq7cQMcTzj6Xxv3daASvSS9LFIBctzSEe1Byx74HgnGdekYlc9W4KGhyHPnSvQojA_vZ_S8hm38_-P-jSGE5wt2a08JhBsszqq2ccXZbocPzxVlIMTAYbLSm5kTI9NDeYyRBf3UFOT1nDzeMSXQ34-eaSMiksPVChi3pwKSDeqzZzvhCeBzhlgDtCXyF2qLEtfowtligDpXmRn30U9H3cbGT2sCMYLeKgjC98pTVVdCylVf23KBJo17C07dnItiWaLV610VgqGnqpCVWWPh5rEk7spZk_GsPXFhq9XOm6Mn86LyMi_9G3SPdmaTdyIwEW7ZxWBbBwW28hct3PJ1zNFSekb7tKu8vqWZkXnhVNjs9K8ft4g-Pf2IZ_8y4-lNI4HKf409IGc74w9h748Md2KtzZv7zGKW1_oLQPSEW6TTb8cd8tAahzvPkOTKvz4cjX6g7-8EcGmOqungku7LmxGehSLqhWoTJt7AOeMuZcofgm8iOZzYjWTgjWvh-mu1JMWsP0MKJUk39F9WIq1fTC5hOPtiZ6fh_20Th2WzbRuEgale1EYyGHMiHm7lAbSGRmka-csNZulB65P08MocLspLVsycLWO4mn68-TLqSI6w5pWyg89ivtMB73TfvCxu2GrdT1VGWjZO41pdHhDxWeowuZmMP2sA_QpdDi5hG5dveXbILnMJuSMqbUzPjPH-ww16fPKtNW_TLiNrf7V_e1YX5Q5JTyOAuBH3pnMx--N3aWjAX3Iv8n-TXzF9YvylhYvFKoA0yVJDv5ErJrpyw4oIyg5Z_YJ2x3Ifjzy8-77VV-h_pRoBi5b-iqR5FV5mbDt2BhDP1j-w8xRh74iU1mx9_SYrq7oc4MUVbaJFWtMmXKd0vDEg_iqUVxKfQkxqd4vJqABH8qasokweGKadM7SOqwL0NyP0UeuxPPMlAQDKfWBxIx5di9TY_KKM8AMz_IHm798MBi5w52R_0nwha7ZVOgilAgawbqYnlXUCQ3VC2NE1h3s8onF2uk2zKwLTxCVGsosIHY_a_gVmSnX-M4gpTmLpX-dYSyHQlyuvdbmPHjicRymclgxuDxDj4tJbBgjnhRvJxzSn04T6I_VK-YtPx-ODnYUO3egcpEAn_Ac-5F43Lx9C_ztRLgweeOWPfq5dEQfSpSTA5hk3z9JE-mQmWoIIOHdm0ZM4f92HvsGhKBgdduXovHZjbEvgdfL8RngpSLxUP4P09TwzNFlhoLoMsMpRrN7wIjn_Sdo7x8TURBcwRN5ZD54doLqZvV0GtgRFyWHznxbwTOHOpXj0KPBf2_gDGarfMEa8VP23tzBFEVm_8sKoZ-mICnVKFW_NP8eJgGYHSk6oCcnBKx-vJZiltvmmJXHuntQBZppuIscHQakYKe9VZVsXzRDR-Zn8Iq_md_BKbaSJ-wUf_uqrO2mvv1eChD3hOS5aBgF8Y7jERoLc"},{"type":"function_call","name":"shell","arguments":"{\"command\":[\"powershell.exe\",\"-NoLogo\",\"-Command\",\"Get-ChildItem\"],\"workdir\":\"D:\\\\ai\\\\code\\\\AIClient2API\"}","call_id":"call_duob9Re2Vhrfzy3LFqjqvnOW"},{"type":"function_call_output","call_id":"call_duob9Re2Vhrfzy3LFqjqvnOW","output":"Exit code: 0\nWall time: 1.6 seconds\nOutput:\n\r\n\r\n 目录: D:\\ai\\code\\AIClient2API\r\n\r\n\r\nMode LastWriteTime Length Name \r\n---- ------------- ------ ---- \r\nd----- 2025/10/16 20:37 .claude \r\nd----- 2025/10/7 16:44 .kilocode \r\nd----- 2025/10/7 16:44 .specify \r\nd----- 2025/10/22 19:34 node_modules \r\nd----- 2025/11/7 14:44 src \r\nd----- 2025/8/2 17:53 tests \r\n-a---- 2025/7/25 10:56 91 .babelrc \r\n-a---- 2025/11/5 22:40 236 .dockerignore \r\n-a---- 2025/10/8 13:54 127 .gitignore \r\n-a---- 2025/11/7 11:14 18813 ARCHITECTURE_DESIGN.md \r\n-a---- 2025/11/7 15:14 968 config.json \r\n-a---- 2025/10/15 22:03 871 config.json.example \r\n-a---- 2025/8/12 12:21 1066 Dockerfile \r\n-a---- 2025/11/7 15:24 11071 fetch_system_prompt.txt \r\n-a---- 2025/8/28 21:23 1028 healthcheck.js \r\n-a---- 2025/11/7 11:20 10087 IMPLEMENTATION_PLAN.md \r\n-a---- 2025/9/1 22:31 18 input_system_prompt.txt \r\n-a---- 2025/8/2 17:53 687 jest.config.js \r\n-a---- 2025/7/29 20:54 35823 LICENSE \r\n-a---- 2025/11/7 15:30 34832 oldResponseChunk1762500653964.json \r\n-a---- 2025/11/7 15:31 74071 oldResponseChunk1762500680771.json \r\n-a---- 2025/11/7 15:31 31251 oldResponseChunk1762500715918.json \r\n-a---- 2025/11/7 15:32 25252 oldResponseChunk1762500762249.json \r\n-a---- 2025/11/7 15:33 38293 oldResponseChunk1762500808580.json \r\n-a---- 2025/11/7 15:30 18192 originalRequestBody1762500645337.json \r\n-a---- 2025/11/7 15:31 20828 originalRequestBody1762500674743.json \r\n-a---- 2025/11/7 15:31 16564 originalRequestBody1762500712251.json \r\n-a---- 2025/11/7 15:32 16563 originalRequestBody1762500757021.json \r\n-a---- 2025/11/7 15:33 17903 originalRequestBody1762500796377.json \r\n-a---- 2025/10/22 19:34 243568 package-lock.json \r\n-a---- 2025/10/22 19:34 891 package.json \r\n-a---- 2025/11/7 15:33 4362 provider_pools.json \r\n-a---- 2025/10/18 15:16 3628 provider_pools.json.example \r\n-a---- 2025/11/6 11:17 25478 README-JA.md \r\n-a---- 2025/11/6 11:16 20166 README-ZH.md \r\n-a---- 2025/11/6 11:15 22176 README.md \r\n-a---- 2025/11/7 11:13 10240 REFACTORING_GUIDE.md \r\n-a---- 2025/11/7 11:27 9947 REFACTORING_README.md \r\n-a---- 2025/11/7 11:15 11076 REFACTORING_SUMMARY.md \r\n-a---- 2025/8/12 12:40 2506 run-docker.bat \r\n-a---- 2025/8/12 12:39 2220 run-docker.sh \r\n-a---- 2025/11/7 11:26 17452 USAGE_EXAMPLES.md \r\n\r\n\r\n"},{"type":"reasoning","summary":[{"type":"summary_text","text":"**Considering README modification with permissions**"}],"content":null,"encrypted_content":"gAAAAABpDaDaMcvFnZL-4EHykMuGp2jvpUAqPx45wzJA-FZhVUtesHLTOb-XJiqSstEorA8Mt3BGaKvkQIapzTkozQAwN4Z0_Aoy9cmrI5kIix57WbvMmxWpl1idpIHPJIJg05BEtnf73nyp9hfRwoEqsbvM2cRlVHcGuDDEZzzVyhNIb9i-BgJl4ZvviK6UxU1edOuvpy2Hl2DcJQi-lIoMlvp3uaKg1VaFrKoHt2IwDsZNK3D7kMCgoEMJDyIZEDj6XEwY_By65Gxo_iiIaVFqNPHbgfXOSWLKC38tKJBWBjXUv_dbhaw2Yji9r4xlicWomYXcKecpysch6KGPKcIpsh-FLxCvBfpfC_OHXm4POKJqQNcwqoStI8sfaLJVUUi7tAe978yGcRNwruLn_4zB3vei_8d5xZBsI6Dgmrz5sqboCxs7Llu_dZGZiw08RDFSeo938WidIuMyfqZeIeVZ9Bi4xJijIHqAg3wPs1K0L9wKCluRVeLTUbulB-SiwkAELYD-tP7_Aa-8awn7GGV5cuqr5L2qiohpPdYEVOCC3iYczm1vzclmvAUQnT9hoF47wrRtAU9_SNoK-e2r7P5Rzfeiirl1ZIpvWwW1eHlh1XvgD0BqWLa21m2XTYXnKZ24BqgOPNPrkuEVfVx59bU8zX_WRNpTn5f4GXH2r4PCFsOFRVz8ujW_9TibIl50fEcvTtVUcKPLOreR2iVHW5qm-qaOwnwnA4RUoS5gvXONIe06vhIsX4hZCQ1PHUBYyLNSlL-cFPbE-l9NtruX0eQgdqQfeK15emTzybhlcRn_Tt64ct9ct2nMV3LKz0GRfB-zKrlmJVwgKEq-q2xYne7RJIGOQO24fKw30SwNIuZurF1iKZZoHutEdZjPpMYUhcK4H2HZ_BR8SYqwV5vD82anj2CNnZZuPg8Cs5dLF7tGrRN8UM2jlhLN9z2FSJE7MOop8MwXYMp2nvJQ0xRGxhVDepTwaPkFiQt_54rG5YNYr8rX5J1G69F_cB8NmxkLDpDdnyvM7hVX6vkDxfGvzVXWyOAYSO-Yjowg6SUSNika2XIZpoYLUBL5ajOxAFr5oC7rvVK-O0U6nm_ZaCp86QGfispYzvQzkWEE55mNxTVg5UH1rvFD2kB98Y2I5uMxfxm-EhMrQplO_lCQwQCXZ6GDVSmyLs6dLW422_RYu97I6zZg4XpUh5A1_QSMwrGAJCNejCLTzIljBhevx2XWFkwnNR3b6NYD8jsG1vUq33hvK8eZCfnYgQXfqMZX7OkCnOYAGA-02BxRovcg9ZOHiOvYZcypR9a-0XC1YxWqw-kmxyLl50-8r5n_Weo-8czK5AA9CHDbyyg3Z3SxtB6uBM5MqWDahKdrog7P9bWcKTIcEHkHcBj5XAm7YXLag9ZMexO5r71_EBzVX8GSG5MlXgbvSmB1vDzs06zspwb3pATP99rixYm19ksYv01UpN6y4WwzLumQRl9YuTBArESOvu8UmhfBUGgXNrBePJCmp46eykpTP9NVXgXs_0rNvRNGUzJg091L7Xyfq4DOu4L9ZwdyrBEkJ0MU5hRNmcB3DgVoKIFW0pgJb4pehDw-6LH7fIl8dT2LRGtc1jJUGHfYA7LZ-wERgB9YEWc2Kh9fY0QzTJo2GjS2C20iHbm2nGj0uRfk_JXAzOQY0nbQq-EfcOQKFD8CPRbbWdfVL9fKpPjOlCqeG-WwAJay9uefaK3ovgbVLSqjj2yJ0fazLaqSmvqGh3E7gI6LrvTDeWHs8FBsDJ9JQcu_-94QpE07eVtReTprTp_ASIMUkORa3o98u1gjA2GB7wNgQ0vomPHMpDLZiwq0XSI1rhb1uIh5d0Lp7Hbd45ADz6ZcvYs8iv4zXqnZBgbwCPVJ7xeymYgD4ZLFy2NDZ_ZY2JPUpzc_h3mK84PZJAIGM3EKXEGKw_B39d0bhFAICtaQ5hy6DY7YZU9jaetN7hA1dFjdnp_kzYDUsdp8LzrRrtOvrfCqeplx0YVRkFBAwuFxZnjkec-r2RWstBVZ-oM-PWmTgEsAOvD8P1NN78Dx420KsTDRi1NxBbVbiHc5IaOolUUt_VHJWoyGECNQtyd62FO7nFJKFmAKs__QHY8WXgq8k9WLEmCE5pV02SB6YwRq3Sl2GQCBOhcGO50WRdVfKKRpdlBeW9XXV-3o5klYpIMqlZP5KcRscQjYdSoAZH4y2MnjwQjJid4VJpfN7aQgSG69kzHhpgki_KRcMW-O8O2TLDlOdhL60SlDNMRZxrpLIQZ59xl2rPLXjXCTzA_5cIsGy5rfeZiNIqlEW57BSmA2sTY3TV8rAfNA4SFlP-5SEOSMJZEM2BPuUhwyD4Od6hfN8zs_o-gJeIJI4OwiOqkOQsAj-RuAMKI3o8Q2eM4MP64MNv-AdZEveNilwWLC_S24WS5WNVRuWiVeQW9Znu3_OQgJZK7w5oTOQcQJF8fgppjynMTtDYj4XMjd7O355Nw7jOGBN7FIrdb6qKUGXWFS79dZ8uLNQkgLI4DwjVoXE5g61fvWd9dfZoXCGqm-H8kaLyDgRvr5PZW_g5emYEg-QE6NYBHsaTbUIVAHWM__wg6jUwJrzZz5ccDYpYekuyDoc2HGsvHq8xPEI9XRnfmoPJeBt806iJn_xteyAUYmTsG9OfPNbLMnHRtalZrkE3wRT9_U7Eb_Hqj-Zmjxn-vH2rlM0bNw2NZKDJuersWcYVsMP0k5F_winyx3tPQrLeZTsIsnS8Yc9mmIpkFhSO4JmLTiUo3DxDNPmFt7kFFWAhx0_EOCrj1qCNdntipJdCge7Gk75PVwISpxaYKGFFRX1YczG-3R5dOvL1x0BRRsgNfpgDBFpJXkpdXiZrrKQ3CdV5WkyhhagfWN9PsSc4YQBlCPE87Vt5bZ1RlPkPGkV5c2ZYOGiO9_Brqsq-MNdfGKGu-3b4kH83OT_G0aeIM3ofEFed5VunBzldJ9l4BhRAKD9bKSVUS_zu3QIYKtUwgzHqkJpml119n9MjeGSGu1MPVleDpKfnBDufc-2u8nxGHjcYrhGqEH7Jiga6AJlzwJ6zGRveYSWzke7dW2UzsGTrxkVcBHM1SEqjQy02AnAdKoPn8sLnSimA69mU0nubf81-4fTf0uf__huE9epfEs7GZyhOC9J-BM6ylNOEZS_8AWsSyCZje_tqZnK06_mrPGJJu7Rv95K8BFB2kr2KGvNBxy88nF8N2b65un0s_oZQv2ieTlu_BSwFAWw9RbYhzR0QNfb3pjKn58gAAai030GH0QYQPmYX7JdaMla6AUnIQ6RDy3fReA3CPFIBIfhJ_AkfSs6AQU8Epzq1xDLzgzgzBsg_V4bIRynPM3KIzr_PVzJftY7RkKWOp0Ai1E-W-eJi6MNYq5SGlsD6bNyYpfOw2Jm8qe38G3JA6zd2Ej26lhGmDCT0M8QUhR9ybv53RW1o3axKGiYnHsn0tpFWioCu4ve8t5jESqncN29a26XMyiCKfeDbkgEK3_abUnKKUOlQ42DQA3ZfhvDhaP4UJs6Zwjf-lq9JCt4Bd7Mzacfbv05-yBqUc9fAa7w5Ti0r4TSZ16VvNKtgqnptgmeAIOJrOFFkfHEcWr-Nn3zxgHOYKYgKMREXI734jJoXlZi27uFkpXvfYpkXF1-oVbNywFaZV9yViwzhI6OhtprQcTCoPvkCD25cZjyMx1yLqnWzYuTVpER-OHFn7YDsa48p51FvZ6YFQ8VL_h6jdJi7EXjBT0U90YiTl8HeWOv87aEtsxoWIoNB1Ed6qloxO4VEG21Z4MGkbKUWNij6_6oMKYEX4UTiLO7sPH6TpHaGjZzpXNYAjh-QzSBA__pO21cmEoigmNp-V10f25JWvdv9jqGZtd70Rx1i0wapvYmoYObh9BvWqSTmTHDrFlDRUmbZUa2Sy5jT0O4r6a_CAU5EB1rV9BPJLHv_X0VZ1YzkxwuTNQ-_hRUwwpA2Arp9ys2GVMO4K52CLFy4rQDrIJYtv9CUQnCu6bcW47Jkv1ZNzxbpJBpM5enzuRt7AYlemMX8FpTdM1lB58S43cfTSK8zyxvec5SRF1UggpfiF3fyoKjQTd1rnbUwle47YJMtJu7NXFDiPuIceHucKrN1VlF9asn3GLIBQmYkltyFi64eQn_gB1BFZ-hQmS4j8EiwzJ9YaqeGRj6EGKSDja_CNIX2AoNqgyV_5xz1NTskgsIhuy6YxJE1NZyW4SYOexjVhxxLuj1MnmEyvrlt4WAwubFw9jG-qBbxVd-gY28vjAd_6Gh5NRqBf59_c6UKKIgAzPvaBJmedet4Z2a5Sd-CSAdVXBlC-nrZSHoifD7gRLCAwcB1_GpwsbWSOhpixKLgq843bZlvzkETukVDNaVLzuhCf7ZpMELzlEcaNHEodhL7MG74TSMd_t8tCT2FerMK9bYwmJaMtSlBswiVcAra_fzmf86WnMlilWiDsMUxWOaGZHoTBysrBt-2WS3gYE4OLj-F1pz6ZWjmy5dMSmT2X_vooobxxE2QLgOZ1UcFioClcEFHEl6rtT6TYZHx5hVYCkB8Q8VyJNkUO4sVjI84dWO9svrN17o8w1P6KroT96gP1QqWhzBdICJxDlgs7EJUV5tyX5OfFiPNoIzxEB7REBiKQx3QxzDHqGEXI5m4TUprrq3HzNCBJVFuZBrQgizQjd5-u2K1yFBall1XTMn-cGDBKymanqOKIWjlihmcydFFDAro2a0ab27sGmX28pWc0L4NwPwR1rSONQt42-VeplKd-e8qIa-vXggZQHb8XMAIzuxn_CHS1Im-Hh3Ftuxn0AsEvLZunfR9cMaQ-1MjmZ8PUxCz7ORSorLVUA74JoNPlPqsZnYehMUELhjINxHSZpyqW4qoiN3QIA2PuJZGW7sUBzkwgBpADEi3qWAD90dTfY5cTzsFYFQ_jRbwOj4gEzEsowFUO9oLveOFu6aNeRp-Jzsnghs6FeWIHpHlDb0zuy5zobj0CQ6Ho6necnqVpl7kh4Jk5t5eEfoctMSGJH4Jp3XCV3BV3RfeGt9oCwifEvIJp2n-st15ku7Zq3NCvSNAu_1te7pcMSm9GBoUPZ4rFnMX-SmYR0va5jqzLIJGZTFzwp6T7pMxHXitkh_lJZQ7hN7Q-AR9_Z9KC7o7jckwnHwoLYG5NnNt9n06bMeyDPz5JN94UwYiRQkmTLI1PWK_slJCNgubEh8EtWNerPxKejRR6MR7j_1AFujrr6Te3TXIyzLS77fiVG2eKLgdCxsnbpMditkTiahwrOGfMH-YLeH8Mwd2wP8wd_FbB3H1eN3gbpHkmUw04hSk6az_mKGQxFYyDufCH4g9RWMFXMsEBnRZGW1A2O37DJ-virgZvEBN1k-UfFrqgJ4P8DS5WeEwJRLL66qCfH2yM3r5-egdpIzYTswhfWjbh6s45yVWk45JIhy47I3dv_T2_K9hmhKc0Uk6si3m4dd17nDa8LEt9Ak1Lf6lFtpBAKGJsaQAes0pgu3qLRMhSVAJgDCZIQPCUE0BmTcoaYueKV8NK_vu0hENgXquDzELFuHetDkIIHugtunjKQ15jxtlkvVeI2DkUmybA604YbMl3alKVsyYatdQSHOZlVKPKLIhq3qLU0B59NLSaPeekM8WIwsUGXvFYzBYoDBYponoCm4a0CZHbC-rqd8hjlmc0NuASadwlQP5K7Du7HEtr3k1-ka467kJzisDX486HyoHewH-Xj9z2q85sbr8RmJ171o-Yq1HIwTf5f3zy5c9kWkCRXH3YzZJC2P0lD4SDRgl_HttqUdUqaQpsDNjo9mNc_JHfWxJzAy1eGc344_XSM6JD5fERBnqcOYGn2L4VsXkkgNiFWG2viCZSEowyFdE7K3A3V-bgi2Lqh6Z2Tcs6NTXKllF4vlV4vRGmJddw8iJsoyjSsa5c63QibEW59fXgfnNyjZFH5FufbByFb8K923ZVScs4pGVp94d73q547U9ioGmo1XjgngHD0_SQjaDcmgsORkilwVJ4hHr0_Z3Xr_jfHOsR4zdBHy9CzzJNMA3GvZhsLISATtCb_O6G4llTn_HVB6fDzlTCSHET09kGxxTDREHGMa9c6vg2rzWIbrjRO5m4SsC4goZaJR9GoNon_27g5g9XvNGASLq-n0bd7FCc1E5r0MlTn"},{"type":"function_call","name":"shell","arguments":"{\"command\":[\"powershell.exe\",\"-NoLogo\",\"-Command\",\"Get-Content README.md -TotalCount 40\"],\"workdir\":\"D:\\\\ai\\\\code\\\\AIClient2API\"}","call_id":"call_Fa5mxBJtjPu6LzaM8gIQvaA2"},{"type":"function_call_output","call_id":"call_Fa5mxBJtjPu6LzaM8gIQvaA2","output":"Exit code: 0\nWall time: 1.7 seconds\nOutput:\n
\r\n\r\n![logo](src/img/logo-min.webp)\r\n\r\n# AIClient-2-API 🚀\r\n\r\n**A powerful proxy that can unify the requests of various large model APIs (Gemini CLI, Qwen Code Plus, Kiro Claude...) that are only used within the client into a local OpenAI compatible interface.**\r\n\r\n
\r\n\r\n
\r\n\r\n\"Ask\r\n\r\n[![License: GPL v3](https://img.shields.io/badge/License-GPLv3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0)\r\n[![Node.js](https://img.shields.io/badge/Node.js-≥20.0.0-green.svg)](https://nodejs.org/)\r\n[![Docker](https://img.shields.io/badge/docker-≥20.0.0-blue.svg)](https://aiproxy.justlikemaki.vip/en/docs/installation/docker-deployment.html)\r\n\r\n\r\n[**中文**](./README-ZH.md) | [**English**](./README.md) | [**日本語**](./README-JA.md) | [**📚 Complete Documentation**](https://aiproxy.justlikemaki.vip/en/)\r\n\r\n
\r\n\r\n`AIClient2API` is an API proxy service that breaks through client limitations, converting free large models originally restricted to client use only (such as Gemini CLI, Qwen Code Plus, Kiro Claude) into standard OpenAI-compatible interfaces that can be called by any application. Built on Node.js, it supports intelligent conversion between three major protocols (OpenAI, Claude, Gemini), enabling tools like Cherry-Studio, NextChat, and Cline to freely use advanced models such as Claude Sonnet 4.5, Gemini 2.5 Flash, and Qwen3 Coder Plus at scale. The project adopts a modular architecture based on strategy and adapter patterns, with built-in account pool management, intelligent polling, automatic failover, and health check mechanisms, ensuring 99.9% service availability.\r\n\r\n> [!NOTE]\r\n> **🎉 Important Milestone**\r\n>\r\n> - Thanks to Ruan Yifeng for the recommendation in [Weekly Issue 359](https://www.ruanyifeng.com/blog/2025/08/weekly-issue-359.html)\r\n>\r\n> **📅 Version Update Log**\r\n>\r\n> - **2025.11.06** - Added support for Gemini 3 Preview, enhanced model compatibility and performance optimization\r\n> - **2025.10.18** - Kiro open registration, new accounts get 500 credits, full support for Claude Sonnet 4.5\r\n> - **2025.09.01** - Integrated Qwen Code CLI, added `qwen3-coder-plus` model support\r\n> - **2025.08.29** - Released account pool management feature, supporting multi-account polling, intelligent failover, and automatic degradation strategies\r\n> - Configuration: Add `PROVIDER_POOLS_FILE_PATH` parameter in config.json\r\n> - Reference configuration: [provider_pools.json](./provider_pools.json.example)\r\n\r\n---\r\n"},{"type":"reasoning","summary":[{"type":"summary_text","text":"**Planning readme random number insertion**"}],"content":null,"encrypted_content":"gAAAAABpDaD1Gvr4CT4tWl0uncIUy99tELW4PGvSeZC6kGNCARx-jA-k38z-jcbgLitn8TfcMnzWrymn1bEGg8oB7TZXp1OTq_FDYhGUtE4rToq7aPpFhYDdIwVKrdZo113dsRJwTGl_XYUEzUBtLMDpBeELQbAP4Wso7hZA3yCaCqunSHjPpWiF-mQgrJqdAeJanBCgxNMHiPv_Y1fx_lvtSiFhSlvVR-HK_ZQ8gLbo4WRTJianS9fCfcO-VvbqYUyMiLGGZrbf2GugEu7rJCpZDs1vw2LCQ5lh7fXQFm4_y5zo28XezNgkvy-88cKPk_p3FbACZzEWxJ4PNsaUQ_jwBPVHTp_kesoaQ8gikha8IJLXrt2uf8eGOezDDGwNtB8FJd9odLt_VQ0kTcz1_nW8dEISeiCBUJGCcrOQRVaWSXMpXaw_I54HE5vBx9iRXQtiN7TBZ4bZYRpgj9pEXUJfyBON0Bu81hNZE5FuK4MBc_Z9XRyTeh8V-JsmrKJ603CMbIwlsRihDKHoIOJQTwMW6O1gIvQ0GkPDKERmwAu61sm75aW7pzKndJe-RVxi2QQL3HW3Ansf3YhUYAM17UhLC-RZF1RYRRx6SyyOCBB6omqmvZ5wVGk1Y_w5jssGxRfnJLHopewM2m8YQrw3spazoALS4ghNh0n8FmAzm223EmtypHRD_qyG4BnmKigDnn4m1-xeMMs_tThJUtkx0tE5HIuGI5v1cEij1DU_VEzvOHW8kXvZsqwP7WcQDVBdwMIHrCar8Yt5XmliRh53oJw5Ov5gsbegxu_fIjerODf4Oqd5egQkM1rKpVaNlM0JyI8VjZuwd4sJxKdUHBpmdaDdjhkTKNOIgPkTQn00GvtEBavoh4NWfryCEUlVKMtnz69uhrA24TCtgGTImee1QECDp7trOfETJtAQxKK07u2uMImLNwzb7nl4sdHL-BFU3dYgHkrDANkNsNO8MjgUTIgn-2N1t0S3UcUJHznqWgmju5nfC36QBShv-nWkZ4rpm2xsJKRjPxsDXCJFzYmLuUfBi_U2Ym0CaVItLz3vnSsV87LGMaToophbLfNjnqrOYxIUJHGRZXDGQ2NG4wy8UpgTjroBUVAgBXwCsW5HDojzFRVxxQbsdJJG8T5u4DmGR6jgNaeRDD3Egx73i6-7fEQn84zQlMPUb7pCFPaBo_Ft6i3ytTSiDvYZz-DO0s9JT2p6yTnIW-rNtebZjIq_20TxcY1oCywhh_-ZncI1bFg7q5fJzKzOyZQBGc1TqhIhF8DP3QF_-TAPhEQ6CDeTpVF4MGBNU3IiSayS2-uIpJtnhqfqcGp2gLJjy5U3f_4O8GmDC3FKosY6ObqgYWv5rG3Tey-nN-tVbxkOmPlYK8Ck9TEsDCu0aFoAGN8ab6FP-N0qqGgIcOMw4SjHXY153Q8hMYQU4u_6SVLf6qBpZCGdeB1W8d_0tm_w-SJjLb5tuCOra_aC6htgL9u6CDYeOIpCHdtKbH1FSxOTvuRDNCOCRIskPUiCieTmIW0d0D4sC89sHYUM8LwY-xMyHIztXznHyUrbyakWhAjJJSpze8ZNRxHXT_h4bA1kp2UZDkZJE4nJcMqjoeG2g2ZjOLi-_AiorlStE-I_YVg1Xb-yeMHgTlVQb84GIVP9-9FdGBWauaV3tWBcASmJFz-yufDA_hc64ZbauhYRDuTIfWxFIOPx009DSBCGFzG53aF6xTWNuir7cyRt7nDuML-unuPddqOAVIRdEKVZmLVljbD4eLfTPxaawDeHYUK1FIWUl4wbNLBg0OMlAWQ1o-29EU7MOtKpZNzdmEkIAb3IsD1NPyztXmGqZ2utTS-4jPSueunBdxSm7wYtKwUbDUGycuE0HOu2LZIJZMuxhGlO5CSYQ32z3ukByHpwuCXFKDoCbrioUFLRq6r0QAc7O2RoxyvXWtG50WsS1-GvqXvqpvO24_KbF_jdTeibap5cXToS1hsZ9osmBgaWzrmMkzqgiWXzeHwmla-L9M6FotHfgkIXZ3JQVDzKFz_ZpyVc7eOqnnlbNuhremyMZrNcS377MUPS4U-XTLHepVT5edX7JzKuRf9Fs9fynwfvQ7T3YCOAZqWpzvMEwcPDokyH058wO1RgM0s3XdOcKbGWBum0mbuM0uZSalhy-xWlDqYahs-Ci-iQbTBkuwGv0GKbLxA_4OvEzluPFgY-hbyL-o4sHrMuYUFPO3YWayiUrJkZS6zCJlJ9JHkxenijTQqgcp7vGrxhHMw8Pk0uQbwT0AydpDn9fV2ZG9689Jt7qyVh2MekE7ftvbU-oXRwjGiI1qEFdd7i-0OR1F9jmrMtycMpnUwZ5nHoWSVvfekCaTE-D-reJkBtuwYHU1pg1SQ4wxRCEIuUPmHSgNvJ-TVgaw4pMRMKXsKBtCBiPBzWfbWyRu4VElBSjUJtMUTRazA7gBnUmdRPteOioeU94zKRjaV3i8QfHxxGTLdZI5eoIN3opdjeyisada1-D7dQR8nBbNrLsbNoN0yz-OLbkiG0QKzendeZjYJtJPXZthbTXJ4sIfDc8iUyhy-0viYPt8q9wVNowGZcrCW2bCI5d2YAPd-wbWplovxsiWd99kmx2szc--WVpeM96AVsbo8ZkjpR2WRg8y-cmwp8vFw6Llcnlo2JlhKbETPkIbfa5ft_70u5vrIaegwalGoS3qOSJ2CeMcorMylS_86tJPhiT7yL3b5BAWtqg1nRCK8Ybyndl4EgfqaJSrct0yoLfYthJRijbxnNrRvH8GzXHTJf8AF_ytWg93xWRCPEP3iIkVNgyjoB5DJbqDvwY5X8JFjKTu3bS_INcTD9LILA3Ut0XBqEdx466M5M_Bx7J2EkEBq2d_nHnjUjL2cgc05uxrX_jE-DoHgNJkk6-2ZQQQoNQCSO5GNT2LKnBpV_LXEg0r73jffEDL0W-GHzN5Bzp0K7hiaSDkZonrxBok-ba3xvHztNnCAYh0oBN6oBhIyhiKmbDMSdW6Fcl9WU_ILyflOFigwtSALrcrXM3Lc4R80zdr-ojSrzmLSnbJlb-S2ZXaAD2c03b8vT8TLoPkMFC-lRD5EX9D1NLXWPicUMHF-8bBsPKvWYHVK8gbvZ3s2QIOjwjVqP68UkG6GnPfulqWm5xSXbT-qePUrTe144J6pIS5C7jqv5MdGbAFHed3gJM_F5KS7AfHaWfskpdBc35l8Fb-Z6VgxGYFrxFU6lT9mnjhM-sqYGhXYbVDLhupA0XIqqcMg2CGbHsc_fV3jtHcIVISoow48CsxUZIdr93mX4CbBdpt4zzh46lLOhmCdAqSX7ezlGv0DldijxkFI1kL-O8Y-j9z7vSaKB7QXaA3G8IEIodb3o3spGqh4XLMui7I0Jt2m_8FRYxMtyS-KamPXUqzIR47UgCjJNPAMs-S74jdrMeySDoJ8bySDPsvd9C7o9XjU0jF4Y5vaZ1DO1TiYw-cF1KCVmD-Ag0mnDvePKI-X23pHzC0oOtIxMLu3aoDJQ7urvSA3BZxiAvHqJtAohS_gnUjoygIHKUmf_DOLAlms6lVeqFxeTv5ADp3mXRwEsoZxxu5Z4ldk2uIR7GaEBo-lH8QxHu7OmVLwM3LpPLVrNGVuNhLue1UYz-xsS2DJAZPKv3Od6Pn7BR1pVpINUAieVOFqcOMhSzO-Vp5xN1thG9v4xhda7uvS8qGvNP3V-aX12SLjaNt6r7LjirU465_1zBs0itAoMFMxEeGsrgwO_zLtSj6F0tZaiFZ8jV4V9Hw5ca7AqgabVLtmL1k6You94iSbauElCF4-rFsCG4mT7VXx5ccBFpfFcTyo0oAG9qaptjX7Z92lRYdv5PlTXe87aNGqAb-_Y2VFCKnWSARPXa2frNNJRDcTt92VFEsxNxkv6KOUe31kqHSccY8sdftjNOhucWLZrqXLhlZNu9Hu_TgaRmbYDLgvGvpr2Z_1whjPxO0N_sf8AxmK9TFvXl__5Ucmv3_RAcPrrj6vAD1TtMQp1JbGh3DrugNJ_JEJ7d-tYQkhHr-69f29aIUt6fegOkO8mpv4Cdx2ODjFtWtBXRYPiK4qExYBTEDphho7_p54O-L1zSUqJUctrz5DUcHiP4p46Cp81INLrXzC6UCGYR8x7MpXoMUVbXA_Uu5I6CVENEIsFVGC7s8K53KqVd4BX7D0JYgOJ_lQJM8T-O75KCX_jNO9aWpndVCzWKmdj8dbKPhed0N0ArdFy2KpJZ-DgUkGWI4u3DISQH98Oi1ujJJgahzIV56QJ3hukup3Fr3XbwZciehMgXi2F6ddFoXaJSCo9UxkNibafwT2mz3tZpE5lpl0b22n3LEC_F0N80U61hobre5jJnRDeuOJo33bvxPuEp819pyZCaZbZJ3_goJ6CgQaVV6qQrdEiJd4-30fSi5XrvOjmTAuGr473A0NWsVJy_jQta4w5JPkZVxpAfzbYHubYTLSteoSXJVlcvEjnlhlyjw0Q7i7lBXHlDKVx5fK1ROzdMfLLQ9H0wYZZTIGMfpMxoNI_01emm8fsBExhZhD4n1YqNTSxcVYGKYHKaN1r8gHqB8lPmXcz1xP54_ksw628VtdFNcgVJMlARwha9mLEVrO2Q4E2keyVhaI21ZXEs1BcR_3HV1K6wfjPybto8YbaA_5KNrQD2YvB-conr5J5bN4oDJ3q-nU8P7NIVaIn4BV0qNyIGoU3qKIpMSc6vuHybf4PRsztdCMWfP4hWt03JYRfiFei5P8RADqMYwLNN0oi9iT_Lb9y3ih1pvFqouoEr3TTg1Vt7L0FPNvnCGiqDticDqNX9f7fZt3UNSsgjGX9tcAp-oJBpm51kg1jwQVlJ9QAINliFKwfJZ7EUJe6vP1ZAjAe3stnoHAN2W_-8KzR4DXdncydZPiLESVnZXV6ZDk2p2jZsRFnybYAKlQQF_hU6If5nWLnSnVHqibbLudI7tz0X2ghOLNw6U2DtpL_UFjAoTldodIK5t2yaUJqNF9L2awMKNdyhM7zA1uCLPJHZSo327vLXRau7FaM_8ZEqwL1Akvuj77L7IVSBWSz0rzk97GQyRwvI_wjLQnTXLUiAH-HwPcOjjPFTjMWJzS86VuB_86L3h-AO3oVKILTGo-2CtZ5RtH8jknGTeGSMIks7BOptr7O3hPDiMVe9B0AK1EJMdKTehiRXg6Fc1v4177ie3n4vsbUxfU8ZuT_0cpmEcQk08mUuw6oa8nMsoLvWac09K4gxznCekR1GgeGeSuS7iMyMcqA9vhJtgMX7NeHFWzS10A0iKHgX0yI5M1_Sk6NThvU_Rl1ep9E6vPqg91A9VmdqvCyrNUSGTb_5GlytY_JHLy-SRrsLel560zOyHkYewjSX8NUPs6IzxjcWRXvEk_CWHfOnnJnxbdTGpFK9mUczgsaiWahuOrUK2hfwsDDF3N_ZFJP8pYSY92H6TRZ7yp5Gj9DQdVukCnS-uJuUtj_tE-cYPq_zEmC7wwtadA4XAVgSY1RlgnILPp230vdt-xK1fAiptjFjKDel_638OWX2Xglc70e-WY849TlaGA-vW8F5jA1FX-hP_aedGC3t-3Llstuh00gszocfzqQivCmCCYoSz_MBsRYZN1eh7XoV3bwxpnCbNV1-uv6EH_ba-jIrarMYqCe-w_VbpUpi1OZeXTiYj6QqbYdX7eIX6vMbbVkF7KIrKdfqIXRwW98p7bQYdLpaCjiD9tBeVhoab6hykovTB3WXty2PD_Bg6pIrj-BdRgoqXguXJ8lz4yTo-dMS8S35MI0smXNVldR3o_24avvlgnPd2VproYYgbcNtU69NaIywsGhhkaQn7AaQaIValV9eRx0IScCPk-Gp7zQqMcaPe-_KOUbtw5oonTWJTrj9Mx7zkOwhm4f0i0pbiefGVoALkGr4EOj_VulUM8x1clbSiOc9VUwOUrYfoCqWgHrSN4hKKBl4MRODmWzyESJHG-Eb1TOEUL9xQPufOeQi-lsWbuJUrOlIjOsgzYCyRe37AgY40NbhmLHzTqJfNHagdX8o2UYJ6OMx6HvEuDYMfL9MJU3wXevGXX7wQbK9ZsF79Q18P_KSh5IPfrO5CxuKYbV9FygqOh6aU7iXj-rKTwrqTo2Zzm6cTw_qqntbdchaA8n_gZQlWnmDzZWSDlg6gn2SqR_zg48l6kBbusJyVqcdGdHOY-w3wuHt3E5dBVrnzxHYS8CYkkFQBMHK7HI3dUo2_HEDxwoZj4svG4wpRp5kNpZ2Y1sCW9PG90ihvNjDMzr8rLl2UUSqNfkGYfWQWnPyhsFrH0XaYe6Sv14CU3DmZMl0Y7BxdcsLavSQ_P3HvXMC0U6aI-ZZfK2hkJXIg288UfQc0lvYbxw16kgdmHJv7LljV8ISDWGfPmHvduQPb1TOhg5vaRNFdS6Q_mWZl-eTCP6BUt-LA9MIjSNfaQSS-ero0nhntcx1DG1zvvW_BA6fOLkOuoF48EZzfHrqbetR8Mp1lQX3bqyzMvxZgAcTqBBUlhqbR-u2a3e8cgSeoibK4pgYnkNElVhvXQCHYHwjXL24vQfskOzX1gYZV7HmXMHTM46lgw9NzgbRHpVvyoedAuSzDsmgZwL6y0JOtXW0-rNMKNelkThxokCVpxRxwo1TryzqosqWMORcIfisN6OJWnW9KsXBV4VQ3fLf_QbMWpbZ1p4IMzCSbmV3IGwXeWcfwio09tUCMEIpLJzlI1S9QXOZJj2goKZiwX023n0PwJn0I41tbcdWD4uT4twBWxWiX8ranyJUiDFC0YMHJQSXP06u40MHLH3zfYNZiDO70514SKes_KKO4dh0JWB2jqrM5P-6ouKsz8Os1oaD3LS7WRkOBIVk-EHsbmr6cjNvZGKADb56lfHNs5ksLIbcmUXZSo-Cw5fxVuvG26tGHKO7skau8Ln9UuSJ1gbozSkQL-AbElCatMDd5cA3-fB1PPrNHMrDX3hn2Xa64fBseCAuOSgwsyjtISQL05CDyJcgJVmdxyQ9H7wj452CVaPFHALrYs2JtmG7oMcmQvOjuv3kAaaSuTIWaxiiYGJ_ux5JEVmj9_V4FxKRPeMG3GASjXSOtPEuzd7R0d6hXsCttDzqYwSB70bZy27owO6_b3UPLQoWHHLRcjEL5z7bkf51tIo3XRVQPnYfeNXnTrqtdTNLhwLKAojRCCNlafrihmEvKHItmdKOB9nq2n0LC_aROk0bNpZREE9y7rMSenfl9N8z59yMLeuUtDv0FSEcqthvbGkB2INNXVJB_sYuVhwX7DkcwUBJs0CmaxjEtzHyDx7AEymqoeR7L6uyF12shwIx_fwRf5gjCg8j2BDW1qszgRblr4u_e05GT-GdPGHnef_d1LbaFaZYO2sF_v9PXjz94ndUfgIye1mTxCr1rrBlbO1B8W4OiEBneZVnmOQ_22CG8-oF4KFaRiF_7nRx6Tf8ala8i9fUCElq-SzrU3b9p8cLt6e79svZFvFc3YlkyFMHyjffTm5T_IduhaRBeP_YGIdFM1ZuNgOfTv7SXckt9PjeHMagKrjjqRBKVK4EYlBo47IDv5KghKeDj3V3GCP0IBBdkIL8kAH2SJAJraRUwRLbpDyIRYpH8v9KEDyho3jAwd1ILy1oeEBVEwofAPlnmFhkhXeZOU9tf1c2-h0Vr7_x3QV1tZwZvJOcoLBOSDGHqk1mBZUCmW43Woo5lViQLc8zaI_l6YVHLhCw3rEm092xRthus50KfuMB_i5AQgnOLHPY7tm0n0qZVnnDimm12i8aN3XHa2iAJ49aIhKoh8U23FZ5KWc3p_qywa06608udaDwLhFnwNZPNQ-Emn8R_Olopx6k_ds_-PhfNHUUxbXJKpBuYpAmLrbUUTC2CMFAkFlHPkxhTQWSIM3mIEbgmdUHtB203K-DLzZZ06uPxPshHH5o7rwDbrvPDJmgxz-AM20Tmqq4NlABzKkLdRRq9PpSXKzEGxRdmqgtNbPOK3CUTGwkh6cf60XBDaEsXd5YPcF90lVRI8hwHUoET1kC9ZetIrFt4V3LOqpfSjulBbgTtEawaKGL1lKZG2sm9mfH1ksTSaiogIZWyZfJnOMhVuIOiAmqWfoDIG6_cc9HxJYCM9rm2a5MZzskGmYSF_I6dDAPuDxd15Bn2As7uk4Cp-_YLRSY6nPFVlKbQp52bmR3V6J3rk4szndtrTgO9X61BN07pbgutXRv8fRh1mo9YgMynR3R4xjEQNVo5cyYm__b7GpgSCbNtsNcLrUjdtZI1o5wHxs2xHePVVJtpFffrjjxBuHcgBiHOGOgczhGQRQ2J8VYfP_SXi_looJH0tpGp6J-Xr3j-dXURgdSkRhVKZ6cT12NlCnPnyLh95BmpBwaO60S9h60vqcrq-6UsEejxZjLZhiU04NzVxv-PxkN1K8cqUw8gZ1n48u2aFMhVD0chz5trpOVL4u3imWs9cdH_vbVNa8VG9MCF5KMkun2OQ4n7KwteDqM5sGCivvIsVsFAqwZETW460KFUmCMKFxiVvv-MU3Irn4CjhSJP0d612jZhZedX-cpmMVOvMfbiW1hoUqMWHO6GuFT3uMVL5e6Ld5IRULXcwh6U0XsWWDsQ1zndglfa8YfuWMesd-MWOeA2Gy9-y18378oXFvSygNMy3GyExpvlHmghfZ7YtYK-8S6OI10vw80XAuH-jJ0JJF-Fgc36m3Lp7RToB505Rl9GVa8FLWD75ilO47cm4H30yR0bRPcNdW19ooNULi4xoJsBv9c-Suzk0R-J9sSGqQsjL-eVMA87opC2yv8WwiaQ0D8RH2Yazm4dd1HZhMpxzqM2xiGHVrqWSuvIbaJ9t3OFQu2Sg9xu4ch90osh96UGq0ec7s0BNbYbk0mxeuNHEgAzEEmtbfxhjf0jvdpf1dYG8WEQFjxmzkoWfpho2XIjSidITSBvdWrG5LQuposrLYDbD9lqVFzdWGyt4GIeu2untI3fzjU3y7RWCkk30_kpDnt9TprTijEgTWV3ZXdgRfgf-fsFqfecQJi0aHUL7HgqVLVw8IL-evHsNC_D6aLmJ3Fxp5Xe-WPnSuH6hvbGihZA_HDjhoM1swd47hhF4O2ykp91BnidJ-k8zCu0cC4ywgF5kQKIauqJiIqJM1kibAwpZWg2Nut1cAwknLPt3MZWxR18_qtnv086blNDVVpcB5w2TXRr-QvW1it9EA6TBTgrk7Hcgth6yYV-qVEegjDWKD5bG9IhJ1n1yNI90wvc2H5M_Jv5runYlsOULv1SPk4yWY0TnWTAN0C8btd9bs3PtS4qo1dttct_O64a3OCCQAMHVWJcm97Fc1vdE0wU2qtKRTsH2EJZfTpdRGx2Tn-TOzBMCe-jaTpkQrD6uJQo--yWDmgeRb4LF8pNRGABSkjBli2_x-T8_flQ6ky6nHE00YKrQUpuSJ0t0S3t83OuVfVm5jKWgHMzzLnZ2Twqqjf6YsaJ7JLveVBnOXfk9ek_hiQ-VlSodSKIr-AIjLXUgJZeePnCEMoGJUPCDjWJkREIy37dHKam1Jp_C9YLnyIBSPyUiSAByexLt8rvw4Fq37oiYitt1CodExo9CktVVNcV3sgeZsYpA__gdAOPLsHKanyV3FE1H6l6p0XTzUPLFcupSf4-CuwbrjQH3dmXrc2_q0KxmhlPWTkpwFo79RPrB2dTbBNxZ6oBe__ZFTCn3dXcO6ukQt5M3CF2t9Z_UDY4MuPdXs6iuiAbiJwKSHaBK0N1EhGUCDhs_FdaHYqFxHNprooaa9eB2SRcvX-IZbWUi0RR0uineohgpPnEh8XIltWBpveUl9i29dKePWyYTeE2ll7R9VCuIBZ_MzxyjPC1mO1MXczjwWLF9eaZnL1DijdJA7lhxEQp3bIBcuzZ0Jj0LcqPsOQbjh61HnVgFT2yyxHy_1KtwF5iGjEmW9GSogDHTeZT1repBfBT1DDcPjgGu7pBdTDx09bKyyD8kD7ISLEelIRAuVUY2bTXfITu4fM_lbBNWiUh9rgG1j9nyOF6K0m_hrqIodgS05d4bqaeLBXAhPS2ioosdIX3qjzhzAjWcaU5Z9sPv4HM9Vm4-Z5radb5BgJsqMmTXsOFXYLFI_p0l0j1d6FtALU3kQ0FOtP8aX40411tDL_CYnQg_2r7qGiUCWmC-oqZflS1ilUcyxJdFieFsvmH4wzeqnhWTrRvMad-C9MvLi47OuuKE7M4wba6PsSgEw-D1cHGH8_rxw3vVpa099fl2rG11u3dWWp9i8dAsthHd7IzAlr7HLNi6_v0OCNrDpEvOtDsmWMK_YAiU04fXcm2QBQgSGgKnQcZqZSzH8MUG2XjSGpZLyCCpZ8bkCRwBcKLchbf2n2ASAbW8luNidPcec_MiDntYI8rfSGvXZZ4qTNTKetegm1Dxs1Xd2b9mmeMd8hYiPMJ6CoOo4Hz8tIJcRI7BXBdWXtW0HlDilPWvxS6YXr0jbAONcBug2QDfqsg9A6j90F9taB2VQvsPNVYl5-Y8weF1f8__y1FHLo4C4GRztmOGy8uV__C7J5KVs29kSYzz7jTk8NTKzswN5m7hRYF5sSYgKyLvWUpsbUzmSGUUve5PdToUw1bo9otTEhl_r_JFsSSvJFSJAmUyV1IccXILgUiVhsQpUNfsSru5Ac3jjj_ym-si3jl3riy8VDi3qMWc5aiTRF6hM70GRrbilZm53x8pms754f8IVFU5iuBYP-851SxRWC5sOBKIxn7UWA0C2MXH6EWK58JciGP1Bs5PQ8146RgVggKypoUJ30EieIH_bY6HpR7uNAgpWQf1ZCtr5uw=="},{"type":"function_call","name":"shell","arguments":"{\"command\":[\"powershell.exe\",\"-NoLogo\",\"-Command\",\"Get-Content README.md -Tail 40\"],\"workdir\":\"D:\\\\ai\\\\code\\\\AIClient2API\"}","call_id":"call_gDv5gaMF1SjU17TGnILze3Qd"},{"type":"function_call_output","call_id":"call_gDv5gaMF1SjU17TGnILze3Qd","output":"Exit code: 0\nWall time: 1.8 seconds\nOutput:\n --port 3000 \\\r\n --api-key my-secret-key \\\r\n --model-provider gemini-cli-oauth \\\r\n --project-id my-gcp-project \\\r\n --gemini-oauth-creds-file ./credentials.json \\\r\n --system-prompt-file ./custom-system-prompt.txt \\\r\n --system-prompt-mode overwrite \\\r\n --log-prompts file \\\r\n --prompt-log-base-name api-logs\r\n```\r\n\r\n---\r\n\r\n## 📄 Open Source License\r\n\r\nThis project operates under the [**GNU General Public License v3 (GPLv3)**](https://www.gnu.org/licenses/gpl-3.0). For complete details, please refer to the `LICENSE` file located in the root directory.\r\n\r\n## 🙏 Acknowledgements\r\n\r\nThe development of this project was significantly inspired by the official Google Gemini CLI and incorporated some code implementations from Cline 3.18.0's `gemini-cli.ts`. We extend our sincere gratitude to the official Google team and the Cline development team for their exceptional work!\r\n\r\n## 🌟 Star History\r\n\r\n[![Star History Chart](https://api.star-history.com/svg?repos=justlovemaki/AIClient-2-API&type=Timeline)](https://www.star-history.com/#justlovemaki/AIClient-2-API&Timeline)\r\n\r\n---\r\n\r\n## ⚠️ Disclaimer\r\n\r\n### Usage Risk Warning\r\nThis project (AIClient-2-API) is for learning and research purposes only. Users assume all risks when using this project. The author is not responsible for any direct, indirect, or consequential losses resulting from the use of this project.\r\n\r\n### Third-Party Service Responsibility Statement\r\nThis project is an API proxy tool and does not provide any AI model services. All AI model services are provided by their respective third-party providers (such as Google, OpenAI, Anthropic, etc.). Users should comply with the terms of service and policies of each third-party service when accessing them through this project. The author is not responsible for the availability, quality, security, or legality of third-party services.\r\n\r\n### Data Privacy Statement\r\nThis project runs locally and does not collect or upload any user data. However, users should protect their API keys and other sensitive information when using this project. It is recommended that users regularly check and update their API keys and avoid using this project in insecure network environments.\r\n\r\n### Legal Compliance Reminder\r\nUsers should comply with the laws and regulations of their country/region when using this project. It is strictly prohibited to use this project for any illegal purposes. Any consequences resulting from users' violation of laws and regulations shall be borne by the users themselves.\r\n"}],"tools":[{"type":"function","name":"shell","description":"Runs a shell command and returns its output.","strict":false,"parameters":{"type":"object","properties":{"command":{"type":"array","items":{"type":"string"},"description":"The command to execute"},"justification":{"type":"string","description":"Only set if with_escalated_permissions is true. 1-sentence explanation of why we want to run this command."},"timeout_ms":{"type":"number","description":"The timeout for the command in milliseconds"},"with_escalated_permissions":{"type":"boolean","description":"Whether to request escalated permissions. Set to true if command needs to be run without sandbox restrictions"},"workdir":{"type":"string","description":"The working directory to execute the command in"}},"required":["command"],"additionalProperties":false}},{"type":"function","name":"list_mcp_resources","description":"Lists resources provided by MCP servers. Resources allow servers to share data that provides context to language models, such as files, database schemas, or application-specific information. Prefer resources over web search when possible.","strict":false,"parameters":{"type":"object","properties":{"cursor":{"type":"string","description":"Opaque cursor returned by a previous list_mcp_resources call for the same server."},"server":{"type":"string","description":"Optional MCP server name. When omitted, lists resources from every configured server."}},"additionalProperties":false}},{"type":"function","name":"list_mcp_resource_templates","description":"Lists resource templates provided by MCP servers. Parameterized resource templates allow servers to share data that takes parameters and provides context to language models, such as files, database schemas, or application-specific information. Prefer resource templates over web search when possible.","strict":false,"parameters":{"type":"object","properties":{"cursor":{"type":"string","description":"Opaque cursor returned by a previous list_mcp_resource_templates call for the same server."},"server":{"type":"string","description":"Optional MCP server name. When omitted, lists resource templates from all configured servers."}},"additionalProperties":false}},{"type":"function","name":"read_mcp_resource","description":"Read a specific resource from an MCP server given the server name and resource URI.","strict":false,"parameters":{"type":"object","properties":{"server":{"type":"string","description":"MCP server name exactly as configured. Must match the 'server' field returned by list_mcp_resources."},"uri":{"type":"string","description":"Resource URI to read. Must be one of the URIs returned by list_mcp_resources."}},"required":["server","uri"],"additionalProperties":false}},{"type":"function","name":"update_plan","description":"Updates the task plan.\nProvide an optional explanation and a list of plan items, each with a step and status.\nAt most one step can be in_progress at a time.\n","strict":false,"parameters":{"type":"object","properties":{"explanation":{"type":"string"},"plan":{"type":"array","items":{"type":"object","properties":{"status":{"type":"string","description":"One of: pending, in_progress, completed"},"step":{"type":"string"}},"required":["step","status"],"additionalProperties":false},"description":"The list of steps"}},"required":["plan"],"additionalProperties":false}},{"type":"custom","name":"apply_patch","description":"Use the `apply_patch` tool to edit files. This is a FREEFORM tool, so do not wrap the patch in JSON.","format":{"type":"grammar","syntax":"lark","definition":"start: begin_patch hunk+ end_patch\r\nbegin_patch: \"*** Begin Patch\" LF\r\nend_patch: \"*** End Patch\" LF?\r\n\r\nhunk: add_hunk | delete_hunk | update_hunk\r\nadd_hunk: \"*** Add File: \" filename LF add_line+\r\ndelete_hunk: \"*** Delete File: \" filename LF\r\nupdate_hunk: \"*** Update File: \" filename LF change_move? change?\r\n\r\nfilename: /(.+)/\r\nadd_line: \"+\" /(.*)/ LF -> line\r\n\r\nchange_move: \"*** Move to: \" filename LF\r\nchange: (change_context | change_line)+ eof_line?\r\nchange_context: (\"@@\" | \"@@ \" /(.+)/) LF\r\nchange_line: (\"+\" | \"-\" | \" \") /(.*)/ LF\r\neof_line: \"*** End of File\" LF\r\n\r\n%import common.LF\r\n"}},{"type":"function","name":"view_image","description":"Attach a local image (by filesystem path) to the conversation context for this turn.","strict":false,"parameters":{"type":"object","properties":{"path":{"type":"string","description":"Local filesystem path to an image file"}},"required":["path"],"additionalProperties":false}}],"tool_choice":"auto","parallel_tool_calls":false,"reasoning":{"effort":"high","summary":"auto"},"store":false,"stream":true,"include":["reasoning.encrypted_content"],"prompt_cache_key":"019a5d3b-1230-76f1-8560-0f402948f38a"} \ No newline at end of file diff --git a/src/gemini/gemini-core.js b/src/gemini/gemini-core.js index 1f111ed..e748064 100644 --- a/src/gemini/gemini-core.js +++ b/src/gemini/gemini-core.js @@ -4,7 +4,7 @@ import { promises as fs } from 'fs'; import * as path from 'path'; import * as os from 'os'; import * as readline from 'readline'; -import { API_ACTIONS, ensureRolesInContents, formatExpiryTime } from '../common.js'; +import { API_ACTIONS, formatExpiryTime } from '../common.js'; // --- Constants --- const AUTH_REDIRECT_PORT = 8085; @@ -18,7 +18,7 @@ const GEMINI_MODELS = ['gemini-2.5-flash', 'gemini-2.5-flash-lite', 'gemini-2.5- const ANTI_TRUNCATION_MODELS = GEMINI_MODELS.map(model => `anti-${model}`); function is_anti_truncation_model(model) { - return ANTI_TRUNCATION_MODELS.some(antiModel => model.includes(antiModel) || antiModel.includes(model)); + return ANTI_TRUNCATION_MODELS.some(antiModel => model.includes(antiModel)); } // 从防截断模型名中提取实际模型名 @@ -41,6 +41,66 @@ function toGeminiApiResponse(codeAssistResponse) { return compliantResponse; } +/** + * Ensures that all content parts in a request body have a 'role' property. + * If 'systemInstruction' is present and lacks a role, it defaults to 'user'. + * If any 'contents' entry lacks a role, it defaults to 'user'. + * @param {Object} requestBody - The request body object. + * @returns {Object} The modified request body with roles ensured. + */ +function ensureRolesInContents(requestBody) { + delete requestBody.model; + // delete requestBody.system_instruction; + // delete requestBody.systemInstruction; + if (requestBody.system_instruction) { + requestBody.systemInstruction = requestBody.system_instruction; + delete requestBody.system_instruction; + } + + if (requestBody.systemInstruction && !requestBody.systemInstruction.role) { + requestBody.systemInstruction.role = 'user'; + } + + if (requestBody.contents && Array.isArray(requestBody.contents)) { + requestBody.contents.forEach(content => { + if (!content.role) { + content.role = 'user'; + } + }); + + // 如果存在 systemInstruction,将其放在 contents 索引 0 的位置 + // if (requestBody.systemInstruction) { + // // 检查 contents[0] 是否与 systemInstruction 内容相同 + // const firstContent = requestBody.contents[0]; + // let isSame = false; + + // if (firstContent && firstContent.parts && requestBody.systemInstruction.parts) { + // // 比较 parts 数组的内容 + // const firstContentText = firstContent.parts + // .filter(p => p?.text) + // .map(p => p.text) + // .join('\n'); + // const systemInstructionText = requestBody.systemInstruction.parts + // .filter(p => p?.text) + // .map(p => p.text) + // .join('\n'); + + // isSame = firstContentText === systemInstructionText; + // } + + // // 如果内容不同,则将 systemInstruction 插入到索引 0 的位置 + // if (!isSame) { + // requestBody.contents.unshift({ + // role: requestBody.systemInstruction.role || 'user', + // parts: requestBody.systemInstruction.parts + // }); + // } + // delete requestBody.systemInstruction; + // } + } + return requestBody; +} + async function* apply_anti_truncation_to_stream(service, model, requestBody) { let currentRequest = { ...requestBody }; let allGeneratedText = ''; @@ -166,12 +226,14 @@ export class GeminiApiService { const credentials = JSON.parse(data); this.authClient.setCredentials(credentials); console.log('[Gemini Auth] Authentication configured successfully from file.'); + if (forceRefresh) { console.log('[Gemini Auth] Forcing token refresh...'); const { credentials: newCredentials } = await this.authClient.refreshAccessToken(); this.authClient.setCredentials(newCredentials); + // Save refreshed credentials back to file await fs.writeFile(credPath, JSON.stringify(newCredentials, null, 2)); - console.log('[Gemini Auth] Token refresh response: ok'); + console.log('[Gemini Auth] Token refreshed and saved successfully.'); } } catch (error) { console.error('[Gemini Auth] Error initializing authentication:', error.code); @@ -252,7 +314,7 @@ export class GeminiApiService { this.availableModels = GEMINI_MODELS; console.log(`[Gemini] Using fixed models: [${this.availableModels.join(', ')}]`); try { - const initialProjectId = "default" + const initialProjectId = "" // Prepare client metadata const clientMetadata = { ideType: "IDE_UNSPECIFIED", @@ -261,18 +323,47 @@ export class GeminiApiService { duetProject: initialProjectId, } - const loadResponse = await this.callApi('loadCodeAssist', { metadata: clientMetadata }); + // Call loadCodeAssist to discover the actual project ID + const loadRequest = { + cloudaicompanionProject: initialProjectId, + metadata: clientMetadata, + } + + const loadResponse = await this.callApi('loadCodeAssist', loadRequest); + + // Check if we already have a project ID from the response if (loadResponse.cloudaicompanionProject) { return loadResponse.cloudaicompanionProject; } + + // If no existing project, we need to onboard const defaultTier = loadResponse.allowedTiers?.find(tier => tier.isDefault); - const onboardRequest = { tierId: defaultTier?.id || 'free-tier', metadata: clientMetadata , cloudaicompanionProject: initialProjectId,}; - let lro = await this.callApi('onboardUser', onboardRequest); - while (!lro.done) { + const tierId = defaultTier?.id || 'free-tier'; + + const onboardRequest = { + tierId: tierId, + cloudaicompanionProject: initialProjectId, + metadata: clientMetadata, + }; + + let lroResponse = await this.callApi('onboardUser', onboardRequest); + + // Poll until operation is complete with timeout protection + const MAX_RETRIES = 30; // Maximum number of retries (60 seconds total) + let retryCount = 0; + + while (!lroResponse.done && retryCount < MAX_RETRIES) { await new Promise(resolve => setTimeout(resolve, 2000)); - lro = await this.callApi('onboardUser', onboardRequest); + lroResponse = await this.callApi('onboardUser', onboardRequest); + retryCount++; } - return lro.response?.cloudaicompanionProject?.id; + + if (!lroResponse.done) { + throw new Error('Onboarding timeout: Operation did not complete within expected time.'); + } + + const discoveredProjectId = lroResponse.response?.cloudaicompanionProject?.id || initialProjectId; + return discoveredProjectId; } catch (error) { console.error('[Gemini] Failed to discover Project ID:', error.response?.data || error.message); throw new Error('Could not discover a valid Google Cloud Project ID.'); @@ -294,8 +385,8 @@ export class GeminiApiService { } async callApi(method, body, isRetry = false, retryCount = 0) { - const maxRetries = this.config.REQUEST_MAX_RETRIES; - const baseDelay = this.config.REQUEST_BASE_DELAY; // 1 second base delay + const maxRetries = this.config.REQUEST_MAX_RETRIES || 3; + const baseDelay = this.config.REQUEST_BASE_DELAY || 1000; // 1 second base delay try { const requestOptions = { @@ -308,8 +399,11 @@ export class GeminiApiService { const res = await this.authClient.request(requestOptions); return res.data; } catch (error) { + console.error(`[API] Error calling ${method}:`, error.response?.status, error.message); + + // Handle 401 (Unauthorized) - refresh auth and retry once if ((error.response?.status === 400 || error.response?.status === 401) && !isRetry) { - console.log('[API] Received 401. Refreshing auth and retrying...'); + console.log('[API] Received 401/400. Refreshing auth and retrying...'); await this.initializeAuth(true); return this.callApi(method, body, true, retryCount); } @@ -335,8 +429,8 @@ export class GeminiApiService { } async * streamApi(method, body, isRetry = false, retryCount = 0) { - const maxRetries = this.config.REQUEST_MAX_RETRIES; - const baseDelay = this.config.REQUEST_BASE_DELAY; // 1 second base delay + const maxRetries = this.config.REQUEST_MAX_RETRIES || 3; + const baseDelay = this.config.REQUEST_BASE_DELAY || 1000; // 1 second base delay try { const requestOptions = { @@ -355,8 +449,11 @@ export class GeminiApiService { } yield* this.parseSSEStream(res.data); } catch (error) { + console.error(`[API] Error during stream ${method}:`, error.response?.status, error.message); + + // Handle 401 (Unauthorized) - refresh auth and retry once if ((error.response?.status === 400 || error.response?.status === 401) && !isRetry) { - console.log('[API] Received 401 during stream. Refreshing auth and retrying...'); + console.log('[API] Received 401/400 during stream. Refreshing auth and retrying...'); await this.initializeAuth(true); yield* this.streamApi(method, body, true, retryCount); return; diff --git a/src/openai/openai-responses-core.js b/src/openai/openai-responses-core.js index ca05fab..9052acc 100644 --- a/src/openai/openai-responses-core.js +++ b/src/openai/openai-responses-core.js @@ -9,7 +9,7 @@ export class OpenAIResponsesApiService { this.config = config; this.apiKey = config.OPENAI_API_KEY; this.baseUrl = config.OPENAI_BASE_URL || 'https://api.openai.com/v1'; - console.log(`[OpenAIResponsesApiService] Base URL: ${JSON.stringify(config)}`); + // console.log(`[OpenAIResponsesApiService] Base URL: ${JSON.stringify(config)}`); this.axiosInstance = axios.create({ baseURL: this.baseUrl, headers: { diff --git a/src/provider-pool-manager.js b/src/provider-pool-manager.js index 8268fa6..512b423 100644 --- a/src/provider-pool-manager.js +++ b/src/provider-pool-manager.js @@ -101,9 +101,9 @@ export class ProviderPoolManager { if (provider.config.errorCount >= this.maxErrorCount) { provider.config.isHealthy = false; - console.warn(`[ProviderPoolManager] Marked provider as unhealthy: ${JSON.stringify(providerConfig)} for type ${providerType}. Total errors: ${provider.config.errorCount}`); + console.warn(`[ProviderPoolManager] Marked provider as unhealthy: ${providerConfig.uuid} for type ${providerType}. Total errors: ${provider.config.errorCount}`); } else { - console.warn(`[ProviderPoolManager] Provider ${JSON.stringify(providerConfig)} for type ${providerType} error count: ${provider.config.errorCount}/${this.maxErrorCount}. Still healthy.`); + console.warn(`[ProviderPoolManager] Provider ${providerConfig.uuid} for type ${providerType} error count: ${provider.config.errorCount}/${this.maxErrorCount}. Still healthy.`); } // 优化1: 使用防抖保存 @@ -128,7 +128,7 @@ export class ProviderPoolManager { if (isInit) { provider.config.usageCount = 0; // Reset usage count on health recovery } - console.log(`[ProviderPoolManager] Marked provider as healthy: ${JSON.stringify(providerConfig)} for type ${providerType}`); + console.log(`[ProviderPoolManager] Marked provider as healthy: ${provider.config.uuid} for type ${providerType}`); // 优化1: 使用防抖保存 this._debouncedSave(providerType); @@ -150,7 +150,7 @@ export class ProviderPoolManager { // Only attempt to health check unhealthy providers after a certain interval if (!providerStatus.config.isHealthy && providerStatus.config.lastErrorTime && (now.getTime() - new Date(providerStatus.config.lastErrorTime).getTime() < this.healthCheckInterval)) { - console.log(`[ProviderPoolManager] Skipping health check for ${JSON.stringify(providerConfig)} (${providerType}). Last error too recent.`); + console.log(`[ProviderPoolManager] Skipping health check for ${providerConfig.uuid} (${providerType}). Last error too recent.`); continue; } @@ -162,20 +162,20 @@ export class ProviderPoolManager { if (!providerStatus.config.isHealthy) { // Provider was unhealthy but is now healthy this.markProviderHealthy(isInit, providerType, providerConfig); - console.log(`[ProviderPoolManager] Health check for ${JSON.stringify(providerConfig)} (${providerType}): Marked Healthy (actual check)`); + console.log(`[ProviderPoolManager] Health check for ${providerConfig.uuid} (${providerType}): Marked Healthy (actual check)`); } else { // Provider was already healthy and still is this.markProviderHealthy(isInit, providerType, providerConfig); - console.log(`[ProviderPoolManager] Health check for ${JSON.stringify(providerConfig)} (${providerType}): Still Healthy`); + console.log(`[ProviderPoolManager] Health check for ${providerConfig.uuid} (${providerType}): Still Healthy`); } } else { // Provider is not healthy - console.warn(`[ProviderPoolManager] Health check for ${JSON.stringify(providerConfig)} (${providerType}) failed: Provider is not responding correctly.`); + console.warn(`[ProviderPoolManager] Health check for ${providerConfig.uuid} (${providerType}) failed: Provider is not responding correctly.`); this.markProviderUnhealthy(providerType, providerConfig); } } catch (error) { - console.error(`[ProviderPoolManager] Health check for ${JSON.stringify(providerConfig)} (${providerType}) failed: ${error.message}`); + console.error(`[ProviderPoolManager] Health check for ${providerConfig.uuid} (${providerType}) failed: ${error.message}`); // If a health check fails, mark it unhealthy, which will update error count and lastErrorTime this.markProviderUnhealthy(providerType, providerConfig); }