litellm/tests/integration/compatibility/test_openai_sdk_client_paths.py
devin-ai-integration[bot] 63d823d28a
fix: support OpenAI SDK 3 while retaining SDK 2 compatibility (#44927)
* fix: support OpenAI SDK 3.x while retaining 2.x compatibility

* fix: preserve HTTPX compatibility across OpenAI SDK versions

* refactor: trim OpenAI SDK compatibility patch

* refactor: simplify SDK client defaults

* test: cover SDK compatibility across provider routes

* fix: use shared names for SDK transport helpers

* fix: address SDK compatibility review feedback

* test(openai): send requests through the SDK API factory clients

* fix(exceptions): keep the provider response on PaymentRequiredError under httpx 2

* ci(base-sdk): count httpx2 as base-only when a base dependency brings it in

OpenAI SDK 3 depends on httpx2, so the litellm-core install at highest resolution pulls it in through openai. The base-only check now resolves the declared base dependency closure of litellm and litellm-core and accepts extras-only modules owned by a distribution inside that closure

* test: cover OpenAI SDK 3 client paths end to end and read SDK HTTP headers case-insensitively

* test: read C02 spend rows once, wait out the worker healthcheck on respawn, and resolve module owners on Python 3.10

---------

Co-authored-by: Marcus Wood <marcuswood@openai.com>
Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com>
2026-10-09 19:45:50 -07:00

1200 lines
53 KiB
Python

from __future__ import annotations
import asyncio
import base64
import binascii
import json
import os
import threading
import uuid
from collections.abc import Awaitable, Callable, Iterator, Mapping
from contextlib import contextmanager
from dataclasses import dataclass
from pathlib import Path
from queue import SimpleQueue
from types import MappingProxyType
from typing import Final, TypeVar
import anthropic
import httpx
import openai
import pytest
import yaml
from anthropic.types import RawContentBlockDeltaEvent, RawMessageStartEvent, RawMessageStreamEvent, TextDelta
from integration._support.client import (
JSON_OBJECT,
Gateway,
Scenario,
eventually,
gateway_from_environment,
object_value,
string_value,
)
from integration._support.database import read_rows
from integration._support.openai_wire import chat_reply, openai_error, responses_reply
from integration._support.process import graceful_stop_seconds, owned_proxy
from integration._support.responses_vendor import response_identities
from integration._support.wire import Reply, Request, Wire, wire_server
from openai.types import Completion, ModerationCreateResponse
from openai.types.chat import ChatCompletionChunk
from openai.types.responses import ResponseStreamEvent
from pydantic import JsonValue
from litellm.constants import PROXY_CONFIG_RELOAD_INTERVAL_SECONDS
PROXY_WORKERS: Final = int(os.environ.get("INTEGRATION_PROXY_WORKERS", "1"))
WORKER_SYNC_SECONDS: Final = 0.0 if PROXY_WORKERS == 1 else PROXY_CONFIG_RELOAD_INTERVAL_SECONDS + 5.0
ROOT: Final = Path(os.environ.get("INTEGRATION_PROXY_ROOT") or Path(__file__).resolve().parents[3])
TEXT: Final = "openai sdk client path"
PROVIDER_KEY: Final = "integration-provider-key"
PROVIDER_AUTHORIZATION: Final = f"Bearer {PROVIDER_KEY}"
UPSTREAM_MODEL: Final = "gpt-4o-mini"
DEPLOYMENT_MODEL: Final = f"openai/{UPSTREAM_MODEL}"
INSTRUCT_MODEL: Final = "gpt-3.5-turbo-instruct"
INSTRUCT_DEPLOYMENT_MODEL: Final = f"openai/{INSTRUCT_MODEL}"
NO_CACHE: Final[Mapping[str, JsonValue]] = MappingProxyType({"cache": {"no-cache": True}})
USAGE: Final[Mapping[str, JsonValue]] = MappingProxyType(
{"prompt_tokens": 5, "completion_tokens": 3, "total_tokens": 8}
)
FIVE_KB: Final = "x" * 5120
REJECTED_CHAT_TIMEOUTS: Final[Mapping[str, JsonValue]] = MappingProxyType(
{"list": [1, 2], "dict": {"read": 5}, "5 KB string": FIVE_KB}
)
ACCEPTED_CHAT_TIMEOUTS: Final[Mapping[str, JsonValue]] = MappingProxyType({"empty string": "", "int": 30})
NO_EXTRA: Final[Mapping[str, JsonValue]] = MappingProxyType({})
REJECTED_DEPLOYMENT_TIMEOUTS: Final[Mapping[str, JsonValue]] = MappingProxyType({"list": [1, 2], "dict": {"read": 5}})
ACCEPTED_DEPLOYMENT_TIMEOUTS: Final[Mapping[str, JsonValue]] = MappingProxyType(
{"empty string": "", "5 KB string": FIVE_KB}
)
MODERATION_CATEGORIES: Final = ("hate", "harassment", "self-harm", "sexual", "violence")
LISTED_JOB: Final = "ftjob-listed"
ASSISTANT: Final = "asst_integration"
CLOUDFLARE_GATEWAY: Final = "/gateway.ai.cloudflare.com/v1/account/gateway/azure-openai/resource"
AZURE_API_VERSION: Final = "2024-10-21"
STREAM_HEAD: Final = b"h" * 65536
STREAM_TAIL: Final = b'{"custom_id": "tail", "response": {"status_code": 200}}\n'
SPEECH_AUDIO: Final = b"OggS" + bytes(range(60))
SPEECH_UPSTREAM_MODEL: Final = "gpt-4o-mini-tts"
PAYMENT_REQUIRED_DEPLOYMENTS: Final[Mapping[str, str]] = MappingProxyType(
{"openai": "openai/gpt-4o-mini", "anthropic": "anthropic/claude-sonnet-4-5"}
)
_R: Final = TypeVar("_R")
def _identity(prefix: str) -> str:
return f"{prefix}-{uuid.uuid4().hex}"
def _path(request: Request) -> str:
return request.target.split("?", 1)[0]
def _body(request: Request) -> Mapping[str, JsonValue]:
return JSON_OBJECT.validate_json(request.body)
def _streams(request: Request) -> bool:
return _body(request).get("stream") is True
def _upstream_model(request: Request) -> str:
return string_value(_body(request).get("model") or UPSTREAM_MODEL)
def _json(
payload: Mapping[str, JsonValue], *, status: int = 200, headers: Mapping[str, str] = MappingProxyType({})
) -> Reply:
return Reply(status=status, body=json.dumps(payload).encode(), headers=headers)
def _peer(route: Callable[[Request], Reply | None]) -> Callable[[Request], Reply]:
def respond(request: Request) -> Reply:
if request.method == "GET" and _path(request).endswith("/models"):
return _json({"object": "list", "data": []})
reply: Final = route(request)
if reply is None:
return _json({"error": {"message": f"unrouted {request.method} {request.target}"}}, status=404)
return reply
return respond
def _is_model_discovery(request: Request) -> bool:
return request.method == "GET" and _path(request).endswith("/models")
def _served(wire: Wire) -> tuple[Request, ...]:
return tuple(request for request in wire.drain() if not _is_model_discovery(request))
def _routes(served: tuple[Request, ...]) -> tuple[tuple[str, str], ...]:
return tuple((request.method, _path(request)) for request in served)
def _is_chat(request: Request) -> bool:
return request.method == "POST" and _path(request).endswith("/chat/completions")
def _chat_peer(identity: str) -> Callable[[Request], Reply]:
def route(request: Request) -> Reply | None:
if not _is_chat(request):
return None
return chat_reply(identity, _upstream_model(request), TEXT, stream=_streams(request))
return _peer(route)
def _fresh_chat_peer(served: SimpleQueue[str]) -> Callable[[Request], Reply]:
def route(request: Request) -> Reply | None:
if not _is_chat(request):
return None
identity: Final = _identity("chatcmpl")
served.put(identity)
return chat_reply(identity, _upstream_model(request), TEXT, stream=False)
return _peer(route)
def _held_chat_peer(gate: threading.Event, identity: str) -> Callable[[Request], Reply]:
def route(request: Request) -> Reply | None:
if not _is_chat(request):
return None
assert gate.wait(timeout=10), "the timeout cell never released its peer"
return chat_reply(identity, _upstream_model(request), TEXT, stream=False)
return _peer(route)
def _status_peer(reply: Reply) -> Callable[[Request], Reply]:
def route(request: Request) -> Reply | None:
return reply if request.method == "POST" else None
return _peer(route)
def _completion_frame(identity: str, model: str, text: str, finish_reason: str | None) -> bytes:
frame: Final = {
"id": identity,
"object": "text_completion",
"created": 1,
"model": model,
"choices": [{"text": text, "index": 0, "finish_reason": finish_reason, "logprobs": None}],
}
return b"data: " + json.dumps(frame).encode() + b"\n\n"
def _completion_reply(identity: str, model: str, *, stream: bool) -> Reply:
if stream:
return Reply(
content_type="text/event-stream",
chunks=(
_completion_frame(identity, model, TEXT, None),
_completion_frame(identity, model, "", "stop") + b"data: [DONE]\n\n",
),
)
return _json(
{
"id": identity,
"object": "text_completion",
"created": 1,
"model": model,
"choices": [{"text": TEXT, "index": 0, "finish_reason": "stop", "logprobs": None}],
"usage": dict(USAGE),
}
)
def _completion_peer(identity: str) -> Callable[[Request], Reply]:
def route(request: Request) -> Reply | None:
if request.method != "POST" or _path(request) != "/v1/completions":
return None
return _completion_reply(identity, _upstream_model(request), stream=_streams(request))
return _peer(route)
def _responses_peer(identity: str) -> Callable[[Request], Reply]:
def route(request: Request) -> Reply | None:
if request.method != "POST" or _path(request) != "/v1/responses":
return None
return responses_reply(identity, _upstream_model(request), TEXT, stream=_streams(request))
return _peer(route)
def _moderation(identity: str, model: str) -> Mapping[str, JsonValue]:
return {
"id": identity,
"model": model,
"results": [
{
"flagged": False,
"categories": {category: False for category in MODERATION_CATEGORIES},
"category_scores": {category: 0.0 for category in MODERATION_CATEGORIES},
"category_applied_input_types": {category: ["text"] for category in MODERATION_CATEGORIES},
}
],
}
def _moderation_peer(identity: str) -> Callable[[Request], Reply]:
def route(request: Request) -> Reply | None:
if request.method != "POST" or _path(request) != "/v1/moderations":
return None
return _json(_moderation(identity, _upstream_model(request)))
return _peer(route)
def _batch_line(custom_id: str) -> bytes:
line: Final = {
"custom_id": custom_id,
"method": "POST",
"url": "/v1/chat/completions",
"body": {"model": UPSTREAM_MODEL, "messages": [_user_message()]},
}
return json.dumps(line).encode() + b"\n"
def _file_object(file_id: str, size: int) -> Mapping[str, JsonValue]:
return {
"id": file_id,
"object": "file",
"purpose": "batch",
"bytes": size,
"created_at": 1,
"filename": "batch.jsonl",
"status": "processed",
}
def _files_peer(file_id: str, content: bytes, chunks: tuple[bytes, ...] | None = None) -> Callable[[Request], Reply]:
def route(request: Request) -> Reply | None:
path: Final = _path(request)
if request.method == "POST" and path == "/v1/files":
return _json(_file_object(file_id, len(content)))
if request.method == "GET" and path == f"/v1/files/{file_id}":
return _json(_file_object(file_id, len(content)))
if request.method == "GET" and path == f"/v1/files/{file_id}/content":
return Reply(content_type="application/octet-stream", body=content, chunks=chunks)
return None
return _peer(route)
def _batch_object(batch_id: str, input_file_id: str, status: str) -> Mapping[str, JsonValue]:
return {
"id": batch_id,
"object": "batch",
"endpoint": "/v1/chat/completions",
"errors": None,
"input_file_id": input_file_id,
"completion_window": "24h",
"status": status,
"output_file_id": None,
"error_file_id": None,
"created_at": 1,
"in_progress_at": None,
"expires_at": None,
"finalizing_at": None,
"completed_at": None,
"failed_at": None,
"expired_at": None,
"cancelling_at": None,
"cancelled_at": None,
"request_counts": {"total": 1, "completed": 0, "failed": 0},
"metadata": None,
}
def _batches_peer(file_id: str, batch_id: str) -> Callable[[Request], Reply]:
def route(request: Request) -> Reply | None:
path: Final = _path(request)
if request.method == "POST" and path == "/v1/files":
return _json(_file_object(file_id, 1))
if request.method == "POST" and path == "/v1/batches":
return _json(_batch_object(batch_id, string_value(_body(request)["input_file_id"]), "validating"))
if request.method == "GET" and path == f"/v1/batches/{batch_id}":
return _json(_batch_object(batch_id, file_id, "in_progress"))
if request.method == "GET" and path == "/v1/batches":
return _json(
{
"object": "list",
"data": [_batch_object(batch_id, file_id, "in_progress")],
"first_id": batch_id,
"last_id": batch_id,
"has_more": False,
}
)
if request.method == "POST" and path == f"/v1/batches/{batch_id}/cancel":
return _json(_batch_object(batch_id, file_id, "cancelling"))
return None
return _peer(route)
def _fine_tuning_job(job_id: str, status: str) -> Mapping[str, JsonValue]:
return {
"id": job_id,
"object": "fine_tuning.job",
"model": UPSTREAM_MODEL,
"created_at": 1,
"fine_tuned_model": None,
"finished_at": None,
"hyperparameters": {"n_epochs": 1, "batch_size": 1, "learning_rate_multiplier": 1.0},
"organization_id": "org-integration",
"result_files": [],
"status": status,
"trained_tokens": None,
"training_file": f"file-{job_id.removeprefix('ftjob-')}",
"validation_file": None,
"seed": 1,
"error": None,
}
def _assistant(identity: str) -> Mapping[str, JsonValue]:
return {
"id": identity,
"object": "assistant",
"created_at": 1,
"name": "integration",
"description": None,
"model": UPSTREAM_MODEL,
"instructions": None,
"tools": [],
"metadata": {},
"temperature": 1.0,
"top_p": 1.0,
"response_format": "auto",
"tool_resources": None,
}
def _configured_peer() -> Callable[[Request], Reply]:
def route(request: Request) -> Reply | None:
path: Final = _path(request)
if request.method == "POST" and path == "/v1/fine_tuning/jobs":
training_file: Final = string_value(_body(request)["training_file"])
return _json(_fine_tuning_job(f"ftjob-{training_file.removeprefix('file-')}", "queued"))
if request.method == "GET" and path == "/v1/fine_tuning/jobs":
return _json({"object": "list", "data": [_fine_tuning_job(LISTED_JOB, "running")], "has_more": False})
if request.method == "POST" and path.startswith("/v1/fine_tuning/jobs/") and path.endswith("/cancel"):
return _json(
_fine_tuning_job(path.removeprefix("/v1/fine_tuning/jobs/").removesuffix("/cancel"), "cancelled")
)
if request.method == "GET" and path.startswith("/v1/fine_tuning/jobs/"):
return _json(_fine_tuning_job(path.removeprefix("/v1/fine_tuning/jobs/"), "running"))
if request.method == "GET" and path == "/v1/assistants":
return _json(
{
"object": "list",
"data": [_assistant(ASSISTANT)],
"first_id": ASSISTANT,
"last_id": ASSISTANT,
"has_more": False,
}
)
return None
return _peer(route)
def _config_with_provider_settings(directory: Path, api_base: str) -> Path:
shared: Final = object_value(yaml.safe_load((ROOT / "tests/integration/proxy_config.yaml").read_text()))
path: Final = directory / "proxy_config_with_provider_settings.yaml"
path.write_text(
yaml.safe_dump(
{
**shared,
"finetune_settings": [{"custom_llm_provider": "openai", "api_base": api_base, "api_key": PROVIDER_KEY}],
"assistant_settings": {
"custom_llm_provider": "openai",
"litellm_params": {"api_base": api_base, "api_key": PROVIDER_KEY},
},
}
)
)
return path
@dataclass(frozen=True, slots=True)
class ConfiguredProxy:
gateway: Gateway
wire: Wire
@pytest.fixture(scope="module")
def configured_proxy(tmp_path_factory: pytest.TempPathFactory) -> Iterator[ConfiguredProxy]:
with gateway_from_environment() as rig_gateway, wire_server(_configured_peer()) as wire:
directory: Final = tmp_path_factory.mktemp("openai_sdk_client_paths")
config: Final = _config_with_provider_settings(directory, f"{wire.url}/v1")
with owned_proxy(rig_gateway, directory, {}, config=config, workers=PROXY_WORKERS) as owned:
yield ConfiguredProxy(owned, wire)
def _v1(gateway: Gateway) -> str:
return f"{str(gateway.client.base_url).rstrip('/')}/v1"
def _origin(gateway: Gateway) -> str:
return str(gateway.client.base_url).rstrip("/")
@contextmanager
def _openai(gateway: Gateway) -> Iterator[openai.OpenAI]:
with (
httpx.Client(timeout=15, trust_env=False) as transport,
openai.OpenAI(api_key=gateway.key, base_url=_v1(gateway), max_retries=0, http_client=transport) as client,
):
yield client
def _async_openai(gateway: Gateway, call: Callable[[openai.AsyncOpenAI], Awaitable[_R]]) -> _R:
async def run() -> _R:
async with (
httpx.AsyncClient(timeout=15, trust_env=False) as transport,
openai.AsyncOpenAI(
api_key=gateway.key, base_url=_v1(gateway), max_retries=0, http_client=transport
) as client,
):
return await call(client)
return asyncio.run(run())
@contextmanager
def _anthropic(gateway: Gateway) -> Iterator[anthropic.Anthropic]:
with (
httpx.Client(timeout=15, trust_env=False) as transport,
anthropic.Anthropic(
api_key=gateway.key, base_url=_origin(gateway), max_retries=0, http_client=transport
) as client,
):
yield client
def _async_anthropic(gateway: Gateway, call: Callable[[anthropic.AsyncAnthropic], Awaitable[_R]]) -> _R:
async def run() -> _R:
async with (
httpx.AsyncClient(timeout=15, trust_env=False) as transport,
anthropic.AsyncAnthropic(
api_key=gateway.key, base_url=_origin(gateway), max_retries=0, http_client=transport
) as client,
):
return await call(client)
return asyncio.run(run())
def _user_message() -> Mapping[str, JsonValue]:
return {"role": "user", "content": TEXT}
def _chat_body(model: str, extra: Mapping[str, JsonValue] = NO_EXTRA) -> Mapping[str, JsonValue]:
return {"model": model, "messages": [_user_message()], **NO_CACHE, **extra}
def _duplicated_timeout_body(model: str) -> bytes:
fields: Final = json.dumps(_chat_body(model)).removesuffix("}")
return f'{fields}, "timeout": 30, "timeout": 30}}'.encode()
def _chat(gateway: Gateway, model: str, extra: Mapping[str, JsonValue] = NO_EXTRA) -> httpx.Response:
return gateway.request("POST", "/v1/chat/completions", _chat_body(model, extra))
def _response_id(response: httpx.Response) -> str:
assert response.status_code == 200, response.text
return string_value(JSON_OBJECT.validate_json(response.content)["id"])
def _error_message(response: httpx.Response) -> str:
body: Final = JSON_OBJECT.validate_json(response.content)
if "error" in body:
return string_value(object_value(body["error"])["message"])
detail: Final = body["detail"]
if isinstance(detail, str):
return detail
assert isinstance(detail, list) and detail, response.text
return string_value(object_value(detail[0])["msg"])
def _assert_rejected(response: httpx.Response, shape: str) -> None:
assert response.status_code >= 400, (shape, response.status_code, response.text)
assert _error_message(response), (shape, response.text)
def _assert_served(response: httpx.Response, identity: str, shape: str) -> None:
assert response.status_code == 200, (shape, response.status_code, response.text)
assert _response_id(response) == identity, (shape, response.text)
def _moderation_through_the_sdk(gateway: Gateway, model: str) -> ModerationCreateResponse | openai.APIStatusError:
with _openai(gateway) as client:
try:
return client.moderations.create(model=model, input=TEXT)
except openai.APIStatusError as error:
return error
def _moderation_after_worker_sync(gateway: Gateway, model: str) -> ModerationCreateResponse | openai.APIStatusError:
return eventually(
lambda: _moderation_through_the_sdk(gateway, model),
lambda outcome: not isinstance(outcome, openai.InternalServerError),
seconds=WORKER_SYNC_SECONDS + 10,
)
def _speech_peer(content_type: str) -> Callable[[Request], Reply]:
def route(request: Request) -> Reply | None:
if request.method != "POST" or not _path(request).endswith("/audio/speech"):
return None
return Reply(body=SPEECH_AUDIO, content_type=content_type)
return _peer(route)
def _speech_through_the_sdk(gateway: Gateway, model: str) -> httpx.Response | openai.APIStatusError:
with _openai(gateway) as client:
try:
return client.audio.speech.create(model=model, voice="alloy", input=TEXT, response_format="mp3").response
except openai.APIStatusError as error:
return error
def _speech_after_worker_sync(gateway: Gateway, model: str) -> httpx.Response | openai.APIStatusError:
return eventually(
lambda: _speech_through_the_sdk(gateway, model),
lambda outcome: not isinstance(outcome, openai.InternalServerError),
seconds=WORKER_SYNC_SECONDS + 10,
)
def _attempt(call: Callable[[], _R]) -> _R | openai.APIStatusError:
try:
return call()
except openai.APIStatusError as error:
return error
def _unknown_to_the_serving_worker(outcome: object) -> bool:
return isinstance(outcome, openai.BadRequestError) and "Invalid model name" in outcome.response.text
def _outcome_after_worker_sync(call: Callable[[], _R]) -> _R | openai.APIStatusError:
return eventually(
lambda: _attempt(call),
lambda outcome: not _unknown_to_the_serving_worker(outcome),
seconds=WORKER_SYNC_SECONDS + 10,
)
def _after_worker_sync(call: Callable[[], _R]) -> _R:
outcome: Final = _outcome_after_worker_sync(call)
assert not isinstance(outcome, openai.APIStatusError), outcome.response.text
return outcome
def _async_after_worker_sync(gateway: Gateway, call: Callable[[openai.AsyncOpenAI], Awaitable[_R]]) -> _R:
return _after_worker_sync(lambda: _async_openai(gateway, call))
def _streamed_file_content(gateway: Gateway, file_id: str, model: str) -> tuple[int, tuple[bytes, ...]]:
with gateway.client.stream(
"GET",
f"/v1/files/{file_id}/content",
params={"model": model},
headers={"Authorization": f"Bearer {gateway.key}"},
) as response:
return response.status_code, tuple(response.iter_bytes())
def _upstream_id(managed: str, prefix: str) -> str:
encoded: Final = managed.removeprefix(prefix)
try:
decoded: Final = base64.urlsafe_b64decode(encoded + "=" * (-len(encoded) % 4)).decode()
except (binascii.Error, UnicodeDecodeError):
return managed
return decoded.removeprefix("litellm:").split(";", 1)[0] if decoded.startswith("litellm:") else managed
def _spend_row(identity: str) -> Mapping[str, JsonValue]:
rows: Final = eventually(
lambda: read_rows(
'SELECT request_id, call_type, model FROM "LiteLLM_SpendLogs" WHERE request_id = %s', (identity,)
),
lambda rows: len(rows) == 1,
seconds=70,
)
return rows[0]
def _spend_row_keyed_by_any(identities: frozenset[str]) -> Mapping[str, JsonValue]:
keys: Final = tuple(sorted(identities))
placeholders: Final = ", ".join("%s" for _ in keys)
rows: Final = eventually(
lambda: read_rows(
f'SELECT request_id, call_type, model FROM "LiteLLM_SpendLogs" WHERE request_id IN ({placeholders})', keys
),
lambda rows: len(rows) == 1,
seconds=70,
)
return rows[0]
def _spend_request_ids(identities: tuple[str, str, str]) -> frozenset[str]:
rows: Final = eventually(
lambda: read_rows('SELECT request_id FROM "LiteLLM_SpendLogs" WHERE request_id IN (%s, %s, %s)', identities),
lambda rows: len(rows) == 3,
seconds=70,
)
return frozenset(string_value(row["request_id"]) for row in rows)
def _anthropic_text(event: RawMessageStreamEvent) -> str:
if isinstance(event, RawContentBlockDeltaEvent) and isinstance(event.delta, TextDelta):
return event.delta.text
return ""
def _chunk_text(chunk: ChatCompletionChunk) -> str:
return chunk.choices[0].delta.content or "" if chunk.choices else ""
def _chunk_finished(chunk: ChatCompletionChunk) -> bool:
return bool(chunk.choices) and chunk.choices[0].finish_reason == "stop"
def _new_model(gateway: Gateway, api_base: str, timeout: JsonValue) -> httpx.Response:
return gateway.request(
"POST",
"/model/new",
{
"model_name": f"integration-{uuid.uuid4().hex}",
"litellm_params": {
"model": f"openai/{UPSTREAM_MODEL}",
"api_key": PROVIDER_KEY,
"api_base": api_base,
"timeout": timeout,
},
"model_info": {},
},
)
def _register_accepted_model(scenario: Scenario, response: httpx.Response, shape: str) -> None:
assert response.status_code == 200, (shape, response.status_code, response.text)
identity: Final = string_value(object_value(JSON_OBJECT.validate_json(response.content)["model_info"])["id"])
scenario.cleanups.callback(scenario.delete_model, identity)
def test_r01_chat_completions_non_stream_through_the_sync_openai_sdk(gateway: Gateway) -> None:
identity: Final = _identity("chatcmpl")
with gateway.scenario() as scenario, wire_server(_chat_peer(identity)) as wire:
model: Final = scenario.model(api_base=f"{wire.url}/v1")
with _openai(gateway) as client:
response: Final = client.chat.completions.create(
model=model, messages=[{"role": "user", "content": TEXT}], extra_body=dict(NO_CACHE)
)
assert response.id == identity, response
assert response.choices[0].message.content == TEXT, response
served: Final = _served(wire)
assert _routes(served) == (("POST", "/v1/chat/completions"),), served
assert served[0].headers["authorization"] == PROVIDER_AUTHORIZATION, served[0].headers
upstream_body: Final = _body(served[0])
assert upstream_body["model"] == UPSTREAM_MODEL, upstream_body
assert upstream_body["messages"] == [_user_message()], upstream_body
assert _spend_row(identity)["model"] == DEPLOYMENT_MODEL
def test_r02_chat_completions_stream_through_the_async_openai_sdk(gateway: Gateway) -> None:
identity: Final = _identity("chatcmpl")
with gateway.scenario() as scenario, wire_server(_chat_peer(identity)) as wire:
model: Final = scenario.model(api_base=f"{wire.url}/v1")
async def stream(client: openai.AsyncOpenAI) -> tuple[ChatCompletionChunk, ...]:
chunks: Final = await client.chat.completions.create(
model=model, messages=[{"role": "user", "content": TEXT}], stream=True, extra_body=dict(NO_CACHE)
)
return tuple([chunk async for chunk in chunks])
received: Final = _async_openai(gateway, stream)
assert {chunk.id for chunk in received} == {identity}, received
assert "".join(_chunk_text(chunk) for chunk in received) == TEXT, received
assert any(_chunk_finished(chunk) for chunk in received), received
served: Final = _served(wire)
assert _routes(served) == (("POST", "/v1/chat/completions"),), served
assert _body(served[0])["stream"] is True, served[0].body
assert _spend_row(identity)["model"] == DEPLOYMENT_MODEL
def test_r03_chat_completions_through_raw_httpx_with_a_body_timeout(gateway: Gateway) -> None:
identity: Final = _identity("chatcmpl")
with gateway.scenario() as scenario, wire_server(_chat_peer(identity)) as wire:
model: Final = scenario.model(api_base=f"{wire.url}/v1")
assert _response_id(_chat(gateway, model, {"timeout": 30})) == identity
served: Final = _served(wire)
assert _routes(served) == (("POST", "/v1/chat/completions"),), served
assert "timeout" not in _body(served[0]), served[0].body
assert _spend_row(identity)["model"] == DEPLOYMENT_MODEL
def test_r04_messages_non_stream_through_the_sync_anthropic_sdk(gateway: Gateway) -> None:
identity: Final = _identity("resp")
with gateway.scenario() as scenario, wire_server(_responses_peer(identity)) as wire:
model: Final = scenario.model(api_base=f"{wire.url}/v1")
with _anthropic(gateway) as client:
message: Final = client.messages.create(
model=model, max_tokens=64, messages=[{"role": "user", "content": TEXT}]
)
assert identity in response_identities(message.id), message.id
first_block: Final = message.content[0]
assert first_block.type == "text" and first_block.text == TEXT, message
served: Final = _served(wire)
assert _routes(served) == (("POST", "/v1/responses"),), served
assert TEXT in served[0].body.decode(), served[0].body
assert _spend_row(message.id)["call_type"] == "anthropic_messages"
def test_r05_messages_stream_through_the_async_anthropic_sdk(gateway: Gateway) -> None:
identity: Final = _identity("resp")
with gateway.scenario() as scenario, wire_server(_responses_peer(identity)) as wire:
model: Final = scenario.model(api_base=f"{wire.url}/v1")
async def stream(client: anthropic.AsyncAnthropic) -> tuple[RawMessageStreamEvent, ...]:
events: Final = await client.messages.create(
model=model, max_tokens=64, messages=[{"role": "user", "content": TEXT}], stream=True
)
return tuple([event async for event in events])
received: Final = _async_anthropic(gateway, stream)
first: Final = received[0]
assert isinstance(first, RawMessageStartEvent), received
assert received[-1].type == "message_stop", received
assert "".join(_anthropic_text(event) for event in received) == TEXT, received
served: Final = _served(wire)
assert _routes(served) == (("POST", "/v1/responses"),), served
assert _body(served[0])["stream"] is True, served[0].body
assert _spend_row(first.message.id)["call_type"] == "anthropic_messages"
def test_r06_responses_non_stream_through_the_sync_openai_sdk(gateway: Gateway) -> None:
identity: Final = _identity("resp")
with gateway.scenario() as scenario, wire_server(_responses_peer(identity)) as wire:
model: Final = scenario.model(api_base=f"{wire.url}/v1")
with _openai(gateway) as client:
response: Final = client.responses.create(model=model, input=TEXT, extra_body=dict(NO_CACHE))
assert identity in response_identities(response.id), response.id
assert response.output_text == TEXT, response
served: Final = _served(wire)
assert _routes(served) == (("POST", "/v1/responses"),), served
upstream_body: Final = _body(served[0])
assert upstream_body["model"] == UPSTREAM_MODEL, upstream_body
assert upstream_body["input"] == TEXT, upstream_body
assert _spend_row(response.id)["model"] == DEPLOYMENT_MODEL
def test_r07_responses_stream_through_the_async_openai_sdk(gateway: Gateway) -> None:
identity: Final = _identity("resp")
with gateway.scenario() as scenario, wire_server(_responses_peer(identity)) as wire:
model: Final = scenario.model(api_base=f"{wire.url}/v1")
async def stream(client: openai.AsyncOpenAI) -> tuple[ResponseStreamEvent, ...]:
events: Final = await client.responses.create(
model=model, input=TEXT, stream=True, extra_body=dict(NO_CACHE)
)
return tuple([event async for event in events])
received: Final = _async_openai(gateway, stream)
assert received[0].type == "response.created", received
last: Final = received[-1]
assert last.type == "response.completed", received
assert identity in response_identities(last.response.id), last
assert last.response.output_text == TEXT, last
served: Final = _served(wire)
assert _routes(served) == (("POST", "/v1/responses"),), served
assert _body(served[0])["stream"] is True, served[0].body
assert _spend_row_keyed_by_any(response_identities(last.response.id))["model"] == DEPLOYMENT_MODEL
def test_r08_completions_non_stream_through_the_sync_openai_sdk(gateway: Gateway) -> None:
identity: Final = _identity("cmpl")
with gateway.scenario() as scenario, wire_server(_completion_peer(identity)) as wire:
model: Final = scenario.model(model=INSTRUCT_DEPLOYMENT_MODEL, api_base=f"{wire.url}/v1")
with _openai(gateway) as client:
completion: Final = client.completions.create(model=model, prompt=TEXT, extra_body=dict(NO_CACHE))
assert completion.id == identity, completion
assert completion.choices[0].text == TEXT, completion
served: Final = _served(wire)
assert _routes(served) == (("POST", "/v1/completions"),), served
upstream_body: Final = _body(served[0])
assert upstream_body["model"] == INSTRUCT_MODEL, upstream_body
assert upstream_body["prompt"] == TEXT, upstream_body
assert _spend_row(identity)["model"] == INSTRUCT_DEPLOYMENT_MODEL
def test_r09_completions_stream_through_the_async_openai_sdk(gateway: Gateway) -> None:
identity: Final = _identity("cmpl")
with gateway.scenario() as scenario, wire_server(_completion_peer(identity)) as wire:
model: Final = scenario.model(model=INSTRUCT_DEPLOYMENT_MODEL, api_base=f"{wire.url}/v1")
async def stream(client: openai.AsyncOpenAI) -> tuple[Completion, ...]:
chunks: Final = await client.completions.create(
model=model, prompt=TEXT, stream=True, extra_body=dict(NO_CACHE)
)
return tuple([chunk async for chunk in chunks])
received: Final = _async_openai(gateway, stream)
assert {chunk.id for chunk in received} == {identity}, received
assert "".join(chunk.choices[0].text or "" for chunk in received if chunk.choices) == TEXT, received
served: Final = _served(wire)
assert _routes(served) == (("POST", "/v1/completions"),), served
assert _body(served[0])["stream"] is True, served[0].body
assert _spend_row(identity)["model"] == INSTRUCT_DEPLOYMENT_MODEL
def test_r10_moderations_through_the_sync_openai_sdk(gateway: Gateway) -> None:
identity: Final = _identity("modr")
with gateway.scenario() as scenario, wire_server(_moderation_peer(identity)) as wire:
model: Final = scenario.model(api_base=f"{wire.url}/v1")
moderation: Final = _moderation_after_worker_sync(gateway, model)
assert isinstance(moderation, ModerationCreateResponse), moderation
assert moderation.id == identity, moderation
assert [result.flagged for result in moderation.results] == [False], moderation
served: Final = _served(wire)
assert _routes(served) == (("POST", "/v1/moderations"),), served
assert served[0].headers["authorization"] == PROVIDER_AUTHORIZATION, served[0].headers
assert _body(served[0])["input"] == TEXT, served[0].body
assert _spend_row(moderation.id)["call_type"] == "amoderation"
def test_r11_files_create_retrieve_and_content_through_the_sync_openai_sdk(gateway: Gateway) -> None:
file_id: Final = _identity("file")
content: Final = _batch_line(file_id)
with gateway.scenario() as scenario, wire_server(_files_peer(file_id, content)) as wire:
model: Final = scenario.model(api_base=f"{wire.url}/v1")
with _openai(gateway) as client:
created: Final = _after_worker_sync(
lambda: client.files.create(file=("batch.jsonl", content), purpose="batch", extra_body={"model": model})
)
retrieved: Final = _after_worker_sync(
lambda: client.files.retrieve(created.id, extra_query={"model": model})
)
downloaded: Final = _after_worker_sync(
lambda: client.files.content(created.id, extra_query={"model": model})
).content
assert _upstream_id(created.id, "file-") == file_id, created
assert _upstream_id(retrieved.id, "file-") == file_id, retrieved
assert retrieved.bytes == len(content), retrieved
assert downloaded == content, downloaded
served: Final = _served(wire)
assert _routes(served) == (
("POST", "/v1/files"),
("GET", f"/v1/files/{file_id}"),
("GET", f"/v1/files/{file_id}/content"),
), served
assert content in served[0].body, served[0].body
assert {request.headers["authorization"] for request in served} == {PROVIDER_AUTHORIZATION}, served
def test_r12_file_content_streams_through_raw_httpx(gateway: Gateway) -> None:
file_id: Final = _identity("file")
content: Final = STREAM_HEAD + STREAM_TAIL
with (
gateway.scenario() as scenario,
wire_server(_files_peer(file_id, content, chunks=(STREAM_HEAD, STREAM_TAIL))) as wire,
):
model: Final = scenario.model(api_base=f"{wire.url}/v1")
status, received = eventually(
lambda: _streamed_file_content(gateway, file_id, model),
lambda outcome: not (outcome[0] == 400 and b"Invalid model name" in b"".join(outcome[1])),
seconds=WORKER_SYNC_SECONDS + 10,
)
assert status == 200, b"".join(received).decode()
assert b"".join(received) == content, (len(received), sum(len(chunk) for chunk in received))
served: Final = _served(wire)
assert _routes(served) == (("GET", f"/v1/files/{file_id}/content"),), served
assert served[0].headers["authorization"] == PROVIDER_AUTHORIZATION, served[0].headers
def test_r13_batches_create_retrieve_list_and_cancel_through_the_async_openai_sdk(gateway: Gateway) -> None:
file_id: Final = _identity("file")
batch_id: Final = _identity("batch")
with gateway.scenario() as scenario, wire_server(_batches_peer(file_id, batch_id)) as wire:
model: Final = scenario.model(api_base=f"{wire.url}/v1")
uploaded: Final = _async_after_worker_sync(
gateway,
lambda client: client.files.create(
file=("batch.jsonl", _batch_line("r1")), purpose="batch", extra_body={"model": model}
),
)
created: Final = _async_after_worker_sync(
gateway,
lambda client: client.batches.create(
input_file_id=uploaded.id,
endpoint="/v1/chat/completions",
completion_window="24h",
extra_body={"model": model},
),
)
retrieved: Final = _async_after_worker_sync(gateway, lambda client: client.batches.retrieve(created.id))
listed: Final = _async_after_worker_sync(
gateway, lambda client: client.batches.list(extra_query={"model": model})
)
cancelled: Final = _async_after_worker_sync(gateway, lambda client: client.batches.cancel(created.id))
listed_ids: Final = tuple(batch.id for batch in listed.data)
assert _upstream_id(uploaded.id, "file-") == file_id, uploaded
assert _upstream_id(created.id, "batch_") == batch_id, created
assert _upstream_id(retrieved.id, "batch_") == batch_id, retrieved
assert batch_id in [_upstream_id(listed_id, "batch_") for listed_id in listed_ids], listed_ids
assert _upstream_id(cancelled.id, "batch_") == batch_id, cancelled
assert cancelled.status == "cancelling", cancelled
served: Final = _served(wire)
assert _routes(served) == (
("POST", "/v1/files"),
("POST", "/v1/batches"),
("GET", f"/v1/batches/{batch_id}"),
("POST", f"/v1/batches/{batch_id}/cancel"),
), served
assert _body(served[1])["input_file_id"] == file_id, served[1].body
assert {request.headers["authorization"] for request in served} == {PROVIDER_AUTHORIZATION}, served
@pytest.mark.timeout(2 * graceful_stop_seconds() + 120)
def test_r14_fine_tuning_jobs_create_list_retrieve_and_cancel_through_the_sync_openai_sdk(
configured_proxy: ConfiguredProxy,
) -> None:
suffix: Final = uuid.uuid4().hex
job_id: Final = f"ftjob-{suffix}"
with _openai(configured_proxy.gateway) as client:
created: Final = client.fine_tuning.jobs.create(
model=UPSTREAM_MODEL, training_file=f"file-{suffix}", extra_body={"custom_llm_provider": "openai"}
)
listed: Final = client.fine_tuning.jobs.list(extra_query={"custom_llm_provider": "openai"})
retrieved: Final = client.fine_tuning.jobs.retrieve(created.id, extra_query={"custom_llm_provider": "openai"})
cancelled: Final = client.fine_tuning.jobs.cancel(created.id, extra_body={"custom_llm_provider": "openai"})
assert created.id == job_id, created
assert created.status == "queued", created
assert [job.id for job in listed.data] == [LISTED_JOB], listed
assert retrieved.id == job_id, retrieved
assert retrieved.training_file == f"file-{suffix}", retrieved
assert cancelled.id == job_id and cancelled.status == "cancelled", cancelled
served: Final = _served(configured_proxy.wire)
assert _routes(served) == (
("POST", "/v1/fine_tuning/jobs"),
("GET", "/v1/fine_tuning/jobs"),
("GET", f"/v1/fine_tuning/jobs/{job_id}"),
("POST", f"/v1/fine_tuning/jobs/{job_id}/cancel"),
), served
assert _body(served[0])["training_file"] == f"file-{suffix}", served[0].body
assert {request.headers["authorization"] for request in served} == {PROVIDER_AUTHORIZATION}, served
@pytest.mark.timeout(2 * graceful_stop_seconds() + 120)
def test_r15_assistants_list_through_raw_httpx(configured_proxy: ConfiguredProxy) -> None:
response: Final = configured_proxy.gateway.request("GET", "/v1/assistants")
assert response.status_code == 200, response.text
body: Final = JSON_OBJECT.validate_json(response.content)
assert body["object"] == "list", response.text
listed: Final = body["data"]
assert isinstance(listed, list), response.text
assert [string_value(object_value(entry)["id"]) for entry in listed] == [ASSISTANT], response.text
served: Final = _served(configured_proxy.wire)
assert _routes(served) == (("GET", "/v1/assistants"),), served
assert served[0].headers["authorization"] == PROVIDER_AUTHORIZATION, served[0].headers
def test_r17_chat_completions_on_an_azure_cloudflare_gateway_deployment_through_the_sync_openai_sdk(
gateway: Gateway,
) -> None:
identity: Final = _identity("chatcmpl")
deployment: Final = f"gpt-4o-mini-{uuid.uuid4().hex[:8]}"
with gateway.scenario() as scenario, wire_server(_chat_peer(identity)) as wire:
model: Final = scenario.model(
model=f"azure/{deployment}", api_base=f"{wire.url}{CLOUDFLARE_GATEWAY}", api_version=AZURE_API_VERSION
)
with _openai(gateway) as client:
response: Final = client.chat.completions.create(
model=model, messages=[{"role": "user", "content": TEXT}], extra_body=dict(NO_CACHE)
)
streamed: Final = tuple(
client.chat.completions.create(
model=model, messages=[{"role": "user", "content": TEXT}], stream=True, extra_body=dict(NO_CACHE)
)
)
assert response.id == identity, response
assert response.choices[0].message.content == TEXT, response
assert {chunk.id for chunk in streamed} == {identity}, streamed
assert "".join(_chunk_text(chunk) for chunk in streamed) == TEXT, streamed
served: Final = _served(wire)
assert _routes(served) == (("POST", f"{CLOUDFLARE_GATEWAY}/{deployment}/chat/completions"),) * 2, served
assert {request.target.split("?", 1)[1] for request in served} == {f"api-version={AZURE_API_VERSION}"}, served
assert {request.headers["api-key"] for request in served} == {PROVIDER_KEY}, served
assert [_streams(request) for request in served] == [False, True], served
assert _spend_row(identity)["model"] == f"azure/{deployment}"
def test_r18_audio_speech_answers_with_the_upstream_audio_type_through_the_sync_openai_sdk(gateway: Gateway) -> None:
with gateway.scenario() as scenario, wire_server(_speech_peer("audio/ogg")) as wire:
model: Final = scenario.model(model=f"openai/{SPEECH_UPSTREAM_MODEL}", api_base=f"{wire.url}/v1")
speech: Final = _speech_after_worker_sync(gateway, model)
assert not isinstance(speech, openai.APIStatusError), speech
assert speech.status_code == 200, speech.text
assert speech.headers["content-type"] == "audio/ogg", dict(speech.headers)
assert speech.content == SPEECH_AUDIO, speech.content[:16]
served: Final = _served(wire)
assert _routes(served) == (("POST", "/v1/audio/speech"),), served
assert served[0].headers["authorization"] == PROVIDER_AUTHORIZATION, served[0].headers
upstream_body: Final = _body(served[0])
assert (upstream_body["model"], upstream_body["input"], upstream_body["response_format"]) == (
SPEECH_UPSTREAM_MODEL,
TEXT,
"mp3",
), upstream_body
assert _spend_row(speech.headers["x-litellm-call-id"])["call_type"] == "aspeech"
def test_s01_chat_completions_upstream_400_reaches_the_sync_openai_sdk_as_a_bad_request(gateway: Gateway) -> None:
with gateway.scenario() as scenario, wire_server(_status_peer(openai_error(400))) as wire:
model: Final = scenario.model(api_base=f"{wire.url}/v1", max_retries=0)
with _openai(gateway) as client, pytest.raises(openai.BadRequestError) as raised:
client.chat.completions.create(model=model, messages=[_user_message()], extra_body=dict(NO_CACHE))
assert raised.value.status_code == 400, raised.value
assert "scripted 400" in raised.value.response.text, raised.value.response.text
assert _routes(_served(wire)) == (("POST", "/v1/chat/completions"),)
def test_s02_chat_completions_upstream_429_keeps_retry_after_through_raw_httpx(gateway: Gateway) -> None:
limited: Final = Reply(status=429, body=openai_error(429).body, headers={"retry-after": "7"})
with gateway.scenario() as scenario, wire_server(_status_peer(limited)) as wire:
model: Final = scenario.model(api_base=f"{wire.url}/v1", max_retries=0)
response: Final = _chat(gateway, model)
assert response.status_code == 429, response.text
assert response.headers.get("llm_provider-retry-after") == "7", dict(response.headers)
assert "scripted 429" in _error_message(response), response.text
assert _routes(_served(wire)) == (("POST", "/v1/chat/completions"),)
def test_s03_chat_completions_upstream_500_reaches_the_sync_openai_sdk_as_a_server_error(gateway: Gateway) -> None:
with gateway.scenario() as scenario, wire_server(_status_peer(openai_error(500))) as wire:
model: Final = scenario.model(api_base=f"{wire.url}/v1", max_retries=0)
with _openai(gateway) as client, pytest.raises(openai.InternalServerError) as raised:
client.chat.completions.create(model=model, messages=[_user_message()], extra_body=dict(NO_CACHE))
assert raised.value.status_code == 500, raised.value
assert "scripted 500" in raised.value.response.text, raised.value.response.text
assert _routes(_served(wire)) == (("POST", "/v1/chat/completions"),)
def test_s04_chat_completions_body_timeout_shapes_answer_without_breaking_the_proxy(gateway: Gateway) -> None:
identity: Final = _identity("chatcmpl")
with gateway.scenario() as scenario, wire_server(_chat_peer(identity)) as wire:
model: Final = scenario.model(api_base=f"{wire.url}/v1")
for shape, timeout in REJECTED_CHAT_TIMEOUTS.items():
_assert_rejected(_chat(gateway, model, {"timeout": timeout}), shape)
for shape, timeout in ACCEPTED_CHAT_TIMEOUTS.items():
_assert_served(_chat(gateway, model, {"timeout": timeout}), identity, shape)
duplicated: Final = gateway.client.post(
"/v1/chat/completions",
content=_duplicated_timeout_body(model),
headers={"Authorization": f"Bearer {gateway.key}", "Content-Type": "application/json"},
)
_assert_served(duplicated, identity, "duplicated key")
_assert_served(_chat(gateway, model), identity, "follow-up")
assert len(_served(wire)) == len(ACCEPTED_CHAT_TIMEOUTS) + 2
def test_s05_model_new_timeout_shapes_are_rejected_or_stored_without_breaking_the_proxy(gateway: Gateway) -> None:
identity: Final = _identity("chatcmpl")
with gateway.scenario() as scenario, wire_server(_chat_peer(identity)) as wire:
model: Final = scenario.model(api_base=f"{wire.url}/v1")
for shape, timeout in REJECTED_DEPLOYMENT_TIMEOUTS.items():
_assert_rejected(_new_model(gateway, f"{wire.url}/v1", timeout), shape)
for shape, timeout in ACCEPTED_DEPLOYMENT_TIMEOUTS.items():
_register_accepted_model(scenario, _new_model(gateway, f"{wire.url}/v1", timeout), shape)
_assert_served(_chat(gateway, model), identity, "follow-up")
assert _routes(_served(wire)) == (("POST", "/v1/chat/completions"),)
def test_s06_upstream_errors_on_completions_moderations_and_files_reach_the_sync_openai_sdk(
gateway: Gateway,
) -> None:
with gateway.scenario() as scenario:
with wire_server(_status_peer(openai_error(401))) as unauthorized:
model: Final = scenario.model(
model=INSTRUCT_DEPLOYMENT_MODEL, api_base=f"{unauthorized.url}/v1", max_retries=0
)
with _openai(gateway) as client, pytest.raises(openai.AuthenticationError) as completion_error:
client.completions.create(model=model, prompt=TEXT, extra_body=dict(NO_CACHE))
assert completion_error.value.status_code == 401, completion_error.value
assert "scripted 401" in completion_error.value.response.text, completion_error.value.response.text
assert _routes(_served(unauthorized)) == (("POST", "/v1/completions"),)
with wire_server(_status_peer(openai_error(400))) as rejecting:
rejected_model: Final = scenario.model(api_base=f"{rejecting.url}/v1", max_retries=0)
moderation_error: Final = _moderation_after_worker_sync(gateway, rejected_model)
assert isinstance(moderation_error, openai.BadRequestError), moderation_error
assert "scripted 400" in moderation_error.response.text, moderation_error.response.text
with _openai(gateway) as client:
file_error: Final = _outcome_after_worker_sync(
lambda: client.files.create(
file=("batch.jsonl", _batch_line("rejected")),
purpose="batch",
extra_body={"model": rejected_model},
)
)
assert isinstance(file_error, openai.BadRequestError), file_error
assert "scripted 400" in file_error.response.text, file_error.response.text
assert _routes(_served(rejecting)) == (("POST", "/v1/moderations"), ("POST", "/v1/files"))
@pytest.mark.parametrize("provider", tuple(PAYMENT_REQUIRED_DEPLOYMENTS))
def test_s07_chat_completions_upstream_402_reaches_the_sync_openai_sdk_as_payment_required(
gateway: Gateway, provider: str
) -> None:
with gateway.scenario() as scenario, wire_server(_status_peer(openai_error(402))) as wire:
model: Final = scenario.model(model=PAYMENT_REQUIRED_DEPLOYMENTS[provider], api_base=wire.url, max_retries=0)
with _openai(gateway) as client, pytest.raises(openai.APIStatusError) as raised:
client.chat.completions.create(model=model, messages=[_user_message()], extra_body=dict(NO_CACHE))
assert raised.value.status_code == 402, raised.value
assert "scripted 402" in raised.value.response.text, raised.value.response.text
assert len(_served(wire)) == 1
def test_e01_deployment_timeout_answers_408_and_a_body_timeout_on_the_same_deployment_recovers(
gateway: Gateway,
) -> None:
identity: Final = _identity("chatcmpl")
gate: Final = threading.Event()
with gateway.scenario() as scenario, wire_server(_held_chat_peer(gate, identity)) as wire:
model: Final = scenario.model(api_base=f"{wire.url}/v1", timeout=0.5, max_retries=0)
timed_out: Final = _chat(gateway, model)
gate.set()
assert timed_out.status_code == 408, timed_out.text
assert "timeout" in _error_message(timed_out).lower(), timed_out.text
recovered: Final = _chat(gateway, model, {"timeout": 5})
assert _response_id(recovered) == identity, recovered.text
assert _routes(_served(wire)) == (("POST", "/v1/chat/completions"),) * 2
def test_e02_three_identical_no_cache_requests_through_raw_httpx_land_three_spend_rows(gateway: Gateway) -> None:
served_ids: Final[SimpleQueue[str]] = SimpleQueue()
with gateway.scenario() as scenario, wire_server(_fresh_chat_peer(served_ids)) as wire:
model: Final = scenario.model(api_base=f"{wire.url}/v1")
received: Final = (
_response_id(_chat(gateway, model)),
_response_id(_chat(gateway, model)),
_response_id(_chat(gateway, model)),
)
assert len(set(received)) == 3, received
assert _routes(_served(wire)) == (("POST", "/v1/chat/completions"),) * 3
assert {served_ids.get_nowait() for _ in range(3)} == set(received), received
assert _spend_request_ids(received) == frozenset(received)
def test_e03_deployment_timeout_as_a_string_still_serves_through_raw_httpx(gateway: Gateway) -> None:
identity: Final = _identity("chatcmpl")
with gateway.scenario() as scenario, wire_server(_chat_peer(identity)) as wire:
model: Final = scenario.model(api_base=f"{wire.url}/v1", timeout="30")
assert _response_id(_chat(gateway, model)) == identity
assert _routes(_served(wire)) == (("POST", "/v1/chat/completions"),)
assert _spend_row(identity)["model"] == DEPLOYMENT_MODEL