diff --git a/packages/ai/src/protocols/bedrock-converse.ts b/packages/ai/src/protocols/bedrock-converse.ts index 5b30c7fb342..08c9c2ce03b 100644 --- a/packages/ai/src/protocols/bedrock-converse.ts +++ b/packages/ai/src/protocols/bedrock-converse.ts @@ -430,10 +430,23 @@ const lowerSystem = (breakpoints: BedrockCache.Breakpoints, system: ReadonlyArra return content.length === 0 ? undefined : content } +// Nova 2 rejects `maxTokens` at high reasoning effort, where its output can exceed the field's maximum. Other models +// that take `reasoningConfig`, such as Grok on Bedrock, accept it. +const isNova2 = (model: LanguageModel) => /\bamazon\.nova-2-/.test(model.id) +const isHighReasoningEffort = Schema.is( + Schema.Struct({ + additionalModelRequestFields: Schema.Struct({ + reasoningConfig: Schema.Struct({ maxReasoningEffort: Schema.Literal("high") }), + }), + }), +) + const fromRequest = Effect.fn("BedrockConverse.fromRequest")(function* (request: LLMRequest) { const toolChoice = request.toolChoice ? yield* lowerToolChoice(request.toolChoice) : undefined const flattened = ProviderShared.flattenToolRequest(request) const generation = request.generation + const maxTokens = + isNova2(request.model) && isHighReasoningEffort(request.http?.body) ? undefined : generation?.maxTokens // 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.id) @@ -455,14 +468,14 @@ const fromRequest = Effect.fn("BedrockConverse.fromRequest")(function* (request: } const inferenceConfig = (() => { if ( - generation?.maxTokens === undefined && + maxTokens === undefined && generation?.temperature === undefined && generation?.topP === undefined && (generation?.stop === undefined || generation.stop.length === 0) ) return undefined return { - maxTokens: generation?.maxTokens, + maxTokens, temperature: generation?.temperature, topP: generation?.topP, stopSequences: generation?.stop, diff --git a/packages/ai/test/provider/bedrock-converse.test.ts b/packages/ai/test/provider/bedrock-converse.test.ts index d643a9b627e..77ca3422b88 100644 --- a/packages/ai/test/provider/bedrock-converse.test.ts +++ b/packages/ai/test/provider/bedrock-converse.test.ts @@ -225,6 +225,25 @@ describe("Bedrock Converse route", () => { }), ) + it.effect("omits maxTokens only for Nova 2 at high reasoning effort", () => + Effect.gen(function* () { + const inferenceConfig = (modelID: string, maxReasoningEffort: string) => + compileRequest( + LLMRequest.update(baseRequest, { + model: AmazonBedrock.model(modelID, { + baseURL: "https://bedrock-runtime.test", + apiKey: "test-bearer", + body: { additionalModelRequestFields: { reasoningConfig: { type: "enabled", maxReasoningEffort } } }, + }), + }), + ).pipe(Effect.map((prepared) => prepared.body.inferenceConfig)) + + expect(yield* inferenceConfig("us.amazon.nova-2-lite-v1:0", "high")).toEqual({ temperature: 0 }) + expect(yield* inferenceConfig("us.amazon.nova-2-lite-v1:0", "low")).toEqual({ maxTokens: 64, temperature: 0 }) + expect(yield* inferenceConfig("us.xai.grok-4.6", "high")).toEqual({ maxTokens: 64, temperature: 0 }) + }), + ) + it.effect("omits additionalModelRequestFields when topK is unset", () => Effect.gen(function* () { const prepared = yield* compileRequest(baseRequest)