mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-09 03:18:44 +00:00
fix(anthropic,bedrock): report provider thinking tokens instead of classifying them as text
Resolves LIT-5244 Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
parent
24dbd2b2db
commit
af2246c5b8
10 changed files with 462 additions and 24 deletions
|
|
@ -583,7 +583,7 @@ class ChunkProcessor:
|
|||
for choice in response.choices:
|
||||
if (
|
||||
hasattr(cast(Choices, choice).message, "reasoning_content")
|
||||
and cast(Choices, choice).message.reasoning_content is not None
|
||||
and cast(Choices, choice).message.reasoning_content
|
||||
):
|
||||
if reasoning_tokens is None:
|
||||
reasoning_tokens = 0
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
import json
|
||||
import re
|
||||
import time
|
||||
from collections.abc import Mapping, Sequence
|
||||
from typing import TYPE_CHECKING, Any, Final, NoReturn, cast
|
||||
|
||||
import httpx
|
||||
from pydantic import ValidationError
|
||||
|
||||
import litellm
|
||||
from litellm.constants import (
|
||||
|
|
@ -38,6 +40,7 @@ from litellm.types.llms.anthropic import (
|
|||
AnthropicMessagesTool,
|
||||
AnthropicMessagesToolChoice,
|
||||
AnthropicOutputSchema,
|
||||
AnthropicOutputTokensDetails,
|
||||
AnthropicSystemMessageContent,
|
||||
AnthropicThinkingParam,
|
||||
AnthropicWebSearchTool,
|
||||
|
|
@ -2104,6 +2107,66 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig):
|
|||
compaction_blocks,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _thinking_tokens_from_usage(usage_object: Mapping[str, object]) -> int | None:
|
||||
details: Final = usage_object.get("output_tokens_details")
|
||||
if not isinstance(details, Mapping):
|
||||
return None
|
||||
try:
|
||||
return AnthropicOutputTokensDetails.model_validate(details).thinking_tokens
|
||||
except ValidationError:
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _response_has_thinking_block(completion_response: Mapping[str, object] | None) -> bool:
|
||||
if completion_response is None:
|
||||
return False
|
||||
content: Final = completion_response.get("content")
|
||||
if not isinstance(content, list):
|
||||
return False
|
||||
return any(
|
||||
isinstance(block, Mapping) and block.get("type") in ("thinking", "redacted_thinking") for block in content
|
||||
)
|
||||
|
||||
def _build_completion_token_details(
|
||||
self,
|
||||
usage_object: Mapping[str, object],
|
||||
iterations: Sequence[object] | None,
|
||||
completion_tokens: int,
|
||||
reasoning_content: str | None,
|
||||
completion_response: Mapping[str, object] | None,
|
||||
) -> CompletionTokensDetailsWrapper:
|
||||
reported_thinking_tokens: Final = (
|
||||
self._sum_iteration_thinking_tokens(iterations)
|
||||
if iterations
|
||||
else self._thinking_tokens_from_usage(usage_object)
|
||||
)
|
||||
if reported_thinking_tokens is not None:
|
||||
capped_reported: Final = min(max(0, reported_thinking_tokens), completion_tokens)
|
||||
return CompletionTokensDetailsWrapper(
|
||||
reasoning_tokens=capped_reported,
|
||||
text_tokens=completion_tokens - capped_reported,
|
||||
)
|
||||
if reasoning_content:
|
||||
estimated: Final = min(
|
||||
token_counter(text=reasoning_content, count_response_tokens=True),
|
||||
completion_tokens,
|
||||
)
|
||||
return CompletionTokensDetailsWrapper(
|
||||
reasoning_tokens=max(0, estimated),
|
||||
text_tokens=completion_tokens - max(0, estimated),
|
||||
)
|
||||
if self._response_has_thinking_block(completion_response):
|
||||
return CompletionTokensDetailsWrapper(reasoning_tokens=None, text_tokens=None)
|
||||
return CompletionTokensDetailsWrapper(reasoning_tokens=0, text_tokens=completion_tokens)
|
||||
|
||||
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)
|
||||
)
|
||||
reported: Final = tuple(tokens for tokens in per_iteration if tokens is not None)
|
||||
return sum(reported) if reported else None
|
||||
|
||||
def calculate_usage(
|
||||
self,
|
||||
usage_object: dict,
|
||||
|
|
@ -2182,14 +2245,12 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig):
|
|||
cache_creation_token_details=cache_creation_token_details,
|
||||
text_tokens=raw_input_tokens,
|
||||
)
|
||||
# Always populate completion_token_details, not just when there's reasoning_content
|
||||
estimated_reasoning_tokens: Final = (
|
||||
token_counter(text=reasoning_content, count_response_tokens=True) if reasoning_content else 0
|
||||
)
|
||||
reasoning_tokens: Final = min(estimated_reasoning_tokens, completion_tokens)
|
||||
completion_token_details: Final = CompletionTokensDetailsWrapper(
|
||||
reasoning_tokens=max(0, reasoning_tokens),
|
||||
text_tokens=(completion_tokens - reasoning_tokens if reasoning_tokens > 0 else completion_tokens),
|
||||
completion_token_details: Final = self._build_completion_token_details(
|
||||
usage_object=_usage,
|
||||
iterations=iterations,
|
||||
completion_tokens=completion_tokens,
|
||||
reasoning_content=reasoning_content,
|
||||
completion_response=completion_response,
|
||||
)
|
||||
total_tokens: Final = prompt_tokens + completion_tokens
|
||||
|
||||
|
|
|
|||
|
|
@ -1764,6 +1764,7 @@ class AmazonConverseConfig(BaseConfig):
|
|||
self,
|
||||
usage: ConverseTokenUsageBlock,
|
||||
reasoning_content: str | None = None,
|
||||
thinking_ran: bool = False,
|
||||
) -> Usage:
|
||||
input_tokens = usage["inputTokens"]
|
||||
output_tokens: Final = usage["outputTokens"]
|
||||
|
|
@ -1784,10 +1785,19 @@ class AmazonConverseConfig(BaseConfig):
|
|||
cache_creation_tokens=cache_creation_input_tokens,
|
||||
text_tokens=raw_input_tokens,
|
||||
)
|
||||
reasoning_tokens = token_counter(text=reasoning_content, count_response_tokens=True) if reasoning_content else 0
|
||||
completion_tokens_details: Final = CompletionTokensDetailsWrapper(
|
||||
reasoning_tokens=reasoning_tokens,
|
||||
text_tokens=(output_tokens - reasoning_tokens if reasoning_tokens > 0 else output_tokens),
|
||||
reasoning_tokens: Final = (
|
||||
token_counter(text=reasoning_content, count_response_tokens=True) if reasoning_content else 0
|
||||
)
|
||||
completion_tokens_details: Final = (
|
||||
CompletionTokensDetailsWrapper(
|
||||
reasoning_tokens=reasoning_tokens,
|
||||
text_tokens=output_tokens - reasoning_tokens,
|
||||
)
|
||||
if reasoning_tokens > 0
|
||||
else CompletionTokensDetailsWrapper(
|
||||
reasoning_tokens=None if thinking_ran else 0,
|
||||
text_tokens=None if thinking_ran else output_tokens,
|
||||
)
|
||||
)
|
||||
openai_usage: Final = Usage(
|
||||
prompt_tokens=input_tokens,
|
||||
|
|
@ -2184,6 +2194,7 @@ class AmazonConverseConfig(BaseConfig):
|
|||
usage: Final = self._transform_usage(
|
||||
completion_response["usage"],
|
||||
reasoning_content=chat_completion_message.get("reasoning_content"),
|
||||
thinking_ran=reasoningContentBlocks is not None,
|
||||
)
|
||||
|
||||
## HANDLE TOOL CALLS
|
||||
|
|
|
|||
|
|
@ -330,6 +330,7 @@ class AWSEventStreamDecoder:
|
|||
self.response_id: str | None = None
|
||||
self.json_mode = json_mode
|
||||
self._current_tool_name: str | None = None
|
||||
self._thinking_ran = False
|
||||
|
||||
def check_empty_tool_call_args(self) -> bool:
|
||||
"""
|
||||
|
|
@ -559,7 +560,12 @@ class AWSEventStreamDecoder:
|
|||
elif "stopReason" in chunk_data:
|
||||
finish_reason = map_finish_reason(chunk_data.get("stopReason", "stop"))
|
||||
elif "usage" in chunk_data:
|
||||
usage = converse_config._transform_usage(chunk_data.get("usage", {}))
|
||||
usage = converse_config._transform_usage(
|
||||
chunk_data.get("usage", {}),
|
||||
thinking_ran=self._thinking_ran,
|
||||
)
|
||||
if thinking_blocks:
|
||||
self._thinking_ran = True
|
||||
|
||||
model_response_provider_specific_fields: Final = {}
|
||||
if "trace" in chunk_data:
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ Handles transforming from Responses API -> LiteLLM completion (Chat Completion
|
|||
|
||||
import json
|
||||
import re
|
||||
from collections.abc import Sequence
|
||||
from collections.abc import Mapping, Sequence
|
||||
from typing import Any, Final, Literal, cast
|
||||
|
||||
from openai.types.chat.chat_completion_named_tool_choice_param import (
|
||||
|
|
@ -1745,6 +1745,12 @@ class LiteLLMCompletionResponsesConfig:
|
|||
output_items.append(item)
|
||||
return output_items
|
||||
|
||||
@staticmethod
|
||||
def _encode_thinking_blocks(message: Message) -> str | None:
|
||||
thinking_blocks: Final[Sequence[Mapping[str, object]]] = getattr(message, "thinking_blocks", None) or ()
|
||||
preserved: Final = tuple(block for block in thinking_blocks if block.get("signature") or block.get("data"))
|
||||
return json.dumps(preserved, separators=(",", ":")) if preserved else None
|
||||
|
||||
@staticmethod
|
||||
def _extract_reasoning_output_items(
|
||||
chat_completion_response: ModelResponse,
|
||||
|
|
@ -1753,23 +1759,31 @@ class LiteLLMCompletionResponsesConfig:
|
|||
for choice in choices:
|
||||
if hasattr(choice, "message") and choice.message:
|
||||
message = choice.message
|
||||
if hasattr(message, "reasoning_content") and message.reasoning_content:
|
||||
reasoning_content = getattr(message, "reasoning_content", None) or ""
|
||||
encrypted_content = LiteLLMCompletionResponsesConfig._encode_thinking_blocks(message)
|
||||
if reasoning_content or encrypted_content:
|
||||
# Only check the first choice for reasoning content
|
||||
return [
|
||||
GenericResponseOutputItem(
|
||||
type="reasoning",
|
||||
id=f"rs_{hash(str(message.reasoning_content))}",
|
||||
id=f"rs_{hash(reasoning_content or encrypted_content)}",
|
||||
status=LiteLLMCompletionResponsesConfig._map_chat_completion_finish_reason_to_responses_status(
|
||||
choice.finish_reason
|
||||
),
|
||||
role="assistant",
|
||||
content=[
|
||||
OutputText(
|
||||
type="output_text",
|
||||
text=message.reasoning_content,
|
||||
annotations=[],
|
||||
)
|
||||
],
|
||||
content=(
|
||||
[
|
||||
OutputText(
|
||||
type="output_text",
|
||||
text=reasoning_content,
|
||||
annotations=[],
|
||||
)
|
||||
]
|
||||
if reasoning_content
|
||||
# mutable-ok: GenericResponseOutputItem.content is typed as a list
|
||||
else []
|
||||
),
|
||||
encrypted_content=encrypted_content,
|
||||
)
|
||||
]
|
||||
return []
|
||||
|
|
|
|||
|
|
@ -626,6 +626,12 @@ class AnthropicResponseUsageBlock(BaseModel):
|
|||
output_tokens: int
|
||||
|
||||
|
||||
class AnthropicOutputTokensDetails(BaseModel):
|
||||
model_config = ConfigDict(extra="allow")
|
||||
|
||||
thinking_tokens: Optional[int] = None
|
||||
|
||||
|
||||
AnthropicFinishReason = Literal["end_turn", "max_tokens", "stop_sequence", "tool_use"]
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1180,3 +1180,49 @@ def test_get_combined_tool_content_joins_many_custom_tool_input_fragments_in_ord
|
|||
assert isinstance(combined[1], ChatCompletionMessageCustomToolCall)
|
||||
assert combined[1].custom.name == "run_script"
|
||||
assert combined[1].custom.input == "".join(object_fragments)
|
||||
|
||||
|
||||
def _reasoning_stream_chunk() -> ModelResponseStream:
|
||||
return ModelResponseStream(
|
||||
id="chatcmpl-reasoning",
|
||||
model="claude-opus-4-8",
|
||||
choices=[StreamingChoices(finish_reason=None, index=0, delta=Delta(content="10", role="assistant"))],
|
||||
)
|
||||
|
||||
|
||||
def test_count_reasoning_tokens_returns_none_for_signature_only_thinking():
|
||||
from litellm.types.utils import Choices, Message, ModelResponse
|
||||
|
||||
processor = ChunkProcessor(chunks=[_reasoning_stream_chunk()])
|
||||
response = ModelResponse(
|
||||
choices=[
|
||||
Choices(
|
||||
finish_reason="stop",
|
||||
index=0,
|
||||
message=Message(content="10", role="assistant", reasoning_content=""),
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
assert processor.count_reasoning_tokens(response) is None
|
||||
|
||||
|
||||
def test_count_reasoning_tokens_counts_visible_reasoning():
|
||||
from litellm.types.utils import Choices, Message, ModelResponse
|
||||
|
||||
processor = ChunkProcessor(chunks=[_reasoning_stream_chunk()])
|
||||
response = ModelResponse(
|
||||
choices=[
|
||||
Choices(
|
||||
finish_reason="stop",
|
||||
index=0,
|
||||
message=Message(
|
||||
content="10",
|
||||
role="assistant",
|
||||
reasoning_content="let me count the primes under thirty",
|
||||
),
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
assert processor.count_reasoning_tokens(response) > 0
|
||||
|
|
|
|||
|
|
@ -119,6 +119,118 @@ def test_calculate_usage_clamps_text_tokens_when_reasoning_estimate_exceeds_outp
|
|||
assert usage.completion_tokens_details.text_tokens == 0
|
||||
|
||||
|
||||
def test_calculate_usage_prefers_provider_reported_thinking_tokens():
|
||||
config = AnthropicConfig()
|
||||
|
||||
usage = config.calculate_usage(
|
||||
usage_object={
|
||||
"input_tokens": 32,
|
||||
"output_tokens": 421,
|
||||
"output_tokens_details": {"thinking_tokens": 372},
|
||||
},
|
||||
reasoning_content="",
|
||||
completion_response={
|
||||
"content": [
|
||||
{"type": "thinking", "thinking": "", "signature": "sig"},
|
||||
{"type": "text", "text": "10"},
|
||||
]
|
||||
},
|
||||
)
|
||||
|
||||
assert usage.completion_tokens_details is not None
|
||||
assert usage.completion_tokens_details.reasoning_tokens == 372
|
||||
assert usage.completion_tokens_details.text_tokens == 49
|
||||
|
||||
|
||||
def test_calculate_usage_provider_thinking_tokens_win_over_visible_reasoning_estimate():
|
||||
config = AnthropicConfig()
|
||||
|
||||
usage = config.calculate_usage(
|
||||
usage_object={
|
||||
"input_tokens": 50,
|
||||
"output_tokens": 811,
|
||||
"output_tokens_details": {"thinking_tokens": 747},
|
||||
},
|
||||
reasoning_content="short visible reasoning that tokenizes to far fewer than 747 tokens",
|
||||
)
|
||||
|
||||
assert usage.completion_tokens_details is not None
|
||||
assert usage.completion_tokens_details.reasoning_tokens == 747
|
||||
assert usage.completion_tokens_details.text_tokens == 64
|
||||
|
||||
|
||||
def test_calculate_usage_sums_provider_thinking_tokens_across_iterations():
|
||||
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, "output_tokens_details": {"thinking_tokens": 90}},
|
||||
],
|
||||
},
|
||||
reasoning_content=None,
|
||||
)
|
||||
|
||||
assert usage.completion_tokens == 300
|
||||
assert usage.completion_tokens_details is not None
|
||||
assert usage.completion_tokens_details.reasoning_tokens == 150
|
||||
assert usage.completion_tokens_details.text_tokens == 150
|
||||
|
||||
|
||||
def test_calculate_usage_reports_unknown_split_when_thinking_ran_without_a_count():
|
||||
config = AnthropicConfig()
|
||||
|
||||
usage = config.calculate_usage(
|
||||
usage_object={"input_tokens": 32, "output_tokens": 580},
|
||||
reasoning_content="",
|
||||
completion_response={
|
||||
"content": [
|
||||
{"type": "redacted_thinking", "data": "encrypted"},
|
||||
{"type": "text", "text": "10"},
|
||||
]
|
||||
},
|
||||
)
|
||||
|
||||
assert usage.completion_tokens == 580
|
||||
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_without_thinking_reports_all_output_as_text():
|
||||
config = AnthropicConfig()
|
||||
|
||||
usage = config.calculate_usage(
|
||||
usage_object={"input_tokens": 32, "output_tokens": 171},
|
||||
reasoning_content=None,
|
||||
completion_response={"content": [{"type": "text", "text": "10"}]},
|
||||
)
|
||||
|
||||
assert usage.completion_tokens_details is not None
|
||||
assert usage.completion_tokens_details.reasoning_tokens == 0
|
||||
assert usage.completion_tokens_details.text_tokens == 171
|
||||
|
||||
|
||||
def test_calculate_usage_ignores_malformed_provider_thinking_tokens():
|
||||
config = AnthropicConfig()
|
||||
|
||||
usage = config.calculate_usage(
|
||||
usage_object={
|
||||
"input_tokens": 32,
|
||||
"output_tokens": 100,
|
||||
"output_tokens_details": {"thinking_tokens": "not-a-number"},
|
||||
},
|
||||
reasoning_content=None,
|
||||
)
|
||||
|
||||
assert usage.completion_tokens_details is not None
|
||||
assert usage.completion_tokens_details.reasoning_tokens == 0
|
||||
assert usage.completion_tokens_details.text_tokens == 100
|
||||
|
||||
|
||||
def test_calculate_usage_handles_mocked_output_tokens_with_reasoning_content():
|
||||
config = AnthropicConfig()
|
||||
|
||||
|
|
|
|||
|
|
@ -5934,3 +5934,84 @@ def test_adaptive_thinking_dropped_when_max_tokens_too_small_converse():
|
|||
)
|
||||
|
||||
assert "thinking" not in optional_params
|
||||
|
||||
|
||||
def test_converse_usage_reports_unknown_split_for_signature_only_thinking():
|
||||
config = AmazonConverseConfig()
|
||||
|
||||
usage = config._transform_usage(
|
||||
ConverseTokenUsageBlock(inputTokens=32, outputTokens=581, totalTokens=613),
|
||||
reasoning_content="",
|
||||
thinking_ran=True,
|
||||
)
|
||||
|
||||
assert usage.completion_tokens == 581
|
||||
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_converse_usage_estimates_split_for_visible_thinking():
|
||||
config = AmazonConverseConfig()
|
||||
|
||||
usage = config._transform_usage(
|
||||
ConverseTokenUsageBlock(inputTokens=32, outputTokens=581, totalTokens=613),
|
||||
reasoning_content="Let me think about how many primes there are under thirty.",
|
||||
thinking_ran=True,
|
||||
)
|
||||
|
||||
assert usage.completion_tokens_details is not None
|
||||
assert usage.completion_tokens_details.reasoning_tokens > 0
|
||||
assert (
|
||||
usage.completion_tokens_details.reasoning_tokens + usage.completion_tokens_details.text_tokens
|
||||
== usage.completion_tokens
|
||||
)
|
||||
|
||||
|
||||
def test_converse_usage_without_thinking_reports_all_output_as_text():
|
||||
config = AmazonConverseConfig()
|
||||
|
||||
usage = config._transform_usage(ConverseTokenUsageBlock(inputTokens=32, outputTokens=171, totalTokens=203))
|
||||
|
||||
assert usage.completion_tokens_details is not None
|
||||
assert usage.completion_tokens_details.reasoning_tokens == 0
|
||||
assert usage.completion_tokens_details.text_tokens == 171
|
||||
|
||||
|
||||
def test_converse_transform_response_signature_only_thinking_reports_unknown_split():
|
||||
config = AmazonConverseConfig()
|
||||
raw_response = MagicMock(status_code=200)
|
||||
raw_response.text = json.dumps(
|
||||
{
|
||||
"output": {
|
||||
"message": {
|
||||
"role": "assistant",
|
||||
"content": [
|
||||
{"reasoningContent": {"reasoningText": {"text": "", "signature": "sig"}}},
|
||||
{"text": "10"},
|
||||
],
|
||||
}
|
||||
},
|
||||
"stopReason": "end_turn",
|
||||
"usage": {"inputTokens": 32, "outputTokens": 581, "totalTokens": 613},
|
||||
}
|
||||
)
|
||||
raw_response.json.return_value = json.loads(raw_response.text)
|
||||
|
||||
response = config._transform_response(
|
||||
model="bedrock/global.anthropic.claude-opus-4-8",
|
||||
response=raw_response,
|
||||
model_response=ModelResponse(),
|
||||
stream=False,
|
||||
logging_obj=None,
|
||||
optional_params={},
|
||||
api_key=None,
|
||||
data={},
|
||||
messages=[],
|
||||
encoding=None,
|
||||
)
|
||||
|
||||
assert response.choices[0].message.reasoning_content == ""
|
||||
|
||||
assert response.usage.completion_tokens_details.reasoning_tokens is None
|
||||
assert response.usage.completion_tokens_details.text_tokens is None
|
||||
|
|
|
|||
|
|
@ -263,6 +263,107 @@ class TestReasoningContentFinalResponse:
|
|||
assert len(reasoning_items) == 1, "Should have exactly one reasoning item"
|
||||
assert reasoning_items[0].content[0].text == "Reasoning for first answer"
|
||||
|
||||
def test_signature_only_thinking_block_still_emits_reasoning_item(self):
|
||||
response = ModelResponse(
|
||||
id="test-id",
|
||||
created=1234567890,
|
||||
model="test-model",
|
||||
object="chat.completion",
|
||||
choices=[
|
||||
Choices(
|
||||
finish_reason="stop",
|
||||
index=0,
|
||||
message=Message(
|
||||
content="10",
|
||||
role="assistant",
|
||||
reasoning_content="",
|
||||
thinking_blocks=[
|
||||
{"type": "thinking", "thinking": "", "signature": "signature-payload"}
|
||||
],
|
||||
),
|
||||
)
|
||||
],
|
||||
)
|
||||
|
||||
responses_api_response = LiteLLMCompletionResponsesConfig.transform_chat_completion_response_to_responses_api_response(
|
||||
request_input="Test input",
|
||||
responses_api_request={},
|
||||
chat_completion_response=response,
|
||||
)
|
||||
|
||||
reasoning_items = [
|
||||
item for item in responses_api_response.output if item.type == "reasoning"
|
||||
]
|
||||
assert len(reasoning_items) == 1, "Signature-only thinking should still surface a reasoning item"
|
||||
assert reasoning_items[0].content == []
|
||||
assert "signature-payload" in reasoning_items[0].encrypted_content
|
||||
|
||||
def test_redacted_thinking_block_preserved_as_encrypted_content(self):
|
||||
response = ModelResponse(
|
||||
id="test-id",
|
||||
created=1234567890,
|
||||
model="test-model",
|
||||
object="chat.completion",
|
||||
choices=[
|
||||
Choices(
|
||||
finish_reason="stop",
|
||||
index=0,
|
||||
message=Message(
|
||||
content="10",
|
||||
role="assistant",
|
||||
thinking_blocks=[{"type": "redacted_thinking", "data": "redacted-payload"}],
|
||||
),
|
||||
)
|
||||
],
|
||||
)
|
||||
|
||||
responses_api_response = LiteLLMCompletionResponsesConfig.transform_chat_completion_response_to_responses_api_response(
|
||||
request_input="Test input",
|
||||
responses_api_request={},
|
||||
chat_completion_response=response,
|
||||
)
|
||||
|
||||
reasoning_items = [
|
||||
item for item in responses_api_response.output if item.type == "reasoning"
|
||||
]
|
||||
assert len(reasoning_items) == 1
|
||||
assert "redacted-payload" in reasoning_items[0].encrypted_content
|
||||
|
||||
def test_visible_thinking_keeps_text_and_signature(self):
|
||||
response = ModelResponse(
|
||||
id="test-id",
|
||||
created=1234567890,
|
||||
model="test-model",
|
||||
object="chat.completion",
|
||||
choices=[
|
||||
Choices(
|
||||
finish_reason="stop",
|
||||
index=0,
|
||||
message=Message(
|
||||
content="10",
|
||||
role="assistant",
|
||||
reasoning_content="counting the primes",
|
||||
thinking_blocks=[
|
||||
{"type": "thinking", "thinking": "counting the primes", "signature": "sig"}
|
||||
],
|
||||
),
|
||||
)
|
||||
],
|
||||
)
|
||||
|
||||
responses_api_response = LiteLLMCompletionResponsesConfig.transform_chat_completion_response_to_responses_api_response(
|
||||
request_input="Test input",
|
||||
responses_api_request={},
|
||||
chat_completion_response=response,
|
||||
)
|
||||
|
||||
reasoning_items = [
|
||||
item for item in responses_api_response.output if item.type == "reasoning"
|
||||
]
|
||||
assert len(reasoning_items) == 1
|
||||
assert reasoning_items[0].content[0].text == "counting the primes"
|
||||
assert "sig" in reasoning_items[0].encrypted_content
|
||||
|
||||
|
||||
def test_streaming_chunk_id_raw():
|
||||
"""Test that streaming chunk IDs are raw (not encoded) to match OpenAI format"""
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue