Skip to content

feat: add support for claude code goal mode for bedrock opus output config#28877

Closed
dennishenry wants to merge 5 commits into
BerriAI:litellm_internal_stagingfrom
dennishenry:dennis/opus-goal-output-config
Closed

feat: add support for claude code goal mode for bedrock opus output config#28877
dennishenry wants to merge 5 commits into
BerriAI:litellm_internal_stagingfrom
dennishenry:dennis/opus-goal-output-config

Conversation

@dennishenry

Copy link
Copy Markdown
Contributor

Pre-Submission checklist

Please complete all items before asking a LiteLLM maintainer to review your PR

  • I have Added testing in the tests/test_litellm/ directory, Adding at least 1 test is a hard requirement - see details
  • My PR passes all unit tests on make test-unit
  • My PR's scope is as isolated as possible, it only solves 1 specific problem
  • I have requested a Greptile review by commenting @greptileai and received a Confidence Score of at least 4/5 before requesting a maintainer review

CI (LiteLLM team)

CI status guideline:

  • 50-55 passing tests: main is stable with minor issues.
  • 45-49 passing tests: acceptable but needs attention
  • <= 40 passing tests: unstable; be careful with your merges and assess the risk.
  • Branch creation CI run
    Link:

  • CI run for the last commit
    Link:

  • Merge / cherry-pick CI run
    Links:

Screenshots / Proof of Fix

Before this patch, the hook evaluator request was rejected with output_config.format: Extra inputs are not permitted, and direct xhigh calls failed on older Opus aliases. After this patch, the same /goal workflow succeeds against patched LiteLLM and Bedrock using AWS profile genai.

Environment

  • LiteLLM repo: ~/litellm
  • Patched LiteLLM proxy: http://127.0.0.1:4011
  • Unpatched baseline worktree: /private/tmp/litellm-goal-baseline
  • Unpatched baseline proxy: http://127.0.0.1:4010
  • Claude Code version: 2.1.144 (Claude Code)
  • Claude Code isolation: Docker image claude-code-test:2.1.144, Linux arm64, clean HOME=/tmp/claude-home
  • AWS profile: genai
  • AWS region: us-east-1
  • Proxy config: /private/tmp/litellm_claude_goal_test_config.yaml

The isolated Claude Code logs confirm no host/user/managed Claude settings were loaded:

Broken symlink or missing file encountered for settings.json at path: /tmp/claude-home/.claude/settings.json
Broken symlink or missing file encountered for settings.json at path: /etc/claude-code/managed-settings.json
Broken symlink or missing file encountered for settings.json at path: /work/.claude/settings.json
Broken symlink or missing file encountered for settings.json at path: /work/.claude/settings.local.json
[ToolSearch:optimistic] disabled: ANTHROPIC_BASE_URL=http://host.docker.internal:4011 is not a first-party Anthropic host.

Test proxy config

model_list:
  - model_name: claude-opus-4-5
    litellm_params:
      model: bedrock/us.anthropic.claude-opus-4-5-20251101-v1:0
      aws_region_name: us-east-1
      aws_profile_name: genai
  - model_name: claude-opus-4-6
    litellm_params:
      model: bedrock/us.anthropic.claude-opus-4-6-v1
      aws_region_name: us-east-1
      aws_profile_name: genai
  - model_name: claude-opus-4-7
    litellm_params:
      model: bedrock/us.anthropic.claude-opus-4-7
      aws_region_name: us-east-1
      aws_profile_name: genai
general_settings:
  master_key: sk-1234
litellm_settings:
  drop_params: true
  telemetry: false

Both baseline and patched proxies reported all three endpoints healthy.

Before: unpatched LiteLLM rejects /goal

Command run inside isolated Claude Code container:

docker run --rm \
  -v /private/tmp:/host_tmp \
  -e ANTHROPIC_BASE_URL=http://host.docker.internal:4010 \
  -e ANTHROPIC_API_KEY=sk-1234 \
  -e DISABLE_TELEMETRY=1 \
  -e CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1 \
  -e CLAUDE_CODE_DISABLE_ALTERNATE_SCREEN=1 \
  claude-code-test:2.1.144 \
  claude -p '/goal The assistant must answer exactly OK.' \
    --model claude-opus-4-7 \
    --effort xhigh \
    --mcp-config '{"mcpServers":{}}' \
    --strict-mcp-config \
    --tools '' \
    --debug-file /host_tmp/claude_vm_goal_baseline_4010_debug.log \
    --verbose

