supportsPromptCaching, supportsAdaptiveThinking, supportsThinkingSignature, and the Claude detection in streamSimpleBedrock/buildAdditionalModelRequestFields all check model.id for Claude model name patterns. Application inference profile ARNs are opaque and do not contain the model name, so these checks silently fail. Fix by also checking model.name (user-controlled via models.json or registerProvider) as a fallback in all affected functions. Added a shared isAnthropicClaudeModel helper for the common Claude detection pattern. Fixes #2925 Co-authored-by: Your Name <you@example.com>
172 lines
6.3 KiB
TypeScript
172 lines
6.3 KiB
TypeScript
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<BedrockThinkingPayload> {
|
|
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"]);
|
|
});
|
|
});
|