Skip to content

Instantly share code, notes, and snippets.

@kpx-dev
Created May 22, 2026 21:36
Show Gist options
  • Select an option

  • Save kpx-dev/dc67d1f0d1c4b9b5873e8da6c3d0066c to your computer and use it in GitHub Desktop.

Select an option

Save kpx-dev/dc67d1f0d1c4b9b5873e8da6c3d0066c to your computer and use it in GitHub Desktop.
"""
Verify Claude Opus 4.7 structured outputs on Bedrock + show the workaround.
Calls to us.anthropic.claude-opus-4-7 fail with
ValidationException: output_config.format: Extra inputs are not permitted
on the Bedrock Converse API.
The error path is snake_case (output_config.format), which means it's
raised by the Anthropic model layer AFTER Bedrock's
outputConfig.textFormat -> output_config.format translation. It is not a
Converse API translation bug; the Opus 4.7 model itself does not yet
accept the structured-output field. The same payload works against
Opus 4.6 and Opus 4.5 — the regression is specific to 4.7.
Until the model-layer fix lands, the recommended workaround on Opus 4.7
is the classic tool-use / tool_choice pattern, which produces
schema-conforming JSON via a forced tool call. This works on every
Claude model on Bedrock (3.5 -> 4.7) because it routes through the
tool-use code path instead of output_config.
This script runs two checks:
1. bedrock-runtime Converse API — outputConfig.textFormat (FAILS on 4.7)
2. bedrock-runtime Converse API — tool-use workaround (WORKS on 4.7)
Requirements:
pip install boto3
AWS credentials configured (via env vars, profile, or IAM role)
Usage:
python structured-output.py
"""
import json
import boto3
REGION = "us-east-1"
MODEL_ID = "us.anthropic.claude-opus-4-7"
PROMPT = "What is 2 + 2? Answer with just the number."
# JSON schema we want the response to conform to
JSON_SCHEMA = {
"type": "object",
"properties": {
"answer": {"type": "integer", "description": "The numeric answer"},
"explanation": {"type": "string", "description": "Brief explanation"},
},
"required": ["answer", "explanation"],
"additionalProperties": False,
}
def test_converse_api():
"""
Test 3: bedrock-runtime Converse API.
Uses outputConfig.textFormat (camelCase, Bedrock-native format).
Expected: FAILS on Opus 4.7 with "Extra inputs are not permitted".
"""
print("=" * 60)
print("TEST 3: bedrock-runtime Converse API (expected to FAIL)")
print("=" * 60)
client = boto3.client("bedrock-runtime", region_name=REGION)
print(f" Model: {MODEL_ID}")
print(f" Payload includes: outputConfig.textFormat (json_schema)")
print()
try:
response = client.converse(
modelId=MODEL_ID,
messages=[
{
"role": "user",
"content": [{"text": PROMPT}],
}
],
outputConfig={
"textFormat": {
"type": "json_schema",
"structure": {
"jsonSchema": {
"schema": json.dumps(JSON_SCHEMA),
"name": "math_answer",
"description": "A structured math answer",
}
},
}
},
)
output = response["output"]["message"]["content"][0]["text"]
print(f" ✅ SUCCESS (unexpected!)")
print(f" Response: {output}")
except client.exceptions.ValidationException as e:
print(f" ❌ FAILED (ValidationException) — as expected")
print(f" Error: {e}")
print()
print(" ⚠️ This confirms the bug: Converse API does not support")
print(" outputConfig.textFormat for Claude Opus 4.7.")
print(" The error path 'output_config.format' (snake_case) confirms")
print(" the error originates from the Anthropic model layer after")
print(" Bedrock's camelCase → snake_case translation.")
except Exception as e:
print(f" ❌ FAILED: {e}")
print()
def test_converse_tool_use_workaround():
"""
Test 4: bedrock-runtime Converse API with the tool-use WORKAROUND.
Instead of outputConfig.textFormat, declare a single tool whose
inputSchema is the desired JSON schema, and force the model to call
it via toolChoice. The model emits the structured payload as the
tool_use input — guaranteed to match the schema.
This pattern works on every Claude model on Bedrock (3.5+, 4.x, 4.7)
because it routes through the tool-use path, not output_config.
"""
print("=" * 60)
print("TEST 4: Converse API tool-use WORKAROUND (expected to PASS)")
print("=" * 60)
client = boto3.client("bedrock-runtime", region_name=REGION)
tool_name = "emit_answer"
tool_spec = {
"toolSpec": {
"name": tool_name,
"description": "Return the answer in the required schema.",
"inputSchema": {"json": JSON_SCHEMA},
}
}
print(f" Model: {MODEL_ID}")
print(f" Strategy: forced tool_use (tool='{tool_name}')")
print()
try:
response = client.converse(
modelId=MODEL_ID,
messages=[
{
"role": "user",
"content": [{"text": PROMPT}],
}
],
toolConfig={
"tools": [tool_spec],
"toolChoice": {"tool": {"name": tool_name}},
},
)
# Pull the tool_use block; its `input` is the schema-conforming JSON.
structured = None
for block in response["output"]["message"]["content"]:
if "toolUse" in block:
structured = block["toolUse"]["input"]
break
if structured is None:
print(" ❌ FAILED: no toolUse block in response")
print(f" Raw output: {response['output']}")
return
print(f" ✅ SUCCESS")
print(f" Stop reason: {response.get('stopReason')}")
print(f" Structured response: {json.dumps(structured, indent=4)}")
except client.exceptions.ValidationException as e:
print(f" ❌ FAILED (ValidationException)")
print(f" Error: {e}")
except Exception as e:
print(f" ❌ FAILED: {e}")
print()
if __name__ == "__main__":
print()
print("🔬 Verifying Claude Opus 4.7 Structured Outputs on Bedrock")
print(" (Converse outputConfig.textFormat + tool-use workaround)")
print()
test_converse_api() # Expected to FAIL — confirms the reported bug
test_converse_tool_use_workaround() # Expected to PASS — recommended workaround
print("=" * 60)
print("SUMMARY")
print("=" * 60)
print(" Test 3 is expected to fail on Opus 4.7 with")
print(" 'output_config.format: Extra inputs are not permitted'")
print(" because the Anthropic model layer behind Opus 4.7 does not")
print(" yet accept the structured-output field.")
print()
print(" Test 4 (tool-use with forced toolChoice) is the recommended")
print(" workaround until the model-layer fix lands. It produces")
print(" schema-conforming JSON on Opus 4.7 today and is portable")
print(" back to Opus 4.6 / 4.5 / Sonnet without code changes.")
print()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment