fix(grok): 修复流式响应中图片渲染和思考块处理问题

- 修复流式响应中图片 URL 被截断的问题,通过缓冲区累积完整 URL
- 改进卡片附件处理,支持从 cardAttachmentsJson 解析并渲染图片
- 优化思考块逻辑,避免在正式内容开始后显示无意义的内部注释
- 修复思考块未正确关闭的问题,确保格式完整性
- 更新文档中的模型列表,将 Qwen Code 替换为 Codex
This commit is contained in:
hex2077 2026-04-01 22:25:43 +08:00
parent 2b0f3adb8a
commit dc153730f7
6 changed files with 140 additions and 18 deletions

View file

@ -4,7 +4,7 @@
# AIClient-2-API 🚀
**複数のクライアント専用大規模言語モデルAPIGemini CLI、Antigravity、Qwen Code、Kiro ...を模擬リクエストし、ローカルのOpenAI互換インターフェースに統一的にラッピングする強力なプロキシ。**
**複数のクライアント専用大規模言語モデルAPIGemini CLI、Antigravity、Codex, Grok、Kiro ...を模擬リクエストし、ローカルのOpenAI互換インターフェースに統一的にラッピングする強力なプロキシ。**
<a href="https://trendshift.io/repositories/15832" target="_blank"><img src="https://trendshift.io/api/badge/repositories/15832" alt="justlovemaki%2FAIClient-2-API | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
</div>
@ -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大プロトコル間のインテリジェント変換をサポートし、クロスプロトコルモデル呼び出しを実現

View file

@ -4,7 +4,7 @@
# AIClient-2-API 🚀
**一个能将多种仅客户端内使用的大模型 APIGemini CLI, Antigravity, Qwen Code, Kiro ...),模拟请求,统一封装为本地 OpenAI 兼容接口的强大代理。**
**一个能将多种仅客户端内使用的大模型 APIGemini CLI, Antigravity, Codex, Grok, Kiro ...),模拟请求,统一封装为本地 OpenAI 兼容接口的强大代理。**
<a href="https://trendshift.io/repositories/15832" target="_blank"><img src="https://trendshift.io/api/badge/repositories/15832" alt="justlovemaki%2FAIClient-2-API | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
</div>
@ -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 三大协议间的智能转换,实现跨协议模型调用

View file

@ -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.**
<a href="https://trendshift.io/repositories/15832" target="_blank"><img src="https://trendshift.io/api/badge/repositories/15832" alt="justlovemaki%2FAIClient-2-API | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
</div>
@ -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

View file

@ -1 +1 @@
2.12.2
2.12.2.1

View file

@ -104,6 +104,7 @@ export class GrokConverter extends BaseConverter {
has_tool_call: false,
rollout_id: "",
in_tool_call: false, // 是否处于 <tool_call> 块内
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) {}
}
// 替换正文中的 <grok:render> 标签为 Markdown 图片
if (content && cardMap.size > 0) {
content = content.replace(/<grok:render[^>]*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</think>\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 += "<think>\n";
state.think_opened = true;
}
deltaReasoning += filtered;
deltaContent += filtered;
} else {
if (state.think_opened) {
deltaContent += "\n</think>\n";
state.think_opened = false;
state.content_started = true;
}
// 将新 token 加入待处理缓冲区,解决 URL 被截断的问题
state.pending_text_buffer += filtered;

View file

@ -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;
}