mirror of
https://github.com/BerriAI/litellm.git
synced 2026-11-15 09:12:18 +00:00
feat(proxy): ask OpenAI for the Codex catalog before GitHub, keyed by the deployment's OpenAI key
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
parent
edf483ef3c
commit
f1b51ee9f3
6 changed files with 234 additions and 22 deletions
|
|
@ -18,7 +18,7 @@ import asyncio
|
|||
import os
|
||||
import re
|
||||
import time
|
||||
from collections import OrderedDict
|
||||
from collections import Counter, OrderedDict
|
||||
from collections.abc import Awaitable, Callable
|
||||
from dataclasses import dataclass
|
||||
from functools import cache
|
||||
|
|
@ -38,6 +38,7 @@ from litellm.proxy.common_utils.codex_model_catalog import (
|
|||
codex_base_instructions,
|
||||
)
|
||||
|
||||
CODEX_CATALOG_OPENAI_URL: Final = "https://chatgpt.com/backend-api/codex/models"
|
||||
CODEX_CATALOG_BASE_URL: Final = "https://raw.githubusercontent.com/openai/codex"
|
||||
CODEX_CATALOG_RETRY_SECONDS: Final = 300.0
|
||||
CODEX_CATALOG_FETCH_TIMEOUT_SECONDS: Final = 10.0
|
||||
|
|
@ -49,7 +50,13 @@ _PROMPT_PATH: Final = "codex-rs/models-manager/prompt.md"
|
|||
|
||||
|
||||
class _AsyncGetClient(Protocol):
|
||||
def get(self, url: str, *, timeout: float | None = None) -> Awaitable[httpx.Response]: ...
|
||||
def get(
|
||||
self,
|
||||
url: str,
|
||||
*,
|
||||
headers: dict[str, str] | None = None, # mutable-ok: httpx's own get signature
|
||||
timeout: float | None = None,
|
||||
) -> Awaitable[httpx.Response]: ...
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
|
|
@ -103,6 +110,7 @@ class CodexCatalogSource:
|
|||
*,
|
||||
client: _AsyncGetClient | None = None,
|
||||
base_url: str | None = None,
|
||||
openai_url: str | None = None,
|
||||
clock: Callable[[], float] = time.monotonic,
|
||||
retry_seconds: float = CODEX_CATALOG_RETRY_SECONDS,
|
||||
timeout: float = CODEX_CATALOG_FETCH_TIMEOUT_SECONDS,
|
||||
|
|
@ -112,6 +120,11 @@ class CodexCatalogSource:
|
|||
self._base_url = (
|
||||
base_url if base_url is not None else os.getenv("LITELLM_CODEX_CATALOG_BASE_URL", CODEX_CATALOG_BASE_URL)
|
||||
).rstrip("/")
|
||||
self._openai_url = (
|
||||
openai_url
|
||||
if openai_url is not None
|
||||
else os.getenv("LITELLM_CODEX_CATALOG_OPENAI_URL", CODEX_CATALOG_OPENAI_URL)
|
||||
)
|
||||
self._clock = clock
|
||||
self._retry_seconds = retry_seconds
|
||||
self._timeout = timeout
|
||||
|
|
@ -119,9 +132,11 @@ class CodexCatalogSource:
|
|||
self._cache: OrderedDict[str, _Cached] = OrderedDict() # mutable-ok: filled as releases are first asked for
|
||||
self._pending: dict[str, asyncio.Task[CodexStock]] = {} # mutable-ok: the fetches in flight, one per release
|
||||
|
||||
async def stock(self, client_version: str | None) -> CodexStock:
|
||||
async def stock(self, client_version: str | None, *, openai_api_key: str | None = None) -> CodexStock:
|
||||
"""The catalog for ``client_version``: OpenAI's live one when an OpenAI key is at hand,
|
||||
else the release's models.json on GitHub, else the bundled pair."""
|
||||
tag: Final = codex_release_tag(client_version)
|
||||
if tag is None or uses_vendored_codex_catalog():
|
||||
if tag is None or client_version is None or uses_vendored_codex_catalog():
|
||||
return vendored_codex_stock()
|
||||
cached: Final = self._cache.get(tag)
|
||||
if cached is not None and (cached.retry_at is None or cached.retry_at > self._clock()):
|
||||
|
|
@ -130,13 +145,13 @@ class CodexCatalogSource:
|
|||
pending: Final = self._pending.get(tag)
|
||||
if pending is not None:
|
||||
return await asyncio.shield(pending)
|
||||
task: Final = asyncio.ensure_future(self._resolve(tag))
|
||||
task: Final = asyncio.ensure_future(self._resolve(tag, client_version, openai_api_key))
|
||||
self._pending[tag] = task
|
||||
task.add_done_callback(lambda _: self._pending.pop(tag, None))
|
||||
return await asyncio.shield(task)
|
||||
|
||||
async def _resolve(self, tag: str) -> CodexStock:
|
||||
fetched: Final = await self._fetch(tag)
|
||||
async def _resolve(self, tag: str, version: str, openai_api_key: str | None) -> CodexStock:
|
||||
fetched: Final = await self._fetch(tag, version, openai_api_key)
|
||||
if isinstance(fetched, _FetchFailed):
|
||||
verbose_proxy_logger.warning(
|
||||
"Codex catalog %s not fetched (%s), serving the %s one for the next %.0fs",
|
||||
|
|
@ -146,7 +161,9 @@ class CodexCatalogSource:
|
|||
self._retry_seconds,
|
||||
)
|
||||
else:
|
||||
verbose_proxy_logger.info("Codex catalog %s fetched, %d models", tag, len(fetched.models))
|
||||
verbose_proxy_logger.info(
|
||||
"Codex catalog %s fetched from %s, %d models", tag, fetched.source, len(fetched.models)
|
||||
)
|
||||
entry: Final = (
|
||||
_Cached(stock=vendored_codex_stock(), retry_at=self._clock() + self._retry_seconds)
|
||||
if isinstance(fetched, _FetchFailed)
|
||||
|
|
@ -158,7 +175,35 @@ class CodexCatalogSource:
|
|||
self._cache.popitem(last=False)
|
||||
return entry.stock
|
||||
|
||||
async def _fetch(self, tag: str) -> CodexStock | _FetchFailed:
|
||||
async def _fetch(self, tag: str, version: str, openai_api_key: str | None) -> CodexStock | _FetchFailed:
|
||||
if openai_api_key:
|
||||
fetched: Final = await self._fetch_openai(version, openai_api_key)
|
||||
if not isinstance(fetched, _FetchFailed):
|
||||
return fetched
|
||||
verbose_proxy_logger.info(
|
||||
"Codex catalog %s not fetched from OpenAI (%s), trying GitHub", tag, fetched.reason
|
||||
)
|
||||
return await self._fetch_github(tag)
|
||||
|
||||
async def _fetch_openai(self, version: str, openai_api_key: str) -> CodexStock | _FetchFailed:
|
||||
url: Final = f"{self._openai_url}?client_version={version}"
|
||||
body: Final = await self._get(url, headers={"Authorization": f"Bearer {openai_api_key}"})
|
||||
if isinstance(body, _FetchFailed):
|
||||
return body
|
||||
try:
|
||||
catalog: Final = CodexStockCatalog.model_validate_json(body)
|
||||
except ValidationError as error:
|
||||
return _FetchFailed(reason=f"{url} body does not parse: {type(error).__name__}")
|
||||
prompts: Final = Counter(model.base_instructions for model in catalog.models if model.base_instructions.strip())
|
||||
if not catalog.models or not prompts:
|
||||
return _FetchFailed(reason=f"{url} catalog is empty")
|
||||
return CodexStock(
|
||||
models=MappingProxyType({model.slug: model for model in catalog.models}),
|
||||
instructions=prompts.most_common(1)[0][0],
|
||||
source=f"openai {version}",
|
||||
)
|
||||
|
||||
async def _fetch_github(self, tag: str) -> CodexStock | _FetchFailed:
|
||||
models, prompt = await asyncio.gather(
|
||||
self._get(f"{self._base_url}/{tag}/{_MODELS_PATH}"), self._get(f"{self._base_url}/{tag}/{_PROMPT_PATH}")
|
||||
)
|
||||
|
|
@ -176,13 +221,17 @@ class CodexCatalogSource:
|
|||
return CodexStock(
|
||||
models=MappingProxyType({model.slug: model for model in catalog.models}),
|
||||
instructions=instructions,
|
||||
source=tag,
|
||||
source=f"github {tag}",
|
||||
)
|
||||
|
||||
async def _get(self, url: str) -> bytes | _FetchFailed:
|
||||
async def _get(
|
||||
self,
|
||||
url: str,
|
||||
headers: dict[str, str] | None = None, # mutable-ok: httpx's own get signature
|
||||
) -> bytes | _FetchFailed:
|
||||
client: Final = self._client if self._client is not None else _default_client()
|
||||
try:
|
||||
response: Final = await client.get(url, timeout=self._timeout)
|
||||
response: Final = await client.get(url, headers=headers, timeout=self._timeout)
|
||||
except httpx.HTTPError as error:
|
||||
return _FetchFailed(reason=f"{type(error).__name__} fetching {url}")
|
||||
if response.status_code != 200:
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ newer tag and renaming the json after it.
|
|||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from collections.abc import Mapping, Sequence
|
||||
from dataclasses import dataclass
|
||||
from functools import cache
|
||||
|
|
@ -36,12 +37,14 @@ from pydantic import ConfigDict, Field, StringConstraints, TypeAdapter, Validati
|
|||
|
||||
from litellm._logging import verbose_proxy_logger
|
||||
from litellm.litellm_core_utils.reasoning_effort_strength import nearest_declared_reasoning_effort
|
||||
from litellm.secret_managers.main import get_secret_str
|
||||
from litellm.types.llms.base_model import LiteLLMBaseModel
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from litellm.router import Router
|
||||
from litellm.router_api import RouterRegistry
|
||||
from litellm.types.proxy.model_listing import ModelInfoResponse
|
||||
from litellm.types.router import DeploymentTypedDict
|
||||
|
||||
CODEX_BASE_INSTRUCTIONS_PATH: Final = Path(__file__).with_name("codex_base_instructions.md")
|
||||
CODEX_BUNDLED_VERSION: Final = "0.166.0"
|
||||
|
|
@ -164,6 +167,7 @@ class CodexStockModel(LiteLLMBaseModel):
|
|||
supported_reasoning_levels: tuple[CodexReasoningLevel, ...] = ()
|
||||
context_window: int | None = None
|
||||
max_context_window: int | None = None
|
||||
base_instructions: str = ""
|
||||
|
||||
|
||||
class CodexStockCatalog(LiteLLMBaseModel):
|
||||
|
|
@ -565,6 +569,38 @@ def _catalog_row(
|
|||
)
|
||||
|
||||
|
||||
def _deployment_openai_key(deployment: DeploymentTypedDict) -> str | None:
|
||||
params: Final = MappingProxyType[str, object](deployment["litellm_params"])
|
||||
model: Final = params.get("model")
|
||||
provider: Final = params.get("custom_llm_provider") or (model.partition("/")[0] if isinstance(model, str) else "")
|
||||
api_key: Final = params.get("api_key")
|
||||
if provider != "openai" or not isinstance(api_key, str) or not api_key:
|
||||
return None
|
||||
return (get_secret_str(api_key) if api_key.startswith("os.environ/") else api_key) or None
|
||||
|
||||
|
||||
def codex_catalog_openai_key(
|
||||
llm_router: Router | None,
|
||||
entries: Sequence[tuple[str, str]],
|
||||
team_id: str | None = None,
|
||||
) -> str | None:
|
||||
"""The OpenAI API key the catalog fetch authenticates with: the first one among the
|
||||
listed entries' OpenAI deployments, else ``OPENAI_API_KEY`` from the environment."""
|
||||
registry: Final = llm_router.registry() if llm_router is not None else None
|
||||
deployments: Final = (
|
||||
tuple(
|
||||
deployment
|
||||
for _, lookup_id in entries
|
||||
for deployment in registry.deployments(
|
||||
lookup_id, source="all", team_id=team_id
|
||||
) # comprehension-ok: flattening the entries' deployment lists
|
||||
)
|
||||
if registry is not None
|
||||
else ()
|
||||
)
|
||||
return next(filter(None, map(_deployment_openai_key, deployments)), None) or os.getenv("OPENAI_API_KEY") or None
|
||||
|
||||
|
||||
def codex_catalog_rows(
|
||||
rows: Sequence[ModelInfoResponse],
|
||||
entries: Sequence[tuple[str, str]],
|
||||
|
|
|
|||
|
|
@ -514,7 +514,7 @@ from litellm.proxy.common_utils.auth_cache_invalidation_pubsub import (
|
|||
)
|
||||
from litellm.proxy.common_utils.callback_utils import initialize_callbacks_on_proxy
|
||||
from litellm.proxy.common_utils.codex_catalog_source import codex_catalog_source
|
||||
from litellm.proxy.common_utils.codex_model_catalog import codex_model_list_body
|
||||
from litellm.proxy.common_utils.codex_model_catalog import codex_catalog_openai_key, codex_model_list_body
|
||||
from litellm.proxy.common_utils.config_includes import resolve_include_file_path, resolve_includes
|
||||
from litellm.proxy.common_utils.config_sync_pubsub import ConfigSyncSubscriber
|
||||
from litellm.proxy.common_utils.credential_hydration import decrypted_or_stored
|
||||
|
|
@ -12815,7 +12815,12 @@ async def model_list(
|
|||
llm_router,
|
||||
team_id or user_api_key_dict.team_id,
|
||||
client_version=client_version,
|
||||
stock=await codex_catalog_source().stock(client_version),
|
||||
stock=await codex_catalog_source().stock(
|
||||
client_version,
|
||||
openai_api_key=codex_catalog_openai_key(
|
||||
llm_router, admin_entries, team_id or user_api_key_dict.team_id
|
||||
),
|
||||
),
|
||||
),
|
||||
media_type="application/json",
|
||||
)
|
||||
|
|
@ -12898,7 +12903,10 @@ async def model_list(
|
|||
team_id or user_api_key_dict.team_id,
|
||||
labels,
|
||||
client_version=client_version,
|
||||
stock=await codex_catalog_source().stock(client_version),
|
||||
stock=await codex_catalog_source().stock(
|
||||
client_version,
|
||||
openai_api_key=codex_catalog_openai_key(llm_router, entries, team_id or user_api_key_dict.team_id),
|
||||
),
|
||||
),
|
||||
media_type="application/json",
|
||||
)
|
||||
|
|
|
|||
|
|
@ -31,6 +31,11 @@ BUNDLED_STOCK_PATH: Final = ROOT / "litellm" / "proxy" / "common_utils" / "codex
|
|||
BASE_INSTRUCTIONS_PATH: Final = ROOT / "litellm" / "proxy" / "common_utils" / "codex_base_instructions.md"
|
||||
PUBLISHED_VERSION: Final = "0.999.0"
|
||||
UNPUBLISHED_VERSION: Final = "0.998.0"
|
||||
OPENAI_VERSION: Final = "0.997.0"
|
||||
OPENAI_SLUG: Final = "gpt-openai-release"
|
||||
OPENAI_PROMPT: Final = "# Base prompt OpenAI serves for its release"
|
||||
OPENAI_MODELS_PATH: Final = "/codex/models"
|
||||
DEPLOYMENT_KEY: Final = "integration-provider-key"
|
||||
FRESH_SLUG: Final = "gpt-fresh-release"
|
||||
FRESH_PROMPT: Final = "# Base prompt of the published release"
|
||||
MODELS_PATH: Final = "codex-rs/models-manager/models.json"
|
||||
|
|
@ -57,7 +62,21 @@ def _published_entry() -> dict[str, JsonValue]:
|
|||
return {**_vendored_entry("gpt-5.5"), "slug": FRESH_SLUG, "display_name": "Fresh Release", "upgrade": None}
|
||||
|
||||
|
||||
def _openai_entry() -> dict[str, JsonValue]:
|
||||
return {
|
||||
**_published_entry(),
|
||||
"slug": OPENAI_SLUG,
|
||||
"display_name": "OpenAI Release",
|
||||
"base_instructions": OPENAI_PROMPT,
|
||||
}
|
||||
|
||||
|
||||
def _raw_tree(request: Request) -> Reply:
|
||||
if request.target.startswith(f"{OPENAI_MODELS_PATH}?"):
|
||||
served: Final = request.target == f"{OPENAI_MODELS_PATH}?client_version={OPENAI_VERSION}"
|
||||
if served and request.headers.get("authorization") == f"Bearer {DEPLOYMENT_KEY}":
|
||||
return Reply(body=json.dumps({"models": [_openai_entry()]}).encode())
|
||||
return Reply(status=401, body=b'{"detail":"Unauthorized"}')
|
||||
if request.target == f"/rust-v{PUBLISHED_VERSION}/{MODELS_PATH}":
|
||||
return Reply(body=json.dumps({"models": [_published_entry()]}).encode())
|
||||
if request.target == f"/rust-v{PUBLISHED_VERSION}/{PROMPT_PATH}":
|
||||
|
|
@ -83,7 +102,11 @@ def raw_tree() -> Iterator[Wire]:
|
|||
def gateway(tmp_path_factory: pytest.TempPathFactory, raw_tree: Wire) -> Iterator[Gateway]:
|
||||
with gateway_from_environment() as parent_gateway:
|
||||
directory: Final = tmp_path_factory.mktemp("codex_catalog_fetch")
|
||||
overrides: Final = {"LITELLM_LOCAL_CODEX_CATALOG": "False", "LITELLM_CODEX_CATALOG_BASE_URL": raw_tree.url}
|
||||
overrides: Final = {
|
||||
"LITELLM_LOCAL_CODEX_CATALOG": "False",
|
||||
"LITELLM_CODEX_CATALOG_BASE_URL": raw_tree.url,
|
||||
"LITELLM_CODEX_CATALOG_OPENAI_URL": f"{raw_tree.url}{OPENAI_MODELS_PATH}",
|
||||
}
|
||||
config: Final = _config_without_model_list(directory)
|
||||
with owned_proxy(parent_gateway, directory, overrides, config=config) as isolated_gateway:
|
||||
yield isolated_gateway
|
||||
|
|
@ -108,6 +131,28 @@ def _catalog(gateway: Gateway, client_version: str) -> dict[str, dict[str, JsonV
|
|||
return {string_value(object_value(entry)["slug"]): object_value(entry) for entry in models}
|
||||
|
||||
|
||||
def test_a_release_openai_serves_is_fetched_from_openai_with_the_deployment_key_and_github_is_skipped(
|
||||
gateway: Gateway, raw_tree: Wire
|
||||
) -> None:
|
||||
assert OPENAI_SLUG not in _vendored_slugs()
|
||||
with gateway.scenario() as scenario:
|
||||
scenario.model(name=OPENAI_SLUG)
|
||||
listing: Final = _catalog_listing(gateway, OPENAI_VERSION, OPENAI_SLUG)
|
||||
expected: Final = {
|
||||
**_openai_entry(),
|
||||
"priority": listing[OPENAI_SLUG]["priority"],
|
||||
"visibility": "list",
|
||||
"supported_in_api": True,
|
||||
"upgrade": None,
|
||||
}
|
||||
assert listing[OPENAI_SLUG] == expected, listing[OPENAI_SLUG]
|
||||
requests: Final = raw_tree.drain()
|
||||
openai_requests: Final = [request for request in requests if request.target.startswith(OPENAI_MODELS_PATH)]
|
||||
assert len(openai_requests) == gateway.workers, [request.target for request in requests]
|
||||
assert {request.headers.get("authorization") for request in openai_requests} == {f"Bearer {DEPLOYMENT_KEY}"}
|
||||
assert not any(request.target.startswith(f"/rust-v{OPENAI_VERSION}/") for request in requests), requests
|
||||
|
||||
|
||||
def test_a_published_release_gets_its_own_entry_for_a_model_the_vendored_pair_lacks(
|
||||
gateway: Gateway, raw_tree: Wire
|
||||
) -> None:
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ from pydantic import JsonValue, TypeAdapter
|
|||
|
||||
from litellm.proxy.common_utils.codex_catalog_source import (
|
||||
CODEX_CATALOG_BASE_URL,
|
||||
CODEX_CATALOG_OPENAI_URL,
|
||||
CodexCatalogSource,
|
||||
codex_release_tag,
|
||||
vendored_codex_stock,
|
||||
|
|
@ -44,9 +45,13 @@ class _RawTree:
|
|||
def __init__(self, replies: Mapping[str, tuple[int, bytes]]) -> None:
|
||||
self.replies = replies
|
||||
self.calls: list[str] = []
|
||||
self.headers: list[dict[str, str] | None] = []
|
||||
|
||||
async def get(self, url: str, *, timeout: float | None = None) -> httpx.Response:
|
||||
async def get(
|
||||
self, url: str, *, headers: dict[str, str] | None = None, timeout: float | None = None
|
||||
) -> httpx.Response:
|
||||
self.calls.append(url)
|
||||
self.headers.append(headers)
|
||||
status, body = self.replies.get(url, (404, b"404: Not Found"))
|
||||
return httpx.Response(status, content=body)
|
||||
|
||||
|
|
@ -96,7 +101,7 @@ def test_the_requesting_release_catalog_serves_its_entry_for_a_model_the_vendore
|
|||
tree = _RawTree(_tree(RELEASE))
|
||||
stock = asyncio.run(_source(tree).stock(RELEASE))
|
||||
entry = _entry(stock.models, stock.instructions, FRESH_SLUG)
|
||||
assert stock.source == f"rust-v{RELEASE}"
|
||||
assert stock.source == f"github rust-v{RELEASE}"
|
||||
assert entry["shell_type"] == "shell_command" and entry["apply_patch_tool_type"] == "freeform", entry
|
||||
assert entry["supports_parallel_tool_calls"] is True, entry
|
||||
assert sorted(tree.calls) == sorted(_tree(RELEASE)), tree.calls
|
||||
|
|
@ -111,7 +116,7 @@ def test_a_release_is_fetched_once_and_concurrent_requests_share_the_fetch() ->
|
|||
again = await source.stock(RELEASE)
|
||||
return [stock.source for stock in (*stocks, again)]
|
||||
|
||||
assert asyncio.run(burst()) == [f"rust-v{RELEASE}"] * 6
|
||||
assert asyncio.run(burst()) == [f"github rust-v{RELEASE}"] * 6
|
||||
assert len(tree.calls) == 2, tree.calls
|
||||
|
||||
|
||||
|
|
@ -128,7 +133,7 @@ def test_a_release_without_a_tag_serves_the_vendored_pair_and_is_retried_after_t
|
|||
assert len(tree.calls) == calls_after_first, "a failed fetch was retried inside its window"
|
||||
clock.now += 2
|
||||
tree.replies = _tree(RELEASE)
|
||||
assert asyncio.run(source.stock(RELEASE)).source == f"rust-v{RELEASE}"
|
||||
assert asyncio.run(source.stock(RELEASE)).source == f"github rust-v{RELEASE}"
|
||||
assert len(tree.calls) > calls_after_first, "a failed fetch was never retried"
|
||||
|
||||
|
||||
|
|
@ -152,7 +157,9 @@ def test_a_release_whose_pair_does_not_parse_serves_the_vendored_pair(replies: M
|
|||
|
||||
def test_a_transport_error_serves_the_vendored_pair() -> None:
|
||||
class _Down:
|
||||
async def get(self, url: str, *, timeout: float | None = None) -> httpx.Response:
|
||||
async def get(
|
||||
self, url: str, *, headers: dict[str, str] | None = None, timeout: float | None = None
|
||||
) -> httpx.Response:
|
||||
raise httpx.ConnectError("offline", request=httpx.Request("GET", url))
|
||||
|
||||
stock = asyncio.run(CodexCatalogSource(client=_Down(), clock=_Clock()).stock(RELEASE))
|
||||
|
|
@ -191,7 +198,7 @@ def test_the_base_url_setting_points_the_fetch_at_a_mirror(monkeypatch: pytest.M
|
|||
for url, reply in _tree(RELEASE).items()
|
||||
}
|
||||
tree = _RawTree(mirrored)
|
||||
assert asyncio.run(_source(tree).stock(RELEASE)).source == f"rust-v{RELEASE}"
|
||||
assert asyncio.run(_source(tree).stock(RELEASE)).source == f"github rust-v{RELEASE}"
|
||||
assert sorted(tree.calls) == sorted(mirrored), tree.calls
|
||||
|
||||
|
||||
|
|
@ -211,3 +218,36 @@ def test_the_cache_keeps_only_the_newest_releases() -> None:
|
|||
newest_models_url = next(url for url in _tree(versions[-1]) if url.endswith("models.json"))
|
||||
assert tree.calls.count(oldest_models_url) == 2, tree.calls
|
||||
assert tree.calls.count(newest_models_url) == 1, tree.calls
|
||||
|
||||
|
||||
OPENAI_KEY: Final = "sk-unit-openai-key"
|
||||
OPENAI_ENTRY: Final = {**FRESH_ENTRY, "shell_type": "unified_exec", "base_instructions": "# OpenAI live prompt"}
|
||||
OPENAI_CATALOG: Final = json.dumps({"models": [OPENAI_ENTRY]}).encode()
|
||||
|
||||
|
||||
def _openai_url(version: str) -> str:
|
||||
return f"{CODEX_CATALOG_OPENAI_URL}?client_version={version}"
|
||||
|
||||
|
||||
def test_an_openai_key_fetches_the_live_catalog_from_openai_with_that_key_and_skips_github() -> None:
|
||||
tree = _RawTree({**_tree(RELEASE), _openai_url(RELEASE): (200, OPENAI_CATALOG)})
|
||||
stock = asyncio.run(_source(tree).stock(RELEASE, openai_api_key=OPENAI_KEY))
|
||||
entry = _entry(stock.models, stock.instructions, FRESH_SLUG)
|
||||
assert stock.source == f"openai {RELEASE}" and stock.instructions == "# OpenAI live prompt", stock.source
|
||||
assert entry["shell_type"] == "unified_exec", entry
|
||||
assert tree.calls == [_openai_url(RELEASE)] and tree.headers == [{"Authorization": f"Bearer {OPENAI_KEY}"}]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("status", [401, 500])
|
||||
def test_a_refused_openai_fetch_falls_back_to_the_release_tag_on_github(status: int) -> None:
|
||||
tree = _RawTree({**_tree(RELEASE), _openai_url(RELEASE): (status, b"{}")})
|
||||
stock = asyncio.run(_source(tree).stock(RELEASE, openai_api_key=OPENAI_KEY))
|
||||
assert stock.source == f"github rust-v{RELEASE}" and FRESH_SLUG in stock.models
|
||||
assert tree.calls[0] == _openai_url(RELEASE) and sorted(tree.calls[1:]) == sorted(_tree(RELEASE)), tree.calls
|
||||
|
||||
|
||||
def test_without_an_openai_key_openai_is_never_asked() -> None:
|
||||
tree = _RawTree({**_tree(RELEASE), _openai_url(RELEASE): (200, OPENAI_CATALOG)})
|
||||
stock = asyncio.run(_source(tree).stock(RELEASE))
|
||||
assert stock.source == f"github rust-v{RELEASE}"
|
||||
assert _openai_url(RELEASE) not in tree.calls and tree.headers == [None, None], tree.calls
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ from litellm.proxy.common_utils.codex_model_catalog import (
|
|||
CodexStockModel,
|
||||
CodexStockUpgrade,
|
||||
bundled_codex_models,
|
||||
codex_catalog_openai_key,
|
||||
codex_catalog_rows,
|
||||
codex_model_list_body,
|
||||
codex_models_response_json,
|
||||
|
|
@ -944,3 +945,36 @@ def test_codex_tool_search_is_offered_off_the_route_and_model_info_and_only_when
|
|||
"mixed": False,
|
||||
}
|
||||
assert models[0]["context_window"] == 1000000
|
||||
|
||||
|
||||
def _router_with(*litellm_params: Mapping[str, str]) -> litellm.Router:
|
||||
return litellm.Router(
|
||||
model_list=[
|
||||
{"model_name": f"catalog-model-{index}", "litellm_params": dict(params)}
|
||||
for index, params in enumerate(litellm_params)
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
def test_the_catalog_key_is_the_listed_openai_deployments_key_before_the_environments(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
monkeypatch.setenv("OPENAI_API_KEY", "sk-from-env")
|
||||
monkeypatch.setenv("CATALOG_OPENAI_KEY", "sk-from-secret-ref")
|
||||
router: Final = _router_with(
|
||||
{"model": "anthropic/claude-sonnet-4-5", "api_key": "sk-ant-not-openai"},
|
||||
{"model": "openai/gpt-5.5", "api_key": "os.environ/CATALOG_OPENAI_KEY"},
|
||||
{"model": "openai/gpt-5.5", "api_key": "sk-second-openai"},
|
||||
)
|
||||
entries: Final = [(f"catalog-model-{index}", f"catalog-model-{index}") for index in range(3)]
|
||||
assert codex_catalog_openai_key(router, entries) == "sk-from-secret-ref"
|
||||
assert codex_catalog_openai_key(router, entries[:1]) == "sk-from-env"
|
||||
|
||||
|
||||
def test_the_catalog_key_is_absent_without_an_openai_deployment_or_environment_key(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
monkeypatch.delenv("OPENAI_API_KEY", raising=False)
|
||||
router: Final = _router_with({"model": "anthropic/claude-sonnet-4-5", "api_key": "sk-ant-not-openai"})
|
||||
assert codex_catalog_openai_key(router, [("catalog-model-0", "catalog-model-0")]) is None
|
||||
assert codex_catalog_openai_key(None, [("catalog-model-0", "catalog-model-0")]) is None
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue