mirror of
https://github.com/tiennm99/litellm.git
synced 2026-07-12 21:04:10 +00:00
53a206a179
* fix(anthropic/adapter): open thinking block for reasoning_content-only streaming chunks The /v1/messages streaming content-block classifier (_translate_streaming_openai_chunk_to_anthropic_content_block) only recognized thinking_blocks. OpenAI-compatible reasoning backends (vLLM/SGLang reasoning parsers: DeepSeek-R1, Qwen3, gpt-oss, ...) populate reasoning_content with thinking_blocks=None, so the classifier fell through to a text block. The delta translator already emits thinking_delta for reasoning_content, so those deltas landed inside a text block and Anthropic streaming clients (Claude Code, SDK .stream()) silently dropped the chain-of-thought. Mirror the reasoning_content fallback already present in the non-stream translator and the streaming delta translator so the classifier opens a thinking block. Adds a focused regression test. * fix(anthropic/adapter): reach reasoning_content branch when thinking_blocks attr is absent Delta deletes the thinking_blocks attribute when unset, so the prior nested check was unreachable for reasoning-only chunks (vLLM/SGLang). Make it a sibling elif so the content block is classified as thinking. * test(proxy): stop component-allowlist test leaking DATABASE_URL into xdist peers The component-allowlist test pins throwaway DATABASE_URL/LITELLM_MASTER_KEY values at import time via os.environ so importing proxy_server doesn't need a live database. Those values persisted for the whole pytest-xdist worker, so a sibling test sharing the worker (test_key_rotation_e2e's DB-backed E2E case) saw the leaked sqlite DATABASE_URL, treated it as an available database instead of skipping, and the Prisma engine rejected the non-postgres URL (P1012 -> httpx.ConnectError). Restore the prior environment after the import so the throwaway values never escape the module. --------- Co-authored-by: Tai An <antai12232931@outlook.com>
91 lines
3.6 KiB
Python
91 lines
3.6 KiB
Python
"""Coverage test for the gateway / backend component allowlists.
|
|
|
|
The componentization scaffold splits the proxy FastAPI app into two runtime
|
|
components by trimming the route table inside a wrapped lifespan context:
|
|
|
|
gateway.main -> only paths matched by gateway/routes/allowlist.py
|
|
backend.main -> only paths matched by backend/routes/allowlist.py
|
|
|
|
If either allowlist drops a path that was reachable on the monolithic app,
|
|
clients hitting that path on the corresponding pod get a 404. This test
|
|
guarantees that the union of the two trimmed route sets equals the full set
|
|
of routes on the proxy app — i.e. no endpoint is dropped on the floor.
|
|
|
|
The test reproduces the same predicate that ``gateway/main.py`` and
|
|
``backend/main.py`` use, without importing them. The component modules wrap
|
|
the shared ``app.router.lifespan_context``; importing them in the test process
|
|
would chain wrappers and corrupt the snapshot.
|
|
"""
|
|
|
|
import os
|
|
import sys
|
|
|
|
# Importing ``litellm.proxy.proxy_server`` runs its module-level setup, which
|
|
# reads ``DATABASE_URL`` (Prisma) and ``LITELLM_MASTER_KEY``. Tier-zero CI
|
|
# runners don't set these. We pin throwaway values before the import so the
|
|
# test never depends on a live database or master key, then restore the prior
|
|
# environment so the throwaway values don't leak into sibling tests sharing the
|
|
# xdist worker (a leaked non-postgres ``DATABASE_URL`` makes DB-backed tests
|
|
# treat a phantom database as available instead of skipping).
|
|
_THROWAWAY_ENV = {
|
|
"DATABASE_URL": "sqlite:///:memory:",
|
|
"LITELLM_MASTER_KEY": "sk-test-component-allowlist",
|
|
}
|
|
_PRE_EXISTING_ENV = {key: os.environ.get(key) for key in _THROWAWAY_ENV}
|
|
for _key, _value in _THROWAWAY_ENV.items():
|
|
os.environ.setdefault(_key, _value)
|
|
|
|
from fastapi.routing import Mount
|
|
|
|
# gateway/ and backend/ live at the repo root, not inside litellm/.
|
|
_REPO_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", ".."))
|
|
if _REPO_ROOT not in sys.path:
|
|
sys.path.insert(0, _REPO_ROOT)
|
|
|
|
from backend.routes.allowlist import BACKEND_EXACT_PATHS, BACKEND_PATH_PREFIXES
|
|
from gateway.routes.allowlist import GATEWAY_EXACT_PATHS, GATEWAY_PATH_PREFIXES
|
|
from litellm.proxy.proxy_server import app
|
|
|
|
for _key, _previous in _PRE_EXISTING_ENV.items():
|
|
if _previous is None:
|
|
os.environ.pop(_key, None)
|
|
else:
|
|
os.environ[_key] = _previous
|
|
|
|
|
|
def _component_paths(routes, exact_paths, path_prefixes) -> set[str]:
|
|
"""Reproduce ``gateway.main._is_gateway_route`` / ``backend.main._is_backend_route``."""
|
|
out: set[str] = set()
|
|
for route in routes:
|
|
if isinstance(route, Mount):
|
|
continue
|
|
path = getattr(route, "path", None)
|
|
if path is None:
|
|
continue
|
|
if path in exact_paths or any(path.startswith(p) for p in path_prefixes):
|
|
out.add(path)
|
|
return out
|
|
|
|
|
|
def test_gateway_plus_backend_covers_full_app():
|
|
"""Every route on the proxy app must be served by gateway or backend."""
|
|
all_paths = {
|
|
getattr(r, "path")
|
|
for r in app.router.routes
|
|
if not isinstance(r, Mount) and getattr(r, "path", None) is not None
|
|
}
|
|
gateway_paths = _component_paths(
|
|
app.router.routes, GATEWAY_EXACT_PATHS, GATEWAY_PATH_PREFIXES
|
|
)
|
|
backend_paths = _component_paths(
|
|
app.router.routes, BACKEND_EXACT_PATHS, BACKEND_PATH_PREFIXES
|
|
)
|
|
|
|
uncovered = all_paths - (gateway_paths | backend_paths)
|
|
|
|
assert not uncovered, (
|
|
f"{len(uncovered)} route(s) are not exposed on either component. "
|
|
f"Update gateway/routes/allowlist.py or backend/routes/allowlist.py to cover:\n "
|
|
+ "\n ".join(sorted(uncovered))
|
|
)
|