mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-13 23:11:40 +00:00
Merge b98bd52c6c into 1c61c2606e
This commit is contained in:
commit
52cfc79fdb
7 changed files with 421 additions and 27 deletions
|
|
@ -20,6 +20,7 @@ from openai.types.responses.tool_choice_custom_param import ToolChoiceCustomPara
|
|||
from openai.types.responses.tool_choice_function_param import ToolChoiceFunctionParam
|
||||
from openai.types.responses.tool_param import FunctionToolParam
|
||||
from pydantic import BaseModel
|
||||
from typing_extensions import ReadOnly
|
||||
|
||||
import litellm
|
||||
from litellm import ModelResponse
|
||||
|
|
@ -105,6 +106,7 @@ class _BuiltReasoningItem(TypedDict):
|
|||
id: str
|
||||
encrypted_content: str | None
|
||||
summary: Sequence[_ReasoningSummaryText]
|
||||
content: ReadOnly[Sequence[_ReasoningSummaryText]]
|
||||
|
||||
|
||||
def _get_reasoning_items(
|
||||
|
|
@ -133,34 +135,64 @@ def _reasoning_input_items(msg: "AllMessageValues") -> list[dict[str, object]]:
|
|||
return [dict(item) for item in replayed] # mutable-ok: API message payload
|
||||
|
||||
|
||||
def _normalize_reasoning_parts(parts: Iterable[object] | None, default_type: str) -> Sequence[_ReasoningSummaryText]:
|
||||
normalized: Final[list[_ReasoningSummaryText]] = [] # mutable-ok: local accumulator, returned once
|
||||
for part in parts or ():
|
||||
if isinstance(part, dict):
|
||||
normalized.append(
|
||||
{ # mutable-ok: freshly built per part
|
||||
"type": part.get("type", default_type),
|
||||
"text": part.get("text", ""),
|
||||
}
|
||||
)
|
||||
else:
|
||||
normalized.append(
|
||||
{ # mutable-ok: freshly built per part
|
||||
"type": getattr(part, "type", default_type),
|
||||
"text": getattr(part, "text", ""),
|
||||
}
|
||||
)
|
||||
return normalized
|
||||
|
||||
|
||||
def _build_reasoning_item(
|
||||
item_id: str,
|
||||
encrypted_content: str | None,
|
||||
summary_raw: Iterable[object] | None,
|
||||
content_raw: Iterable[object] | None = None,
|
||||
) -> _BuiltReasoningItem:
|
||||
"""Build a ChatCompletionReasoningItem-shaped dict from raw response data.
|
||||
|
||||
Handles both pydantic objects (attribute access) and plain dicts.
|
||||
"""
|
||||
summary: Final[list[_ReasoningSummaryText]] = []
|
||||
for s in summary_raw or []:
|
||||
if isinstance(s, dict):
|
||||
summary.append({"type": s.get("type", "summary_text"), "text": s.get("text", "")})
|
||||
else:
|
||||
summary.append(
|
||||
{
|
||||
"type": getattr(s, "type", "summary_text"),
|
||||
"text": getattr(s, "text", ""),
|
||||
}
|
||||
)
|
||||
summary: Final[Sequence[_ReasoningSummaryText]] = _normalize_reasoning_parts(
|
||||
summary_raw, default_type="summary_text"
|
||||
)
|
||||
content: Final[Sequence[_ReasoningSummaryText]] = _normalize_reasoning_parts(
|
||||
content_raw, default_type="reasoning_text"
|
||||
)
|
||||
return {
|
||||
"id": item_id,
|
||||
"type": "reasoning",
|
||||
"encrypted_content": encrypted_content,
|
||||
"summary": summary,
|
||||
"content": content,
|
||||
}
|
||||
|
||||
|
||||
def _reasoning_content_from_built_item(reasoning_item: _BuiltReasoningItem) -> str:
|
||||
"""Chat ``reasoning_content`` for a reasoning item.
|
||||
|
||||
Raw reasoning content (e.g. vLLM ``reasoning_text`` parts) is the full chain of
|
||||
thought and wins when present; the summary is the fallback so the two are never
|
||||
concatenated into duplicate text.
|
||||
"""
|
||||
content_text: Final = " ".join(part["text"] for part in reasoning_item["content"] if part.get("text"))
|
||||
if content_text:
|
||||
return content_text
|
||||
return " ".join(s["text"] for s in reasoning_item["summary"] if s.get("text"))
|
||||
|
||||
|
||||
def _reasoning_item_from_output_item(item: object) -> _BuiltReasoningItem | None:
|
||||
from openai.types.responses import ResponseReasoningItem
|
||||
|
||||
|
|
@ -169,13 +201,24 @@ def _reasoning_item_from_output_item(item: object) -> _BuiltReasoningItem | None
|
|||
item_id=item.id,
|
||||
encrypted_content=getattr(item, "encrypted_content", None),
|
||||
summary_raw=item.summary,
|
||||
content_raw=getattr(item, "content", None),
|
||||
)
|
||||
if isinstance(item, dict) and item.get("type") == "reasoning":
|
||||
return _build_reasoning_item(
|
||||
item_id=item.get("id", ""),
|
||||
encrypted_content=item.get("encrypted_content"),
|
||||
summary_raw=item.get("summary"),
|
||||
content_raw=item.get("content"),
|
||||
)
|
||||
if isinstance(item, BaseModel):
|
||||
dumped: Final = item.model_dump()
|
||||
if dumped.get("type") == "reasoning":
|
||||
return _build_reasoning_item(
|
||||
item_id=dumped.get("id", ""),
|
||||
encrypted_content=dumped.get("encrypted_content"),
|
||||
summary_raw=dumped.get("summary"),
|
||||
content_raw=dumped.get("content"),
|
||||
)
|
||||
return None
|
||||
|
||||
|
||||
|
|
@ -325,9 +368,8 @@ class LiteLLMResponsesTransformationHandler(CompletionTransformationBridge):
|
|||
|
||||
item_type: Final = item.get("type")
|
||||
|
||||
# Ignore reasoning items for now
|
||||
if item_type == "reasoning":
|
||||
return None, index
|
||||
# Reasoning dicts are intercepted by _convert_response_output_to_choices
|
||||
# before this callback runs.
|
||||
|
||||
# Handle message items with output_text content
|
||||
if item_type == "message":
|
||||
|
|
@ -656,7 +698,6 @@ class LiteLLMResponsesTransformationHandler(CompletionTransformationBridge):
|
|||
from openai.types.responses import (
|
||||
ResponseFunctionToolCall,
|
||||
ResponseOutputMessage,
|
||||
ResponseReasoningItem,
|
||||
)
|
||||
|
||||
try:
|
||||
|
|
@ -679,15 +720,15 @@ class LiteLLMResponsesTransformationHandler(CompletionTransformationBridge):
|
|||
tool_call_index = 0
|
||||
|
||||
for item in output_items:
|
||||
if isinstance(item, ResponseReasoningItem):
|
||||
pending_reasoning_item = _build_reasoning_item(
|
||||
item_id=item.id,
|
||||
encrypted_content=getattr(item, "encrypted_content", None),
|
||||
summary_raw=item.summary,
|
||||
)
|
||||
reasoning_content = " ".join(s["text"] for s in pending_reasoning_item["summary"] if s.get("text"))
|
||||
# Typed ResponseReasoningItem and dict-form reasoning items (from providers
|
||||
# whose output skips SDK parsing, e.g. GPT-5 Codex raw items) both land here
|
||||
built_reasoning_item = _reasoning_item_from_output_item(item) # rebind-ok: reassessed per loop item
|
||||
if built_reasoning_item is not None:
|
||||
pending_reasoning_item = built_reasoning_item
|
||||
reasoning_content = _reasoning_content_from_built_item(built_reasoning_item)
|
||||
continue
|
||||
|
||||
elif isinstance(item, ResponseOutputMessage):
|
||||
if isinstance(item, ResponseOutputMessage):
|
||||
for content in item.content:
|
||||
response_text = getattr(content, "text", "")
|
||||
# Extract annotations from content if present
|
||||
|
|
@ -760,6 +801,11 @@ class LiteLLMResponsesTransformationHandler(CompletionTransformationBridge):
|
|||
elif handle_raw_dict_callback is not None:
|
||||
choice, index = handle_raw_dict_callback(item=raw_item, index=index)
|
||||
if choice is not None:
|
||||
if pending_reasoning_item is not None:
|
||||
choice.message.reasoning_content = reasoning_content
|
||||
choice.message.reasoning_items = [pending_reasoning_item] # mutable-ok: response payload
|
||||
reasoning_content = None # flush
|
||||
pending_reasoning_item = None # flush
|
||||
choices.append(choice)
|
||||
else:
|
||||
pass # don't fail request if item in list is not supported
|
||||
|
|
@ -790,10 +836,9 @@ class LiteLLMResponsesTransformationHandler(CompletionTransformationBridge):
|
|||
|
||||
reasoning_items: Final = _reasoning_items_from_output_items(output_items)
|
||||
reasoning_content: Final = " ".join(
|
||||
summary_block["text"]
|
||||
for reasoning_item in reasoning_items
|
||||
for summary_block in reasoning_item["summary"]
|
||||
if summary_block.get("text")
|
||||
text
|
||||
for text in (_reasoning_content_from_built_item(reasoning_item) for reasoning_item in reasoning_items)
|
||||
if text
|
||||
)
|
||||
message: Final = Message(
|
||||
content="",
|
||||
|
|
@ -1549,6 +1594,22 @@ class OpenAiResponsesToChatCompletionStreamIterator(BaseModelResponseIterator):
|
|||
)
|
||||
]
|
||||
)
|
||||
elif event_type == "response.reasoning_text.delta":
|
||||
# Raw (non-summary) reasoning deltas, e.g. from vLLM and other
|
||||
# Responses-compatible backends that expose the full reasoning content.
|
||||
content_part = parsed_chunk.get("delta", None)
|
||||
if content_part:
|
||||
# One Responses generation maps to one Chat choice, so reasoning
|
||||
# deltas stream on choice 0 like output_text deltas do; content_index
|
||||
# is a part index within the reasoning item, not a choice index.
|
||||
return ModelResponseStream(
|
||||
choices=[ # mutable-ok: API streaming payload
|
||||
StreamingChoices(
|
||||
index=0,
|
||||
delta=Delta(reasoning_content=content_part),
|
||||
)
|
||||
]
|
||||
)
|
||||
elif event_type in ("response.completed", "response.incomplete"):
|
||||
response_data: Final = parsed_chunk.get("response", {})
|
||||
output_items: Final = response_data.get("output", []) if response_data else []
|
||||
|
|
|
|||
|
|
@ -96,6 +96,21 @@ def _redact_function_call(function_call) -> None:
|
|||
function_call.arguments = REDACTED_BY_LITELLM
|
||||
|
||||
|
||||
def _redact_reasoning_items_dict(reasoning_items: object, redacted_str: str) -> None:
|
||||
"""Redact summary and raw content text inside reasoning items (dict form)."""
|
||||
if not isinstance(reasoning_items, list):
|
||||
return
|
||||
for reasoning_item in reasoning_items:
|
||||
if not isinstance(reasoning_item, dict):
|
||||
continue
|
||||
for key in ("summary", "content"):
|
||||
parts = reasoning_item.get(key) # rebind-ok: reassessed per key
|
||||
if isinstance(parts, list):
|
||||
for part in parts:
|
||||
if isinstance(part, dict) and part.get("text") is not None:
|
||||
part["text"] = redacted_str
|
||||
|
||||
|
||||
def _redact_choice_content(choice):
|
||||
"""Helper to redact content in a choice (message or delta)."""
|
||||
if isinstance(choice, litellm.Choices):
|
||||
|
|
@ -105,6 +120,7 @@ def _redact_choice_content(choice):
|
|||
choice.message.reasoning_content = REDACTED_BY_LITELLM
|
||||
if hasattr(choice.message, "thinking_blocks"):
|
||||
choice.message.thinking_blocks = None
|
||||
_redact_reasoning_items_dict(getattr(choice.message, "reasoning_items", None), REDACTED_BY_LITELLM)
|
||||
_redact_tool_calls(getattr(choice.message, "tool_calls", None))
|
||||
_redact_function_call(getattr(choice.message, "function_call", None))
|
||||
elif isinstance(choice, litellm.utils.StreamingChoices):
|
||||
|
|
@ -114,6 +130,7 @@ def _redact_choice_content(choice):
|
|||
choice.delta.reasoning_content = REDACTED_BY_LITELLM
|
||||
if hasattr(choice.delta, "thinking_blocks"):
|
||||
choice.delta.thinking_blocks = None
|
||||
_redact_reasoning_items_dict(getattr(choice.delta, "reasoning_items", None), REDACTED_BY_LITELLM)
|
||||
_redact_tool_calls(getattr(choice.delta, "tool_calls", None))
|
||||
_redact_function_call(getattr(choice.delta, "function_call", None))
|
||||
|
||||
|
|
@ -226,6 +243,7 @@ def _redact_model_response_dict_choices(choices, redacted_str: str):
|
|||
choice["message"]["thinking_blocks"] = None
|
||||
if "audio" in choice["message"]:
|
||||
choice["message"]["audio"] = None
|
||||
_redact_reasoning_items_dict(choice["message"].get("reasoning_items"), redacted_str)
|
||||
_redact_tool_calls_dict(choice["message"])
|
||||
elif "delta" in choice and isinstance(choice["delta"], dict):
|
||||
if choice["delta"].get("content") is not None:
|
||||
|
|
@ -236,6 +254,7 @@ def _redact_model_response_dict_choices(choices, redacted_str: str):
|
|||
choice["delta"]["thinking_blocks"] = None
|
||||
if "audio" in choice["delta"]:
|
||||
choice["delta"]["audio"] = None
|
||||
_redact_reasoning_items_dict(choice["delta"].get("reasoning_items"), redacted_str)
|
||||
_redact_tool_calls_dict(choice["delta"])
|
||||
else:
|
||||
_redact_choice_content(choice)
|
||||
|
|
|
|||
|
|
@ -31639,6 +31639,13 @@
|
|||
"ChatCompletionReasoningItem": {
|
||||
"description": "Represents an OpenAI Responses API reasoning item for round-tripping in conversation history.",
|
||||
"properties": {
|
||||
"content": {
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/ChatCompletionReasoningTextBlock"
|
||||
},
|
||||
"title": "Content",
|
||||
"type": "array"
|
||||
},
|
||||
"encrypted_content": {
|
||||
"anyOf": [
|
||||
{
|
||||
|
|
@ -31691,6 +31698,24 @@
|
|||
"title": "ChatCompletionReasoningSummaryTextBlock",
|
||||
"type": "object"
|
||||
},
|
||||
"ChatCompletionReasoningTextBlock": {
|
||||
"properties": {
|
||||
"text": {
|
||||
"title": "Text",
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"const": "reasoning_text",
|
||||
"title": "Type",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"type"
|
||||
],
|
||||
"title": "ChatCompletionReasoningTextBlock",
|
||||
"type": "object"
|
||||
},
|
||||
"ChatCompletionRedactedThinkingBlock": {
|
||||
"properties": {
|
||||
"cache_control": {
|
||||
|
|
|
|||
|
|
@ -626,6 +626,11 @@ class ChatCompletionReasoningSummaryTextBlock(TypedDict, total=False):
|
|||
text: str
|
||||
|
||||
|
||||
class ChatCompletionReasoningTextBlock(TypedDict, total=False):
|
||||
type: Required[Literal["reasoning_text"]] # writable-ok: Pydantic warns on ReadOnly TypedDict fields
|
||||
text: str # writable-ok: Pydantic warns on ReadOnly TypedDict fields
|
||||
|
||||
|
||||
class ChatCompletionReasoningItem(TypedDict, total=False):
|
||||
"""Represents an OpenAI Responses API reasoning item for round-tripping in conversation history."""
|
||||
|
||||
|
|
@ -633,6 +638,7 @@ class ChatCompletionReasoningItem(TypedDict, total=False):
|
|||
id: str
|
||||
encrypted_content: str | None
|
||||
summary: ReadOnly[list[ChatCompletionReasoningSummaryTextBlock]]
|
||||
content: ReadOnly[list[ChatCompletionReasoningTextBlock]]
|
||||
|
||||
|
||||
class WebSearchOptionsUserLocationApproximate(TypedDict, total=False):
|
||||
|
|
|
|||
|
|
@ -4287,3 +4287,189 @@ def test_system_string_after_a_developer_message_stays_in_input_in_client_order(
|
|||
assert instructions is None
|
||||
assert [item["role"] for item in input_items] == ["developer", "system", "user"]
|
||||
assert input_items[1] == _system_input_item("Be brief.")
|
||||
|
||||
def test_openai_responses_chunk_parser_reasoning_text_delta():
|
||||
"""Raw reasoning_text deltas map to delta.reasoning_content (issue #40654)."""
|
||||
from litellm.completion_extras.litellm_responses_transformation.transformation import (
|
||||
OpenAiResponsesToChatCompletionStreamIterator,
|
||||
)
|
||||
from litellm.types.utils import Delta, ModelResponseStream, StreamingChoices
|
||||
|
||||
iterator = OpenAiResponsesToChatCompletionStreamIterator(
|
||||
streaming_response=None, sync_stream=True
|
||||
)
|
||||
|
||||
chunk = {
|
||||
"content_index": 0,
|
||||
"delta": "First, count the bolts.",
|
||||
"item_id": "rs_a1892a8a9de47516",
|
||||
"output_index": 0,
|
||||
"sequence_number": 3,
|
||||
"type": "response.reasoning_text.delta",
|
||||
}
|
||||
|
||||
result = iterator.chunk_parser(chunk)
|
||||
|
||||
assert isinstance(result, ModelResponseStream)
|
||||
assert len(result.choices) == 1
|
||||
choice = result.choices[0]
|
||||
assert isinstance(choice, StreamingChoices)
|
||||
assert choice.index == 0
|
||||
delta = choice.delta
|
||||
assert isinstance(delta, Delta)
|
||||
assert delta.content is None
|
||||
assert delta.reasoning_content == "First, count the bolts."
|
||||
assert delta.tool_calls is None
|
||||
|
||||
|
||||
def _make_raw_reasoning_output(content_text: str | None, summary_text: str | None) -> list:
|
||||
from openai.types.responses import ResponseOutputMessage, ResponseOutputText
|
||||
from openai.types.responses.response_reasoning_item import (
|
||||
Content,
|
||||
ResponseReasoningItem,
|
||||
Summary,
|
||||
)
|
||||
|
||||
reasoning_item = ResponseReasoningItem(
|
||||
id="rs_a1892a8a9de47516",
|
||||
summary=[Summary(text=summary_text, type="summary_text")]
|
||||
if summary_text is not None
|
||||
else [],
|
||||
type="reasoning",
|
||||
content=[Content(text=content_text, type="reasoning_text")]
|
||||
if content_text is not None
|
||||
else None,
|
||||
encrypted_content=None,
|
||||
status=None,
|
||||
)
|
||||
output_message = ResponseOutputMessage(
|
||||
id="msg_01",
|
||||
content=[
|
||||
ResponseOutputText(
|
||||
annotations=[], text="303 bolts remain.", type="output_text", logprobs=[]
|
||||
)
|
||||
],
|
||||
role="assistant",
|
||||
status="completed",
|
||||
type="message",
|
||||
)
|
||||
return [reasoning_item, output_message]
|
||||
|
||||
|
||||
def test_convert_response_output_raw_reasoning_content_without_summary():
|
||||
"""Reasoning items with raw content and an empty summary surface the raw text."""
|
||||
from litellm.completion_extras.litellm_responses_transformation.transformation import (
|
||||
LiteLLMResponsesTransformationHandler,
|
||||
)
|
||||
|
||||
output_items = _make_raw_reasoning_output(
|
||||
content_text="raw thinking trace", summary_text=None
|
||||
)
|
||||
|
||||
choices = LiteLLMResponsesTransformationHandler._convert_response_output_to_choices(
|
||||
output_items
|
||||
)
|
||||
|
||||
assert len(choices) == 1
|
||||
message = choices[0].message
|
||||
assert message.reasoning_content == "raw thinking trace"
|
||||
assert message.reasoning_items is not None
|
||||
assert message.reasoning_items[0]["content"][0]["text"] == "raw thinking trace"
|
||||
assert message.reasoning_items[0]["content"][0]["type"] == "reasoning_text"
|
||||
|
||||
|
||||
def test_convert_response_output_mixed_reasoning_content_and_summary_no_duplication():
|
||||
"""With both raw content and a summary, the raw content wins; nothing is concatenated."""
|
||||
from litellm.completion_extras.litellm_responses_transformation.transformation import (
|
||||
LiteLLMResponsesTransformationHandler,
|
||||
)
|
||||
|
||||
output_items = _make_raw_reasoning_output(
|
||||
content_text="full raw trace", summary_text="short summary"
|
||||
)
|
||||
|
||||
choices = LiteLLMResponsesTransformationHandler._convert_response_output_to_choices(
|
||||
output_items
|
||||
)
|
||||
|
||||
assert len(choices) == 1
|
||||
assert choices[0].message.reasoning_content == "full raw trace"
|
||||
|
||||
|
||||
def test_convert_response_output_summary_only_reasoning_unchanged():
|
||||
"""Summary-only reasoning items keep the pre-existing summary behavior."""
|
||||
from litellm.completion_extras.litellm_responses_transformation.transformation import (
|
||||
LiteLLMResponsesTransformationHandler,
|
||||
)
|
||||
|
||||
output_items = _make_raw_reasoning_output(
|
||||
content_text=None, summary_text="short summary"
|
||||
)
|
||||
|
||||
choices = LiteLLMResponsesTransformationHandler._convert_response_output_to_choices(
|
||||
output_items
|
||||
)
|
||||
|
||||
assert len(choices) == 1
|
||||
assert choices[0].message.reasoning_content == "short summary"
|
||||
assert choices[0].message.reasoning_items[0]["content"] == []
|
||||
|
||||
|
||||
def test_convert_response_output_dict_reasoning_carried_to_message_choice():
|
||||
"""Dict-form reasoning items (SDK parsing skipped) land on the following message choice."""
|
||||
from litellm.completion_extras.litellm_responses_transformation.transformation import (
|
||||
LiteLLMResponsesTransformationHandler,
|
||||
)
|
||||
|
||||
handler = LiteLLMResponsesTransformationHandler()
|
||||
items = [
|
||||
{
|
||||
"type": "reasoning",
|
||||
"id": "rs_dict1",
|
||||
"summary": [],
|
||||
"content": [{"type": "reasoning_text", "text": "dict raw trace"}],
|
||||
},
|
||||
{
|
||||
"type": "message",
|
||||
"id": "msg_dict1",
|
||||
"role": "assistant",
|
||||
"status": "completed",
|
||||
"content": [{"type": "output_text", "text": "303 bolts remain.", "annotations": []}],
|
||||
},
|
||||
]
|
||||
|
||||
choices = LiteLLMResponsesTransformationHandler._convert_response_output_to_choices(
|
||||
items,
|
||||
handle_raw_dict_callback=handler._handle_raw_dict_response_item,
|
||||
)
|
||||
|
||||
assert len(choices) == 1
|
||||
message = choices[0].message
|
||||
assert message.content == "303 bolts remain."
|
||||
assert message.reasoning_content == "dict raw trace"
|
||||
assert message.reasoning_items is not None
|
||||
assert message.reasoning_items[0]["content"][0]["text"] == "dict raw trace"
|
||||
|
||||
|
||||
def test_convert_response_output_dict_reasoning_item_with_content():
|
||||
"""Raw dict reasoning items (non-SDK-parsed payloads) also surface their content."""
|
||||
from litellm.completion_extras.litellm_responses_transformation.transformation import (
|
||||
_reasoning_content_from_built_item,
|
||||
_reasoning_items_from_output_items,
|
||||
)
|
||||
|
||||
reasoning_items = _reasoning_items_from_output_items(
|
||||
[
|
||||
{
|
||||
"type": "reasoning",
|
||||
"id": "rs_dict",
|
||||
"summary": [],
|
||||
"content": [{"type": "reasoning_text", "text": "dict raw trace"}],
|
||||
}
|
||||
]
|
||||
)
|
||||
|
||||
assert len(reasoning_items) == 1
|
||||
assert reasoning_items[0]["content"][0]["text"] == "dict raw trace"
|
||||
assert _reasoning_content_from_built_item(reasoning_items[0]) == "dict raw trace"
|
||||
|
||||
|
|
|
|||
|
|
@ -263,6 +263,91 @@ class TestPerformRedaction:
|
|||
assert delta["thinking_blocks"] is None
|
||||
assert delta["audio"] is None
|
||||
|
||||
def test_redacts_reasoning_items_in_model_response_dict_choices(self):
|
||||
result = {
|
||||
"choices": [
|
||||
{
|
||||
"message": {
|
||||
"content": "message content",
|
||||
"reasoning_items": [
|
||||
{
|
||||
"type": "reasoning",
|
||||
"id": "rs_1",
|
||||
"summary": [{"type": "summary_text", "text": "summary text"}],
|
||||
"content": [{"type": "reasoning_text", "text": "raw reasoning"}],
|
||||
}
|
||||
],
|
||||
}
|
||||
},
|
||||
{
|
||||
"delta": {
|
||||
"content": "delta content",
|
||||
"reasoning_items": [
|
||||
{
|
||||
"type": "reasoning",
|
||||
"id": "rs_2",
|
||||
"summary": [{"type": "summary_text", "text": "delta summary"}],
|
||||
"content": [{"type": "reasoning_text", "text": "delta raw reasoning"}],
|
||||
}
|
||||
],
|
||||
}
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
redacted = perform_redaction({}, result)
|
||||
|
||||
message_item = redacted["choices"][0]["message"]["reasoning_items"][0]
|
||||
assert message_item["summary"][0]["text"] == "redacted-by-litellm"
|
||||
assert message_item["content"][0]["text"] == "redacted-by-litellm"
|
||||
|
||||
delta_item = redacted["choices"][1]["delta"]["reasoning_items"][0]
|
||||
assert delta_item["summary"][0]["text"] == "redacted-by-litellm"
|
||||
assert delta_item["content"][0]["text"] == "redacted-by-litellm"
|
||||
|
||||
def test_redacts_reasoning_items_on_choice_objects(self):
|
||||
from litellm.litellm_core_utils.redact_messages import _redact_choice_content
|
||||
from litellm.types.utils import Choices, Delta, Message, StreamingChoices
|
||||
|
||||
reasoning_items = [
|
||||
{
|
||||
"type": "reasoning",
|
||||
"id": "rs_1",
|
||||
"summary": [{"type": "summary_text", "text": "summary text"}],
|
||||
"content": [{"type": "reasoning_text", "text": "raw reasoning"}],
|
||||
}
|
||||
]
|
||||
|
||||
message_choice = Choices(
|
||||
message=Message(role="assistant", content="answer", reasoning_items=reasoning_items),
|
||||
finish_reason="stop",
|
||||
index=0,
|
||||
)
|
||||
_redact_choice_content(message_choice)
|
||||
item = message_choice.message.reasoning_items[0]
|
||||
assert item["summary"][0]["text"] == "redacted-by-litellm"
|
||||
assert item["content"][0]["text"] == "redacted-by-litellm"
|
||||
|
||||
stream_choice = StreamingChoices(
|
||||
delta=Delta(
|
||||
content="chunk",
|
||||
reasoning_items=[
|
||||
{
|
||||
"type": "reasoning",
|
||||
"id": "rs_2",
|
||||
"summary": [{"type": "summary_text", "text": "delta summary"}],
|
||||
"content": [{"type": "reasoning_text", "text": "delta raw"}],
|
||||
}
|
||||
],
|
||||
),
|
||||
finish_reason=None,
|
||||
index=0,
|
||||
)
|
||||
_redact_choice_content(stream_choice)
|
||||
delta_item = stream_choice.delta.reasoning_items[0]
|
||||
assert delta_item["summary"][0]["text"] == "redacted-by-litellm"
|
||||
assert delta_item["content"][0]["text"] == "redacted-by-litellm"
|
||||
|
||||
def test_redacts_standard_logging_model_response_dict_choices(self):
|
||||
details = {
|
||||
"standard_logging_object": {
|
||||
|
|
|
|||
12
ui/litellm-dashboard/src/lib/http/schema.d.ts
generated
vendored
12
ui/litellm-dashboard/src/lib/http/schema.d.ts
generated
vendored
|
|
@ -25028,6 +25028,8 @@ export interface components {
|
|||
* @description Represents an OpenAI Responses API reasoning item for round-tripping in conversation history.
|
||||
*/
|
||||
ChatCompletionReasoningItem: {
|
||||
/** Content */
|
||||
content?: components["schemas"]["ChatCompletionReasoningTextBlock"][];
|
||||
/** Encrypted Content */
|
||||
encrypted_content?: string | null;
|
||||
/** Id */
|
||||
|
|
@ -25050,6 +25052,16 @@ export interface components {
|
|||
*/
|
||||
type: "summary_text";
|
||||
};
|
||||
/** ChatCompletionReasoningTextBlock */
|
||||
ChatCompletionReasoningTextBlock: {
|
||||
/** Text */
|
||||
text?: string;
|
||||
/**
|
||||
* Type
|
||||
* @constant
|
||||
*/
|
||||
type: "reasoning_text";
|
||||
};
|
||||
/** ChatCompletionRedactedThinkingBlock */
|
||||
ChatCompletionRedactedThinkingBlock: {
|
||||
/** Cache Control */
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue