test: migrate openai and small-provider translation tests

This commit is contained in:
yuneng 2026-10-10 04:17:07 +00:00
parent ecc2f2c9ad
commit 7216244011
11 changed files with 861 additions and 1339 deletions

View file

@ -0,0 +1,258 @@
import json
from collections.abc import Callable
from typing import Final
from integration._support.client import Gateway
from integration._support.openai_wire import responses_reply
from integration._support.wire import Reply, Request, Wire, wire_server
from pydantic import JsonValue
TOKEN: Final = "scripted-openai-search-key"
SEARCH_ANNOTATION: Final[dict[str, JsonValue]] = {
"type": "url_citation",
"url_citation": {
"start_index": 0,
"end_index": 11,
"url": "https://news.example.test/positive-story",
"title": "Positive news",
},
}
def _responses_peer(identity: str, model: str, text: str) -> Callable[[Request], Reply]:
def respond(_: Request) -> Reply:
return responses_reply(identity, model, text, stream=True)
return respond
def _single_upstream_request(wire: Wire) -> Request:
received: Final = wire.drain()
assert len(received) == 1
return received[0]
def test_gpt_5_reasoning_streaming_sends_reasoning_effort(gateway: Gateway) -> None:
provider_body: Final = {
"model": "gpt-5-mini",
"input": [
{
"type": "message",
"role": "user",
"content": [{"type": "input_text", "text": "Think of a poem, and then write it."}],
}
],
"reasoning": {"effort": "low"},
"stream": True,
}
with wire_server(_responses_peer("reasoning-stream", "gpt-5-mini", "A small poem.")) as wire:
with gateway.scenario() as scenario:
model: Final = scenario.model(
model="openai/responses/gpt-5-mini",
api_key=TOKEN,
api_base=wire.url,
)
response: Final = gateway.request(
"POST",
"/v1/chat/completions",
{
"model": model,
"messages": [{"role": "user", "content": "Think of a poem, and then write it."}],
"reasoning_effort": "low",
"stream": True,
"cache": {"no-cache": True},
},
)
request: Final = _single_upstream_request(wire)
assert response.status_code == 200, response.text
assert (request.method, request.target) == ("POST", "/responses")
assert json.loads(request.body) == provider_body
assert "A small poem." in response.text
def test_gpt_5_web_search_maps_builtin_tool(gateway: Gateway) -> None:
provider_body: Final = {
"model": "gpt-5",
"input": [
{
"type": "message",
"role": "user",
"content": [{"type": "input_text", "text": "get price of nvda"}],
}
],
"temperature": 1,
"max_output_tokens": 8192,
"tools": [{"type": "web_search"}],
"stream": True,
}
with wire_server(_responses_peer("web-search", "gpt-5", "NVDA is listed at $123.")) as wire:
with gateway.scenario() as scenario:
model: Final = scenario.model(model="openai/responses/gpt-5", api_key=TOKEN, api_base=wire.url)
response: Final = gateway.request(
"POST",
"/v1/chat/completions",
{
"model": model,
"messages": [{"role": "user", "content": "get price of nvda"}],
"stream": True,
"temperature": 1,
"max_tokens": 8192,
"tools": [{"type": "web_search"}],
"cache": {"no-cache": True},
},
)
request: Final = _single_upstream_request(wire)
assert response.status_code == 200, response.text
assert (request.method, request.target) == ("POST", "/responses")
assert json.loads(request.body) == provider_body
assert "NVDA is listed at $123." in response.text
def test_openai_web_search_response_preserves_url_citation(gateway: Gateway) -> None:
provider_body: Final = {
"model": "gpt-5-search-api",
"messages": [{"role": "user", "content": "What was a positive news story from today?"}],
}
provider_reply: Final = {
"id": "chatcmpl-search",
"object": "chat.completion",
"created": 1,
"model": "gpt-5-search-api",
"choices": [
{
"index": 0,
"message": {
"role": "assistant",
"content": "A positive story was reported today.",
"annotations": [SEARCH_ANNOTATION],
},
"finish_reason": "stop",
}
],
"usage": {"prompt_tokens": 10, "completion_tokens": 8, "total_tokens": 18},
}
def search_peer(_: Request) -> Reply:
return Reply(body=json.dumps(provider_reply).encode())
with wire_server(search_peer) as wire:
with gateway.scenario() as scenario:
model: Final = scenario.model(model="openai/gpt-5-search-api", api_key=TOKEN, api_base=wire.url)
response: Final = gateway.request(
"POST",
"/v1/chat/completions",
{
"model": model,
"messages": [{"role": "user", "content": "What was a positive news story from today?"}],
"cache": {"no-cache": True},
},
)
request: Final = _single_upstream_request(wire)
assert response.status_code == 200, response.text
assert (request.method, request.target) == ("POST", "/chat/completions")
assert json.loads(request.body) == provider_body
annotations: Final = response.json()["choices"][0]["message"]["annotations"]
assert annotations == [SEARCH_ANNOTATION]
def test_openai_web_search_streaming_preserves_url_citation(gateway: Gateway) -> None:
provider_body: Final = {
"model": "gpt-5-search-api",
"messages": [{"role": "user", "content": "What was a positive news story from today?"}],
"stream": True,
"stream_options": {"include_usage": True},
}
stream_body: Final = b"".join(
(
b"data: "
+ json.dumps(
{
"id": "chatcmpl-search-stream",
"object": "chat.completion.chunk",
"created": 1,
"model": "gpt-5-search-api",
"choices": [
{
"index": 0,
"delta": {
"role": "assistant",
"content": "A positive story.",
"annotations": [SEARCH_ANNOTATION],
},
"finish_reason": None,
}
],
}
).encode()
+ b"\n\n",
b'data: {"id":"chatcmpl-search-stream","object":"chat.completion.chunk","created":1,'
b'"model":"gpt-5-search-api","choices":[{"index":0,"delta":{},"finish_reason":"stop"}]}\n\n',
b"data: [DONE]\n\n",
)
)
def search_peer(_: Request) -> Reply:
return Reply(content_type="text/event-stream", body=stream_body)
with wire_server(search_peer) as wire:
with gateway.scenario() as scenario:
model: Final = scenario.model(model="openai/gpt-5-search-api", api_key=TOKEN, api_base=wire.url)
response: Final = gateway.request(
"POST",
"/v1/chat/completions",
{
"model": model,
"messages": [{"role": "user", "content": "What was a positive news story from today?"}],
"stream": True,
"cache": {"no-cache": True},
},
)
request: Final = _single_upstream_request(wire)
assert response.status_code == 200, response.text
assert (request.method, request.target) == ("POST", "/chat/completions")
assert json.loads(request.body) == provider_body
chunks: Final = tuple(
json.loads(line.removeprefix("data: ")) for line in response.text.splitlines() if line.startswith("data: {")
)
annotations: Final = chunks[0]["choices"][0]["delta"]["annotations"]
assert annotations == [SEARCH_ANNOTATION]
def test_openai_codex_stream_uses_responses_endpoint(gateway: Gateway) -> None:
provider_body: Final = {
"model": "gpt-5.3-codex",
"input": [
{
"type": "message",
"role": "user",
"content": [{"type": "input_text", "text": "Hey!"}],
}
],
"stream": True,
}
with wire_server(_responses_peer("codex-stream", "gpt-5.3-codex", "Hello.")) as wire:
with gateway.scenario() as scenario:
model: Final = scenario.model(model="openai/gpt-5.3-codex", api_key=TOKEN, api_base=wire.url)
response: Final = gateway.request(
"POST",
"/v1/chat/completions",
{
"model": model,
"messages": [{"role": "user", "content": "Hey!"}],
"stream": True,
"cache": {"no-cache": True},
},
)
request: Final = _single_upstream_request(wire)
assert response.status_code == 200, response.text
assert (request.method, request.target) == ("POST", "/responses")
assert json.loads(request.body) == provider_body
assert "Hello." in response.text

View file

@ -1,59 +0,0 @@
"""
Unit tests for litellm.compress().
"""
import os
import pytest
import litellm
from litellm.types.utils import CallTypes
CALL_TYPE = CallTypes.completion
# ---------------------------------------------------------------------------
# BM25 scorer
# ---------------------------------------------------------------------------
# ---------------------------------------------------------------------------
# Content detection
# ---------------------------------------------------------------------------
# ---------------------------------------------------------------------------
# Message stubbing
# ---------------------------------------------------------------------------
# ---------------------------------------------------------------------------
# Retrieval tool
# ---------------------------------------------------------------------------
# ---------------------------------------------------------------------------
# compress() — end-to-end
# ---------------------------------------------------------------------------
# ---------------------------------------------------------------------------
# Embedding scorer — integration test (skipped without API key)
# ---------------------------------------------------------------------------
@pytest.mark.skipif(not os.environ.get("OPENAI_API_KEY"), reason="Needs OPENAI_API_KEY")
def test_embedding_scorer():
result = litellm.compress(
messages=[
{"role": "user", "content": "Authentication code " * 2000},
{"role": "user", "content": "Unrelated cooking recipes " * 2000},
{"role": "user", "content": "Fix auth"},
],
model="gpt-4o",
call_type=CALL_TYPE,
compression_trigger=1000,
embedding_model="text-embedding-3-small",
)
assert result["compression_ratio"] > 0
assert len(result["cache"]) > 0

View file

@ -1,76 +0,0 @@
import pytest
import litellm
# Test implementations
def test_completion_cost_deepseek():
litellm.set_verbose = True
model_name = "deepseek/deepseek-chat"
messages_1 = [
{
"role": "system",
"content": "You are a history expert. The user will provide a series of questions, and your answers should be concise and start with `Answer:`",
},
{
"role": "user",
"content": "In what year did Qin Shi Huang unify the six states?",
},
{"role": "assistant", "content": "Answer: 221 BC"},
{"role": "user", "content": "Who was the founder of the Han Dynasty?"},
{"role": "assistant", "content": "Answer: Liu Bang"},
{"role": "user", "content": "Who was the last emperor of the Tang Dynasty?"},
{"role": "assistant", "content": "Answer: Li Zhu"},
{
"role": "user",
"content": "Who was the founding emperor of the Ming Dynasty?",
},
{"role": "assistant", "content": "Answer: Zhu Yuanzhang"},
{
"role": "user",
"content": "Who was the founding emperor of the Qing Dynasty?",
},
]
message_2 = [
{
"role": "system",
"content": "You are a history expert. The user will provide a series of questions, and your answers should be concise and start with `Answer:`",
},
{
"role": "user",
"content": "In what year did Qin Shi Huang unify the six states?",
},
{"role": "assistant", "content": "Answer: 221 BC"},
{"role": "user", "content": "Who was the founder of the Han Dynasty?"},
{"role": "assistant", "content": "Answer: Liu Bang"},
{"role": "user", "content": "Who was the last emperor of the Tang Dynasty?"},
{"role": "assistant", "content": "Answer: Li Zhu"},
{
"role": "user",
"content": "Who was the founding emperor of the Ming Dynasty?",
},
{"role": "assistant", "content": "Answer: Zhu Yuanzhang"},
{"role": "user", "content": "When did the Shang Dynasty fall?"},
]
try:
response_1 = litellm.completion(model=model_name, messages=messages_1)
response_2 = litellm.completion(model=model_name, messages=message_2)
# Add any assertions here to check the response
print(response_2)
assert response_2.usage.prompt_cache_hit_tokens is not None
assert response_2.usage.prompt_cache_miss_tokens is not None
assert (
response_2.usage.prompt_tokens
== response_2.usage.prompt_cache_miss_tokens
+ response_2.usage.prompt_cache_hit_tokens
)
assert (
response_2.usage._cache_read_input_tokens
== response_2.usage.prompt_cache_hit_tokens
)
except litellm.APIError as e:
pass
except Exception as e:
pytest.fail(f"Error occurred: {e}")

View file

@ -1,27 +0,0 @@
import json
from datetime import datetime
from unittest.mock import AsyncMock
from base_rerank_unit_tests import BaseLLMRerankTest
import litellm
class TestJinaAI(BaseLLMRerankTest):
def get_custom_llm_provider(self) -> litellm.LlmProviders:
return litellm.LlmProviders.JINA_AI
def get_base_rerank_call_args(self) -> dict:
return {
"model": "jina_ai/jina-reranker-v2-base-multilingual",
}
def test_jina_ai_embedding():
litellm.embedding(
model="jina_ai/jina-embeddings-v3",
input=["a"],
task="separation",
dimensions=1024,
)

View file

