diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index c1a588e6..3813adc2 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -4,6 +4,7 @@ ### Fixed +- Fixed Bedrock bearer-token authentication to use the SDK's native token auth path and omit Claude `thinking.display` for GovCloud targets, avoiding duplicate `Authorization` headers and GovCloud Converse validation errors ([#3359](https://github.com/badlogic/pi-mono/issues/3359)) - Fixed direct Mistral tool definitions to strip TypeBox symbol metadata before passing schemas to the SDK, restoring tool calls after the SDK's stricter outbound validation ([#3361](https://github.com/badlogic/pi-mono/issues/3361)) ## [0.67.67] - 2026-04-17 diff --git a/packages/ai/src/providers/amazon-bedrock.ts b/packages/ai/src/providers/amazon-bedrock.ts index 79c98038..4dd36690 100644 --- a/packages/ai/src/providers/amazon-bedrock.ts +++ b/packages/ai/src/providers/amazon-bedrock.ts @@ -15,15 +15,11 @@ import { type ConverseStreamMetadataEvent, ImageFormat, type Message, - type ServiceInputTypes, - type ServiceOutputTypes, type SystemContentBlock, type ToolChoice, type ToolConfiguration, ToolResultStatus, } from "@aws-sdk/client-bedrock-runtime"; -import type { FinalizeRequestMiddleware } from "@smithy/types"; - import { calculateCost } from "../models.js"; import type { Api, @@ -119,8 +115,9 @@ export const streamBedrock: StreamFunction<"bedrock-converse-stream", BedrockOpt profile: options.profile, }; - // Resolve bearer token for API key auth (bypasses SigV4) + // Resolve bearer token for Bedrock API key auth. const bearerToken = options.bearerToken || process.env.AWS_BEARER_TOKEN_BEDROCK || undefined; + const useBearerToken = bearerToken !== undefined && process.env.AWS_BEDROCK_SKIP_AUTH !== "1"; // in Node.js/Bun environment only if (typeof process !== "undefined" && (process.versions?.node || process.versions?.bun)) { @@ -142,15 +139,6 @@ export const streamBedrock: StreamFunction<"bedrock-converse-stream", BedrockOpt }; } - // Bearer token auth: use API key instead of SigV4 signing. - // Requires bedrock:CallWithBearerToken IAM permission. - if (bearerToken && process.env.AWS_BEDROCK_SKIP_AUTH !== "1") { - config.credentials = { - accessKeyId: "bearer-token-auth", - secretAccessKey: "bearer-token-auth", - }; - } - if ( process.env.HTTP_PROXY || process.env.HTTPS_PROXY || @@ -182,37 +170,13 @@ export const streamBedrock: StreamFunction<"bedrock-converse-stream", BedrockOpt config.region = options.region || "us-east-1"; } + if (useBearerToken) { + config.token = { token: bearerToken }; + config.authSchemePreference = ["httpBearerAuth"]; + } + try { const client = new BedrockRuntimeClient(config); - - // Inject bearer token middleware after SigV4 signing - if (bearerToken) { - const bearerTokenAuthMiddleware: FinalizeRequestMiddleware = - (next) => async (args) => { - const request = args.request; - if ( - typeof request === "object" && - request !== null && - "headers" in request && - typeof request.headers === "object" && - request.headers !== null - ) { - const headers = request.headers as Record; - headers.authorization = `Bearer ${bearerToken}`; - delete headers["x-amz-date"]; - delete headers["x-amz-security-token"]; - delete headers["x-amz-content-sha256"]; - } - return next(args); - }; - - client.middlewareStack.addRelativeTo(bearerTokenAuthMiddleware, { - relation: "after", - toMiddleware: "awsAuthMiddleware", - name: "bearerTokenAuth", - }); - } - const cacheRetention = resolveCacheRetention(options.cacheRetention); let commandInput = { modelId: model.id, @@ -812,6 +776,16 @@ function mapStopReason(reason: string | undefined): StopReason { } } +function isGovCloudBedrockTarget(model: Model<"bedrock-converse-stream">, options: BedrockOptions): boolean { + const region = options.region || process.env.AWS_REGION || process.env.AWS_DEFAULT_REGION; + if (region?.toLowerCase().startsWith("us-gov-")) { + return true; + } + + const modelId = model.id.toLowerCase(); + return modelId.startsWith("us-gov.") || modelId.startsWith("arn:aws-us-gov:"); +} + function buildAdditionalModelRequestFields( model: Model<"bedrock-converse-stream">, options: BedrockOptions, @@ -821,12 +795,12 @@ function buildAdditionalModelRequestFields( } if (model.id.includes("anthropic.claude") || model.id.includes("anthropic/claude")) { - // Default to "summarized" so Opus 4.7 and Mythos Preview behave like - // older Claude 4 models (whose API default is also "summarized"). - const display: BedrockThinkingDisplay = options.thinkingDisplay ?? "summarized"; + // GovCloud Bedrock currently rejects the Claude thinking.display field. + // Omit it there until the GovCloud Converse schema catches up. + const display = isGovCloudBedrockTarget(model, options) ? undefined : (options.thinkingDisplay ?? "summarized"); const result: Record = supportsAdaptiveThinking(model.id) ? { - thinking: { type: "adaptive", display }, + thinking: { type: "adaptive", ...(display !== undefined ? { display } : {}) }, output_config: { effort: mapThinkingLevelToEffort(options.reasoning, model.id) }, } : (() => { @@ -846,7 +820,7 @@ function buildAdditionalModelRequestFields( thinking: { type: "enabled", budget_tokens: budget, - display, + ...(display !== undefined ? { display } : {}), }, }; })(); diff --git a/packages/ai/test/bedrock-thinking-payload.test.ts b/packages/ai/test/bedrock-thinking-payload.test.ts index a6c5b338..71ed9a70 100644 --- a/packages/ai/test/bedrock-thinking-payload.test.ts +++ b/packages/ai/test/bedrock-thinking-payload.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it } from "vitest"; import { getModel } from "../src/models.js"; -import { streamBedrock } from "../src/providers/amazon-bedrock.js"; -import type { Context, Model, SimpleStreamOptions } from "../src/types.js"; +import { type BedrockOptions, streamBedrock } from "../src/providers/amazon-bedrock.js"; +import type { Context, Model } from "../src/types.js"; interface BedrockThinkingPayload { additionalModelRequestFields?: { @@ -19,7 +19,7 @@ function makeContext(): Context { async function capturePayload( model: Model<"bedrock-converse-stream">, - options?: SimpleStreamOptions, + options?: BedrockOptions, ): Promise { let capturedPayload: BedrockThinkingPayload | undefined; const s = streamBedrock(model, makeContext(), { @@ -75,4 +75,33 @@ describe("Bedrock thinking payload", () => { 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(); + }); });