Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/fix-bedrock-thinking-generate-object.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@effect/ai-amazon-bedrock": patch
---

`generateObject` no longer fails when extended thinking is configured via `withConfigOverride`. Anthropic's API rejects requests that set `thinking` in `additionalModelRequestFields` alongside a forced `toolChoice` — which `generateObject` always does. The fix strips `thinking` from `additionalModelRequestFields` in the `json` response format path before the request is sent.
29 changes: 22 additions & 7 deletions packages/ai/amazon-bedrock/src/AmazonBedrockLanguageModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,26 @@ export const make = Effect.fnUntraced(function*(options: {
const { messages, system } = yield* prepareMessages(providerOptions)
const { additionalTools, betas, toolConfig } = yield* prepareTools(providerOptions, config)
const responseFormat = providerOptions.responseFormat

// Anthropic rejects requests that combine extended thinking with forced tool use.
// generateObject always forces toolChoice, so strip "thinking" in the json path.
// Return an explicit object (even if empty) when fields are present so the spread
// overrides the thinking config already placed in the request by ...config above.
const requestAdditionalFields: Record<string, unknown> | undefined = responseFormat.type === "json"
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's make this block a bit more coherent by removing all the nested ternaries and using more imperative if / else logic.

? (Predicate.isNotUndefined(config.additionalModelRequestFields) ||
Predicate.isNotUndefined(additionalTools))
? (() => {
const { thinking: _thinking, ...rest } = {
...config.additionalModelRequestFields,
...additionalTools
}
return rest
})()
: undefined
: Predicate.isNotUndefined(additionalTools)
? { ...config.additionalModelRequestFields, ...additionalTools }
: undefined

const request: typeof ConverseRequest.Encoded = {
...config,
system,
Expand All @@ -231,13 +251,8 @@ export const make = Effect.fnUntraced(function*(options: {
? { toolConfig }
: {}),
// Handle additional model request fields
...(Predicate.isNotUndefined(additionalTools)
? {
additionalModelRequestFields: {
...config.additionalModelRequestFields,
...additionalTools
}
}
...(Predicate.isNotUndefined(requestAdditionalFields)
? { additionalModelRequestFields: requestAdditionalFields }
: {})
}
return { betas, request }
Expand Down
175 changes: 175 additions & 0 deletions packages/ai/amazon-bedrock/test/AmazonBedrockLanguageModel.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
import * as LanguageModel from "@effect/ai/LanguageModel"
import { assert, describe, it } from "@effect/vitest"
import * as Effect from "effect/Effect"
import * as Layer from "effect/Layer"
import * as Schema from "effect/Schema"
import { AmazonBedrockClient } from "../src/AmazonBedrockClient.js"
import * as AmazonBedrockLanguageModel from "../src/AmazonBedrockLanguageModel.js"
import { ConverseResponse } from "../src/AmazonBedrockSchema.js"
import type { ConverseRequest } from "../src/AmazonBedrockSchema.js"

// ---------------------------------------------------------------------------
// Schema decoders
// ---------------------------------------------------------------------------

const decodeConverseResponse = Schema.decodeUnknownSync(ConverseResponse)

// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------

const TestModel = "us.anthropic.claude-3-5-sonnet-20241022-v2:0" as const
const TestObjectName = "MockResult"
const MockResultSchema = Schema.Struct({ field: Schema.String })

const textResponse = () =>
decodeConverseResponse({
output: {
message: {
role: "assistant",
content: [{ text: "hello" }]
}
},
metrics: { latencyMs: 100 },
stopReason: "end_turn",
usage: { inputTokens: 10, outputTokens: 5, totalTokens: 15 }
})

const toolUseResponse = (name: string) =>
decodeConverseResponse({
output: {
message: {
role: "assistant",
content: [{
toolUse: {
toolUseId: "test-id",
name,
input: { field: "value" }
}
}]
}
},
metrics: { latencyMs: 100 },
stopReason: "tool_use",
usage: { inputTokens: 10, outputTokens: 5, totalTokens: 15 }
})

const makeCapturingLayer = (
captured: Array<typeof ConverseRequest.Encoded>,
response: ConverseResponse
) =>
AmazonBedrockLanguageModel.layer({ model: TestModel }).pipe(
Layer.provide(
Layer.succeed(AmazonBedrockClient, {
client: null as any,
streamRequest: null as any,
converse: (opts) =>
Effect.sync(() => {
captured.push(opts.payload)
return response
}),
converseStream: null as any
})
)
)

// ---------------------------------------------------------------------------
// makeRequest — request construction tests
// ---------------------------------------------------------------------------

describe("AmazonBedrockLanguageModel", () => {
describe("makeRequest / additionalModelRequestFields", () => {
it.effect("strips thinking when using generateObject (forced toolChoice.tool)", () =>
Effect.gen(function*() {
const captured: Array<typeof ConverseRequest.Encoded> = []

yield* LanguageModel.generateObject({
prompt: [],
schema: MockResultSchema,
objectName: TestObjectName
}).pipe(
AmazonBedrockLanguageModel.withConfigOverride({
additionalModelRequestFields: {
thinking: { type: "enabled", budget_tokens: 5000 }
}
}),
Effect.provide(makeCapturingLayer(captured, toolUseResponse(TestObjectName)))
)

assert.strictEqual(captured.length, 1)
const req = captured[0]

// Must force tool use for generateObject
assert.deepStrictEqual(req.toolConfig?.toolChoice, { tool: { name: TestObjectName } })

// Anthropic rejects thinking + forced tool use — must be stripped
assert.isUndefined(req.additionalModelRequestFields?.["thinking"])
}))

it.effect("preserves thinking when using generateText (no forced toolChoice)", () =>
Effect.gen(function*() {
const captured: Array<typeof ConverseRequest.Encoded> = []

yield* LanguageModel.generateText({ prompt: [] }).pipe(
AmazonBedrockLanguageModel.withConfigOverride({
additionalModelRequestFields: {
thinking: { type: "enabled", budget_tokens: 5000 }
}
}),
Effect.provide(makeCapturingLayer(captured, textResponse()))
)

assert.strictEqual(captured.length, 1)
const req = captured[0]

// thinking must flow through for non-forced-tool-use requests
assert.deepStrictEqual(req.additionalModelRequestFields?.["thinking"], {
type: "enabled",
budget_tokens: 5000
})
}))

it.effect("does not set additionalModelRequestFields when none configured and using generateObject", () =>
Effect.gen(function*() {
const captured: Array<typeof ConverseRequest.Encoded> = []

yield* LanguageModel.generateObject({
prompt: [],
schema: MockResultSchema,
objectName: TestObjectName
}).pipe(
Effect.provide(makeCapturingLayer(captured, toolUseResponse(TestObjectName)))
)

assert.strictEqual(captured.length, 1)
// No additionalModelRequestFields should be injected when none was configured
assert.isUndefined(captured[0].additionalModelRequestFields)
}))

it.effect("preserves non-thinking fields in additionalModelRequestFields with generateObject", () =>
Effect.gen(function*() {
const captured: Array<typeof ConverseRequest.Encoded> = []

yield* LanguageModel.generateObject({
prompt: [],
schema: MockResultSchema,
objectName: TestObjectName
}).pipe(
AmazonBedrockLanguageModel.withConfigOverride({
additionalModelRequestFields: {
thinking: { type: "enabled", budget_tokens: 5000 },
someOtherField: "preserved"
}
}),
Effect.provide(makeCapturingLayer(captured, toolUseResponse(TestObjectName)))
)

assert.strictEqual(captured.length, 1)
const req = captured[0]

// thinking stripped, other fields survive
assert.isUndefined(req.additionalModelRequestFields?.["thinking"])
assert.strictEqual(req.additionalModelRequestFields?.["someOtherField"], "preserved")
}))
})
})
Loading