Skip to content

Commit 7ce6e67

Browse files
committed
fix(llm): reject invalid anthropic text content
1 parent 1f73866 commit 7ce6e67

4 files changed

Lines changed: 49 additions & 25 deletions

File tree

‎RELEASE_NOTES.md‎

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,16 @@
1-
# Anything Analyzer v3.6.35
1+
# Anything Analyzer v3.6.36
22

33
## 修复
44

5-
- **流式文本增量格式校验** — 避免 SSE 文本增量不是字符串时被拼成 `[object Object]`
6-
- OpenAI Chat、Responses API、Anthropic/MiniMax 流式解析现在都会拒绝非字符串文本 delta
7-
- 新增回归测试覆盖三个流式接口的非字符串增量异常路径
5+
- **Anthropic/MiniMax 非流式文本格式校验** — 避免 text content 不是字符串时被拼成 `[object Object]`
6+
- 普通请求和工具调用最终轮次复用同一段 Anthropic 文本块校验逻辑
7+
- 新增回归测试覆盖非字符串 text content 的异常路径
88

99
## 下载
1010

1111
| 平台 | 文件 |
1212
|------|------|
13-
| Windows | Anything-Analyzer-Setup-3.6.35.exe |
14-
| macOS (Apple Silicon) | Anything-Analyzer-3.6.35-arm64.dmg |
15-
| macOS (Intel) | Anything-Analyzer-3.6.35-x64.dmg |
16-
| Linux | Anything-Analyzer-3.6.35.AppImage |
13+
| Windows | Anything-Analyzer-Setup-3.6.36.exe |
14+
| macOS (Apple Silicon) | Anything-Analyzer-3.6.36-arm64.dmg |
15+
| macOS (Intel) | Anything-Analyzer-3.6.36-x64.dmg |
16+
| Linux | Anything-Analyzer-3.6.36.AppImage |

‎package.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "anything-analyzer",
3-
"version": "3.6.35",
3+
"version": "3.6.36",
44
"description": "Universal web protocol analyzer with embedded browser and AI-powered analysis",
55
"packageManager": "pnpm@10.24.0",
66
"main": "./out/main/index.js",

‎src/main/ai/llm-router.ts‎

Lines changed: 19 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -105,10 +105,26 @@ function readStreamTextDelta(
105105
throw new Error(`${providerName} stream error: ${fieldName} must be a string`);
106106
}
107107

108+
function readAnthropicTextContent(data: {
109+
content: Array<{ type: string; text?: unknown }>;
110+
}): string {
111+
const textBlocks = data.content.filter((block) => block.type === "text");
112+
if (textBlocks.some((block) => typeof block.text !== "string")) {
113+
throw new Error(`LLM 响应格式异常: text content 必须是字符串 — ${JSON.stringify(data).slice(0, 200)}`);
114+
}
115+
116+
const content = textBlocks.map((block) => block.text as string).join("");
117+
if (content.length === 0) {
118+
throw new Error(`LLM 响应格式异常: 缺少 text content 字段 — ${JSON.stringify(data).slice(0, 200)}`);
119+
}
120+
121+
return content;
122+
}
123+
108124
/**
109125
* Sanitize string content in LLM request body to remove control characters
110126
* that may break JSON parsing in intermediate proxies.
111-
*/
127+
*/
112128
function sanitizeForJson(obj: unknown): unknown {
113129
if (typeof obj === 'string') {
114130
// Remove ASCII control chars (except \n \r \t) and Unicode replacement char
@@ -457,14 +473,7 @@ export class LLMRouter {
457473
}
458474

459475
// No tool use → extract text content as final answer
460-
const textContent = data.content
461-
.filter((b): b is AnthropicTextBlock => b.type === "text")
462-
.map((b) => b.text)
463-
.join("");
464-
465-
if (textContent.length === 0) {
466-
throw new Error(`LLM 响应格式异常: 缺少 text content 字段 — ${JSON.stringify(data).slice(0, 200)}`);
467-
}
476+
const textContent = readAnthropicTextContent(data);
468477
if (onChunk && textContent) onChunk(textContent);
469478
return {
470479
content: textContent,
@@ -730,13 +739,7 @@ export class LLMRouter {
730739
if (!Array.isArray(data.content)) {
731740
throw new Error(`LLM 响应格式异常: 缺少 content 字段 — ${JSON.stringify(data).slice(0, 200)}`);
732741
}
733-
const content = data.content
734-
.filter((c) => c.type === "text")
735-
.map((c) => c.text)
736-
.join("");
737-
if (content.length === 0) {
738-
throw new Error(`LLM 响应格式异常: 缺少 text content 字段 — ${JSON.stringify(data).slice(0, 200)}`);
739-
}
742+
const content = readAnthropicTextContent(data);
740743
return {
741744
content,
742745
promptTokens: data.usage?.input_tokens || 0,

‎tests/main/ai/llm-router.test.ts‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,27 @@ describe("LLMRouter", () => {
231231
).rejects.toThrow("LLM 响应格式异常: 缺少 text content 字段");
232232
});
233233

234+
it("should reject Anthropic-compatible responses with non-string text content", async () => {
235+
const config: LLMProviderConfig = {
236+
name: "minimax",
237+
baseUrl: "https://api.minimax.io/anthropic/v1",
238+
apiKey: "test-minimax-key",
239+
model: "MiniMax-M2.7-highspeed",
240+
maxTokens: 4096,
241+
};
242+
fetchSpy.mockResolvedValueOnce(
243+
createJSONResponse({
244+
content: [{ type: "text", text: { value: "not a string" } }],
245+
}),
246+
);
247+
248+
const router = new LLMRouter(config);
249+
250+
await expect(
251+
router.complete([{ role: "user", content: "hello" }]),
252+
).rejects.toThrow("LLM 响应格式异常: text content 必须是字符串");
253+
});
254+
234255
it("should route to completions endpoint when apiType is undefined", async () => {
235256
const config: LLMProviderConfig = { ...baseConfig };
236257
fetchSpy.mockResolvedValueOnce(

0 commit comments

Comments
 (0)