Skip to content

Commit e2d630a

Browse files
committed
test: add unit coverage for Claude Opus 4.8 across providers
- anthropic.spec.ts: 5 cases mirroring 4.7 (1M-beta-header guard, adaptive thinking ON/OFF, custom maxTokens, getModel info). - anthropic-vertex.spec.ts: 1M context tier pricing for Vertex Opus 4.8. - shared/api.spec.ts: getModelMaxOutputTokens hybrid-token handling on 4.8. - bedrock.spec.ts: new 'Claude 4.7+ adaptive thinking' block with 5 cases covering 4.7 + 4.8 adaptive thinking, reasoning-off behaviour, a 4.6 regression guard (budget_tokens + temperature), and cross-region prefix detection (us.anthropic.claude-opus-4-8). 235 unit tests pass, 0 type errors. Validated live end-to-end via Bedrock Global Inference (global.anthropic.claude-opus-4-8).
1 parent 0b84512 commit e2d630a

4 files changed

Lines changed: 287 additions & 0 deletions

File tree

src/api/providers/__tests__/anthropic-vertex.spec.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -929,6 +929,22 @@ describe("VertexHandler", () => {
929929
expect(model.betas).toContain("context-1m-2025-08-07")
930930
})
931931

932+
it("should enable 1M context for Claude Opus 4.8 when beta flag is set", () => {
933+
const handler = new AnthropicVertexHandler({
934+
apiModelId: "claude-opus-4-8",
935+
vertexProjectId: "test-project",
936+
vertexRegion: "us-central1",
937+
vertex1MContext: true,
938+
})
939+
940+
const model = handler.getModel()
941+
expect(model.info.contextWindow).toBe(1_000_000)
942+
expect(model.info.inputPrice).toBe(10.0)
943+
expect(model.info.outputPrice).toBe(37.5)
944+
expect(model.info.supportsTemperature).toBe(false)
945+
expect(model.betas).toContain("context-1m-2025-08-07")
946+
})
947+
932948
it("should not enable 1M context when flag is disabled", () => {
933949
const handler = new AnthropicVertexHandler({
934950
apiModelId: VERTEX_1M_CONTEXT_MODEL_IDS[0],

src/api/providers/__tests__/anthropic.spec.ts

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -304,6 +304,101 @@ describe("AnthropicHandler", () => {
304304
expect(requestBody?.thinking).toEqual({ type: "adaptive" })
305305
expect(requestBody?.max_tokens).toBe(32768)
306306
})
307+
308+
it("should not require the 1M context beta header for Claude Opus 4.8", async () => {
309+
const opus48Handler = new AnthropicHandler({
310+
apiKey: "test-api-key",
311+
apiModelId: "claude-opus-4-8",
312+
anthropicBeta1MContext: true,
313+
})
314+
315+
const stream = opus48Handler.createMessage(systemPrompt, [
316+
{
317+
role: "user",
318+
content: [{ type: "text" as const, text: "Hello" }],
319+
},
320+
])
321+
322+
for await (const _chunk of stream) {
323+
// Consume stream
324+
}
325+
326+
const requestBody = mockCreate.mock.calls[mockCreate.mock.calls.length - 1]?.[0]
327+
const requestOptions = mockCreate.mock.calls[mockCreate.mock.calls.length - 1]?.[1]
328+
expect(requestBody?.temperature).toBeUndefined()
329+
expect(requestOptions?.headers?.["anthropic-beta"]).toContain("prompt-caching-2024-07-31")
330+
expect(requestOptions?.headers?.["anthropic-beta"]).not.toContain("context-1m-2025-08-07")
331+
})
332+
333+
it("should use adaptive thinking for Claude Opus 4.8 when reasoning is enabled", async () => {
334+
const opus48Handler = new AnthropicHandler({
335+
apiKey: "test-api-key",
336+
apiModelId: "claude-opus-4-8",
337+
enableReasoningEffort: true,
338+
})
339+
340+
const stream = opus48Handler.createMessage(systemPrompt, [
341+
{
342+
role: "user",
343+
content: [{ type: "text" as const, text: "Hello" }],
344+
},
345+
])
346+
347+
for await (const _chunk of stream) {
348+
// Consume stream
349+
}
350+
351+
const requestBody = mockCreate.mock.calls[mockCreate.mock.calls.length - 1]?.[0]
352+
expect(requestBody?.thinking).toEqual({ type: "adaptive" })
353+
expect(requestBody?.max_tokens).toBe(16384)
354+
})
355+
356+
it("should omit thinking for Claude Opus 4.8 when reasoning is disabled", async () => {
357+
const opus48Handler = new AnthropicHandler({
358+
apiKey: "test-api-key",
359+
apiModelId: "claude-opus-4-8",
360+
enableReasoningEffort: false,
361+
})
362+
363+
const stream = opus48Handler.createMessage(systemPrompt, [
364+
{
365+
role: "user",
366+
content: [{ type: "text" as const, text: "Hello" }],
367+
},
368+
])
369+
370+
for await (const _chunk of stream) {
371+
// Consume stream
372+
}
373+
374+
const requestBody = mockCreate.mock.calls[mockCreate.mock.calls.length - 1]?.[0]
375+
expect(requestBody?.thinking).toBeUndefined()
376+
expect(requestBody?.max_tokens).toBe(8192)
377+
})
378+
379+
it("should preserve custom maxTokens for Claude Opus 4.8 when reasoning is enabled", async () => {
380+
const opus48Handler = new AnthropicHandler({
381+
apiKey: "test-api-key",
382+
apiModelId: "claude-opus-4-8",
383+
enableReasoningEffort: true,
384+
modelMaxTokens: 32768,
385+
})
386+
387+
const stream = opus48Handler.createMessage(systemPrompt, [
388+
{
389+
role: "user",
390+
content: [{ type: "text" as const, text: "Hello" }],
391+
},
392+
])
393+
394+
for await (const _chunk of stream) {
395+
// Consume stream
396+
}
397+
398+
const requestBody = mockCreate.mock.calls[mockCreate.mock.calls.length - 1]?.[0]
399+
expect(requestBody?.thinking).toEqual({ type: "adaptive" })
400+
expect(requestBody?.max_tokens).toBe(32768)
401+
})
307402
})
308403

309404
describe("completePrompt", () => {
@@ -431,6 +526,23 @@ describe("AnthropicHandler", () => {
431526
expect(model.reasoningBudget).toBeUndefined()
432527
})
433528

529+
it("should handle Claude Opus 4.8 model correctly", () => {
530+
const handler = new AnthropicHandler({
531+
apiKey: "test-api-key",
532+
apiModelId: "claude-opus-4-8",
533+
})
534+
const model = handler.getModel()
535+
expect(model.id).toBe("claude-opus-4-8")
536+
expect(model.info.maxTokens).toBe(128000)
537+
expect(model.info.contextWindow).toBe(1000000)
538+
expect(model.maxTokens).toBe(8192)
539+
expect(model.info.supportsReasoningBinary).toBe(true)
540+
expect(model.info.supportsReasoningBudget).toBe(true)
541+
expect(model.info.supportsPromptCache).toBe(true)
542+
expect(model.info.supportsTemperature).toBe(false)
543+
expect(model.reasoningBudget).toBeUndefined()
544+
})
545+
434546
it("should enable 1M context for Claude 4.5 Sonnet when beta flag is set", () => {
435547
const handler = new AnthropicHandler({
436548
apiKey: "test-api-key",

src/api/providers/__tests__/bedrock.spec.ts

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1327,4 +1327,134 @@ describe("AwsBedrockHandler", () => {
13271327
expect(hasCachePoint).toBe(false)
13281328
})
13291329
})
1330+
1331+
describe("Claude 4.7+ adaptive thinking (Opus 4.7 / Opus 4.8)", () => {
1332+
beforeEach(() => {
1333+
mockConverseStreamCommand.mockReset()
1334+
})
1335+
1336+
const messages: Anthropic.Messages.MessageParam[] = [{ role: "user", content: "Hello" }]
1337+
1338+
it("should send adaptive thinking with effort xhigh for Claude Opus 4.7 when reasoning is enabled", async () => {
1339+
const opus47Handler = new AwsBedrockHandler({
1340+
apiModelId: "anthropic.claude-opus-4-7",
1341+
awsAccessKey: "test-access-key",
1342+
awsSecretKey: "test-secret-key",
1343+
awsRegion: "us-east-1",
1344+
enableReasoningEffort: true,
1345+
})
1346+
1347+
const generator = opus47Handler.createMessage("System prompt", messages)
1348+
await generator.next()
1349+
1350+
expect(mockConverseStreamCommand).toHaveBeenCalled()
1351+
const commandArg = mockConverseStreamCommand.mock.calls[0][0] as any
1352+
1353+
// Adaptive thinking — no budget_tokens, must use effort levels.
1354+
expect(commandArg.additionalModelRequestFields?.thinking).toEqual({
1355+
type: "adaptive",
1356+
display: "summarized",
1357+
})
1358+
expect(commandArg.additionalModelRequestFields?.output_config).toEqual({ effort: "xhigh" })
1359+
// 4.7+ rejects sampling parameters: temperature must be omitted entirely.
1360+
expect(commandArg.inferenceConfig?.temperature).toBeUndefined()
1361+
})
1362+
1363+
it("should send adaptive thinking with effort xhigh for Claude Opus 4.8 when reasoning is enabled", async () => {
1364+
const opus48Handler = new AwsBedrockHandler({
1365+
apiModelId: "anthropic.claude-opus-4-8",
1366+
awsAccessKey: "test-access-key",
1367+
awsSecretKey: "test-secret-key",
1368+
awsRegion: "us-east-1",
1369+
enableReasoningEffort: true,
1370+
})
1371+
1372+
const generator = opus48Handler.createMessage("System prompt", messages)
1373+
await generator.next()
1374+
1375+
expect(mockConverseStreamCommand).toHaveBeenCalled()
1376+
const commandArg = mockConverseStreamCommand.mock.calls[0][0] as any
1377+
1378+
// 4.8 inherits the 4.7 adaptive-thinking contract — no breaking API changes.
1379+
expect(commandArg.additionalModelRequestFields?.thinking).toEqual({
1380+
type: "adaptive",
1381+
display: "summarized",
1382+
})
1383+
expect(commandArg.additionalModelRequestFields?.output_config).toEqual({ effort: "xhigh" })
1384+
// Sampling parameters are still rejected on 4.8 — temperature must be absent.
1385+
expect(commandArg.inferenceConfig?.temperature).toBeUndefined()
1386+
})
1387+
1388+
it("should omit thinking and temperature for Claude Opus 4.8 when reasoning is disabled", async () => {
1389+
const opus48Handler = new AwsBedrockHandler({
1390+
apiModelId: "anthropic.claude-opus-4-8",
1391+
awsAccessKey: "test-access-key",
1392+
awsSecretKey: "test-secret-key",
1393+
awsRegion: "us-east-1",
1394+
enableReasoningEffort: false,
1395+
})
1396+
1397+
const generator = opus48Handler.createMessage("System prompt", messages)
1398+
await generator.next()
1399+
1400+
expect(mockConverseStreamCommand).toHaveBeenCalled()
1401+
const commandArg = mockConverseStreamCommand.mock.calls[0][0] as any
1402+
1403+
// Without reasoning enabled, no adaptive thinking payload is sent.
1404+
expect(commandArg.additionalModelRequestFields?.thinking).toBeUndefined()
1405+
// Temperature is still omitted for 4.8 because the API rejects sampling params.
1406+
expect(commandArg.inferenceConfig?.temperature).toBeUndefined()
1407+
})
1408+
1409+
it("should still send temperature and budget_tokens thinking for older Claude Opus 4.6", async () => {
1410+
// Regression guard: the adaptive-thinking branch must NOT activate for 4.6 or earlier.
1411+
const opus46Handler = new AwsBedrockHandler({
1412+
apiModelId: "anthropic.claude-opus-4-6-v1",
1413+
awsAccessKey: "test-access-key",
1414+
awsSecretKey: "test-secret-key",
1415+
awsRegion: "us-east-1",
1416+
enableReasoningEffort: true,
1417+
})
1418+
1419+
const generator = opus46Handler.createMessage("System prompt", messages)
1420+
await generator.next()
1421+
1422+
expect(mockConverseStreamCommand).toHaveBeenCalled()
1423+
const commandArg = mockConverseStreamCommand.mock.calls[0][0] as any
1424+
1425+
// 4.6 still uses the budget_tokens-based thinking format.
1426+
expect(commandArg.additionalModelRequestFields?.thinking?.type).toBe("enabled")
1427+
expect(commandArg.additionalModelRequestFields?.thinking?.budget_tokens).toBeGreaterThan(0)
1428+
// 4.6 still accepts temperature.
1429+
expect(commandArg.inferenceConfig?.temperature).toBeDefined()
1430+
})
1431+
1432+
it("should detect adaptive-thinking models via cross-region inference prefix (us.anthropic.claude-opus-4-8)", async () => {
1433+
// Regression guard: the heuristic uses parseBaseModelId, so cross-region prefixes
1434+
// like `us.` / `eu.` / `global.` must still be detected as 4.8.
1435+
const opus48GlobalHandler = new AwsBedrockHandler({
1436+
apiModelId: "anthropic.claude-opus-4-8",
1437+
awsAccessKey: "test-access-key",
1438+
awsSecretKey: "test-secret-key",
1439+
awsRegion: "us-east-1",
1440+
awsUseCrossRegionInference: true,
1441+
enableReasoningEffort: true,
1442+
})
1443+
1444+
const generator = opus48GlobalHandler.createMessage("System prompt", messages)
1445+
await generator.next()
1446+
1447+
expect(mockConverseStreamCommand).toHaveBeenCalled()
1448+
const commandArg = mockConverseStreamCommand.mock.calls[0][0] as any
1449+
1450+
// Model ID should carry the cross-region prefix.
1451+
expect(commandArg.modelId).toBe("us.anthropic.claude-opus-4-8")
1452+
// Adaptive thinking must still apply despite the prefix.
1453+
expect(commandArg.additionalModelRequestFields?.thinking).toEqual({
1454+
type: "adaptive",
1455+
display: "summarized",
1456+
})
1457+
expect(commandArg.inferenceConfig?.temperature).toBeUndefined()
1458+
})
1459+
})
13301460
})

src/shared/__tests__/api.spec.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,35 @@ describe("getModelMaxOutputTokens", () => {
106106
).toBe(32_768)
107107
})
108108

109+
test("should preserve Anthropic hybrid token handling for Claude Opus 4.8", () => {
110+
// 4.8 inherits the same adaptive-thinking + binary-reasoning capability as 4.7
111+
// (no breaking API changes between 4.7 and 4.8 per the official migration guide).
112+
const model: ModelInfo = {
113+
contextWindow: 1_000_000,
114+
supportsPromptCache: true,
115+
supportsReasoningBudget: true,
116+
supportsReasoningBinary: true,
117+
supportsTemperature: false,
118+
maxTokens: 128_000,
119+
}
120+
121+
expect(
122+
getModelMaxOutputTokens({
123+
modelId: "claude-opus-4-8",
124+
model,
125+
settings: { apiProvider: "anthropic", enableReasoningEffort: false },
126+
}),
127+
).toBe(ANTHROPIC_DEFAULT_MAX_TOKENS)
128+
129+
expect(
130+
getModelMaxOutputTokens({
131+
modelId: "claude-opus-4-8",
132+
model,
133+
settings: { apiProvider: "anthropic", enableReasoningEffort: true, modelMaxTokens: 32_768 },
134+
}),
135+
).toBe(32_768)
136+
})
137+
109138
test("should return model.maxTokens for non-Anthropic models that support reasoning budget but aren't using it", () => {
110139
const geminiModelId = "gemini-2.5-flash-preview-04-17"
111140
const model: ModelInfo = {

0 commit comments

Comments
 (0)