Skip to content

Commit 60405ef

Browse files
committed
test(ai): live-probe overflow classification and output clamping
1 parent 2e9cd36 commit 60405ef

2 files changed

Lines changed: 98 additions & 1 deletion

File tree

packages/ai/src/providers/anthropic.live.test.ts

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import { describe, expect, it } from "vitest";
22
import type { Context, Model } from "../types.js";
3-
import { streamSimpleAnthropic } from "./anthropic.js";
3+
import { isContextOverflow } from "../utils/overflow.js";
4+
import { streamAnthropic, streamSimpleAnthropic } from "./anthropic.js";
5+
import { clampMaxTokensToContext, estimateContextInputTokens } from "./simple-options.js";
46

57
const apiKey = process.env.ANTHROPIC_API_KEY?.trim() ?? "";
68
const live = process.env.OPENCLAW_LIVE_TEST === "1" && apiKey.length > 0;
@@ -83,4 +85,66 @@ describeLive("Anthropic provider live", () => {
8385
},
8486
timeoutMs,
8587
);
88+
89+
it(
90+
"classifies the provider's current context-overflow error",
91+
async () => {
92+
const result = await streamAnthropic(
93+
model,
94+
{
95+
messages: [
96+
{
97+
role: "user",
98+
content: "x ".repeat(Math.ceil(model.contextWindow * 1.1)),
99+
timestamp: 0,
100+
},
101+
],
102+
},
103+
{ apiKey, maxTokens: 1, maxRetries: 0 },
104+
).result();
105+
106+
expect(result.stopReason).toBe("error");
107+
expect(isContextOverflow(result)).toBe(true);
108+
},
109+
timeoutMs,
110+
);
111+
112+
it(
113+
"clamps an excessive output request to the remaining context",
114+
async () => {
115+
let sentMaxTokens: number | undefined;
116+
const contextWithoutSystem: Context = {
117+
messages: [{ role: "user", content: "Reply briefly with ok.", timestamp: 0 }],
118+
};
119+
const promptUnit = "x ";
120+
const baseEstimate = estimateContextInputTokens(contextWithoutSystem);
121+
const unitEstimate =
122+
estimateContextInputTokens({ ...contextWithoutSystem, systemPrompt: promptUnit }) -
123+
baseEstimate;
124+
const targetEstimate = model.contextWindow - Math.floor(model.maxTokens / 2);
125+
const context: Context = {
126+
...contextWithoutSystem,
127+
systemPrompt: promptUnit.repeat(Math.ceil((targetEstimate - baseEstimate) / unitEstimate)),
128+
};
129+
const requestedMaxTokens = model.maxTokens * 100;
130+
expect(estimateContextInputTokens(context)).toBeGreaterThanOrEqual(targetEstimate);
131+
const expectedMaxTokens = clampMaxTokensToContext(model, context, requestedMaxTokens);
132+
expect(expectedMaxTokens).toBeLessThan(model.maxTokens);
133+
134+
const result = await streamSimpleAnthropic(model, context, {
135+
apiKey,
136+
maxTokens: requestedMaxTokens,
137+
maxRetries: 0,
138+
reasoning: "off",
139+
onPayload: (payload) => {
140+
sentMaxTokens = (payload as { max_tokens?: number }).max_tokens;
141+
},
142+
}).result();
143+
144+
expect(sentMaxTokens).toBe(expectedMaxTokens);
145+
expect(result.errorMessage).toBeUndefined();
146+
expect(["stop", "length"]).toContain(result.stopReason);
147+
},
148+
timeoutMs,
149+
);
86150
});

packages/ai/src/providers/openai-responses.live.test.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { Type } from "typebox";
22
import { describe, expect, it } from "vitest";
33
import type { Context, Model } from "../types.js";
4+
import { isContextOverflow } from "../utils/overflow.js";
45
import { streamOpenAIResponses } from "./openai-responses.js";
56

67
// Live coverage for the Responses stream state machine: real streams interleave
@@ -12,6 +13,8 @@ const describeLive = LIVE && OPENAI_KEY ? describe : describe.skip;
1213

1314
const LIVE_MODEL_ID = process.env.OPENCLAW_LIVE_RESPONSES_MODEL || "gpt-5.6-luna";
1415
const LIVE_TIMEOUT_MS = 120_000;
16+
const OVERFLOW_MODEL_ID = "gpt-4o-mini";
17+
const OVERFLOW_CONTEXT_WINDOW = 128_000;
1518

1619
function liveModel(overrides: Partial<Model<"openai-responses">> = {}) {
1720
return {
@@ -113,4 +116,34 @@ describeLive("OpenAI Responses live", () => {
113116
},
114117
LIVE_TIMEOUT_MS,
115118
);
119+
120+
it(
121+
"classifies the provider's current context-overflow error",
122+
async () => {
123+
const overflowModel = liveModel({
124+
id: OVERFLOW_MODEL_ID,
125+
name: OVERFLOW_MODEL_ID,
126+
contextWindow: OVERFLOW_CONTEXT_WINDOW,
127+
maxTokens: 16_384,
128+
reasoning: false,
129+
});
130+
const result = await streamOpenAIResponses(
131+
overflowModel,
132+
{
133+
messages: [
134+
{
135+
role: "user",
136+
content: "x ".repeat(Math.ceil(overflowModel.contextWindow * 1.1)),
137+
timestamp: 0,
138+
},
139+
],
140+
},
141+
{ apiKey: OPENAI_KEY, maxTokens: 16, maxRetries: 0 },
142+
).result();
143+
144+
expect(result.stopReason).toBe("error");
145+
expect(isContextOverflow(result)).toBe(true);
146+
},
147+
LIVE_TIMEOUT_MS,
148+
);
116149
});

0 commit comments

Comments
 (0)