fix(anthropic-messages): build thinking usage without mutation and expect it in bridged integration cases

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
mateo 2026-10-31 23:02:47 +00:00
parent 987250ffa8
commit 7adf073fb7
13 changed files with 61 additions and 33 deletions

View file

@ -180,6 +180,7 @@ from litellm.types.llms.anthropic import (
ServerToolUsage,
StreamingContentBlockDeltaType,
UsageDelta,
UsageDeltaOutputTokensDetails,
UsageIteration,
)
from litellm.types.llms.anthropic_messages.anthropic_response import (
@ -1608,14 +1609,18 @@ class LiteLLMAnthropicMessagesAdapter:
if cache_read_input_tokens > 0:
usage_delta["cache_read_input_tokens"] = cache_read_input_tokens
reasoning_tokens: Final = cls._get_reasoning_tokens(usage)
if reasoning_tokens > 0:
usage_delta["output_tokens_details"] = {"thinking_tokens": reasoning_tokens}
usage_with_thinking: Final = (
usage_delta
| UsageDelta(output_tokens_details=UsageDeltaOutputTokensDetails(thinking_tokens=reasoning_tokens))
if reasoning_tokens > 0
else usage_delta
)
if web_search_requests > 0:
return UsageDelta(
**usage_delta,
**usage_with_thinking,
server_tool_use=ServerToolUsage(web_search_requests=web_search_requests),
)
return usage_delta
return usage_with_thinking
_translate_openai_usage_to_anthropic_usage_delta = translate_openai_usage_to_anthropic_usage_delta

View file

@ -465,7 +465,11 @@ def test_messages_stream_opens_thinking_block_at_index_zero_for_reasoning_conten
"message_stop",
), text
message_delta: Final = next(event for event in events if event["type"] == "message_delta")
assert message_delta["usage"] == {"input_tokens": 11, "output_tokens": 6}, text
assert message_delta["usage"] == {
"input_tokens": 11,
"output_tokens": 6,
"output_tokens_details": {"thinking_tokens": 4},
}, text
requests: Final = wire.drain()
assert len(requests) == 1
outbound: Final = json.loads(requests[0].body)

View file

@ -409,7 +409,7 @@ GPT_6_LUNA_TEST_CASE: Final = TranslationTestCase(
**MESSAGES_BRIDGE_RESPONSE,
"id": "resp_0534b0c3898d6d1b006ad023ba700c8195b63f1819ab327ab3",
"model": "azure/gpt-6-luna",
"usage": {"input_tokens": 309, "output_tokens": 114},
"usage": {"input_tokens": 309, "output_tokens": 114, "output_tokens_details": {"thinking_tokens": 50}},
"content": [
{
"type": "text",
@ -543,7 +543,7 @@ GPT_6_1_SOL_TEST_CASE: Final = TranslationTestCase(
**MESSAGES_BRIDGE_RESPONSE,
"id": "resp_0a4a896afe94ce65006ad023b047248193999d7c3a0c4fec39",
"model": "azure/gpt-6.1-sol",
"usage": {"input_tokens": 309, "output_tokens": 137},
"usage": {"input_tokens": 309, "output_tokens": 137, "output_tokens_details": {"thinking_tokens": 72}},
"content": [
{
"type": "text",
@ -679,7 +679,7 @@ GPT_6_ASTRA_TEST_CASE: Final = TranslationTestCase(
**MESSAGES_BRIDGE_RESPONSE,
"id": "resp_019af305501e4822006ad023c3a50881958c4cf40623699cf8",
"model": "azure/gpt-6-astra",
"usage": {"input_tokens": 309, "output_tokens": 138},
"usage": {"input_tokens": 309, "output_tokens": 138, "output_tokens_details": {"thinking_tokens": 72}},
"content": [
{
"type": "text",
@ -812,7 +812,7 @@ GPT_5_6_TERRA_TEST_CASE: Final = TranslationTestCase(
**MESSAGES_BRIDGE_RESPONSE,
"id": "resp_0d178c5d2401a624006ad023cf1c588194ab41d5803ed29401",
"model": "azure/gpt-5.6-terra",
"usage": {"input_tokens": 309, "output_tokens": 92},
"usage": {"input_tokens": 309, "output_tokens": 92, "output_tokens_details": {"thinking_tokens": 22}},
"content": [
{
"type": "text",

View file

@ -55,7 +55,7 @@ GROK_4_7_TEST_CASE: Final = TranslationTestCase(
"role": "assistant",
"model": "bedrock/global.xai.grok-4.7",
"stop_sequence": None,
"usage": {"input_tokens": 30, "output_tokens": 37},
"usage": {"input_tokens": 30, "output_tokens": 37, "output_tokens_details": {"thinking_tokens": 27}},
"content": [{"type": "text", "text": "Hello."}],
"stop_reason": "end_turn",
"stop_details": None,

View file

@ -277,7 +277,12 @@ CLAUDE_HAIKU_5_5_TEST_CASE: Final = TranslationTestCase(
**BEDROCK_CONVERSE_MESSAGES_RESPONSE,
"id": ANY,
"model": "bedrock/converse/us.anthropic.claude-haiku-5-5",
"usage": {"input_tokens": 4, "output_tokens": 297, "cache_creation_input_tokens": 853},
"usage": {
"input_tokens": 4,
"output_tokens": 297,
"cache_creation_input_tokens": 853,
"output_tokens_details": {"thinking_tokens": 147},
},
"content": [
{
"type": "thinking",
@ -403,7 +408,12 @@ CLAUDE_SONNET_5_5_TEST_CASE: Final = TranslationTestCase(
**BEDROCK_CONVERSE_MESSAGES_RESPONSE,
"id": ANY,
"model": "bedrock/converse/us.anthropic.claude-sonnet-5-5",
"usage": {"input_tokens": 4, "output_tokens": 430, "cache_creation_input_tokens": 853},
"usage": {
"input_tokens": 4,
"output_tokens": 430,
"cache_creation_input_tokens": 853,
"output_tokens_details": {"thinking_tokens": 155},
},
"content": [
{
"type": "thinking",
@ -504,7 +514,12 @@ CLAUDE_OPUS_5_5_TEST_CASE: Final = TranslationTestCase(
**BEDROCK_CONVERSE_MESSAGES_RESPONSE,
"id": ANY,
"model": "bedrock/converse/us.anthropic.claude-opus-5-5",
"usage": {"input_tokens": 4, "output_tokens": 190, "cache_creation_input_tokens": 853},
"usage": {
"input_tokens": 4,
"output_tokens": 190,
"cache_creation_input_tokens": 853,
"output_tokens_details": {"thinking_tokens": 35},
},
"content": [
{
"type": "thinking",

View file

@ -534,7 +534,7 @@ GPT_6_1_SOL_TEST_CASE: Final = TranslationTestCase(
**BEDROCK_MANTLE_OPENAI_RESPONSES_MESSAGES_RESPONSE,
"id": "resp_n4higs7hpzdjowbay2ujeesbgcxavsl4pbkzhbh7pjpimjjaciaa",
"model": "bedrock_mantle/openai.gpt-6.1-sol",
"usage": {"input_tokens": 309, "output_tokens": 141},
"usage": {"input_tokens": 309, "output_tokens": 141, "output_tokens_details": {"thinking_tokens": 76}},
"content": [
{
"type": "text",
@ -620,7 +620,7 @@ GPT_6_LUNA_TEST_CASE: Final = TranslationTestCase(
**BEDROCK_MANTLE_OPENAI_RESPONSES_MESSAGES_RESPONSE,
"id": "resp_di3jhnf3mehuuht6smddkuygfeo7rzxdxmwvrc52aigdygbts5ca",
"model": "bedrock_mantle/openai.gpt-6-luna",
"usage": {"input_tokens": 309, "output_tokens": 119},
"usage": {"input_tokens": 309, "output_tokens": 119, "output_tokens_details": {"thinking_tokens": 54}},
"content": [
{
"type": "text",
@ -706,7 +706,7 @@ GPT_6_ASTRA_TEST_CASE: Final = TranslationTestCase(
**BEDROCK_MANTLE_OPENAI_RESPONSES_MESSAGES_RESPONSE,
"id": "resp_eznficwibpblmnocp4vuvti6d5bxboyxws4vqf5bk2lboyvvbx7q",
"model": "bedrock_mantle/openai.gpt-6-astra",
"usage": {"input_tokens": 309, "output_tokens": 138},
"usage": {"input_tokens": 309, "output_tokens": 138, "output_tokens_details": {"thinking_tokens": 73}},
"content": [
{
"type": "text",
@ -793,7 +793,7 @@ GPT_5_6_TERRA_TEST_CASE: Final = TranslationTestCase(
**BEDROCK_MANTLE_OPENAI_RESPONSES_MESSAGES_RESPONSE,
"id": "resp_fqwklh5v6lnfftfaq6ddfbclshh3vkgmth75hufjjxjrhn726b2a",
"model": "bedrock_mantle/openai.gpt-5.6-terra",
"usage": {"input_tokens": 309, "output_tokens": 109},
"usage": {"input_tokens": 309, "output_tokens": 109, "output_tokens_details": {"thinking_tokens": 23}},
"content": [
{
"type": "text",

View file

@ -152,6 +152,7 @@ GEMINI_3_8_FLASH_TEST_CASE: Final = TranslationTestCase(
"usage": {
"input_tokens": 354,
"output_tokens": 419,
"output_tokens_details": {"thinking_tokens": 355},
},
},
)
@ -286,6 +287,7 @@ GEMINI_3_1_PRO_PREVIEW_TEST_CASE: Final = TranslationTestCase(
"usage": {
"input_tokens": 354,
"output_tokens": 302,
"output_tokens_details": {"thinking_tokens": 233},
},
},
)
@ -416,6 +418,7 @@ GEMINI_4_ARGON_TEST_CASE: Final = TranslationTestCase(
"usage": {
"input_tokens": 412,
"output_tokens": 192,
"output_tokens_details": {"thinking_tokens": 124},
},
},
)
@ -467,7 +470,7 @@ GEMINI_3_5_FLASH_TEST_CASE: Final = TranslationTestCase(
"role": "assistant",
"model": "gemini/gemini-3.5-flash",
"stop_sequence": None,
"usage": {"input_tokens": 10, "output_tokens": 127},
"usage": {"input_tokens": 10, "output_tokens": 127, "output_tokens_details": {"thinking_tokens": 125}},
"content": [{"type": "text", "text": "Hello."}],
"stop_reason": "end_turn",
"stop_details": None,

View file

@ -394,7 +394,7 @@ GPT_6_LUNA_TEST_CASE: Final = TranslationTestCase(
**MESSAGES_BRIDGE_RESPONSE,
"id": "resp_bGl0ZWxsbTpjdXN0b21fbGxtX3Byb3ZpZGVyOm9wZW5haTttb2RlbF9pZDo0ZmY4OWU3OTgyMGFkZDVjMmM1ZmZlM2MyODk4ZDNkOGEzZDczY2IxY2ExN2JmZGMxYTRmNTRlZmFlNzhkMTIzO3Jlc3BvbnNlX2lkOnJlc3BfMDNlYzQ5MWM1Y2FhZTAyOTAwNmFkMDIzOWMwODM0ODdkMDk5YTIxZDYwYmU2YjVmZGU=",
"model": "openai/gpt-6-luna",
"usage": {"input_tokens": 309, "output_tokens": 214},
"usage": {"input_tokens": 309, "output_tokens": 214, "output_tokens_details": {"thinking_tokens": 150}},
"content": [
{
"type": "redacted_thinking",
@ -517,7 +517,7 @@ GPT_6_1_SOL_TEST_CASE: Final = TranslationTestCase(
**MESSAGES_BRIDGE_RESPONSE,
"id": "resp_bGl0ZWxsbTpjdXN0b21fbGxtX3Byb3ZpZGVyOm9wZW5haTttb2RlbF9pZDphMTYxN2NmYzNlODRjNzYxNDdjNTgxMmFhMjIwNGU0MDczOTM0M2YyMTg4YzQxNzIxMzRlOTc1ZTQ0YzQ3MjgyO3Jlc3BvbnNlX2lkOnJlc3BfMGFmNjdjYjc4YmVhZDJjNjAwNmFkMDIzYTcyNzM0ODdkMGIxNzY4Yjc3ZjY5NTZiNzM=",
"model": "openai/gpt-6.1-sol",
"usage": {"input_tokens": 309, "output_tokens": 140},
"usage": {"input_tokens": 309, "output_tokens": 140, "output_tokens_details": {"thinking_tokens": 75}},
"content": [
{
"type": "redacted_thinking",
@ -640,7 +640,7 @@ GPT_6_ASTRA_TEST_CASE: Final = TranslationTestCase(
**MESSAGES_BRIDGE_RESPONSE,
"id": "resp_bGl0ZWxsbTpjdXN0b21fbGxtX3Byb3ZpZGVyOm9wZW5haTttb2RlbF9pZDphODRkZDk3MTcwNzk0ZDlhYzQ5ODhlZWMzOTJmN2Q5YjQwN2MyNDY1YmJhZTA4MGE1ZGYzMThlOTViOTA2NzlkO3Jlc3BvbnNlX2lkOnJlc3BfMDk2NGE0MWY1ODE1M2FjNTAwNmFkMDIzOGY5YmMwODdkMGE5ZTc5ZmRhMzc1YmVlM2M=",
"model": "openai/gpt-6-astra",
"usage": {"input_tokens": 309, "output_tokens": 152},
"usage": {"input_tokens": 309, "output_tokens": 152, "output_tokens_details": {"thinking_tokens": 87}},
"content": [
{
"type": "redacted_thinking",
@ -764,7 +764,7 @@ GPT_5_6_TERRA_TEST_CASE: Final = TranslationTestCase(
**MESSAGES_BRIDGE_RESPONSE,
"id": "resp_bGl0ZWxsbTpjdXN0b21fbGxtX3Byb3ZpZGVyOm9wZW5haTttb2RlbF9pZDo0Mjg5MWEzMzRjNTlhMjhlMjQ2MjQ0YTQ4YTJjOGVlMTM5NGYyZGNhZWMxYmVjZDhmZDk2N2NmNjYxNWNmZDM5O3Jlc3BvbnNlX2lkOnJlc3BfMDg1MjZjY2FiOWFhNjUzZTAwNmFkMDIzODg0YTFjODdkMDlhMjNmNmQ1YzYxZDY1ZDM=",
"model": "openai/gpt-5.6-terra",
"usage": {"input_tokens": 309, "output_tokens": 107},
"usage": {"input_tokens": 309, "output_tokens": 107, "output_tokens_details": {"thinking_tokens": 42}},
"content": [
{
"type": "redacted_thinking",

View file

@ -201,7 +201,7 @@ GPT_6_1_SOL_TEST_CASE: Final = TranslationTestCase(
**MESSAGES_BRIDGE_RESPONSE,
"id": "resp_bGl0ZWxsbTpjdXN0b21fbGxtX3Byb3ZpZGVyOm9wZW5haTttb2RlbF9pZDo3ZGE3NDY3Njc3N2IyZjVkYmQ3MDI1MTI1ZjEzMDdlYWQ0MjM3YTgzN2QxZTJmZDhkY2FkNjhlODM4ODU1Zjg2O3Jlc3BvbnNlX2lkOnJlc3BfMGFmNjdjYjc4YmVhZDJjNjAwNmFkMDIzYTcyNzM0ODdkMGIxNzY4Yjc3ZjY5NTZiNzM=",
"model": "openai/responses/gpt-6.1-sol",
"usage": {"input_tokens": 309, "output_tokens": 140},
"usage": {"input_tokens": 309, "output_tokens": 140, "output_tokens_details": {"thinking_tokens": 75}},
"content": [
{
"type": "redacted_thinking",

View file

@ -295,7 +295,7 @@ GEMINI_3_5_FLASH_TEST_CASE: Final = TranslationTestCase(
"role": "assistant",
"model": "vertex_ai/gemini-3.5-flash",
"stop_sequence": None,
"usage": {"input_tokens": 9, "output_tokens": 86},
"usage": {"input_tokens": 9, "output_tokens": 86, "output_tokens_details": {"thinking_tokens": 84}},
"content": [{"type": "text", "text": "Hello."}],
"stop_reason": "end_turn",
"stop_details": None,
@ -845,7 +845,7 @@ GEMINI_3_8_FLASH_TEST_CASE: Final = TranslationTestCase(
**MESSAGES_BRIDGE_RESPONSE,
"id": "ySjQarHoLb2apucPwee_YQ",
"model": "vertex_ai/gemini-3.8-flash",
"usage": {"input_tokens": 242, "output_tokens": 314},
"usage": {"input_tokens": 242, "output_tokens": 314, "output_tokens_details": {"thinking_tokens": 243}},
"content": [
{
"type": "thinking",
@ -973,7 +973,7 @@ GEMINI_3_1_PRO_PREVIEW_TEST_CASE: Final = TranslationTestCase(
**MESSAGES_BRIDGE_RESPONSE,
"id": "xCjQasWfKcyL9LsPx7HAqQ8",
"model": "vertex_ai/gemini-3.1-pro-preview",
"usage": {"input_tokens": 242, "output_tokens": 257},
"usage": {"input_tokens": 242, "output_tokens": 257, "output_tokens_details": {"thinking_tokens": 198}},
"content": [
{
"type": "thinking",
@ -1097,7 +1097,7 @@ GEMINI_4_ARGON_TEST_CASE: Final = TranslationTestCase(
**MESSAGES_BRIDGE_RESPONSE,
"id": "0CjQauKQPNyFpucPldSPiAY",
"model": "vertex_ai/gemini-4-argon",
"usage": {"input_tokens": 412, "output_tokens": 192},
"usage": {"input_tokens": 412, "output_tokens": 192, "output_tokens_details": {"thinking_tokens": 123}},
"content": [
{
"type": "thinking",

View file

@ -285,7 +285,7 @@ GEMINI_3_8_FLASH_THOUGHTS_OFF_TEST_CASE: Final = replace(
"provider_specific_fields": {"signature": GEMINI_3_8_FLASH_THOUGHTS_OFF_THOUGHT_SIGNATURE},
},
],
"usage": {"input_tokens": 354, "output_tokens": 385},
"usage": {"input_tokens": 354, "output_tokens": 385, "output_tokens_details": {"thinking_tokens": 321}},
},
)
@ -370,4 +370,5 @@ def test_a_streamed_turn_keeps_thinking_and_billing_reasoning_without_thought_te
assert [object_value(event["usage"]) for event in events if event.get("type") == "message_delta"][-1] == {
"input_tokens": 354,
"output_tokens": 360,
"output_tokens_details": {"thinking_tokens": 295},
}

View file

@ -134,7 +134,7 @@ GROK_4_7_TOOL_SEARCH_TEST_CASE: Final = replace(
expected_litellm_response={
**GROK_4_7_TEST_CASE.expected_litellm_response,
"id": "chatcmpl-xevjeh2pxs6gjvcn2yz76piwdc4pi6fb2sqwb7psnwitm5higvvq",
"usage": {"input_tokens": 162, "output_tokens": 76},
"usage": {"input_tokens": 162, "output_tokens": 76, "output_tokens_details": {"thinking_tokens": 57}},
"content": [{"type": "text", "text": "The web search tool is now loaded and available for use."}],
},
)

View file

@ -3403,23 +3403,23 @@ def test_translate_openai_usage_to_anthropic_cache_tokens_from_dict_details_with
def test_translate_openai_usage_to_anthropic_maps_reasoning_tokens_to_thinking_tokens():
usage = Usage(
usage: Final = Usage(
prompt_tokens=19,
completion_tokens=30,
total_tokens=49,
completion_tokens_details=CompletionTokensDetailsWrapper(reasoning_tokens=25),
)
anthropic_usage = LiteLLMAnthropicMessagesAdapter.translate_openai_usage_to_anthropic_usage_delta(usage)
anthropic_usage: Final = LiteLLMAnthropicMessagesAdapter.translate_openai_usage_to_anthropic_usage_delta(usage)
assert anthropic_usage["output_tokens"] == 30
assert anthropic_usage["output_tokens_details"] == {"thinking_tokens": 25}
def test_translate_openai_usage_to_anthropic_omits_output_tokens_details_without_reasoning_tokens():
usage = Usage(prompt_tokens=19, completion_tokens=5, total_tokens=24)
usage: Final = Usage(prompt_tokens=19, completion_tokens=5, total_tokens=24)
anthropic_usage = LiteLLMAnthropicMessagesAdapter.translate_openai_usage_to_anthropic_usage_delta(usage)
anthropic_usage: Final = LiteLLMAnthropicMessagesAdapter.translate_openai_usage_to_anthropic_usage_delta(usage)
assert "output_tokens_details" not in anthropic_usage