From ad2432a37c0185ce785df4c6a3b7c56a0398409c Mon Sep 17 00:00:00 2001 From: hex2077 Date: Thu, 22 Jan 2026 21:36:05 +0800 Subject: [PATCH] =?UTF-8?q?feat(converter):=20=E5=AE=8C=E5=96=84Claude?= =?UTF-8?q?=E5=92=8CGemini=E8=BD=AC=E6=8D=A2=E5=99=A8=E5=AF=B9thinking?= =?UTF-8?q?=E5=9D=97=E5=92=8C=E5=B7=A5=E5=85=B7=E8=B0=83=E7=94=A8=E7=9A=84?= =?UTF-8?q?=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在ClaudeConverter中添加thinking块和redacted_thinking块的处理逻辑 - 改进tool_use和tool_result块的转换,支持ID映射和参数规范化 - 在GeminiConverter中实现thinking块与Claude格式的互转 - 添加工具参数重映射逻辑,解决Gemini与Claude参数命名差异问题 - 支持流式场景下的thinking_delta和signature_delta事件处理 --- README-JA.md | 18 + README-ZH.md | 18 + README.md | 18 + src/converters/strategies/ClaudeConverter.js | 133 ++++++- src/converters/strategies/GeminiConverter.js | 362 +++++++++++++++++-- 5 files changed, 508 insertions(+), 41 deletions(-) diff --git a/README-JA.md b/README-JA.md index 4b892d4..a1e7de0 100644 --- a/README-JA.md +++ b/README-JA.md @@ -34,6 +34,8 @@ >
> クリックして詳細なバージョン履歴を展開 > +> - **2026.01.22** - Orchidsプロトコルサポートの追加、Clerk OAuth認証でClaude Sonnet 4.5、Claude Opus 4.5、Gemini 3 Flash、GPT-5.2などのモデルにアクセス可能、WebSocketストリーミングとツール呼び出しをサポート +> - **2026.01.15** - プロバイダープールマネージャーの最適化:非同期リフレッシュキューメカニズム、バッファキュー重複排除、グローバル並行制御、ノードウォームアップと自動期限切れ検出を追加 > - **2026.01.07** - iFlowプロトコルサポートの追加、OAuth認証方式でQwen、Kimi、DeepSeek、GLMシリーズモデルにアクセス可能、自動トークンリフレッシュ機能をサポート > - **2026.01.03** - テーマ切替機能を追加し、プロバイダープール初期化を最適化、プロバイダーのデフォルト設定を使用するフォールバック戦略を削除 > - **2025.12.30** - メインプロセス管理と自動更新機能を追加 @@ -247,6 +249,20 @@ Web UI管理インターフェースでは、極めて迅速に認証設定を 3. **ベストプラクティス**:**Claude Code**との併用を推奨、最適な体験を得られる 4. **重要なお知らせ**:Kiroサービス使用ポリシーが更新されました、最新の使用制限と条件については公式ウェブサイトをご確認ください。 +#### Orchids OAuth設定 +1. **プラットフォームにログイン**:[Orchidsプラットフォーム](https://orchids.app)にアクセスしてアカウントにログイン +2. **認証情報を取得**:ブラウザの開発者ツール(F12)を開き、Application > Cookies > https://orchids.app に移動 +3. **トークンをコピー**:`__client` を見つけてその値(長いJWT文字列)をコピー +4. **認証情報をインポート**:Web UIの「トークンをインポート」機能を使用して値を貼り付け +5. **サポートモデル**:Claude Sonnet 4.5、Claude Opus 4.5、Gemini 3 Flash、GPT-5.2など + +#### iFlow OAuth設定 +1. **初回認証**:Web UIの「設定管理」または「プロバイダープール」ページで、iFlowの「認証生成」ボタンをクリック +2. **電話番号ログイン**:システムがiFlow認証ページを開き、電話番号でログイン認証を完了 +3. **自動保存**:認証成功後、システムは自動的にAPI Keyを取得し認証情報を保存 +4. **サポートモデル**:Qwen3シリーズ、Kimi K2、DeepSeek V3/R1、GLM-4.6/4.7など +5. **自動リフレッシュ**:システムはトークンの期限切れが近づくと自動的にリフレッシュ、手動介入不要 + #### アカウントプール管理設定 1. **プール設定ファイルの作成**:[provider_pools.json.example](./configs/provider_pools.json.example) を参考に設定ファイルを作成します 2. **プールパラメータの設定**:config.json で `PROVIDER_POOLS_FILE_PATH` を設定し、プール設定ファイルを指定します @@ -268,6 +284,8 @@ Web UI管理インターフェースでは、極めて迅速に認証設定を | **Kiro** | `~/.aws/sso/cache/kiro-auth-token.json` | Kiro認証トークン | | **Qwen** | `~/.qwen/oauth_creds.json` | Qwen OAuth認証情報 | | **Antigravity** | `~/.antigravity/oauth_creds.json` | Antigravity OAuth認証情報 (Claude 4.5 Opus サポート) | +| **Orchids** | `configs/orchids/{timestamp}_orchids_creds.json` | Orchids Clerk JWT認証情報 (Claude 4.5、GPT-5.2 サポート) | +| **iFlow** | `~/.iflow/oauth_creds.json` | iFlow OAuth認証情報 (Qwen、Kimi、DeepSeek、GLM サポート) | > **説明**:`~`はユーザーホームディレクトリを表します(Windows: `C:\Users\ユーザー名`、Linux/macOS: `/home/ユーザー名`または`/Users/ユーザー名`) diff --git a/README-ZH.md b/README-ZH.md index d088962..2491b4d 100644 --- a/README-ZH.md +++ b/README-ZH.md @@ -34,6 +34,8 @@ >
> 点击展开查看详细版本历史 > +> - **2026.01.22** - 新增 Orchids 协议支持,通过 Clerk OAuth 认证访问 Claude Sonnet 4.5、Claude Opus 4.5、Gemini 3 Flash、GPT-5.2 等模型,支持 WebSocket 流式传输和工具调用 +> - **2026.01.15** - 优化提供商池管理器:新增异步刷新队列机制、缓冲队列去重、全局并发控制,支持节点预热和自动过期检测 > - **2026.01.07** - 新增 iFlow 协议支持,通过 OAuth 认证方式访问 Qwen、Kimi、DeepSeek 和 GLM 系列模型,支持自动 token 刷新功能 > - **2026.01.03** - 新增主题切换功能并优化提供商池初始化,移除使用提供商默认配置的降级策略 > - **2025.12.30** - 添加主进程管理和自动更新功能 @@ -246,6 +248,20 @@ docker compose up -d 3. **最佳实践**:推荐配合 **Claude Code** 使用,可获得最优体验 4. **重要提示**:Kiro 服务使用政策已更新,请访问官方网站查看最新使用限制和条款 +#### Orchids OAuth 配置 +1. **登录平台**:访问 [Orchids 平台](https://orchids.app) 并登录账号 +2. **获取凭据**:打开浏览器开发者工具 (F12),切换到 Application > Cookies > https://orchids.app +3. **复制 Token**:找到 `__client` 并复制其值(一个长的 JWT 字符串) +4. **导入凭据**:在 Web UI 中使用"导入 Token"功能粘贴该值 +5. **支持模型**:Claude Sonnet 4.5、Claude Opus 4.5、Gemini 3 Flash、GPT-5.2 等 + +#### iFlow OAuth 配置 +1. **首次授权**:在 Web UI 的"配置管理"或"提供商池"页面,点击 iFlow 的"生成授权"按钮 +2. **手机登录**:系统将打开 iFlow 授权页面,使用手机号完成登录验证 +3. **自动保存**:授权成功后,系统会自动获取 API Key 并保存凭据 +4. **支持模型**:Qwen3 系列、Kimi K2、DeepSeek V3/R1、GLM-4.6/4.7 等 +5. **自动刷新**:系统会在 Token 即将过期时自动刷新,无需手动干预 + #### 账号池管理配置 1. **创建号池配置文件**:参考 [provider_pools.json.example](./configs/provider_pools.json.example) 创建配置文件 2. **配置号池参数**:在 `configs/config.json` 中设置 `PROVIDER_POOLS_FILE_PATH` 指向号池配置文件 @@ -267,6 +283,8 @@ docker compose up -d | **Kiro** | `~/.aws/sso/cache/kiro-auth-token.json` | Kiro 认证令牌 | | **Qwen** | `~/.qwen/oauth_creds.json` | Qwen OAuth 凭据 | | **Antigravity** | `~/.antigravity/oauth_creds.json` | Antigravity OAuth 凭据 (支持 Claude 4.5 Opus) | +| **Orchids** | `configs/orchids/{timestamp}_orchids_creds.json` | Orchids Clerk JWT 凭据 (支持 Claude 4.5、GPT-5.2) | +| **iFlow** | `~/.iflow/oauth_creds.json` | iFlow OAuth 凭据 (支持 Qwen、Kimi、DeepSeek、GLM) | > **说明**:`~` 表示用户主目录(Windows: `C:\Users\用户名`,Linux/macOS: `/home/用户名` 或 `/Users/用户名`) diff --git a/README.md b/README.md index bfc2e5f..ae2a838 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,8 @@ >
> Click to expand detailed version history > +> - **2026.01.22** - Added Orchids protocol support, accessing Claude Sonnet 4.5, Claude Opus 4.5, Gemini 3 Flash, GPT-5.2 and more via Clerk OAuth authentication, with WebSocket streaming and tool calling support +> - **2026.01.15** - Optimized provider pool manager: added async refresh queue mechanism, buffer queue deduplication, global concurrency control, node warmup and automatic expiry detection > - **2026.01.07** - Added iFlow protocol support, enabling access to Qwen, Kimi, DeepSeek, and GLM series models via OAuth authentication with automatic token refresh > - **2026.01.03** - Added theme switching functionality and optimized provider pool initialization, removed the fallback strategy of using provider default configuration > - **2025.12.30** - Added main process management and automatic update functionality @@ -247,6 +249,20 @@ In the Web UI management interface, you can complete authorization configuration 3. **Best Practice**: Recommended to use with **Claude Code** for optimal experience 4. **Important Notice**: Kiro service usage policy has been updated, please visit the official website for the latest usage restrictions and terms +#### Orchids OAuth Configuration +1. **Login to Platform**: Visit [Orchids Platform](https://orchids.app) and log in to your account +2. **Get Credentials**: Open browser developer tools (F12), navigate to Application > Cookies > https://orchids.app +3. **Copy Token**: Find `__client` and copy its value (a long JWT string) +4. **Import Credentials**: Use the "Import Token" function in Web UI to paste the value +5. **Supported Models**: Claude Sonnet 4.5, Claude Opus 4.5, Gemini 3 Flash, GPT-5.2, etc. + +#### iFlow OAuth Configuration +1. **First Authorization**: In Web UI's "Configuration" or "Provider Pools" page, click the "Generate Authorization" button for iFlow +2. **Phone Login**: The system will open the iFlow authorization page, complete login verification using your phone number +3. **Auto Save**: After successful authorization, the system will automatically obtain the API Key and save credentials +4. **Supported Models**: Qwen3 series, Kimi K2, DeepSeek V3/R1, GLM-4.6/4.7, etc. +5. **Auto Refresh**: The system will automatically refresh tokens when they are about to expire, no manual intervention required + #### Account Pool Management Configuration 1. **Create Pool Configuration File**: Create a configuration file referencing [provider_pools.json.example](./configs/provider_pools.json.example) 2. **Configure Pool Parameters**: Set `PROVIDER_POOLS_FILE_PATH` in `configs/config.json` to point to the pool configuration file @@ -268,6 +284,8 @@ Default storage locations for authorization credential files of each service: | **Kiro** | `~/.aws/sso/cache/kiro-auth-token.json` | Kiro authentication token | | **Qwen** | `~/.qwen/oauth_creds.json` | Qwen OAuth credentials | | **Antigravity** | `~/.antigravity/oauth_creds.json` | Antigravity OAuth credentials (supports Claude 4.5 Opus) | +| **Orchids** | `configs/orchids/{timestamp}_orchids_creds.json` | Orchids Clerk JWT credentials (supports Claude 4.5, GPT-5.2) | +| **iFlow** | `~/.iflow/oauth_creds.json` | iFlow OAuth credentials (supports Qwen, Kimi, DeepSeek, GLM) | > **Note**: `~` represents the user home directory (Windows: `C:\Users\username`, Linux/macOS: `/home/username` or `/Users/username`) diff --git a/src/converters/strategies/ClaudeConverter.js b/src/converters/strategies/ClaudeConverter.js index 08242a2..3349498 100644 --- a/src/converters/strategies/ClaudeConverter.js +++ b/src/converters/strategies/ClaudeConverter.js @@ -815,6 +815,31 @@ export class ClaudeConverter extends BaseConverter { parts.push({ text: block.text }); } break; + + // [FIX] 参考 ag/request.rs 添加 thinking 块处理 + case 'thinking': + if (typeof block.thinking === 'string' && block.thinking.length > 0) { + const thinkingPart = { + text: block.thinking, + thought: true + }; + // 如果有签名,添加 thoughtSignature + if (block.signature && block.signature.length >= 50) { + thinkingPart.thoughtSignature = block.signature; + } + parts.push(thinkingPart); + } + break; + + // [FIX] 处理 redacted_thinking 块 + case 'redacted_thinking': + // 将 redacted_thinking 转换为普通文本 + if (block.data) { + parts.push({ + text: `[Redacted Thinking: ${block.data}]` + }); + } + break; case 'tool_use': // 转换为 Gemini functionCall 格式 @@ -852,20 +877,40 @@ export class ClaudeConverter extends BaseConverter { case 'tool_result': // 转换为 Gemini functionResponse 格式 + // [FIX] 参考 ag/request.rs 的实现,正确处理 tool_use_id 到函数名的映射 const toolCallId = block.tool_use_id; if (toolCallId) { - // 从 tool_use_id 中提取函数名 - // 格式通常是 "funcName-uuid" 或直接是函数名 + // 尝试从之前的 tool_use 块中查找对应的函数名 + // 如果找不到,则从 tool_use_id 中提取 let funcName = toolCallId; - const toolCallIdParts = toolCallId.split('-'); - if (toolCallIdParts.length > 1) { - // 移除最后一个部分(UUID),保留函数名 - funcName = toolCallIdParts.slice(0, -1).join('-'); + + // 检查是否有缓存的 tool_id -> name 映射 + // 格式通常是 "funcName-uuid" 或 "toolu_xxx" + if (toolCallId.startsWith('toolu_')) { + // Claude 格式的 tool_use_id,需要从上下文中查找函数名 + // 这里我们保留原始 ID 作为 name(Gemini 会处理) + funcName = toolCallId; + } else { + const toolCallIdParts = toolCallId.split('-'); + if (toolCallIdParts.length > 1) { + // 移除最后一个部分(UUID),保留函数名 + funcName = toolCallIdParts.slice(0, -1).join('-'); + } } // 获取响应数据 let responseData = block.content; - if (typeof responseData !== 'string') { + + // [FIX] 参考 ag/request.rs 的 tool_result_compressor 逻辑 + // 处理嵌套的 content 数组(如图片等) + if (Array.isArray(responseData)) { + // 提取文本内容 + const textParts = responseData + .filter(item => item && item.type === 'text') + .map(item => item.text) + .join('\n'); + responseData = textParts || JSON.stringify(responseData); + } else if (typeof responseData !== 'string') { responseData = JSON.stringify(responseData); } @@ -1057,13 +1102,38 @@ export class ClaudeConverter extends BaseConverter { } break; + // [FIX] 参考 ag/response.rs 添加 thinking 块处理 + case 'thinking': + if (block.thinking) { + const thinkingPart = { + text: block.thinking, + thought: true + }; + // 如果有签名,添加 thoughtSignature + if (block.signature && block.signature.length >= 50) { + thinkingPart.thoughtSignature = block.signature; + } + parts.push(thinkingPart); + } + break; + case 'tool_use': - parts.push({ + // [FIX] 添加 id 和 thoughtSignature 支持 + const functionCallPart = { functionCall: { name: block.name, args: block.input || {} } - }); + }; + // 添加 id(如果存在) + if (block.id) { + functionCallPart.functionCall.id = block.id; + } + // 添加签名(如果存在) + if (block.signature && block.signature.length >= 50) { + functionCallPart.thoughtSignature = block.signature; + } + parts.push(functionCallPart); break; case 'image': @@ -1125,6 +1195,32 @@ export class ClaudeConverter extends BaseConverter { // 处理Claude流式事件 if (typeof claudeChunk === 'object' && !Array.isArray(claudeChunk)) { + // content_block_start 事件 - 处理 thinking 块开始 + if (claudeChunk.type === 'content_block_start') { + const contentBlock = claudeChunk.content_block; + if (contentBlock && contentBlock.type === 'thinking') { + // thinking 块开始,返回空(等待 delta) + return null; + } + if (contentBlock && contentBlock.type === 'tool_use') { + // tool_use 块开始 + return { + candidates: [{ + content: { + role: "model", + parts: [{ + functionCall: { + name: contentBlock.name, + args: {}, + id: contentBlock.id + } + }] + } + }] + }; + } + } + // content_block_delta 事件 if (claudeChunk.type === 'content_block_delta') { const delta = claudeChunk.delta; @@ -1143,19 +1239,33 @@ export class ClaudeConverter extends BaseConverter { }; } - // 处理 thinking_delta - 映射为文本 + // [FIX] 处理 thinking_delta - 转换为 Gemini 的 thought 格式 if (delta && delta.type === 'thinking_delta') { return { candidates: [{ content: { role: "model", parts: [{ - text: delta.thinking || "" + text: delta.thinking || "", + thought: true }] } }] }; } + + // [FIX] 处理 signature_delta + if (delta && delta.type === 'signature_delta') { + // 签名通常与前一个 thinking 块关联 + // 在流式场景中,我们可以忽略或记录 + return null; + } + + // [FIX] 处理 input_json_delta (tool arguments) + if (delta && delta.type === 'input_json_delta') { + // 工具参数增量,Gemini 不支持增量参数,忽略 + return null; + } } // message_delta 事件 - 流结束 @@ -1165,6 +1275,7 @@ export class ClaudeConverter extends BaseConverter { candidates: [{ finishReason: stopReason === 'end_turn' ? 'STOP' : stopReason === 'max_tokens' ? 'MAX_TOKENS' : + stopReason === 'tool_use' ? 'STOP' : 'OTHER' }] }; diff --git a/src/converters/strategies/GeminiConverter.js b/src/converters/strategies/GeminiConverter.js index 9364058..2ccd8ff 100644 --- a/src/converters/strategies/GeminiConverter.js +++ b/src/converters/strategies/GeminiConverter.js @@ -26,6 +26,136 @@ import { generateResponseCompleted } from '../../providers/openai/openai-responses-core.mjs'; +/** + * [FIX] 参考 ag/response.rs 和 ag/streaming.rs 的 remap_function_call_args 函数 + * 修复 Gemini 返回的工具参数名称问题 + * Gemini 有时会使用不同的参数名称,需要映射到 Claude Code 期望的格式 + */ +function remapFunctionCallArgs(toolName, args) { + if (!args || typeof args !== 'object') return args; + + const remappedArgs = { ...args }; + const toolNameLower = toolName.toLowerCase(); + + // [IMPORTANT] Claude Code CLI 的 EnterPlanMode 工具禁止携带任何参数 + if (toolName === 'EnterPlanMode') { + return {}; + } + + switch (toolNameLower) { + case 'grep': + case 'search': + case 'search_code_definitions': + case 'search_code_snippets': + // [FIX] Gemini hallucination: maps parameter description to "description" field + if (remappedArgs.description && !remappedArgs.pattern) { + remappedArgs.pattern = remappedArgs.description; + delete remappedArgs.description; + } + + // Gemini uses "query", Claude Code expects "pattern" + if (remappedArgs.query && !remappedArgs.pattern) { + remappedArgs.pattern = remappedArgs.query; + delete remappedArgs.query; + } + + // [CRITICAL FIX] Claude Code uses "path" (string), NOT "paths" (array)! + if (!remappedArgs.path) { + if (remappedArgs.paths) { + if (Array.isArray(remappedArgs.paths)) { + remappedArgs.path = remappedArgs.paths[0] || '.'; + } else if (typeof remappedArgs.paths === 'string') { + remappedArgs.path = remappedArgs.paths; + } else { + remappedArgs.path = '.'; + } + delete remappedArgs.paths; + } else { + // Default to current directory if missing + remappedArgs.path = '.'; + } + } + // Note: We keep "-n" and "output_mode" if present as they are valid in Grep schema + break; + + case 'glob': + // [FIX] Gemini hallucination: maps parameter description to "description" field + if (remappedArgs.description && !remappedArgs.pattern) { + remappedArgs.pattern = remappedArgs.description; + delete remappedArgs.description; + } + + // Gemini uses "query", Claude Code expects "pattern" + if (remappedArgs.query && !remappedArgs.pattern) { + remappedArgs.pattern = remappedArgs.query; + delete remappedArgs.query; + } + + // [CRITICAL FIX] Claude Code uses "path" (string), NOT "paths" (array)! + // [NOTE] 与 grep 不同,glob 不添加默认 path(参考 Rust 代码) + if (!remappedArgs.path) { + if (remappedArgs.paths) { + if (Array.isArray(remappedArgs.paths)) { + remappedArgs.path = remappedArgs.paths[0] || '.'; + } else if (typeof remappedArgs.paths === 'string') { + remappedArgs.path = remappedArgs.paths; + } else { + remappedArgs.path = '.'; + } + delete remappedArgs.paths; + } + // [FIX] glob 不添加默认 path,与 Rust 代码保持一致 + } + break; + + case 'read': + // Gemini might use "path" vs "file_path" + if (remappedArgs.path && !remappedArgs.file_path) { + remappedArgs.file_path = remappedArgs.path; + delete remappedArgs.path; + } + break; + + case 'ls': + // LS tool: ensure "path" parameter exists + if (!remappedArgs.path) { + remappedArgs.path = '.'; + } + break; + + default: + // [NEW] [Issue #785] Generic Property Mapping for all tools + // If a tool has "paths" (array of 1) but no "path", convert it. + // [FIX] 与 Rust 代码保持一致:只在 paths.length === 1 时转换,不删除原始 paths + if (!remappedArgs.path && remappedArgs.paths) { + if (Array.isArray(remappedArgs.paths) && remappedArgs.paths.length === 1) { + const pathValue = remappedArgs.paths[0]; + if (typeof pathValue === 'string') { + remappedArgs.path = pathValue; + // [FIX] Rust 代码中不删除 paths,这里也不删除 + } + } + } + break; + } + + return remappedArgs; +} + +/** + * [FIX] 规范化工具名称 + * Gemini 有时会返回 "search" 而不是 "Grep" + */ +function normalizeToolName(name) { + if (!name) return name; + + const nameLower = name.toLowerCase(); + if (nameLower === 'search') { + return 'Grep'; + } + return name; +} + /** * Gemini转换器类 * 实现Gemini协议到其他协议的转换 @@ -511,19 +641,94 @@ export class GeminiConverter extends BaseConverter { if (candidate) { const parts = candidate.content?.parts; - // 提取文本内容 + // [FIX] 参考 ag/streaming.rs 处理 thinking 和 text 块 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 + const results = []; + + for (const part of parts) { + if (!part) continue; + + if (typeof part.text === 'string') { + if (part.thought === true) { + // [FIX] 这是一个 thinking 块 + const thinkingResult = { + type: "content_block_delta", + index: 0, + delta: { + type: "thinking_delta", + thinking: part.text + } + }; + results.push(thinkingResult); + + // 如果有签名,发送 signature_delta + if (part.thoughtSignature) { + let signature = part.thoughtSignature; + try { + const decoded = Buffer.from(signature, 'base64').toString('utf-8'); + if (decoded && decoded.length > 0 && !decoded.includes('\ufffd')) { + signature = decoded; + } + } catch (e) { + // 解码失败,保持原样 + } + results.push({ + type: "content_block_delta", + index: 0, + delta: { + type: "signature_delta", + signature: signature + } + }); + } + } else { + // 普通文本 + results.push({ + type: "content_block_delta", + index: 0, + delta: { + type: "text_delta", + text: part.text + } + }); } - }; + } + + // [FIX] 处理 functionCall + if (part.functionCall) { + // [FIX] 规范化工具名称和参数映射 + const toolName = normalizeToolName(part.functionCall.name); + const remappedArgs = remapFunctionCallArgs(toolName, part.functionCall.args || {}); + + // 发送 tool_use 开始 + const toolId = part.functionCall.id || `${toolName}-${uuidv4().split('-')[0]}`; + results.push({ + type: "content_block_start", + index: 0, + content_block: { + type: "tool_use", + id: toolId, + name: toolName, + input: {} + } + }); + // 发送参数 + results.push({ + type: "content_block_delta", + index: 0, + delta: { + type: "input_json_delta", + partial_json: JSON.stringify(remappedArgs) + } + }); + } + } + + // 如果有多个结果,返回数组;否则返回单个或 null + if (results.length > 1) { + return results; + } else if (results.length === 1) { + return results[0]; } } @@ -595,11 +800,38 @@ export class GeminiConverter extends BaseConverter { parts.forEach(part => { if (!part) return; + // [FIX] 参考 ag/response.rs 处理 thinking 块 + // Gemini 使用 thought: true 和 thoughtSignature 表示思考内容 if (part.text) { - content.push({ - type: 'text', - text: part.text - }); + if (part.thought === true) { + // 这是一个 thinking 块 + const thinkingBlock = { + type: 'thinking', + thinking: part.text + }; + // 处理签名 - 可能是 Base64 编码的 + if (part.thoughtSignature) { + let signature = part.thoughtSignature; + // 尝试 Base64 解码 + try { + const decoded = Buffer.from(signature, 'base64').toString('utf-8'); + // 检查解码后是否是有效的 UTF-8 字符串 + if (decoded && decoded.length > 0 && !decoded.includes('\ufffd')) { + signature = decoded; + } + } catch (e) { + // 解码失败,保持原样 + } + thinkingBlock.signature = signature; + } + content.push(thinkingBlock); + } else { + // 普通文本 + content.push({ + type: 'text', + text: part.text + }); + } } if (part.inlineData) { @@ -614,19 +846,44 @@ export class GeminiConverter extends BaseConverter { } if (part.functionCall) { - content.push({ + // [FIX] 规范化工具名称和参数映射 + const toolName = normalizeToolName(part.functionCall.name); + const remappedArgs = remapFunctionCallArgs(toolName, part.functionCall.args || {}); + + // [FIX] 使用 Gemini 提供的 id,如果没有则生成 + const toolUseBlock = { type: 'tool_use', - id: uuidv4(), - name: part.functionCall.name, - input: part.functionCall.args || {} - }); + id: part.functionCall.id || `${toolName}-${uuidv4().split('-')[0]}`, + name: toolName, + input: remappedArgs + }; + // [FIX] 如果有签名,添加到 tool_use 块 + if (part.thoughtSignature) { + let signature = part.thoughtSignature; + try { + const decoded = Buffer.from(signature, 'base64').toString('utf-8'); + if (decoded && decoded.length > 0 && !decoded.includes('\ufffd')) { + signature = decoded; + } + } catch (e) { + // 解码失败,保持原样 + } + toolUseBlock.signature = signature; + } + content.push(toolUseBlock); } if (part.functionResponse) { + // [FIX] 正确处理 functionResponse + let responseContent = part.functionResponse.response; + // 如果 response 是对象且有 result 字段,提取它 + if (responseContent && typeof responseContent === 'object' && responseContent.result !== undefined) { + responseContent = responseContent.result; + } content.push({ type: 'tool_result', tool_use_id: part.functionResponse.name, - content: part.functionResponse.response + content: typeof responseContent === 'string' ? responseContent : JSON.stringify(responseContent) }); } }); @@ -641,6 +898,7 @@ export class GeminiConverter extends BaseConverter { if (!geminiResponse || !geminiResponse.candidates || geminiResponse.candidates.length === 0) return []; const content = []; + let hasToolUse = false; for (const candidate of geminiResponse.candidates) { if (candidate.finishReason && candidate.finishReason !== 'STOP') { @@ -655,11 +913,35 @@ export class GeminiConverter extends BaseConverter { if (candidate.content && candidate.content.parts) { for (const part of candidate.content.parts) { + // [FIX] 参考 ag/response.rs 处理 thinking 块 if (part.text) { - content.push({ - type: 'text', - text: part.text - }); + if (part.thought === true) { + // 这是一个 thinking 块 + const thinkingBlock = { + type: 'thinking', + thinking: part.text + }; + // 处理签名 + if (part.thoughtSignature) { + let signature = part.thoughtSignature; + try { + const decoded = Buffer.from(signature, 'base64').toString('utf-8'); + if (decoded && decoded.length > 0 && !decoded.includes('\ufffd')) { + signature = decoded; + } + } catch (e) { + // 解码失败,保持原样 + } + thinkingBlock.signature = signature; + } + content.push(thinkingBlock); + } else { + // 普通文本 + content.push({ + type: 'text', + text: part.text + }); + } } else if (part.inlineData) { content.push({ type: 'image', @@ -670,12 +952,32 @@ export class GeminiConverter extends BaseConverter { } }); } else if (part.functionCall) { - content.push({ + hasToolUse = true; + // [FIX] 规范化工具名称和参数映射 + const toolName = normalizeToolName(part.functionCall.name); + const remappedArgs = remapFunctionCallArgs(toolName, part.functionCall.args || {}); + + // [FIX] 使用 Gemini 提供的 id + const toolUseBlock = { type: 'tool_use', - id: uuidv4(), - name: part.functionCall.name, - input: part.functionCall.args || {} - }); + id: part.functionCall.id || `${toolName}-${uuidv4().split('-')[0]}`, + name: toolName, + input: remappedArgs + }; + // 添加签名(如果存在) + if (part.thoughtSignature) { + let signature = part.thoughtSignature; + try { + const decoded = Buffer.from(signature, 'base64').toString('utf-8'); + if (decoded && decoded.length > 0 && !decoded.includes('\ufffd')) { + signature = decoded; + } + } catch (e) { + // 解码失败,保持原样 + } + toolUseBlock.signature = signature; + } + content.push(toolUseBlock); } } }