mirror of
https://github.com/BerriAI/litellm.git
synced 2026-11-15 09:12:18 +00:00
Merge 95264b4e6d into add7dea9aa
This commit is contained in:
commit
5a5f3f9260
18 changed files with 1285 additions and 203 deletions
|
|
@ -1491,6 +1491,19 @@ def get_bedrock_base_model(model: str) -> str:
|
|||
return model
|
||||
|
||||
|
||||
def bedrock_model_name_without_provider(model: str) -> str:
|
||||
"""The model id without Bedrock's routing and region prefixes and without its provider namespace.
|
||||
|
||||
A Bedrock foundation model id is ``<provider>.<model>`` with a one-word provider (``anthropic.``,
|
||||
``deepseek.``, ``moonshotai.``), so a base model whose first dot-separated part is one word drops it:
|
||||
``bedrock/us.openai.gpt-6.1-sol`` and ``us-east-1/openai.gpt-6.1-sol`` are ``gpt-6.1-sol``,
|
||||
``us.deepseek.r1-v1:0`` is ``r1-v1:0``. Any other base model, such as ``gpt-5.5`` or an imported model's
|
||||
ARN id, is returned as is."""
|
||||
base: Final = get_bedrock_base_model(model)
|
||||
provider, separator, name = base.partition(".")
|
||||
return name if separator and provider.isalnum() else base
|
||||
|
||||
|
||||
def bedrock_converse_accepts_redacted_thinking(model: str, base_model: str | None) -> bool:
|
||||
"""
|
||||
Whether ``reasoningContent.redactedContent`` may be replayed to this Converse target.
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -3,37 +3,49 @@
|
|||
Codex CLI discovers a provider's models with ``GET <model_catalog_url>?client_version=<its version>``
|
||||
(the provider's ``model_catalog_url`` pointed at ``/v1/models``, with its ``api_key_model_discovery``
|
||||
feature on) and decodes the body as its own ``ModelsResponse``, ``{"models": [ModelInfo, ...]}``,
|
||||
never the OpenAI list shape. This module builds that body from the rows ``/v1/models`` already
|
||||
lists: a model Codex knows keeps Codex's own entry for it, any other model gets the fallback entry
|
||||
Codex itself uses for an unknown slug, and ``model_info.service_tiers`` becomes the entry's
|
||||
never the OpenAI list shape. Codex then ignores its own bundled catalog, so this body is all it knows
|
||||
about the provider's models. It is built from the rows ``/v1/models`` already lists: a model Codex
|
||||
knows, by its id or by the upstream model its deployments call once the provider's own prefixes are
|
||||
stripped, keeps Codex's own entry for it, minus any service tier some deployment behind it prices tokens
|
||||
but not that tier for; any other model gets an entry translated from the capabilities its deployments' model info
|
||||
declares (see ``codex_model_translation``). ``model_info.service_tiers`` becomes the entry's
|
||||
``service_tiers``, which Codex turns into slash commands (``/ultrafast``) that send ``service_tier``
|
||||
upstream. A ``model_info.max_input_tokens`` the operator sets replaces a known model's
|
||||
``context_window``, up to the most Codex lets its own ``model_context_window`` raise that model to.
|
||||
A fallback entry offers the model group's ``supported_reasoning_efforts`` as its reasoning levels,
|
||||
A translated entry offers the model group's ``supported_reasoning_efforts`` as its reasoning levels,
|
||||
trimmed to the ones the requesting Codex release can parse.
|
||||
|
||||
The stock entries are Codex 0.159.3's bundled catalog, vendored unchanged from
|
||||
https://github.com/openai/codex/blob/rust-v0.159.3/codex-rs/models-manager/models.json (Apache-2.0)
|
||||
as ``codex_bundled_models_0.159.3.json``, and ``codex_base_instructions.md`` is
|
||||
The stock entries are Codex 0.166.0's bundled catalog, vendored unchanged from
|
||||
https://github.com/openai/codex/blob/rust-v0.166.0/codex-rs/models-manager/models.json (Apache-2.0)
|
||||
as ``codex_bundled_models_0.166.0.json``, and ``codex_base_instructions.md`` is
|
||||
``codex-rs/models-manager/prompt.md`` at the same tag. Refresh both by downloading those two paths at
|
||||
a newer tag.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Mapping, Sequence
|
||||
from collections.abc import Callable, Mapping, Sequence
|
||||
from dataclasses import dataclass
|
||||
from functools import cache
|
||||
from itertools import accumulate, pairwise
|
||||
from pathlib import Path
|
||||
from types import MappingProxyType, NoneType
|
||||
from typing import TYPE_CHECKING, Annotated, Final, Literal, TypeAlias
|
||||
from types import MappingProxyType
|
||||
from typing import TYPE_CHECKING, Final, Literal, TypeAlias
|
||||
|
||||
from packaging.version import Version
|
||||
from pydantic import ConfigDict, Field, StringConstraints, TypeAdapter, ValidationError
|
||||
from pydantic import ConfigDict, TypeAdapter, ValidationError
|
||||
|
||||
from litellm._logging import verbose_proxy_logger
|
||||
from litellm.litellm_core_utils.reasoning_effort_strength import nearest_declared_reasoning_effort
|
||||
from litellm.proxy.common_utils.codex_model_translation import (
|
||||
TOKEN_PRICE_KEY,
|
||||
CodexDeploymentCapabilities,
|
||||
CodexFallbackModel,
|
||||
CodexServiceTier,
|
||||
TierId,
|
||||
codex_fallback_entry,
|
||||
combined_capabilities,
|
||||
no_priced_deployment_lacks_tier,
|
||||
tier_price_keys,
|
||||
)
|
||||
from litellm.types.llms.base_model import LiteLLMBaseModel
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
|
@ -42,89 +54,24 @@ if TYPE_CHECKING:
|
|||
from litellm.types.proxy.model_listing import ModelInfoResponse
|
||||
|
||||
CODEX_BASE_INSTRUCTIONS_PATH: Final = Path(__file__).with_name("codex_base_instructions.md")
|
||||
CODEX_BUNDLED_MODELS_PATH: Final = Path(__file__).with_name("codex_bundled_models_0.159.3.json")
|
||||
CODEX_BUNDLED_MODELS_PATH: Final = Path(__file__).with_name("codex_bundled_models_0.166.0.json")
|
||||
CODEX_CATALOG_BYTE_LIMIT: Final = 1024 * 1024
|
||||
CODEX_CHAT_MODES: Final = frozenset({"chat", "responses"})
|
||||
|
||||
_TierId = Annotated[str, StringConstraints(strip_whitespace=True, min_length=1)]
|
||||
_BODY_PREFIX: Final = '{"models":['
|
||||
_BODY_SUFFIX: Final = "]}"
|
||||
_FIRST_CODEX_WITH_CUSTOM_EFFORTS: Final = Version("0.138.0")
|
||||
_EFFORTS_EVERY_CODEX_PARSES: Final = frozenset({"none", "minimal", "low", "medium", "high", "xhigh"})
|
||||
_PREFERRED_DEFAULT_EFFORT: Final = "medium"
|
||||
_CODEX_UNSET_EFFORT: Final = "none"
|
||||
|
||||
|
||||
class CodexServiceTier(LiteLLMBaseModel):
|
||||
"""A `ModelServiceTier` as Codex reads it: the slash command is `name` lowercased, and toggling
|
||||
it sends `id` as the request's `service_tier`."""
|
||||
|
||||
model_config = ConfigDict(frozen=True)
|
||||
|
||||
id: _TierId
|
||||
name: str
|
||||
description: str
|
||||
_NO_UPDATE: Final[Mapping[str, object]] = MappingProxyType({})
|
||||
|
||||
|
||||
class _ConfiguredTier(LiteLLMBaseModel):
|
||||
model_config = ConfigDict(frozen=True, extra="forbid")
|
||||
|
||||
id: _TierId
|
||||
id: TierId
|
||||
name: str | None = None
|
||||
description: str | None = None
|
||||
|
||||
|
||||
_CONFIGURED_TIERS: Final = TypeAdapter(tuple[_TierId | _ConfiguredTier, ...])
|
||||
|
||||
|
||||
class CodexFallbackReasoningLevel(LiteLLMBaseModel):
|
||||
"""A `ReasoningEffortPreset` as Codex reads it: picking it sends `effort` as the request's reasoning effort."""
|
||||
|
||||
model_config = ConfigDict(frozen=True)
|
||||
|
||||
effort: str
|
||||
description: str
|
||||
|
||||
|
||||
class CodexTruncationPolicy(LiteLLMBaseModel):
|
||||
mode: Literal["bytes"] = "bytes"
|
||||
limit: int = 10_000
|
||||
|
||||
|
||||
class CodexFallbackModel(LiteLLMBaseModel):
|
||||
"""One `ModelInfo` entry of a Codex model catalog for a model Codex does not know.
|
||||
|
||||
Every field that some Codex release since `model_catalog_json` appeared
|
||||
(0.105.0) deserializes without a default is spelled out here, so one catalog
|
||||
parses on all of them; the values match the fallback metadata Codex uses for
|
||||
a model slug it does not know, so picking such a proxy model behaves the
|
||||
same as `codex -m` did.
|
||||
"""
|
||||
|
||||
model_config = ConfigDict(frozen=True)
|
||||
|
||||
slug: str
|
||||
display_name: str
|
||||
description: NoneType = None
|
||||
default_reasoning_level: str | None = Field(default=None, exclude_if=lambda level: level is None)
|
||||
supported_reasoning_levels: tuple[CodexFallbackReasoningLevel, ...] = ()
|
||||
shell_type: Literal["unified_exec"] = "unified_exec"
|
||||
visibility: Literal["list"] = "list"
|
||||
supported_in_api: Literal[True] = True
|
||||
priority: int
|
||||
service_tiers: tuple[CodexServiceTier, ...] = ()
|
||||
availability_nux: NoneType = None
|
||||
upgrade: NoneType = None
|
||||
support_verbosity: Literal[False] = False
|
||||
supports_reasoning_summaries: Literal[False] = False
|
||||
supports_parallel_tool_calls: Literal[False] = False
|
||||
default_verbosity: NoneType = None
|
||||
apply_patch_tool_type: NoneType = None
|
||||
truncation_policy: CodexTruncationPolicy = CodexTruncationPolicy()
|
||||
experimental_supported_tools: tuple[()] = ()
|
||||
context_window: int | None
|
||||
base_instructions: str
|
||||
supports_search_tool: bool = False
|
||||
_CONFIGURED_TIERS: Final = TypeAdapter(tuple[TierId | _ConfiguredTier, ...])
|
||||
|
||||
|
||||
class CodexStockUpgrade(LiteLLMBaseModel):
|
||||
|
|
@ -156,6 +103,7 @@ class CodexStockModel(LiteLLMBaseModel):
|
|||
supported_in_api: bool = True
|
||||
upgrade: CodexStockUpgrade | None = None
|
||||
service_tiers: tuple[CodexServiceTier, ...] = ()
|
||||
additional_speed_tiers: tuple[str, ...] = ()
|
||||
default_service_tier: str | None = None
|
||||
supports_search_tool: bool = False
|
||||
supported_reasoning_levels: tuple[CodexReasoningLevel, ...] = ()
|
||||
|
|
@ -174,22 +122,26 @@ CodexAliasDefaults: TypeAlias = Literal["codex", "deployment"]
|
|||
@dataclass(frozen=True, slots=True)
|
||||
class CodexCatalogRow:
|
||||
"""What the catalog needs to know about one listed model: its public id, the listing's mode and
|
||||
input limit, the upstream model behind it, the reasoning effort its deployments pin, and the
|
||||
input limit, the upstream model behind it and the provider its deployment declares, the reasoning
|
||||
effort its deployments pin, and the
|
||||
`model_info` values the operator set; `service_tiers` holds one raw `model_info.service_tiers`
|
||||
value per deployment behind the id, None where a deployment sets none, and
|
||||
`configured_max_input_tokens` one `model_info.max_input_tokens` per deployment the same way.
|
||||
`supported_reasoning_efforts` is the model group's, None when unknown."""
|
||||
`deployment_capabilities` holds what each deployment's model info, merged with the cost map,
|
||||
declares. `supported_reasoning_efforts` is the model group's, None when unknown."""
|
||||
|
||||
id: str
|
||||
mode: str | None = None
|
||||
max_input_tokens: int | None = None
|
||||
upstream_model: str | None = None
|
||||
upstream_provider: str | None = None
|
||||
reasoning_effort: str | None = None
|
||||
display_name: str | None = None
|
||||
service_tiers: tuple[object, ...] = ()
|
||||
supported_reasoning_efforts: tuple[str, ...] | None = None
|
||||
search_tool_support: tuple[bool | None, ...] = ()
|
||||
configured_max_input_tokens: tuple[int | None, ...] = ()
|
||||
deployment_capabilities: tuple[CodexDeploymentCapabilities, ...] = ()
|
||||
|
||||
|
||||
@cache
|
||||
|
|
@ -292,44 +244,74 @@ def _codex_can_drive(model_id: str, mode: str | None) -> bool:
|
|||
return "*" not in model_id and (mode is None or mode in CODEX_CHAT_MODES)
|
||||
|
||||
|
||||
def _parses_custom_efforts(client_version: str | None) -> bool:
|
||||
try:
|
||||
return client_version is not None and Version(client_version) >= _FIRST_CODEX_WITH_CUSTOM_EFFORTS
|
||||
except ValueError:
|
||||
return False
|
||||
@cache
|
||||
def _provider_model_names() -> Mapping[str, Callable[[str], str]]:
|
||||
"""Imported on first use for the same reason as `tier_price_keys`."""
|
||||
from litellm.llms.bedrock.common_utils import bedrock_model_name_without_provider
|
||||
|
||||
|
||||
def codex_reasoning_levels(
|
||||
efforts: Sequence[str], client_version: str | None
|
||||
) -> tuple[tuple[CodexFallbackReasoningLevel, ...], str | None]:
|
||||
"""The reasoning levels a fallback entry offers for a model group's efforts, and its default level.
|
||||
|
||||
Codex before 0.138.0 fails the whole catalog on an effort it has no name for, so for it, and for
|
||||
a version that does not parse, only the efforts every release knows are offered. The default is
|
||||
set whenever a level is offered, because Codex reads a missing one as `none`: `none` itself when the
|
||||
group accepts it, so a user who picks no level sends what Codex sent before, else medium, else the
|
||||
nearest weaker declared level, so picking no level never costs more reasoning than Codex's usual medium,
|
||||
else the weakest declared level, else the first one."""
|
||||
parses_any: Final = _parses_custom_efforts(client_version)
|
||||
offered: Final = tuple(effort for effort in efforts if parses_any or effort in _EFFORTS_EVERY_CODEX_PARSES)
|
||||
if not offered:
|
||||
return (), None
|
||||
nearest: Final = nearest_declared_reasoning_effort(_PREFERRED_DEFAULT_EFFORT, offered, "down")
|
||||
levels: Final = tuple(
|
||||
CodexFallbackReasoningLevel(effort=effort, description=f"Sends reasoning_effort={effort} upstream")
|
||||
for effort in offered
|
||||
return MappingProxyType(
|
||||
{"bedrock": bedrock_model_name_without_provider, "bedrock_mantle": bedrock_model_name_without_provider}
|
||||
)
|
||||
if _CODEX_UNSET_EFFORT in offered:
|
||||
return levels, _CODEX_UNSET_EFFORT
|
||||
return levels, nearest if nearest in offered else offered[0]
|
||||
|
||||
|
||||
def _provider_model_name(upstream_model: str, upstream_provider: str | None) -> str:
|
||||
"""The upstream model without the prefixes its provider adds, the provider being the one the deployment
|
||||
declares in `custom_llm_provider`, else the model's `provider/` prefix."""
|
||||
from litellm.litellm_core_utils.get_llm_provider_logic import declared_provider
|
||||
|
||||
provider: Final = declared_provider(upstream_model, upstream_provider)
|
||||
if provider is None:
|
||||
return upstream_model
|
||||
model: Final = upstream_model.removeprefix(f"{provider}/")
|
||||
name_for: Final = _provider_model_names().get(provider)
|
||||
return model if name_for is None else name_for(model)
|
||||
|
||||
|
||||
def _stock_for(row: CodexCatalogRow, stock: Mapping[str, CodexStockModel]) -> CodexStockModel | None:
|
||||
listed: Final = stock.get(row.id)
|
||||
if listed is not None or row.upstream_model is None:
|
||||
return listed
|
||||
upstream: Final = stock.get(row.upstream_model)
|
||||
return upstream if upstream is not None else stock.get(row.upstream_model.partition("/")[2])
|
||||
"""Codex's entry for the row's id, else for its upstream model as written, else for that model's name
|
||||
once its provider's own prefixes are stripped (`bedrock/us.openai.gpt-6.1-sol` is `gpt-6.1-sol`)."""
|
||||
names: Final = (
|
||||
(row.id,)
|
||||
if row.upstream_model is None
|
||||
else (row.id, row.upstream_model, _provider_model_name(row.upstream_model, row.upstream_provider))
|
||||
)
|
||||
return next((stock[name] for name in names if name in stock), None)
|
||||
|
||||
|
||||
def _tier_fields(stock_model: CodexStockModel, tiers: tuple[CodexServiceTier, ...]) -> Mapping[str, object]:
|
||||
"""The tier fields of an entry offering `tiers`. Codex turns `/fast` on off a speed tier as much as off a
|
||||
service tier, so a speed tier stays only while a service tier billed the same way is still offered, and the
|
||||
default tier only while it is still offered."""
|
||||
from litellm.litellm_core_utils.llm_cost_calc.utils import get_service_tier_cost_key
|
||||
|
||||
offered: Final = frozenset(tier.id for tier in tiers)
|
||||
offered_prices: Final = frozenset(get_service_tier_cost_key(TOKEN_PRICE_KEY, tier) for tier in offered)
|
||||
speed_tiers: Final = tuple(
|
||||
tier
|
||||
for tier in stock_model.additional_speed_tiers
|
||||
if get_service_tier_cost_key(TOKEN_PRICE_KEY, tier) in offered_prices - {TOKEN_PRICE_KEY}
|
||||
)
|
||||
return MappingProxyType(
|
||||
{
|
||||
"service_tiers": tiers,
|
||||
**({"additional_speed_tiers": speed_tiers} if stock_model.additional_speed_tiers else {}),
|
||||
"default_service_tier": (
|
||||
stock_model.default_service_tier if stock_model.default_service_tier in offered else None
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def _priced_tiers(row: CodexCatalogRow, stock_model: CodexStockModel) -> Mapping[str, object]:
|
||||
"""Codex's own tiers for the model without the ones some deployment behind the row prices tokens but not
|
||||
that tier for, or nothing when none is dropped. Sending a tier the upstream does not sell fails the
|
||||
request, and the cost map prices a tier for exactly the models that sell it, while a deployment it has no
|
||||
token price for at all says nothing either way."""
|
||||
deployments: Final = row.deployment_capabilities
|
||||
tiers: Final = tuple(
|
||||
tier for tier in stock_model.service_tiers if no_priced_deployment_lacks_tier(tier.id, deployments)
|
||||
)
|
||||
return _NO_UPDATE if tiers == stock_model.service_tiers else _tier_fields(stock_model, tiers)
|
||||
|
||||
|
||||
def _alias_context_window(row: CodexCatalogRow, stock_model: CodexStockModel) -> Mapping[str, int]:
|
||||
|
|
@ -369,8 +351,7 @@ def _deployment_defaults(row: CodexCatalogRow, stock_model: CodexStockModel) ->
|
|||
none otherwise, and a window no larger than the alias's own limit."""
|
||||
return MappingProxyType(
|
||||
{
|
||||
"service_tiers": (),
|
||||
"default_service_tier": None,
|
||||
**_tier_fields(stock_model, ()),
|
||||
"default_verbosity": None,
|
||||
"upgrade": None,
|
||||
"availability_nux": None,
|
||||
|
|
@ -396,17 +377,16 @@ def _entry(
|
|||
configured: Final = configured_service_tiers(row.service_tiers, row.id, stock_tiers)
|
||||
search_tool: Final = offered_search_tool(row.search_tool_support)
|
||||
if stock_model is None:
|
||||
levels, default_level = codex_reasoning_levels(row.supported_reasoning_efforts or (), client_version)
|
||||
return CodexFallbackModel(
|
||||
return codex_fallback_entry(
|
||||
slug=row.id,
|
||||
display_name=row.display_name if row.display_name is not None else row.id,
|
||||
default_reasoning_level=default_level,
|
||||
supported_reasoning_levels=levels,
|
||||
priority=index,
|
||||
capabilities=combined_capabilities(row.deployment_capabilities, row.max_input_tokens),
|
||||
instructions=instructions,
|
||||
service_tiers=configured if configured is not None else (),
|
||||
context_window=row.max_input_tokens,
|
||||
base_instructions=instructions,
|
||||
supports_search_tool=search_tool is True,
|
||||
reasoning_efforts=row.supported_reasoning_efforts or (),
|
||||
client_version=client_version,
|
||||
)
|
||||
upgrade: Final = (
|
||||
stock_model.upgrade if stock_model.upgrade is not None and stock_model.upgrade.model in served else None
|
||||
|
|
@ -425,17 +405,8 @@ def _entry(
|
|||
**_configured_window(row, stock_model),
|
||||
}
|
||||
if configured is None:
|
||||
return stock_model.model_copy(update=listing)
|
||||
configured_ids: Final = frozenset(tier.id for tier in configured)
|
||||
return stock_model.model_copy(
|
||||
update={
|
||||
**listing,
|
||||
"service_tiers": configured,
|
||||
"default_service_tier": (
|
||||
stock_model.default_service_tier if stock_model.default_service_tier in configured_ids else None
|
||||
),
|
||||
}
|
||||
)
|
||||
return stock_model.model_copy(update={**_priced_tiers(row, stock_model), **listing})
|
||||
return stock_model.model_copy(update={**listing, **_tier_fields(stock_model, configured)})
|
||||
|
||||
|
||||
def _serialized(entry: CodexCatalogEntry) -> str:
|
||||
|
|
@ -503,8 +474,10 @@ def codex_models_response_json(
|
|||
`max_context_window`. A row listed under another name than the Codex model its `upstream_model` names (an alias) takes that model's
|
||||
entry; `alias_defaults="deployment"` drops the defaults Codex picks for that model in favor of
|
||||
what the alias's deployments pin, while `"codex"` keeps them, as Codex would use them for the
|
||||
model's own name. `client_version` is the requesting Codex's version, which decides the reasoning
|
||||
levels a fallback entry may offer; None counts as a version too old for custom levels.
|
||||
model's own name. Codex's own service tiers stay on a known model's entry only when no deployment
|
||||
behind the row prices tokens without pricing them, and only while the operator configures none. `client_version` is the
|
||||
requesting Codex's version, which decides the reasoning levels a fallback entry may offer; None counts
|
||||
as a version too old for custom levels.
|
||||
"""
|
||||
known: Final = bundled_codex_models() if stock is None else stock
|
||||
prompt: Final = codex_base_instructions() if instructions is None else instructions
|
||||
|
|
@ -531,6 +504,35 @@ def _group_reasoning_efforts(
|
|||
return group.supported_reasoning_efforts if group is not None else None
|
||||
|
||||
|
||||
def deployment_capabilities(model_info: Mapping[str, object] | None, model_id: str) -> CodexDeploymentCapabilities:
|
||||
"""One deployment's model info, merged with the cost map, as the capabilities a Codex entry describes.
|
||||
Model info that does not validate declares nothing, logged, so one bad value never fails the listing."""
|
||||
if model_info is None:
|
||||
return CodexDeploymentCapabilities()
|
||||
prices_tokens: Final = model_info.get(TOKEN_PRICE_KEY) is not None
|
||||
priced: Final = frozenset(key for key in tier_price_keys() if model_info.get(key) is not None)
|
||||
try:
|
||||
return CodexDeploymentCapabilities.model_validate(
|
||||
{**model_info, "prices_tokens": prices_tokens, "priced_service_tier_keys": priced}
|
||||
)
|
||||
except ValidationError as e:
|
||||
verbose_proxy_logger.warning(
|
||||
"model info of a deployment of %s declares no capability to Codex: %s", model_id, e.errors()[0]["msg"]
|
||||
)
|
||||
return CodexDeploymentCapabilities(prices_tokens=prices_tokens, priced_service_tier_keys=priced)
|
||||
|
||||
|
||||
def _deployment_capabilities(
|
||||
lookup_id: str, registry: RouterRegistry | None, team_id: str | None
|
||||
) -> tuple[CodexDeploymentCapabilities, ...]:
|
||||
if registry is None:
|
||||
return ()
|
||||
return tuple(
|
||||
deployment_capabilities(model_info, lookup_id)
|
||||
for model_info in registry.group(lookup_id, field="deployment_model_info", team_id=team_id)
|
||||
)
|
||||
|
||||
|
||||
def _catalog_row(
|
||||
row: ModelInfoResponse, lookup_id: str, label_id: str, llm_router: Router | None, team_id: str | None
|
||||
) -> CodexCatalogRow:
|
||||
|
|
@ -540,6 +542,9 @@ def _catalog_row(
|
|||
mode=row.get("mode"),
|
||||
max_input_tokens=row.get("max_input_tokens"),
|
||||
upstream_model=registry.resolve(lookup_id, kind="upstream", team_id=team_id) if registry is not None else None,
|
||||
upstream_provider=(
|
||||
registry.resolve(lookup_id, kind="upstream_provider", team_id=team_id) if registry is not None else None
|
||||
),
|
||||
display_name=registry.group(label_id, field="display_name") if registry is not None else None,
|
||||
service_tiers=registry.group(lookup_id, field="service_tiers", team_id=team_id) if registry is not None else (),
|
||||
supported_reasoning_efforts=_group_reasoning_efforts(row, lookup_id, registry, team_id),
|
||||
|
|
@ -549,6 +554,7 @@ def _catalog_row(
|
|||
configured_max_input_tokens=(
|
||||
registry.group(lookup_id, field="max_input_tokens", team_id=team_id) if registry is not None else ()
|
||||
),
|
||||
deployment_capabilities=_deployment_capabilities(lookup_id, registry, team_id),
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -562,8 +568,9 @@ def codex_catalog_rows(
|
|||
"""`rows` joined with the router's configured metadata, looked up by each entry's internal id so
|
||||
team-scoped rows resolve the way the Anthropic listing's display names do, a `model_group_alias`
|
||||
reading its target's deployments under its own name; `team_id` is the requesting key's team, so the
|
||||
upstream model that picks Codex's stock entry, the tiers, and the configured input limits are read
|
||||
only off the deployments its requests can route to, and the reasoning efforts are the ones
|
||||
upstream model that picks Codex's stock entry, the tiers, the configured input limits and each
|
||||
deployment's capabilities are read only off the deployments its requests can route to, and the
|
||||
reasoning efforts are the ones
|
||||
`/model_group/info` reports for the entry. Display names are read under `label_entries`' ids when given."""
|
||||
lookup_ids: Final = MappingProxyType(dict(entries))
|
||||
label_ids: Final = lookup_ids if label_entries is None else MappingProxyType(dict(label_entries))
|
||||
|
|
|
|||
250
litellm/proxy/common_utils/codex_model_translation.py
Normal file
250
litellm/proxy/common_utils/codex_model_translation.py
Normal file
|
|
@ -0,0 +1,250 @@
|
|||
"""Codex's `ModelInfo` entry for a model Codex does not know, translated from what LiteLLM knows about the
|
||||
deployments behind it.
|
||||
|
||||
Field names, types and enum values follow `codex-rs/protocol/src/openai_models.rs` at rust-v0.166.0, and every
|
||||
field a release back to 0.140.0 decodes without a default is spelled out, so one body parses on all of them.
|
||||
Codex rejects the whole catalog when one entry fails to decode, so only enum values every one of those
|
||||
releases knows are sent.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Iterable, Sequence
|
||||
from dataclasses import dataclass
|
||||
from functools import cache
|
||||
from types import NoneType
|
||||
from typing import Annotated, Final, Literal, TypeAlias
|
||||
|
||||
from packaging.version import Version
|
||||
from pydantic import ConfigDict, Field, StringConstraints
|
||||
|
||||
from litellm.litellm_core_utils.reasoning_effort_strength import nearest_declared_reasoning_effort
|
||||
from litellm.types.llms.base_model import LiteLLMBaseModel
|
||||
|
||||
TierId: TypeAlias = Annotated[str, StringConstraints(strip_whitespace=True, min_length=1)]
|
||||
CodexInputModality: TypeAlias = Literal["text", "image"]
|
||||
|
||||
_TEXT_ONLY: Final[tuple[CodexInputModality, ...]] = ("text",)
|
||||
_TEXT_AND_IMAGE: Final[tuple[CodexInputModality, ...]] = ("text", "image")
|
||||
TOKEN_PRICE_KEY: Final = "input_cost_per_token"
|
||||
_FIRST_CODEX_WITH_CUSTOM_EFFORTS: Final = Version("0.138.0")
|
||||
_EFFORTS_EVERY_CODEX_PARSES: Final = frozenset({"none", "minimal", "low", "medium", "high", "xhigh"})
|
||||
_PREFERRED_DEFAULT_EFFORT: Final = "medium"
|
||||
_CODEX_UNSET_EFFORT: Final = "none"
|
||||
|
||||
|
||||
def _unset(value: object) -> bool:
|
||||
return value is None
|
||||
|
||||
|
||||
class CodexServiceTier(LiteLLMBaseModel):
|
||||
"""A `ModelServiceTier` as Codex reads it: the slash command is `name` lowercased, and toggling
|
||||
it sends `id` as the request's `service_tier`."""
|
||||
|
||||
model_config = ConfigDict(frozen=True)
|
||||
|
||||
id: TierId
|
||||
name: str
|
||||
description: str
|
||||
|
||||
|
||||
class CodexFallbackReasoningLevel(LiteLLMBaseModel):
|
||||
"""A `ReasoningEffortPreset` as Codex reads it: picking it sends `effort` as the request's reasoning effort."""
|
||||
|
||||
model_config = ConfigDict(frozen=True)
|
||||
|
||||
effort: str
|
||||
description: str
|
||||
|
||||
|
||||
class CodexTruncationPolicy(LiteLLMBaseModel):
|
||||
"""How Codex truncates a long tool output before sending it back, the policy its own entries use."""
|
||||
|
||||
model_config = ConfigDict(frozen=True)
|
||||
|
||||
mode: Literal["tokens"] = "tokens"
|
||||
limit: int = 10_000
|
||||
|
||||
|
||||
class CodexFallbackModel(LiteLLMBaseModel):
|
||||
"""One `ModelInfo` entry of a Codex model catalog for a model Codex does not know.
|
||||
|
||||
`supports_reasoning_summaries` and `supports_parallel_tool_calls` are read only by Codex before 0.150.0,
|
||||
which requires them, and `base_instructions` is the prompt field every release still decodes.
|
||||
"""
|
||||
|
||||
model_config = ConfigDict(frozen=True)
|
||||
|
||||
slug: str
|
||||
display_name: str
|
||||
description: NoneType = None
|
||||
default_reasoning_level: str | None = Field(default=None, exclude_if=_unset)
|
||||
supported_reasoning_levels: tuple[CodexFallbackReasoningLevel, ...] = ()
|
||||
shell_type: Literal["unified_exec"] = "unified_exec"
|
||||
visibility: Literal["list"] = "list"
|
||||
supported_in_api: Literal[True] = True
|
||||
priority: int
|
||||
service_tiers: tuple[CodexServiceTier, ...] = ()
|
||||
availability_nux: NoneType = None
|
||||
upgrade: NoneType = None
|
||||
support_verbosity: Literal[False] = False
|
||||
supports_reasoning_summary_parameter: bool | None = Field(default=None, exclude_if=_unset)
|
||||
supports_reasoning_summaries: bool = False
|
||||
supports_parallel_tool_calls: bool = False
|
||||
default_verbosity: NoneType = None
|
||||
apply_patch_tool_type: Literal["freeform"] | None = None
|
||||
truncation_policy: CodexTruncationPolicy = CodexTruncationPolicy()
|
||||
experimental_supported_tools: tuple[()] = ()
|
||||
input_modalities: tuple[CodexInputModality, ...] = _TEXT_AND_IMAGE
|
||||
context_window: int | None
|
||||
base_instructions: str
|
||||
supports_search_tool: bool = False
|
||||
use_responses_lite: Literal[False] = False
|
||||
|
||||
|
||||
class CodexDeploymentCapabilities(LiteLLMBaseModel):
|
||||
"""What one deployment's model info says about the capabilities a Codex entry describes, None where
|
||||
it says nothing, whether it has a token price at all, and the input price keys of the service tiers it
|
||||
has a price for."""
|
||||
|
||||
model_config = ConfigDict(frozen=True, extra="ignore")
|
||||
|
||||
max_input_tokens: int | None = None
|
||||
supports_vision: bool | None = None
|
||||
supports_function_calling: bool | None = None
|
||||
supports_parallel_function_calling: bool | None = None
|
||||
supports_reasoning: bool | None = None
|
||||
prices_tokens: bool = False
|
||||
priced_service_tier_keys: frozenset[str] = frozenset()
|
||||
|
||||
|
||||
@cache
|
||||
def tier_price_keys() -> tuple[str, ...]:
|
||||
"""The input price key of every service tier cost tracking prices. Cost tracking is imported on first use
|
||||
because `lite codex` imports this module before the SDK is initialized, and cost tracking imports the rest
|
||||
of the SDK."""
|
||||
from litellm.litellm_core_utils.llm_cost_calc.utils import SERVICE_TIER_COST_KEY_SUFFIXES
|
||||
|
||||
return tuple(f"{TOKEN_PRICE_KEY}{suffix}" for suffix in SERVICE_TIER_COST_KEY_SUFFIXES)
|
||||
|
||||
|
||||
def no_priced_deployment_lacks_tier(tier_id: str, deployments: Sequence[CodexDeploymentCapabilities]) -> bool:
|
||||
"""Whether every deployment that has a token price also has one for `tier_id`, the way cost tracking
|
||||
prices a request sent at that tier. A deployment with no token price at all (a subscription upstream,
|
||||
a deployment name the cost map does not know) says nothing about tiers, so it does not vote, and a tier
|
||||
cost tracking has no price key for is lacked by every deployment that prices tokens."""
|
||||
from litellm.litellm_core_utils.llm_cost_calc.utils import get_service_tier_cost_key
|
||||
|
||||
price_key: Final = get_service_tier_cost_key(TOKEN_PRICE_KEY, tier_id)
|
||||
return all(
|
||||
price_key in deployment.priced_service_tier_keys for deployment in deployments if deployment.prices_tokens
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class CodexCapabilities:
|
||||
"""What a request to a listed name can count on whichever deployment serves it: a capability is False
|
||||
when one deployment lacks it, True when one has it and none lacks it, None when none says, and the
|
||||
context window is the smallest one any deployment declares."""
|
||||
|
||||
context_window: int | None = None
|
||||
supports_vision: bool | None = None
|
||||
supports_function_calling: bool | None = None
|
||||
supports_parallel_function_calling: bool | None = None
|
||||
supports_reasoning: bool | None = None
|
||||
|
||||
|
||||
def _combined(flags: Iterable[bool | None]) -> bool | None:
|
||||
said: Final = frozenset(flag for flag in flags if flag is not None)
|
||||
return None if not said else False not in said
|
||||
|
||||
|
||||
def combined_capabilities(
|
||||
deployments: Sequence[CodexDeploymentCapabilities], listed_context_window: int | None = None
|
||||
) -> CodexCapabilities:
|
||||
"""The capabilities every one of `deployments` offers, the window falling back to
|
||||
`listed_context_window` when no deployment declares one."""
|
||||
windows: Final = tuple(
|
||||
deployment.max_input_tokens
|
||||
for deployment in deployments
|
||||
if deployment.max_input_tokens is not None and deployment.max_input_tokens > 0
|
||||
)
|
||||
return CodexCapabilities(
|
||||
context_window=min(windows) if windows else listed_context_window,
|
||||
supports_vision=_combined(deployment.supports_vision for deployment in deployments),
|
||||
supports_function_calling=_combined(deployment.supports_function_calling for deployment in deployments),
|
||||
supports_parallel_function_calling=_combined(
|
||||
deployment.supports_parallel_function_calling for deployment in deployments
|
||||
),
|
||||
supports_reasoning=_combined(deployment.supports_reasoning for deployment in deployments),
|
||||
)
|
||||
|
||||
|
||||
def _parses_custom_efforts(client_version: str | None) -> bool:
|
||||
try:
|
||||
return client_version is not None and Version(client_version) >= _FIRST_CODEX_WITH_CUSTOM_EFFORTS
|
||||
except ValueError:
|
||||
return False
|
||||
|
||||
|
||||
def codex_reasoning_levels(
|
||||
efforts: Sequence[str], client_version: str | None
|
||||
) -> tuple[tuple[CodexFallbackReasoningLevel, ...], str | None]:
|
||||
"""The reasoning levels a fallback entry offers for a model group's efforts, and its default level.
|
||||
|
||||
Codex before 0.138.0 fails the whole catalog on an effort it has no name for, so for it, and for
|
||||
a version that does not parse, only the efforts every release knows are offered. The default is
|
||||
set whenever a level is offered, because Codex reads a missing one as `none`: `none` itself when the
|
||||
group accepts it, so a user who picks no level sends what Codex sent before, else medium, else the
|
||||
nearest weaker declared level, so picking no level never costs more reasoning than Codex's usual medium,
|
||||
else the weakest declared level, else the first one."""
|
||||
parses_any: Final = _parses_custom_efforts(client_version)
|
||||
offered: Final = tuple(effort for effort in efforts if parses_any or effort in _EFFORTS_EVERY_CODEX_PARSES)
|
||||
if not offered:
|
||||
return (), None
|
||||
nearest: Final = nearest_declared_reasoning_effort(_PREFERRED_DEFAULT_EFFORT, offered, "down")
|
||||
levels: Final = tuple(
|
||||
CodexFallbackReasoningLevel(effort=effort, description=f"Sends reasoning_effort={effort} upstream")
|
||||
for effort in offered
|
||||
)
|
||||
if _CODEX_UNSET_EFFORT in offered:
|
||||
return levels, _CODEX_UNSET_EFFORT
|
||||
return levels, nearest if nearest in offered else offered[0]
|
||||
|
||||
|
||||
def codex_fallback_entry(
|
||||
*,
|
||||
slug: str,
|
||||
display_name: str,
|
||||
priority: int,
|
||||
capabilities: CodexCapabilities,
|
||||
instructions: str,
|
||||
service_tiers: tuple[CodexServiceTier, ...] = (),
|
||||
supports_search_tool: bool = False,
|
||||
reasoning_efforts: Sequence[str] = (),
|
||||
client_version: str | None = None,
|
||||
) -> CodexFallbackModel:
|
||||
"""Codex's entry for a model it does not know, from the capabilities its deployments share.
|
||||
|
||||
A capability no deployment declares keeps Codex's own default for an unknown model, except the freeform
|
||||
`apply_patch` tool, which goes out only to a model that calls tools: `supports_reasoning_summary_parameter`
|
||||
is left out, which Codex reads as true, and the fields only Codex before 0.150.0 reads keep the false
|
||||
it requires.
|
||||
"""
|
||||
levels, default_level = codex_reasoning_levels(reasoning_efforts, client_version)
|
||||
return CodexFallbackModel(
|
||||
slug=slug,
|
||||
display_name=display_name,
|
||||
default_reasoning_level=default_level,
|
||||
supported_reasoning_levels=levels,
|
||||
priority=priority,
|
||||
service_tiers=service_tiers,
|
||||
supports_reasoning_summary_parameter=capabilities.supports_reasoning,
|
||||
supports_reasoning_summaries=capabilities.supports_reasoning is True,
|
||||
supports_parallel_tool_calls=capabilities.supports_parallel_function_calling is True,
|
||||
apply_patch_tool_type="freeform" if capabilities.supports_function_calling is True else None,
|
||||
input_modalities=_TEXT_ONLY if capabilities.supports_vision is False else _TEXT_AND_IMAGE,
|
||||
context_window=capabilities.context_window,
|
||||
base_instructions=instructions,
|
||||
supports_search_tool=supports_search_tool,
|
||||
)
|
||||
|
|
@ -143,6 +143,7 @@ ResolveKind: TypeAlias = Literal[
|
|||
"team_model",
|
||||
"recognized",
|
||||
"upstream",
|
||||
"upstream_provider",
|
||||
"group_of_id",
|
||||
"wildcard_access_group",
|
||||
"team_model_alternatives",
|
||||
|
|
@ -156,6 +157,7 @@ GroupField: TypeAlias = Literal[
|
|||
"mode",
|
||||
"service_tiers",
|
||||
"max_input_tokens",
|
||||
"deployment_model_info",
|
||||
"token_limits",
|
||||
"listing_info",
|
||||
]
|
||||
|
|
@ -684,7 +686,7 @@ class RouterRegistry:
|
|||
self,
|
||||
name: str,
|
||||
*,
|
||||
kind: Literal["alias", "upstream"],
|
||||
kind: Literal["alias", "upstream", "upstream_provider"],
|
||||
team_id: str | None = None,
|
||||
custom_llm_provider: str | None = None,
|
||||
served_model: str | None = None,
|
||||
|
|
@ -764,6 +766,8 @@ class RouterRegistry:
|
|||
- ``team_model``: a team's public name to its group (was ``map_team_model``; ``team_id`` required)
|
||||
- ``recognized``: whether the router knows the name (was ``is_recognized_model``)
|
||||
- ``upstream``: the upstream model a name routes to (was ``get_routable_upstream_model``)
|
||||
- ``upstream_provider``: the provider the deployment ``upstream`` reads declares, by its
|
||||
``custom_llm_provider`` or its model's ``provider/`` prefix
|
||||
- ``group_of_id``: the group of a deployment id (was ``get_model_group``)
|
||||
- ``wildcard_access_group``: whether an access group grants a wildcard route
|
||||
(was ``is_model_access_group_for_wildcard_route``)
|
||||
|
|
@ -793,10 +797,8 @@ class RouterRegistry:
|
|||
return name if served_model is None else r._core_resolve_served_model_name(name, served_model)
|
||||
if kind == "recognized":
|
||||
return r._core_is_recognized_model(name)
|
||||
if kind == "upstream":
|
||||
return r._core_get_routable_upstream_model(name, team_id=team_id)
|
||||
if kind == "search_tool_support":
|
||||
return r._core_get_search_tool_support(name, team_id=team_id)
|
||||
if kind in ("upstream", "upstream_provider", "search_tool_support"):
|
||||
return self._resolve_routable(name, kind, team_id)
|
||||
if kind == "group_of_id":
|
||||
return r._core_get_model_group(id=name)
|
||||
if kind == "wildcard_access_group":
|
||||
|
|
@ -805,6 +807,19 @@ class RouterRegistry:
|
|||
return r._core_team_model_has_alternatives(name)
|
||||
raise ValueError(f"unknown resolve kind {kind!r}")
|
||||
|
||||
def _resolve_routable(
|
||||
self,
|
||||
name: str,
|
||||
kind: Literal["upstream", "upstream_provider", "search_tool_support"],
|
||||
team_id: str | None,
|
||||
) -> str | tuple[bool | None, ...] | None:
|
||||
r: Final = self._router
|
||||
if kind == "upstream":
|
||||
return r._core_get_routable_upstream_model(name, team_id=team_id)
|
||||
if kind == "upstream_provider":
|
||||
return r._core_get_routable_upstream_provider(name, team_id=team_id)
|
||||
return r._core_get_search_tool_support(name, team_id=team_id)
|
||||
|
||||
def aliases(
|
||||
self,
|
||||
) -> "dict[str, str | RouterModelGroupAliasItem]": # mutable-ok: the model_group_alias attribute, the live dict
|
||||
|
|
@ -866,6 +881,16 @@ class RouterRegistry:
|
|||
team_id: str | None = None,
|
||||
) -> tuple[int | None, ...]: ...
|
||||
|
||||
@overload
|
||||
def group(
|
||||
self,
|
||||
name: str,
|
||||
*,
|
||||
field: Literal["deployment_model_info"],
|
||||
include_hidden: bool = False,
|
||||
team_id: str | None = None,
|
||||
) -> "tuple[ModelMapInfo | None, ...]": ...
|
||||
|
||||
@overload
|
||||
def group(
|
||||
self,
|
||||
|
|
@ -904,6 +929,7 @@ class RouterRegistry:
|
|||
``get_configured_token_limits``, ``get_model_listing_info``)
|
||||
- ``service_tiers``: (was ``get_configured_service_tiers``)
|
||||
- ``max_input_tokens``: each routable deployment's own ``model_info.max_input_tokens``
|
||||
- ``deployment_model_info``: each routable deployment's model info, merged with the pricing map
|
||||
"""
|
||||
r: Final = self._router
|
||||
if field == "info":
|
||||
|
|
@ -918,6 +944,8 @@ class RouterRegistry:
|
|||
return r._core_get_configured_service_tiers(name, team_id=team_id)
|
||||
if field == "max_input_tokens":
|
||||
return r._core_get_configured_max_input_tokens(name, team_id=team_id)
|
||||
if field == "deployment_model_info":
|
||||
return r._core_get_routable_deployment_model_info(name, team_id=team_id)
|
||||
if field == "token_limits":
|
||||
return r._core_get_configured_token_limits(name)
|
||||
if field == "listing_info":
|
||||
|
|
|
|||
|
|
@ -139,6 +139,7 @@ from litellm.litellm_core_utils.deployment_pricing import deployment_pricing_mod
|
|||
from litellm.litellm_core_utils.get_litellm_params import SUPPLEMENTAL_KWARGS_KEYS
|
||||
from litellm.litellm_core_utils.get_llm_provider_logic import (
|
||||
declared_authenticating_provider,
|
||||
declared_provider,
|
||||
inferred_provider,
|
||||
is_registered_custom_provider,
|
||||
resolve_model_and_provider_for_metadata,
|
||||
|
|
@ -789,6 +790,16 @@ def _operator_max_input_tokens(deployment: Mapping[str, object]) -> int | None:
|
|||
return coerce_token_limit(model_info.get("max_input_tokens"))
|
||||
|
||||
|
||||
def _deployment_params(deployment: DeploymentTypedDict) -> Mapping[str, object]:
|
||||
return deployment["litellm_params"]
|
||||
|
||||
|
||||
def _model_info_key(deployment: DeploymentTypedDict) -> tuple[str, str] | None:
|
||||
deployment_id: Final = _configured_model_info(deployment).get("id")
|
||||
model: Final = _deployment_params(deployment).get("model")
|
||||
return (deployment_id, model) if isinstance(deployment_id, str) and isinstance(model, str) else None
|
||||
|
||||
|
||||
def _search_tool_support(deployment: DeploymentTypedDict) -> bool | None:
|
||||
from litellm.responses.litellm_completion_transformation.tool_search import chat_config_builds_anthropic_request
|
||||
|
||||
|
|
@ -12773,6 +12784,24 @@ class PythonCore(DeprecatedRouterMembers):
|
|||
"""
|
||||
return tuple(_search_tool_support(deployment) for deployment in self._routable_deployments(model_name, team_id))
|
||||
|
||||
def _core_get_routable_deployment_model_info(
|
||||
self, model_name: str, team_id: str | None = None
|
||||
) -> tuple[ModelMapInfo | None, ...]:
|
||||
"""
|
||||
The model info of each deployment a request from team_id to model_name
|
||||
can route to (see _routable_deployments), read one deployment at a time
|
||||
the way cost tracking reads it: the cost map entry of its upstream model
|
||||
merged with its own model_info, None where neither is known. Unlike
|
||||
get_model_group_info nothing is merged across deployments, so a caller
|
||||
can tell a deployment that lacks a capability from one that says nothing.
|
||||
|
||||
Returns an empty tuple for wildcard-expanded or unknown names.
|
||||
"""
|
||||
keys: Final = tuple(
|
||||
_model_info_key(deployment) for deployment in self._routable_deployments(model_name, team_id)
|
||||
)
|
||||
return tuple(None if key is None else self._core_cached_deployment_model_info(*key) for key in keys)
|
||||
|
||||
def _core_get_routable_upstream_model(self, model_name: str, team_id: str | None = None) -> str | None:
|
||||
"""
|
||||
Return the `litellm_params.model` of the first deployment a request
|
||||
|
|
@ -12784,6 +12813,25 @@ class PythonCore(DeprecatedRouterMembers):
|
|||
deployment: Final = next(iter(self._routable_deployments(model_name, team_id)), None)
|
||||
return deployment["litellm_params"].get("model") if deployment is not None else None
|
||||
|
||||
def _core_get_routable_upstream_provider(self, model_name: str, team_id: str | None = None) -> str | None:
|
||||
"""
|
||||
Return the provider the deployment _core_get_routable_upstream_model
|
||||
reads declares: its `litellm_params.custom_llm_provider`, else the
|
||||
`provider/` prefix of its `litellm_params.model`.
|
||||
|
||||
Returns None when that deployment declares neither, and wherever
|
||||
_core_get_routable_upstream_model returns None.
|
||||
"""
|
||||
deployment: Final = next(iter(self._routable_deployments(model_name, team_id)), None)
|
||||
if deployment is None:
|
||||
return None
|
||||
params: Final = _deployment_params(deployment)
|
||||
model: Final = params.get("model")
|
||||
provider: Final = params.get("custom_llm_provider")
|
||||
return declared_provider(
|
||||
model if isinstance(model, str) else None, provider if isinstance(provider, str) else None
|
||||
)
|
||||
|
||||
def _core_get_credential_deployment(self, model_id: str, team_id: str | None = None) -> Deployment | None:
|
||||
"""
|
||||
The deployment a passthrough endpoint (files, batches, etc.) resolves for a
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ include = [
|
|||
"litellm/router_strategy/complexity_router/fuse_presets.json",
|
||||
"litellm/proxy/model_insights_tasks.json",
|
||||
"litellm/proxy/common_utils/codex_base_instructions.md",
|
||||
"litellm/proxy/common_utils/codex_bundled_models_0.159.3.json",
|
||||
"litellm/proxy/common_utils/codex_bundled_models_0.166.0.json",
|
||||
]
|
||||
exclude = [
|
||||
"litellm/proxy/_experimental/out",
|
||||
|
|
|
|||
|
|
@ -334,7 +334,7 @@ include = [
|
|||
"litellm/proxy/model_insights_tasks.json",
|
||||
"litellm/proxy/public_endpoints/whats_new.json",
|
||||
"litellm/proxy/common_utils/codex_base_instructions.md",
|
||||
"litellm/proxy/common_utils/codex_bundled_models_0.159.3.json",
|
||||
"litellm/proxy/common_utils/codex_bundled_models_0.166.0.json",
|
||||
{ path = "model_prices_and_context_window.json", format = "sdist" },
|
||||
]
|
||||
exclude = [
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ RELOAD_MARGIN_SECONDS: Final = 5.0
|
|||
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 + RELOAD_MARGIN_SECONDS
|
||||
ROOT: Final = Path(os.environ.get("INTEGRATION_PROXY_ROOT") or Path(__file__).resolve().parents[3])
|
||||
BUNDLED_STOCK_PATH: Final = ROOT / "litellm" / "proxy" / "common_utils" / "codex_bundled_models_0.159.3.json"
|
||||
BUNDLED_STOCK_PATH: Final = ROOT / "litellm" / "proxy" / "common_utils" / "codex_bundled_models_0.166.0.json"
|
||||
BASE_INSTRUCTIONS_PATH: Final = ROOT / "litellm" / "proxy" / "common_utils" / "codex_base_instructions.md"
|
||||
FRESH_CONNECTION: Final = {"Connection": "close"}
|
||||
ANTHROPIC_HEADERS: Final = {"anthropic-version": "2023-06-01"}
|
||||
|
|
|
|||
|
|
@ -130,7 +130,7 @@ def test_yaml_tiers_and_alias_in_owned_proxy(gateway: Gateway, tmp_path: Path) -
|
|||
|
||||
|
||||
ROOT: Final = Path(os.environ.get("INTEGRATION_PROXY_ROOT") or Path(__file__).resolve().parents[3])
|
||||
BUNDLED_STOCK_PATH: Final = ROOT / "litellm" / "proxy" / "common_utils" / "codex_bundled_models_0.159.3.json"
|
||||
BUNDLED_STOCK_PATH: Final = ROOT / "litellm" / "proxy" / "common_utils" / "codex_bundled_models_0.166.0.json"
|
||||
STOCK_UPSTREAM: Final = "gpt-5.5"
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ from litellm.llms.bedrock.common_utils import(
|
|||
NativeOutputFallback,
|
||||
bedrock_native_output_is_opt_in,
|
||||
bedrock_strict_tools_fallback,
|
||||
bedrock_model_name_without_provider,
|
||||
deployment_model_info,
|
||||
extract_model_name_from_bedrock_arn,
|
||||
get_bedrock_base_model,
|
||||
|
|
@ -304,6 +305,26 @@ def test_legacy_mantle_route_prefix_stripped_for_cost_lookup():
|
|||
assert BedrockModelInfo.get_bedrock_route("bedrock/mantle/anthropic.claude-sonnet-5") == "mantle"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("model", "expected"),
|
||||
[
|
||||
("bedrock/us.openai.gpt-6.1-sol", "gpt-6.1-sol"),
|
||||
("global.openai.gpt-6.1-sol", "gpt-6.1-sol"),
|
||||
("bedrock/converse/eu.anthropic.claude-sonnet-5-5", "claude-sonnet-5-5"),
|
||||
("us-east-1/openai.gpt-6.1-sol", "gpt-6.1-sol"),
|
||||
("openai.gpt-5.5", "gpt-5.5"),
|
||||
("arn:aws:bedrock:us-east-1:123456789012:inference-profile/us.meta.llama4-scout-17b", "llama4-scout-17b"),
|
||||
("us.deepseek.r1-v1:0", "r1-v1:0"),
|
||||
("moonshotai.kimi-k2-thinking", "kimi-k2-thinking"),
|
||||
("bedrock/qwen.qwen3-32b-v1:0", "qwen3-32b-v1:0"),
|
||||
("gpt-5.5", "gpt-5.5"),
|
||||
("arn:aws:bedrock:us-east-1:123456789012:imported-model/abc123", "abc123"),
|
||||
],
|
||||
)
|
||||
def test_bedrock_model_name_without_provider_strips_routing_region_and_provider_prefixes_only(model, expected):
|
||||
assert bedrock_model_name_without_provider(model) == expected
|
||||
|
||||
|
||||
def test_output_config_effort_normalization_uses_model_info_ceiling(monkeypatch):
|
||||
import litellm.llms.bedrock.common_utils as mod
|
||||
|
||||
|
|
|
|||
|
|
@ -1037,10 +1037,11 @@ class TestCodexModelSync:
|
|||
assert entry["supported_in_api"] is True
|
||||
assert entry["shell_type"] == "unified_exec"
|
||||
assert entry["supported_reasoning_levels"] == []
|
||||
assert entry["truncation_policy"] == {"mode": "bytes", "limit": 10000}
|
||||
assert entry["truncation_policy"] == {"mode": "tokens", "limit": 10000}
|
||||
assert entry["experimental_supported_tools"] == []
|
||||
assert entry["support_verbosity"] is False
|
||||
assert entry["supports_reasoning_summaries"] is False
|
||||
assert "supports_reasoning_summary_parameter" not in entry
|
||||
assert entry["supports_parallel_tool_calls"] is False
|
||||
for nullable in ("description", "availability_nux", "upgrade", "default_verbosity", "apply_patch_tool_type"):
|
||||
assert nullable in entry and entry[nullable] is None
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import json
|
||||
from collections.abc import Mapping
|
||||
from dataclasses import replace
|
||||
from types import MappingProxyType
|
||||
from typing import Final, Literal
|
||||
|
||||
|
|
@ -7,10 +8,10 @@ import pytest
|
|||
|
||||
import litellm
|
||||
from litellm.litellm_core_utils.get_model_cost_map import GetModelCostMap
|
||||
from litellm.litellm_core_utils.llm_cost_calc.utils import get_service_tier_cost_key
|
||||
from litellm.proxy.common_utils.codex_model_catalog import (
|
||||
CODEX_CATALOG_BYTE_LIMIT,
|
||||
CodexCatalogRow,
|
||||
CodexServiceTier,
|
||||
CodexStockModel,
|
||||
CodexStockUpgrade,
|
||||
bundled_codex_models,
|
||||
|
|
@ -18,13 +19,17 @@ from litellm.proxy.common_utils.codex_model_catalog import (
|
|||
codex_model_list_body,
|
||||
codex_models_response_json,
|
||||
configured_service_tiers,
|
||||
deployment_capabilities,
|
||||
)
|
||||
from litellm.proxy.common_utils.codex_model_translation import CodexDeploymentCapabilities, CodexServiceTier
|
||||
|
||||
_PROMPT = "base prompt"
|
||||
_FAST = {"id": "priority", "name": "Fast", "description": "2x speed, increased usage"}
|
||||
|
||||
|
||||
def _stock(slug, *, visibility="list", supported_in_api=True, upgrade=None, service_tiers=(_FAST,), default_tier=None):
|
||||
def _stock(
|
||||
slug, *, visibility="list", supported_in_api=True, upgrade=None, service_tiers=(_FAST,), default_tier=None
|
||||
):
|
||||
return CodexStockModel(
|
||||
slug=slug,
|
||||
display_name=slug.upper(),
|
||||
|
|
@ -33,6 +38,7 @@ def _stock(slug, *, visibility="list", supported_in_api=True, upgrade=None, serv
|
|||
supported_in_api=supported_in_api,
|
||||
upgrade=CodexStockUpgrade(model=upgrade) if upgrade else None,
|
||||
service_tiers=tuple(CodexServiceTier(**tier) for tier in service_tiers),
|
||||
additional_speed_tiers=("fast",) if service_tiers else (),
|
||||
default_service_tier=default_tier,
|
||||
model_messages={"instructions_template": f"{slug} prompt"},
|
||||
)
|
||||
|
|
@ -84,29 +90,10 @@ def test_body_is_exactly_codex_models_response():
|
|||
assert [entry["slug"] for entry in body["models"]] == ["gpt-6-astra"]
|
||||
|
||||
|
||||
def test_unknown_model_entry_carries_every_field_codex_deserializes_without_a_default():
|
||||
"""Codex 0.159.3 `codex-rs/protocol/src/openai_models.rs` ModelInfo, the fields with no
|
||||
`#[serde(default)]`, read on 2026-10-01; `base_instructions` because its `ModelsResponse`
|
||||
decoder rejects an entry with neither it nor `model_messages.instructions_template`."""
|
||||
def test_unknown_model_entry_takes_its_slug_window_and_prompt_from_the_row():
|
||||
"""The fields every Codex release decodes are pinned in test_codex_model_translation.py."""
|
||||
(entry,) = _models(_row("gpt-6-astra"))
|
||||
|
||||
assert entry.keys() >= {
|
||||
"slug",
|
||||
"display_name",
|
||||
"description",
|
||||
"supported_reasoning_levels",
|
||||
"shell_type",
|
||||
"visibility",
|
||||
"supported_in_api",
|
||||
"priority",
|
||||
"availability_nux",
|
||||
"upgrade",
|
||||
"support_verbosity",
|
||||
"default_verbosity",
|
||||
"apply_patch_tool_type",
|
||||
"truncation_policy",
|
||||
"experimental_supported_tools",
|
||||
}
|
||||
assert (entry["slug"], entry["display_name"], entry["visibility"], entry["supported_in_api"]) == (
|
||||
"gpt-6-astra",
|
||||
"gpt-6-astra",
|
||||
|
|
@ -116,25 +103,14 @@ def test_unknown_model_entry_carries_every_field_codex_deserializes_without_a_de
|
|||
assert (entry["context_window"], entry["base_instructions"], entry["service_tiers"]) == (272000, _PROMPT, [])
|
||||
|
||||
|
||||
_NO_REASONING_BODY = (
|
||||
'{"models":[{"slug":"gpt-6-astra","display_name":"gpt-6-astra","description":null,'
|
||||
'"supported_reasoning_levels":[],"shell_type":"unified_exec","visibility":"list","supported_in_api":true,'
|
||||
'"priority":0,"service_tiers":[],"availability_nux":null,"upgrade":null,"support_verbosity":false,'
|
||||
'"supports_reasoning_summaries":false,"supports_parallel_tool_calls":false,"default_verbosity":null,'
|
||||
'"apply_patch_tool_type":null,"truncation_policy":{"mode":"bytes","limit":10000},'
|
||||
'"experimental_supported_tools":[],"context_window":272000,"base_instructions":"base prompt",'
|
||||
'"supports_search_tool":false}]}'
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("efforts", [None, ()], ids=["unknown", "non-reasoning"])
|
||||
@pytest.mark.parametrize("client_version", ["0.165.0", "0.120.0", None])
|
||||
def test_unknown_or_no_reasoning_efforts_keep_the_fallback_entry_byte_for_byte(efforts, client_version):
|
||||
"""The body an unknown model got before reasoning efforts reached the catalog."""
|
||||
assert (
|
||||
_body(_row("gpt-6-astra", supported_reasoning_efforts=efforts), client_version=client_version).json
|
||||
== _NO_REASONING_BODY
|
||||
)
|
||||
def test_unknown_or_no_reasoning_efforts_offer_no_reasoning_level(efforts, client_version):
|
||||
"""Codex reads a missing default level as `none`, so an entry offering no level sends no effort."""
|
||||
(entry,) = _models(_row("mystery", supported_reasoning_efforts=efforts), client_version=client_version)
|
||||
|
||||
assert entry["supported_reasoning_levels"] == []
|
||||
assert "default_reasoning_level" not in entry
|
||||
|
||||
|
||||
_ABSENT = object()
|
||||
|
|
@ -516,6 +492,27 @@ def test_configured_service_tiers_replace_stock_tiers_and_a_known_id_keeps_its_c
|
|||
assert _tiers(entry) == expected
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("rows", "alias_defaults"),
|
||||
[
|
||||
((_row("gpt-5.5", service_tiers=[]),), "codex"),
|
||||
((_row("gpt-5.5", service_tiers=["ultrafast"]),), "codex"),
|
||||
((_row("gpt-5.5", service_tiers=["priority"]),), "codex"),
|
||||
((_row("gpt-5.5"),), "codex"),
|
||||
((_row("team-55", upstream_model="gpt-5.5"),), "deployment"),
|
||||
((_row("team-55", upstream_model="gpt-5.5", service_tiers=["priority"]),), "deployment"),
|
||||
],
|
||||
)
|
||||
def test_codexs_fast_toggle_is_on_exactly_when_the_entry_offers_the_priority_tier(rows, alias_defaults):
|
||||
"""Codex shows `/fast` for a speed tier `fast` as well as for a `priority` service tier, but only sends
|
||||
the tier when `service_tiers` lists it (codex-rs `ModelPreset::supports_fast_mode` and
|
||||
`get_service_tier`, 0.166.0), so a leftover speed tier is a `/fast` toggle that does nothing."""
|
||||
(entry,) = _models(*rows, alias_defaults=alias_defaults)
|
||||
|
||||
offers_priority = "priority" in [tier["id"] for tier in entry["service_tiers"]]
|
||||
assert ("fast" in entry["additional_speed_tiers"]) is offers_priority
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"invalid", ["ultrafast", [""], [" "], [1], [{"name": "no id"}], [{"id": "x", "bogus": 1}], {"id": "x"}]
|
||||
)
|
||||
|
|
@ -724,12 +721,13 @@ def test_catalog_rows_read_the_router_by_each_entry_lookup_id():
|
|||
|
||||
rows = codex_catalog_rows(listing, (("gpt-5.5-team", "model_name_team-1_c0ffee"), ("plain", "plain")), router)
|
||||
|
||||
assert rows == (
|
||||
assert tuple(replace(row, deployment_capabilities=()) for row in rows) == (
|
||||
CodexCatalogRow(
|
||||
id="gpt-5.5-team",
|
||||
mode="chat",
|
||||
max_input_tokens=7,
|
||||
upstream_model="openai/gpt-5.5",
|
||||
upstream_provider="openai",
|
||||
display_name="Team 5.5",
|
||||
service_tiers=(["ultrafast"], None),
|
||||
supported_reasoning_efforts=router.get_model_group_info(
|
||||
|
|
@ -741,11 +739,15 @@ def test_catalog_rows_read_the_router_by_each_entry_lookup_id():
|
|||
CodexCatalogRow(
|
||||
id="plain",
|
||||
upstream_model="openai/some-unmapped-model",
|
||||
upstream_provider="openai",
|
||||
service_tiers=(None,),
|
||||
search_tool_support=(None,),
|
||||
configured_max_input_tokens=(None,),
|
||||
),
|
||||
)
|
||||
catalog_window = GetModelCostMap.loaded_model_cost_map()["gpt-5.5"]["max_input_tokens"]
|
||||
assert [deployment.max_input_tokens for deployment in rows[0].deployment_capabilities] == [catalog_window] * 2
|
||||
assert rows[1].deployment_capabilities == (CodexDeploymentCapabilities(),)
|
||||
|
||||
|
||||
def test_catalog_rows_read_an_alias_off_its_target_under_the_alias_name():
|
||||
|
|
@ -765,11 +767,13 @@ def test_catalog_rows_read_an_alias_off_its_target_under_the_alias_name():
|
|||
|
||||
(row,) = codex_catalog_rows(listing, (("gpt-6", "gpt-6"),), router)
|
||||
|
||||
assert row == CodexCatalogRow(
|
||||
assert len(row.deployment_capabilities) == 1
|
||||
assert replace(row, deployment_capabilities=()) == CodexCatalogRow(
|
||||
id="gpt-6",
|
||||
mode="chat",
|
||||
max_input_tokens=9,
|
||||
upstream_model="openai/gpt-6-astra",
|
||||
upstream_provider="openai",
|
||||
service_tiers=(["ultrafast"],),
|
||||
supported_reasoning_efforts=router.get_model_group_info("gpt-6-astra").supported_reasoning_efforts,
|
||||
search_tool_support=(None,),
|
||||
|
|
@ -944,3 +948,292 @@ def test_codex_tool_search_is_offered_off_the_route_and_model_info_and_only_when
|
|||
"mixed": False,
|
||||
}
|
||||
assert models[0]["context_window"] == 1000000
|
||||
|
||||
|
||||
_SOL_SLUG: Final = next(
|
||||
slug for slug, model in bundled_codex_models().items() if model.default_service_tier is not None
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"upstream",
|
||||
[
|
||||
"bedrock/us.openai.{slug}",
|
||||
"bedrock/global.openai.{slug}",
|
||||
"bedrock/converse/us.openai.{slug}",
|
||||
"bedrock/openai.{slug}",
|
||||
"bedrock_mantle/openai.{slug}",
|
||||
"bedrock_mantle/us-east-1/openai.{slug}",
|
||||
"openai/{slug}",
|
||||
"{slug}",
|
||||
],
|
||||
)
|
||||
@pytest.mark.parametrize("slug", sorted(bundled_codex_models()))
|
||||
def test_a_model_codex_knows_keeps_codexs_entry_under_any_provider_prefix(upstream: str, slug: str) -> None:
|
||||
stock: Final = bundled_codex_models()[slug].model_dump(mode="json")
|
||||
|
||||
(entry,) = json.loads(
|
||||
codex_models_response_json(
|
||||
(_row("team-model", upstream_model=upstream.format(slug=slug)),), byte_limit=None
|
||||
).json
|
||||
)["models"]
|
||||
|
||||
assert entry["model_messages"] == stock["model_messages"]
|
||||
assert entry["supported_reasoning_levels"] == stock["supported_reasoning_levels"]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("model", ["us.openai.{slug}", "converse/global.openai.{slug}"])
|
||||
def test_a_bedrock_deployment_naming_its_provider_apart_from_the_model_keeps_codexs_entry(model: str) -> None:
|
||||
router: Final = litellm.Router(
|
||||
model_list=[
|
||||
{
|
||||
"model_name": "team-model",
|
||||
"litellm_params": {
|
||||
"model": model.format(slug=_SOL_SLUG),
|
||||
"custom_llm_provider": "bedrock",
|
||||
"aws_region_name": "us-east-1",
|
||||
},
|
||||
}
|
||||
]
|
||||
)
|
||||
|
||||
entry: Final = _served(router, "team-model")["team-model"]
|
||||
|
||||
assert entry["model_messages"] == bundled_codex_models()[_SOL_SLUG].model_dump(mode="json")["model_messages"]
|
||||
|
||||
|
||||
def test_an_upstream_with_no_provider_prefix_matches_only_by_its_own_name() -> None:
|
||||
(unknown, known) = json.loads(
|
||||
codex_models_response_json(
|
||||
(
|
||||
_row("first", upstream_model=f"us.openai.{_SOL_SLUG}"),
|
||||
_row("second", upstream_model=_SOL_SLUG),
|
||||
),
|
||||
instructions=_PROMPT,
|
||||
).json
|
||||
)["models"]
|
||||
|
||||
assert "model_messages" not in unknown and unknown["base_instructions"] == _PROMPT
|
||||
assert known["model_messages"] == bundled_codex_models()[_SOL_SLUG].model_dump(mode="json")["model_messages"]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"upstream",
|
||||
["bedrock/us.openai.{slug}-mini", "bedrock/us.meta.{slug}x", "bedrock_mantle/openai.{slug}.1", "{slug}-0"],
|
||||
)
|
||||
def test_a_model_whose_name_only_starts_like_one_codex_knows_gets_a_translated_entry(upstream: str) -> None:
|
||||
(entry,) = json.loads(
|
||||
codex_models_response_json(
|
||||
(_row("team-model", upstream_model=upstream.format(slug=_SOL_SLUG)),), instructions=_PROMPT
|
||||
).json
|
||||
)["models"]
|
||||
|
||||
assert "model_messages" not in entry and entry["base_instructions"] == _PROMPT
|
||||
|
||||
|
||||
def _listing(*names: str, max_input_tokens: int | None = None) -> tuple[dict[str, object], ...]:
|
||||
return tuple(
|
||||
{
|
||||
"id": name,
|
||||
"object": "model",
|
||||
"created": 0,
|
||||
"owned_by": "x",
|
||||
"mode": "chat",
|
||||
"max_input_tokens": max_input_tokens,
|
||||
}
|
||||
for name in names
|
||||
)
|
||||
|
||||
|
||||
def _served(
|
||||
router: litellm.Router, *names: str, team_id: str | None = None, **listing: int
|
||||
) -> dict[str, Mapping[str, object]]:
|
||||
body: Final = codex_model_list_body(
|
||||
_listing(*names, **listing), tuple((name, name) for name in names), router, team_id, client_version="0.166.0"
|
||||
)
|
||||
return {entry["slug"]: entry for entry in json.loads(body)["models"]}
|
||||
|
||||
|
||||
def _vllm(name: str, host: str, **model_info: object) -> dict[str, object]:
|
||||
return {
|
||||
"model_name": name,
|
||||
"litellm_params": {"model": f"hosted_vllm/coder-{host}", "api_base": f"https://{host}.example/v1"},
|
||||
"model_info": model_info,
|
||||
}
|
||||
|
||||
|
||||
def test_a_text_only_deployment_takes_images_off_the_name_and_a_silent_one_does_not() -> None:
|
||||
router: Final = litellm.Router(
|
||||
model_list=[
|
||||
_vllm("mixed", "h0", supports_vision=True),
|
||||
_vllm("mixed", "h1", supports_vision=False),
|
||||
_vllm("vision-and-silent", "h2", supports_vision=True),
|
||||
_vllm("vision-and-silent", "h3"),
|
||||
]
|
||||
)
|
||||
|
||||
served: Final = _served(router, "mixed", "vision-and-silent")
|
||||
|
||||
assert served["mixed"]["input_modalities"] == ["text"]
|
||||
assert served["vision-and-silent"]["input_modalities"] == ["text", "image"]
|
||||
|
||||
|
||||
def test_a_silent_deployment_does_not_vote_against_tool_calls_but_one_without_them_does() -> None:
|
||||
router: Final = litellm.Router(
|
||||
model_list=[
|
||||
_vllm("tools-and-silent", "h4", supports_function_calling=True),
|
||||
_vllm("tools-and-silent", "h5"),
|
||||
_vllm("tools-and-none", "h6", supports_function_calling=True),
|
||||
_vllm("tools-and-none", "h7", supports_function_calling=False),
|
||||
]
|
||||
)
|
||||
|
||||
served: Final = _served(router, "tools-and-silent", "tools-and-none")
|
||||
|
||||
assert served["tools-and-silent"]["apply_patch_tool_type"] == "freeform"
|
||||
assert served["tools-and-none"]["apply_patch_tool_type"] is None
|
||||
|
||||
|
||||
def test_a_mixed_group_takes_the_smallest_window_any_deployment_declares() -> None:
|
||||
router: Final = litellm.Router(
|
||||
model_list=[
|
||||
_vllm("mixed", "h8", max_input_tokens=200_000),
|
||||
_vllm("mixed", "h9", max_input_tokens=128_000, supports_vision=True),
|
||||
_vllm("mixed", "h10"),
|
||||
_vllm("undeclared", "h11"),
|
||||
]
|
||||
)
|
||||
|
||||
served: Final = _served(router, "mixed", "undeclared", max_input_tokens=200_000)
|
||||
|
||||
assert served["mixed"]["context_window"] == 128_000
|
||||
assert served["undeclared"]["context_window"] == 200_000
|
||||
|
||||
|
||||
def test_a_claude_like_deployment_gets_codexs_coding_setup() -> None:
|
||||
router: Final = litellm.Router(
|
||||
model_list=[
|
||||
_vllm(
|
||||
"claude-like",
|
||||
"h-claude",
|
||||
max_input_tokens=1_000_000,
|
||||
supports_vision=True,
|
||||
supports_function_calling=True,
|
||||
supports_reasoning=True,
|
||||
),
|
||||
_vllm("plain", "h12"),
|
||||
_vllm("no-reasoning", "h12b", supports_reasoning=False),
|
||||
]
|
||||
)
|
||||
|
||||
served: Final = _served(router, "claude-like", "plain", "no-reasoning")
|
||||
|
||||
claude_like: Final = served["claude-like"]
|
||||
assert claude_like["apply_patch_tool_type"] == "freeform"
|
||||
assert claude_like["truncation_policy"] == {"mode": "tokens", "limit": 10000}
|
||||
assert claude_like["supports_reasoning_summary_parameter"] is True
|
||||
assert claude_like["context_window"] == 1_000_000
|
||||
assert served["plain"]["apply_patch_tool_type"] is None
|
||||
assert "supports_reasoning_summary_parameter" not in served["plain"]
|
||||
assert served["no-reasoning"]["supports_reasoning_summary_parameter"] is False
|
||||
|
||||
|
||||
def test_capabilities_are_read_only_off_the_deployments_the_key_team_routes_to() -> None:
|
||||
router: Final = litellm.Router(
|
||||
model_list=[
|
||||
_vllm("coder", "h13", supports_vision=True),
|
||||
_vllm("coder", "h14", supports_vision=False, team_id="team-2"),
|
||||
]
|
||||
)
|
||||
|
||||
assert _served(router, "coder", team_id="team-1")["coder"]["input_modalities"] == ["text", "image"]
|
||||
assert _served(router, "coder", team_id="team-2")["coder"]["input_modalities"] == ["text"]
|
||||
|
||||
|
||||
def _tier_fields(entry: Mapping[str, object]) -> tuple[object, object, object]:
|
||||
return (entry["service_tiers"], entry.get("additional_speed_tiers"), entry.get("default_service_tier"))
|
||||
|
||||
|
||||
def test_codexs_own_tiers_stay_only_when_every_deployment_behind_the_name_prices_them() -> None:
|
||||
stock: Final = bundled_codex_models()[_SOL_SLUG].model_dump(mode="json")
|
||||
assert stock["service_tiers"] and stock["additional_speed_tiers"] and stock["default_service_tier"]
|
||||
token_price: Final = {"input_cost_per_token": 1e-06}
|
||||
priority_price: Final = {**token_price, "input_cost_per_token_priority": 1e-05}
|
||||
|
||||
def upstream(host: str) -> dict[str, str]:
|
||||
return {"model": f"hosted_vllm/{_SOL_SLUG}", "api_base": f"https://{host}.example/v1"}
|
||||
|
||||
router: Final = litellm.Router(
|
||||
model_list=[
|
||||
{"model_name": "all-priced", "litellm_params": upstream("a"), "model_info": priority_price},
|
||||
{"model_name": "all-priced", "litellm_params": upstream("b"), "model_info": priority_price},
|
||||
{"model_name": "half-priced", "litellm_params": upstream("c"), "model_info": priority_price},
|
||||
{"model_name": "half-priced", "litellm_params": upstream("d"), "model_info": token_price},
|
||||
{"model_name": "priced-and-unknown", "litellm_params": upstream("f"), "model_info": priority_price},
|
||||
{"model_name": "priced-and-unknown", "litellm_params": {"model": "azure/prod-sol-eastus"}},
|
||||
{
|
||||
"model_name": "configured",
|
||||
"litellm_params": upstream("e"),
|
||||
"model_info": {"service_tiers": ["priority"]},
|
||||
},
|
||||
]
|
||||
)
|
||||
|
||||
served: Final = _served(router, "all-priced", "half-priced", "priced-and-unknown", "configured")
|
||||
|
||||
assert _tier_fields(served["all-priced"]) == _tier_fields(stock)
|
||||
assert _tier_fields(served["priced-and-unknown"]) == _tier_fields(stock)
|
||||
assert _tier_fields(served["half-priced"]) == ([], [], None)
|
||||
assert served["half-priced"]["model_messages"] == stock["model_messages"]
|
||||
assert [tier["id"] for tier in served["configured"]["service_tiers"]] == ["priority"]
|
||||
|
||||
|
||||
def _tiered_stock(default_service_tier: str) -> Mapping[str, CodexStockModel]:
|
||||
tiers: Final = (
|
||||
CodexServiceTier(id="priority", name="Fast", description="faster"),
|
||||
CodexServiceTier(id="flex", name="Flex", description="cheaper"),
|
||||
)
|
||||
return MappingProxyType(
|
||||
{
|
||||
"tiered": CodexStockModel(
|
||||
slug="tiered",
|
||||
display_name="Tiered",
|
||||
priority=0,
|
||||
visibility="list",
|
||||
service_tiers=tiers,
|
||||
default_service_tier=default_service_tier,
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(("default_tier", "kept_default"), [("priority", "priority"), ("flex", None)])
|
||||
def test_dropping_one_stock_tier_keeps_the_default_only_while_it_is_still_offered(default_tier, kept_default) -> None:
|
||||
priority_and_flex: Final = CodexDeploymentCapabilities(
|
||||
prices_tokens=True,
|
||||
priced_service_tier_keys=frozenset(
|
||||
get_service_tier_cost_key("input_cost_per_token", tier) for tier in ("priority", "flex")
|
||||
),
|
||||
)
|
||||
priority_only: Final = CodexDeploymentCapabilities(
|
||||
prices_tokens=True,
|
||||
priced_service_tier_keys=frozenset({get_service_tier_cost_key("input_cost_per_token", "priority")}),
|
||||
)
|
||||
row: Final = _row("tiered", deployment_capabilities=(priority_and_flex, priority_only))
|
||||
|
||||
(entry,) = json.loads(
|
||||
codex_models_response_json((row,), stock=_tiered_stock(default_tier), instructions=_PROMPT).json
|
||||
)["models"]
|
||||
|
||||
assert [tier["id"] for tier in entry["service_tiers"]] == ["priority"]
|
||||
assert entry["default_service_tier"] == kept_default
|
||||
|
||||
|
||||
def test_model_info_that_does_not_validate_declares_no_capability_but_keeps_its_prices() -> None:
|
||||
priced: Final = get_service_tier_cost_key("input_cost_per_token", "priority")
|
||||
|
||||
capabilities: Final = deployment_capabilities(
|
||||
{"supports_vision": "sometimes", "supports_function_calling": False, priced: 1e-05}, "odd"
|
||||
)
|
||||
|
||||
assert capabilities == CodexDeploymentCapabilities(priced_service_tier_keys=frozenset({priced}))
|
||||
|
|
|
|||
224
tests/unit/proxy/common_utils/test_codex_model_translation.py
Normal file
224
tests/unit/proxy/common_utils/test_codex_model_translation.py
Normal file
|
|
@ -0,0 +1,224 @@
|
|||
import json
|
||||
from typing import Final
|
||||
|
||||
import pytest
|
||||
|
||||
from litellm.litellm_core_utils.llm_cost_calc.utils import get_service_tier_cost_key
|
||||
from litellm.proxy.common_utils.codex_model_translation import (
|
||||
CodexCapabilities,
|
||||
CodexDeploymentCapabilities,
|
||||
CodexServiceTier,
|
||||
codex_fallback_entry,
|
||||
combined_capabilities,
|
||||
no_priced_deployment_lacks_tier,
|
||||
)
|
||||
|
||||
_PROMPT: Final = "base prompt"
|
||||
_SILENT: Final = CodexDeploymentCapabilities()
|
||||
_ABSENT: Final = object()
|
||||
|
||||
|
||||
def _entry(capabilities: CodexCapabilities, **kwargs: object) -> dict[str, object]:
|
||||
entry: Final = codex_fallback_entry(
|
||||
slug="m", display_name="M", priority=3, capabilities=capabilities, instructions=_PROMPT, **kwargs
|
||||
)
|
||||
return json.loads(entry.model_dump_json())
|
||||
|
||||
|
||||
_CLAUDE_LIKE: Final = CodexCapabilities(
|
||||
context_window=1_000_000, supports_vision=True, supports_function_calling=True, supports_reasoning=True
|
||||
)
|
||||
|
||||
|
||||
def test_a_model_that_calls_tools_and_reasons_gets_codexs_own_coding_setup():
|
||||
entry: Final = _entry(_CLAUDE_LIKE)
|
||||
|
||||
assert entry["apply_patch_tool_type"] == "freeform"
|
||||
assert entry["truncation_policy"] == {"mode": "tokens", "limit": 10000}
|
||||
assert entry["supports_reasoning_summary_parameter"] is True
|
||||
assert entry["supports_reasoning_summaries"] is True
|
||||
assert entry["input_modalities"] == ["text", "image"]
|
||||
assert entry["context_window"] == 1_000_000
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("flag", "field", "by_value"),
|
||||
[
|
||||
("supports_function_calling", "apply_patch_tool_type", {True: "freeform", False: None, None: None}),
|
||||
("supports_reasoning", "supports_reasoning_summary_parameter", {True: True, False: False, None: _ABSENT}),
|
||||
("supports_reasoning", "supports_reasoning_summaries", {True: True, False: False, None: False}),
|
||||
("supports_parallel_function_calling", "supports_parallel_tool_calls", {True: True, False: False, None: False}),
|
||||
],
|
||||
)
|
||||
@pytest.mark.parametrize("value", [True, False, None])
|
||||
def test_each_codex_feature_follows_what_the_deployments_declare(flag, field, by_value, value):
|
||||
"""A missing `supports_reasoning_summary_parameter` is Codex's own default (true, `default_true` in
|
||||
`codex-rs/protocol/src/openai_models.rs` at rust-v0.166.0), so a model nobody describes keeps it."""
|
||||
entry: Final = _entry(CodexCapabilities(**{flag: value}))
|
||||
|
||||
assert entry.get(field, _ABSENT) == by_value[value]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("vision", "modalities"), [(False, ["text"]), (True, ["text", "image"]), (None, ["text", "image"])]
|
||||
)
|
||||
def test_images_are_offered_unless_a_deployment_cannot_read_them(vision, modalities):
|
||||
assert _entry(CodexCapabilities(supports_vision=vision))["input_modalities"] == modalities
|
||||
|
||||
|
||||
def test_the_rest_of_the_entry_is_what_the_listing_decides():
|
||||
tier: Final = CodexServiceTier(id="flex", name="Flex", description="cheaper")
|
||||
|
||||
entry: Final = _entry(
|
||||
CodexCapabilities(),
|
||||
service_tiers=(tier,),
|
||||
supports_search_tool=True,
|
||||
reasoning_efforts=("low", "high"),
|
||||
client_version="0.166.0",
|
||||
)
|
||||
|
||||
assert (entry["slug"], entry["display_name"], entry["priority"], entry["base_instructions"]) == (
|
||||
"m",
|
||||
"M",
|
||||
3,
|
||||
_PROMPT,
|
||||
)
|
||||
assert entry["service_tiers"] == [{"id": "flex", "name": "Flex", "description": "cheaper"}]
|
||||
assert entry["supports_search_tool"] is True
|
||||
assert [level["effort"] for level in entry["supported_reasoning_levels"]] == ["low", "high"]
|
||||
assert entry["default_reasoning_level"] == "low"
|
||||
assert entry["context_window"] is None
|
||||
assert (entry["shell_type"], entry["visibility"], entry["supported_in_api"]) == ("unified_exec", "list", True)
|
||||
assert (entry["support_verbosity"], entry["use_responses_lite"]) == (False, False)
|
||||
assert "tool_mode" not in entry
|
||||
|
||||
|
||||
_REQUIRED_SINCE_CODEX_0_140: Final = frozenset(
|
||||
{
|
||||
"slug",
|
||||
"display_name",
|
||||
"description",
|
||||
"supported_reasoning_levels",
|
||||
"shell_type",
|
||||
"visibility",
|
||||
"supported_in_api",
|
||||
"priority",
|
||||
"availability_nux",
|
||||
"upgrade",
|
||||
"base_instructions",
|
||||
"supports_reasoning_summaries",
|
||||
"support_verbosity",
|
||||
"default_verbosity",
|
||||
"apply_patch_tool_type",
|
||||
"truncation_policy",
|
||||
"supports_parallel_tool_calls",
|
||||
"experimental_supported_tools",
|
||||
}
|
||||
)
|
||||
_CODEX_ENUMS: Final = {
|
||||
"shell_type": {"unified_exec", "disabled"},
|
||||
"visibility": {"list", "hide", "none"},
|
||||
"apply_patch_tool_type": {"freeform", None},
|
||||
}
|
||||
_CODEX_MODALITIES: Final = {"text", "image"}
|
||||
_CODEX_TRUNCATION_MODES: Final = {"bytes", "tokens"}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("described", [True, False], ids=["described", "undescribed"])
|
||||
def test_every_entry_carries_the_fields_and_enum_values_every_supported_codex_decodes(described):
|
||||
"""`codex-rs/protocol/src/openai_models.rs` ModelInfo at rust-v0.140.0 and rust-v0.166.0, the fields with
|
||||
no `#[serde(default)]` and the variants of InputModality, ConfigShellToolType, ModelVisibility,
|
||||
ApplyPatchToolType and TruncationMode in both, read on 2026-11-02. Codex rejects the whole catalog when
|
||||
one entry does not decode."""
|
||||
entry: Final = _entry(
|
||||
CodexCapabilities(
|
||||
context_window=128_000,
|
||||
supports_vision=False,
|
||||
supports_function_calling=True,
|
||||
supports_parallel_function_calling=True,
|
||||
supports_reasoning=True,
|
||||
)
|
||||
if described
|
||||
else CodexCapabilities()
|
||||
)
|
||||
|
||||
assert entry.keys() >= _REQUIRED_SINCE_CODEX_0_140
|
||||
assert all(entry[field] in values for field, values in _CODEX_ENUMS.items())
|
||||
assert set(entry["input_modalities"]) <= _CODEX_MODALITIES and "text" in entry["input_modalities"]
|
||||
assert entry["truncation_policy"]["mode"] in _CODEX_TRUNCATION_MODES
|
||||
|
||||
|
||||
def test_a_deployment_that_says_nothing_does_not_vote():
|
||||
declares: Final = CodexDeploymentCapabilities(
|
||||
supports_vision=True,
|
||||
supports_function_calling=True,
|
||||
supports_parallel_function_calling=True,
|
||||
supports_reasoning=True,
|
||||
)
|
||||
|
||||
assert combined_capabilities((declares, _SILENT)) == CodexCapabilities(
|
||||
supports_vision=True,
|
||||
supports_function_calling=True,
|
||||
supports_parallel_function_calling=True,
|
||||
supports_reasoning=True,
|
||||
)
|
||||
assert combined_capabilities((_SILENT, _SILENT)) == CodexCapabilities()
|
||||
assert combined_capabilities(()) == CodexCapabilities()
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"flag",
|
||||
["supports_vision", "supports_function_calling", "supports_parallel_function_calling", "supports_reasoning"],
|
||||
)
|
||||
def test_one_deployment_lacking_a_capability_takes_it_off_the_whole_name(flag):
|
||||
has: Final = CodexDeploymentCapabilities(**{flag: True})
|
||||
lacks: Final = CodexDeploymentCapabilities(**{flag: False})
|
||||
|
||||
for order in ((has, lacks, _SILENT), (_SILENT, lacks, has)):
|
||||
assert getattr(combined_capabilities(order), flag) is False
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("windows", "listed", "expected"),
|
||||
[
|
||||
((200_000, 128_000, None), 400_000, 128_000),
|
||||
((None, 64_000), 400_000, 64_000),
|
||||
((None, None), 400_000, 400_000),
|
||||
((), 400_000, 400_000),
|
||||
((0, -1, 32_000), None, 32_000),
|
||||
((0,), None, None),
|
||||
],
|
||||
)
|
||||
def test_the_window_is_the_smallest_any_deployment_declares(windows, listed, expected):
|
||||
deployments: Final = tuple(CodexDeploymentCapabilities(max_input_tokens=window) for window in windows)
|
||||
|
||||
assert combined_capabilities(deployments, listed).context_window == expected
|
||||
|
||||
|
||||
def _priced(*tier_ids: str) -> CodexDeploymentCapabilities:
|
||||
return CodexDeploymentCapabilities(
|
||||
prices_tokens=True,
|
||||
priced_service_tier_keys=frozenset(
|
||||
get_service_tier_cost_key("input_cost_per_token", tier) for tier in tier_ids
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("deployments", "tier", "kept"),
|
||||
[
|
||||
((_priced("priority"), _priced("priority", "flex")), "priority", True),
|
||||
((_priced("priority"), _priced("flex")), "priority", False),
|
||||
((_priced("priority"), _priced()), "priority", False),
|
||||
((_priced("priority"), _SILENT), "priority", True),
|
||||
((_SILENT, _SILENT), "priority", True),
|
||||
((_priced("priority"),), "fast", True),
|
||||
((_priced("flex"),), "fast", False),
|
||||
((_priced("priority", "flex"),), "made-up-tier", False),
|
||||
((), "priority", True),
|
||||
],
|
||||
)
|
||||
def test_a_tier_stays_unless_a_deployment_that_prices_tokens_has_no_price_for_it(deployments, tier, kept):
|
||||
"""A deployment with no token price at all (a subscription upstream, a deployment name the cost map does
|
||||
not know) says nothing about which tiers its upstream sells, so it does not vote."""
|
||||
assert no_priced_deployment_lacks_tier(tier, deployments) is kept
|
||||
|
|
@ -56,9 +56,11 @@ def patched_models(monkeypatch):
|
|||
deployment.litellm_params.model = "gpt-4"
|
||||
router.get_deployment_by_model_group_name = MagicMock(return_value=deployment)
|
||||
router.get_routable_upstream_model = MagicMock(return_value="gpt-4")
|
||||
router.get_routable_upstream_provider = MagicMock(return_value=None)
|
||||
router.get_configured_display_name = MagicMock(return_value=None)
|
||||
router.get_configured_service_tiers = MagicMock(return_value=())
|
||||
router.get_configured_max_input_tokens = MagicMock(return_value=())
|
||||
router.get_routable_deployment_model_info = MagicMock(return_value=())
|
||||
router.get_model_group_info = MagicMock(return_value=None)
|
||||
router.get_search_tool_support = MagicMock(return_value=())
|
||||
route_entry_points_to_stubs(router)
|
||||
|
|
|
|||
|
|
@ -99,6 +99,8 @@ def route_entry_points_to_stubs(router: _RouterDouble) -> _RouterDouble:
|
|||
return router.is_recognized_model(name)
|
||||
if kind == "upstream":
|
||||
return router.get_routable_upstream_model(name, **_given(team_id=team_id))
|
||||
if kind == "upstream_provider":
|
||||
return router.get_routable_upstream_provider(name, **_given(team_id=team_id))
|
||||
if kind == "search_tool_support":
|
||||
return router.get_search_tool_support(name, **_given(team_id=team_id))
|
||||
if kind == "from_id":
|
||||
|
|
@ -125,6 +127,8 @@ def route_entry_points_to_stubs(router: _RouterDouble) -> _RouterDouble:
|
|||
return router.get_configured_service_tiers(name, **_given(team_id=team_id))
|
||||
if field == "max_input_tokens":
|
||||
return router.get_configured_max_input_tokens(name, **_given(team_id=team_id))
|
||||
if field == "deployment_model_info":
|
||||
return router.get_routable_deployment_model_info(name, **_given(team_id=team_id))
|
||||
if field == "listing_info":
|
||||
return router.get_model_listing_info(name)
|
||||
if field == "mode":
|
||||
|
|
|
|||
|
|
@ -13950,6 +13950,27 @@ def test_upstream_model_and_service_tiers_are_read_off_the_deployments_the_team_
|
|||
assert router.get_configured_service_tiers(model_name, team_id) == service_tiers
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("litellm_params", "provider"),
|
||||
[
|
||||
({"model": "us.openai.gpt-6.1-sol", "custom_llm_provider": "bedrock"}, "bedrock"),
|
||||
({"model": "bedrock_mantle/openai.gpt-6.1-sol"}, "bedrock_mantle"),
|
||||
({"model": "gpt-6.1-sol"}, None),
|
||||
],
|
||||
)
|
||||
def test_the_upstream_provider_is_the_one_the_routable_deployment_declares(litellm_params, provider):
|
||||
router = litellm.Router(
|
||||
model_list=[
|
||||
{"model_name": "sol", "litellm_params": litellm_params, "model_info": {"team_id": "team-1"}},
|
||||
{"model_name": "sol", "litellm_params": {"model": "azure/sol-deployment"}, "model_info": {"team_id": "t2"}},
|
||||
]
|
||||
)
|
||||
|
||||
assert router.registry().resolve("sol", kind="upstream_provider", team_id="team-1") == provider
|
||||
assert router.registry().resolve("sol", kind="upstream_provider", team_id="t2") == "azure"
|
||||
assert router.registry().resolve("not-a-real-model", kind="upstream_provider") is None
|
||||
|
||||
|
||||
def test_get_configured_display_name_treats_malformed_values_as_absent():
|
||||
malformed = ["", " ", 12345, ["Kimi K3"], {"name": "Kimi K3"}, True]
|
||||
router = litellm.Router(
|
||||
|
|
|
|||
|
|
@ -656,6 +656,11 @@ FORWARDS = [
|
|||
"_core_get_routable_upstream_model",
|
||||
(("gpt",), {"team_id": "t"}),
|
||||
),
|
||||
(
|
||||
lambda r: r.registry().resolve("gpt", kind="upstream_provider", team_id="t"),
|
||||
"_core_get_routable_upstream_provider",
|
||||
(("gpt",), {"team_id": "t"}),
|
||||
),
|
||||
(
|
||||
lambda r: r.registry().resolve("gpt", kind="search_tool_support", team_id="t"),
|
||||
"_core_get_search_tool_support",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue