Skip to content

Commit 98c1e57

Browse files
allquixoticclaude
andcommitted
feat(bedrock): strict JSON-schema structured output with 30-day model cache (3.53.9)
Adds an opt-in (default ON) per-Bedrock-profile toggle that sets `strict: true` on tool specs in the Converse payload so Bedrock validates tool arguments against their JSON Schema. Dynamic: first real request is the smoke test. On 400 ValidationException matching structured-output rejection patterns, the handler marks the model as unsupported in a hidden 30-day-TTL global-state cache, shows the verbatim Bedrock error plus a disablement notice, and silently retries once without strict. On "schema is being compiled" (400/503) the handler polls with bounded exponential backoff (3s start, 1.7x growth, 45s cap, 6 attempts, 180s total) yielding progress chunks. Expired cache entries are purged lazily on next write so models get re-probed after 30 days. Also strips Bedrock-strict-incompatible schema constraints before sending — numeric minimum/maximum/exclusiveMinimum/exclusiveMaximum/multipleOf, array maxItems, and array minItems>1 — appending them to the schema description as a hint to the model. This prevents real CRC tool schemas (edit_file's expected_replacements minimum, ask_followup_question's follow_up bounds) from triggering false STRUCTURED_OUTPUT_UNSUPPORTED classifications on otherwise- supporting models. Empirically validated approach from pydantic-ai PR RooCodeInc#4237. Zero impact on non-Bedrock providers. Two new optional accessors on ApiHandlerCreateMessageMetadata are only read inside AwsBedrockHandler. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent eafd1b1 commit 98c1e57

31 files changed

Lines changed: 1115 additions & 49 deletions
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# AWS Bedrock structured outputs (strict tool use)
2+
3+
## Why this exists
4+
5+
AWS Bedrock's Converse API supports structured outputs via a `strict: true` flag on each tool definition. When set, Bedrock validates the model's tool arguments against the supplied JSON Schema (draft 2020-12 subset) so downstream code can rely on shape-correct payloads instead of paying for post-hoc parsing and retries. Upstream Roo/CRC sent native tools to Bedrock without strict validation, so tool-call arguments could drift from schema on any supported model — wasting tokens on malformed invocations and requiring defensive parsing in the agent loop.
6+
7+
## Verified current behavior
8+
9+
- Bedrock handler at `src/api/providers/bedrock.ts` sends `toolConfig.tools` via `ConverseStreamCommand` without any strict flag; tool schemas are already normalized to JSON Schema draft 2020-12 via `normalizeToolSchema()` in `src/utils/json-schema.ts`.
10+
- `ApiHandlerCreateMessageMetadata` (in `src/api/index.ts`) carries `tools`, `tool_choice`, `taskId`, `mode`; no per-provider extension points existed for Bedrock-specific state.
11+
- `ERROR_TYPES` in `bedrock.ts` classifies throttling / validation / access-denied / quota / model-not-ready / internal-server errors, but has no awareness of structured-output-specific failures.
12+
- Global state (`ContextProxy``GLOBAL_STATE_KEYS`, declared in `packages/types/src/global-settings.ts`) is the established place for hidden per-install cache data; secrets go in `SECRET_STATE_KEYS`.
13+
14+
## Implemented downstream behavior
15+
16+
- New per-profile checkbox **Attempt strict structured output** in the Bedrock Roo Settings panel. Default **enabled** when the setting is missing; user can uncheck to force the pre-feature behavior.
17+
- When a request goes out and strict is eligible (profile enabled AND at least one native tool AND the target model has not been recently marked unsupported), each tool spec in the Converse payload carries `strict: true`.
18+
- First real request is the smoke test — no synthetic probe. If Bedrock rejects the request with a structured-output-indicative error (400 with a pattern like "does not support strict", "strict is not supported", "textformat is not supported", "output_config is not supported"), the handler:
19+
1. Marks the model ID as unsupported in a hidden global-state record `bedrockStructuredOutputUnsupported` (map: modelId → expiry epoch ms, TTL 30 days).
20+
2. Yields a user-visible chunk with the **verbatim Bedrock error message** and a notice that strict mode has been disabled for that model for 30 days.
21+
3. Rebuilds the payload with `strict` stripped and silently retries once. Subsequent requests on the same model skip strict until the 30-day entry expires.
22+
- When Bedrock signals the schema is still being compiled ("up to a few minutes" per AWS docs — we pattern-match messages like "schema is being compiled", "grammar compilation", "schema compilation in progress" on 400/503), the handler enters a bounded polling loop: 6 attempts max, total wait capped at 180 s, initial 3 s delay growing ×1.7 per attempt up to 45 s per step. Each wait yields a text chunk telling the user what's happening and which attempt is next.
23+
- Expired cache entries (value <= now) are pruned lazily on the next write to `bedrockStructuredOutputUnsupported`, so a model gets re-probed automatically 30 days after its last observed rejection.
24+
- **Schema normalization for strict mode**: Bedrock rejects several JSON Schema constraints under strict even on otherwise-supporting models — specifically numeric `minimum`/`maximum`/`exclusiveMinimum`/`exclusiveMaximum`/`multipleOf`, array `maxItems`, and array `minItems > 1`. (Verified empirically against the live API in pydantic-ai [PR #4237](https://github.com/pydantic/pydantic-ai/pull/4237).) Without handling these, a supported model like Claude Sonnet would be falsely cached as unsupported the first time a tool with such a constraint is used. When strict is enabled, we strip these constraints from the tool schema and append their former values to the schema's `description` as a hint to the model (e.g., `"How many times to replace (minimum=1)"`). This is a pure-schema transform — string constraints (`minLength`, `maxLength`, `pattern`, `format`), `enum`/`const`/`default`, `$ref`/`$defs`, `anyOf`/`oneOf`, and `additionalProperties: false` are all preserved. Two real CRC tool schemas triggered this — `edit_file` (`expected_replacements: minimum 1`) and `ask_followup_question` (`follow_up: minItems 1, maxItems 4`) — both now pass cleanly.
25+
- All other provider handlers are untouched. The two new `ApiHandlerCreateMessageMetadata` fields (`isModelStructuredOutputUnsupported`, `markModelStructuredOutputUnsupported`) are optional and read only inside `AwsBedrockHandler`; non-Bedrock providers see no behavior change.
26+
27+
## Implemented touchpoints
28+
29+
- `packages/types/src/provider-settings.ts` — add `awsBedrockStructuredOutput: z.boolean().optional()` to `bedrockSchema` (default ON at read site via `?? true`).
30+
- `packages/types/src/global-settings.ts` — add hidden `bedrockStructuredOutputUnsupported: z.record(z.string(), z.number()).optional()` to `globalSettingsSchema`.
31+
- `src/shared/bedrock-structured-output-cache.ts` (new) — pure helper exporting `isUnsupported`, `markUnsupported`, `THIRTY_DAYS_MS`.
32+
- `src/api/index.ts` — extend `ApiHandlerCreateMessageMetadata` with the two optional Bedrock-specific accessors.
33+
- `src/core/task/Task.ts` — private method `getBedrockStructuredOutputAccessors()` plumbed into all four metadata construction sites, reading/writing via `providerRef.deref()?.contextProxy`.
34+
- `src/api/providers/bedrock.ts``convertToolsForBedrock(tools, { strict })` adds `strict: true` per toolSpec and runs the input schema through the Bedrock strict-incompatible-constraint stripper; `createMessage` wraps `client.send(...)` in a preflight retry loop that handles the two new error types; two new `ERROR_TYPES` entries (`STRUCTURED_OUTPUT_UNSUPPORTED`, `STRUCTURED_OUTPUT_COMPILING`) with HTTP-status gating so they don't swallow unrelated 400/503 errors.
35+
- `src/utils/json-schema.ts` — new `stripBedrockStrictIncompatibleConstraints()` exported alongside `normalizeToolSchema()`. Recursive tree-walk transform that strips numeric `minimum`/`maximum`/etc. and array `maxItems`/`minItems > 1`, appending the stripped values to the schema's description.
36+
- `webview-ui/src/components/settings/providers/Bedrock.tsx` — checkbox always visible in the Bedrock profile panel.
37+
- `webview-ui/src/i18n/locales/*/settings.json` — three new keys (`awsBedrockStructuredOutput`, `awsBedrockStructuredOutputTooltip`, `awsBedrockStructuredOutputDescription`) in every locale.
38+
39+
## Validation
40+
41+
- Supported model (e.g. `anthropic.claude-sonnet-4-5-20250929-v1:0`): first request carries `strict: true` on tool specs and streams a clean, schema-valid tool call; no fallback chunks; no entry in `bedrockStructuredOutputUnsupported`.
42+
- Unsupported model (e.g. an older Titan/Llama variant): first request fails at send-time with a 400; user sees a text chunk containing the verbatim Bedrock error plus the disablement notice; global state gets a new entry for that model with expiry ≈ now + 30 days; second request on the same model omits `strict` silently, no further user-visible notice.
43+
- Schema-compile simulation (inject a 400 with "schema is being compiled" on first N attempts, then success): user sees N progress chunks with attempt numbers and wait durations; streaming eventually proceeds normally.
44+
- Cache expiry: manually time-travel an entry to the past; next request treats the model as supported again and re-attempts with strict on.
45+
- Non-Bedrock providers: identical network payloads before and after the change; the two new metadata accessors are never invoked.

packages/types/src/global-settings.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,15 @@ export const globalSettingsSchema = z.object({
242242
* Tools in this list will be excluded from prompt generation and rejected at execution time.
243243
*/
244244
disabledTools: z.array(toolNamesSchema).optional(),
245+
246+
/**
247+
* Hidden per-model cache of AWS Bedrock models observed to reject strict structured output.
248+
* Map key: Bedrock model id / ARN (whatever `getModel().id` returns on AwsBedrockHandler).
249+
* Map value: expiry timestamp (ms since epoch). Entries whose value is <= Date.now() are
250+
* treated as expired and purged on the next write so a model gets re-probed after 30 days.
251+
* Never exposed in the settings UI.
252+
*/
253+
bedrockStructuredOutputUnsupported: z.record(z.string(), z.number()).optional(),
245254
})
246255

247256
export type GlobalSettings = z.infer<typeof globalSettingsSchema>

packages/types/src/provider-settings.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,7 @@ const bedrockSchema = apiModelIdProviderModelSchema.extend({
238238
awsBedrockEndpoint: z.string().optional(),
239239
awsBedrock1MContext: z.boolean().optional(), // Enable 'context-1m-2025-08-07' beta for 1M context window.
240240
awsBedrockServiceTier: z.enum(["STANDARD", "FLEX", "PRIORITY"]).optional(), // AWS Bedrock service tier selection
241+
awsBedrockStructuredOutput: z.boolean().optional(), // Attempt strict JSON-schema tool validation on Bedrock Converse. Default ON when unset.
241242
})
242243

243244
const vertexSchema = apiModelIdProviderModelSchema.extend({

src/api/index.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,18 @@ export interface ApiHandlerCreateMessageMetadata {
8787
* Only applies to providers that support function calling restrictions (e.g., Gemini).
8888
*/
8989
allowedFunctionNames?: string[]
90+
/**
91+
* Bedrock-specific: read-only accessor returning true if the given model is currently
92+
* cached as not supporting strict structured output (Converse `strict: true`). Only
93+
* AwsBedrockHandler reads this; other providers ignore it.
94+
*/
95+
isModelStructuredOutputUnsupported?: (modelId: string) => boolean
96+
/**
97+
* Bedrock-specific: marks the given model as not supporting strict structured output
98+
* for 30 days so future requests silently skip strict mode. Only AwsBedrockHandler
99+
* calls this; other providers ignore it.
100+
*/
101+
markModelStructuredOutputUnsupported?: (modelId: string) => void
90102
}
91103

92104
export interface ApiHandler {

0 commit comments

Comments
 (0)