import { describe, expect, it } from "vitest"; import { getModel } from "../src/models.js"; import { type BedrockOptions, streamBedrock } from "../src/providers/amazon-bedrock.js"; import type { Context, Model } from "../src/types.js"; interface BedrockThinkingPayload { additionalModelRequestFields?: { thinking?: { type: string; budget_tokens?: number; display?: string }; output_config?: { effort?: string }; anthropic_beta?: string[]; }; } function makeContext(): Context { return { messages: [{ role: "user", content: "Hello", timestamp: Date.now() }], }; } async function capturePayload( model: Model<"bedrock-converse-stream">, options?: BedrockOptions, ): Promise { let capturedPayload: BedrockThinkingPayload | undefined; const s = streamBedrock(model, makeContext(), { ...options, reasoning: options?.reasoning ?? "high", signal: AbortSignal.abort(), onPayload: (payload) => { capturedPayload = payload as BedrockThinkingPayload; return payload; }, }); for await (const event of s) { if (event.type === "error") { break; } } if (!capturedPayload) { throw new Error("Expected Bedrock payload to be captured before request abort"); } return capturedPayload; } describe("Bedrock thinking payload", () => { it("uses adaptive thinking for Claude Opus 4.7 when reasoning is enabled", async () => { const baseModel = getModel("amazon-bedrock", "global.anthropic.claude-opus-4-6-v1"); const model: Model<"bedrock-converse-stream"> = { ...baseModel, id: "global.anthropic.claude-opus-4-7-v1", name: "Claude Opus 4.7 (Global)", }; const payload = await capturePayload(model); expect(payload.additionalModelRequestFields?.thinking).toEqual({ type: "adaptive", display: "summarized" }); expect(payload.additionalModelRequestFields?.output_config).toEqual({ effort: "high" }); expect(payload.additionalModelRequestFields?.anthropic_beta).toBeUndefined(); }); it("maps xhigh reasoning to effort=xhigh for Claude Opus 4.7", async () => { const baseModel = getModel("amazon-bedrock", "global.anthropic.claude-opus-4-6-v1"); const model: Model<"bedrock-converse-stream"> = { ...baseModel, id: "global.anthropic.claude-opus-4-7-v1", name: "Claude Opus 4.7 (Global)", }; const payload = await capturePayload(model, { reasoning: "xhigh" }); expect(payload.additionalModelRequestFields?.thinking).toEqual({ type: "adaptive", display: "summarized" }); expect(payload.additionalModelRequestFields?.output_config).toEqual({ effort: "xhigh" }); expect(payload.additionalModelRequestFields?.anthropic_beta).toBeUndefined(); }); it("omits display for GovCloud model ids on non-adaptive Claude thinking", async () => { const baseModel = getModel("amazon-bedrock", "us.anthropic.claude-sonnet-4-5-20250929-v1:0"); const model: Model<"bedrock-converse-stream"> = { ...baseModel, id: "us-gov.anthropic.claude-sonnet-4-5-20250929-v1:0", name: "Claude Sonnet 4.5 (GovCloud)", }; const payload = await capturePayload(model); expect(payload.additionalModelRequestFields?.thinking).toEqual({ type: "enabled", budget_tokens: 16384 }); expect(payload.additionalModelRequestFields?.anthropic_beta).toEqual(["interleaved-thinking-2025-05-14"]); }); it("omits display for GovCloud regions on adaptive Claude thinking", async () => { const baseModel = getModel("amazon-bedrock", "global.anthropic.claude-opus-4-6-v1"); const model: Model<"bedrock-converse-stream"> = { ...baseModel, id: "global.anthropic.claude-opus-4-7-v1", name: "Claude Opus 4.7 (Global)", }; const payload = await capturePayload(model, { region: "us-gov-west-1" }); expect(payload.additionalModelRequestFields?.thinking).toEqual({ type: "adaptive" }); expect(payload.additionalModelRequestFields?.output_config).toEqual({ effort: "high" }); expect(payload.additionalModelRequestFields?.anthropic_beta).toBeUndefined(); }); }); describe("Application inference profile support", () => { it("uses adaptive thinking when model.name contains the model name but ARN does not", async () => { const baseModel = getModel("amazon-bedrock", "global.anthropic.claude-opus-4-6-v1"); const model: Model<"bedrock-converse-stream"> = { ...baseModel, id: "arn:aws:bedrock:us-east-1:123456789012:application-inference-profile/my-profile", name: "Claude Opus 4.6", }; const payload = await capturePayload(model); expect(payload.additionalModelRequestFields?.thinking).toEqual({ type: "adaptive", display: "summarized" }); expect(payload.additionalModelRequestFields?.output_config).toEqual({ effort: "high" }); }); it("injects cache points when model.name identifies a supported Claude model", async () => { const baseModel = getModel("amazon-bedrock", "global.anthropic.claude-opus-4-6-v1"); const model: Model<"bedrock-converse-stream"> = { ...baseModel, id: "arn:aws:bedrock:us-east-1:123456789012:application-inference-profile/my-profile", name: "Claude Sonnet 4.6", }; let capturedPayload: any; const s = streamBedrock( model, { systemPrompt: "You are helpful.", messages: [{ role: "user", content: "Hello", timestamp: Date.now() }], }, { signal: AbortSignal.abort(), onPayload: (payload) => { capturedPayload = payload; return payload; }, }, ); for await (const event of s) { if (event.type === "error") break; } // System prompt should have a cache point expect(capturedPayload.system).toHaveLength(2); expect(capturedPayload.system[1]).toHaveProperty("cachePoint"); // Last user message should have a cache point const lastMsg = capturedPayload.messages[capturedPayload.messages.length - 1]; const lastContent = lastMsg.content[lastMsg.content.length - 1]; expect(lastContent).toHaveProperty("cachePoint"); }); it("falls back to fixed-budget thinking for non-adaptive Claude via model.name", async () => { const baseModel = getModel("amazon-bedrock", "us.anthropic.claude-sonnet-4-5-20250929-v1:0"); const model: Model<"bedrock-converse-stream"> = { ...baseModel, id: "arn:aws:bedrock:us-east-1:123456789012:application-inference-profile/my-profile", name: "Claude Sonnet 4.5", }; const payload = await capturePayload(model); expect(payload.additionalModelRequestFields?.thinking).toMatchObject({ type: "enabled", budget_tokens: expect.any(Number), }); expect(payload.additionalModelRequestFields?.anthropic_beta).toEqual(["interleaved-thinking-2025-05-14"]); }); });