mirror of
https://github.com/BerriAI/litellm.git
synced 2026-11-10 08:28:25 +00:00
* test(integration): add rich anthropic claude haiku 5.5, opus 5.5 and fable 5.1 translation cases Capture-backed basic cases on /v1/chat/completions, /v1/messages and /v1/responses for claude-haiku-5-5, claude-opus-5-5 and claude-fable-5-1, reusing the shared client templates and ANTHROPIC_MESSAGES_REQUEST/RESPONSE with model-specific id, content and usage overrides. The minimal Opus 5.5 hello cases are replaced by the rich conversation. Sonnet 5.5 and legacy models are unchanged * test(translation): rich OpenAI captures for gpt-6.1-sol, gpt-6-luna, gpt-6-astra and gpt-5.6-terra Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * test(translation): rich Azure gpt-6.1-sol, gpt-6-luna, gpt-6-astra and gpt-5.6-terra cases * test(translation): rich azure_ai Claude cases for haiku-5-5 and fable-5-1 Bind the shared chat_completions, messages and responses client templates and the anthropic provider templates to azure_ai/claude-haiku-5-5 and azure_ai/claude-fable-5-1 from real Azure AI Foundry captures, keeping the Azure dialect exact (/anthropic/v1/messages, x-api-key plus api-key on chat_completions and responses, system as a text block list on /v1/messages). Add prompt_cache_hit variants for both models from the second call inside the cache window. The two deployments need rows in tests/integration/proxy_config.yaml Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * test(translation): rich Gemini bases for gemini-3.8-flash, gemini-3.1-pro-preview and gemini-4-argon Add the shared generateContent provider templates in templates/gemini.py from real captures and bind the three target models on chat completions, messages and responses with explicit per-model response overrides. gemini-4-argon is registered in the three basic lists. gemini-3.5-flash is unchanged * test(translation): enrich vertex_ai claude and gemini cases from live captures Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * test(integration): rich Bedrock converse, invoke and mantle translation cases from real captures * test(integration): register the rich translation deployments and follow the rich Gemini 3.8 Flash cases Add the synthetic deployments the rich provider cases bind (anthropic, openai, azure, azure_ai, gemini, vertex_ai, bedrock converse, invoke and mantle) to tests/integration/proxy_config.yaml, all pointed at the fake provider on 127.0.0.1:8191 with synthetic keys. The Gemini omitted-temperature contract now takes its expected generationConfig and reply text from the rich GEMINI_3_8_FLASH_TEST_CASE literals instead of the retired minimal hello cases * test(translation): reuse bridged and native response templates --------- Co-authored-by: devin-ai-integration[bot] <158243242+devin-ai-integration[bot]@users.noreply.github.com> Co-authored-by: kerry <kerry@berri.ai>
92 lines
4.3 KiB
Python
92 lines
4.3 KiB
Python
"""A Gemini 3+ request that omits temperature reaches the provider without one: streamed on each endpoint, and in
|
|
a burst of mixed-endpoint requests spread across the proxy's workers."""
|
|
|
|
import json
|
|
from collections.abc import Mapping
|
|
from concurrent.futures import ThreadPoolExecutor
|
|
from typing import Final
|
|
|
|
import pytest
|
|
from integration._support.client import JSON_OBJECT, Gateway, object_value
|
|
from integration._support.provider import SharedProvider
|
|
from integration._support.wire import Reply, Request
|
|
from integration.translation.case import TranslationTestCase
|
|
from integration.translation.chat_completions.bases.gemini import (
|
|
GEMINI_3_8_FLASH_TEST_CASE as CHAT_COMPLETIONS_TEST_CASE,
|
|
)
|
|
from integration.translation.messages.bases.gemini import GEMINI_3_8_FLASH_TEST_CASE as MESSAGES_TEST_CASE
|
|
from integration.translation.responses.bases.gemini import GEMINI_3_8_FLASH_TEST_CASE as RESPONSES_TEST_CASE
|
|
from pydantic import JsonValue
|
|
|
|
GENERATE_TARGET: Final = "/models/gemini-3.8-flash:generateContent"
|
|
STREAM_TARGET: Final = "/models/gemini-3.8-flash:streamGenerateContent?alt=sse"
|
|
ENDPOINT_CASES: Final = (CHAT_COMPLETIONS_TEST_CASE, MESSAGES_TEST_CASE, RESPONSES_TEST_CASE)
|
|
BURST: Final = ENDPOINT_CASES * 4
|
|
BURST_ANSWER: Final = "I need to inspect `shop/validation.py` next."
|
|
STREAM_USAGE: Final = {
|
|
"promptTokenCount": 10,
|
|
"candidatesTokenCount": 2,
|
|
"totalTokenCount": 12,
|
|
"promptTokensDetails": [{"modality": "TEXT", "tokenCount": 10}],
|
|
}
|
|
|
|
|
|
def _frame(payload: Mapping[str, JsonValue]) -> bytes:
|
|
return f"data: {json.dumps(payload)}\n\n".encode()
|
|
|
|
|
|
def _streamed_reply() -> Reply:
|
|
opening: Final = {
|
|
"candidates": [{"content": {"role": "model", "parts": [{"text": "Hello."}]}, "index": 0}],
|
|
"modelVersion": "gemini-3.8-flash",
|
|
"responseId": "stream-gemini-3-8-flash",
|
|
}
|
|
closing: Final = {
|
|
"candidates": [{"content": {"role": "model", "parts": [{"text": ""}]}, "finishReason": "STOP", "index": 0}],
|
|
"usageMetadata": STREAM_USAGE,
|
|
"modelVersion": "gemini-3.8-flash",
|
|
"responseId": "stream-gemini-3-8-flash",
|
|
}
|
|
return Reply(content_type="text/event-stream", chunks=(_frame(opening), _frame(closing)))
|
|
|
|
|
|
def _generation_config(request: Request) -> dict[str, JsonValue]:
|
|
return object_value(JSON_OBJECT.validate_json(request.body)["generationConfig"])
|
|
|
|
|
|
def _expected_generation_config(case: TranslationTestCase) -> dict[str, JsonValue]:
|
|
expected: Final = object_value(case.expected_provider_request["generationConfig"])
|
|
assert "temperature" not in expected, expected
|
|
return expected
|
|
|
|
|
|
@pytest.mark.parametrize("case", ENDPOINT_CASES, ids=lambda case: case.litellm_endpoint)
|
|
def test_streamed_request_without_temperature_forwards_none(
|
|
case: TranslationTestCase, gateway: Gateway, provider: SharedProvider
|
|
) -> None:
|
|
provider.expect(_streamed_reply())
|
|
response: Final = gateway.request("POST", case.litellm_endpoint, {**case.litellm_request, "stream": True})
|
|
assert response.status_code == 200, response.text
|
|
assert "Hello." in response.text, response.text
|
|
received: Final = provider.received()
|
|
assert [(request.method, request.target) for request in received] == [("POST", STREAM_TARGET)]
|
|
assert _generation_config(received[0]) == _expected_generation_config(case)
|
|
|
|
|
|
def test_concurrent_mixed_endpoint_burst_without_temperature_forwards_none(
|
|
gateway: Gateway, provider: SharedProvider
|
|
) -> None:
|
|
reply_body: Final = json.dumps(CHAT_COMPLETIONS_TEST_CASE.mock_provider_response).encode()
|
|
provider.expect(*(Reply(body=reply_body) for _ in BURST))
|
|
with ThreadPoolExecutor(max_workers=len(BURST)) as pool:
|
|
responses: Final = tuple(
|
|
pool.map(lambda case: gateway.request("POST", case.litellm_endpoint, case.litellm_request), BURST)
|
|
)
|
|
assert [response.status_code for response in responses] == [200] * len(BURST), [r.text for r in responses]
|
|
assert all(BURST_ANSWER in response.text for response in responses), [r.text for r in responses]
|
|
received: Final = provider.received()
|
|
assert [(request.method, request.target) for request in received] == [("POST", GENERATE_TARGET)] * len(BURST)
|
|
expected_configs: Final = [_expected_generation_config(CHAT_COMPLETIONS_TEST_CASE)] * len(BURST)
|
|
assert sorted(map(json.dumps, (_generation_config(r) for r in received))) == sorted(
|
|
map(json.dumps, expected_configs)
|
|
)
|