CLI output:

OK

Debug log shows the user-visible answer succeeded, but the /goal Stop-hook evaluator failed:

Added session hook for event Stop in session 4360c704-c95b-4a8a-9bc6-b417b9adfb0a
[API REQUEST] /v1/messages source=sdk
ARGUMENTS: {"session_id":"[REDACTED]","transcript_path":"/tmp/claude-home/.claude/projects/-work/4360c704-c95b-4a8a-9bc6-b417b9adfb0a.jsonl","cwd":"/work","permission_mode":"default","effort":{"level":"xhigh"},"hook_event_name":"Stop","stop_hook_active":false,"last_assistant_message":"OK"}
[API REQUEST] /v1/messages source=hook_prompt
[ERROR] API error (attempt 1/11): 400 400 {"error":{"message":"{\"message\":\"output_config.format: Extra inputs are not permitted\"}. Received Model Group=claude-opus-4-7\nAvailable Model Group Fallbacks=None","type":"None","param":"None","code":"400"}}
[ERROR] Hooks: prompt-hook evaluator API error: API Error: 400 {"message":"output_config.format: Extra inputs are not permitted"}. Received Model Group=claude-opus-4-7

Baseline LiteLLM proxy log:

LiteLLM Proxy:ERROR - litellm.proxy.proxy_server.anthropic_response(): Exception occured - {"message":"output_config.format: Extra inputs are not permitted"}
INFO: 127.0.0.1 - "POST /v1/messages?beta=true HTTP/1.1" 400 Bad Request

Before: direct Anthropic-compatible calls

Payload shape used for each model:

{
  "model": "<alias>",
  "max_tokens": 64,
  "messages": [
    {
      "role": "user",
      "content": "Return JSON with answer set to ok."
    }
  ],
  "output_config": {
    "effort": "xhigh",
    "format": {
      "type": "json_schema",
      "schema": {
        "type": "object",
        "properties": {
          "answer": { "type": "string" }
        },
        "required": ["answer"],
        "additionalProperties": false
      }
    }
  }
}

Results on unpatched baseline:

claude-opus-4-5:
{"error":{"message":"{\"message\":\"output_config.effort: Input should be 'low', 'medium' or 'high'\"}. Received Model Group=claude-opus-4-5\nAvailable Model Group Fallbacks=None","type":"None","param":"None","code":"400"}}
HTTP_STATUS:400

claude-opus-4-6:
{"error":{"message":"{\"message\":\"output_config.effort: Input should be 'low', 'medium', 'high' or 'max'\"}. Received Model Group=claude-opus-4-6\nAvailable Model Group Fallbacks=None","type":"None","param":"None","code":"400"}}
HTTP_STATUS:400

claude-opus-4-7:
{"error":{"message":"{\"message\":\"output_config.format: Extra inputs are not permitted\"}. Received Model Group=claude-opus-4-7\nAvailable Model Group Fallbacks=None","type":"None","param":"None","code":"400"}}
HTTP_STATUS:400

After: patched LiteLLM accepts /goal

Command run inside the same isolated Claude Code container:

docker run --rm \
  -v /private/tmp:/host_tmp \
  -e ANTHROPIC_BASE_URL=http://host.docker.internal:4011 \
  -e ANTHROPIC_API_KEY=sk-1234 \
  -e DISABLE_TELEMETRY=1 \
  -e CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1 \
  -e CLAUDE_CODE_DISABLE_ALTERNATE_SCREEN=1 \
  claude-code-test:2.1.144 \
  claude -p '/goal The assistant must answer exactly OK.' \
    --model claude-opus-4-7 \
    --effort xhigh \
    --mcp-config '{"mcpServers":{}}' \
    --strict-mcp-config \
    --tools '' \
    --debug-file /host_tmp/claude_vm_goal_patched_4011_debug.log \
    --verbose

