211 lines
6.2 KiB
Python
211 lines
6.2 KiB
Python
"""
|
|
Test for Gemini thought signatures in streaming function calls.
|
|
|
|
Validates that thought signatures are captured from streaming chunks
|
|
and included in the final function call events.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from collections.abc import AsyncIterator
|
|
from typing import Any, cast
|
|
|
|
import pytest
|
|
from openai.types.chat import ChatCompletionChunk
|
|
from openai.types.chat.chat_completion_chunk import (
|
|
Choice,
|
|
ChoiceDelta,
|
|
ChoiceDeltaToolCall,
|
|
ChoiceDeltaToolCallFunction,
|
|
)
|
|
from openai.types.responses import Response
|
|
|
|
from agents.models.chatcmpl_stream_handler import ChatCmplStreamHandler
|
|
|
|
# ========== Helper Functions ==========
|
|
|
|
|
|
def create_tool_call_delta(
|
|
index: int,
|
|
tool_call_id: str | None = None,
|
|
function_name: str | None = None,
|
|
arguments: str | None = None,
|
|
provider_specific_fields: dict[str, Any] | None = None,
|
|
extra_content: dict[str, Any] | None = None,
|
|
) -> ChoiceDeltaToolCall:
|
|
"""Create a tool call delta for streaming."""
|
|
function = ChoiceDeltaToolCallFunction(
|
|
name=function_name,
|
|
arguments=arguments,
|
|
)
|
|
|
|
delta = ChoiceDeltaToolCall(
|
|
index=index,
|
|
id=tool_call_id,
|
|
type="function" if tool_call_id else None,
|
|
function=function,
|
|
)
|
|
|
|
# Add provider_specific_fields (litellm format)
|
|
if provider_specific_fields:
|
|
delta_any = cast(Any, delta)
|
|
delta_any.provider_specific_fields = provider_specific_fields
|
|
|
|
# Add extra_content (Google chatcmpl format)
|
|
if extra_content:
|
|
delta_any = cast(Any, delta)
|
|
delta_any.extra_content = extra_content
|
|
|
|
return delta
|
|
|
|
|
|
def create_chunk(
|
|
tool_calls: list[ChoiceDeltaToolCall] | None = None,
|
|
content: str | None = None,
|
|
include_usage: bool = False,
|
|
) -> ChatCompletionChunk:
|
|
"""Create a ChatCompletionChunk for testing."""
|
|
delta = ChoiceDelta(
|
|
content=content,
|
|
role="assistant" if content or tool_calls else None,
|
|
tool_calls=tool_calls,
|
|
)
|
|
|
|
chunk = ChatCompletionChunk(
|
|
id="chunk-id-123",
|
|
created=1,
|
|
model="gemini/gemini-3-pro",
|
|
object="chat.completion.chunk",
|
|
choices=[Choice(index=0, delta=delta, finish_reason=None)],
|
|
)
|
|
|
|
if include_usage:
|
|
from openai.types.completion_usage import CompletionUsage
|
|
|
|
chunk.usage = CompletionUsage(
|
|
completion_tokens=10,
|
|
prompt_tokens=5,
|
|
total_tokens=15,
|
|
)
|
|
|
|
return chunk
|
|
|
|
|
|
def create_final_chunk() -> ChatCompletionChunk:
|
|
"""Create a final chunk with finish_reason='tool_calls'."""
|
|
return ChatCompletionChunk(
|
|
id="chunk-id-456",
|
|
created=1,
|
|
model="gemini/gemini-3-pro",
|
|
object="chat.completion.chunk",
|
|
choices=[Choice(index=0, delta=ChoiceDelta(), finish_reason="tool_calls")],
|
|
)
|
|
|
|
|
|
async def create_fake_stream(
|
|
chunks: list[ChatCompletionChunk],
|
|
) -> AsyncIterator[ChatCompletionChunk]:
|
|
"""Create an async iterator from chunks."""
|
|
for chunk in chunks:
|
|
yield chunk
|
|
|
|
|
|
def create_mock_response() -> Response:
|
|
"""Create a mock Response object."""
|
|
return Response(
|
|
id="resp-id",
|
|
created_at=0,
|
|
model="gemini/gemini-3-pro",
|
|
object="response",
|
|
output=[],
|
|
tool_choice="auto",
|
|
tools=[],
|
|
parallel_tool_calls=False,
|
|
)
|
|
|
|
|
|
# ========== Tests ==========
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_stream_captures_litellmprovider_specific_fields_thought_signature():
|
|
"""Test streaming captures thought_signature from litellm's provider_specific_fields."""
|
|
chunks = [
|
|
create_chunk(
|
|
tool_calls=[
|
|
create_tool_call_delta(
|
|
index=0,
|
|
tool_call_id="call_stream_1",
|
|
function_name="get_weather",
|
|
provider_specific_fields={"thought_signature": "litellm_sig_123"},
|
|
)
|
|
]
|
|
),
|
|
create_chunk(tool_calls=[create_tool_call_delta(index=0, arguments='{"city": "Tokyo"}')]),
|
|
create_final_chunk(),
|
|
]
|
|
|
|
response = create_mock_response()
|
|
stream = create_fake_stream(chunks)
|
|
|
|
events = []
|
|
async for event in ChatCmplStreamHandler.handle_stream(
|
|
response,
|
|
stream, # type: ignore[arg-type]
|
|
model="gemini/gemini-3-pro",
|
|
):
|
|
events.append(event)
|
|
|
|
# Find function call done event
|
|
done_events = [e for e in events if e.type == "response.output_item.done"]
|
|
func_done = [
|
|
e for e in done_events if hasattr(e.item, "type") and e.item.type == "function_call"
|
|
]
|
|
assert len(func_done) == 1
|
|
|
|
provider_data = func_done[0].item.model_dump().get("provider_data", {})
|
|
assert provider_data.get("thought_signature") == "litellm_sig_123"
|
|
assert provider_data["model"] == "gemini/gemini-3-pro"
|
|
assert provider_data["response_id"] == "chunk-id-123"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_stream_captures_google_extra_content_thought_signature():
|
|
"""Test streaming captures thought_signature from Google's extra_content format."""
|
|
chunks = [
|
|
create_chunk(
|
|
tool_calls=[
|
|
create_tool_call_delta(
|
|
index=0,
|
|
tool_call_id="call_stream_2",
|
|
function_name="search",
|
|
extra_content={"google": {"thought_signature": "google_sig_456"}},
|
|
)
|
|
]
|
|
),
|
|
create_chunk(tool_calls=[create_tool_call_delta(index=0, arguments='{"query": "test"}')]),
|
|
create_final_chunk(),
|
|
]
|
|
|
|
response = create_mock_response()
|
|
stream = create_fake_stream(chunks)
|
|
|
|
events = []
|
|
async for event in ChatCmplStreamHandler.handle_stream(
|
|
response,
|
|
stream, # type: ignore[arg-type]
|
|
model="gemini/gemini-3-pro",
|
|
):
|
|
events.append(event)
|
|
|
|
done_events = [e for e in events if e.type == "response.output_item.done"]
|
|
func_done = [
|
|
e for e in done_events if hasattr(e.item, "type") and e.item.type == "function_call"
|
|
]
|
|
assert len(func_done) == 1
|
|
|
|
provider_data = func_done[0].item.model_dump().get("provider_data", {})
|
|
assert provider_data.get("thought_signature") == "google_sig_456"
|
|
assert provider_data["model"] == "gemini/gemini-3-pro"
|
|
assert provider_data["response_id"] == "chunk-id-123"
|