Skip to content

Commit df5dcb8

Browse files
ralyodioclaude
andcommitted
fix: stream Claude audit + engine catalog contract test
- claude-engine.ts: messages.parse → messages.stream + finalMessage. High-effort adaptive thinking + web_search/web_fetch routinely pushed the request past the SDK's 10-minute non-streaming HTTP ceiling, throwing "Streaming is required for operations that may take longer than 10 minutes." The streaming path preserves the exact parsed_output semantics we rely on. - tests/contract/engines.test.ts: locks in the engine catalog (kimi stays disabled, cost math, dedupe, availability) so a future edit can't quietly re-enable kimi or change rule's free status. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent b0393d7 commit df5dcb8

2 files changed

Lines changed: 115 additions & 4 deletions

File tree

‎lib/audit/claude-engine.ts‎

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -118,15 +118,15 @@ export async function claudeAudit(targetUrl: string): Promise<ClaudeAuditResult>
118118

119119
const userPrompt = `Audit this URL: ${targetUrl}\nCompany name (for the report header): ${company}\n\nPretend you've never heard of this company. Use only the public web. Use web_fetch and web_search.`;
120120

121-
const response = await client.messages.parse({
121+
// Stream the request — high-effort adaptive thinking + web tools routinely
122+
// pushes past the SDK's 10-minute non-streaming HTTP timeout. `.finalMessage()`
123+
// gives us the same ParsedMessage shape `.parse()` did.
124+
const stream = client.messages.stream({
122125
model: "claude-opus-4-7",
123126
max_tokens: 64000,
124127
thinking: { type: "adaptive" },
125128
output_config: {
126129
effort: "high",
127-
// SDK helper's type definition still expects v3 schemas; runtime is fine
128-
// because the helper imports zod/v4 internally. Cast to any to silence
129-
// the structural mismatch.
130130
// eslint-disable-next-line @typescript-eslint/no-explicit-any
131131
format: zodOutputFormat(ResultSchema as any),
132132
},
@@ -139,6 +139,7 @@ export async function claudeAudit(targetUrl: string): Promise<ClaudeAuditResult>
139139
],
140140
messages: [{ role: "user", content: userPrompt }],
141141
});
142+
const response = await stream.finalMessage();
142143

143144
const parsed = response.parsed_output;
144145
if (!parsed) {

‎tests/contract/engines.test.ts‎

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import { describe, it, expect } from "vitest";
2+
import {
3+
ENGINES,
4+
type Engine,
5+
dedupeEngines,
6+
engineAvailable,
7+
engineCost,
8+
selectionCost,
9+
} from "@/lib/credits";
10+
11+
const ALL_ENGINES: Engine[] = [
12+
"rule",
13+
"claude",
14+
"openai",
15+
"qwen",
16+
"kimi",
17+
"gemini",
18+
"deepseek",
19+
];
20+
21+
describe("ENGINES catalog", () => {
22+
it("includes every engine the action accepts", () => {
23+
for (const e of ALL_ENGINES) {
24+
expect(ENGINES[e]).toBeDefined();
25+
expect(typeof ENGINES[e].label).toBe("string");
26+
expect(typeof ENGINES[e].blurb).toBe("string");
27+
expect(typeof ENGINES[e].cost).toBe("number");
28+
expect(typeof ENGINES[e].available).toBe("boolean");
29+
}
30+
});
31+
32+
it("rule is the only free engine", () => {
33+
expect(ENGINES.rule.cost).toBe(0);
34+
for (const e of ALL_ENGINES) {
35+
if (e === "rule") continue;
36+
expect(ENGINES[e].cost).toBeGreaterThan(0);
37+
}
38+
});
39+
40+
it("kimi stays disabled (Moonshot balance / 429 regression)", () => {
41+
expect(ENGINES.kimi.available).toBe(false);
42+
});
43+
44+
it("all engines except kimi are available", () => {
45+
for (const e of ALL_ENGINES) {
46+
if (e === "kimi") continue;
47+
expect(ENGINES[e].available).toBe(true);
48+
}
49+
});
50+
});
51+
52+
describe("engineCost / engineAvailable", () => {
53+
it("engineCost matches the catalog", () => {
54+
expect(engineCost("rule")).toBe(0);
55+
expect(engineCost("claude")).toBe(1);
56+
expect(engineCost("openai")).toBe(1);
57+
});
58+
59+
it("engineAvailable reflects the available flag", () => {
60+
expect(engineAvailable("rule")).toBe(true);
61+
expect(engineAvailable("claude")).toBe(true);
62+
expect(engineAvailable("kimi")).toBe(false);
63+
});
64+
});
65+
66+
describe("selectionCost", () => {
67+
it("returns 0 for rule-only", () => {
68+
expect(selectionCost(["rule"])).toBe(0);
69+
});
70+
71+
it("sums one credit per paid engine, rule rides free", () => {
72+
expect(selectionCost(["rule", "claude"])).toBe(1);
73+
expect(selectionCost(["claude", "openai"])).toBe(2);
74+
expect(selectionCost(["rule", "claude", "openai", "gemini"])).toBe(3);
75+
});
76+
77+
it("returns 0 for an empty selection", () => {
78+
expect(selectionCost([])).toBe(0);
79+
});
80+
});
81+
82+
describe("dedupeEngines", () => {
83+
it("preserves first-seen order", () => {
84+
expect(dedupeEngines(["claude", "rule", "openai"])).toEqual([
85+
"claude",
86+
"rule",
87+
"openai",
88+
]);
89+
});
90+
91+
it("collapses duplicates", () => {
92+
expect(dedupeEngines(["rule", "rule", "claude", "claude"])).toEqual([
93+
"rule",
94+
"claude",
95+
]);
96+
});
97+
98+
it("drops unknown engine names without throwing", () => {
99+
const out = dedupeEngines([
100+
"rule",
101+
"not-an-engine" as Engine,
102+
"claude",
103+
]);
104+
expect(out).toEqual(["rule", "claude"]);
105+
});
106+
107+
it("returns empty for empty input", () => {
108+
expect(dedupeEngines([])).toEqual([]);
109+
});
110+
});

0 commit comments

Comments
 (0)