CLI output:

OK

Debug log:

Added session hook for event Stop in session 74613540-ef9b-4434-b186-9c64ecbcc6f5
[API REQUEST] /v1/messages source=sdk
ARGUMENTS: {"session_id":"[REDACTED]","transcript_path":"/tmp/claude-home/.claude/projects/-work/74613540-ef9b-4434-b186-9c64ecbcc6f5.jsonl","cwd":"/work","permission_mode":"default","effort":{"level":"xhigh"},"hook_event_name":"Stop","stop_hook_active":false,"last_assistant_message":"OK"}
[API REQUEST] /v1/messages source=hook_prompt
Hooks: Model response: {"ok": true, "reason": "The assistant's response was exactly 'OK' as shown in the transcript: 'Assistant: OK'"}
Hooks: Prompt hook condition was met: The assistant's response was exactly 'OK' as shown in the transcript: 'Assistant: OK'

Patched LiteLLM proxy log:

INFO: 127.0.0.1 - "POST /v1/messages?beta=true HTTP/1.1" 200 OK
INFO: 127.0.0.1 - "POST /v1/messages?beta=true HTTP/1.1" 200 OK

After: /goal verified for Opus 4.5, 4.6, and 4.7

All three aliases completed the same isolated Claude Code /goal run without an API rejection.

claude-opus-4-5:
CLI output: OK
[API REQUEST] /v1/messages source=sdk
[API REQUEST] /v1/messages source=hook_prompt
Hooks: Model response: {"ok": true, "reason": "The assistant's last message was exactly \"OK\", which satisfies the condition that the assistant must answer exactly OK."}
Hooks: Prompt hook condition was met: The assistant's last message was exactly "OK", which satisfies the condition that the assistant must answer exactly OK.

claude-opus-4-6:
CLI output: OK
[API REQUEST] /v1/messages source=sdk
[API REQUEST] /v1/messages source=hook_prompt
Hooks: Model response: {"ok": true, "reason": "The assistant's last message is exactly \"OK\", which satisfies the condition."}
Hooks: Prompt hook condition was met: The assistant's last message is exactly "OK", which satisfies the condition.

claude-opus-4-7:
CLI output: OK
[API REQUEST] /v1/messages source=sdk
[API REQUEST] /v1/messages source=hook_prompt
Hooks: Model response: {"ok": true, "reason": "The assistant's response was exactly 'OK' as shown in the transcript: 'Assistant: OK'"}
Hooks: Prompt hook condition was met: The assistant's response was exactly 'OK' as shown in the transcript: 'Assistant: OK'

After: plan-mode /goal verified

Command used:

docker run --rm \
  -v /private/tmp:/host_tmp \
  -e ANTHROPIC_BASE_URL=http://host.docker.internal:4011 \
  -e ANTHROPIC_API_KEY=sk-1234 \
  -e DISABLE_TELEMETRY=1 \
  -e CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1 \
  -e CLAUDE_CODE_DISABLE_ALTERNATE_SCREEN=1 \
  claude-code-test:2.1.144 \
  claude -p '/goal The assistant must answer exactly OK.' \
    --permission-mode plan \
    --model claude-opus-4-7 \
    --effort xhigh \
    --mcp-config '{"mcpServers":{}}' \
    --strict-mcp-config \
    --tools '' \
    --debug-file /host_tmp/claude_vm_goal_planmode_patched_4011_opus47_debug.log \
    --verbose

Output and debug evidence:

CLI output: OK
ARGUMENTS: {"session_id":"[REDACTED]","transcript_path":"/tmp/claude-home/.claude/projects/-work/4bc60012-e208-418c-aa3a-d985cdd02b94.jsonl","cwd":"/work","permission_mode":"plan","effort":{"level":"xhigh"},"hook_event_name":"Stop","stop_hook_active":false,"last_assistant_message":"OK"}
[API REQUEST] /v1/messages source=hook_prompt
Hooks: Model response: {"ok": true, "reason": "The assistant's last message was exactly 'OK', satisfying the condition."}
Hooks: Prompt hook condition was met: The assistant's last message was exactly 'OK', satisfying the condition.

