mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-29 01:42:19 +00:00
* ci: drop the ocr_testing job now that tests/ocr_tests is gone Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(ocr): restore the live OCR matrix and the ocr_testing job The public litellm.ocr / aocr / Router interface is unchanged by the Rust migration, so the live provider matrix still applies. Drops the stale VCR skip list for the deleted test_rust_bridge.py. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(messages): show streamed upstream headers never reach the native stream The Python handler puts the upstream response headers on the stream's _hidden_params before the first chunk so the proxy can forward them as llm_provider-* headers. The native route drops them, and this test fails on the Rust path while passing on Python. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * feat(messages): hand upstream response headers to the native stream before its first chunk The Messages route fills MessagesStreamHead from the upstream response and yields it on Open. The Python driver converts it through the protocol host and hands it to Stream and SyncStream as their _hidden_params, so a streamed native call carries additional_headers the same way the Python handler does and the proxy can forward them as llm_provider-* headers. The relay contract lives in the core crate test, the hand-off in the host-python driver test, and the header projection in the route host test, so the recording-server test that showed the gap is dropped. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * wip --------- Co-authored-by: Yujong Lee <yujong@berri.ai> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
145 lines
5.5 KiB
Python
145 lines
5.5 KiB
Python
"""Ordered rollout policy for routes, cache backends, and secret managers.
|
|
|
|
The first matching rule wins; unmatched contexts stay on Python. Native
|
|
admission separately decides whether the selected implementation can execute.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass
|
|
from enum import Enum, auto
|
|
from typing import Final, TypeAlias
|
|
|
|
from litellm.rust_bridge.configuration import Decision, Rollout
|
|
from litellm.rust_bridge.configuration import decision as _decision
|
|
from litellm.types.caching import LiteLLMCacheType
|
|
from litellm.types.secret_managers.main import KeyManagementSystem
|
|
|
|
|
|
class Route(str, Enum):
|
|
CHAT_COMPLETIONS = "chat_completions"
|
|
EMBEDDINGS = "embeddings"
|
|
MESSAGES = "messages"
|
|
RESPONSES = "responses"
|
|
TRANSCRIPTION = "transcription"
|
|
OCR = "ocr"
|
|
TOKEN_COUNTER = "token_counter"
|
|
TOKENIZER = "tokenizer"
|
|
|
|
|
|
class Delivery(Enum):
|
|
COMPLETED = auto()
|
|
STREAMING = auto()
|
|
WEBSOCKET = auto()
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class RouteContext:
|
|
route: Route
|
|
provider: str | None = None
|
|
model: str | None = None
|
|
delivery: Delivery = Delivery.COMPLETED
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class RouteRule:
|
|
route: Route
|
|
rollout: Rollout
|
|
providers: frozenset[str] | None = None
|
|
models: frozenset[str] | None = None
|
|
deliveries: frozenset[Delivery] | None = None
|
|
|
|
def matches(self, context: Context) -> bool:
|
|
return (
|
|
isinstance(context, RouteContext)
|
|
and context.route is self.route
|
|
and (self.providers is None or context.provider in self.providers)
|
|
and (self.models is None or context.model in self.models)
|
|
and (self.deliveries is None or context.delivery in self.deliveries)
|
|
)
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class CacheContext:
|
|
backend: str
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class CacheRule:
|
|
rollout: Rollout
|
|
backends: frozenset[str] | None = None
|
|
|
|
def matches(self, context: Context) -> bool:
|
|
return isinstance(context, CacheContext) and (self.backends is None or context.backend in self.backends)
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class SecretManagerContext:
|
|
system: str
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class SecretManagerRule:
|
|
rollout: Rollout
|
|
systems: frozenset[str] | None = None
|
|
|
|
def matches(self, context: Context) -> bool:
|
|
return isinstance(context, SecretManagerContext) and (self.systems is None or context.system in self.systems)
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class LoggerContext:
|
|
pass
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class LoggerRule:
|
|
rollout: Rollout
|
|
|
|
def matches(self, context: Context) -> bool:
|
|
return isinstance(context, LoggerContext)
|
|
|
|
|
|
Context: TypeAlias = RouteContext | CacheContext | SecretManagerContext | LoggerContext
|
|
Rule: TypeAlias = RouteRule | CacheRule | SecretManagerRule | LoggerRule
|
|
Rules: TypeAlias = tuple[Rule, ...]
|
|
|
|
RULES: Final[Rules] = (
|
|
LoggerRule(Rollout.RUST_OPT_IN),
|
|
RouteRule(Route.CHAT_COMPLETIONS, Rollout.PYTHON_ONLY),
|
|
RouteRule(Route.EMBEDDINGS, Rollout.PYTHON_ONLY),
|
|
RouteRule(Route.OCR, Rollout.RUST_REQUIRED),
|
|
RouteRule(Route.MESSAGES, Rollout.PYTHON_ONLY, providers=frozenset({"anthropic"})),
|
|
RouteRule(Route.MESSAGES, Rollout.PYTHON_ONLY),
|
|
RouteRule(Route.RESPONSES, Rollout.PYTHON_ONLY),
|
|
RouteRule(Route.TOKEN_COUNTER, Rollout.PYTHON_ONLY),
|
|
RouteRule(Route.TOKENIZER, Rollout.PYTHON_ONLY),
|
|
RouteRule(Route.TRANSCRIPTION, Rollout.RUST_REQUIRED, providers=frozenset({"bedrock"})),
|
|
CacheRule(Rollout.PYTHON_ONLY, backends=frozenset({LiteLLMCacheType.LOCAL})),
|
|
CacheRule(Rollout.PYTHON_ONLY, backends=frozenset({LiteLLMCacheType.REDIS})),
|
|
CacheRule(Rollout.PYTHON_ONLY, backends=frozenset({LiteLLMCacheType.REDIS_SEMANTIC})),
|
|
CacheRule(Rollout.PYTHON_ONLY, backends=frozenset({LiteLLMCacheType.VALKEY_SEMANTIC})),
|
|
CacheRule(Rollout.PYTHON_ONLY, backends=frozenset({LiteLLMCacheType.S3})),
|
|
CacheRule(Rollout.PYTHON_ONLY, backends=frozenset({LiteLLMCacheType.DISK})),
|
|
CacheRule(Rollout.PYTHON_ONLY, backends=frozenset({LiteLLMCacheType.QDRANT_SEMANTIC})),
|
|
CacheRule(Rollout.PYTHON_ONLY, backends=frozenset({LiteLLMCacheType.AZURE_BLOB})),
|
|
CacheRule(Rollout.PYTHON_ONLY, backends=frozenset({LiteLLMCacheType.GCS})),
|
|
SecretManagerRule(Rollout.PYTHON_ONLY, systems=frozenset({KeyManagementSystem.GOOGLE_KMS.value})),
|
|
SecretManagerRule(Rollout.PYTHON_ONLY, systems=frozenset({KeyManagementSystem.AZURE_KEY_VAULT.value})),
|
|
SecretManagerRule(Rollout.PYTHON_ONLY, systems=frozenset({KeyManagementSystem.AWS_SECRET_MANAGER.value})),
|
|
SecretManagerRule(Rollout.PYTHON_ONLY, systems=frozenset({KeyManagementSystem.GOOGLE_SECRET_MANAGER.value})),
|
|
SecretManagerRule(Rollout.PYTHON_ONLY, systems=frozenset({KeyManagementSystem.HASHICORP_VAULT.value})),
|
|
SecretManagerRule(Rollout.PYTHON_ONLY, systems=frozenset({KeyManagementSystem.CYBERARK.value})),
|
|
SecretManagerRule(Rollout.PYTHON_ONLY, systems=frozenset({KeyManagementSystem.LOCAL.value})),
|
|
SecretManagerRule(Rollout.PYTHON_ONLY, systems=frozenset({KeyManagementSystem.AWS_KMS.value})),
|
|
SecretManagerRule(Rollout.PYTHON_ONLY, systems=frozenset({KeyManagementSystem.CUSTOM.value})),
|
|
)
|
|
|
|
|
|
def rollout(context: Context, rules: Rules | None = None) -> Rollout:
|
|
selected_rules: Final = RULES if rules is None else rules
|
|
return next((rule.rollout for rule in selected_rules if rule.matches(context)), Rollout.PYTHON_ONLY)
|
|
|
|
|
|
def decision(context: Context, rules: Rules | None = None) -> Decision:
|
|
return _decision(rollout(context, rules))
|