diff --git a/README-JA.md b/README-JA.md index 25353f4..227c362 100644 --- a/README-JA.md +++ b/README-JA.md @@ -4,7 +4,7 @@ # AIClient-2-API 🚀 -**複数のクライアント専用大規模言語モデルAPI(Gemini CLI、Antigravity、Qwen Code、Kiro ...)を模擬リクエストし、ローカルのOpenAI互換インターフェースに統一的にラッピングする強力なプロキシ。** +**複数のクライアント専用大規模言語モデルAPI(Gemini CLI、Antigravity、Codex, Grok、Kiro ...)を模擬リクエストし、ローカルのOpenAI互換インターフェースに統一的にラッピングする強力なプロキシ。** justlovemaki%2FAIClient-2-API | Trendshift @@ -62,7 +62,7 @@ ## 🚀 概要 -`AIClient2API` はクライアント制限を突破するAPIプロキシサービスで、Gemini、Antigravity、Qwen Code、Kiroなど、元々クライアント内でのみ使用可能な無料大規模モデルを、あらゆるアプリケーションから呼び出せる標準OpenAI互換インターフェースに変換します。Node.jsをベースに構築され、OpenAI、Claude、Geminiの3大プロトコル間のインテリジェント変換をサポートし、Cherry-Studio、NextChat、Clineなどのツールで、Claude Opus 4.5、Gemini 3.0 Pro、Qwen3 Coder Plusなどの高度なモデルを大規模に無料で使用できるようにします。プロジェクトはストラテジーパターンとアダプターパターンに基づくモジュラーアーキテクチャを採用し、アカウントプール管理、インテリジェントポーリング、自動フェイルオーバー、ヘルスチェック機構を内蔵し、99.9%のサービス可用性を保証します。 +`AIClient2API` はクライアント制限を突破するAPIプロキシサービスで、Gemini、Antigravity、Codex, Grok、Kiroなど、元々クライアント内でのみ使用可能な無料大規模モデルを、あらゆるアプリケーションから呼び出せる標準OpenAI互換インターフェースに変換します。Node.jsをベースに構築され、OpenAI、Claude、Geminiの3大プロトコル間のインテリジェント変換をサポートし、Cherry-Studio、NextChat、Clineなどのツールで、Claude Opus 4.5、Gemini 3.0 Pro、Qwen3 Coder Plusなどの高度なモデルを大規模に無料で使用できるようにします。プロジェクトはストラテジーパターンとアダプターパターンに基づくモジュラーアーキテクチャを採用し、アカウントプール管理、インテリジェントポーリング、自動フェイルオーバー、ヘルスチェック機構を内蔵し、99.9%のサービス可用性を保証します。 > [!NOTE] > **🎉 重要なマイルストーン** @@ -100,7 +100,7 @@ ## 💡 コアアドバンテージ ### 🎯 統一アクセス、ワンストップ管理 -* **マルチモデル統一インターフェース**:標準OpenAI互換プロトコルを通じて、一度の設定でGemini、Claude、Grok、Qwen Code、Kimi K2、MiniMax M2などの主流大規模モデルにアクセス +* **マルチモデル統一インターフェース**:標準OpenAI互換プロトコルを通じて、一度の設定でGemini、Claude、Grok、Codex、 K2、MiniMax M2などの主流大規模モデルにアクセス * **柔軟な切り替えメカニズム**:Pathルーティング、起動パラメータ、環境変数の3つの方法で動的にモデルを切り替え、異なるシナリオのニーズに対応 * **ゼロコスト移行**:OpenAI API仕様と完全互換、Cherry-Studio、NextChat、Clineなどのツールを変更なしで使用可能 * **マルチプロトコルインテリジェント変換**:OpenAI、Claude、Geminiの3大プロトコル間のインテリジェント変換をサポートし、クロスプロトコルモデル呼び出しを実現 diff --git a/README-ZH.md b/README-ZH.md index 56b3d6e..e7e2ddf 100644 --- a/README-ZH.md +++ b/README-ZH.md @@ -4,7 +4,7 @@ # AIClient-2-API 🚀 -**一个能将多种仅客户端内使用的大模型 API(Gemini CLI, Antigravity, Qwen Code, Kiro ...),模拟请求,统一封装为本地 OpenAI 兼容接口的强大代理。** +**一个能将多种仅客户端内使用的大模型 API(Gemini CLI, Antigravity, Codex, Grok, Kiro ...),模拟请求,统一封装为本地 OpenAI 兼容接口的强大代理。** justlovemaki%2FAIClient-2-API | Trendshift @@ -61,7 +61,7 @@ ## 🚀 项目概览 -`AIClient2API` 是一个突破客户端限制的 API 代理服务,将 Gemini、Antigravity、Qwen Code、Kiro 等原本仅限客户端内使用的免费大模型,转换为可供任何应用调用的标准 OpenAI 兼容接口。基于 Node.js 构建,支持 OpenAI、Claude、Gemini 三大协议的智能互转,让 Cherry-Studio、NextChat、Cline 等工具能够免费大量使用 Claude Opus 4.5、Gemini 3.0 Pro、Qwen3 Coder Plus 等高级模型。项目采用策略模式和适配器模式的模块化架构,内置账号池管理、智能轮询、自动故障转移和健康检查机制,确保 99.9% 的服务可用性。 +`AIClient2API` 是一个突破客户端限制的 API 代理服务,将 Gemini、Antigravity、Codex, Grok、Kiro 等原本仅限客户端内使用的免费大模型,转换为可供任何应用调用的标准 OpenAI 兼容接口。基于 Node.js 构建,支持 OpenAI、Claude、Gemini 三大协议的智能互转,让 Cherry-Studio、NextChat、Cline 等工具能够免费大量使用 Claude Opus 4.5、Gemini 3.0 Pro、Qwen3 Coder Plus 等高级模型。项目采用策略模式和适配器模式的模块化架构,内置账号池管理、智能轮询、自动故障转移和健康检查机制,确保 99.9% 的服务可用性。 > [!NOTE] > **🎉 重要里程碑** @@ -98,7 +98,7 @@ ## 💡 核心优势 ### 🎯 统一接入,一站式管理 -* **多模型统一接口**:通过标准 OpenAI 兼容协议,一次配置即可接入 Gemini、Claude、Grok、Qwen Code、Kimi K2、MiniMax M2 等主流大模型 +* **多模型统一接口**:通过标准 OpenAI 兼容协议,一次配置即可接入 Gemini、Claude、Grok、Codex、Kimi K2、MiniMax M2 等主流大模型 * **灵活切换机制**:Path 路由、支持通过启动参数、环境变量三种方式动态切换模型,满足不同场景需求 * **零成本迁移**:完全兼容 OpenAI API 规范,Cherry-Studio、NextChat、Cline 等工具无需修改即可使用 * **多协议智能转换**:支持 OpenAI、Claude、Gemini 三大协议间的智能转换,实现跨协议模型调用 diff --git a/README.md b/README.md index 9bc84ed..8c0895e 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ # AIClient-2-API 🚀 -**A powerful proxy that can unify the requests of various client-only large model APIs (Gemini CLI, Antigravity, Qwen Code, Kiro ...), simulate requests, and encapsulate them into a local OpenAI-compatible interface.** +**A powerful proxy that can unify the requests of various client-only large model APIs (Gemini CLI, Antigravity, Codex, Grok, Kiro ...), simulate requests, and encapsulate them into a local OpenAI-compatible interface.** justlovemaki%2FAIClient-2-API | Trendshift @@ -62,7 +62,7 @@ ## 🚀 Overview -`AIClient2API` is an API proxy service that breaks through client limitations, converting free large models originally restricted to client use only (such as Gemini, Antigravity, Qwen Code, Kiro) into standard OpenAI-compatible interfaces that can be called by any application. Built on Node.js, it supports intelligent conversion between OpenAI, Claude, and Gemini protocols, enabling tools like Cherry-Studio, NextChat, and Cline to freely use advanced models such as Claude Opus 4.5, Gemini 3.0 Pro, 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. +`AIClient2API` is an API proxy service that breaks through client limitations, converting free large models originally restricted to client use only (such as Gemini, Antigravity, Codex, Grok, Kiro) into standard OpenAI-compatible interfaces that can be called by any application. Built on Node.js, it supports intelligent conversion between OpenAI, Claude, and Gemini protocols, enabling tools like Cherry-Studio, NextChat, and Cline to freely use advanced models such as Claude Opus 4.5, Gemini 3.0 Pro, 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. > [!NOTE] > **🎉 Important Milestone** @@ -100,7 +100,7 @@ ## 💡 Core Advantages ### 🎯 Unified Access, One-Stop Management -* **Multi-Model Unified Interface**: Through standard OpenAI-compatible protocol, configure once to access mainstream large models including Gemini, Claude, Grok, Qwen Code, Kimi K2, MiniMax M2 +* **Multi-Model Unified Interface**: Through standard OpenAI-compatible protocol, configure once to access mainstream large models including Gemini, Claude, Grok, Codex, Kimi K2, MiniMax M2 * **Flexible Switching Mechanism**: Path routing, support dynamic model switching via startup parameters or environment variables to meet different scenario requirements * **Zero-Cost Migration**: Fully compatible with OpenAI API specifications, tools like Cherry-Studio, NextChat, Cline can be used without modification * **Multi-Protocol Intelligent Conversion**: Support intelligent conversion between OpenAI, Claude, and Gemini protocols for cross-protocol model invocation diff --git a/VERSION b/VERSION index 371a952..500b629 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -2.12.2 +2.12.2.1 diff --git a/src/converters/strategies/GrokConverter.js b/src/converters/strategies/GrokConverter.js index 7fb48f4..38ebfb5 100644 --- a/src/converters/strategies/GrokConverter.js +++ b/src/converters/strategies/GrokConverter.js @@ -104,6 +104,7 @@ export class GrokConverter extends BaseConverter { has_tool_call: false, rollout_id: "", in_tool_call: false, // 是否处于 块内 + content_started: false, // 是否已经开始输出正式内容 requestBaseUrl: "", uuid: null, pending_text_buffer: "" // 用于处理流式输出中被截断的 URL @@ -352,6 +353,17 @@ export class GrokConverter extends BaseConverter { } continue; } + if (key === "cardAttachmentsJson" && Array.isArray(item)) { + item.forEach(jsonStr => { + if (typeof jsonStr !== 'string') return; + try { + const card = JSON.parse(jsonStr); + const url = card.image?.original; + if (url) add(url); + } catch (e) {} + }); + continue; + } walk(item); } } @@ -484,13 +496,55 @@ export class GrokConverter extends BaseConverter { content = this._filterToken(content, responseId); content = this._processGrokAssetsInText(content, state); - // 收集图片并追加 + // 处理 cardAttachmentsJson 中的图片,将其映射到卡片 ID + const cardMap = new Map(); + const modelResponse = grokResponse.modelResponse || {}; + + // 收集所有的卡片原始数据(可能是 cardAttachmentsJson 中的,或者是单独收集的 cardAttachments 数组) + const allCardSources = []; + if (Array.isArray(modelResponse.cardAttachmentsJson)) allCardSources.push(...modelResponse.cardAttachmentsJson); + if (Array.isArray(grokResponse.cardAttachments)) { + grokResponse.cardAttachments.forEach(card => card.jsonData && allCardSources.push(card.jsonData)); + } else if (grokResponse.cardAttachment?.jsonData) { + allCardSources.push(grokResponse.cardAttachment.jsonData); + } + + for (const raw of allCardSources) { + try { + const cardData = JSON.parse(raw); + const cardId = cardData.id; + const image = cardData.image || {}; + const original = image.original; + const title = image.title || "image"; + if (cardId && original) { + cardMap.set(cardId, { title, original }); + } + } catch (e) {} + } + + // 替换正文中的 标签为 Markdown 图片 + if (content && cardMap.size > 0) { + content = content.replace(/]*card_id="([^"]+)"[^>]*>.*?<\/grok:render>/gs, (match, cardId) => { + const item = cardMap.get(cardId); + if (!item) return ""; + return this._renderImage(item.original, item.title || "image", state); + }); + } + + // 收集未在正文中渲染的其他图片并追加 const imageUrls = this._collectImages(grokResponse); if (imageUrls.length > 0) { - content += "\n"; + // 已通过卡片 ID 渲染过的 URL 记录 + const handledUrls = new Set(); + for (const item of cardMap.values()) handledUrls.add(item.original); + + let appendContent = ""; for (const url of imageUrls) { - content += this._renderImage(url, "image", state) + "\n"; + if (!handledUrls.has(url)) { + appendContent += this._renderImage(url, "image", state) + "\n"; + } } + if (appendContent) content += "\n" + appendContent; } // 处理视频 (非流式模式) @@ -585,6 +639,13 @@ export class GrokConverter extends BaseConverter { // 处理结束标志 if (resp.isDone) { let finalContent = ""; + + // 如果思考块未关闭,在此关闭 + if (state.think_opened) { + finalContent += "\n\n"; + state.think_opened = false; + } + // 处理剩余的缓冲区 if (state.pending_text_buffer) { finalContent += this._processGrokAssetsInText(state.pending_text_buffer, state); @@ -722,11 +783,26 @@ export class GrokConverter extends BaseConverter { const token = resp.token; const filtered = this._filterToken(token, responseId); const isThinking = !!resp.isThinking; - const inThink = isThinking || state.image_think_active || state.video_think_active; + const hasStepId = !!resp.messageStepId; + const inThink = isThinking || hasStepId || state.image_think_active || state.video_think_active; - if (inThink) { + // 正式内容已开始后,丢弃中途插入的 Agent 思考(1-2 句内部注释,无用户价值) + if (state.content_started && inThink && !state.image_think_active && !state.video_think_active) { + // 跳过不展示 + } else if (inThink) { + if (!state.think_opened) { + deltaContent += "\n"; + state.think_opened = true; + } deltaReasoning += filtered; + deltaContent += filtered; } else { + if (state.think_opened) { + deltaContent += "\n\n"; + state.think_opened = false; + state.content_started = true; + } + // 将新 token 加入待处理缓冲区,解决 URL 被截断的问题 state.pending_text_buffer += filtered; diff --git a/src/providers/grok/grok-core.js b/src/providers/grok/grok-core.js index ab9f92c..815e07b 100644 --- a/src/providers/grok/grok-core.js +++ b/src/providers/grok/grok-core.js @@ -439,7 +439,20 @@ export class GrokApiService { async generateContent(model, requestBody) { logger.info(`[Grok] Starting generateContent (unified processing)`); const stream = this.generateContentStream(model, requestBody); - const collected = { message: "", responseId: "", postId: "", llmInfo: {}, rolloutId: "", modelResponse: null, cardAttachment: null, streamingImageGenerationResponse: null, streamingVideoGenerationResponse: null, finalVideoUrl: null, finalThumbnailUrl: null }; + const collected = { + message: "", + responseId: "", + postId: "", + llmInfo: {}, + rolloutId: "", + modelResponse: null, + cardAttachment: null, + cardAttachments: [], // 收集所有的卡片附件 + streamingImageGenerationResponse: null, + streamingVideoGenerationResponse: null, + finalVideoUrl: null, + finalThumbnailUrl: null + }; for await (const chunk of stream) { const resp = chunk.result?.response; @@ -450,8 +463,41 @@ export class GrokApiService { if (resp.rolloutId) collected.rolloutId = resp.rolloutId; if (resp._requestBaseUrl) collected._requestBaseUrl = resp._requestBaseUrl; if (resp._uuid) collected._uuid = resp._uuid; - if (resp.modelResponse) collected.modelResponse = resp.modelResponse; - if (resp.cardAttachment) collected.cardAttachment = resp.cardAttachment; + + if (resp.modelResponse) { + if (!collected.modelResponse) { + collected.modelResponse = resp.modelResponse; + } else { + // 合并 modelResponse 中的数据 + if (resp.modelResponse.message) collected.modelResponse.message = resp.modelResponse.message; + if (Array.isArray(resp.modelResponse.cardAttachmentsJson)) { + if (!collected.modelResponse.cardAttachmentsJson) { + collected.modelResponse.cardAttachmentsJson = resp.modelResponse.cardAttachmentsJson; + } else { + const currentIds = new Set(collected.modelResponse.cardAttachmentsJson.map(raw => { + try { return JSON.parse(raw).id; } catch (e) { return null; } + }).filter(id => id)); + + for (const raw of resp.modelResponse.cardAttachmentsJson) { + try { + const id = JSON.parse(raw).id; + if (!id || !currentIds.has(id)) { + collected.modelResponse.cardAttachmentsJson.push(raw); + if (id) currentIds.add(id); + } + } catch (e) { + collected.modelResponse.cardAttachmentsJson.push(raw); + } + } + } + } + } + } + + if (resp.cardAttachment) { + collected.cardAttachment = resp.cardAttachment; + collected.cardAttachments.push(resp.cardAttachment); + } if (resp.streamingImageGenerationResponse) { collected.streamingImageGenerationResponse = resp.streamingImageGenerationResponse; }