Captured request body from Claude Code /goal

A localhost capture proxy was inserted between Claude Code and patched LiteLLM:

Claude Code container -> http://host.docker.internal:4020 -> patched LiteLLM http://127.0.0.1:4011

Capture output summary:

{"path":"/","status":200,"model":null,"output_config":null}
{"path":"/v1/messages?beta=true","status":200,"model":"claude-opus-4-7","output_config":{"effort":"xhigh"}}
{"path":"/v1/messages?beta=true","status":200,"model":"claude-opus-4-7","output_config":{"effort":"xhigh","format":{"schema":{"additionalProperties":false,"properties":{"impossible":{"type":"boolean"},"ok":{"type":"boolean"},"reason":{"type":"string"}},"required":["ok","reason"],"type":"object"},"type":"json_schema"}}}

The second POST is the Stop-hook evaluator request. It contains both:

  • output_config.effort = "xhigh"
  • output_config.format.type = "json_schema"

and returned HTTP 200 after the patch.

Full captured request/response body is in:

/private/tmp/claude_goal_capture.jsonl

After: direct Anthropic-compatible calls

Results against patched LiteLLM with the same direct payload shown above:

claude-opus-4-5:
{"model":"claude-opus-4-5","id":"msg_bdrk_018YJFTeRS9MVUzRkiASMsWU","type":"message","role":"assistant","content":[{"type":"text","text":"```json\n{\"answer\": \"ok\"}\n```"}],"stop_reason":"end_turn","stop_sequence":null,"stop_details":null,"usage":{"input_tokens":47,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":0},"output_tokens":14,"total_tokens":61}}
HTTP_STATUS:200

claude-opus-4-6:
{"model":"claude-opus-4-6","id":"msg_bdrk_01GWXJA1EH5cQa4wBLqwKepd","type":"message","role":"assistant","content":[{"type":"text","text":"```json\n{\"answer\": \"ok\"}\n```"}],"stop_reason":"end_turn","stop_sequence":null,"stop_details":null,"usage":{"input_tokens":47,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":0},"output_tokens":14,"total_tokens":61}}
HTTP_STATUS:200

claude-opus-4-7:
{"model":"claude-opus-4-7","id":"msg_bdrk_ounbvbgljhut6t2ja5z72bfu2hxky6uwfvgox3goocakxidypfvq","type":"message","role":"assistant","content":[{"type":"text","text":"{\"answer\":\"ok\"}"}],"stop_reason":"end_turn","stop_sequence":null,"stop_details":null,"usage":{"input_tokens":69,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":0},"output_tokens":14,"service_tier":"standard","total_tokens":83}}
HTTP_STATUS:200

Automated tests

Focused regression suite:

env PYENV_VERSION=3.13.11 uv run pytest \
  tests/test_litellm/llms/anthropic/chat/test_anthropic_chat_transformation.py \
  tests/test_litellm/llms/anthropic/experimental_pass_through/messages/test_anthropic_messages_structured_outputs.py \
  tests/test_litellm/llms/bedrock/chat/invoke_transformations/test_bedrock_chat_invoke_transformations_anthropic_claude3_transformation.py \
  tests/test_litellm/llms/bedrock/chat/test_converse_transformation.py \
  tests/test_litellm/llms/bedrock/messages/invoke_transformations/test_anthropic_claude3_transformation.py \
  -q

Result:

417 passed in 26.18s

Type

🆕 New Feature
🐛 Bug Fix

Changes

This change fixes the Anthropic-compatible adapter path used by Claude Code /goal and plan-mode goal evaluation for Bedrock-backed Opus aliases:

  • claude-opus-4-5
  • claude-opus-4-6
  • claude-opus-4-7

@codecov

codecov Bot commented May 26, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 94.11765% with 8 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
litellm/llms/bedrock/common_utils.py 90.47% 6 Missing ⚠️
...ransformations/anthropic_claude3_transformation.py 90.00% 1 Missing ⚠️
...ransformations/anthropic_claude3_transformation.py 97.14% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

@greptile-apps

greptile-apps Bot commented May 26, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes the Claude Code /goal mode workflow on Bedrock-backed Opus aliases by teaching LiteLLM how to handle Anthropic's output_config shape (both effort and the nested format subfield) across the Invoke and Converse Bedrock paths.

  • Adds normalize_bedrock_opus_output_config_effort in common_utils.py, driven by a new bedrock_output_config_effort_ceiling JSON flag, which clamps xhigh/max down to the ceiling each model variant accepts on Bedrock (high/max/xhigh for Opus 4.5/4.6/4.7 respectively).
  • Adds pop_bedrock_invoke_output_config_format and convert_bedrock_invoke_output_format_to_inline_schema in common_utils.py (consolidated from the previously duplicated Invoke path), and wires them into both Bedrock Invoke chat and messages transformations as well as the Converse path where the format is translated into Bedrock's native outputConfig.textFormat.
  • Extends AnthropicOutputConfig to include xhigh and max effort literals and the format subfield, adds the structured-output beta header to output_config.format in both Anthropic chat and pass-through transformations, and updates model_prices_and_context_window.json for all Bedrock Opus model variants.

Confidence Score: 5/5

Safe to merge. The change is additive — new normalization helpers, new JSON capability flags, and new beta-header detection. No existing request paths are altered in a breaking way.

All three Bedrock paths (Invoke chat, Invoke messages, Converse) have dedicated test coverage for both effort normalization and output_config.format extraction. The capability-flag-driven approach follows the team's established pattern. The two noted issues are a dead fallback branch and a schema-format inconsistency between paths, neither of which affects the correctness of the primary code paths exercised by Claude Code's /goal workflow.

litellm/llms/bedrock/common_utils.py — the _get_bedrock_output_config_effort_ceiling function has a dead local-map fallback that never runs, and convert_bedrock_invoke_output_format_to_inline_schema embeds the schema without the additionalProperties: false normalization applied by the Converse path.

Important Files Changed

Filename Overview
litellm/llms/bedrock/common_utils.py Adds normalize_bedrock_opus_output_config_effort, pop_bedrock_invoke_output_config_format, and convert_bedrock_invoke_output_format_to_inline_schema helpers; includes a local model-map fallback in _get_bedrock_output_config_effort_ceiling that is unreachable at runtime
litellm/llms/bedrock/chat/invoke_transformations/anthropic_claude3_transformation.py Adds effort normalization and output_config.format extraction before calling AnthropicConfig.transform_request; correctly shallow-copies output_config before mutation; removes redundant second normalization call
litellm/llms/bedrock/messages/invoke_transformations/anthropic_claude3_transformation.py Moves _convert_output_format_to_inline_schema and beta-header logic to shared helpers; adds output_config.format extraction and effort normalization; extracts _get_bedrock_invoke_anthropic_beta_headers and _strip_unsupported_bedrock_invoke_fields for readability
litellm/llms/bedrock/chat/converse_transformation.py Adds output_config.format extraction and conversion to native Bedrock outputConfig; applies effort normalization at both the reasoning_effort mapping point and the additionalModelRequestFields injection point
model_prices_and_context_window.json Adds supports_output_config, supports_xhigh_reasoning_effort, and bedrock_output_config_effort_ceiling (high/max/xhigh per model tier) to Bedrock Opus 4.5/4.6/4.7 entries and Anthropic-direct Opus aliases

Reviews (2): Last reviewed commit: "fixing failed test" | Re-trigger Greptile

Comment thread litellm/llms/bedrock/common_utils.py Outdated
@dennishenry dennishenry changed the title Dennis/opus goal output config feat: add support for claude cod goal mode for bedrock opus output config May 26, 2026
@dennishenry

Copy link
Copy Markdown
Contributor Author

@greptile-ai please re-review

@dennishenry dennishenry changed the title feat: add support for claude cod goal mode for bedrock opus output config feat: add support for claude code goal mode for bedrock opus output config May 26, 2026
@mateo-berri

Copy link
Copy Markdown
Collaborator

This has been merged. Thanks for the contribution!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants