fix(anthropic): fall back when only some compaction iterations report thinking tokens

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
Devin AI 2026-08-05 21:34:36 +00:00
parent aadfa89ff6
commit 53ee9c8293
4 changed files with 60 additions and 17 deletions

View file

@ -2134,9 +2134,10 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig):
reasoning_content: str | None,
completion_response: Mapping[str, object] | None,
) -> CompletionTokensDetailsWrapper:
iteration_thinking_tokens: Final = self._sum_iteration_thinking_tokens(iterations) if iterations else None
reported_thinking_tokens: Final = (
self._sum_iteration_thinking_tokens(iterations)
if iterations
iteration_thinking_tokens
if iteration_thinking_tokens is not None
else self._thinking_tokens_from_usage(usage_object)
)
if reported_thinking_tokens is not None:
@ -2160,10 +2161,11 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig):
def _sum_iteration_thinking_tokens(self, iterations: Sequence[object]) -> int | None:
per_iteration: Final = tuple(
self._thinking_tokens_from_usage(iteration) for iteration in iterations if isinstance(iteration, Mapping)
self._thinking_tokens_from_usage(iteration) if isinstance(iteration, Mapping) else None
for iteration in iterations
)
reported: Final = tuple(tokens for tokens in per_iteration if tokens is not None)
return sum(reported) if reported else None
return sum(reported) if len(reported) == len(per_iteration) else None
@staticmethod
def is_anthropic_usage_object(usage_object: dict) -> bool:

View file

@ -1763,18 +1763,15 @@ class LiteLLMCompletionResponsesConfig:
choice.finish_reason
),
role="assistant",
content=(
[
OutputText(
type="output_text",
text=reasoning_content,
annotations=[],
)
]
if reasoning_content
# mutable-ok: GenericResponseOutputItem.content is typed as a list
else []
),
content=[
OutputText(
type="output_text",
text=text,
annotations=[],
)
for text in (reasoning_content,)
if text
],
encrypted_content=encrypted_content,
)
]

View file

@ -612,7 +612,7 @@ class AnthropicResponseUsageBlock(BaseModel):
class AnthropicOutputTokensDetails(BaseModel):
model_config = ConfigDict(extra="allow")
thinking_tokens: Optional[int] = None
thinking_tokens: int | None = None
AnthropicFinishReason = Literal["end_turn", "max_tokens", "stop_sequence", "tool_use"]

View file

@ -180,6 +180,50 @@ def test_calculate_usage_sums_provider_thinking_tokens_across_iterations():
assert usage.completion_tokens_details.text_tokens == 150
def test_calculate_usage_falls_back_when_only_some_iterations_report_thinking_tokens():
config = AnthropicConfig()
usage = config.calculate_usage(
usage_object={
"input_tokens": 10,
"output_tokens": 300,
"output_tokens_details": {"thinking_tokens": 240},
"iterations": [
{"input_tokens": 5, "output_tokens": 100, "output_tokens_details": {"thinking_tokens": 60}},
{"input_tokens": 5, "output_tokens": 200},
],
},
reasoning_content=None,
)
assert usage.completion_tokens == 300
assert usage.completion_tokens_details is not None
assert usage.completion_tokens_details.reasoning_tokens == 240
assert usage.completion_tokens_details.text_tokens == 60
def test_calculate_usage_reports_unknown_split_when_only_some_iterations_report_thinking_tokens():
config = AnthropicConfig()
usage = config.calculate_usage(
usage_object={
"input_tokens": 10,
"output_tokens": 300,
"iterations": [
{"input_tokens": 5, "output_tokens": 100, "output_tokens_details": {"thinking_tokens": 60}},
{"input_tokens": 5, "output_tokens": 200},
],
},
reasoning_content="",
completion_response={"content": [{"type": "thinking", "thinking": "", "signature": "sig"}]},
)
assert usage.completion_tokens == 300
assert usage.completion_tokens_details is not None
assert usage.completion_tokens_details.reasoning_tokens is None
assert usage.completion_tokens_details.text_tokens is None
def test_calculate_usage_reports_unknown_split_when_thinking_ran_without_a_count():
config = AnthropicConfig()