Structured output is broken on Bedrock Claude: output_config.format: Extra inputs are not permitted
Component: hindsight-api 0.8.6 (self-hosted), engine/providers/litellm_llm.py
Provider: HINDSIGHT_API_LLM_PROVIDER=bedrock, au.anthropic.claude-haiku-4-5-20251001-v1:0, ap-southeast-2
litellm: 1.93.0
Severity: High — on Bedrock Claude, retain and consolidation are completely non-functional
Summary
With provider=bedrock and any Anthropic Claude model, every structured-output
call fails. LiteLLMLLM.call() passes an OpenAI-style response_format; litellm
translates it into a Converse outputConfig block; Bedrock's Anthropic layer
rewrites that internally to snake_case output_config.format and its own validator
rejects the key:
litellm.BadRequestError: BedrockException - {"message": "The model returned the
following errors: output_config.format: Extra inputs are not permitted"}
Consequences:
POST /v1/{tenant}/banks/{bank}/memories → HTTP 500 whenever
retain_extraction_mode needs the LLM (e.g. concise). No facts, no entity graph.
- Consolidation silently degrades — 3 retries then
skipping batch, so
observations and mental models are never built. This one produces no API error,
only log lines, so it's easy to miss.
recall and reflect are unaffected. reflect goes through
call_with_tools(), which emits toolConfig — and Bedrock accepts that.
Same model, same credentials, same region for both paths. The only variable is
response_format vs tools.
This is not a litellm bug, and not a config problem
litellm 1.93.0 sends a well-formed request. Captured wire request to
https://bedrock-runtime.ap-southeast-2.amazonaws.com/model/au.anthropic.claude-haiku-4-5-20251001-v1%3A0/converse:
{"outputConfig": {"textFormat": {"type": "json_schema",
"structure": {"jsonSchema": {"...": "..."}}}}}
That is correct camelCase Converse shape — the snake_case output_config.format
from the error never appears in litellm's payload. Bedrock's Anthropic layer
generates it during internal translation, then refuses it.
Ruled out:
pop_bedrock_invoke_output_config_format is present in 1.93.0 (it covers the
Invoke path; this is the Converse route, confirmed via
BedrockModelInfo.get_bedrock_route). Bumping litellm does not help.
HINDSIGHT_API_LLM_STRICT_SCHEMA already defaults to False (config.py), so
strict mode is not the trigger.
- Patching
litellm.model_cost[...]["supports_response_schema"] = False does not
reach the Converse path. drop_params likewise doesn't strip it.
- No env-var-only workaround exists.
Reproduction (raw boto3, no litellm involved)
import boto3, json
c = boto3.client("bedrock-runtime", region_name="ap-southeast-2")
MODEL = "au.anthropic.claude-haiku-4-5-20251001-v1:0"
msgs = [{"role": "user", "content": [{"text": "hi"}]}]
schema = {"type": "object", "properties": {"a": {"type": "string"}}, "required": ["a"]}
# 1. plain
c.converse(modelId=MODEL, messages=msgs) # OK
# 2. structured output via outputConfig
c.converse(modelId=MODEL, messages=msgs, outputConfig={
"textFormat": {"type": "json_schema", "structure": {"jsonSchema": schema}}})
# ValidationException: output_config.format: Extra inputs are not permitted
# 3. structured output via toolConfig
c.converse(modelId=MODEL, messages=msgs, toolConfig={
"tools": [{"toolSpec": {"name": "r", "inputSchema": {"json": schema}}}],
"toolChoice": {"tool": {"name": "r"}}}) # OK
Results: PLAIN: OK / OUTPUTCONFIG: ValidationException … / TOOLCONFIG: OK.
Via the API:
curl -s -X POST "$HINDSIGHT_URL/v1/default/banks/$BANK/memories" \
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{"items":[{"content":"The sky is blue."}]}'
# {"detail":"Fact extraction failed: 1/1 chunks failed. First failures: chunk 0:
# BadRequestError: litellm.BadRequestError: BedrockException - {\"message\":
# \"The model returned the following errors: output_config.format: Extra inputs
# are not permitted\"}"}
POST .../memories/dry-run-extract fails identically.
Scope
- Not model-specific:
au.anthropic.claude-sonnet-4-5-20250929-v1:0 fails identically.
- Not fixable by switching to Amazon models:
au.amazon.nova-lite-v1:0 /
apac.amazon.nova-lite-v1:0 don't support outputConfig in ap-southeast-2 at all.
- Affects every
LiteLLMLLM.call() caller with a response_format — fact extraction,
consolidation, and any other structured operation on Bedrock Claude.
Suggested fix
Use a forced tool call for structured output when the model routes through Bedrock —
exactly what providers/anthropic_llm.py already does for the native SDK
(use_forced_tool + tool_choice={"type":"tool","name":...}).
In LiteLLMLLM.call(), when self.model.startswith("bedrock/"), replace the
response_format kwarg with:
call_kwargs["tools"] = [{
"type": "function",
"function": {"name": "structured_response",
"description": f"Return the structured response ({schema_name}).",
"parameters": schema},
}]
call_kwargs["tool_choice"] = {"type": "function",
"function": {"name": "structured_response"}}
then substitute the tool call's arguments for content before the existing
parse/validate block, so retries, markdown-stripping, parse_llm_json repair,
usage accounting, and tracing all keep working unchanged. Fall back to the text
parse when the tool call is absent (a gateway may drop tool_choice).
Verified locally against real Bedrock — call() with a list[str] schema returns a
validated model instead of 400:
prefers_forced_tool = True
RESULT: facts=['The sky is blue', 'Grass is green']
End-to-end on an otherwise-pristine 0.8.6 image: retain in concise mode returns
success: true with real usage (input_tokens=3587, output_tokens=349), atomic facts
are extracted with entity annotations, and consolidation logs
created=2 updated=0 skipped=0 plus mental-model refreshes, with zero
output_config errors.
Happy to open a PR if the approach looks right.
Workaround for other operators, in the meantime
Set the bank's retain_extraction_mode to chunks, which bypasses the LLM on the
write path:
curl -s -X PATCH "$HINDSIGHT_URL/v1/default/banks/$BANK/config" \
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{"updates":{"retain_extraction_mode":"chunks"}}'
Retain and recall then work, but there is no fact extraction, no entity/relationship
graph, and no consolidation/observations.
Minor follow-up
POST /v1/{tenant}/banks/{bank}/health/llm is gated behind
HINDSIGHT_API_ENABLE_BANK_LLM_HEALTH. Since it exercises exactly the path that
breaks here, enabling it by default (or documenting it prominently) would make this
class of problem much faster to triage — a structured-output probe would have caught
this immediately.
Structured output is broken on Bedrock Claude:
output_config.format: Extra inputs are not permittedComponent:
hindsight-api0.8.6 (self-hosted),engine/providers/litellm_llm.pyProvider:
HINDSIGHT_API_LLM_PROVIDER=bedrock,au.anthropic.claude-haiku-4-5-20251001-v1:0,ap-southeast-2litellm: 1.93.0
Severity: High — on Bedrock Claude,
retainand consolidation are completely non-functionalSummary
With
provider=bedrockand any Anthropic Claude model, every structured-outputcall fails.
LiteLLMLLM.call()passes an OpenAI-styleresponse_format; litellmtranslates it into a Converse
outputConfigblock; Bedrock's Anthropic layerrewrites that internally to snake_case
output_config.formatand its own validatorrejects the key:
Consequences:
POST /v1/{tenant}/banks/{bank}/memories→ HTTP 500 wheneverretain_extraction_modeneeds the LLM (e.g.concise). No facts, no entity graph.skipping batch, soobservations and mental models are never built. This one produces no API error,
only log lines, so it's easy to miss.
recallandreflectare unaffected.reflectgoes throughcall_with_tools(), which emitstoolConfig— and Bedrock accepts that.Same model, same credentials, same region for both paths. The only variable is
response_formatvstools.This is not a litellm bug, and not a config problem
litellm 1.93.0 sends a well-formed request. Captured wire request to
https://bedrock-runtime.ap-southeast-2.amazonaws.com/model/au.anthropic.claude-haiku-4-5-20251001-v1%3A0/converse:{"outputConfig": {"textFormat": {"type": "json_schema", "structure": {"jsonSchema": {"...": "..."}}}}}That is correct camelCase Converse shape — the snake_case
output_config.formatfrom the error never appears in litellm's payload. Bedrock's Anthropic layer
generates it during internal translation, then refuses it.
Ruled out:
pop_bedrock_invoke_output_config_formatis present in 1.93.0 (it covers theInvoke path; this is the Converse route, confirmed via
BedrockModelInfo.get_bedrock_route). Bumping litellm does not help.HINDSIGHT_API_LLM_STRICT_SCHEMAalready defaults toFalse(config.py), sostrict mode is not the trigger.
litellm.model_cost[...]["supports_response_schema"] = Falsedoes notreach the Converse path.
drop_paramslikewise doesn't strip it.Reproduction (raw boto3, no litellm involved)
Results:
PLAIN: OK/OUTPUTCONFIG: ValidationException …/TOOLCONFIG: OK.Via the API:
POST .../memories/dry-run-extractfails identically.Scope
au.anthropic.claude-sonnet-4-5-20250929-v1:0fails identically.au.amazon.nova-lite-v1:0/apac.amazon.nova-lite-v1:0don't supportoutputConfiginap-southeast-2at all.LiteLLMLLM.call()caller with aresponse_format— fact extraction,consolidation, and any other structured operation on Bedrock Claude.
Suggested fix
Use a forced tool call for structured output when the model routes through Bedrock —
exactly what
providers/anthropic_llm.pyalready does for the native SDK(
use_forced_tool+tool_choice={"type":"tool","name":...}).In
LiteLLMLLM.call(), whenself.model.startswith("bedrock/"), replace theresponse_formatkwarg with:then substitute the tool call's
argumentsforcontentbefore the existingparse/validate block, so retries, markdown-stripping,
parse_llm_jsonrepair,usage accounting, and tracing all keep working unchanged. Fall back to the text
parse when the tool call is absent (a gateway may drop
tool_choice).Verified locally against real Bedrock —
call()with alist[str]schema returns avalidated model instead of 400:
End-to-end on an otherwise-pristine 0.8.6 image:
retaininconcisemode returnssuccess: truewith real usage (input_tokens=3587, output_tokens=349), atomic factsare extracted with entity annotations, and consolidation logs
created=2 updated=0 skipped=0plus mental-model refreshes, with zerooutput_configerrors.Happy to open a PR if the approach looks right.
Workaround for other operators, in the meantime
Set the bank's
retain_extraction_modetochunks, which bypasses the LLM on thewrite path:
Retain and recall then work, but there is no fact extraction, no entity/relationship
graph, and no consolidation/observations.
Minor follow-up
POST /v1/{tenant}/banks/{bank}/health/llmis gated behindHINDSIGHT_API_ENABLE_BANK_LLM_HEALTH. Since it exercises exactly the path thatbreaks here, enabling it by default (or documenting it prominently) would make this
class of problem much faster to triage — a structured-output probe would have caught
this immediately.