Skip to content

Commit 40b9823

Browse files
author
MouseWW
committed
fix(llm): handle non-object json responses
1 parent 6073df9 commit 40b9823

4 files changed

Lines changed: 29 additions & 17 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.28
1+
# Anything Analyzer v3.6.29
22

33
## 修复
44

5-
- **OpenAI Chat 非流式响应格式校验**避免兼容服务缺少 `message.content` 时被误判为空成功结果
6-
- 普通 Chat Completions 请求现在会明确拒绝缺少 `message.content` 的 malformed choice
7-
- 工具调用最终文本轮次复用同一校验,保留工具调用中 `content` 为空的合法场景
5+
- **LLM 非对象 JSON 响应诊断**避免兼容服务返回 `null` 等合法但非对象 JSON 时抛出内部属性访问错误
6+
- `safeParseJson` 现在会将非对象 JSON 归一为空对象,由各调用路径继续给出明确格式错误
7+
- OpenAI Chat Completions 回归测试覆盖 `null` 响应,确保诊断保持为缺少 `choices` 字段
88

99
## 下载
1010

1111
| 平台 | 文件 |
1212
|------|------|
13-
| Windows | Anything-Analyzer-Setup-3.6.28.exe |
14-
| macOS (Apple Silicon) | Anything-Analyzer-3.6.28-arm64.dmg |
15-
| macOS (Intel) | Anything-Analyzer-3.6.28-x64.dmg |
16-
| Linux | Anything-Analyzer-3.6.28.AppImage |
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 |

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.28",
3+
"version": "3.6.29",
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: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -135,8 +135,10 @@ export class LLMRouter {
135135
throw new Error(`LLM 返回了非 JSON 响应 (${response.status}): ${preview}`);
136136
}
137137

138-
// Anthropic error format: { type: "error", error: { type, message } }
139-
const obj = data as Record<string, unknown>;
138+
// Anthropic error format: { type: "error", error: { type, message } }
139+
const obj = data !== null && typeof data === "object"
140+
? data as Record<string, unknown>
141+
: {};
140142
if (obj.type === 'error' && typeof obj.error === 'object' && obj.error !== null) {
141143
const err = obj.error as Record<string, unknown>;
142144
throw new Error(`LLM API 错误: ${err.type ?? 'unknown'}${err.message ?? JSON.stringify(err)}`);
@@ -153,8 +155,8 @@ export class LLMRouter {
153155
throw new Error(`LLM API 错误: ${err.message ?? JSON.stringify(err)}`);
154156
}
155157

156-
return data as T;
157-
}
158+
return obj as T;
159+
}
158160

159161
async complete(
160162
messages: ChatMessage[],

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

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -46,10 +46,10 @@ function createRawSSEResponse(body: string): Response {
4646
// Helper: create a mock JSON Response
4747
function createJSONResponse(body: unknown): Response {
4848
return new Response(JSON.stringify(body), {
49-
status: 200,
50-
headers: { "content-type": "application/json" },
51-
});
52-
}
49+
status: 200,
50+
headers: { "content-type": "application/json" },
51+
});
52+
}
5353

5454
const baseConfig: LLMProviderConfig = {
5555
name: "openai",
@@ -261,6 +261,16 @@ describe("LLMRouter", () => {
261261
).rejects.toThrow("LLM 响应格式异常: 缺少 choices 字段");
262262
});
263263

264+
it("should reject non-object OpenAI completion JSON with a clear format error", async () => {
265+
fetchSpy.mockResolvedValueOnce(createJSONResponse(null));
266+
267+
const router = new LLMRouter(baseConfig);
268+
269+
await expect(
270+
router.complete([{ role: "user", content: "test" }]),
271+
).rejects.toThrow("LLM 响应格式异常: 缺少 choices 字段");
272+
});
273+
264274
it("should reject OpenAI completion choices without message content", async () => {
265275
fetchSpy.mockResolvedValueOnce(
266276
createJSONResponse({

0 commit comments

Comments
 (0)