mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-19 00:01:29 +00:00
refactor(interactions): remove expired use_legacy_interactions_schema shim
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
parent
decbb96382
commit
07d593d831
6 changed files with 87 additions and 268 deletions
|
|
@ -267,10 +267,6 @@ route_all_chat_openai_to_responses: bool = (
|
|||
# When True, Gemini/Vertex Live setup is deferred until client `session.update`.
|
||||
# Default False preserves historical behavior (auto-send setup on connect).
|
||||
gemini_live_defer_setup: bool = os.getenv("LITELLM_GEMINI_LIVE_DEFER_SETUP", "false").lower() == "true"
|
||||
use_legacy_interactions_schema: bool = (
|
||||
os.getenv("LITELLM_USE_LEGACY_INTERACTIONS_SCHEMA", "false").lower() == "true"
|
||||
) # When True, sends Api-Revision: 2026-05-07 to Google so responses use the legacy `outputs`
|
||||
# schema instead of the new `steps` schema. Remove this flag after June 8, 2026.
|
||||
retry = True
|
||||
### AUTH ###
|
||||
api_key: Optional[str] = None
|
||||
|
|
|
|||
|
|
@ -33,11 +33,8 @@ class LiteLLMResponsesInteractionsStreamingIterator:
|
|||
streaming events (output.text.delta, response.completed, etc.) to Interactions
|
||||
API streaming events.
|
||||
|
||||
Schema selection:
|
||||
- New schema (default, use_legacy_interactions_schema=False):
|
||||
interaction.created -> step.start -> step.delta ... -> step.stop -> interaction.completed
|
||||
- Legacy schema (use_legacy_interactions_schema=True, remove after June 8 2026):
|
||||
interaction.start -> content.start -> content.delta ... -> content.stop -> interaction.complete
|
||||
Emits the event sequence
|
||||
``interaction.created -> step.start -> step.delta ... -> step.stop -> interaction.completed``.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
|
|
@ -49,8 +46,6 @@ class LiteLLMResponsesInteractionsStreamingIterator:
|
|||
custom_llm_provider: str | None = None,
|
||||
litellm_metadata: dict[str, Any] | None = None,
|
||||
):
|
||||
import litellm
|
||||
|
||||
self.model = model
|
||||
self.responses_stream_iterator = litellm_custom_stream_wrapper
|
||||
self.request_input = request_input
|
||||
|
|
@ -61,10 +56,6 @@ class LiteLLMResponsesInteractionsStreamingIterator:
|
|||
self.collected_text = ""
|
||||
self.sent_interaction_start = False
|
||||
self.sent_content_start = False
|
||||
# Capture the schema flag once at construction time so all events
|
||||
# emitted by this stream use a consistent schema, even if the global
|
||||
# flag is mutated mid-stream (e.g. by a config reload).
|
||||
self._use_legacy: bool = litellm.use_legacy_interactions_schema
|
||||
# Buffer of events that have been derived from upstream chunks but not
|
||||
# yet returned to the caller. A single Responses API chunk may expand
|
||||
# into multiple Interactions API events (e.g. the first text delta
|
||||
|
|
@ -85,9 +76,8 @@ class LiteLLMResponsesInteractionsStreamingIterator:
|
|||
# ------------------------------------------------------------------
|
||||
|
||||
def _build_interaction_start_event(self, interaction_id: str) -> InteractionsAPIStreamingResponse:
|
||||
event_type: Final = "interaction.start" if self._use_legacy else "interaction.created"
|
||||
return InteractionsAPIStreamingResponse(
|
||||
event_type=event_type,
|
||||
event_type="interaction.created",
|
||||
id=interaction_id,
|
||||
object="interaction",
|
||||
status="in_progress",
|
||||
|
|
@ -95,13 +85,6 @@ class LiteLLMResponsesInteractionsStreamingIterator:
|
|||
)
|
||||
|
||||
def _build_content_start_event(self, interaction_id: str) -> InteractionsAPIStreamingResponse:
|
||||
if self._use_legacy:
|
||||
return InteractionsAPIStreamingResponse(
|
||||
event_type="content.start",
|
||||
id=interaction_id,
|
||||
object="content",
|
||||
delta={"type": "text", "text": ""},
|
||||
)
|
||||
return InteractionsAPIStreamingResponse(
|
||||
event_type="step.start",
|
||||
index=0,
|
||||
|
|
@ -109,13 +92,6 @@ class LiteLLMResponsesInteractionsStreamingIterator:
|
|||
)
|
||||
|
||||
def _build_text_delta_event(self, interaction_id: str, delta_text: str) -> InteractionsAPIStreamingResponse:
|
||||
if self._use_legacy:
|
||||
return InteractionsAPIStreamingResponse(
|
||||
event_type="content.delta",
|
||||
id=interaction_id,
|
||||
object="content",
|
||||
delta={"type": "text", "text": delta_text},
|
||||
)
|
||||
return InteractionsAPIStreamingResponse(
|
||||
event_type="step.delta",
|
||||
index=0,
|
||||
|
|
@ -123,28 +99,12 @@ class LiteLLMResponsesInteractionsStreamingIterator:
|
|||
)
|
||||
|
||||
def _build_content_stop_event(self, interaction_id: str | None) -> InteractionsAPIStreamingResponse:
|
||||
if self._use_legacy:
|
||||
return InteractionsAPIStreamingResponse(
|
||||
event_type="content.stop",
|
||||
id=interaction_id,
|
||||
object="content",
|
||||
delta={"type": "text", "text": self.collected_text},
|
||||
)
|
||||
return InteractionsAPIStreamingResponse(
|
||||
event_type="step.stop",
|
||||
index=0,
|
||||
)
|
||||
|
||||
def _build_completion_event(self, response_id: str) -> InteractionsAPIStreamingResponse:
|
||||
if self._use_legacy:
|
||||
return InteractionsAPIStreamingResponse(
|
||||
event_type="interaction.complete",
|
||||
id=response_id,
|
||||
object="interaction",
|
||||
status="completed",
|
||||
model=self.model,
|
||||
outputs=[{"type": "text", "text": self.collected_text}],
|
||||
)
|
||||
return InteractionsAPIStreamingResponse(
|
||||
event_type="interaction.completed",
|
||||
id=response_id,
|
||||
|
|
@ -234,7 +194,7 @@ class LiteLLMResponsesInteractionsStreamingIterator:
|
|||
"""
|
||||
Build the events to flush when the upstream stream ends without a
|
||||
ResponseCompletedEvent. Ensures consumers always observe a terminal
|
||||
interaction.completed/interaction.complete carrying the full text.
|
||||
interaction.completed carrying the full text.
|
||||
"""
|
||||
if self._sent_completion_event:
|
||||
return []
|
||||
|
|
|
|||
|
|
@ -3899,9 +3899,8 @@ class Logging(LiteLLMLoggingBaseClass):
|
|||
) -> InteractionsAPIResponse | None:
|
||||
"""
|
||||
The Interactions API streaming iterator hands the terminal event to the
|
||||
success handlers: the new schema (Api-Revision: 2026-05-20) emits
|
||||
``interaction.completed`` carrying the full interaction object, the
|
||||
legacy schema (2026-05-07) emits a chunk with ``status="completed"``
|
||||
success handlers: ``interaction.completed`` may carry the full
|
||||
interaction object, or the final chunk may carry ``status="completed"``
|
||||
and usage on the chunk itself. Build the equivalent non-streaming
|
||||
response so cost calculation and spend tracking see one shape.
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -6,10 +6,7 @@ Per OpenAPI spec (https://ai.google.dev/static/api/interactions.openapi.json):
|
|||
- Get: GET https://generativelanguage.googleapis.com/{api_version}/interactions/{interaction_id}
|
||||
- Delete: DELETE https://generativelanguage.googleapis.com/{api_version}/interactions/{interaction_id}
|
||||
|
||||
Schema versioning:
|
||||
- Default (Api-Revision: 2026-05-20): new `steps` schema.
|
||||
- Legacy (Api-Revision: 2026-05-07): old `outputs` schema, controlled via
|
||||
litellm.use_legacy_interactions_schema = True. Remove flag after June 8, 2026.
|
||||
Requests use Api-Revision 2026-05-20 (`steps` schema).
|
||||
"""
|
||||
|
||||
from typing import TYPE_CHECKING, Any, Final, Protocol, TypeAlias
|
||||
|
|
@ -17,7 +14,6 @@ from typing import TYPE_CHECKING, Any, Final, Protocol, TypeAlias
|
|||
import httpx
|
||||
from typing_extensions import ReadOnly, TypedDict
|
||||
|
||||
import litellm
|
||||
from litellm._logging import verbose_logger
|
||||
from litellm.litellm_core_utils.core_helpers import process_response_headers
|
||||
from litellm.litellm_core_utils.url_utils import encode_url_path_segment
|
||||
|
|
@ -137,13 +133,7 @@ class GoogleAIStudioInteractionsConfig(BaseInteractionsAPIConfig):
|
|||
if api_key:
|
||||
headers["x-goog-api-key"] = api_key
|
||||
|
||||
# Inject the Api-Revision header to select the response schema.
|
||||
# Default to the new `steps` schema unless the operator has opted out.
|
||||
# Remove this conditional after June 8, 2026 and always use 2026-05-20.
|
||||
if litellm.use_legacy_interactions_schema:
|
||||
headers["Api-Revision"] = "2026-05-07"
|
||||
else:
|
||||
headers["Api-Revision"] = "2026-05-20"
|
||||
headers["Api-Revision"] = "2026-05-20"
|
||||
|
||||
return headers
|
||||
|
||||
|
|
@ -180,17 +170,11 @@ class GoogleAIStudioInteractionsConfig(BaseInteractionsAPIConfig):
|
|||
"""
|
||||
Build request body per OpenAPI spec.
|
||||
|
||||
When on the new schema (use_legacy_interactions_schema=False, the default):
|
||||
- ``response_mime_type`` is folded into ``response_format`` and stripped from
|
||||
the body (the field was removed in Api-Revision 2026-05-20).
|
||||
- ``generation_config.image_config`` is moved to a ``response_format`` entry
|
||||
with ``"type": "image"`` (also removed from generation_config in 2026-05-20).
|
||||
|
||||
When on the legacy schema (use_legacy_interactions_schema=True):
|
||||
- All fields are forwarded as-is.
|
||||
"""
|
||||
use_legacy: Final[bool] = litellm.use_legacy_interactions_schema
|
||||
|
||||
request_body: Final[dict[str, object]] = {}
|
||||
|
||||
# Model or Agent (one required)
|
||||
|
|
@ -205,7 +189,6 @@ class GoogleAIStudioInteractionsConfig(BaseInteractionsAPIConfig):
|
|||
if input is not None:
|
||||
request_body["input"] = input
|
||||
|
||||
# Pass through optional params — legacy schema keeps all fields as-is.
|
||||
optional_keys: Final = [
|
||||
"tools",
|
||||
"system_instruction",
|
||||
|
|
@ -220,58 +203,51 @@ class GoogleAIStudioInteractionsConfig(BaseInteractionsAPIConfig):
|
|||
if optional_params.get(key) is not None:
|
||||
request_body[key] = optional_params[key]
|
||||
|
||||
if use_legacy:
|
||||
# Legacy schema: forward response_mime_type and response_format as-is.
|
||||
for key in ("response_format", "response_mime_type", "generation_config"):
|
||||
if optional_params.get(key) is not None:
|
||||
request_body[key] = optional_params[key]
|
||||
else:
|
||||
# New schema (Api-Revision: 2026-05-20):
|
||||
# response_mime_type is removed — fold it into response_format.
|
||||
response_format = optional_params.get("response_format")
|
||||
response_mime_type: Final = optional_params.get("response_mime_type")
|
||||
|
||||
if (
|
||||
response_mime_type
|
||||
and not isinstance(response_format, list)
|
||||
and (not isinstance(response_format, dict) or "mime_type" not in response_format)
|
||||
):
|
||||
# Wrap the legacy schema into the new polymorphic format.
|
||||
new_rf: Final[dict[str, object]] = {
|
||||
"type": "text",
|
||||
"mime_type": response_mime_type,
|
||||
}
|
||||
if response_format is not None:
|
||||
new_rf["schema"] = response_format
|
||||
response_format = new_rf
|
||||
# response_mime_type is removed — fold it into response_format.
|
||||
response_format = optional_params.get("response_format")
|
||||
response_mime_type: Final = optional_params.get("response_mime_type")
|
||||
|
||||
if (
|
||||
response_mime_type
|
||||
and not isinstance(response_format, list)
|
||||
and (not isinstance(response_format, dict) or "mime_type" not in response_format)
|
||||
):
|
||||
# Wrap the legacy schema into the new polymorphic format.
|
||||
new_rf: Final[dict[str, object]] = {
|
||||
"type": "text",
|
||||
"mime_type": response_mime_type,
|
||||
}
|
||||
if response_format is not None:
|
||||
request_body["response_format"] = response_format
|
||||
new_rf["schema"] = response_format
|
||||
response_format = new_rf
|
||||
|
||||
if response_format is not None:
|
||||
request_body["response_format"] = response_format
|
||||
|
||||
# image_config moves out of generation_config into response_format.
|
||||
generation_config: dict[str, Any] | None = optional_params.get("generation_config")
|
||||
if generation_config is not None:
|
||||
image_config = None
|
||||
if isinstance(generation_config, dict):
|
||||
generation_config = dict(generation_config) # avoid mutating the caller's dict
|
||||
image_config = generation_config.pop("image_config", None)
|
||||
if not generation_config:
|
||||
generation_config = None
|
||||
|
||||
# image_config moves out of generation_config into response_format.
|
||||
generation_config: dict[str, Any] | None = optional_params.get("generation_config")
|
||||
if generation_config is not None:
|
||||
image_config = None
|
||||
if isinstance(generation_config, dict):
|
||||
generation_config = dict(generation_config) # avoid mutating the caller's dict
|
||||
image_config = generation_config.pop("image_config", None)
|
||||
if not generation_config:
|
||||
generation_config = None
|
||||
request_body["generation_config"] = generation_config
|
||||
|
||||
if generation_config is not None:
|
||||
request_body["generation_config"] = generation_config
|
||||
|
||||
if image_config is not None:
|
||||
# Move image_config to response_format with type=image.
|
||||
image_rf: Final[_JsonObject] = {"type": "image", **image_config}
|
||||
existing_rf: Final = request_body.get("response_format")
|
||||
if existing_rf is None:
|
||||
request_body["response_format"] = image_rf
|
||||
elif isinstance(existing_rf, list):
|
||||
request_body["response_format"] = [*existing_rf, image_rf]
|
||||
else:
|
||||
# Convert single entry to array for multimodal output.
|
||||
request_body["response_format"] = [existing_rf, image_rf]
|
||||
if image_config is not None:
|
||||
# Move image_config to response_format with type=image.
|
||||
image_rf: Final[_JsonObject] = {"type": "image", **image_config}
|
||||
existing_rf: Final = request_body.get("response_format")
|
||||
if existing_rf is None:
|
||||
request_body["response_format"] = image_rf
|
||||
elif isinstance(existing_rf, list):
|
||||
request_body["response_format"] = [*existing_rf, image_rf]
|
||||
else:
|
||||
# Convert single entry to array for multimodal output.
|
||||
request_body["response_format"] = [existing_rf, image_rf]
|
||||
|
||||
return request_body
|
||||
|
||||
|
|
|
|||
|
|
@ -6037,13 +6037,6 @@ class ProxyConfig:
|
|||
health_check_interval = general_settings.get("health_check_interval", DEFAULT_HEALTH_CHECK_INTERVAL)
|
||||
health_check_concurrency = general_settings.get("health_check_concurrency", None)
|
||||
health_check_details = general_settings.get("health_check_details", True)
|
||||
### INTERACTIONS API SCHEMA ###
|
||||
_use_legacy_interactions_schema: Final = general_settings.get("use_legacy_interactions_schema")
|
||||
if _use_legacy_interactions_schema is not None:
|
||||
if isinstance(_use_legacy_interactions_schema, str):
|
||||
litellm.use_legacy_interactions_schema = _use_legacy_interactions_schema.lower() == "true"
|
||||
else:
|
||||
litellm.use_legacy_interactions_schema = bool(_use_legacy_interactions_schema)
|
||||
# Health-check-driven routing (opt-in, passes through to Router later)
|
||||
_enable_hc_routing = general_settings.get("enable_health_check_routing", False)
|
||||
_hc_staleness = general_settings.get("health_check_staleness_threshold", None)
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
Tests for Gemini Interactions API transformation.
|
||||
|
||||
Covers:
|
||||
- validate_environment: x-goog-api-key header, Api-Revision schema selection
|
||||
- validate_environment: x-goog-api-key header, Api-Revision header
|
||||
- get_complete_url: API key excluded from URL
|
||||
- get/delete/cancel interaction request URLs
|
||||
- transform_request: response_mime_type coalescing, image_config migration
|
||||
|
|
@ -13,7 +13,6 @@ from unittest.mock import MagicMock, patch
|
|||
import pytest
|
||||
|
||||
|
||||
import litellm
|
||||
from litellm.interactions.litellm_responses_transformation.streaming_iterator import (
|
||||
LiteLLMResponsesInteractionsStreamingIterator,
|
||||
)
|
||||
|
|
@ -83,22 +82,10 @@ class TestValidateEnvironment:
|
|||
assert headers["X-Custom"] == "value"
|
||||
assert headers["x-goog-api-key"] == "test-key"
|
||||
|
||||
def test_api_revision_new_schema_by_default(self, config, monkeypatch: pytest.MonkeyPatch):
|
||||
# Default: use_legacy_interactions_schema=False → new steps schema
|
||||
monkeypatch.setattr(litellm, "use_legacy_interactions_schema", False)
|
||||
headers = config.validate_environment(
|
||||
headers={}, model="gemini-2.5-flash", litellm_params=None
|
||||
)
|
||||
def test_sets_api_revision_header(self, config):
|
||||
headers = config.validate_environment(headers={}, model="gemini-2.5-flash", litellm_params=None)
|
||||
assert headers["Api-Revision"] == "2026-05-20"
|
||||
|
||||
def test_api_revision_legacy_schema_when_flag_set(self, config, monkeypatch: pytest.MonkeyPatch):
|
||||
# Flag on → legacy outputs schema until June 8, 2026
|
||||
monkeypatch.setattr(litellm, "use_legacy_interactions_schema", True)
|
||||
headers = config.validate_environment(
|
||||
headers={}, model="gemini-2.5-flash", litellm_params=None
|
||||
)
|
||||
assert headers["Api-Revision"] == "2026-05-07"
|
||||
|
||||
|
||||
class TestGetCompleteUrl:
|
||||
def test_url_excludes_api_key(self, config):
|
||||
|
|
@ -158,9 +145,7 @@ class TestTransformRequest:
|
|||
assert request_body["agent"] == "my-custom-slides-agent"
|
||||
assert request_body["environment"] == "remote"
|
||||
assert request_body["stream"] is False
|
||||
assert request_body["input"] == [
|
||||
{"type": "text", "text": "Create a 5-slide presentation about AI trends."}
|
||||
]
|
||||
assert request_body["input"] == [{"type": "text", "text": "Create a 5-slide presentation about AI trends."}]
|
||||
|
||||
def test_passes_environment_object_to_request_body(self, config):
|
||||
environment_config = {
|
||||
|
|
@ -221,24 +206,15 @@ class TestTransformRequest:
|
|||
|
||||
|
||||
class TestStreamingIterator:
|
||||
def _make_iterator(
|
||||
self, use_legacy: bool = False
|
||||
) -> LiteLLMResponsesInteractionsStreamingIterator:
|
||||
original = litellm.use_legacy_interactions_schema
|
||||
litellm.use_legacy_interactions_schema = use_legacy
|
||||
try:
|
||||
return LiteLLMResponsesInteractionsStreamingIterator(
|
||||
model="gpt-5.4",
|
||||
litellm_custom_stream_wrapper=MagicMock(),
|
||||
request_input="hi",
|
||||
optional_params={},
|
||||
)
|
||||
finally:
|
||||
litellm.use_legacy_interactions_schema = original
|
||||
def _make_iterator(self) -> LiteLLMResponsesInteractionsStreamingIterator:
|
||||
return LiteLLMResponsesInteractionsStreamingIterator(
|
||||
model="gpt-5.4",
|
||||
litellm_custom_stream_wrapper=MagicMock(),
|
||||
request_input="hi",
|
||||
optional_params={},
|
||||
)
|
||||
|
||||
def _make_text_delta(
|
||||
self, text: str, item_id: str = "item_1"
|
||||
) -> OutputTextDeltaEvent:
|
||||
def _make_text_delta(self, text: str, item_id: str = "item_1") -> OutputTextDeltaEvent:
|
||||
event = MagicMock(spec=OutputTextDeltaEvent)
|
||||
event.delta = text
|
||||
event.item_id = item_id
|
||||
|
|
@ -251,58 +227,29 @@ class TestStreamingIterator:
|
|||
|
||||
def test_step_delta_includes_type_field(self):
|
||||
"""step.delta events must carry delta.type='text' so the UI can display them."""
|
||||
it = self._make_iterator(use_legacy=False)
|
||||
it = self._make_iterator()
|
||||
it.sent_interaction_start = True
|
||||
it.sent_content_start = True
|
||||
|
||||
chunk = it._transform_responses_chunk_to_interactions_chunk(
|
||||
self._make_text_delta("Hello")
|
||||
)
|
||||
chunk = it._transform_responses_chunk_to_interactions_chunk(self._make_text_delta("Hello"))
|
||||
|
||||
assert chunk is not None
|
||||
assert chunk.event_type == "step.delta"
|
||||
assert chunk.delta == {"type": "text", "text": "Hello"}
|
||||
|
||||
def test_content_delta_legacy_schema(self):
|
||||
"""Legacy schema emits content.delta with type and text fields."""
|
||||
it = self._make_iterator(use_legacy=True)
|
||||
it.sent_interaction_start = True
|
||||
it.sent_content_start = True
|
||||
|
||||
chunk = it._transform_responses_chunk_to_interactions_chunk(
|
||||
self._make_text_delta("Hello")
|
||||
)
|
||||
|
||||
assert chunk is not None
|
||||
assert chunk.event_type == "content.delta"
|
||||
assert chunk.delta == {"type": "text", "text": "Hello"}
|
||||
|
||||
def test_response_created_emits_interaction_created(self):
|
||||
it = self._make_iterator(use_legacy=False)
|
||||
it = self._make_iterator()
|
||||
|
||||
chunk = it._transform_responses_chunk_to_interactions_chunk(
|
||||
self._make_response_created()
|
||||
)
|
||||
chunk = it._transform_responses_chunk_to_interactions_chunk(self._make_response_created())
|
||||
|
||||
assert chunk is not None
|
||||
assert chunk.event_type == "interaction.created"
|
||||
assert chunk.id == "resp_123"
|
||||
assert it.sent_interaction_start is True
|
||||
|
||||
def test_response_created_emits_interaction_start_legacy(self):
|
||||
it = self._make_iterator(use_legacy=True)
|
||||
|
||||
chunk = it._transform_responses_chunk_to_interactions_chunk(
|
||||
self._make_response_created()
|
||||
)
|
||||
|
||||
assert chunk is not None
|
||||
assert chunk.event_type == "interaction.start"
|
||||
assert chunk.id == "resp_123"
|
||||
|
||||
def test_text_delta_sequence_new_schema(self):
|
||||
def test_text_delta_sequence(self):
|
||||
"""First chunk yields created + step.start + step.delta; later chunks yield step.delta."""
|
||||
it = self._make_iterator(use_legacy=False)
|
||||
it = self._make_iterator()
|
||||
|
||||
first_events = it._events_for_chunk(self._make_text_delta("Hello"))
|
||||
assert [e.event_type for e in first_events] == [
|
||||
|
|
@ -322,24 +269,8 @@ class TestStreamingIterator:
|
|||
assert [e.event_type for e in third_events] == ["step.delta"]
|
||||
assert third_events[0].delta == {"type": "text", "text": "!"}
|
||||
|
||||
def test_text_delta_sequence_legacy_schema(self):
|
||||
"""Legacy: first chunk yields interaction.start + content.start + content.delta."""
|
||||
it = self._make_iterator(use_legacy=True)
|
||||
|
||||
first_events = it._events_for_chunk(self._make_text_delta("Hello"))
|
||||
assert [e.event_type for e in first_events] == [
|
||||
"interaction.start",
|
||||
"content.start",
|
||||
"content.delta",
|
||||
]
|
||||
assert first_events[-1].delta == {"type": "text", "text": "Hello"}
|
||||
|
||||
second_events = it._events_for_chunk(self._make_text_delta(" World"))
|
||||
assert [e.event_type for e in second_events] == ["content.delta"]
|
||||
assert second_events[0].delta == {"type": "text", "text": " World"}
|
||||
|
||||
def test_first_text_delta_without_item_id_uses_fallback_id(self):
|
||||
it = self._make_iterator(use_legacy=False)
|
||||
it = self._make_iterator()
|
||||
event = self._make_text_delta("Hi")
|
||||
event.item_id = None
|
||||
|
||||
|
|
@ -350,11 +281,9 @@ class TestStreamingIterator:
|
|||
|
||||
def test_first_text_delta_emits_text_via_compat_shim(self):
|
||||
"""The legacy single-chunk shim must surface the synthetic events AND the delta."""
|
||||
it = self._make_iterator(use_legacy=False)
|
||||
it = self._make_iterator()
|
||||
|
||||
first = it._transform_responses_chunk_to_interactions_chunk(
|
||||
self._make_text_delta("Hello")
|
||||
)
|
||||
first = it._transform_responses_chunk_to_interactions_chunk(self._make_text_delta("Hello"))
|
||||
assert first is not None
|
||||
assert first.event_type == "interaction.created"
|
||||
|
||||
|
|
@ -369,7 +298,7 @@ class TestStreamingIterator:
|
|||
|
||||
def test_response_created_then_text_delta_emits_step_start_and_delta(self):
|
||||
"""Realistic flow: response.created arrives first, then text delta."""
|
||||
it = self._make_iterator(use_legacy=False)
|
||||
it = self._make_iterator()
|
||||
|
||||
first = it._events_for_chunk(self._make_response_created())
|
||||
assert [e.event_type for e in first] == ["interaction.created"]
|
||||
|
|
@ -380,7 +309,7 @@ class TestStreamingIterator:
|
|||
|
||||
def test_no_text_token_is_dropped_during_streaming(self):
|
||||
"""Concatenated step.delta payloads must equal the upstream text."""
|
||||
it = self._make_iterator(use_legacy=False)
|
||||
it = self._make_iterator()
|
||||
|
||||
chunks = ["Hello", " ", "world", "!"]
|
||||
emitted_text = ""
|
||||
|
|
@ -401,17 +330,12 @@ class TestStreamingIterator:
|
|||
sync_iter.__iter__ = lambda self: self
|
||||
sync_iter.__next__ = MagicMock(side_effect=[text_event, StopIteration])
|
||||
|
||||
original = litellm.use_legacy_interactions_schema
|
||||
litellm.use_legacy_interactions_schema = False
|
||||
try:
|
||||
it = LiteLLMResponsesInteractionsStreamingIterator(
|
||||
model="gpt-5.4",
|
||||
litellm_custom_stream_wrapper=sync_iter,
|
||||
request_input="hi",
|
||||
optional_params={},
|
||||
)
|
||||
finally:
|
||||
litellm.use_legacy_interactions_schema = original
|
||||
it = LiteLLMResponsesInteractionsStreamingIterator(
|
||||
model="gpt-5.4",
|
||||
litellm_custom_stream_wrapper=sync_iter,
|
||||
request_input="hi",
|
||||
optional_params={},
|
||||
)
|
||||
|
||||
emitted: list = []
|
||||
try:
|
||||
|
|
@ -450,17 +374,12 @@ class TestStreamingIterator:
|
|||
sync_iter.__iter__ = lambda self: self
|
||||
sync_iter.__next__ = MagicMock(side_effect=[text_event, completed])
|
||||
|
||||
original = litellm.use_legacy_interactions_schema
|
||||
litellm.use_legacy_interactions_schema = False
|
||||
try:
|
||||
it = LiteLLMResponsesInteractionsStreamingIterator(
|
||||
model="gpt-5.4",
|
||||
litellm_custom_stream_wrapper=sync_iter,
|
||||
request_input="hi",
|
||||
optional_params={},
|
||||
)
|
||||
finally:
|
||||
litellm.use_legacy_interactions_schema = original
|
||||
it = LiteLLMResponsesInteractionsStreamingIterator(
|
||||
model="gpt-5.4",
|
||||
litellm_custom_stream_wrapper=sync_iter,
|
||||
request_input="hi",
|
||||
optional_params={},
|
||||
)
|
||||
|
||||
emitted: list = []
|
||||
try:
|
||||
|
|
@ -506,9 +425,7 @@ class TestInteractionOperationUrls:
|
|||
),
|
||||
],
|
||||
)
|
||||
def test_url_excludes_key(
|
||||
self, config, method_name, interaction_id, expected_suffix
|
||||
):
|
||||
def test_url_excludes_key(self, config, method_name, interaction_id, expected_suffix):
|
||||
with patch(_PATCH_GET_API_KEY, return_value="secret-key"):
|
||||
url, params = getattr(config, method_name)(
|
||||
interaction_id=interaction_id,
|
||||
|
|
@ -550,8 +467,7 @@ class TestInteractionOperationUrls:
|
|||
class TestTransformRequestSchemaCoalescing:
|
||||
"""Test new-schema request coalescing (Api-Revision: 2026-05-20)."""
|
||||
|
||||
def test_response_mime_type_folded_into_response_format(self, config, monkeypatch: pytest.MonkeyPatch):
|
||||
monkeypatch.setattr(litellm, "use_legacy_interactions_schema", False)
|
||||
def test_response_mime_type_folded_into_response_format(self, config):
|
||||
body = config.transform_request(
|
||||
model="gemini/gemini-2.5-flash",
|
||||
agent=None,
|
||||
|
|
@ -571,8 +487,7 @@ class TestTransformRequestSchemaCoalescing:
|
|||
assert rf["mime_type"] == "application/json"
|
||||
assert "schema" in rf
|
||||
|
||||
def test_image_config_moved_to_response_format(self, config, monkeypatch: pytest.MonkeyPatch):
|
||||
monkeypatch.setattr(litellm, "use_legacy_interactions_schema", False)
|
||||
def test_image_config_moved_to_response_format(self, config):
|
||||
body = config.transform_request(
|
||||
model="gemini/gemini-2.5-flash",
|
||||
agent=None,
|
||||
|
|
@ -594,9 +509,8 @@ class TestTransformRequestSchemaCoalescing:
|
|||
assert rf["type"] == "image"
|
||||
assert rf["aspect_ratio"] == "1:1"
|
||||
|
||||
def test_response_mime_type_skipped_when_response_format_is_list(self, config, monkeypatch: pytest.MonkeyPatch):
|
||||
def test_response_mime_type_skipped_when_response_format_is_list(self, config):
|
||||
"""Lists are already polymorphic; do not wrap them into schema."""
|
||||
monkeypatch.setattr(litellm, "use_legacy_interactions_schema", False)
|
||||
rf_list = [
|
||||
{"type": "text", "mime_type": "application/json"},
|
||||
{"type": "image", "aspect_ratio": "1:1"},
|
||||
|
|
@ -619,10 +533,8 @@ class TestTransformRequestSchemaCoalescing:
|
|||
def test_image_config_appended_to_response_format_list_without_mutating_input(
|
||||
self,
|
||||
config,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
):
|
||||
"""When response_format is already a list, image_config must not mutate optional_params."""
|
||||
monkeypatch.setattr(litellm, "use_legacy_interactions_schema", False)
|
||||
text_rf = {"type": "text", "mime_type": "application/json"}
|
||||
optional_params = {
|
||||
"response_format": [text_rf],
|
||||
|
|
@ -659,20 +571,3 @@ class TestTransformRequestSchemaCoalescing:
|
|||
)
|
||||
assert len(optional_params["response_format"]) == 1
|
||||
assert body_retry["response_format"] == body["response_format"]
|
||||
|
||||
def test_legacy_schema_passes_fields_unchanged(self, config, monkeypatch: pytest.MonkeyPatch):
|
||||
monkeypatch.setattr(litellm, "use_legacy_interactions_schema", True)
|
||||
body = config.transform_request(
|
||||
model="gemini/gemini-2.5-flash",
|
||||
agent=None,
|
||||
input="hello",
|
||||
optional_params={
|
||||
"response_mime_type": "application/json",
|
||||
"generation_config": {"image_config": {"aspect_ratio": "16:9"}},
|
||||
},
|
||||
litellm_params=GenericLiteLLMParams(),
|
||||
headers={},
|
||||
)
|
||||
|
||||
assert body["response_mime_type"] == "application/json"
|
||||
assert body["generation_config"]["image_config"]["aspect_ratio"] == "16:9"
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue