Skip to content

fix(bedrock): make prompt caching work on the Nova InvokeModel route - #41343

Merged
mateo-berri merged 4 commits into
mainfrom
litellm_lit5030_bedrock_invoke_nova_prompt_caching
Sep 16, 2026
Merged

mateo-berri merged 4 commits into
mainfrom
litellm_lit5030_bedrock_invoke_nova_prompt_caching

Conversation

@devin-ai-integration

@devin-ai-integration devin-ai-integration Bot commented Sep 16, 2026 •

Copy link
Copy Markdown
Contributor

TLDR

Problem this solves:

  • bedrock/invoke/ Nova requests with cache_control get a Bedrock 400 on the cache point
  • The same request with an image or a tool result 400s too
  • A cached reply comes back as 5 prompt tokens: cache reads vanish from usage and spend
  • Invoke streams bill a 12k-token prompt as 13 tokens and never carry cache counts
  • Nova price entries have no cache read price, so cache reads bill at $0

How it solves it:

  • Fold each standalone cache point into the text block before it, the InvokeModel shape
  • A point behind an image or tool result moves to the last text block
  • Read Invoke's cacheReadInputTokenCount and cacheWriteInputTokenCount next to the Converse names
  • Unwrap every Nova Invoke stream event so the final usage and stop reason reach the client
  • Add cache_read_input_token_cost to the 16 Nova entries that lacked it
  • Drop tool_config injection points on Invoke, which has no tool caching

User Flow

Before: a developer whose app sends the same long system prompt to Nova Pro through a bedrock/invoke/ deployment turns on prompt caching and every request fails, and the requests that do get through report a fraction of the tokens they used

  1. They send POST https://litellm-domain/v1/chat/completions with "model": "nova-pro" (a bedrock/invoke/us.amazon.nova-pro-v1:0 deployment), a 12k-token system message carrying "cache_control": {"type": "ephemeral"}, and the user message Reply with exactly: OK.
  2. They get HTTP 400 Malformed input request: #/system/1: required key [text] not found and x-litellm-response-cost: 0
  3. They send it again a few seconds later, hoping for a cache hit, and get the same HTTP 400
  4. They add "stream": true and "stream_options": {"include_usage": true} and get zero SSE chunks, only the same 400 error body
  5. When the cached prompt also carries an image and a tool result, the 400 reads #/messages/0/content/2: extraneous key [cachePoint] is not permitted
  6. Even when Bedrock answers from a warm cache (the run replays a recorded Nova InvokeModel cache-hit response to show it), the reply reads "prompt_tokens": 5, "cached_tokens": 0, and x-litellm-response-cost for 5 input tokens, so the 12,262 cached tokens vanish from usage and spend
  7. They send the same 12k-token prompt through POST https://litellm-domain/v1/responses with "stream": true and response.completed reports "input_tokens": 13 at a cost of $0.0000168

After: the same requests go through, the second one reads from the cache, and usage and spend carry the cached tokens at the cache read rate

  1. They send the same POST https://litellm-domain/v1/chat/completions with cache_control on the system message
  2. They get HTTP 200 with "cache_creation_input_tokens": 12262, "cached_tokens": 0, and x-litellm-response-cost-cache-creation in the response headers
  3. They send it again a few seconds later and get HTTP 200 with "cached_tokens": 12262, "cache_read_input_tokens": 12262, and x-litellm-response-cost: 0.002466, about a quarter of the first call, with x-litellm-response-cost-cache-read set
  4. With "stream": true the chunks arrive with "finish_reason": "stop" and the last chunk's usage carries the same cached_tokens and cost
  5. The request with the image and the tool result returns HTTP 200 and reports a cache write of 1161 tokens
  6. The reply served from the cache comes back with "prompt_tokens": 12267, "cached_tokens": 12262, and the same cache read cost
  7. POST https://litellm-domain/v1/responses with "stream": true ends with response.completed reporting "input_tokens": 12267 and the real cost

Relevant issues

Related: #31693 maps the Count-suffixed usage fields only and its branch now carries unrelated commits; #34498 and #34542 cover the Claude Invoke route and stay separate

Affected release

Linear ticket

Resolves LIT-5030

Pre-Submission checklist

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

  • I have added meaningful tests
  • The handful of test files covering my change pass locally, e.g. uv run pytest tests/test_litellm/<your_test_file>.py -v. Leave the suites (make test-unit-*, make test-unit) to CI: it finishes in ~15 minutes where a laptop takes an hour or more
  • My PR passes all required CI/CD checks (e.g., lint, schema.d.ts sync check, etc.)
  • My PR's scope is as isolated as possible; it only solves 1 specific problem
  • I have received a Greptile Confidence Score of at least 4/5 before requesting a maintainer review (Greptile reviews automatically once the PR is opened; only comment @greptileai to re-request a review after pushing changes)

Delays in PR merge?

If you're seeing a delay in your PR being merged, ping the LiteLLM Team on Slack (#pr-review).

Screenshots / Proof of Fix

Both sides ran the same seven cases in the same order against live Bedrock in us-east-1, each proxy booted with no database and --num_workers 2 from its own worktree and venv, with LITELLM_LOCAL_MODEL_COST_MAP=True so prices come from that tree's cost map: Before at the merge base 226b1e1 on port 20101, After at 0dff64c on port 38470. Commit 6015437 came after the run and changes two test assertions only, so it cannot change behavior and the After run stands at 0dff64c

Config on both sides (credentials via os.environ):

model_list:
  - model_name: nova-pro
    litellm_params:
      model: bedrock/invoke/us.amazon.nova-pro-v1:0
      aws_region_name: us-east-1
  - model_name: nova-pro-converse
    litellm_params:
      model: bedrock/converse/us.amazon.nova-pro-v1:0
      aws_region_name: us-east-1
  - model_name: nova-pro-replay
    litellm_params:
      model: bedrock/invoke/us.amazon.nova-pro-v1:0
      aws_region_name: us-east-1
      aws_bedrock_runtime_endpoint: http://127.0.0.1:59442
general_settings:
  master_key: sk-lit5030
litellm_settings:
  drop_params: true

nova-pro-replay points at a local endpoint that replays one Nova InvokeModel response recorded from a live cache hit (cacheReadInputTokenCount: 12262), so both sides read the same cache-hit body regardless of Bedrock's cache timing. Payloads:

  • req.json: nova-pro, a 69,318-character system message (Section 1: The quarterly compliance handbook ...) with "cache_control": {"type": "ephemeral"}, the user message Reply with exactly: OK., max_tokens: 20
  • req_stream.json: req.json plus "stream": true, "stream_options": {"include_usage": true}
  • req_messages.json: the Anthropic Messages shape for /v1/messages, the same text as a system block with cache_control, the same user message
  • req_responses_stream.json: the same text as instructions, input Reply with exactly: OK., "stream": true, max_output_tokens: 20
  • req_image_tool.json: a user turn with a text block plus an image block carrying cache_control, an assistant get_weather tool call, a tool result sunny carrying cache_control, the get_weather tool, max_tokens: 30
  • req_converse.json: req.json with "model": "nova-pro-converse" and the text prefixed Converse control.
  • req_replay.json: req.json with "model": "nova-pro-replay" and no cache_control

J below is the jq summary filter {status: (.error.message // .choices[0].message.content), finish_reason: .choices[0].finish_reason, usage}

Before (226b1e1)

Chat cache write then read

  1. curl -s -o before_run1_body.json -w 'HTTP %{http_code}\n' -D before_run1_headers.txt localhost:20101/v1/chat/completions -H 'Authorization: Bearer sk-lit5030' -H 'Content-Type: application/json' -d @req.json
    HTTP 400
    
  2. jq -c "$J" before_run1_body.json
    {"status":"litellm.BadRequestError: BedrockException - {\"message\":\"Malformed input request: #/system/1: required key [text] not found, please reformat your input and try again.\"}. Received Model Group=nova-pro\nAvailable Model Group Fallbacks=None","finish_reason":null,"usage":null}
    
  3. grep -i 'x-litellm-response-cost' before_run1_headers.txt
    x-litellm-response-cost: 0
    
  4. Five seconds later, the same request again: curl -s -o before_run2_body.json -w 'HTTP %{http_code}\n' -D before_run2_headers.txt localhost:20101/v1/chat/completions -H 'Authorization: Bearer sk-lit5030' -H 'Content-Type: application/json' -d @req.json
    HTTP 400
    
  5. jq -c "$J" before_run2_body.json
    {"status":"litellm.BadRequestError: BedrockException - {\"message\":\"Malformed input request: #/system/1: required key [text] not found, please reformat your input and try again.\"}. Received Model Group=nova-pro\nAvailable Model Group Fallbacks=None","finish_reason":null,"usage":null}
    
  6. grep -i 'x-litellm-response-cost' before_run2_headers.txt
    x-litellm-response-cost: 0
    

Chat stream usage

  1. curl -s -N localhost:20101/v1/chat/completions -H 'Authorization: Bearer sk-lit5030' -H 'Content-Type: application/json' -d @req_stream.json | tee before_stream_raw.txt | grep -c '^data:'
    0
    
  2. grep -o '"finish_reason":"[a-z_]*"' before_stream_raw.txt | sort | uniq -c
    (no output)
    
  3. grep '"usage":{' before_stream_raw.txt | sed 's/^data: //' | jq -c '.usage // .error'
    (no output)
    
  4. tail -c 300 before_stream_raw.txt
    litellm.BadRequestError: BedrockException - {\"message\":\"Malformed input request: #/system/1: required key [text] not found, please reformat your input and try again.\"}. Received Model Group=nova-pro\nAvailable Model Group Fallbacks=None","type":"invalid_request_error","param":null,"code":"400"}}
    

Messages API route

  1. curl -s -o before_messages_body.json -w 'HTTP %{http_code}\n' localhost:20101/v1/messages -H 'Authorization: Bearer sk-lit5030' -H 'Content-Type: application/json' -d @req_messages.json
    HTTP 200
    
  2. jq -c 'if .error then .error else {text: .content[0].text, stop_reason, usage} end' before_messages_body.json
    {"text":"OK.","stop_reason":"end_turn","usage":{"input_tokens":12267,"output_tokens":3}}
    

Responses API stream usage

  1. curl -s -N localhost:20101/v1/responses -H 'Authorization: Bearer sk-lit5030' -H 'Content-Type: application/json' -d @req_responses_stream.json | tee before_responses_raw.txt | grep -o '^data: {"type":"[a-z_.]*"' | sed 's/^data: //' | sort | uniq -c
       1 {"type":"response.completed"
       1 {"type":"response.content_part.added"
       1 {"type":"response.content_part.done"
       1 {"type":"response.created"
       1 {"type":"response.in_progress"
       1 {"type":"response.output_item.added"
       1 {"type":"response.output_item.done"
       2 {"type":"response.output_text.delta"
       1 {"type":"response.output_text.done"
    
  2. grep '^data: {' before_responses_raw.txt | sed 's/^data: //' | jq -c 'select(.type == "response.completed") | {status: .response.status, usage: .response.usage}'
    {"status":"completed","usage":{"input_tokens":13,"output_tokens":2,"output_tokens_details":{"reasoning_tokens":0},"total_tokens":15,"cost":0.0000168}}
    

Chat image and tool result cache

  1. curl -s -o before_image_tool_body.json -w 'HTTP %{http_code}\n' localhost:20101/v1/chat/completions -H 'Authorization: Bearer sk-lit5030' -H 'Content-Type: application/json' -d @req_image_tool.json
    HTTP 400
    
  2. jq -c "$J" before_image_tool_body.json
    {"status":"litellm.BadRequestError: BedrockException - {\"message\":\"Malformed input request: #/messages/0/content/2: extraneous key [cachePoint] is not permitted, please reformat your input and try again.\"}. Received Model Group=nova-pro\nAvailable Model Group Fallbacks=None","finish_reason":null,"usage":null}
    

Converse route control

  1. curl -s -o before_converse_body.json -w 'HTTP %{http_code}\n' localhost:20101/v1/chat/completions -H 'Authorization: Bearer sk-lit5030' -H 'Content-Type: application/json' -d @req_converse.json
    HTTP 200
    
  2. jq -c "$J" before_converse_body.json
    {"status":"OK.","finish_reason":"stop","usage":{"completion_tokens":3,"prompt_tokens":12270,"total_tokens":12273,"completion_tokens_details":{"reasoning_tokens":0,"text_tokens":3},"prompt_tokens_details":{"cached_tokens":0,"text_tokens":5,"cache_write_tokens":12265,"cache_creation_tokens":12265},"cache_creation_input_tokens":12265,"cache_read_input_tokens":0}}
    

Replay cache-hit body

  1. curl -s -o before_replay_body.json -w 'HTTP %{http_code}\n' -D before_replay_headers.txt localhost:20101/v1/chat/completions -H 'Authorization: Bearer sk-lit5030' -H 'Content-Type: application/json' -d @req_replay.json
    HTTP 200
    
  2. jq -c "$J" before_replay_body.json
    {"status":"OK.","finish_reason":"stop","usage":{"completion_tokens":3,"prompt_tokens":5,"total_tokens":12270,"completion_tokens_details":{"reasoning_tokens":0,"text_tokens":3},"prompt_tokens_details":{"cached_tokens":0,"text_tokens":5,"cache_write_tokens":0,"cache_creation_tokens":0},"cache_creation_input_tokens":0,"cache_read_input_tokens":0}}
    
  3. grep -i 'x-litellm-response-cost' before_replay_headers.txt
    x-litellm-response-cost: 1.3599999999999999e-05
    x-litellm-response-cost-original: 1.3599999999999999e-05
    x-litellm-response-cost-discount-amount: 0.0
    x-litellm-response-cost-margin-amount: 0.0
    x-litellm-response-cost-margin-percent: 0.0
    x-litellm-response-cost-input: 4e-06
    x-litellm-response-cost-output: 9.6e-06
    x-litellm-response-cost-tool-usage: 0.0
    

After (0dff64c)

Chat cache write then read

  1. curl -s -o after_run1_body.json -w 'HTTP %{http_code}\n' -D after_run1_headers.txt localhost:38470/v1/chat/completions -H 'Authorization: Bearer sk-lit5030' -H 'Content-Type: application/json' -d @req.json
    HTTP 200
    
  2. jq -c "$J" after_run1_body.json
    {"status":"OK.","finish_reason":"stop","usage":{"completion_tokens":3,"prompt_tokens":12267,"total_tokens":12270,"completion_tokens_details":{"reasoning_tokens":0,"text_tokens":3},"prompt_tokens_details":{"cached_tokens":0,"text_tokens":5,"cache_write_tokens":12262,"cache_creation_tokens":12262},"cache_creation_input_tokens":12262,"cache_read_input_tokens":0}}
    
  3. grep -i 'x-litellm-response-cost' after_run1_headers.txt
    x-litellm-response-cost: 0.0098232
    x-litellm-response-cost-original: 0.0098232
    x-litellm-response-cost-discount-amount: 0.0
    x-litellm-response-cost-margin-amount: 0.0
    x-litellm-response-cost-margin-percent: 0.0
    x-litellm-response-cost-input: 4.000000000000531e-06
    x-litellm-response-cost-output: 9.6e-06
    x-litellm-response-cost-cache-creation: 0.0098096
    x-litellm-response-cost-tool-usage: 0.0
    
  4. Five seconds later, the same request again: curl -s -o after_run2_body.json -w 'HTTP %{http_code}\n' -D after_run2_headers.txt localhost:38470/v1/chat/completions -H 'Authorization: Bearer sk-lit5030' -H 'Content-Type: application/json' -d @req.json
    HTTP 200
    
  5. jq -c "$J" after_run2_body.json
    {"status":"OK.","finish_reason":"stop","usage":{"completion_tokens":3,"prompt_tokens":12267,"total_tokens":12270,"completion_tokens_details":{"reasoning_tokens":0,"text_tokens":3},"prompt_tokens_details":{"cached_tokens":12262,"text_tokens":5,"cache_write_tokens":0,"cache_creation_tokens":0},"cache_creation_input_tokens":0,"cache_read_input_tokens":12262}}
    
  6. grep -i 'x-litellm-response-cost' after_run2_headers.txt
    x-litellm-response-cost: 0.002466
    x-litellm-response-cost-original: 0.002466
    x-litellm-response-cost-discount-amount: 0.0
    x-litellm-response-cost-margin-amount: 0.0
    x-litellm-response-cost-margin-percent: 0.0
    x-litellm-response-cost-input: 4.000000000000097e-06
    x-litellm-response-cost-output: 9.6e-06
    x-litellm-response-cost-cache-read: 0.0024524
    x-litellm-response-cost-tool-usage: 0.0
    

Chat stream usage

  1. curl -s -N localhost:38470/v1/chat/completions -H 'Authorization: Bearer sk-lit5030' -H 'Content-Type: application/json' -d @req_stream.json | tee after_stream_raw.txt | grep -c '^data:'
    5
    
  2. grep -o '"finish_reason":"[a-z_]*"' after_stream_raw.txt | sort | uniq -c
       1 "finish_reason":"stop"
    
  3. grep '"usage":{' after_stream_raw.txt | sed 's/^data: //' | jq -c '.usage // .error'
    {"completion_tokens":3,"prompt_tokens":12267,"total_tokens":12270,"completion_tokens_details":{"reasoning_tokens":0,"text_tokens":3},"prompt_tokens_details":{"cached_tokens":12262,"text_tokens":5,"cache_write_tokens":0,"cache_creation_tokens":0},"cost":0.002466,"cache_creation_input_tokens":0,"cache_read_input_tokens":12262}
    
  4. tail -c 300 after_stream_raw.txt
    7,"total_tokens":12270,"completion_tokens_details":{"reasoning_tokens":0,"text_tokens":3},"prompt_tokens_details":{"cached_tokens":12262,"text_tokens":5,"cache_write_tokens":0,"cache_creation_tokens":0},"cost":0.002466,"cache_creation_input_tokens":0,"cache_read_input_tokens":12262}}
    
    data: [DONE]
    

Messages API route

  1. curl -s -o after_messages_body.json -w 'HTTP %{http_code}\n' localhost:38470/v1/messages -H 'Authorization: Bearer sk-lit5030' -H 'Content-Type: application/json' -d @req_messages.json
    HTTP 200
    
  2. jq -c 'if .error then .error else {text: .content[0].text, stop_reason, usage} end' after_messages_body.json
    {"text":"OK.","stop_reason":"end_turn","usage":{"input_tokens":12267,"output_tokens":3}}
    

Responses API stream usage

  1. curl -s -N localhost:38470/v1/responses -H 'Authorization: Bearer sk-lit5030' -H 'Content-Type: application/json' -d @req_responses_stream.json | tee after_responses_raw.txt | grep -o '^data: {"type":"[a-z_.]*"' | sed 's/^data: //' | sort | uniq -c
       1 {"type":"response.completed"
       1 {"type":"response.content_part.added"
       1 {"type":"response.content_part.done"
       1 {"type":"response.created"
       1 {"type":"response.in_progress"
       1 {"type":"response.output_item.added"
       1 {"type":"response.output_item.done"
       2 {"type":"response.output_text.delta"
       1 {"type":"response.output_text.done"
    
  2. grep '^data: {' after_responses_raw.txt | sed 's/^data: //' | jq -c 'select(.type == "response.completed") | {status: .response.status, usage: .response.usage}'
    {"status":"completed","usage":{"input_tokens":12267,"input_tokens_details":{"cached_tokens":0,"text_tokens":12267,"cache_write_tokens":0},"output_tokens":3,"output_tokens_details":{"reasoning_tokens":0,"text_tokens":3},"total_tokens":12270,"cost":0.009823199999999999}}
    

Chat image and tool result cache

  1. curl -s -o after_image_tool_body.json -w 'HTTP %{http_code}\n' localhost:38470/v1/chat/completions -H 'Authorization: Bearer sk-lit5030' -H 'Content-Type: application/json' -d @req_image_tool.json
    HTTP 200
    
  2. jq -c "$J" after_image_tool_body.json
    {"status":"The weather in SF is currently sunny.","finish_reason":"stop","usage":{"completion_tokens":9,"prompt_tokens":1161,"total_tokens":1170,"completion_tokens_details":{"reasoning_tokens":0,"text_tokens":9},"prompt_tokens_details":{"cached_tokens":0,"text_tokens":0,"cache_write_tokens":1161,"cache_creation_tokens":1161},"cache_creation_input_tokens":1161,"cache_read_input_tokens":0}}
    

Converse route control

  1. curl -s -o after_converse_body.json -w 'HTTP %{http_code}\n' localhost:38470/v1/chat/completions -H 'Authorization: Bearer sk-lit5030' -H 'Content-Type: application/json' -d @req_converse.json
    HTTP 200
    
  2. jq -c "$J" after_converse_body.json
    {"status":"OK.","finish_reason":"stop","usage":{"completion_tokens":3,"prompt_tokens":12270,"total_tokens":12273,"completion_tokens_details":{"reasoning_tokens":0,"text_tokens":3},"prompt_tokens_details":{"cached_tokens":0,"text_tokens":5,"cache_write_tokens":12265,"cache_creation_tokens":12265},"cache_creation_input_tokens":12265,"cache_read_input_tokens":0}}
    

Replay cache-hit body

  1. curl -s -o after_replay_body.json -w 'HTTP %{http_code}\n' -D after_replay_headers.txt localhost:38470/v1/chat/completions -H 'Authorization: Bearer sk-lit5030' -H 'Content-Type: application/json' -d @req_replay.json
    HTTP 200
    
  2. jq -c "$J" after_replay_body.json
    {"status":"OK.","finish_reason":"stop","usage":{"completion_tokens":3,"prompt_tokens":12267,"total_tokens":12270,"completion_tokens_details":{"reasoning_tokens":0,"text_tokens":3},"prompt_tokens_details":{"cached_tokens":12262,"text_tokens":5,"cache_write_tokens":0,"cache_creation_tokens":0},"cache_creation_input_tokens":0,"cache_read_input_tokens":12262}}
    
  3. grep -i 'x-litellm-response-cost' after_replay_headers.txt
    x-litellm-response-cost: 0.002466
    x-litellm-response-cost-original: 0.002466
    x-litellm-response-cost-discount-amount: 0.0
    x-litellm-response-cost-margin-amount: 0.0
    x-litellm-response-cost-margin-percent: 0.0
    x-litellm-response-cost-input: 4.000000000000097e-06
    x-litellm-response-cost-output: 9.6e-06
    x-litellm-response-cost-cache-read: 0.0024524
    x-litellm-response-cost-tool-usage: 0.0
    

Observations from the run, each noting whether this PR causes it:

  • /v1/messages reports no cache fields on either side: the Messages adapter drops cache_control for non-Claude models, left alone by this PR
  • Bedrock counted the whole 1161-token image request as a cache write, not caused by this PR
  • The Converse control reads back cached tokens on both sides, but only After prices them (see Caveats)

Type

🐛 Bug Fix

Caveats (if any)

Low

  • Nova cache reads on bedrock/converse/ now cost 25% of input instead of $0, so spend rises for teams already caching there. Keeping the $0 rate would keep under-reporting what AWS bills, so it stays
  • A standalone cache point behind an image or tool result moves to the last text block before it, and is dropped when there is none. The alternatives were the Before 400 or dropping every such point; anchoring keeps the longest prefix Bedrock accepts cached
  • tool_config injection points are dropped on Invoke, so tools are never cached there (Bedrock 400s on the point Before)
  • /v1/messages never sends a cache point for Nova, since the adapter drops cache_control for non-Claude models (pre-existing)
  • A /v1/responses request with cache_control rides the same chat transform after the Responses bridge and was not run live with a cache point; the Responses leg proves the stream usage only
  • Nova batch input records keep the Converse cache point shape (pre-existing, not tested)
  • Nova passthrough streams now unwrap every event: not observed live, since both proxies answer /bedrock/model/nova-pro/invoke-with-response-stream with Bedrock's UnknownOperationException before and after
  • A merge with current main (56f1e89288) applied cleanly and passed 451 unit tests, but no proxy run was made on the merged tree
  • The red CircleCI jobs at 6015437 (Together AI openai/gpt-oss-20b non-serverless 400s across llm_translation_testing and local_testing, the budget reset and router fallback tests, the Sumo Logic NDJSON test) fail identically on every other branch pipeline today and touch no Bedrock code. The AssemblyAI passthrough test in proxy_pass_through_endpoint_tests failed on an AssemblyAI 500 that the sibling pipelines hit on the same suite, and it passed at 0dff64c

Final Attestation

  • The tests check the right things, including the edge cases, and regressions in the respective real-world customer use-cases are not possible after this PR

  • 0dff64c passes /live-pr-risk

Nova InvokeModel rejects the standalone cachePoint blocks the shared Converse transform emits, so each one is folded into the block it caches and tool_config injection points are dropped before the transform runs, since this route has no tool caching to credit. Usage reads Bedrock's Count-suffixed cache keys and adds cached tokens into prompt_tokens, streaming routes every wrapped InvokeModel event through the Converse chunk parser and tolerates the missing totalTokens, and the Nova 1 cost-map entries gain cache_read_input_token_cost at a quarter of the input rate
@devin-ai-integration

Copy link
Copy Markdown
Contributor Author

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment, CI, and merge conflict monitoring

@codspeed

codspeed Bot commented Sep 16, 2026 •

Copy link
Copy Markdown
Contributor

Merging this PR will not alter performance

✅ 31 untouched benchmarks


Comparing litellm_lit5030_bedrock_invoke_nova_prompt_caching (6015437) with main (365c6de)

Open in CodSpeed

@greptile-apps

greptile-apps Bot commented Sep 16, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR enables prompt caching on the Bedrock Nova InvokeModel route by translating cache points into Nova’s accepted request shape, normalizing InvokeModel cache usage fields, preserving stream lifecycle events, and adding cache-read pricing metadata and regression coverage

  • Moves standalone cache points onto eligible text blocks
  • Drops unsupported tool-configuration cache points
  • Normalizes cache read and write counts across Converse and InvokeModel responses
  • Unwraps all Nova InvokeModel stream event types
  • Adds cache-read prices and focused request, usage, billing, and streaming tests

Confidence Score: 5/5

The PR appears safe to merge, with no outstanding correctness or repository-rule findings

The latest changes replace vendor-rate assertions with stable range and billing invariants, and the prior thread was manually resolved after that correction

Important Files Changed
Filename Overview
litellm/llms/bedrock/chat/invoke_transformations/amazon_nova_transformation.py Converts Converse-style cache points into the inline text-block representation accepted by Nova InvokeModel
litellm/llms/bedrock/chat/converse_transformation.py Normalizes Converse and InvokeModel cache usage keys while handling streaming usage without totalTokens
litellm/llms/bedrock/chat/invoke_handler.py Unwraps Nova InvokeModel lifecycle, content, stop, and metadata stream events through the shared Converse parser
model_prices_and_context_window.json Adds discounted cache-read token costs for supported Nova model entries
tests/test_litellm/llms/bedrock/chat/test_converse_transformation.py Covers usage normalization and billing through stable LiteLLM-owned pricing invariants
tests/test_litellm/llms/bedrock/chat/test_invoke_handler.py Verifies Nova InvokeModel streaming content, finish state, and cache usage propagation
tests/test_litellm/llms/bedrock/chat/invoke_transformations/test_amazon_nova_transformation.py Covers cache-point placement, unsupported block handling, and tool-configuration behavior

Reviews (3): Last reviewed commit: "test(bedrock): assert the Nova cache-rea..." | Re-trigger Greptile

@codecov

codecov Bot commented Sep 16, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 96.49123% with 2 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
...voke_transformations/amazon_nova_transformation.py 94.11% 2 Missing ⚠️

📢 Thoughts on this report? Let us know!

@mateo-berri

Copy link
Copy Markdown
Contributor

@greptileai

Comment thread tests/test_litellm/llms/bedrock/chat/test_converse_transformation.py Outdated
@mateo-berri

Copy link
Copy Markdown
Contributor

@greptileai

@mateo-berri mateo-berri added run-ci and removed run-ci labels Sep 16, 2026
@mateo-berri

Copy link
Copy Markdown
Contributor

bugbot run

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit 6015437. Configure here.

@mateo-berri mateo-berri left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@mateo-berri
mateo-berri merged commit 8e52437 into main Sep 16, 2026
130 of 139 checks passed
@mateo-berri
mateo-berri deleted the litellm_lit5030_bedrock_invoke_nova_prompt_caching branch September 16, 2026 20:42
Stanley00 pushed a commit to stanley-fork/litellm that referenced this pull request Sep 16, 2026
…ext merge

PR BerriAI#41343 and PR BerriAI#41112 both added cache_read_input_token_cost to the six amazon.nova-{micro,lite,pro}-v1:0 and us.amazon.nova-* entries, one at the top of each entry and one at the bottom. The merge kept both, so every PR now fails test_price_map_has_no_duplicate_keys. Both copies carried the same value, so this only removes the trailing duplicate in both price files
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant