Skip to content

Commit f507ee3

Browse files
committed
fix(llm): reject empty responses output
1 parent f9b6bb7 commit f507ee3

4 files changed

Lines changed: 66 additions & 37 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.30
1+
# Anything Analyzer v3.6.31
22

33
## 修复
44

5-
- **Anthropic/MiniMax 非流式空文本诊断**避免兼容服务只返回工具块或非文本块时被误判为空成功结果
6-
- 非流式 Anthropic 兼容响应现在要求至少包含一个文本内容块
7-
- MiniMax 回归测试覆盖无 `text` 内容的响应,确保给出明确格式错误
5+
- **Responses API 非流式空文本诊断**避免 `completed` 响应缺少 `output_text` 文本时被误判为空成功结果
6+
- 非流式 Responses 响应现在要求顶层 `output_text``output[].content[]` 至少提取到一个文本块
7+
- 工具调用最终文本轮次复用同一校验,避免无文本结果静默通过
88

99
## 下载
1010

1111
| 平台 | 文件 |
1212
|------|------|
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 |
13+
| Windows | Anything-Analyzer-Setup-3.6.31.exe |
14+
| macOS (Apple Silicon) | Anything-Analyzer-3.6.31-arm64.dmg |
15+
| macOS (Intel) | Anything-Analyzer-3.6.31-x64.dmg |
16+
| Linux | Anything-Analyzer-3.6.31.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.30",
3+
"version": "3.6.31",
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: 41 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -43,18 +43,41 @@ type AnthropicContentBlock = AnthropicTextBlock | AnthropicToolUseBlock;
4343

4444
const DEFAULT_TIMEOUT = 600000; // 10 minutes — LLM relay servers can be slow; user can cancel manually
4545

46-
function extractResponsesOutputText(output: Array<{ type: string; content?: Array<{ type: string; text: string }> }>): string {
46+
interface ResponsesOutputItem {
47+
type: string;
48+
content?: Array<{ type: string; text?: unknown }>;
49+
}
50+
51+
function extractResponsesOutputText(output: ResponsesOutputItem[]): string {
4752
let content = "";
4853
for (const item of output) {
4954
if (item.type === "message" && Array.isArray(item.content)) {
5055
content += item.content
51-
.filter((c) => c.type === "output_text")
52-
.map((c) => c.text)
56+
.filter((c) => c.type === "output_text" && typeof c.text === "string")
57+
.map((c) => c.text as string)
5358
.join("");
5459
}
5560
}
5661
return content;
5762
}
63+
64+
function readResponsesOutputText(data: {
65+
output_text?: unknown;
66+
output?: ResponsesOutputItem[];
67+
}): string {
68+
const content =
69+
typeof data.output_text === "string" && data.output_text.length > 0
70+
? data.output_text
71+
: Array.isArray(data.output)
72+
? extractResponsesOutputText(data.output)
73+
: "";
74+
75+
if (content.length === 0) {
76+
throw new Error(`LLM 响应格式异常: 缺少 output_text 字段 — ${JSON.stringify(data).slice(0, 200)}`);
77+
}
78+
79+
return content;
80+
}
5881

5982
/**
6083
* Sanitize string content in LLM request body to remove control characters
@@ -475,16 +498,14 @@ export class LLMRouter {
475498
status?: string;
476499
incomplete_details?: ResponsesIncompleteDetails;
477500
error?: { message?: string };
478-
output: Array<{
479-
type: string;
501+
output: Array<ResponsesOutputItem & {
480502
id?: string;
481503
name?: string;
482504
arguments?: string;
483-
content?: Array<{ type: string; text: string }>;
484-
}>;
485-
output_text?: string;
486-
usage?: { input_tokens: number; output_tokens: number };
487-
}>(response);
505+
}>;
506+
output_text?: string;
507+
usage?: { input_tokens: number; output_tokens: number };
508+
}>(response);
488509

489510
totalPromptTokens += data.usage?.input_tokens || 0;
490511
totalCompletionTokens += data.usage?.output_tokens || 0;
@@ -527,19 +548,14 @@ export class LLMRouter {
527548
});
528549
}
529550
continue;
530-
}
531-
532-
// No function calls → extract text
533-
let content = extractResponsesOutputText(data.output);
534-
535-
// Fallback: check output_text at top level
536-
if (!content && typeof data.output_text === "string") {
537-
content = data.output_text;
538-
}
539-
540-
if (onChunk && content) onChunk(content);
541-
return {
542-
content,
551+
}
552+
553+
// No function calls → extract text
554+
const content = readResponsesOutputText(data);
555+
556+
if (onChunk && content) onChunk(content);
557+
return {
558+
content,
543559
promptTokens: totalPromptTokens,
544560
completionTokens: totalCompletionTokens,
545561
};
@@ -627,10 +643,7 @@ export class LLMRouter {
627643
incomplete_details?: ResponsesIncompleteDetails;
628644
error?: { message?: string };
629645
output_text?: string;
630-
output?: Array<{
631-
type: string;
632-
content?: Array<{ type: string; text: string }>;
633-
}>;
646+
output?: ResponsesOutputItem[];
634647
usage?: { input_tokens: number; output_tokens: number };
635648
}>(response);
636649
if (data.status === "incomplete") {
@@ -642,7 +655,7 @@ export class LLMRouter {
642655
if (typeof data.output_text !== "string" && !Array.isArray(data.output)) {
643656
throw new Error(`LLM 响应格式异常: 缺少 output 字段 — ${JSON.stringify(data).slice(0, 200)}`);
644657
}
645-
const content = data.output_text || (Array.isArray(data.output) ? extractResponsesOutputText(data.output) : "");
658+
const content = readResponsesOutputText(data);
646659
return {
647660
content,
648661
promptTokens: data.usage?.input_tokens || 0,

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

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -427,6 +427,22 @@ describe("LLMRouter", () => {
427427
).rejects.toThrow("LLM 响应格式异常: 缺少 output 字段");
428428
});
429429

430+
it("should reject Responses API results without output text", async () => {
431+
const config: LLMProviderConfig = { ...baseConfig, apiType: "responses" };
432+
fetchSpy.mockResolvedValueOnce(
433+
createJSONResponse({
434+
status: "completed",
435+
output: [],
436+
}),
437+
);
438+
439+
const router = new LLMRouter(config);
440+
441+
await expect(
442+
router.complete([{ role: "user", content: "test" }]),
443+
).rejects.toThrow("LLM 响应格式异常: 缺少 output_text 字段");
444+
});
445+
430446
it("should reject incomplete Responses API results", async () => {
431447
const config: LLMProviderConfig = { ...baseConfig, apiType: "responses" };
432448
fetchSpy.mockResolvedValueOnce(

0 commit comments

Comments
 (0)