fix(grok): 修复流式响应中图片渲染和思考块处理问题
- 修复流式响应中图片 URL 被截断的问题,通过缓冲区累积完整 URL - 改进卡片附件处理,支持从 cardAttachmentsJson 解析并渲染图片 - 优化思考块逻辑,避免在正式内容开始后显示无意义的内部注释 - 修复思考块未正确关闭的问题,确保格式完整性 - 更新文档中的模型列表,将 Qwen Code 替换为 Codex
This commit is contained in:
parent
2b0f3adb8a
commit
dc153730f7
6 changed files with 140 additions and 18 deletions
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
# AIClient-2-API 🚀
|
||||
|
||||
**複数のクライアント専用大規模言語モデルAPI(Gemini CLI、Antigravity、Qwen Code、Kiro ...)を模擬リクエストし、ローカルのOpenAI互換インターフェースに統一的にラッピングする強力なプロキシ。**
|
||||
**複数のクライアント専用大規模言語モデルAPI(Gemini 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大プロトコル間のインテリジェント変換をサポートし、クロスプロトコルモデル呼び出しを実現
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
# AIClient-2-API 🚀
|
||||
|
||||
**一个能将多种仅客户端内使用的大模型 API(Gemini CLI, Antigravity, Qwen Code, Kiro ...),模拟请求,统一封装为本地 OpenAI 兼容接口的强大代理。**
|
||||
**一个能将多种仅客户端内使用的大模型 API(Gemini 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 三大协议间的智能转换,实现跨协议模型调用
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
2
VERSION
2
VERSION
|
|
@ -1 +1 @@
|
|||
2.12.2
|
||||
2.12.2.1
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue