@@ -669,13 +669,20 @@ def _convert_messages_for_litellm(self, messages: list[dict[str, Any]]) -> list[
669669 tr_content = "\n " .join (
670670 b .get ("text" , "" ) for b in tr_content if b .get ("type" ) == "text"
671671 )
672- converted .append (
673- {
674- "role" : "tool" ,
675- "tool_call_id" : tr ["tool_use_id" ],
676- "content" : str (tr_content ),
677- }
678- )
672+ tool_msg : dict [str , Any ] = {
673+ "role" : "tool" ,
674+ "tool_call_id" : tr ["tool_use_id" ],
675+ "content" : str (tr_content ),
676+ }
677+ # Claude Code's moving cache breakpoint usually lands on the
678+ # tail tool_result, not just the system prompt. Carry
679+ # cache_control through so LiteLLM's Bedrock Converse
680+ # transformation can inject a cachePoint here too (#1390
681+ # covers the system-prompt/text-block case; this is the
682+ # tool_result case, out of scope there).
683+ if "cache_control" in tr :
684+ tool_msg ["cache_control" ] = tr ["cache_control" ]
685+ converted .append (tool_msg )
679686 continue
680687
681688 # tool_use blocks → OpenAI assistant message with tool_calls
@@ -966,15 +973,39 @@ async def stream_message(
966973 },
967974 )
968975
976+ # Request usage in the final streaming chunk so cache metrics
977+ # (cache_read_input_tokens / cache_creation_input_tokens) come back at
978+ # all. Without this, LiteLLM/Bedrock never emits a usage chunk over SSE
979+ # and the caller's cache stats always read 0, even when caching is
980+ # working server-side.
981+ kwargs ["stream_options" ] = {"include_usage" : True }
982+
969983 # Stream content — blocks emitted dynamically based on response
970984 response = await acompletion (** kwargs )
971985 output_tokens = 0
972986 current_block_index = - 1
973987 active_block_type : str | None = None # "text" or "tool_use"
974988 tool_block_map : dict [int , int ] = {} # litellm tc.index → SSE block index
975989 stop_reason = "end_turn"
990+ # Populated from the final usage chunk (stream_options.include_usage=True
991+ # above). The message_start emitted before this loop always carries
992+ # input_tokens=0 and no cache fields because LiteLLM/Bedrock only reports
993+ # usage on the trailing chunk. Carry the final cache stats on the terminal
994+ # message_delta instead of emitting a second protocol-invalid
995+ # message_start after content has already streamed.
996+ final_input_tokens = 0
997+ final_cache_read_tokens = 0
998+ final_cache_write_tokens = 0
976999
9771000 async for chunk in response :
1001+ if hasattr (chunk , "usage" ) and chunk .usage :
1002+ cu = chunk .usage
1003+ final_input_tokens = int (getattr (cu , "prompt_tokens" , 0 ) or 0 )
1004+ final_cache_read_tokens = int (getattr (cu , "cache_read_input_tokens" , 0 ) or 0 )
1005+ final_cache_write_tokens = int (
1006+ getattr (cu , "cache_creation_input_tokens" , 0 ) or 0
1007+ )
1008+
9781009 if not hasattr (chunk , "choices" ) or not chunk .choices :
9791010 continue
9801011
@@ -1080,13 +1111,21 @@ async def stream_message(
10801111 data = {"type" : "content_block_stop" , "index" : current_block_index },
10811112 )
10821113
1114+ delta_usage : dict [str , Any ] = {"output_tokens" : output_tokens }
1115+ if final_input_tokens or final_cache_read_tokens or final_cache_write_tokens :
1116+ delta_usage ["input_tokens" ] = final_input_tokens
1117+ if final_cache_read_tokens :
1118+ delta_usage ["cache_read_input_tokens" ] = final_cache_read_tokens
1119+ if final_cache_write_tokens :
1120+ delta_usage ["cache_creation_input_tokens" ] = final_cache_write_tokens
1121+
10831122 # Emit message_delta with correct stop reason
10841123 yield StreamEvent (
10851124 event_type = "message_delta" ,
10861125 data = {
10871126 "type" : "message_delta" ,
10881127 "delta" : {"stop_reason" : stop_reason , "stop_sequence" : None },
1089- "usage" : { "output_tokens" : output_tokens } ,
1128+ "usage" : delta_usage ,
10901129 },
10911130 )
10921131
0 commit comments