mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-14 23:21:35 +00:00
fix(anthropic): report reasoning tokens in /v1/messages usage for non-anthropic models
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
parent
f6b9518ddb
commit
bf4175612d
4 changed files with 104 additions and 0 deletions
|
|
@ -80,6 +80,7 @@ from litellm.types.llms.anthropic import (
|
|||
AnthropicMessagesRequest,
|
||||
AnthropicMessagesToolChoice,
|
||||
AnthropicMessagesUserMessageParam,
|
||||
AnthropicOutputTokensDetails,
|
||||
AnthropicResponseContentBlockRedactedThinking,
|
||||
AnthropicResponseContentBlockText,
|
||||
AnthropicResponseContentBlockThinking,
|
||||
|
|
@ -1265,6 +1266,15 @@ class LiteLLMAnthropicMessagesAdapter:
|
|||
return explicit_value
|
||||
return cls._first_positive_prompt_tokens_detail_value(usage, ("cache_creation_tokens", "cache_write_tokens"))
|
||||
|
||||
@classmethod
|
||||
def _get_reasoning_tokens(cls, usage: Usage) -> int:
|
||||
completion_tokens_details: Final = getattr(usage, "completion_tokens_details", None)
|
||||
if completion_tokens_details is None:
|
||||
return 0
|
||||
if isinstance(completion_tokens_details, dict):
|
||||
return cls._positive_int(completion_tokens_details.get("reasoning_tokens"))
|
||||
return cls._positive_int(getattr(completion_tokens_details, "reasoning_tokens", None))
|
||||
|
||||
@classmethod
|
||||
def _translate_openai_usage_to_anthropic_usage_delta(cls, usage: Usage) -> UsageDelta:
|
||||
cache_read_input_tokens: Final = cls._get_cache_read_input_tokens(usage)
|
||||
|
|
@ -1282,6 +1292,9 @@ class LiteLLMAnthropicMessagesAdapter:
|
|||
usage_delta["cache_creation_input_tokens"] = cache_creation_input_tokens
|
||||
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"] = AnthropicOutputTokensDetails(thinking_tokens=reasoning_tokens)
|
||||
return usage_delta
|
||||
|
||||
@classmethod
|
||||
|
|
|
|||
|
|
@ -486,11 +486,18 @@ class MessageDelta(TypedDict, total=False):
|
|||
stop_reason: str | None
|
||||
|
||||
|
||||
class AnthropicOutputTokensDetails(TypedDict, total=False):
|
||||
"""Anthropic ``usage.output_tokens_details`` (extended thinking breakdown)."""
|
||||
|
||||
thinking_tokens: int
|
||||
|
||||
|
||||
class UsageDelta(TypedDict, total=False):
|
||||
input_tokens: int
|
||||
output_tokens: int
|
||||
cache_creation_input_tokens: int
|
||||
cache_read_input_tokens: int
|
||||
output_tokens_details: AnthropicOutputTokensDetails
|
||||
|
||||
|
||||
class AppliedEdit(TypedDict, total=False):
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ from typing import Any, Literal, TypeAlias
|
|||
from typing_extensions import NotRequired, TypedDict
|
||||
|
||||
from litellm.types.llms.anthropic import (
|
||||
AnthropicOutputTokensDetails,
|
||||
AnthropicResponseContentBlockText,
|
||||
AnthropicResponseContentBlockToolUse,
|
||||
ContextManagementResponse,
|
||||
|
|
@ -71,6 +72,11 @@ class AnthropicUsage(TypedDict, total=False):
|
|||
cache_creation_input_tokens: int
|
||||
cache_read_input_tokens: int
|
||||
|
||||
"""
|
||||
Extended thinking breakdown of ``output_tokens``
|
||||
"""
|
||||
output_tokens_details: AnthropicOutputTokensDetails
|
||||
|
||||
|
||||
class AnthropicMessagesResponse(TypedDict, total=False):
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -2331,6 +2331,84 @@ def test_translate_openai_usage_to_anthropic_cache_tokens_from_dict_details_with
|
|||
assert anthropic_usage["cache_creation_input_tokens"] == 20
|
||||
|
||||
|
||||
def test_translate_openai_usage_to_anthropic_reports_reasoning_tokens():
|
||||
"""
|
||||
Reasoning models served through /v1/messages must report the reasoning tokens they billed for,
|
||||
in Anthropic's own shape (usage.output_tokens_details.thinking_tokens).
|
||||
|
||||
Fixes: https://github.com/BerriAI/litellm/issues/36376
|
||||
"""
|
||||
from litellm.types.utils import CompletionTokensDetailsWrapper
|
||||
|
||||
usage = Usage(
|
||||
prompt_tokens=15,
|
||||
completion_tokens=100,
|
||||
total_tokens=115,
|
||||
completion_tokens_details=CompletionTokensDetailsWrapper(reasoning_tokens=68, text_tokens=32),
|
||||
)
|
||||
|
||||
anthropic_usage = LiteLLMAnthropicMessagesAdapter._translate_openai_usage_to_anthropic_usage_delta(usage)
|
||||
|
||||
assert anthropic_usage["output_tokens"] == 100
|
||||
assert anthropic_usage["output_tokens_details"] == {"thinking_tokens": 68}
|
||||
|
||||
|
||||
def test_translate_openai_usage_to_anthropic_reads_reasoning_tokens_from_dict_details():
|
||||
usage = Usage(prompt_tokens=15, completion_tokens=100, total_tokens=115)
|
||||
usage.completion_tokens_details = {"reasoning_tokens": 68}
|
||||
|
||||
anthropic_usage = LiteLLMAnthropicMessagesAdapter._translate_openai_usage_to_anthropic_usage_delta(usage)
|
||||
|
||||
assert anthropic_usage["output_tokens_details"] == {"thinking_tokens": 68}
|
||||
|
||||
|
||||
def test_translate_openai_usage_to_anthropic_omits_zero_reasoning_tokens():
|
||||
from litellm.types.utils import CompletionTokensDetailsWrapper
|
||||
|
||||
usage = Usage(
|
||||
prompt_tokens=15,
|
||||
completion_tokens=100,
|
||||
total_tokens=115,
|
||||
completion_tokens_details=CompletionTokensDetailsWrapper(reasoning_tokens=0, text_tokens=100),
|
||||
)
|
||||
|
||||
anthropic_usage = LiteLLMAnthropicMessagesAdapter._translate_openai_usage_to_anthropic_usage_delta(usage)
|
||||
|
||||
assert "output_tokens_details" not in anthropic_usage
|
||||
|
||||
|
||||
def test_translate_openai_response_to_anthropic_reports_reasoning_tokens():
|
||||
from litellm.types.utils import CompletionTokensDetailsWrapper
|
||||
|
||||
usage = Usage(
|
||||
prompt_tokens=15,
|
||||
completion_tokens=100,
|
||||
total_tokens=115,
|
||||
completion_tokens_details=CompletionTokensDetailsWrapper(reasoning_tokens=68, text_tokens=32),
|
||||
)
|
||||
response = ModelResponse(
|
||||
id="chatcmpl-1",
|
||||
choices=[
|
||||
Choices(
|
||||
finish_reason="stop",
|
||||
index=0,
|
||||
message=Message(content="2", role="assistant"),
|
||||
)
|
||||
],
|
||||
created=1,
|
||||
model="gpt-5.1",
|
||||
object="chat.completion",
|
||||
usage=usage,
|
||||
)
|
||||
|
||||
anthropic_response = LiteLLMAnthropicMessagesAdapter().translate_openai_response_to_anthropic(
|
||||
response=response,
|
||||
tool_name_mapping=None,
|
||||
)
|
||||
|
||||
assert anthropic_response["usage"]["output_tokens_details"] == {"thinking_tokens": 68}
|
||||
|
||||
|
||||
def test_translate_openai_usage_to_anthropic_ignores_fractional_cache_tokens():
|
||||
usage = Usage(
|
||||
prompt_tokens=120,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue