Created
May 22, 2026 21:36
-
-
Save kpx-dev/dc67d1f0d1c4b9b5873e8da6c3d0066c to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| """ | |
| 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