feat(proxy): give Codex accurate model entries for every model in /v1/models

This commit is contained in:
ryan-crabbe-berri 2026-11-02 22:54:48 -08:00
parent 0d697017b5
commit 2e0939d2f2
16 changed files with 1030 additions and 166 deletions

View file

@ -13,7 +13,7 @@ import warnings
from collections.abc import Callable, Iterator, Mapping, Sequence
from dataclasses import dataclass
from types import MappingProxyType
from typing import TYPE_CHECKING, Any, Final, Literal, TypeAlias
from typing import TYPE_CHECKING, Any, Final, Literal, TypeAlias, get_args
from typing_extensions import (
ReadOnly,
@ -32,6 +32,7 @@ from pydantic import ConfigDict, TypeAdapter, ValidationError
import litellm
from litellm import verbose_logger
from litellm.constants import BEDROCK_INVOKE_PROVIDERS_LITERAL
from litellm.litellm_core_utils.aws_partition import get_aws_dns_suffix
from litellm.litellm_core_utils.optional_imports import ensure_optional_import
from litellm.litellm_core_utils.prompt_templates.common_utils import schema_property_count
@ -1490,6 +1491,14 @@ def get_bedrock_base_model(model: str) -> str:
return model
def bedrock_vendor_model_name(model: str) -> str:
"""The name the model's vendor publishes it under, without Bedrock's routing, region and vendor
prefixes: ``bedrock/us.openai.gpt-6.1-sol`` and ``us-east-1/openai.gpt-6.1-sol`` are ``gpt-6.1-sol``."""
base: Final = get_bedrock_base_model(model)
vendor, separator, name = base.partition(".")
return name if separator and vendor in get_args(BEDROCK_INVOKE_PROVIDERS_LITERAL) 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.

View file

@ -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 has no
price 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.llms.bedrock.common_utils import bedrock_vendor_model_name
from litellm.proxy.common_utils.codex_model_translation import (
TIER_PRICE_KEYS,
CodexDeploymentCapabilities,
CodexFallbackModel,
CodexServiceTier,
TierId,
codex_fallback_entry,
combined_capabilities,
every_deployment_prices,
)
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, ...] = ()
@ -178,7 +126,8 @@ class CodexCatalogRow:
`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
@ -190,6 +139,7 @@ class CodexCatalogRow:
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 +242,49 @@ 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
_VENDOR_MODEL_NAMES: Final[Mapping[str, Callable[[str], str]]] = MappingProxyType(
{"bedrock": bedrock_vendor_model_name, "bedrock_mantle": bedrock_vendor_model_name}
)
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 _vendor_model_name(upstream_model: str) -> str:
provider, _, model = upstream_model.partition("/")
name_for: Final = _VENDOR_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 the 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, _vendor_model_name(row.upstream_model))
)
return next((stock[name] for name in names if name in stock), 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 has no price for,
or nothing when every deployment prices them all. 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."""
deployments: Final = row.deployment_capabilities
tiers: Final = tuple(tier for tier in stock_model.service_tiers if every_deployment_prices(tier.id, deployments))
speed_tiers: Final = tuple(
tier for tier in stock_model.additional_speed_tiers if every_deployment_prices(tier, deployments)
)
if tiers == stock_model.service_tiers and speed_tiers == stock_model.additional_speed_tiers:
return _NO_UPDATE
offered: Final = frozenset(tier.id for tier in tiers)
return MappingProxyType(
{
"service_tiers": tiers,
"additional_speed_tiers": speed_tiers,
"default_service_tier": (
stock_model.default_service_tier if stock_model.default_service_tier in offered else None
),
}
)
def _alias_context_window(row: CodexCatalogRow, stock_model: CodexStockModel) -> Mapping[str, int]:
@ -396,17 +351,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,7 +379,7 @@ def _entry(
**_configured_window(row, stock_model),
}
if configured is None:
return stock_model.model_copy(update=listing)
return stock_model.model_copy(update={**_priced_tiers(row, stock_model), **listing})
configured_ids: Final = frozenset(tier.id for tier in configured)
return stock_model.model_copy(
update={
@ -503,8 +457,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 every deployment
behind the row prices 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 +487,32 @@ 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()
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, "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(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:
@ -549,6 +531,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 +545,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))

View file

@ -0,0 +1,229 @@
"""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 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.llm_cost_calc.utils import SERVICE_TIER_COST_KEY_SUFFIXES, get_service_tier_cost_key
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")
_TIER_PRICE_BASE_KEY: Final = "input_cost_per_token"
TIER_PRICE_KEYS: Final = tuple(f"{_TIER_PRICE_BASE_KEY}{suffix}" for suffix in SERVICE_TIER_COST_KEY_SUFFIXES)
_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
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=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_summary_parameter: bool = False
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, 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
priced_service_tier_keys: frozenset[str] = frozenset()
def every_deployment_prices(tier_id: str, deployments: Sequence[CodexDeploymentCapabilities]) -> bool:
"""Whether every deployment has a price for `tier_id`, the way cost tracking prices a request sent at
that tier; a tier cost tracking has no price key for is priced by none."""
price_key: Final = get_service_tier_cost_key(_TIER_PRICE_BASE_KEY, tier_id)
return all(price_key in deployment.priced_service_tier_keys for deployment in deployments)
@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 two
that send something a model may reject: the `reasoning.summary` parameter goes out only to a model
that reasons, and the freeform `apply_patch` tool only to one that calls tools.
"""
levels, default_level = codex_reasoning_levels(reasoning_efforts, client_version)
reasons: Final = capabilities.supports_reasoning is True
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=reasons,
supports_reasoning_summaries=reasons,
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,
)

View file

@ -156,6 +156,7 @@ GroupField: TypeAlias = Literal[
"mode",
"service_tiers",
"max_input_tokens",
"deployment_model_info",
"token_limits",
"listing_info",
]
@ -866,6 +867,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 +915,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 +930,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":

View file

@ -12740,6 +12740,26 @@ 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.
"""
return tuple(
self._core_cached_deployment_model_info(deployment_id, deployment["litellm_params"]["model"])
if isinstance(deployment_id := _configured_model_info(deployment).get("id"), str)
else None
for deployment in self._routable_deployments(model_name, team_id)
)
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

View file

@ -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",

View file

@ -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 = [

View file

@ -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"}

View file

@ -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"

View file

@ -9,6 +9,7 @@ from litellm.llms.bedrock.common_utils import(
NativeOutputFallback,
bedrock_native_output_is_opt_in,
bedrock_strict_tools_fallback,
bedrock_vendor_model_name,
deployment_model_info,
extract_model_name_from_bedrock_arn,
get_bedrock_base_model,
@ -302,6 +303,23 @@ 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"),
("gpt-5.5", "gpt-5.5"),
("unknownvendor.some-model", "unknownvendor.some-model"),
],
)
def test_bedrock_vendor_model_name_strips_routing_region_and_vendor_prefixes_only(model, expected):
assert bedrock_vendor_model_name(model) == expected
def test_output_config_effort_normalization_uses_model_info_ceiling(monkeypatch):
import litellm.llms.bedrock.common_utils as mod

View file

@ -1037,7 +1037,7 @@ 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

View file

@ -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,7 +19,9 @@ 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"}
@ -85,8 +88,8 @@ def test_body_is_exactly_codex_models_response():
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`
"""Codex 0.166.0 `codex-rs/protocol/src/openai_models.rs` ModelInfo, the fields with no
`#[serde(default)]`, read on 2026-11-02; `base_instructions` because its `ModelsResponse`
decoder rejects an entry with neither it nor `model_messages.instructions_template`."""
(entry,) = _models(_row("gpt-6-astra"))
@ -116,25 +119,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()
@ -724,7 +716,7 @@ 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",
@ -746,6 +738,9 @@ def test_catalog_rows_read_the_router_by_each_entry_lookup_id():
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,7 +760,8 @@ 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,
@ -944,3 +940,211 @@ 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(
"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, dict]:
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"),
]
)
served: Final = _served(router, "claude-like", "plain")
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"], served["plain"]["supports_reasoning_summary_parameter"]) == (
None,
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"]
priority_price: Final = {"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_name": "configured",
"litellm_params": upstream("e"),
"model_info": {"service_tiers": ["priority"]},
},
]
)
served: Final = _served(router, "all-priced", "half-priced", "configured")
assert _tier_fields(served["all-priced"]) == _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 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}))

View file

@ -0,0 +1,218 @@
import json
from itertools import product
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,
every_deployment_prices,
)
_PROMPT: Final = "base prompt"
_SILENT: Final = CodexDeploymentCapabilities()
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", "when_true", "otherwise"),
[
("supports_function_calling", "apply_patch_tool_type", "freeform", None),
("supports_reasoning", "supports_reasoning_summary_parameter", True, False),
("supports_reasoning", "supports_reasoning_summaries", True, False),
("supports_parallel_function_calling", "supports_parallel_tool_calls", True, False),
],
)
@pytest.mark.parametrize("value", [True, False, None])
def test_a_feature_that_sends_something_upstream_is_on_only_when_every_deployment_declares_it(
flag, field, when_true, otherwise, value
):
entry: Final = _entry(CodexCapabilities(**{flag: value}))
assert entry[field] == (when_true if value is True else otherwise)
@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(
"flags", list(product((True, False, None), repeat=4)), ids=lambda flags: "-".join(map(str, flags))
)
def test_every_entry_carries_the_fields_and_enum_values_every_supported_codex_decodes(flags):
"""`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."""
vision, tools, parallel, reasoning = flags
entry: Final = _entry(
CodexCapabilities(
context_window=128_000,
supports_vision=vision,
supports_function_calling=tools,
supports_parallel_function_calling=parallel,
supports_reasoning=reasoning,
)
)
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(
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", "priced"),
[
((_priced("priority"), _priced("priority", "flex")), "priority", True),
((_priced("priority"), _priced("flex")), "priority", False),
((_priced("priority"), _SILENT), "priority", False),
((_priced("priority"),), "fast", True),
((_priced("flex"),), "fast", False),
((_priced("priority", "flex"),), "made-up-tier", False),
((), "priority", True),
],
)
def test_a_tier_is_priced_only_when_every_deployment_prices_it_the_way_cost_tracking_does(deployments, tier, priced):
assert every_deployment_prices(tier, deployments) is priced

View file

@ -59,6 +59,7 @@ def patched_models(monkeypatch):
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)

View file

@ -125,6 +125,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":