fix: make model path identity selectable

This commit is contained in:
9qeklajc
2026-07-26 20:12:55 +02:00
parent c5da73f1e9
commit 16fc548b48
7 changed files with 401 additions and 165 deletions
+28 -28
View File
@@ -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(
+28
View File
@@ -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
View File
@@ -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,
+9 -1
View File
@@ -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")
+63 -44
View File
@@ -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
View File
@@ -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"]