mirror of
https://github.com/Routstr/routstr-core.git
synced 2026-10-05 20:28:23 +00:00
fix: make model path identity selectable
This commit is contained in:
+28
-28
@@ -329,8 +329,8 @@ GET /v1/models
|
||||
|
||||
### List Model Paths
|
||||
|
||||
Get the upstream provider paths each advertised model can be reached through.
|
||||
This is discovery data only; routing still chooses the provider per request.
|
||||
Get the selectable upstream routes for each advertised model. This endpoint is
|
||||
discovery-only; request-side selection will be added separately.
|
||||
|
||||
```http
|
||||
GET /v1/models/paths
|
||||
@@ -342,45 +342,45 @@ GET /v1/models/paths
|
||||
{
|
||||
"data": [
|
||||
{
|
||||
"id": "claude-sonnet-4",
|
||||
"id": "anthropic/claude-sonnet-4",
|
||||
"paths": [
|
||||
{"path": "anthropic"},
|
||||
{"path": "openrouter:Anthropic"}
|
||||
{
|
||||
"path": "provider=12",
|
||||
"provider": {"id": 12, "slug": "anthropic-primary", "type": "anthropic"},
|
||||
"endpoint": null
|
||||
},
|
||||
{
|
||||
"path": "provider=42&endpoint=google-vertex%2Fus",
|
||||
"provider": {"id": 42, "slug": "openrouter-main", "type": "openrouter"},
|
||||
"endpoint": {"tag": "google-vertex/us", "name": "Google"}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
],
|
||||
"updated_at": 1753500000
|
||||
}
|
||||
```
|
||||
|
||||
`path` is an opaque, percent-encoded selector. Clients must store and return it
|
||||
unchanged rather than parsing or reconstructing it. The configured provider's
|
||||
stable node-local ID defines the upstream route; no upstream URL is exposed.
|
||||
OpenRouter routes additionally use the exact machine-readable endpoint `tag`.
|
||||
Provider slugs/types and endpoint names are display data and never participate
|
||||
in identity. When request-side selection is implemented, an endpoint tag must
|
||||
not silently fall back to another backend.
|
||||
|
||||
### List Paths for One Model
|
||||
|
||||
Use a query parameter so model IDs containing `/` are handled safely. Lookup is
|
||||
by the public, unqualified model ID: `glm-5v-turbo` resolves
|
||||
`z-ai/glm-5v-turbo`, and `deepseek-v4-pro` and `deepseek/deepseek-v4-pro`
|
||||
return the same merged path set.
|
||||
Use the exact model ID advertised by `/v1/models`. The query parameter safely
|
||||
supports IDs containing `/`.
|
||||
|
||||
```http
|
||||
GET /v1/models/paths/model?model_id=anthropic/claude-sonnet-4
|
||||
```
|
||||
|
||||
**Response:**
|
||||
|
||||
```json
|
||||
{
|
||||
"data": [
|
||||
{"path": "anthropic"},
|
||||
{"path": "openrouter:Anthropic"}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Model IDs in responses are base model IDs: the leading provider prefix such as
|
||||
`z-ai/` or `openai/` is stripped (the same rule routing uses, so the ID can be
|
||||
sent back to `/v1/chat/completions` verbatim). Path values match the provider
|
||||
string stamped on chat-completion responses, such as `anthropic`,
|
||||
`generic:Anthropic`, `openrouter:Anthropic`, or `unknown` (native OpenRouter
|
||||
with no usable sub-provider). Responses also carry an `updated_at` Unix
|
||||
timestamp of the last successful refresh (`null` when no refresh has run).
|
||||
The response uses the same path objects and `updated_at` field as the collection
|
||||
endpoint. An unknown model returns `404 Model not found`. A known model whose
|
||||
paths have not been discovered yet returns `200` with an empty `data` array.
|
||||
|
||||
## Wallet Management
|
||||
|
||||
|
||||
@@ -22,6 +22,10 @@ def upgrade() -> None:
|
||||
sa.Column("id", sa.Integer(), nullable=False),
|
||||
sa.Column("model_id", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
|
||||
sa.Column("path", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
|
||||
sa.Column("provider_slug", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
|
||||
sa.Column("provider_type", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
|
||||
sa.Column("endpoint_tag", sqlmodel.sql.sqltypes.AutoString(), nullable=True),
|
||||
sa.Column("endpoint_name", sqlmodel.sql.sqltypes.AutoString(), nullable=True),
|
||||
sa.Column("upstream_provider_id", sa.Integer(), nullable=False),
|
||||
sa.Column("updated_at", sa.Integer(), nullable=False, server_default="0"),
|
||||
sa.ForeignKeyConstraint(
|
||||
|
||||
@@ -51,6 +51,23 @@ ADMIN_SESSION_DURATION = 3600
|
||||
MAX_USAGE_ANALYTICS_HOURS = 365 * 24
|
||||
|
||||
|
||||
async def _refresh_provider_model_paths(upstream_provider_id: int) -> None:
|
||||
"""Best-effort immediate discovery sync after an admin mutation."""
|
||||
from ..upstream.model_paths import refresh_model_paths_for_provider
|
||||
|
||||
try:
|
||||
await refresh_model_paths_for_provider(upstream_provider_id)
|
||||
except Exception as exc: # noqa: BLE001 - committed admin writes must survive
|
||||
logger.warning(
|
||||
"Failed to refresh model paths after admin mutation",
|
||||
extra={
|
||||
"upstream_provider_id": upstream_provider_id,
|
||||
"error": str(exc),
|
||||
"error_type": type(exc).__name__,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
async def require_admin_api(request: Request) -> None:
|
||||
auth_header = request.headers.get("Authorization")
|
||||
if not auth_header or not auth_header.startswith("Bearer "):
|
||||
@@ -579,6 +596,7 @@ async def upsert_provider_model(
|
||||
await session.refresh(row)
|
||||
|
||||
await refresh_model_maps()
|
||||
await _refresh_provider_model_paths(provider_pk)
|
||||
return _row_to_model(
|
||||
row, apply_provider_fee=True, provider_fee=provider.provider_fee
|
||||
).dict() # type: ignore
|
||||
@@ -633,6 +651,7 @@ async def delete_provider_model(provider_id: str, model_id: str) -> dict[str, ob
|
||||
await session.delete(row)
|
||||
await session.commit()
|
||||
await refresh_model_maps()
|
||||
await _refresh_provider_model_paths(provider_pk)
|
||||
return {"ok": True, "deleted_id": model_id}
|
||||
|
||||
|
||||
@@ -652,6 +671,7 @@ async def delete_all_provider_models(provider_id: str) -> dict[str, object]:
|
||||
await session.delete(row) # type: ignore
|
||||
await session.commit()
|
||||
await refresh_model_maps()
|
||||
await _refresh_provider_model_paths(provider_pk)
|
||||
return {"ok": True, "deleted": len(rows)}
|
||||
|
||||
|
||||
@@ -705,6 +725,9 @@ async def batch_override_provider_models(
|
||||
json.dumps(model_data.alias_ids) if model_data.alias_ids else None
|
||||
)
|
||||
existing_row.enabled = model_data.enabled
|
||||
existing_row.forwarded_model_id = (
|
||||
model_data.forwarded_model_id or model_data.id
|
||||
)
|
||||
session.add(existing_row)
|
||||
else:
|
||||
# Create new
|
||||
@@ -735,6 +758,7 @@ async def batch_override_provider_models(
|
||||
),
|
||||
upstream_provider_id=provider_pk,
|
||||
enabled=model_data.enabled,
|
||||
forwarded_model_id=model_data.forwarded_model_id or model_data.id,
|
||||
)
|
||||
session.add(row)
|
||||
|
||||
@@ -743,6 +767,7 @@ async def batch_override_provider_models(
|
||||
await session.commit()
|
||||
|
||||
await refresh_model_maps()
|
||||
await _refresh_provider_model_paths(provider_pk)
|
||||
return {
|
||||
"ok": True,
|
||||
"count": overridden_count,
|
||||
@@ -943,6 +968,7 @@ async def create_upstream_provider(
|
||||
|
||||
await reinitialize_upstreams()
|
||||
await refresh_model_maps()
|
||||
await _refresh_provider_model_paths(_provider_pk(provider))
|
||||
return _serialize_provider(provider)
|
||||
|
||||
|
||||
@@ -968,6 +994,7 @@ async def update_upstream_provider(
|
||||
|
||||
await reinitialize_upstreams()
|
||||
await refresh_model_maps()
|
||||
await _refresh_provider_model_paths(_provider_pk(provider))
|
||||
return _serialize_provider(provider)
|
||||
|
||||
|
||||
@@ -1003,6 +1030,7 @@ async def update_upstream_provider_by_slug(
|
||||
|
||||
await reinitialize_upstreams()
|
||||
await refresh_model_maps()
|
||||
await _refresh_provider_model_paths(_provider_pk(provider))
|
||||
return _serialize_provider(provider)
|
||||
|
||||
|
||||
|
||||
+12
-2
@@ -338,8 +338,18 @@ class ModelPathRow(SQLModel, table=True): # type: ignore
|
||||
description="Client-visible /v1/models id (forwarded_model_id or id)"
|
||||
)
|
||||
path: str = Field(
|
||||
description="Provider path stamped on chat completion responses, e.g. "
|
||||
"'anthropic' or 'openrouter:Anthropic'"
|
||||
description="Opaque selector containing provider ID and optional endpoint tag"
|
||||
)
|
||||
provider_slug: str = Field(
|
||||
description="Public slug of the configured upstream provider"
|
||||
)
|
||||
provider_type: str = Field(description="Configured upstream provider type")
|
||||
endpoint_tag: str | None = Field(
|
||||
default=None,
|
||||
description="Exact OpenRouter endpoint tag used for request-side selection",
|
||||
)
|
||||
endpoint_name: str | None = Field(
|
||||
default=None, description="Human-readable endpoint display name"
|
||||
)
|
||||
upstream_provider_id: int = Field(
|
||||
index=True,
|
||||
|
||||
@@ -613,9 +613,17 @@ async def model_paths_for_model(model_id: str) -> dict:
|
||||
model ids containing ``/`` (e.g. ``anthropic/claude-opus-4.6``) need no URL
|
||||
encoding and there is no dynamic-route ambiguity.
|
||||
"""
|
||||
from ..proxy import get_unique_models
|
||||
from ..upstream.model_paths import get_paths_for_model
|
||||
|
||||
return await get_paths_for_model(model_id)
|
||||
result = await get_paths_for_model(model_id)
|
||||
if not result["data"]:
|
||||
advertised_ids = {
|
||||
model.forwarded_model_id or model.id for model in get_unique_models()
|
||||
}
|
||||
if model_id not in advertised_ids:
|
||||
raise HTTPException(status_code=404, detail="Model not found")
|
||||
return result
|
||||
|
||||
|
||||
@models_router.get("/v1/models")
|
||||
|
||||
@@ -4,12 +4,13 @@ Exposes every selectable upstream route a Routstr model is reachable through.
|
||||
This PR remains discovery-only: request-side routing will consume the opaque
|
||||
selectors in a follow-up.
|
||||
|
||||
A path is a standard percent-encoded query string containing the normalized
|
||||
upstream URL and, for an exact OpenRouter endpoint, its machine-readable tag.
|
||||
Display names never participate in identity::
|
||||
A path is a standard percent-encoded query string containing the configured
|
||||
provider's stable node-local ID and, for an exact OpenRouter endpoint, its
|
||||
machine-readable tag. Upstream URLs and display names never participate in or
|
||||
leak through public identity::
|
||||
|
||||
url=https%3A%2F%2Fapi.anthropic.com%2Fv1
|
||||
url=https%3A%2F%2Fopenrouter.ai%2Fapi%2Fv1&provider=google-vertex%2Fus
|
||||
provider=42
|
||||
provider=42&endpoint=google-vertex%2Fus
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -57,15 +58,24 @@ class EndpointIdentity:
|
||||
provider_name: str | None
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ConfiguredProviderIdentity:
|
||||
"""Public-safe identity of one configured upstream provider."""
|
||||
|
||||
id: int
|
||||
slug: str
|
||||
provider_type: str
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class DiscoveredPath:
|
||||
"""One model route ready for persistence and API serialization."""
|
||||
|
||||
model_id: str
|
||||
path: str
|
||||
upstream_url: str
|
||||
provider_tag: str | None = None
|
||||
provider_name: str | None = None
|
||||
provider: ConfiguredProviderIdentity
|
||||
endpoint_tag: str | None = None
|
||||
endpoint_name: str | None = None
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@@ -76,16 +86,11 @@ class ProviderPathSnapshot:
|
||||
preserve_model_ids: frozenset[str] = frozenset()
|
||||
|
||||
|
||||
def normalize_upstream_url(base_url: str) -> str:
|
||||
"""Normalize route identity without changing URL semantics."""
|
||||
return base_url.rstrip("/")
|
||||
|
||||
|
||||
def encode_model_path(base_url: str, provider_tag: str | None = None) -> str:
|
||||
"""Encode a stable opaque selector for future request-side routing."""
|
||||
components = [("url", normalize_upstream_url(base_url))]
|
||||
if provider_tag:
|
||||
components.append(("provider", provider_tag))
|
||||
def encode_model_path(provider_id: int, endpoint_tag: str | None = None) -> str:
|
||||
"""Encode a stable opaque selector without exposing upstream URLs."""
|
||||
components: list[tuple[str, str | int]] = [("provider", provider_id)]
|
||||
if endpoint_tag:
|
||||
components.append(("endpoint", endpoint_tag))
|
||||
return urlencode(components)
|
||||
|
||||
|
||||
@@ -218,9 +223,11 @@ async def _fetch_openrouter_endpoint_subproviders(
|
||||
return None
|
||||
|
||||
try:
|
||||
endpoints = resp.json().get("data", {}).get("endpoints", [])
|
||||
payload = resp.json()
|
||||
data = payload.get("data") if isinstance(payload, dict) else None
|
||||
endpoints = data.get("endpoints") if isinstance(data, dict) else None
|
||||
if not isinstance(endpoints, list):
|
||||
endpoints = []
|
||||
raise ValueError("endpoints must be a list")
|
||||
identities: dict[str, EndpointIdentity] = {}
|
||||
for endpoint in endpoints:
|
||||
if not isinstance(endpoint, dict):
|
||||
@@ -238,6 +245,8 @@ async def _fetch_openrouter_endpoint_subproviders(
|
||||
else None,
|
||||
),
|
||||
)
|
||||
if endpoints and not identities:
|
||||
raise ValueError("endpoints contain no usable tags")
|
||||
result = list(identities.values())
|
||||
except Exception as e: # noqa: BLE001
|
||||
logger.warning(
|
||||
@@ -251,7 +260,9 @@ async def _fetch_openrouter_endpoint_subproviders(
|
||||
|
||||
|
||||
async def _load_model_visibility() -> tuple[
|
||||
dict[ModelKey, ModelRow], set[ModelKey], set[int]
|
||||
dict[ModelKey, ModelRow],
|
||||
set[ModelKey],
|
||||
dict[int, ConfiguredProviderIdentity],
|
||||
]:
|
||||
"""Load the same DB model visibility inputs used by routing.
|
||||
|
||||
@@ -271,12 +282,16 @@ async def _load_model_visibility() -> tuple[
|
||||
|
||||
overrides_by_key: dict[ModelKey, ModelRow] = {}
|
||||
disabled_model_keys: set[ModelKey] = set()
|
||||
enabled_provider_ids: set[int] = set()
|
||||
provider_identities: dict[int, ConfiguredProviderIdentity] = {}
|
||||
|
||||
for provider in provider_rows:
|
||||
if not provider.enabled or provider.id is None:
|
||||
continue
|
||||
enabled_provider_ids.add(provider.id)
|
||||
provider_identities[provider.id] = ConfiguredProviderIdentity(
|
||||
id=provider.id,
|
||||
slug=provider.slug or f"provider-{provider.id}",
|
||||
provider_type=provider.provider_type,
|
||||
)
|
||||
for model in provider.models:
|
||||
key = (model.id.lower(), provider.id)
|
||||
if model.enabled:
|
||||
@@ -284,7 +299,7 @@ async def _load_model_visibility() -> tuple[
|
||||
else:
|
||||
disabled_model_keys.add(key)
|
||||
|
||||
return overrides_by_key, disabled_model_keys, enabled_provider_ids
|
||||
return overrides_by_key, disabled_model_keys, provider_identities
|
||||
|
||||
|
||||
def _apply_model_visibility(
|
||||
@@ -338,6 +353,7 @@ def _apply_model_visibility(
|
||||
|
||||
async def _collect_provider_paths(
|
||||
upstream: BaseUpstreamProvider,
|
||||
provider_identity: ConfiguredProviderIdentity,
|
||||
overrides_by_key: dict[ModelKey, ModelRow] | None = None,
|
||||
disabled_model_keys: set[ModelKey] | None = None,
|
||||
cycle: _RefreshCycleState | None = None,
|
||||
@@ -350,13 +366,12 @@ async def _collect_provider_paths(
|
||||
"""
|
||||
cycle = cycle or _RefreshCycleState()
|
||||
models = _apply_model_visibility(upstream, overrides_by_key, disabled_model_keys)
|
||||
upstream_url = normalize_upstream_url(upstream.base_url)
|
||||
|
||||
def _base_path(model: object) -> DiscoveredPath:
|
||||
return DiscoveredPath(
|
||||
model_id=exposed_model_id(model),
|
||||
path=encode_model_path(upstream_url),
|
||||
upstream_url=upstream_url,
|
||||
path=encode_model_path(provider_identity.id),
|
||||
provider=provider_identity,
|
||||
)
|
||||
|
||||
if not is_openrouter_base_url(upstream.base_url):
|
||||
@@ -389,10 +404,10 @@ async def _collect_provider_paths(
|
||||
paths.extend(
|
||||
DiscoveredPath(
|
||||
model_id=model_id,
|
||||
path=encode_model_path(upstream_url, endpoint.tag),
|
||||
upstream_url=upstream_url,
|
||||
provider_tag=endpoint.tag,
|
||||
provider_name=endpoint.provider_name,
|
||||
path=encode_model_path(provider_identity.id, endpoint.tag),
|
||||
provider=provider_identity,
|
||||
endpoint_tag=endpoint.tag,
|
||||
endpoint_name=endpoint.provider_name,
|
||||
)
|
||||
for endpoint in endpoints
|
||||
)
|
||||
@@ -448,9 +463,10 @@ async def _persist_provider_paths(
|
||||
{
|
||||
"model_id": discovered.model_id,
|
||||
"path": discovered.path,
|
||||
"upstream_url": discovered.upstream_url,
|
||||
"provider_tag": discovered.provider_tag,
|
||||
"provider_name": discovered.provider_name,
|
||||
"provider_slug": discovered.provider.slug,
|
||||
"provider_type": discovered.provider.provider_type,
|
||||
"endpoint_tag": discovered.endpoint_tag,
|
||||
"endpoint_name": discovered.endpoint_name,
|
||||
"upstream_provider_id": upstream_provider_id,
|
||||
"updated_at": now,
|
||||
}
|
||||
@@ -505,17 +521,18 @@ async def refresh_model_paths(
|
||||
(
|
||||
overrides_by_key,
|
||||
disabled_model_keys,
|
||||
enabled_provider_ids,
|
||||
provider_identities,
|
||||
) = await _load_model_visibility()
|
||||
await prune_model_paths_for_inactive_providers()
|
||||
|
||||
cycle = _RefreshCycleState()
|
||||
for upstream in upstreams:
|
||||
if upstream.db_id is None or upstream.db_id not in enabled_provider_ids:
|
||||
if upstream.db_id is None or upstream.db_id not in provider_identities:
|
||||
continue
|
||||
try:
|
||||
snapshot = await _collect_provider_paths(
|
||||
upstream,
|
||||
provider_identity=provider_identities[upstream.db_id],
|
||||
overrides_by_key=overrides_by_key,
|
||||
disabled_model_keys=disabled_model_keys,
|
||||
cycle=cycle,
|
||||
@@ -611,13 +628,17 @@ async def refresh_model_paths_periodically(
|
||||
|
||||
|
||||
def _serialize_path(row: ModelPathRow) -> dict[str, Any]:
|
||||
provider = None
|
||||
if row.provider_tag or row.provider_name:
|
||||
provider = {"name": row.provider_name, "slug": row.provider_tag}
|
||||
endpoint = None
|
||||
if row.endpoint_tag or row.endpoint_name:
|
||||
endpoint = {"tag": row.endpoint_tag, "name": row.endpoint_name}
|
||||
return {
|
||||
"path": row.path,
|
||||
"upstream_url": row.upstream_url,
|
||||
"provider": provider,
|
||||
"provider": {
|
||||
"id": row.upstream_provider_id,
|
||||
"slug": row.provider_slug,
|
||||
"type": row.provider_type,
|
||||
},
|
||||
"endpoint": endpoint,
|
||||
}
|
||||
|
||||
|
||||
@@ -646,9 +667,7 @@ async def get_all_model_paths() -> dict:
|
||||
data = [
|
||||
{
|
||||
"id": grouped_model_id,
|
||||
"paths": sorted(
|
||||
grouped[grouped_model_id], key=lambda item: str(item["path"])
|
||||
),
|
||||
"paths": grouped[grouped_model_id],
|
||||
}
|
||||
for grouped_model_id in sorted(grouped)
|
||||
]
|
||||
|
||||
+257
-90
@@ -142,11 +142,18 @@ def _mock_transport(
|
||||
return counter
|
||||
|
||||
|
||||
def _endpoints_response(*provider_names: str) -> httpx.Response:
|
||||
return httpx.Response(
|
||||
200,
|
||||
json={"data": {"endpoints": [{"provider_name": n} for n in provider_names]}},
|
||||
)
|
||||
def _endpoints_response(
|
||||
*providers: str | tuple[str, str],
|
||||
) -> httpx.Response:
|
||||
endpoints = []
|
||||
for provider in providers:
|
||||
if isinstance(provider, tuple):
|
||||
provider_name, tag = provider
|
||||
else:
|
||||
provider_name = provider
|
||||
tag = provider.lower().replace(" ", "-")
|
||||
endpoints.append({"provider_name": provider_name, "tag": tag})
|
||||
return httpx.Response(200, json={"data": {"endpoints": endpoints}})
|
||||
|
||||
|
||||
_SEEDED_PROVIDER_IDS = (1, 2, 4, 5, 7)
|
||||
@@ -206,6 +213,29 @@ def _ids_of(payload: dict) -> set[str]:
|
||||
return {entry["id"] for entry in payload["data"]}
|
||||
|
||||
|
||||
def _path_entry(
|
||||
provider_id: int,
|
||||
*,
|
||||
provider_slug: str | None = None,
|
||||
provider_type: str | None = None,
|
||||
endpoint_tag: str | None = None,
|
||||
endpoint_name: str | None = None,
|
||||
) -> dict[str, Any]:
|
||||
endpoint = None
|
||||
if endpoint_tag or endpoint_name:
|
||||
endpoint = {"tag": endpoint_tag, "name": endpoint_name}
|
||||
return {
|
||||
"path": mp.encode_model_path(provider_id, endpoint_tag),
|
||||
"provider": {
|
||||
"id": provider_id,
|
||||
"slug": provider_slug or f"p{provider_id}",
|
||||
"type": provider_type
|
||||
or ("anthropic" if provider_id == 1 else "openrouter"),
|
||||
},
|
||||
"endpoint": endpoint,
|
||||
}
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Predicates / pure helpers
|
||||
# --------------------------------------------------------------------------- #
|
||||
@@ -223,6 +253,13 @@ def test_native_anthropic_not_openrouter() -> None:
|
||||
assert mp.is_openrouter_base_url("https://api.anthropic.com/v1") is False
|
||||
|
||||
|
||||
def test_encode_model_path_uses_provider_id_without_exposing_url() -> None:
|
||||
assert mp.encode_model_path(42) == "provider=42"
|
||||
assert mp.encode_model_path(42, "google-vertex/us-east5") == (
|
||||
"provider=42&endpoint=google-vertex%2Fus-east5"
|
||||
)
|
||||
|
||||
|
||||
def test_exposed_model_id_prefers_forwarded() -> None:
|
||||
assert (
|
||||
mp.exposed_model_id(_model("claude-x", forwarded_model_id="fwd-claude"))
|
||||
@@ -300,9 +337,7 @@ async def test_direct_provider_single_path_uses_provider_type(
|
||||
)
|
||||
await mp.refresh_model_paths([provider])
|
||||
payload = await mp.get_all_model_paths()
|
||||
assert payload["data"] == [
|
||||
{"id": "claude-opus-4.6", "paths": [{"path": "anthropic"}]}
|
||||
]
|
||||
assert payload["data"] == [{"id": "claude-opus-4.6", "paths": [_path_entry(1)]}]
|
||||
assert payload["updated_at"] is not None
|
||||
|
||||
|
||||
@@ -320,6 +355,22 @@ async def test_direct_path_stores_exposed_model_id(
|
||||
assert _ids_of(await mp.get_all_model_paths()) == {"claude-opus-4.6"}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_forwarded_model_id_with_slash_remains_exact_and_routable(
|
||||
patched_session: AsyncEngine,
|
||||
) -> None:
|
||||
provider = _FakeProvider(
|
||||
provider_type="anthropic",
|
||||
base_url="https://api.anthropic.com/v1",
|
||||
models=[_model("local-alias", forwarded_model_id="anthropic/claude-opus-4.6")],
|
||||
db_id=1,
|
||||
)
|
||||
await mp.refresh_model_paths([provider])
|
||||
|
||||
assert _ids_of(await mp.get_all_model_paths()) == {"anthropic/claude-opus-4.6"}
|
||||
assert (await mp.get_paths_for_model("anthropic/claude-opus-4.6"))["data"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_disabled_cached_models_excluded(
|
||||
patched_session: AsyncEngine,
|
||||
@@ -361,7 +412,7 @@ async def test_disabling_model_on_one_provider_keeps_other_provider(
|
||||
await mp.refresh_model_paths([p1, p2])
|
||||
|
||||
payload = await mp.get_all_model_paths()
|
||||
assert _paths_of(payload, "shared-model") == {"anthropic"}
|
||||
assert _paths_of(payload, "shared-model") == {mp.encode_model_path(1)}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -395,8 +446,8 @@ async def test_override_alias_not_applied_across_providers(
|
||||
await mp.refresh_model_paths([p1, p2])
|
||||
|
||||
payload = await mp.get_all_model_paths()
|
||||
assert _paths_of(payload, "shared-model") == {"anthropic"}
|
||||
assert _paths_of(payload, "private-alias") == {"generic"}
|
||||
assert _paths_of(payload, "shared-model") == {mp.encode_model_path(1)}
|
||||
assert _paths_of(payload, "private-alias") == {mp.encode_model_path(2)}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -464,7 +515,7 @@ async def test_refresh_model_paths_uses_db_forwarded_alias(
|
||||
await mp.refresh_model_paths([provider])
|
||||
|
||||
assert (await mp.get_all_model_paths())["data"] == [
|
||||
{"id": "public-alias", "paths": [{"path": "anthropic"}]}
|
||||
{"id": "public-alias", "paths": [_path_entry(1)]}
|
||||
]
|
||||
|
||||
|
||||
@@ -486,7 +537,7 @@ async def test_refresh_model_paths_includes_enabled_db_override_missing_from_cac
|
||||
await mp.refresh_model_paths([provider])
|
||||
|
||||
assert (await mp.get_all_model_paths())["data"] == [
|
||||
{"id": "public-deployment", "paths": [{"path": "generic"}]}
|
||||
{"id": "public-deployment", "paths": [_path_entry(1)]}
|
||||
]
|
||||
|
||||
|
||||
@@ -638,24 +689,34 @@ async def test_openrouter_provider_adds_endpoint_paths(
|
||||
)
|
||||
_mock_transport(
|
||||
monkeypatch,
|
||||
lambda request: _endpoints_response("Anthropic", "Amazon Bedrock"),
|
||||
lambda request: _endpoints_response(
|
||||
("Google", "google-vertex/eu"),
|
||||
("Google", "google-vertex/us"),
|
||||
),
|
||||
)
|
||||
|
||||
await mp.refresh_model_paths([provider])
|
||||
|
||||
paths = _paths_of(await mp.get_all_model_paths(), "claude-opus-4.6")
|
||||
assert "openrouter:Anthropic" in paths
|
||||
assert "openrouter:Amazon Bedrock" in paths
|
||||
assert "openrouter" not in paths
|
||||
payload = await mp.get_paths_for_model("claude-opus-4.6")
|
||||
assert {item["path"] for item in payload["data"]} == {
|
||||
mp.encode_model_path(2),
|
||||
mp.encode_model_path(2, "google-vertex/eu"),
|
||||
mp.encode_model_path(2, "google-vertex/us"),
|
||||
}
|
||||
assert {
|
||||
item["endpoint"]["tag"] for item in payload["data"] if item["endpoint"]
|
||||
} == {"google-vertex/eu", "google-vertex/us"}
|
||||
assert {
|
||||
item["endpoint"]["name"] for item in payload["data"] if item["endpoint"]
|
||||
} == {"Google"}
|
||||
assert {item["provider"]["id"] for item in payload["data"]} == {2}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_openrouter_self_echoing_subprovider_maps_to_unknown(
|
||||
async def test_openrouter_uses_exact_tag_even_when_display_name_is_router(
|
||||
patched_session: AsyncEngine, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
"""Responses stamped for a sub-provider echoing "OpenRouter" say
|
||||
``unknown``; discovery must advertise the same string, never
|
||||
``openrouter:OpenRouter``."""
|
||||
"""Machine-readable endpoint tags, not display names, define identity."""
|
||||
provider = _FakeOpenRouterProvider(
|
||||
models=[_model("claude-opus-4.6", canonical_slug="anthropic/claude-opus-4.6")],
|
||||
db_id=2,
|
||||
@@ -665,17 +726,14 @@ async def test_openrouter_self_echoing_subprovider_maps_to_unknown(
|
||||
await mp.refresh_model_paths([provider])
|
||||
|
||||
paths = _paths_of(await mp.get_all_model_paths(), "claude-opus-4.6")
|
||||
assert "openrouter:OpenRouter" not in paths
|
||||
assert "unknown" in paths
|
||||
assert paths == {mp.encode_model_path(2), mp.encode_model_path(2, "openrouter")}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_generic_provider_with_openrouter_base_url_discovers(
|
||||
patched_session: AsyncEngine, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
"""A generic provider pointed at OpenRouter exposes both the bare
|
||||
``generic`` path (stamped when the upstream omits its provider field) and
|
||||
the ``generic:<upstream>`` endpoint paths."""
|
||||
"""Configured provider identity is independent from its endpoint URL."""
|
||||
provider = _FakeProvider(
|
||||
provider_type="generic",
|
||||
base_url="https://openrouter.ai/api/v1",
|
||||
@@ -687,7 +745,35 @@ async def test_generic_provider_with_openrouter_base_url_discovers(
|
||||
await mp.refresh_model_paths([provider])
|
||||
|
||||
paths = _paths_of(await mp.get_all_model_paths(), "claude-opus-4.6")
|
||||
assert paths == {"generic", "generic:Anthropic"}
|
||||
assert paths == {mp.encode_model_path(1), mp.encode_model_path(1, "anthropic")}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_openrouter_partial_failure_keeps_failed_models_previous_rows(
|
||||
patched_session: AsyncEngine, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
provider = _FakeOpenRouterProvider(
|
||||
models=[
|
||||
_model("good", canonical_slug="author/good"),
|
||||
_model("degraded", canonical_slug="author/degraded"),
|
||||
],
|
||||
db_id=2,
|
||||
)
|
||||
_mock_transport(monkeypatch, lambda request: _endpoints_response("Anthropic"))
|
||||
await mp.refresh_model_paths([provider])
|
||||
before = _paths_of(await mp.get_all_model_paths(), "degraded")
|
||||
assert before
|
||||
|
||||
def _partial_failure(request: httpx.Request) -> httpx.Response:
|
||||
if request.url.path.endswith("/author/degraded/endpoints"):
|
||||
return httpx.Response(503)
|
||||
return _endpoints_response("Google")
|
||||
|
||||
_mock_transport(monkeypatch, _partial_failure)
|
||||
await mp.refresh_model_paths([provider])
|
||||
|
||||
assert _paths_of(await mp.get_all_model_paths(), "degraded") == before
|
||||
assert _paths_of(await mp.get_all_model_paths(), "good") != before
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -703,7 +789,7 @@ async def test_openrouter_failure_keeps_previous_rows(
|
||||
_mock_transport(monkeypatch, lambda request: _endpoints_response("Anthropic"))
|
||||
await mp.refresh_model_paths([provider])
|
||||
before = _paths_of(await mp.get_all_model_paths(), "claude-opus-4.6")
|
||||
assert "openrouter:Anthropic" in before
|
||||
assert mp.encode_model_path(2, "anthropic") in before
|
||||
|
||||
def _network_down(request: httpx.Request) -> httpx.Response:
|
||||
raise httpx.ConnectError("network down", request=request)
|
||||
@@ -726,10 +812,8 @@ async def test_openrouter_rate_limit_aborts_cycle_and_keeps_rows(
|
||||
|
||||
_mock_transport(monkeypatch, lambda request: _endpoints_response("Anthropic"))
|
||||
await mp.refresh_model_paths([provider])
|
||||
assert _paths_of(await mp.get_all_model_paths(), "m0") == {
|
||||
"unknown",
|
||||
"openrouter:Anthropic",
|
||||
}
|
||||
expected = {mp.encode_model_path(2), mp.encode_model_path(2, "anthropic")}
|
||||
assert _paths_of(await mp.get_all_model_paths(), "m0") == expected
|
||||
|
||||
counter = _mock_transport(monkeypatch, lambda request: httpx.Response(429))
|
||||
await mp.refresh_model_paths([provider])
|
||||
@@ -739,27 +823,28 @@ async def test_openrouter_rate_limit_aborts_cycle_and_keeps_rows(
|
||||
assert counter["requests"] <= mp._OPENROUTER_CONCURRENCY, (
|
||||
"429 must abort the remaining fan-out"
|
||||
)
|
||||
assert _paths_of(await mp.get_all_model_paths(), "m0") == {
|
||||
"unknown",
|
||||
"openrouter:Anthropic",
|
||||
}
|
||||
assert _paths_of(await mp.get_all_model_paths(), "m0") == expected
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_openrouter_bad_payload_shapes_do_not_raise(
|
||||
async def test_openrouter_bad_payload_shapes_preserve_previous_rows(
|
||||
patched_session: AsyncEngine, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
"""``endpoints: null`` and non-list endpoint payloads are swallowed as
|
||||
documented, not raised into the generic task-errored bucket."""
|
||||
"""Malformed successful responses are degraded snapshots, not empty sets."""
|
||||
provider = _FakeOpenRouterProvider(
|
||||
models=[_model("m", canonical_slug="a/m")], db_id=2
|
||||
)
|
||||
_mock_transport(monkeypatch, lambda request: _endpoints_response("Anthropic"))
|
||||
await mp.refresh_model_paths([provider])
|
||||
before = _paths_of(await mp.get_all_model_paths(), "m")
|
||||
|
||||
for payload in (
|
||||
{"data": {"endpoints": None}},
|
||||
{"data": {"endpoints": "none"}},
|
||||
{"data": {"endpoints": [{"provider_name": "Anthropic"}]}},
|
||||
{"data": None},
|
||||
{},
|
||||
):
|
||||
provider = _FakeOpenRouterProvider(
|
||||
models=[_model("m", canonical_slug="a/m")], db_id=2
|
||||
)
|
||||
|
||||
def _handler(
|
||||
request: httpx.Request, p: dict[str, Any] | None = payload
|
||||
@@ -767,8 +852,8 @@ async def test_openrouter_bad_payload_shapes_do_not_raise(
|
||||
return httpx.Response(200, json=p)
|
||||
|
||||
_mock_transport(monkeypatch, _handler)
|
||||
# Must not raise.
|
||||
await mp.refresh_model_paths([provider])
|
||||
assert _paths_of(await mp.get_all_model_paths(), "m") == before
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -795,8 +880,12 @@ async def test_openrouter_shared_base_url_fetched_once(
|
||||
|
||||
assert counter["requests"] == 1
|
||||
paths = _paths_of(await mp.get_all_model_paths(), "claude-opus-4.6")
|
||||
assert "openrouter:Anthropic" in paths
|
||||
assert "generic:Anthropic" in paths
|
||||
assert paths == {
|
||||
mp.encode_model_path(2),
|
||||
mp.encode_model_path(2, "anthropic"),
|
||||
mp.encode_model_path(4),
|
||||
mp.encode_model_path(4, "anthropic"),
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -857,13 +946,16 @@ async def test_same_model_two_providers_two_paths(
|
||||
assert len(payload["data"]) == 1
|
||||
entry = payload["data"][0]
|
||||
assert entry["id"] == "claude-opus-4.6"
|
||||
assert {p["path"] for p in entry["paths"]} == {"anthropic", "generic"}
|
||||
assert {p["path"] for p in entry["paths"]} == {
|
||||
mp.encode_model_path(1),
|
||||
mp.encode_model_path(2),
|
||||
}
|
||||
assert "canonical_id" not in entry
|
||||
assert all("canonical_id" not in p for p in entry["paths"])
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_all_model_paths_deduplicates_visible_paths(
|
||||
async def test_get_all_model_paths_keeps_distinct_configured_providers(
|
||||
patched_session: AsyncEngine,
|
||||
) -> None:
|
||||
p1 = _FakeProvider(
|
||||
@@ -881,7 +973,10 @@ async def test_get_all_model_paths_deduplicates_visible_paths(
|
||||
await mp.refresh_model_paths([p1, p2])
|
||||
|
||||
assert (await mp.get_all_model_paths())["data"] == [
|
||||
{"id": "claude-opus-4.6", "paths": [{"path": "anthropic"}]}
|
||||
{
|
||||
"id": "claude-opus-4.6",
|
||||
"paths": [_path_entry(1), _path_entry(2)],
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@@ -906,14 +1001,13 @@ async def test_get_all_model_paths_is_deterministic(
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_paths_for_model_returns_only_paths(
|
||||
async def test_get_paths_for_model_returns_route_identity(
|
||||
patched_session: AsyncEngine,
|
||||
) -> None:
|
||||
await _seed_two_provider_shared_model(patched_session)
|
||||
|
||||
payload = await mp.get_paths_for_model("claude-opus-4.6")
|
||||
assert {p["path"] for p in payload["data"]} == {"anthropic", "generic"}
|
||||
assert all(set(p.keys()) == {"path"} for p in payload["data"])
|
||||
assert payload["data"] == [_path_entry(1), _path_entry(2)]
|
||||
assert (await mp.get_paths_for_model("does-not-exist"))["data"] == []
|
||||
|
||||
|
||||
@@ -929,13 +1023,11 @@ async def test_get_paths_for_model_falls_back_to_provider_prefixed_id(
|
||||
)
|
||||
await mp.refresh_model_paths([provider])
|
||||
|
||||
assert (await mp.get_paths_for_model("glm-5v-turbo"))["data"] == [
|
||||
{"path": "generic"}
|
||||
]
|
||||
assert (await mp.get_paths_for_model("glm-5v-turbo"))["data"] == [_path_entry(4)]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_paths_for_model_merges_prefixed_and_unprefixed_aliases(
|
||||
async def test_get_paths_for_model_requires_exact_advertised_id(
|
||||
patched_session: AsyncEngine,
|
||||
) -> None:
|
||||
p1 = _FakeProvider(
|
||||
@@ -955,8 +1047,8 @@ async def test_get_paths_for_model_merges_prefixed_and_unprefixed_aliases(
|
||||
short_paths = (await mp.get_paths_for_model("deepseek-v4-pro"))["data"]
|
||||
prefixed_paths = (await mp.get_paths_for_model("deepseek/deepseek-v4-pro"))["data"]
|
||||
|
||||
assert {p["path"] for p in short_paths} == {"generic", "anthropic"}
|
||||
assert prefixed_paths == short_paths
|
||||
assert short_paths == [_path_entry(4), _path_entry(7)]
|
||||
assert prefixed_paths == []
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -975,18 +1067,40 @@ async def test_get_paths_for_model_multi_segment_id_matches_models_listing(
|
||||
|
||||
assert _ids_of(await mp.get_all_model_paths()) == {"fireworks/models/glm-5"}
|
||||
assert (await mp.get_paths_for_model("fireworks/models/glm-5"))["data"] == [
|
||||
{"path": "generic"}
|
||||
_path_entry(1)
|
||||
]
|
||||
assert (await mp.get_paths_for_model("accounts/fireworks/models/glm-5"))[
|
||||
"data"
|
||||
] == [{"path": "generic"}]
|
||||
] == []
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Periodic refresh loop
|
||||
# Immediate and periodic refresh
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_refresh_model_paths_for_provider_selects_mutated_provider(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
import routstr.proxy as proxy
|
||||
|
||||
target = SimpleNamespace(db_id=2)
|
||||
other = SimpleNamespace(db_id=1)
|
||||
seen: list[list[Any]] = []
|
||||
|
||||
monkeypatch.setattr(proxy, "get_upstreams", lambda: [other, target])
|
||||
|
||||
async def _fake_refresh(upstreams: list[Any]) -> None:
|
||||
seen.append(upstreams)
|
||||
|
||||
monkeypatch.setattr(mp, "refresh_model_paths", _fake_refresh)
|
||||
|
||||
await mp.refresh_model_paths_for_provider(2)
|
||||
|
||||
assert seen == [[target]]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_refresh_loop_rereads_interval_and_picks_up_providers(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
@@ -1093,37 +1207,88 @@ def _make_model_paths_app() -> FastAPI:
|
||||
def test_model_paths_endpoint_returns_all_paths(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
expected = {
|
||||
"data": [
|
||||
{
|
||||
"id": "claude-opus-4.6",
|
||||
"paths": [
|
||||
_path_entry(1),
|
||||
_path_entry(
|
||||
2,
|
||||
endpoint_tag="google-vertex/us",
|
||||
endpoint_name="Google",
|
||||
),
|
||||
],
|
||||
}
|
||||
],
|
||||
"updated_at": 1753500000,
|
||||
}
|
||||
|
||||
async def _fake_get_all_model_paths() -> dict[str, Any]:
|
||||
return {
|
||||
"data": [
|
||||
{
|
||||
"id": "claude-opus-4.6",
|
||||
"paths": [
|
||||
{"path": "anthropic"},
|
||||
{"path": "openrouter:Anthropic"},
|
||||
],
|
||||
}
|
||||
],
|
||||
"updated_at": 1753500000,
|
||||
}
|
||||
return expected
|
||||
|
||||
monkeypatch.setattr(mp, "get_all_model_paths", _fake_get_all_model_paths)
|
||||
|
||||
response = TestClient(_make_model_paths_app()).get("/v1/models/paths")
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {
|
||||
"data": [
|
||||
{
|
||||
"id": "claude-opus-4.6",
|
||||
"paths": [
|
||||
{"path": "anthropic"},
|
||||
{"path": "openrouter:Anthropic"},
|
||||
],
|
||||
}
|
||||
],
|
||||
"updated_at": 1753500000,
|
||||
}
|
||||
assert response.json() == expected
|
||||
|
||||
|
||||
def test_model_paths_for_model_returns_404_for_unknown_model(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
import routstr.proxy as proxy
|
||||
|
||||
async def _fake_get_paths_for_model(model_id: str) -> dict[str, Any]:
|
||||
return {"data": [], "updated_at": None}
|
||||
|
||||
monkeypatch.setattr(mp, "get_paths_for_model", _fake_get_paths_for_model)
|
||||
monkeypatch.setattr(proxy, "get_unique_models", lambda: [])
|
||||
|
||||
response = TestClient(_make_model_paths_app()).get(
|
||||
"/v1/models/paths/model", params={"model_id": "does-not-exist"}
|
||||
)
|
||||
|
||||
assert response.status_code == 404
|
||||
assert response.json() == {"detail": "Model not found"}
|
||||
|
||||
|
||||
def test_model_paths_for_known_model_can_return_empty_collection(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
import routstr.proxy as proxy
|
||||
|
||||
async def _fake_get_paths_for_model(model_id: str) -> dict[str, Any]:
|
||||
return {"data": [], "updated_at": None}
|
||||
|
||||
monkeypatch.setattr(mp, "get_paths_for_model", _fake_get_paths_for_model)
|
||||
monkeypatch.setattr(proxy, "get_unique_models", lambda: [_model("known")])
|
||||
|
||||
response = TestClient(_make_model_paths_app()).get(
|
||||
"/v1/models/paths/model", params={"model_id": "known"}
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"data": [], "updated_at": None}
|
||||
|
||||
|
||||
def test_model_paths_for_routing_only_alias_returns_404(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
import routstr.proxy as proxy
|
||||
|
||||
async def _fake_get_paths_for_model(model_id: str) -> dict[str, Any]:
|
||||
return {"data": [], "updated_at": None}
|
||||
|
||||
monkeypatch.setattr(mp, "get_paths_for_model", _fake_get_paths_for_model)
|
||||
monkeypatch.setattr(proxy, "get_unique_models", lambda: [_model("advertised")])
|
||||
|
||||
response = TestClient(_make_model_paths_app()).get(
|
||||
"/v1/models/paths/model", params={"model_id": "routing-alias"}
|
||||
)
|
||||
|
||||
assert response.status_code == 404
|
||||
|
||||
|
||||
def test_model_paths_for_model_endpoint_accepts_slash_model_id(
|
||||
@@ -1131,9 +1296,14 @@ def test_model_paths_for_model_endpoint_accepts_slash_model_id(
|
||||
) -> None:
|
||||
calls: list[str] = []
|
||||
|
||||
expected = {
|
||||
"data": [_path_entry(2, endpoint_tag="anthropic", endpoint_name="Anthropic")],
|
||||
"updated_at": None,
|
||||
}
|
||||
|
||||
async def _fake_get_paths_for_model(model_id: str) -> dict[str, Any]:
|
||||
calls.append(model_id)
|
||||
return {"data": [{"path": "generic:Anthropic"}], "updated_at": None}
|
||||
return expected
|
||||
|
||||
monkeypatch.setattr(mp, "get_paths_for_model", _fake_get_paths_for_model)
|
||||
|
||||
@@ -1143,8 +1313,5 @@ def test_model_paths_for_model_endpoint_accepts_slash_model_id(
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {
|
||||
"data": [{"path": "generic:Anthropic"}],
|
||||
"updated_at": None,
|
||||
}
|
||||
assert response.json() == expected
|
||||
assert calls == ["anthropic/claude-opus-4.6"]
|
||||
|
||||
Reference in New Issue
Block a user