@ -1,45 +0,0 @@
"""
Tests for Lambda AI provider integration
"""
import os
from unittest import mock
import pytest
import litellm
from litellm.llms.lambda_ai.chat.transformation import LambdaAIChatConfig
@pytest.mark.asyncio
async def test_lambda_ai_completion_call():
"""Test completion call with Lambda AI provider (requires LAMBDA_API_KEY)"""
# Skip if no API key is available
if not os.getenv("LAMBDA_API_KEY"):
pytest.skip("LAMBDA_API_KEY not set")
try:
response = await litellm.acompletion(
model="lambda_ai/llama3.1-8b-instruct",
messages=[{"role": "user", "content": "Hello, this is a test"}],
max_tokens=10,
)
assert response.choices[0].message.content
assert response.model
assert response.usage
except Exception as e:
# If the API key is invalid or there's a network issue, that's okay
# The important thing is that the provider was recognized
if "lambda_ai" not in str(e) and "provider" not in str(e).lower():
# Re-raise if it's not a provider-related error
raise

File diff suppressed because one or more lines are too long

View file

@ -79,6 +79,52 @@ def _assert_deepseek_request(request: httpx.Request, messages: list[dict[str, st
assert actual_data["stream"] is stream
@pytest.mark.usefixtures("_deepseek_httpx_transport")
@pytest.mark.respx(assert_all_called=True)
def test_completion_cost_deepseek_reports_prompt_cache_usage(respx_mock: respx.MockRouter) -> None:
messages: Final = [{"role": "user", "content": "A cached prompt"}]
route: Final = respx_mock.post(DEEPSEEK_CHAT_COMPLETIONS_URL).mock(
return_value=httpx.Response(
200,
json={
"id": "chatcmpl-deepseek-cache",
"object": "chat.completion",
"created": 1,
"model": "deepseek-chat",
"choices": [
{
"index": 0,
"message": {"role": "assistant", "content": "Answer"},
"finish_reason": "stop",
}
],
"usage": {
"prompt_tokens": 10,
"completion_tokens": 2,
"total_tokens": 12,
"prompt_cache_hit_tokens": 4,
"prompt_cache_miss_tokens": 6,
},
},
)
)
response: Final = litellm.completion(
model="deepseek/deepseek-chat",
messages=messages,
api_key=DEEPSEEK_API_KEY,
num_retries=0,
max_retries=0,
)
request_body: Final = json.loads(route.calls[0].request.content)
assert request_body == {"model": "deepseek-chat", "messages": messages}
assert response.usage.prompt_cache_hit_tokens == 4
assert response.usage.prompt_cache_miss_tokens == 6
assert response.usage.prompt_tokens == 10
assert response.usage._cache_read_input_tokens == response.usage.prompt_cache_hit_tokens
def _function_tool(name: str) -> dict:
return {
"type": "function",

View file

@ -1,10 +1,14 @@
import json
from typing import Final
from unittest.mock import MagicMock
import httpx
import pytest
import respx
from pydantic import ValidationError
import litellm
from litellm.llms.jina_ai.embedding.transformation import JinaAIEmbeddingConfig
from litellm.types.utils import EmbeddingResponse
@ -136,6 +140,44 @@ class TestJinaAIEmbeddingTransform:
assert result == expected_result
@pytest.mark.respx(assert_all_called=True)
def test_jina_ai_embedding_forwards_task_and_dimensions_and_parses_values(
respx_mock: respx.MockRouter,
) -> None:
route: Final = respx_mock.post("https://api.jina.ai/v1/embeddings").mock(
return_value=httpx.Response(
200,
json={
"model": "jina-embeddings-v3",
"object": "list",
"data": [{"object": "embedding", "index": 0, "embedding": [0.1, 0.2]}],
"usage": {"prompt_tokens": 3, "total_tokens": 3},
},
)
)
response: Final = litellm.embedding(
model="jina_ai/jina-embeddings-v3",
input=["a"],
task="separation",
dimensions=1024,
api_key="jina-test-key",
api_base="https://api.jina.ai/v1",
num_retries=0,
max_retries=0,
)
request_body: Final = json.loads(route.calls[0].request.content)
assert request_body == {
"model": "jina-embeddings-v3",
"input": ["a"],
"task": "separation",
"dimensions": 1024,
}
assert response.data[0]["embedding"] == [0.1, 0.2]
assert response.usage.prompt_tokens == 3
def _transform(raw_response: httpx.Response) -> EmbeddingResponse:
return JinaAIEmbeddingConfig().transform_embedding_response(
model="jina-embeddings-v3",

View file

@ -1096,6 +1096,30 @@ def test_openai_pdf_url_is_downloaded_into_file_data() -> None:
assert base64.b64decode(file_content["file_data"].split(",", maxsplit=1)[1]) == pdf_content
def test_vision_with_custom_model_preserves_base64_image_in_request() -> None:
image_data: Final = b"test-image"
image_uri: Final = f"data:image/png;base64,{base64.b64encode(image_data).decode('utf-8')}"
messages: Final = [
{
"role": "user",
"content": [
{"type": "text", "text": "What's in this image?"},
{"type": "image_url", "image_url": {"url": image_uri}},
],
}
]
request: Final = OpenAIGPTConfig().transform_request(
model="my-custom-model",
messages=messages,
optional_params={"max_tokens": 10},
litellm_params={"api_base": "https://my-custom.api.openai.com"},
headers={},
)
assert request == {"model": "my-custom-model", "messages": messages, "max_tokens": 10}
assert request["messages"][0]["content"][1]["image_url"]["url"] == image_uri
class TestToolMessageImageHoisting:
"""transform_request moves tool-message images into a following user message
(OpenAI-compatible APIs only accept text in role:"tool" messages)."""

View file

@ -1,13 +1,21 @@
import asyncio, importlib, os
import asyncio
import importlib
import json
import os
from collections.abc import AsyncIterator
from typing import Final
from unittest.mock import AsyncMock, Mock, patch
from unittest.mock import Mock, patch
import httpx
import pytest
import respx
from openai import AsyncOpenAI, OpenAI
from pydantic import JsonValue
import litellm
from litellm import create_thread, get_thread
from litellm.caching.caching import Cache, LiteLLMCacheType
from litellm.google_genai.adapters.transformation import GoogleGenAIStreamWrapper
from litellm.llms.openai.openai import (
AssistantEventHandler,
AsyncAssistantEventHandler,
@ -19,8 +27,7 @@ from litellm.llms.openai.openai import (
SyncCursorPage,
Thread,
)
from litellm.types.utils import ImageResponse, ModelResponse
from litellm import create_thread, get_thread
from litellm.types.utils import Delta, ImageResponse, ModelResponse, ModelResponseStream, StreamingChoices
from litellm.litellm_core_utils.logging_worker import GLOBAL_LOGGING_WORKER
from litellm.utils import _invalidate_model_cost_lowercase_map
from openai.types.beta.assistant import Assistant
@ -273,6 +280,432 @@ async def test_acompletion_streams_tool_call_arguments_over_injected_transport()
assert rebuilt.choices[0].finish_reason == "tool_calls"
def _openai_sse_chunk(chunk_id: str, model: str, choices: list[JsonValue]) -> bytes:
body: Final[dict[str, JsonValue]] = {
"id": chunk_id,
"object": "chat.completion.chunk",
"created": 0,
"model": model,
"choices": choices,
}
return f"data: {json.dumps(body)}\n\n".encode()
@pytest.mark.respx(assert_all_called=True)
def test_o1_parallel_tool_calls_are_dropped_for_unsupported_models(respx_mock: respx.MockRouter) -> None:
model: Final = "o1"
route: Final = respx_mock.post("https://api.openai.com/v1/chat/completions").mock(
return_value=httpx.Response(
200,
json={
"id": "chatcmpl-o-series",
"object": "chat.completion",
"created": 0,
"model": model,
"choices": [
{
"index": 0,
"message": {"role": "assistant", "content": "done"},
"finish_reason": "stop",
}
],
"usage": {"prompt_tokens": 1, "completion_tokens": 1, "total_tokens": 2},
},
)
)
response: Final = litellm.completion(
model=model,
messages=[{"role": "user", "content": "foo"}],
parallel_tool_calls=True,
drop_params=True,
api_key="sk-openai-test",
num_retries=0,
max_retries=0,
)
request_body: Final = json.loads(route.calls[0].request.content)
assert request_body == {"model": model, "messages": [{"role": "user", "content": "foo"}]}
assert response.choices[0].message.content == "done"
@pytest.mark.respx(assert_all_called=True)
def test_prediction_parameter_participates_in_completion_cache_key(
monkeypatch: pytest.MonkeyPatch, respx_mock: respx.MockRouter
) -> None:
monkeypatch.setattr(litellm, "cache", Cache(type=LiteLLMCacheType.LOCAL))
def respond(request: httpx.Request) -> httpx.Response:
request_body: Final = json.loads(request.content)
prediction_content: Final = request_body["prediction"]["content"]
response_id: Final = (
"chatcmpl-prediction-one" if prediction_content == "prediction one" else "chatcmpl-prediction-two"
)
return httpx.Response(
200,
json={
"id": response_id,
"object": "chat.completion",
"created": 0,
"model": "gpt-4o",
"choices": [
{
"index": 0,
"message": {"role": "assistant", "content": prediction_content},
"finish_reason": "stop",
}
],
"usage": {"prompt_tokens": 1, "completion_tokens": 1, "total_tokens": 2},
},
)
route: Final = respx_mock.post("https://api.openai.com/v1/chat/completions").mock(side_effect=respond)
messages: Final = [{"role": "user", "content": "replace the username"}]
first_prediction: Final = {"type": "content", "content": "prediction one"}
second_prediction: Final = {"type": "content", "content": "prediction two"}
first: Final = litellm.completion(
model="gpt-4o",
messages=messages,
prediction=first_prediction,
caching=True,
api_key="sk-openai-test",
num_retries=0,
max_retries=0,
)
cached: Final = litellm.completion(
model="gpt-4o",
messages=messages,
prediction=first_prediction,
caching=True,
api_key="sk-openai-test",
num_retries=0,
max_retries=0,
)
distinct: Final = litellm.completion(
model="gpt-4o",
messages=messages,
prediction=second_prediction,
caching=True,
api_key="sk-openai-test",
num_retries=0,
max_retries=0,
)
request_bodies: Final = tuple(json.loads(call.request.content) for call in route.calls)
assert first.id == cached.id == "chatcmpl-prediction-one"
assert distinct.id == "chatcmpl-prediction-two"
assert route.call_count == 2
assert request_bodies == (
{"model": "gpt-4o", "messages": messages, "prediction": first_prediction},
{"model": "gpt-4o", "messages": messages, "prediction": second_prediction},
)
@pytest.mark.respx(assert_all_called=True)
def test_openai_tool_calling_sends_schema_and_parses_function_call(respx_mock: respx.MockRouter) -> None:
tools: Final = [
{
"type": "function",
"function": {
"name": "get_stock_price",
"description": "Get the current stock price for a given ticker symbol.",
"parameters": {
"type": "object",
"properties": {"ticker": {"type": "string", "description": "The stock ticker symbol."}},
"required": ["ticker"],
},
},
}
]
route: Final = respx_mock.post("https://api.openai.com/v1/chat/completions").mock(
return_value=httpx.Response(
200,
json={
"id": "chatcmpl-tool-call",
"object": "chat.completion",
"created": 0,
"model": "gpt-4.1",
"choices": [
{
"index": 0,
"message": {
"role": "assistant",
"content": None,
"tool_calls": [
{
"id": "call-tsla",
"type": "function",
"function": {"name": "get_stock_price", "arguments": '{"ticker":"TSLA"}'},
}
],
},
"finish_reason": "tool_calls",
}
],
"usage": {"prompt_tokens": 8, "completion_tokens": 3, "total_tokens": 11},
},
)
)
response: Final = litellm.completion(
model="openai/gpt-4.1",
messages=[{"role": "user", "content": [{"type": "text", "text": "What is TSLA stock price at today?"}]}],
temperature=0.5,
max_tokens=1600,
tools=tools,
api_key="sk-openai-test",
num_retries=0,
max_retries=0,
)
request_body: Final = json.loads(route.calls[0].request.content)
tool_call: Final = response.choices[0].message.tool_calls[0]
assert request_body == {
"model": "gpt-4.1",
"messages": [{"role": "user", "content": [{"type": "text", "text": "What is TSLA stock price at today?"}]}],
"temperature": 0.5,
"max_tokens": 1600,
"tools": tools,
}
assert (tool_call.id, tool_call.function.name, tool_call.function.arguments) == (
"call-tsla",
"get_stock_price",
'{"ticker":"TSLA"}',
)
assert response.choices[0].finish_reason == "tool_calls"
@pytest.mark.respx(assert_all_called=True)
@pytest.mark.asyncio
async def test_openai_via_gemini_streaming_bridge_translates_openai_chunks() -> None:
chunk: Final = ModelResponseStream(
id="chatcmpl-bridge",
object="chat.completion.chunk",
created=0,
model="gpt-3.5-turbo",
choices=[
StreamingChoices(
index=0,
delta=Delta(role="assistant", content="A starship crossed the sky."),
finish_reason=None,
)
],
)
async def stream() -> AsyncIterator[ModelResponseStream]:
yield chunk
wrapper: Final = GoogleGenAIStreamWrapper(stream())
chunks: Final = tuple([item async for item in wrapper])
assert chunks == (
{
"candidates": [
{
"content": {"parts": [{"text": "A starship crossed the sky."}], "role": "model"},
"finishReason": None,
"index": 0,
"safetyRatings": [],
}
]
},
)
@pytest.mark.respx(assert_all_called=True)
def test_streaming_content_with_n_greater_than_one_preserves_choice_indices(
respx_mock: respx.MockRouter,
) -> None:
stream_body: Final = b"".join(
(
_openai_sse_chunk(
"chatcmpl-n-content",
"gpt-4o",
[{"index": 0, "delta": {"role": "assistant", "content": "Hello"}, "finish_reason": None}],
),
_openai_sse_chunk(
"chatcmpl-n-content",
"gpt-4o",
[{"index": 1, "delta": {"role": "assistant", "content": "Hi"}, "finish_reason": None}],
),
_openai_sse_chunk("chatcmpl-n-content", "gpt-4o", [{"index": 0, "delta": {}, "finish_reason": "stop"}]),
_openai_sse_chunk("chatcmpl-n-content", "gpt-4o", [{"index": 1, "delta": {}, "finish_reason": "stop"}]),
b"data: [DONE]\n\n",
)
)
route: Final = respx_mock.post("https://api.openai.com/v1/chat/completions").mock(
return_value=httpx.Response(200, headers={"content-type": "text/event-stream"}, content=stream_body)
)
response: Final = litellm.completion(
model="openai/gpt-4o",
messages=[{"role": "user", "content": "Say hello in one word"}],
stream=True,
n=2,
max_tokens=10,
api_key="sk-openai-test",
num_retries=0,
max_retries=0,
)
chunks: Final = tuple(response)
request_body: Final = json.loads(route.calls[0].request.content)
indices: Final = tuple(chunk.choices[0].index for chunk in chunks if chunk.choices)
content_by_index: Final = {
index: "".join(
chunk.choices[0].delta.content or ""
for chunk in chunks
if chunk.choices and chunk.choices[0].index == index
)
for index in (0, 1)
}
assert route.call_count == 1
assert request_body == {
"model": "gpt-4o",
"messages": [{"role": "user", "content": "Say hello in one word"}],
"stream": True,
"n": 2,
"max_tokens": 10,
"stream_options": {"include_usage": True},
}
assert frozenset(indices) == frozenset({0, 1})
assert content_by_index == {0: "Hello", 1: "Hi"}
@pytest.mark.respx(assert_all_called=True)
def test_streaming_tool_calls_with_n_greater_than_one_preserves_choice_indices(
respx_mock: respx.MockRouter,
) -> None:
tools: Final = [
{
"type": "function",
"function": {
"name": "get_current_weather",
"description": "Get the current weather in a given location",
"parameters": {
"type": "object",
"properties": {
"location": {"type": "string"},
"unit": {"type": "string", "enum": ["celsius", "fahrenheit"]},
},
"required": ["location", "unit"],
"additionalProperties": False,
},
},
}
]
stream_body: Final = b"".join(
(
_openai_sse_chunk(
"chatcmpl-n-tools",
"gpt-4o",
[
{
"index": 0,
"delta": {
"role": "assistant",
"tool_calls": [
{
"index": 0,
"id": "call-0",
"type": "function",
"function": {"name": "get_current_weather", "arguments": "{}"},
}
],
},
"finish_reason": None,
}
],
),
_openai_sse_chunk(
"chatcmpl-n-tools",
"gpt-4o",
[{"index": 0, "delta": {}, "finish_reason": "tool_calls"}],
),
_openai_sse_chunk(
"chatcmpl-n-tools",
"gpt-4o",
[
{
"index": 1,
"delta": {
"role": "assistant",
"tool_calls": [
{
"index": 0,
"id": "call-1",
"type": "function",
"function": {"name": "get_current_weather", "arguments": "{}"},
}
],
},
"finish_reason": None,
}
],
),
_openai_sse_chunk(
"chatcmpl-n-tools",
"gpt-4o",
[{"index": 1, "delta": {}, "finish_reason": "tool_calls"}],
),
_openai_sse_chunk(
"chatcmpl-n-tools",
"gpt-4o",
[
{
"index": 2,
"delta": {
"role": "assistant",
"tool_calls": [
{
"index": 0,
"id": "call-2",
"type": "function",
"function": {"name": "get_current_weather", "arguments": "{}"},
}
],
},
"finish_reason": None,
}
],
),
_openai_sse_chunk(
"chatcmpl-n-tools",
"gpt-4o",
[{"index": 2, "delta": {}, "finish_reason": "tool_calls"}],
),
b"data: [DONE]\n\n",
)
)
route: Final = respx_mock.post("https://api.openai.com/v1/chat/completions").mock(
return_value=httpx.Response(200, headers={"content-type": "text/event-stream"}, content=stream_body)
)
response: Final = litellm.completion(
model="openai/gpt-4o",
messages=[{"role": "user", "content": "What is the weather in San Francisco?"}],
tools=tools,
stream=True,
n=3,
api_key="sk-openai-test",
num_retries=0,
max_retries=0,
)
chunks: Final = tuple(response)
request_body: Final = json.loads(route.calls[0].request.content)
indices: Final = tuple(chunk.choices[0].index for chunk in chunks if chunk.choices)
assert route.call_count == 1
assert request_body == {
"model": "gpt-4o",
"messages": [{"role": "user", "content": "What is the weather in San Francisco?"}],
"tools": tools,
"stream": True,
"n": 3,
"stream_options": {"include_usage": True},
}
assert frozenset(indices) == frozenset({0, 1, 2})
_PROVIDER_HEADERS: Final = {"x-request-id": "req_openai", "x-ratelimit-remaining-requests": "41"}

View file

@ -3,8 +3,12 @@ Unit tests for litellm.compress().
"""
import importlib
import json
from typing import Final
import httpx
import pytest
import respx
import litellm
from litellm.compression.scoring.bm25 import bm25_score_messages
@ -437,6 +441,56 @@ def test_embedding_scorer_forwards_embedding_model_params(monkeypatch):
assert captured["api_base"] == "https://example-embeddings.test"
@pytest.mark.respx(assert_all_called=True)
def test_embedding_scorer_compresses_context_using_requested_embedding_model(
respx_mock: respx.MockRouter,
) -> None:
auth_context: Final = "Authentication code " * 2000
recipe_context: Final = "Unrelated cooking recipes " * 2000
messages: Final = [
{"role": "user", "content": auth_context},
{"role": "user", "content": recipe_context},
{"role": "user", "content": "Fix auth"},
]
route: Final = respx_mock.post("https://api.openai.com/v1/embeddings").mock(
return_value=httpx.Response(
200,
json={
"object": "list",
"model": "text-embedding-3-small",
"data": [
{"object": "embedding", "index": 0, "embedding": [1.0, 0.0]},
{"object": "embedding", "index": 1, "embedding": [0.9, 0.1]},
{"object": "embedding", "index": 2, "embedding": [0.0, 1.0]},
{"object": "embedding", "index": 3, "embedding": [1.0, 0.0]},
],
"usage": {"prompt_tokens": 4, "total_tokens": 4},
},
)
)
result: Final = litellm.compress(
messages=messages,
model="gpt-4o",
call_type=CALL_TYPE,
compression_trigger=1000,
embedding_model="text-embedding-3-small",
embedding_model_params={"api_key": "sk-test"},
)
request_body: Final = json.loads(route.calls[0].request.content)
assert request_body == {
"model": "text-embedding-3-small",
"input": [
"Fix auth",
auth_context[:15000] + "\n...\n" + auth_context[-15000:],
recipe_context[:15000] + "\n...\n" + recipe_context[-15000:],
"Fix auth",
],
}
assert result["compression_ratio"] > 0
assert len(result["cache"]) > 0
# ---------------------------------------------------------------------------
# Embedding scorer — integration test (skipped without API key)
# ---------------------------------------------------------------------------