From 74556a25c7aeed357e7d274b75e2e2f778132a80 Mon Sep 17 00:00:00 2001 From: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Date: Mon, 26 Oct 2026 01:02:33 -0500 Subject: [PATCH] feat(ai): add typed Bedrock Converse options (#59171) --- packages/ai/src/protocols/bedrock-converse.ts | 139 ++++++++++++++-- packages/ai/src/providers/amazon-bedrock.ts | 48 +++--- ...trains-claude-output-to-a-json-schema.json | 33 ++++ ...ing-service-tier-and-request-metadata.json | 34 ++++ ...asoning-effort-with-the-priority-tier.json | 34 ++++ .../sends-nova-2-reasoning-effort.json | 34 ++++ .../provider-options/amazon-bedrock.types.ts | 33 ++++ .../bedrock-converse-options.recorded.test.ts | 119 ++++++++++++++ .../provider/bedrock-converse-options.test.ts | 149 ++++++++++++++++++ 9 files changed, 586 insertions(+), 37 deletions(-) create mode 100644 packages/ai/test/fixtures/recordings/bedrock-converse-options/constrains-claude-output-to-a-json-schema.json create mode 100644 packages/ai/test/fixtures/recordings/bedrock-converse-options/sends-claude-effort-with-adaptive-thinking-service-tier-and-request-metadata.json create mode 100644 packages/ai/test/fixtures/recordings/bedrock-converse-options/sends-gpt-oss-reasoning-effort-with-the-priority-tier.json create mode 100644 packages/ai/test/fixtures/recordings/bedrock-converse-options/sends-nova-2-reasoning-effort.json create mode 100644 packages/ai/test/provider-options/amazon-bedrock.types.ts create mode 100644 packages/ai/test/provider/bedrock-converse-options.recorded.test.ts create mode 100644 packages/ai/test/provider/bedrock-converse-options.test.ts diff --git a/packages/ai/src/protocols/bedrock-converse.ts b/packages/ai/src/protocols/bedrock-converse.ts index 01d4bd9dfdd..e86a77334ae 100644 --- a/packages/ai/src/protocols/bedrock-converse.ts +++ b/packages/ai/src/protocols/bedrock-converse.ts @@ -13,6 +13,7 @@ import { type LLMRequest, type LanguageModel, type ProviderMetadata, + type ProviderOptions, type ReasoningPart, type ToolCallPart, type ToolDefinition, @@ -24,7 +25,7 @@ import { JsonObject, optionalArray, ProviderShared } from "./shared.js" import { BedrockAuth } from "./utils/bedrock-auth.js" import { BedrockCache } from "./utils/bedrock-cache.js" import { BedrockMedia } from "./utils/bedrock-media.js" -import { supportsThinkingBlockBinding, THINKING_BINDING_BETA } from "./utils/claude-model.js" +import { claudeVersion, supportsThinkingBlockBinding, THINKING_BINDING_BETA } from "./utils/claude-model.js" import { Lifecycle } from "./utils/lifecycle.js" import { MistralToolID } from "./utils/mistral-tool-id.js" import { ToolStream } from "./utils/tool-stream.js" @@ -125,6 +126,12 @@ const BedrockToolChoice = Schema.Union([ Schema.Struct({ tool: Schema.Struct({ name: Schema.String }) }), ]) +// Value sets from the Converse API reference. Unknown values pass through for forward compatibility. +const ServiceTierType = ProviderShared.knownString<"priority" | "default" | "flex" | "reserved">() +const GuardrailTrace = ProviderShared.knownString<"enabled" | "disabled" | "enabled_full">() +const GuardrailStreamProcessingMode = ProviderShared.knownString<"sync" | "async">() +const TextFormatType = ProviderShared.knownString<"json_schema">() + const BedrockBodyFields = { modelId: Schema.String, messages: Schema.Array(BedrockMessage), @@ -144,6 +151,30 @@ const BedrockBodyFields = { }), ), additionalModelRequestFields: Schema.optional(JsonObject), + serviceTier: Schema.optional(Schema.Struct({ type: ServiceTierType })), + guardrailConfig: Schema.optional( + Schema.Struct({ + guardrailIdentifier: Schema.String, + guardrailVersion: Schema.String, + trace: Schema.optional(GuardrailTrace), + streamProcessingMode: Schema.optional(GuardrailStreamProcessingMode), + }), + ), + outputConfig: Schema.optional( + Schema.Struct({ + textFormat: Schema.Struct({ + type: TextFormatType, + structure: Schema.Struct({ + jsonSchema: Schema.Struct({ + schema: Schema.String, + name: Schema.optional(Schema.String), + description: Schema.optional(Schema.String), + }), + }), + }), + }), + ), + requestMetadata: Schema.optional(Schema.Record(Schema.String, Schema.String)), } const BedrockConverseBody = Schema.Struct(BedrockBodyFields) export type BedrockConverseBody = Schema.Schema.Type @@ -467,10 +498,57 @@ const isHighReasoningEffort = Schema.is( }), ) +// Claude 4.6+ pairs effort with adaptive thinking; earlier Claude models take effort beside a manual budget. +const supportsAdaptiveThinking = (model: LanguageModel) => { + const version = claudeVersion(model.id) + return version !== undefined && (version.major > 4 || (version.major === 4 && version.minor >= 6)) +} + +const effortFields = (model: LanguageModel, effort: string) => { + if (model.id.includes("anthropic")) return { output_config: { effort } } + // gpt-oss takes the flat chat-completions spelling; newer OpenAI and xAI models take the Responses one. + if (model.id.includes("openai.gpt-oss")) return { reasoning_effort: effort } + if (model.id.includes("openai.") || model.id.includes("xai.")) return { reasoning: { effort } } + return { reasoningConfig: { type: "enabled", maxReasoningEffort: effort } } +} + const Options = Schema.Struct({ - thinking: Schema.optional(Schema.Struct({ type: Schema.Literal("enabled"), budgetTokens: Schema.Number })), + /** Claude thinking. `adaptive` and `disabled` apply to Claude models only. */ + thinking: Schema.optional( + Schema.Union([ + Schema.Struct({ type: Schema.Literal("enabled"), budgetTokens: Schema.Number }), + Schema.Struct({ + type: Schema.Literal("adaptive"), + display: Schema.optional(ProviderShared.knownString<"summarized" | "omitted">()), + }), + Schema.Struct({ type: Schema.Literal("disabled") }), + ]), + ), + /** Reasoning effort, sent in the field each model family reads. */ + effort: Schema.optional(ProviderShared.knownString<"low" | "medium" | "high" | "xhigh" | "max">()), + serviceTier: Schema.optional(ServiceTierType), + /** Constrains the response text to a JSON schema. */ + outputFormat: Schema.optional( + Schema.Struct({ + type: TextFormatType, + schema: JsonObject, + name: Schema.optional(Schema.String), + description: Schema.optional(Schema.String), + }), + ), + guardrail: Schema.optional( + Schema.Struct({ + identifier: Schema.String, + version: Schema.String, + trace: Schema.optional(GuardrailTrace), + streamProcessingMode: Schema.optional(GuardrailStreamProcessingMode), + }), + ), + /** Key-value pairs for filtering invocation logs. */ + requestMetadata: Schema.optional(Schema.Record(Schema.String, Schema.String)), }) -export type OptionsInput = typeof Options.Type +/** Caller-facing provider options; unknown keys are accepted and ignored. */ +export type OptionsInput = ProviderOptions & typeof Options.Encoded const decodeOptions = ProviderShared.validateWith(Schema.decodeUnknownEffect(Options)) // Claude on Bedrock requires the thinking budget below `maxTokens`, with a minimum of 1,024. const MIN_THINKING_BUDGET = 1_024 @@ -486,7 +564,7 @@ const isThinkingDisabled = Schema.is( // Claude 5.1+ binds each thinking signature to the prefix above it. Ask Bedrock to drop the affected blocks instead of // failing when that prefix changes. `http.body` overlays this field by field, so callers can still override it. const applyThinkingBindingDefault = (request: LLMRequest, thinking: Readonly> | undefined) => { - if (isThinkingDisabled(request.http?.body)) return thinking + if (thinking?.type === "disabled" || isThinkingDisabled(request.http?.body)) return thinking if (!supportsThinkingBlockBinding(request.model)) return thinking return { ...(thinking ?? { type: "adaptive" as const }), @@ -500,20 +578,32 @@ const fromRequest = Effect.fn("BedrockConverse.fromRequest")(function* (request: const generation = request.generation const options = yield* decodeOptions(request.providerOptions ?? {}) const maxTokens = - isNova2(request.model) && isHighReasoningEffort(request.http?.body) ? undefined : generation?.maxTokens + isNova2(request.model) && (options.effort === "high" || isHighReasoningEffort(request.http?.body)) + ? undefined + : generation?.maxTokens const thinking = applyThinkingBindingDefault( request, - options.thinking === undefined - ? undefined - : { + (() => { + if (options.thinking?.type === "enabled") + return { type: "enabled", budget_tokens: ProviderShared.fitThinkingBudget( options.thinking.budgetTokens, maxTokens, MIN_THINKING_BUDGET, ), - }, + } + if (options.thinking !== undefined) return options.thinking + if (options.effort !== undefined && supportsAdaptiveThinking(request.model)) return { type: "adaptive" } + })(), ) + const additionalModelRequestFields = { + ...(generation?.topK === undefined ? {} : { top_k: generation.topK }), + ...(thinking === undefined ? {} : { thinking }), + ...(options.effort === undefined ? {} : effortFields(request.model, options.effort)), + // Converse takes Anthropic betas in the body, and Bedrock rejects `block_binding` without this one. + ...(thinking?.block_binding === undefined ? {} : { anthropic_beta: [THINKING_BINDING_BETA] }), + } // Bedrock-Claude shares Anthropic's 4-breakpoint cap. Spend the budget in // tools → system → messages order to favour the highest-impact prefixes. const breakpoints = BedrockCache.breakpoints(request.model) @@ -554,17 +644,36 @@ const fromRequest = Effect.fn("BedrockConverse.fromRequest")(function* (request: system, inferenceConfig, toolConfig, - // Converse's base inferenceConfig has no topK or thinking; Anthropic/Nova accept them + // Converse's base inferenceConfig has no topK, thinking or effort; models accept them // as model-specific fields, so they go through additionalModelRequestFields. additionalModelRequestFields: - generation?.topK === undefined && thinking === undefined + Object.keys(additionalModelRequestFields).length === 0 ? undefined : additionalModelRequestFields, + serviceTier: options.serviceTier === undefined ? undefined : { type: options.serviceTier }, + guardrailConfig: + options.guardrail === undefined ? undefined : { - ...(generation?.topK === undefined ? {} : { top_k: generation.topK }), - ...(thinking === undefined ? {} : { thinking }), - // Converse takes Anthropic betas in the body, and Bedrock rejects `block_binding` without this one. - ...(thinking?.block_binding === undefined ? {} : { anthropic_beta: [THINKING_BINDING_BETA] }), + guardrailIdentifier: options.guardrail.identifier, + guardrailVersion: options.guardrail.version, + trace: options.guardrail.trace, + streamProcessingMode: options.guardrail.streamProcessingMode, }, + outputConfig: + options.outputFormat === undefined + ? undefined + : { + textFormat: { + type: options.outputFormat.type, + structure: { + jsonSchema: { + schema: ProviderShared.encodeJson(options.outputFormat.schema), + name: options.outputFormat.name, + description: options.outputFormat.description, + }, + }, + }, + }, + requestMetadata: options.requestMetadata, } }) diff --git a/packages/ai/src/providers/amazon-bedrock.ts b/packages/ai/src/providers/amazon-bedrock.ts index 5f35fc4f33d..4d7ea859533 100644 --- a/packages/ai/src/providers/amazon-bedrock.ts +++ b/packages/ai/src/providers/amazon-bedrock.ts @@ -21,18 +21,19 @@ export type Config = RouteDefaultsInput & { readonly region?: string /** Override the computed `https://bedrock-runtime..amazonaws.com` URL. */ readonly baseURL?: string + readonly providerOptions?: BedrockConverse.OptionsInput } -export interface Settings extends ProviderPackage.Settings { - readonly apiKey?: string - readonly auth?: "bearer" | "sigv4" - readonly baseURL?: string - readonly credentials?: BedrockCredentials - readonly profile?: string - readonly region?: string - readonly topP?: number - readonly thinking?: BedrockConverse.OptionsInput["thinking"] -} +export type Settings = ProviderPackage.Settings & + BedrockConverse.OptionsInput & { + readonly apiKey?: string + readonly auth?: "bearer" | "sigv4" + readonly baseURL?: string + readonly credentials?: BedrockCredentials + readonly profile?: string + readonly region?: string + readonly topP?: number + } export const routes = [BedrockConverse.route] const bedrockBaseURL = (region: string) => `https://bedrock-runtime.${region}.amazonaws.com` @@ -57,22 +58,25 @@ export const configure = (input: Config = {}) => { const route = configuredRoute(input) return { id, - model: (modelID: string | ModelID) => route.model({ id: modelID }), + model: (modelID: string | ModelID) => route.model({ id: modelID }), configure, } } export const provider = configure() -export const model: ProviderPackage.Definition["model"] = (modelID, settings) => +export const model: ProviderPackage.Definition["model"] = ( + modelID, + { apiKey, auth, baseURL, credentials, profile, region, topP, headers, body, ...providerOptions }, +) => configure({ - apiKey: settings.apiKey, - auth: settings.auth, - baseURL: settings.baseURL, - credentials: settings.credentials, - generation: settings.topP === undefined ? undefined : { topP: settings.topP }, - headers: settings.headers === undefined ? undefined : { ...settings.headers }, - http: settings.body === undefined ? undefined : { body: { ...settings.body } }, - providerOptions: settings.thinking === undefined ? undefined : { thinking: settings.thinking }, - profile: settings.profile, - region: settings.region, + apiKey, + auth, + baseURL, + credentials, + generation: topP === undefined ? undefined : { topP }, + headers: headers === undefined ? undefined : { ...headers }, + http: body === undefined ? undefined : { body: { ...body } }, + providerOptions, + profile, + region, }).model(modelID) diff --git a/packages/ai/test/fixtures/recordings/bedrock-converse-options/constrains-claude-output-to-a-json-schema.json b/packages/ai/test/fixtures/recordings/bedrock-converse-options/constrains-claude-output-to-a-json-schema.json new file mode 100644 index 00000000000..bab0d590543 --- /dev/null +++ b/packages/ai/test/fixtures/recordings/bedrock-converse-options/constrains-claude-output-to-a-json-schema.json @@ -0,0 +1,33 @@ +{ + "version": 1, + "metadata": { + "tags": [ + "prefix:bedrock-converse-options", + "provider:amazon-bedrock", + "protocol:bedrock-converse" + ], + "name": "bedrock-converse-options/constrains-claude-output-to-a-json-schema", + "recordedAt": "2026-10-26T05:24:46.975Z" + }, + "interactions": [ + { + "transport": "http", + "request": { + "method": "POST", + "url": "https://bedrock-runtime.us-east-1.amazonaws.com/model/us.anthropic.claude-sonnet-4-6/converse-stream", + "headers": { + "content-type": "application/json" + }, + "body": "{\"modelId\":\"us.anthropic.claude-sonnet-4-6\",\"messages\":[{\"role\":\"user\",\"content\":[{\"text\":\"What is 17*23?\"}]}],\"inferenceConfig\":{\"maxTokens\":200},\"outputConfig\":{\"textFormat\":{\"type\":\"json_schema\",\"structure\":{\"jsonSchema\":{\"schema\":\"{\\\"type\\\":\\\"object\\\",\\\"properties\\\":{\\\"product\\\":{\\\"type\\\":\\\"integer\\\"}},\\\"required\\\":[\\\"product\\\"],\\\"additionalProperties\\\":false}\",\"name\":\"answer\"}}}}}" + }, + "response": { + "status": 200, + "headers": { + "content-type": "application/vnd.amazon.eventstream" + }, + "body": "AAAArQAAAFJo8LSWCzpldmVudC10eXBlBwAMbWVzc2FnZVN0YXJ0DTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsicCI6ImFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6QUJDREVGR0hJSktMTU5PUFFSU1RVViIsInJvbGUiOiJhc3Npc3RhbnQiff/V9LIAAADEAAAAV4y4VSULOmV2ZW50LXR5cGUHABFjb250ZW50QmxvY2tEZWx0YQ06Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImNvbnRlbnRCbG9ja0luZGV4IjowLCJkZWx0YSI6eyJ0ZXh0Ijoie1wiIn0sInAiOiJhYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ekFCQ0RFRkdISUpLTE1OIn1nS/0LAAAAuQAAAFeN+nFbCzpldmVudC10eXBlBwARY29udGVudEJsb2NrRGVsdGENOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJjb250ZW50QmxvY2tJbmRleCI6MCwiZGVsdGEiOnsidGV4dCI6InByb2R1Y3RcIjoifSwicCI6ImFiY2RlZmdoaWprbG1ub3BxcnN0dXYifabu2fMAAACmAAAAV29KcQgLOmV2ZW50LXR5cGUHABFjb250ZW50QmxvY2tEZWx0YQ06Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImNvbnRlbnRCbG9ja0luZGV4IjowLCJkZWx0YSI6eyJ0ZXh0IjoiMzkxIn0sInAiOiJhYmNkZWZnaGlqIn1V027xAAAAvwAAAFcCuoT7CzpldmVudC10eXBlBwARY29udGVudEJsb2NrRGVsdGENOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJjb250ZW50QmxvY2tJbmRleCI6MCwiZGVsdGEiOnsidGV4dCI6In0ifSwicCI6ImFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6QUJDREVGR0hJSksifT8Lk2QAAAC1AAAAVj8NrMwLOmV2ZW50LXR5cGUHABBjb250ZW50QmxvY2tTdG9wDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsiY29udGVudEJsb2NrSW5kZXgiOjAsInAiOiJhYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ekFCQ0RFRkdISUpLTE1OT1BRUlNUVVZXIn2NFJ+9AAAAoAAAAFEJaSGdCzpldmVudC10eXBlBwALbWVzc2FnZVN0b3ANOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJwIjoiYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXpBQkNERSIsInN0b3BSZWFzb24iOiJlbmRfdHVybiJ9p5BdEgAAANsAAABOCmP9tgs6ZXZlbnQtdHlwZQcACG1ldGFkYXRhDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsibWV0cmljcyI6eyJsYXRlbmN5TXMiOjEyMDh9LCJwIjoiYWJjZCIsInVzYWdlIjp7ImlucHV0VG9rZW5zIjoxNTUsIm91dHB1dFRva2VucyI6OCwic2VydmVyVG9vbFVzYWdlIjp7fSwidG90YWxUb2tlbnMiOjE2M319Zt/PuA==", + "bodyEncoding": "base64" + } + } + ] +} diff --git a/packages/ai/test/fixtures/recordings/bedrock-converse-options/sends-claude-effort-with-adaptive-thinking-service-tier-and-request-metadata.json b/packages/ai/test/fixtures/recordings/bedrock-converse-options/sends-claude-effort-with-adaptive-thinking-service-tier-and-request-metadata.json new file mode 100644 index 00000000000..95485a59b04 --- /dev/null +++ b/packages/ai/test/fixtures/recordings/bedrock-converse-options/sends-claude-effort-with-adaptive-thinking-service-tier-and-request-metadata.json @@ -0,0 +1,34 @@ +{ + "version": 1, + "metadata": { + "tags": [ + "prefix:bedrock-converse-options", + "provider:amazon-bedrock", + "protocol:bedrock-converse", + "reasoning" + ], + "name": "bedrock-converse-options/sends-claude-effort-with-adaptive-thinking-service-tier-and-request-metadata", + "recordedAt": "2026-10-26T05:24:42.553Z" + }, + "interactions": [ + { + "transport": "http", + "request": { + "method": "POST", + "url": "https://bedrock-runtime.us-east-1.amazonaws.com/model/us.anthropic.claude-sonnet-4-6/converse-stream", + "headers": { + "content-type": "application/json" + }, + "body": "{\"modelId\":\"us.anthropic.claude-sonnet-4-6\",\"messages\":[{\"role\":\"user\",\"content\":[{\"text\":\"How many primes are below 60? Reply with the number only.\"}]}],\"inferenceConfig\":{\"maxTokens\":4000},\"additionalModelRequestFields\":{\"thinking\":{\"type\":\"adaptive\",\"display\":\"summarized\"},\"output_config\":{\"effort\":\"high\"}},\"serviceTier\":{\"type\":\"default\"},\"requestMetadata\":{\"suite\":\"opencode-ai\"}}" + }, + "response": { + "status": 200, + "headers": { + "content-type": "application/vnd.amazon.eventstream" + }, + "body": "AAAAqwAAAFLnsEE2CzpldmVudC10eXBlBwAMbWVzc2FnZVN0YXJ0DTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsicCI6ImFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6QUJDREVGR0hJSktMTU5PUFFSU1QiLCJyb2xlIjoiYXNzaXN0YW50In16qcl8AAAAvgAAAFc/2q1LCzpldmVudC10eXBlBwARY29udGVudEJsb2NrRGVsdGENOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJjb250ZW50QmxvY2tJbmRleCI6MCwiZGVsdGEiOnsicmVhc29uaW5nQ29udGVudCI6eyJ0ZXh0IjoiUHIifX0sInAiOiJhYmNkZWZnaGlqa2xtbiJ9KsuSDAAAAPgAAABX6GkAogs6ZXZlbnQtdHlwZQcAEWNvbnRlbnRCbG9ja0RlbHRhDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsiY29udGVudEJsb2NrSW5kZXgiOjAsImRlbHRhIjp7InJlYXNvbmluZ0NvbnRlbnQiOnsidGV4dCI6ImltZXMgYmVsb3cgNjA6In19LCJwIjoiYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXpBQkNERUZHSElKS0xNTk9QUVJTVFVWV1hZWjAxMjM0NTY3In1+JGFAAAAAvAAAAFdFGv4rCzpldmVudC10eXBlBwARY29udGVudEJsb2NrRGVsdGENOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJjb250ZW50QmxvY2tJbmRleCI6MCwiZGVsdGEiOnsicmVhc29uaW5nQ29udGVudCI6eyJ0ZXh0IjoiXG4yIn19LCJwIjoiYWJjZGVmZ2hpamsifd/D1UcAAADmAAAAVze5KUELOmV2ZW50LXR5cGUHABFjb250ZW50QmxvY2tEZWx0YQ06Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImNvbnRlbnRCbG9ja0luZGV4IjowLCJkZWx0YSI6eyJyZWFzb25pbmdDb250ZW50Ijp7InRleHQiOiIsIDMsIDUsICJ9fSwicCI6ImFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6QUJDREVGR0hJSktMTU5PUFFSU1RVViJ94gGywwAAAO0AAABXQGkYUAs6ZXZlbnQtdHlwZQcAEWNvbnRlbnRCbG9ja0RlbHRhDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsiY29udGVudEJsb2NrSW5kZXgiOjAsImRlbHRhIjp7InJlYXNvbmluZ0NvbnRlbnQiOnsidGV4dCI6IjcsIDExLCAxMywifX0sInAiOiJhYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ekFCQ0RFRkdISUpLTE1OT1BRUlNUVVZXWFlaMCJ9HGS9oAAAAN0AAABX4Uig1gs6ZXZlbnQtdHlwZQcAEWNvbnRlbnRCbG9ja0RlbHRhDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsiY29udGVudEJsb2NrSW5kZXgiOjAsImRlbHRhIjp7InJlYXNvbmluZ0NvbnRlbnQiOnsidGV4dCI6IiAxNywgMTksIDIzIn19LCJwIjoiYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXpBQkNERUZHSElKIn1J+fHjAAAA0gAAAFdjGDcHCzpldmVudC10eXBlBwARY29udGVudEJsb2NrRGVsdGENOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJjb250ZW50QmxvY2tJbmRleCI6MCwiZGVsdGEiOnsicmVhc29uaW5nQ29udGVudCI6eyJ0ZXh0IjoiLCAyOSwgMzEsICJ9fSwicCI6ImFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6In3fWx7pAAAA2wAAAFduCFV2CzpldmVudC10eXBlBwARY29udGVudEJsb2NrRGVsdGENOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJjb250ZW50QmxvY2tJbmRleCI6MCwiZGVsdGEiOnsicmVhc29uaW5nQ29udGVudCI6eyJ0ZXh0IjoiMzcsIDQxLCA0MywifX0sInAiOiJhYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ekFCQ0RFRkdIIn3POmzBAAAA1AAAAFfsWMKnCzpldmVudC10eXBlBwARY29udGVudEJsb2NrRGVsdGENOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJjb250ZW50QmxvY2tJbmRleCI6MCwiZGVsdGEiOnsicmVhc29uaW5nQ29udGVudCI6eyJ0ZXh0IjoiIDQ3LCA1MywgNTkifX0sInAiOiJhYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ekEifX/dw1QAAADfAAAAV5uI87YLOmV2ZW50LXR5cGUHABFjb250ZW50QmxvY2tEZWx0YQ06Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImNvbnRlbnRCbG9ja0luZGV4IjowLCJkZWx0YSI6eyJyZWFzb25pbmdDb250ZW50Ijp7InRleHQiOiJcblxuVGhhdCJ9fSwicCI6ImFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6QUJDREVGR0hJSktMTU5PIn0nPZ3pAAAA0AAAAFcZ2GRnCzpldmVudC10eXBlBwARY29udGVudEJsb2NrRGVsdGENOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJjb250ZW50QmxvY2tJbmRleCI6MCwiZGVsdGEiOnsicmVhc29uaW5nQ29udGVudCI6eyJ0ZXh0IjoiJ3MgMTcifX0sInAiOiJhYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ekFCQyJ9ANoZdAAAANEAAABXJLhN1ws6ZXZlbnQtdHlwZQcAEWNvbnRlbnRCbG9ja0RlbHRhDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsiY29udGVudEJsb2NrSW5kZXgiOjAsImRlbHRhIjp7InJlYXNvbmluZ0NvbnRlbnQiOnsidGV4dCI6IiBwcmltZXMuIn19LCJwIjoiYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXpBIn3Hr86AAAAA0wAAAFdeeB63CzpldmVudC10eXBlBwARY29udGVudEJsb2NrRGVsdGENOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJjb250ZW50QmxvY2tJbmRleCI6MCwiZGVsdGEiOnsicmVhc29uaW5nQ29udGVudCI6eyJ0ZXh0IjoiIn19LCJwIjoiYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXpBQkNERUZHSElKSyJ99+quegAAApYAAABXg6NohQs6ZXZlbnQtdHlwZQcAEWNvbnRlbnRCbG9ja0RlbHRhDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsiY29udGVudEJsb2NrSW5kZXgiOjAsImRlbHRhIjp7InJlYXNvbmluZ0NvbnRlbnQiOnsic2lnbmF0dXJlIjoiRXNFQ0Nud0lFeEFCR0FJcVFKeE9CNGF5MjA0QWtxbTFEMnl3OG5CZXRTNzhKTEZIR0JVYzN0MGJoc205ZDVpRm9DY0ZsanI4OThhc2tQWGN4U2JyaklRUWMwS0V5SUdtd1FBemtqRXlFV05zWVhWa1pTMXpiMjV1WlhRdE5DMDJPQUJDQ0hSb2FXNXJhVzVuV2d3eU16STNOekU0TlRZM09ER29BWnJJKzlZR0VneWNVNy9ydnROZVdEYUtJbm9hREtQb0ZKQ1VCa3QwKzlFM0JTSXc4OUJSVDdJWVZTVkFtN3Voakg0Q1Z5TmJobFFkZmNLNHZqN2o1QTkwWWRzRytPd0pzU3ZtajNjUE1vQmhIMEV6S25NNm40MVhvek1acHJLRVFVejBxcmJCZVJJNlMvd0ludHc0bTNKNkUybUNkYktKUkN3SzJSS1owNFBKUnNEdUFRUDBnSTFnd1p0RWQzZzBNekNkZ3ZydlBvRmk4UG53ZlprMHp2UzVIVC85TGJOZlJodmFOckRpOVQyZy9EODkzQVljYlJwNDV3ays2RHFYczAwY3dsLzdMVHpZR0FFPSJ9fSwicCI6ImFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6QUJDREVGR0hJSktMTU5PUFFSU1RVIn0Kc31GAAAApgAAAFYYTUGeCzpldmVudC10eXBlBwAQY29udGVudEJsb2NrU3RvcA06Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImNvbnRlbnRCbG9ja0luZGV4IjowLCJwIjoiYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXpBQkNERUZHSCJ9XtkDNQAAAK4AAABXXzo6yQs6ZXZlbnQtdHlwZQcAEWNvbnRlbnRCbG9ja0RlbHRhDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsiY29udGVudEJsb2NrSW5kZXgiOjEsImRlbHRhIjp7InRleHQiOiIxNyJ9LCJwIjoiYWJjZGVmZ2hpamtsbW5vcHFycyJ9vjLcPQAAALIAAABWjS1w3As6ZXZlbnQtdHlwZQcAEGNvbnRlbnRCbG9ja1N0b3ANOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJjb250ZW50QmxvY2tJbmRleCI6MSwicCI6ImFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6QUJDREVGR0hJSktMTU5PUFFSU1Qifa+sENsAAACiAAAAUXOpcv0LOmV2ZW50LXR5cGUHAAttZXNzYWdlU3RvcA06Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7InAiOiJhYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ekFCQ0RFRkciLCJzdG9wUmVhc29uIjoiZW5kX3R1cm4ifbmQA7wAAAEgAAAATv7bT18LOmV2ZW50LXR5cGUHAAhtZXRhZGF0YQ06Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7Im1ldHJpY3MiOnsibGF0ZW5jeU1zIjoxMTYxfSwicCI6ImFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6QUJDREVGR0hJSktMTU4iLCJzZXJ2aWNlVGllciI6eyJ0eXBlIjoiZGVmYXVsdCJ9LCJ1c2FnZSI6eyJpbnB1dFRva2VucyI6MjIsIm91dHB1dFRva2VucyI6ODUsInNlcnZlclRvb2xVc2FnZSI6e30sInRvdGFsVG9rZW5zIjoxMDd9fc422g8=", + "bodyEncoding": "base64" + } + } + ] +} diff --git a/packages/ai/test/fixtures/recordings/bedrock-converse-options/sends-gpt-oss-reasoning-effort-with-the-priority-tier.json b/packages/ai/test/fixtures/recordings/bedrock-converse-options/sends-gpt-oss-reasoning-effort-with-the-priority-tier.json new file mode 100644 index 00000000000..229535d8e15 --- /dev/null +++ b/packages/ai/test/fixtures/recordings/bedrock-converse-options/sends-gpt-oss-reasoning-effort-with-the-priority-tier.json @@ -0,0 +1,34 @@ +{ + "version": 1, + "metadata": { + "tags": [ + "prefix:bedrock-converse-options", + "provider:amazon-bedrock", + "protocol:bedrock-converse", + "reasoning" + ], + "name": "bedrock-converse-options/sends-gpt-oss-reasoning-effort-with-the-priority-tier", + "recordedAt": "2026-10-26T05:24:44.284Z" + }, + "interactions": [ + { + "transport": "http", + "request": { + "method": "POST", + "url": "https://bedrock-runtime.us-east-1.amazonaws.com/model/openai.gpt-oss-120b-1%3A0/converse-stream", + "headers": { + "content-type": "application/json" + }, + "body": "{\"modelId\":\"openai.gpt-oss-120b-1:0\",\"messages\":[{\"role\":\"user\",\"content\":[{\"text\":\"What is 17*23? Reply with the number only.\"}]}],\"inferenceConfig\":{\"maxTokens\":1000},\"additionalModelRequestFields\":{\"reasoning_effort\":\"low\"},\"serviceTier\":{\"type\":\"priority\"},\"requestMetadata\":{\"suite\":\"opencode-ai\"}}" + }, + "response": { + "status": 200, + "headers": { + "content-type": "application/vnd.amazon.eventstream" + }, + "body": "AAAAnQAAAFLJ0QwQCzpldmVudC10eXBlBwAMbWVzc2FnZVN0YXJ0DTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsicCI6ImFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6QUJDREVGIiwicm9sZSI6ImFzc2lzdGFudCJ9EQf77QAAAMkAAABXdCiRlAs6ZXZlbnQtdHlwZQcAEWNvbnRlbnRCbG9ja0RlbHRhDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsiY29udGVudEJsb2NrSW5kZXgiOjAsImRlbHRhIjp7InRleHQiOiIifSwicCI6ImFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6QUJDREVGR0hJSktMTU5PUFFSU1RVViJ9oXumJQAAAMAAAABWDj/Dcws6ZXZlbnQtdHlwZQcAEGNvbnRlbnRCbG9ja1N0b3ANOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJjb250ZW50QmxvY2tJbmRleCI6MCwicCI6ImFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6QUJDREVGR0hJSktMTU5PUFFSU1RVVldYWVowMTIzNDU2NyJ9EPTSwQAAAMkAAABXdCiRlAs6ZXZlbnQtdHlwZQcAEWNvbnRlbnRCbG9ja0RlbHRhDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsiY29udGVudEJsb2NrSW5kZXgiOjEsImRlbHRhIjp7InJlYXNvbmluZ0NvbnRlbnQiOnsidGV4dCI6Ikp1c3QgY29tcHV0ZSAxNyoyMyA9IDM5MS4ifX0sInAiOiJhYiJ9u2fddQAAALcAAABWRc3/rAs6ZXZlbnQtdHlwZQcAEGNvbnRlbnRCbG9ja1N0b3ANOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJjb250ZW50QmxvY2tJbmRleCI6MSwicCI6ImFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6QUJDREVGR0hJSktMTU5PUFFSU1RVVldYWSJ9hmJaHgAAAKMAAABXp6r+eAs6ZXZlbnQtdHlwZQcAEWNvbnRlbnRCbG9ja0RlbHRhDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsiY29udGVudEJsb2NrSW5kZXgiOjIsImRlbHRhIjp7InRleHQiOiIzOTEifSwicCI6ImFiY2RlZmcifT1TPPUAAADBAAAAVjNf6sMLOmV2ZW50LXR5cGUHABBjb250ZW50QmxvY2tTdG9wDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsiY29udGVudEJsb2NrSW5kZXgiOjIsInAiOiJhYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ekFCQ0RFRkdISUpLTE1OT1BRUlNUVVZXWFlaMDEyMzQ1Njc4In0WfQjAAAAAnwAAAFEqGA7KCzpldmVudC10eXBlBwALbWVzc2FnZVN0b3ANOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJwIjoiYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXpBQkNEIiwic3RvcFJlYXNvbiI6ImVuZF90dXJuIn2rNcIbAAABIQAAAE7Du2bvCzpldmVudC10eXBlBwAIbWV0YWRhdGENOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJtZXRyaWNzIjp7ImxhdGVuY3lNcyI6MTU3NX0sInAiOiJhYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ekFCQ0RFRkdISUpLTE1OIiwic2VydmljZVRpZXIiOnsidHlwZSI6InByaW9yaXR5In0sInVzYWdlIjp7ImlucHV0VG9rZW5zIjo4MCwib3V0cHV0VG9rZW5zIjoyMSwic2VydmVyVG9vbFVzYWdlIjp7fSwidG90YWxUb2tlbnMiOjEwMX19od/llw==", + "bodyEncoding": "base64" + } + } + ] +} diff --git a/packages/ai/test/fixtures/recordings/bedrock-converse-options/sends-nova-2-reasoning-effort.json b/packages/ai/test/fixtures/recordings/bedrock-converse-options/sends-nova-2-reasoning-effort.json new file mode 100644 index 00000000000..7d775fa5ac5 --- /dev/null +++ b/packages/ai/test/fixtures/recordings/bedrock-converse-options/sends-nova-2-reasoning-effort.json @@ -0,0 +1,34 @@ +{ + "version": 1, + "metadata": { + "tags": [ + "prefix:bedrock-converse-options", + "provider:amazon-bedrock", + "protocol:bedrock-converse", + "reasoning" + ], + "name": "bedrock-converse-options/sends-nova-2-reasoning-effort", + "recordedAt": "2026-10-26T05:24:45.686Z" + }, + "interactions": [ + { + "transport": "http", + "request": { + "method": "POST", + "url": "https://bedrock-runtime.us-east-1.amazonaws.com/model/us.amazon.nova-2-lite-v1%3A0/converse-stream", + "headers": { + "content-type": "application/json" + }, + "body": "{\"modelId\":\"us.amazon.nova-2-lite-v1:0\",\"messages\":[{\"role\":\"user\",\"content\":[{\"text\":\"What is 17*23? Reply with the number only.\"}]}],\"inferenceConfig\":{\"maxTokens\":1000},\"additionalModelRequestFields\":{\"reasoningConfig\":{\"type\":\"enabled\",\"maxReasoningEffort\":\"low\"}}}" + }, + "response": { + "status": 200, + "headers": { + "content-type": "application/vnd.amazon.eventstream" + }, + "body": "AAAAmAAAAFIBMYNgCzpldmVudC10eXBlBwAMbWVzc2FnZVN0YXJ0DTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsicCI6ImFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6QSIsInJvbGUiOiJhc3Npc3RhbnQifRK9PwwAAADaAAAAV1NofMYLOmV2ZW50LXR5cGUHABFjb250ZW50QmxvY2tEZWx0YQ06Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImNvbnRlbnRCbG9ja0luZGV4IjowLCJkZWx0YSI6eyJyZWFzb25pbmdDb250ZW50Ijp7InRleHQiOiJbUkVEQUNURURdIn19LCJwIjoiYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXpBQkNERUZHSCJ966snJwAAAM8AAABX+2hkNAs6ZXZlbnQtdHlwZQcAEWNvbnRlbnRCbG9ja0RlbHRhDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsiY29udGVudEJsb2NrSW5kZXgiOjAsImRlbHRhIjp7InJlYXNvbmluZ0NvbnRlbnQiOnsidGV4dCI6Ii4ifX0sInAiOiJhYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ekFCQ0RFRiJ9txtPJQAAAIkAAABWW9z5Sws6ZXZlbnQtdHlwZQcAEGNvbnRlbnRCbG9ja1N0b3ANOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJjb250ZW50QmxvY2tJbmRleCI6MCwicCI6ImFiY2RlIn3nHPCvAAAApwAAAFdSKli4CzpldmVudC10eXBlBwARY29udGVudEJsb2NrRGVsdGENOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJjb250ZW50QmxvY2tJbmRleCI6MSwiZGVsdGEiOnsidGV4dCI6IjM5MSJ9LCJwIjoiYWJjZGVmZ2hpamsifVFEEycAAACHAAAAVuTsRyoLOmV2ZW50LXR5cGUHABBjb250ZW50QmxvY2tTdG9wDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsiY29udGVudEJsb2NrSW5kZXgiOjEsInAiOiJhYmMifVAMp9gAAACjAAAAUU7JW00LOmV2ZW50LXR5cGUHAAttZXNzYWdlU3RvcA06Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7InAiOiJhYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ekFCQ0RFRkdIIiwic3RvcFJlYXNvbiI6ImVuZF90dXJuIn3wGF+JAAABCwAAAE5IylFKCzpldmVudC10eXBlBwAIbWV0YWRhdGENOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJtZXRyaWNzIjp7ImxhdGVuY3lNcyI6MTMzMn0sInAiOiJhYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ekFCQ0RFRkdISUpLTE1OT1BRUlNUVVZXWCIsInVzYWdlIjp7ImlucHV0VG9rZW5zIjoxMDksIm91dHB1dFRva2VucyI6MjQ2LCJzZXJ2ZXJUb29sVXNhZ2UiOnt9LCJ0b3RhbFRva2VucyI6MzU1fX3ERMiI", + "bodyEncoding": "base64" + } + } + ] +} diff --git a/packages/ai/test/provider-options/amazon-bedrock.types.ts b/packages/ai/test/provider-options/amazon-bedrock.types.ts new file mode 100644 index 00000000000..1c40584e7b7 --- /dev/null +++ b/packages/ai/test/provider-options/amazon-bedrock.types.ts @@ -0,0 +1,33 @@ +import { LLM } from "../../src/effect.js" +import { AmazonBedrock } from "../../src/providers.js" + +const model = AmazonBedrock.provider.model("us.anthropic.claude-sonnet-4-6") + +LLM.request({ + model, + prompt: "Hello", + providerOptions: { + effort: "future-effort", + serviceTier: "future-tier", + thinking: { type: "adaptive", display: "future-display" }, + guardrail: { identifier: "abc123", version: "DRAFT", trace: "future-trace", streamProcessingMode: "future-mode" }, + outputFormat: { type: "future-format", schema: { type: "object" } }, + requestMetadata: { suite: "opencode-ai" }, + }, +}) + +AmazonBedrock.model("us.amazon.nova-2-lite-v1:0", { effort: "low", serviceTier: "flex" }) + +LLM.request({ + model, + prompt: "Hello", + // @ts-expect-error Bedrock thinking modes are a fixed union. + providerOptions: { thinking: { type: "automatic" } }, +}) + +LLM.request({ + model, + prompt: "Hello", + // @ts-expect-error Converse request metadata values are strings. + providerOptions: { requestMetadata: { attempt: 1 } }, +}) diff --git a/packages/ai/test/provider/bedrock-converse-options.recorded.test.ts b/packages/ai/test/provider/bedrock-converse-options.recorded.test.ts new file mode 100644 index 00000000000..20ecd3ff606 --- /dev/null +++ b/packages/ai/test/provider/bedrock-converse-options.recorded.test.ts @@ -0,0 +1,119 @@ +import { describe, expect } from "bun:test" +import { Effect } from "effect" +import { LLM } from "../../src/effect.js" +import { LLMClient } from "../../src/route.js" +import { AmazonBedrock } from "../../src/providers.js" +import { recordedTests } from "../recorded-test.js" + +const RECORDING_REGION = process.env.BEDROCK_RECORDING_REGION ?? "us-east-1" + +const bedrock = (id: string, settings: Omit[1], "apiKey" | "region">) => + AmazonBedrock.model(id, { + apiKey: process.env.AWS_BEARER_TOKEN_BEDROCK ?? "fixture", + region: RECORDING_REGION, + ...settings, + }) + +const recorded = recordedTests({ + prefix: "bedrock-converse-options", + provider: "amazon-bedrock", + protocol: "bedrock-converse", + requires: ["AWS_BEARER_TOKEN_BEDROCK"], +}) + +describe("Bedrock Converse options recorded", () => { + // Claude on Bedrock only accepts the default tier; other tiers fail with "not supported for this model". + recorded.effect.with( + "sends Claude effort with adaptive thinking, service tier and request metadata", + { tags: ["reasoning"] }, + () => + Effect.gen(function* () { + const response = yield* LLMClient.generate( + LLM.request({ + id: "recorded_bedrock_options_claude", + model: bedrock("us.anthropic.claude-sonnet-4-6", { + effort: "high", + thinking: { type: "adaptive", display: "summarized" }, + serviceTier: "default", + requestMetadata: { suite: "opencode-ai" }, + }), + prompt: "How many primes are below 60? Reply with the number only.", + cache: "none", + generation: { maxTokens: 4_000 }, + }), + ) + + expect(response.finishReason?.normalized).toBe("stop") + expect(response.reasoning.length).toBeGreaterThan(0) + expect(response.text).toContain("17") + }), + ) + + recorded.effect.with("sends gpt-oss reasoning effort with the priority tier", { tags: ["reasoning"] }, () => + Effect.gen(function* () { + const response = yield* LLMClient.generate( + LLM.request({ + id: "recorded_bedrock_options_gpt_oss", + model: bedrock("openai.gpt-oss-120b-1:0", { + effort: "low", + serviceTier: "priority", + requestMetadata: { suite: "opencode-ai" }, + }), + prompt: "What is 17*23? Reply with the number only.", + cache: "none", + generation: { maxTokens: 1_000 }, + }), + ) + + expect(response.finishReason?.normalized).toBe("stop") + expect(response.reasoning.length).toBeGreaterThan(0) + expect(response.text).toContain("391") + }), + ) + + recorded.effect.with("sends Nova 2 reasoning effort", { tags: ["reasoning"] }, () => + Effect.gen(function* () { + const response = yield* LLMClient.generate( + LLM.request({ + id: "recorded_bedrock_options_nova", + model: bedrock("us.amazon.nova-2-lite-v1:0", { effort: "low" }), + prompt: "What is 17*23? Reply with the number only.", + cache: "none", + generation: { maxTokens: 1_000 }, + }), + ) + + expect(response.finishReason?.normalized).toBe("stop") + expect(response.reasoning.length).toBeGreaterThan(0) + expect(response.text).toContain("391") + }), + ) + + recorded.effect("constrains Claude output to a JSON schema", () => + Effect.gen(function* () { + const response = yield* LLMClient.generate( + LLM.request({ + id: "recorded_bedrock_options_output_format", + model: bedrock("us.anthropic.claude-sonnet-4-6", { + outputFormat: { + type: "json_schema", + name: "answer", + schema: { + type: "object", + properties: { product: { type: "integer" } }, + required: ["product"], + additionalProperties: false, + }, + }, + }), + prompt: "What is 17*23?", + cache: "none", + generation: { maxTokens: 200 }, + }), + ) + + expect(response.finishReason?.normalized).toBe("stop") + expect(JSON.parse(response.text)).toEqual({ product: 391 }) + }), + ) +}) diff --git a/packages/ai/test/provider/bedrock-converse-options.test.ts b/packages/ai/test/provider/bedrock-converse-options.test.ts new file mode 100644 index 00000000000..dfde0b8f6fd --- /dev/null +++ b/packages/ai/test/provider/bedrock-converse-options.test.ts @@ -0,0 +1,149 @@ +import { describe, expect } from "bun:test" +import { Effect } from "effect" +import { LLM } from "../../src/effect.js" +import { LLMClient } from "../../src/route.js" +import { compileRequest } from "../../src/route/client.js" +import { AmazonBedrock } from "../../src/providers.js" +import { it } from "../lib/effect.js" +import { dynamicResponse } from "../lib/http.js" + +const bedrock = (id: string, settings: Omit[1], "apiKey" | "baseURL">) => + AmazonBedrock.model(id, { baseURL: "https://bedrock-runtime.test", apiKey: "test-bearer", ...settings }) + +const request = (model: ReturnType) => + LLM.request({ model, prompt: "Say hello.", cache: "none", generation: { maxTokens: 64 } }) + +const body = (model: ReturnType) => + compileRequest(request(model)).pipe(Effect.map((prepared) => prepared.body)) + +const fields = (model: ReturnType) => + body(model).pipe(Effect.map((prepared) => prepared.additionalModelRequestFields)) + +const binding = { prefix_mismatch_behavior: "drop_block" } +const beta = ["thinking-binding-controls-2026-08-01"] + +describe("Bedrock Converse options", () => { + it.effect("maps effort to each model family's field", () => + Effect.gen(function* () { + expect(yield* fields(bedrock("us.anthropic.claude-sonnet-4-6", { effort: "high" }))).toEqual({ + thinking: { type: "adaptive" }, + output_config: { effort: "high" }, + }) + expect(yield* fields(bedrock("global.anthropic.claude-opus-5-5", { effort: "high" }))).toEqual({ + thinking: { type: "adaptive", block_binding: binding }, + output_config: { effort: "high" }, + anthropic_beta: beta, + }) + expect(yield* fields(bedrock("openai.gpt-oss-120b-1:0", { effort: "low" }))).toEqual({ reasoning_effort: "low" }) + expect(yield* fields(bedrock("openai.gpt-5.6", { effort: "low" }))).toEqual({ reasoning: { effort: "low" } }) + expect(yield* fields(bedrock("us.xai.grok-4.6", { effort: "low" }))).toEqual({ reasoning: { effort: "low" } }) + expect(yield* fields(bedrock("us.amazon.nova-2-lite-v1:0", { effort: "medium" }))).toEqual({ + reasoningConfig: { type: "enabled", maxReasoningEffort: "medium" }, + }) + }), + ) + + for (const id of ["us.anthropic.claude-opus-4-5-20251101-v1:0", "us.anthropic.claude-3-7-sonnet-20250219-v1:0"]) + it.effect(`keeps ${id} effort beside its manual thinking budget`, () => + Effect.gen(function* () { + expect(yield* fields(bedrock(id, { effort: "high" }))).toEqual({ output_config: { effort: "high" } }) + expect( + yield* fields(bedrock(id, { effort: "high", thinking: { type: "enabled", budgetTokens: 1_024 } })), + ).toEqual({ thinking: { type: "enabled", budget_tokens: 1_024 }, output_config: { effort: "high" } }) + }), + ) + + it.effect("sends explicit adaptive and disabled Claude thinking", () => + Effect.gen(function* () { + expect( + yield* fields( + bedrock("us.anthropic.claude-sonnet-4-6", { + effort: "low", + thinking: { type: "adaptive", display: "summarized" }, + }), + ), + ).toEqual({ thinking: { type: "adaptive", display: "summarized" }, output_config: { effort: "low" } }) + expect(yield* fields(bedrock("global.anthropic.claude-opus-5-5", { thinking: { type: "disabled" } }))).toEqual({ + thinking: { type: "disabled" }, + }) + }), + ) + + it.effect("omits maxTokens for Nova 2 at high effort", () => + Effect.gen(function* () { + expect((yield* body(bedrock("us.amazon.nova-2-lite-v1:0", { effort: "high" }))).inferenceConfig).toBeUndefined() + }), + ) + + it.effect("sends service tier, guardrail, structured output and request metadata", () => + Effect.gen(function* () { + const prepared = yield* body( + bedrock("us.anthropic.claude-sonnet-4-6", { + serviceTier: "priority", + guardrail: { identifier: "abc123", version: "1", trace: "enabled", streamProcessingMode: "async" }, + outputFormat: { + type: "json_schema", + name: "answer", + schema: { type: "object", properties: { product: { type: "integer" } } }, + }, + requestMetadata: { suite: "opencode-ai" }, + }), + ) + + expect(prepared).toEqual({ + modelId: "us.anthropic.claude-sonnet-4-6", + messages: [{ role: "user", content: [{ text: "Say hello." }] }], + inferenceConfig: { maxTokens: 64 }, + serviceTier: { type: "priority" }, + guardrailConfig: { + guardrailIdentifier: "abc123", + guardrailVersion: "1", + trace: "enabled", + streamProcessingMode: "async", + }, + outputConfig: { + textFormat: { + type: "json_schema", + structure: { + jsonSchema: { + name: "answer", + schema: '{"type":"object","properties":{"product":{"type":"integer"}}}', + }, + }, + }, + }, + requestMetadata: { suite: "opencode-ai" }, + }) + }), + ) + + it.effect("lets http.body override typed options field by field", () => + Effect.gen(function* () { + const sent: string[] = [] + yield* LLMClient.generate( + request( + bedrock("us.anthropic.claude-sonnet-4-6", { + effort: "low", + serviceTier: "priority", + body: { additionalModelRequestFields: { output_config: { effort: "max" } }, serviceTier: { type: "flex" } }, + }), + ), + ).pipe( + Effect.provide( + dynamicResponse((input) => + Effect.sync(() => { + sent.push(input.text) + return input.respond("{}", { status: 400 }) + }), + ), + ), + Effect.ignore, + ) + + expect(JSON.parse(sent[0]!)).toMatchObject({ + additionalModelRequestFields: { thinking: { type: "adaptive" }, output_config: { effort: "max" } }, + serviceTier: { type: "flex" }, + }) + }), + ) +})