Skip to content

Commit f9b6bb7

Browse files
author
MouseWW
committed
fix(llm): reject anthropic empty text responses
1 parent 40b9823 commit f9b6bb7

4 files changed

Lines changed: 55 additions & 31 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.29
1+
# Anything Analyzer v3.6.30
22

33
## 修复
44

5-
- **LLM 非对象 JSON 响应诊断**避免兼容服务返回 `null` 等合法但非对象 JSON 时抛出内部属性访问错误
6-
- `safeParseJson` 现在会将非对象 JSON 归一为空对象,由各调用路径继续给出明确格式错误
7-
- OpenAI Chat Completions 回归测试覆盖 `null` 响应,确保诊断保持为缺少 `choices` 字段
5+
- **Anthropic/MiniMax 非流式空文本诊断**避免兼容服务只返回工具块或非文本块时被误判为空成功结果
6+
- 非流式 Anthropic 兼容响应现在要求至少包含一个文本内容块
7+
- MiniMax 回归测试覆盖无 `text` 内容的响应,确保给出明确格式错误
88

99
## 下载
1010

1111
| 平台 | 文件 |
1212
|------|------|
13-
| Windows | Anything-Analyzer-Setup-3.6.29.exe |
14-
| macOS (Apple Silicon) | Anything-Analyzer-3.6.29-arm64.dmg |
15-
| macOS (Intel) | Anything-Analyzer-3.6.29-x64.dmg |
16-
| Linux | Anything-Analyzer-3.6.29.AppImage |
13+
| Windows | Anything-Analyzer-Setup-3.6.30.exe |
14+
| macOS (Apple Silicon) | Anything-Analyzer-3.6.30-arm64.dmg |
15+
| macOS (Intel) | Anything-Analyzer-3.6.30-x64.dmg |
16+
| Linux | Anything-Analyzer-3.6.30.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.29",
3+
"version": "3.6.30",
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: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -688,14 +688,17 @@ export class LLMRouter {
688688
if (!Array.isArray(data.content)) {
689689
throw new Error(`LLM 响应格式异常: 缺少 content 字段 — ${JSON.stringify(data).slice(0, 200)}`);
690690
}
691-
const content = data.content
692-
.filter((c) => c.type === "text")
693-
.map((c) => c.text)
694-
.join("");
695-
return {
696-
content,
697-
promptTokens: data.usage?.input_tokens || 0,
698-
completionTokens: data.usage?.output_tokens || 0,
691+
const content = data.content
692+
.filter((c) => c.type === "text")
693+
.map((c) => c.text)
694+
.join("");
695+
if (content.length === 0) {
696+
throw new Error(`LLM 响应格式异常: 缺少 text content 字段 — ${JSON.stringify(data).slice(0, 200)}`);
697+
}
698+
return {
699+
content,
700+
promptTokens: data.usage?.input_tokens || 0,
701+
completionTokens: data.usage?.output_tokens || 0,
699702
};
700703
}
701704

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

Lines changed: 35 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -187,11 +187,11 @@ describe("LLMRouter", () => {
187187
expect(options.headers).not.toHaveProperty("Authorization");
188188
});
189189

190-
it("should parse MiniMax response content and usage correctly", async () => {
191-
const config: LLMProviderConfig = {
192-
name: "minimax",
193-
baseUrl: "https://api.minimax.io/anthropic/v1",
194-
apiKey: "test-minimax-key",
190+
it("should parse MiniMax response content and usage correctly", async () => {
191+
const config: LLMProviderConfig = {
192+
name: "minimax",
193+
baseUrl: "https://api.minimax.io/anthropic/v1",
194+
apiKey: "test-minimax-key",
195195
model: "MiniMax-M2.7-highspeed",
196196
maxTokens: 4096,
197197
};
@@ -206,15 +206,36 @@ describe("LLMRouter", () => {
206206
const result = await router.complete([{ role: "user", content: "hello" }]);
207207

208208
expect(result.content).toBe("MiniMax response");
209-
expect(result.promptTokens).toBe(20);
210-
expect(result.completionTokens).toBe(10);
211-
});
212-
213-
it("should route to completions endpoint when apiType is undefined", async () => {
214-
const config: LLMProviderConfig = { ...baseConfig };
215-
fetchSpy.mockResolvedValueOnce(
216-
createJSONResponse({
217-
choices: [{ message: { content: "hello" } }],
209+
expect(result.promptTokens).toBe(20);
210+
expect(result.completionTokens).toBe(10);
211+
});
212+
213+
it("should reject Anthropic-compatible responses without text content", async () => {
214+
const config: LLMProviderConfig = {
215+
name: "minimax",
216+
baseUrl: "https://api.minimax.io/anthropic/v1",
217+
apiKey: "test-minimax-key",
218+
model: "MiniMax-M2.7-highspeed",
219+
maxTokens: 4096,
220+
};
221+
fetchSpy.mockResolvedValueOnce(
222+
createJSONResponse({
223+
content: [{ type: "tool_use", id: "call-1", name: "lookup", input: {} }],
224+
}),
225+
);
226+
227+
const router = new LLMRouter(config);
228+
229+
await expect(
230+
router.complete([{ role: "user", content: "hello" }]),
231+
).rejects.toThrow("LLM 响应格式异常: 缺少 text content 字段");
232+
});
233+
234+
it("should route to completions endpoint when apiType is undefined", async () => {
235+
const config: LLMProviderConfig = { ...baseConfig };
236+
fetchSpy.mockResolvedValueOnce(
237+
createJSONResponse({
238+
choices: [{ message: { content: "hello" } }],
218239
usage: { prompt_tokens: 10, completion_tokens: 5 },
219240
}),
220241
);

0 commit comments

Comments
 (0)