mirror of
https://github.com/Routstr/routstr-core.git
synced 2026-10-05 12:28:22 +00:00
Merge pull request #748 from Routstr/one-provider-support
feat: drop provider id from model paths and route them to the cheapest provider
This commit is contained in:
+19
-11
@@ -423,13 +423,13 @@ GET /v1/models/paths
|
|||||||
"id": "anthropic/claude-sonnet-4",
|
"id": "anthropic/claude-sonnet-4",
|
||||||
"paths": [
|
"paths": [
|
||||||
{
|
{
|
||||||
"path": "url=https%3A%2F%2Fapi.anthropic.com%2Fv1&provider-id=12&model-id=anthropic%2Fclaude-sonnet-4",
|
"path": "url=https%3A%2F%2Fapi.anthropic.com%2Fv1&model-id=anthropic%2Fclaude-sonnet-4",
|
||||||
"provider": {"id": 12, "slug": "anthropic-primary", "type": "anthropic"},
|
"provider": {"slug": "anthropic-primary", "type": "anthropic"},
|
||||||
"endpoint": null
|
"endpoint": null
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "url=https%3A%2F%2Fopenrouter.ai%2Fapi%2Fv1&provider-id=42&model-id=anthropic%2Fclaude-sonnet-4&endpoint=google-vertex%2Fus",
|
"path": "url=https%3A%2F%2Fopenrouter.ai%2Fapi%2Fv1&model-id=anthropic%2Fclaude-sonnet-4&endpoint=google-vertex%2Fus",
|
||||||
"provider": {"id": 42, "slug": "openrouter-main", "type": "openrouter"},
|
"provider": {"slug": "openrouter-main", "type": "openrouter"},
|
||||||
"endpoint": {"tag": "google-vertex/us", "name": "Google"}
|
"endpoint": {"tag": "google-vertex/us", "name": "Google"}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -440,13 +440,21 @@ GET /v1/models/paths
|
|||||||
```
|
```
|
||||||
|
|
||||||
`path` is an opaque, percent-encoded selector. Clients must store and return it
|
`path` is an opaque, percent-encoded selector. Clients must store and return it
|
||||||
unchanged rather than parsing or reconstructing it. It identifies the exact
|
unchanged rather than parsing or reconstructing it. It identifies the route with
|
||||||
configured route with `url`, `provider-id`, and `model-id`. To avoid exposing
|
`url` and `model-id`. To avoid exposing private network details, a configured
|
||||||
private network details, a configured private IP address or any URL with an
|
private IP address or any URL with an explicit port is advertised as
|
||||||
explicit port is advertised as `http://localhost`. OpenRouter routes additionally
|
`http://localhost`. OpenRouter routes additionally preserve the exact
|
||||||
preserve the exact machine-readable endpoint `tag`. Provider slugs/types and
|
machine-readable endpoint `tag`. Provider slugs/types and endpoint names remain
|
||||||
endpoint names remain display data. When request-side selection is implemented,
|
display data. When request-side selection is implemented, an endpoint tag must
|
||||||
an endpoint tag must not silently fall back to another backend.
|
not silently fall back to another backend.
|
||||||
|
|
||||||
|
A path names no provider, so several configured providers sharing an upstream URL
|
||||||
|
collapse onto a single path. Such a path always routes to the cheapest of those
|
||||||
|
providers, and the advertised slug, type and pricing describe that same cheapest
|
||||||
|
provider. A pinned path never fails over: if the selected provider errors, the
|
||||||
|
error is returned rather than retried elsewhere. Paths issued before this change
|
||||||
|
still carry `provider-id` and are still honoured, pinning the exact provider they
|
||||||
|
name.
|
||||||
|
|
||||||
### List Paths for One Model
|
### List Paths for One Model
|
||||||
|
|
||||||
|
|||||||
+17
-7
@@ -132,12 +132,18 @@ def _candidate_for_selector(
|
|||||||
selector: ModelPathSelector,
|
selector: ModelPathSelector,
|
||||||
candidates: list[tuple[Model, BaseUpstreamProvider]],
|
candidates: list[tuple[Model, BaseUpstreamProvider]],
|
||||||
) -> tuple[Model, BaseUpstreamProvider] | None:
|
) -> tuple[Model, BaseUpstreamProvider] | None:
|
||||||
|
"""Resolve a route selector to one candidate.
|
||||||
|
|
||||||
|
``candidates`` is ranked by cost, so the first URL match is the cheapest
|
||||||
|
provider configured against that URL. A selector still carrying a legacy
|
||||||
|
``provider-id`` keeps pinning that exact provider instead.
|
||||||
|
"""
|
||||||
for model_obj, upstream in candidates:
|
for model_obj, upstream in candidates:
|
||||||
if (
|
if public_provider_url(upstream.base_url) != selector.base_url:
|
||||||
upstream.db_id == selector.provider_id
|
continue
|
||||||
and public_provider_url(upstream.base_url) == selector.base_url
|
if selector.provider_id is not None and upstream.db_id != selector.provider_id:
|
||||||
):
|
continue
|
||||||
return model_obj, upstream
|
return model_obj, upstream
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
@@ -540,10 +546,14 @@ async def _proxy(
|
|||||||
if selector is not None:
|
if selector is not None:
|
||||||
pinned = _candidate_for_selector(selector, candidates)
|
pinned = _candidate_for_selector(selector, candidates)
|
||||||
if pinned is None:
|
if pinned is None:
|
||||||
|
target = (
|
||||||
|
f"provider {selector.provider_id}"
|
||||||
|
if selector.provider_id is not None
|
||||||
|
else f"'{selector.base_url}'"
|
||||||
|
)
|
||||||
return create_error_response(
|
return create_error_response(
|
||||||
"invalid_model_path",
|
"invalid_model_path",
|
||||||
f"Model '{selector.model_id}' is not routable through provider "
|
f"Model '{selector.model_id}' is not routable through {target}",
|
||||||
f"{selector.provider_id}",
|
|
||||||
404,
|
404,
|
||||||
request=request,
|
request=request,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -2,11 +2,16 @@
|
|||||||
|
|
||||||
Exposes every selectable upstream route a Routstr model is reachable through.
|
Exposes every selectable upstream route a Routstr model is reachable through.
|
||||||
A path is a standard percent-encoded query string containing the configured
|
A path is a standard percent-encoded query string containing the configured
|
||||||
upstream URL, provider ID, client-visible model ID and, for an exact OpenRouter
|
upstream URL, client-visible model ID and, for an exact OpenRouter endpoint,
|
||||||
endpoint, its machine-readable tag::
|
its machine-readable tag::
|
||||||
|
|
||||||
url=https%3A%2F%2Fapi.anthropic.com%2Fv1&provider-id=12&model-id=claude-sonnet-4
|
url=https%3A%2F%2Fapi.anthropic.com%2Fv1&model-id=claude-sonnet-4
|
||||||
url=https%3A%2F%2Fopenrouter.ai%2Fapi%2Fv1&provider-id=42&model-id=claude-sonnet-4&endpoint=google-vertex%2Fus
|
url=https%3A%2F%2Fopenrouter.ai%2Fapi%2Fv1&model-id=claude-sonnet-4&endpoint=google-vertex%2Fus
|
||||||
|
|
||||||
|
A path names no provider, so several providers sharing an upstream URL collapse
|
||||||
|
onto one selector that routes to the cheapest of them. ``provider-id`` is still
|
||||||
|
accepted when decoding so paths issued before that change keep pinning the exact
|
||||||
|
provider they named.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
@@ -16,6 +21,7 @@ import ipaddress
|
|||||||
import json
|
import json
|
||||||
import random
|
import random
|
||||||
import time
|
import time
|
||||||
|
from collections.abc import Iterable
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import TYPE_CHECKING, Any, Callable
|
from typing import TYPE_CHECKING, Any, Callable
|
||||||
from urllib.parse import parse_qsl, urlencode, urlsplit
|
from urllib.parse import parse_qsl, urlencode, urlsplit
|
||||||
@@ -117,16 +123,11 @@ def public_provider_url(base_url: str) -> str:
|
|||||||
|
|
||||||
def encode_model_path(
|
def encode_model_path(
|
||||||
base_url: str,
|
base_url: str,
|
||||||
provider_id: int,
|
|
||||||
model_id: str,
|
model_id: str,
|
||||||
endpoint_tag: str | None = None,
|
endpoint_tag: str | None = None,
|
||||||
) -> str:
|
) -> str:
|
||||||
"""Encode the complete upstream route selector advertised to clients."""
|
"""Encode the upstream route selector advertised to clients."""
|
||||||
components: list[tuple[str, str | int]] = [
|
components = [("url", base_url), ("model-id", model_id)]
|
||||||
("url", base_url),
|
|
||||||
("provider-id", provider_id),
|
|
||||||
("model-id", model_id),
|
|
||||||
]
|
|
||||||
if endpoint_tag:
|
if endpoint_tag:
|
||||||
components.append(("endpoint", endpoint_tag))
|
components.append(("endpoint", endpoint_tag))
|
||||||
return urlencode(components)
|
return urlencode(components)
|
||||||
@@ -137,9 +138,9 @@ class ModelPathSelector:
|
|||||||
"""Decoded client-supplied route selector."""
|
"""Decoded client-supplied route selector."""
|
||||||
|
|
||||||
base_url: str
|
base_url: str
|
||||||
provider_id: int
|
|
||||||
model_id: str
|
model_id: str
|
||||||
endpoint_tag: str | None = None
|
endpoint_tag: str | None = None
|
||||||
|
provider_id: int | None = None
|
||||||
|
|
||||||
|
|
||||||
def decode_model_path(path: str) -> ModelPathSelector | None:
|
def decode_model_path(path: str) -> ModelPathSelector | None:
|
||||||
@@ -166,20 +167,21 @@ def decode_model_path(path: str) -> ModelPathSelector | None:
|
|||||||
return None
|
return None
|
||||||
base_url = params.get("url", "")
|
base_url = params.get("url", "")
|
||||||
model_id = params.get("model-id", "")
|
model_id = params.get("model-id", "")
|
||||||
raw_provider_id = params.get("provider-id", "")
|
if not base_url or not model_id:
|
||||||
if not base_url or not model_id or not raw_provider_id:
|
|
||||||
return None
|
|
||||||
try:
|
|
||||||
provider_id = int(raw_provider_id)
|
|
||||||
except ValueError:
|
|
||||||
return None
|
|
||||||
if provider_id <= 0:
|
|
||||||
return None
|
return None
|
||||||
|
provider_id: int | None = None
|
||||||
|
if (raw_provider_id := params.get("provider-id")) is not None:
|
||||||
|
try:
|
||||||
|
provider_id = int(raw_provider_id)
|
||||||
|
except ValueError:
|
||||||
|
return None
|
||||||
|
if provider_id <= 0:
|
||||||
|
return None
|
||||||
return ModelPathSelector(
|
return ModelPathSelector(
|
||||||
base_url=base_url,
|
base_url=base_url,
|
||||||
provider_id=provider_id,
|
|
||||||
model_id=model_id,
|
model_id=model_id,
|
||||||
endpoint_tag=params.get("endpoint") or None,
|
endpoint_tag=params.get("endpoint") or None,
|
||||||
|
provider_id=provider_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -490,9 +492,7 @@ async def _collect_provider_paths(
|
|||||||
model_id = exposed_model_id(model)
|
model_id = exposed_model_id(model)
|
||||||
return DiscoveredPath(
|
return DiscoveredPath(
|
||||||
model_id=model_id,
|
model_id=model_id,
|
||||||
path=encode_model_path(
|
path=encode_model_path(provider_identity.base_url, model_id),
|
||||||
provider_identity.base_url, provider_identity.id, model_id
|
|
||||||
),
|
|
||||||
provider=provider_identity,
|
provider=provider_identity,
|
||||||
model_metadata=_serialize_model_metadata(model, model_id),
|
model_metadata=_serialize_model_metadata(model, model_id),
|
||||||
)
|
)
|
||||||
@@ -529,7 +529,6 @@ async def _collect_provider_paths(
|
|||||||
model_id=model_id,
|
model_id=model_id,
|
||||||
path=encode_model_path(
|
path=encode_model_path(
|
||||||
provider_identity.base_url,
|
provider_identity.base_url,
|
||||||
provider_identity.id,
|
|
||||||
model_id,
|
model_id,
|
||||||
endpoint.tag,
|
endpoint.tag,
|
||||||
),
|
),
|
||||||
@@ -900,7 +899,6 @@ def _serialize_path(row: ModelPathRow, provider_fee: float) -> dict[str, Any]:
|
|||||||
return {
|
return {
|
||||||
"path": row.path,
|
"path": row.path,
|
||||||
"provider": {
|
"provider": {
|
||||||
"id": row.upstream_provider_id,
|
|
||||||
"slug": row.provider_slug,
|
"slug": row.provider_slug,
|
||||||
"type": row.provider_type,
|
"type": row.provider_type,
|
||||||
},
|
},
|
||||||
@@ -914,6 +912,25 @@ async def _provider_fees(session: "AsyncSession") -> dict[int, float]:
|
|||||||
return {row.id: row.provider_fee for row in rows if row.id is not None}
|
return {row.id: row.provider_fee for row in rows if row.id is not None}
|
||||||
|
|
||||||
|
|
||||||
|
def _cheapest_rows_by_path(
|
||||||
|
rows: Iterable[ModelPathRow], fees: dict[int, float]
|
||||||
|
) -> list[tuple[ModelPathRow, float]]:
|
||||||
|
"""Collapse rows sharing a path onto the cheapest provider, with its fee.
|
||||||
|
|
||||||
|
Routing sends a provider-less path to the cheapest provider on that URL, so
|
||||||
|
the advertised slug and pricing must come from that same provider or clients
|
||||||
|
are quoted a price they will never be charged.
|
||||||
|
"""
|
||||||
|
best: dict[tuple[str, str], tuple[ModelPathRow, float]] = {}
|
||||||
|
for row in rows:
|
||||||
|
key = (row.model_id, row.path)
|
||||||
|
fee = fees.get(row.upstream_provider_id, 1.01)
|
||||||
|
incumbent = best.get(key)
|
||||||
|
if incumbent is None or fee < incumbent[1]:
|
||||||
|
best[key] = (row, fee)
|
||||||
|
return list(best.values())
|
||||||
|
|
||||||
|
|
||||||
async def get_all_model_paths() -> dict:
|
async def get_all_model_paths() -> dict:
|
||||||
"""All models with exact routes and provider-specific model metadata."""
|
"""All models with exact routes and provider-specific model metadata."""
|
||||||
async with create_session() as session:
|
async with create_session() as session:
|
||||||
@@ -929,16 +946,9 @@ async def get_all_model_paths() -> dict:
|
|||||||
fees = await _provider_fees(session)
|
fees = await _provider_fees(session)
|
||||||
|
|
||||||
grouped: dict[str, list[dict[str, Any]]] = {}
|
grouped: dict[str, list[dict[str, Any]]] = {}
|
||||||
seen_paths: dict[str, set[str]] = {}
|
updated_at = max((row.updated_at for row in rows), default=0)
|
||||||
updated_at = 0
|
for row, fee in _cheapest_rows_by_path(rows, fees):
|
||||||
for row in rows:
|
grouped.setdefault(row.model_id, []).append(_serialize_path(row, fee))
|
||||||
updated_at = max(updated_at, row.updated_at)
|
|
||||||
if row.path in seen_paths.setdefault(row.model_id, set()):
|
|
||||||
continue
|
|
||||||
seen_paths[row.model_id].add(row.path)
|
|
||||||
grouped.setdefault(row.model_id, []).append(
|
|
||||||
_serialize_path(row, fees.get(row.upstream_provider_id, 1.01))
|
|
||||||
)
|
|
||||||
data = [
|
data = [
|
||||||
{
|
{
|
||||||
"id": grouped_model_id,
|
"id": grouped_model_id,
|
||||||
@@ -974,13 +984,8 @@ async def get_paths_for_model(model_id: str) -> dict:
|
|||||||
rows = await load_rows(session, unprefixed_id)
|
rows = await load_rows(session, unprefixed_id)
|
||||||
fees = await _provider_fees(session)
|
fees = await _provider_fees(session)
|
||||||
|
|
||||||
seen: set[str] = set()
|
updated_at = max((row.updated_at for row in rows), default=0)
|
||||||
paths: list[dict] = []
|
paths = [
|
||||||
updated_at = 0
|
_serialize_path(row, fee) for row, fee in _cheapest_rows_by_path(rows, fees)
|
||||||
for row in rows:
|
]
|
||||||
updated_at = max(updated_at, row.updated_at)
|
|
||||||
if row.path in seen:
|
|
||||||
continue
|
|
||||||
seen.add(row.path)
|
|
||||||
paths.append(_serialize_path(row, fees.get(row.upstream_provider_id, 1.01)))
|
|
||||||
return {"data": paths, "updated_at": updated_at or None}
|
return {"data": paths, "updated_at": updated_at or None}
|
||||||
|
|||||||
@@ -75,17 +75,31 @@ async def _run_proxy(
|
|||||||
|
|
||||||
def test_decode_model_path_round_trips_encode() -> None:
|
def test_decode_model_path_round_trips_encode() -> None:
|
||||||
selector = decode_model_path(
|
selector = decode_model_path(
|
||||||
encode_model_path("https://openrouter.ai/api/v1", 7, MODEL_ID, "deepinfra/fp8")
|
encode_model_path("https://openrouter.ai/api/v1", MODEL_ID, "deepinfra/fp8")
|
||||||
)
|
)
|
||||||
assert selector is not None
|
assert selector is not None
|
||||||
assert selector.base_url == "https://openrouter.ai/api/v1"
|
assert selector.base_url == "https://openrouter.ai/api/v1"
|
||||||
assert selector.provider_id == 7
|
assert selector.provider_id is None
|
||||||
assert selector.model_id == MODEL_ID
|
assert selector.model_id == MODEL_ID
|
||||||
assert selector.endpoint_tag == "deepinfra/fp8"
|
assert selector.endpoint_tag == "deepinfra/fp8"
|
||||||
|
|
||||||
|
|
||||||
|
def test_encoded_path_carries_no_provider_id() -> None:
|
||||||
|
assert "provider-id" not in encode_model_path("http://localhost", MODEL_ID)
|
||||||
|
|
||||||
|
|
||||||
|
def test_decode_model_path_still_accepts_a_legacy_provider_id() -> None:
|
||||||
|
selector = decode_model_path(
|
||||||
|
"url=http%3A%2F%2Flocalhost&provider-id=7&model-id=test-model"
|
||||||
|
)
|
||||||
|
assert selector is not None
|
||||||
|
assert selector.provider_id == 7
|
||||||
|
assert selector.base_url == "http://localhost"
|
||||||
|
assert selector.model_id == MODEL_ID
|
||||||
|
|
||||||
|
|
||||||
def test_decode_model_path_without_endpoint_has_no_tag() -> None:
|
def test_decode_model_path_without_endpoint_has_no_tag() -> None:
|
||||||
selector = decode_model_path(encode_model_path("http://localhost", 1, MODEL_ID))
|
selector = decode_model_path(encode_model_path("http://localhost", MODEL_ID))
|
||||||
assert selector is not None
|
assert selector is not None
|
||||||
assert selector.endpoint_tag is None
|
assert selector.endpoint_tag is None
|
||||||
|
|
||||||
@@ -94,10 +108,12 @@ def test_decode_model_path_without_endpoint_has_no_tag() -> None:
|
|||||||
"path",
|
"path",
|
||||||
[
|
[
|
||||||
"",
|
"",
|
||||||
"url=http://localhost&model-id=test-model",
|
|
||||||
"url=http://localhost&provider-id=abc&model-id=test-model",
|
"url=http://localhost&provider-id=abc&model-id=test-model",
|
||||||
|
"url=http://localhost&provider-id=0&model-id=test-model",
|
||||||
"provider-id=1&model-id=test-model",
|
"provider-id=1&model-id=test-model",
|
||||||
"url=http://localhost&provider-id=1",
|
"url=http://localhost&provider-id=1",
|
||||||
|
"model-id=test-model",
|
||||||
|
"url=http://localhost",
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
def test_decode_model_path_rejects_malformed_selectors(path: str) -> None:
|
def test_decode_model_path_rejects_malformed_selectors(path: str) -> None:
|
||||||
@@ -105,20 +121,63 @@ def test_decode_model_path_rejects_malformed_selectors(path: str) -> None:
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_model_path_routes_to_the_selected_provider() -> None:
|
async def test_model_path_routes_to_the_cheapest_provider_sharing_the_url() -> None:
|
||||||
first, selected = _make_upstream(1), _make_upstream(2)
|
# get_candidates ranks by cost, so the first match for a provider-less
|
||||||
|
# selector is the cheapest provider configured against that URL.
|
||||||
|
cheapest, pricier = _make_upstream(1), _make_upstream(2)
|
||||||
request = _make_request(
|
request = _make_request(
|
||||||
{
|
{
|
||||||
"authorization": "Bearer sk-mpkey",
|
"authorization": "Bearer sk-mpkey",
|
||||||
"x-routstr-model-path": encode_model_path("http://localhost", 2, MODEL_ID),
|
"x-routstr-model-path": encode_model_path("http://localhost", MODEL_ID),
|
||||||
},
|
},
|
||||||
json.dumps({"model": MODEL_ID}).encode(),
|
json.dumps({"model": MODEL_ID}).encode(),
|
||||||
)
|
)
|
||||||
|
|
||||||
await _run_proxy(request, [(MagicMock(), first), (MagicMock(), selected)])
|
await _run_proxy(request, [(MagicMock(), cheapest), (MagicMock(), pricier)])
|
||||||
|
|
||||||
selected.forward_request.assert_awaited_once()
|
cheapest.forward_request.assert_awaited_once()
|
||||||
first.forward_request.assert_not_awaited()
|
pricier.forward_request.assert_not_awaited()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_cheapest_provider_failure_does_not_fall_back_to_the_pricier_one() -> (
|
||||||
|
None
|
||||||
|
):
|
||||||
|
cheapest, pricier = _make_upstream(1, status_code=503), _make_upstream(2)
|
||||||
|
request = _make_request(
|
||||||
|
{
|
||||||
|
"authorization": "Bearer sk-mpkey",
|
||||||
|
"x-routstr-model-path": encode_model_path("http://localhost", MODEL_ID),
|
||||||
|
},
|
||||||
|
json.dumps({"model": MODEL_ID}).encode(),
|
||||||
|
)
|
||||||
|
|
||||||
|
response = await _run_proxy(
|
||||||
|
request, [(MagicMock(), cheapest), (MagicMock(), pricier)]
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 503
|
||||||
|
cheapest.forward_request.assert_awaited_once()
|
||||||
|
pricier.forward_request.assert_not_awaited()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_legacy_provider_id_still_pins_that_exact_provider() -> None:
|
||||||
|
cheapest, pinned = _make_upstream(1), _make_upstream(2)
|
||||||
|
request = _make_request(
|
||||||
|
{
|
||||||
|
"authorization": "Bearer sk-mpkey",
|
||||||
|
"x-routstr-model-path": (
|
||||||
|
f"url=http%3A%2F%2Flocalhost&provider-id=2&model-id={MODEL_ID}"
|
||||||
|
),
|
||||||
|
},
|
||||||
|
json.dumps({"model": MODEL_ID}).encode(),
|
||||||
|
)
|
||||||
|
|
||||||
|
await _run_proxy(request, [(MagicMock(), cheapest), (MagicMock(), pinned)])
|
||||||
|
|
||||||
|
pinned.forward_request.assert_awaited_once()
|
||||||
|
cheapest.forward_request.assert_not_awaited()
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
@@ -131,7 +190,7 @@ async def test_model_path_failure_is_returned_without_falling_back(
|
|||||||
request = _make_request(
|
request = _make_request(
|
||||||
{
|
{
|
||||||
"authorization": "Bearer sk-mpkey",
|
"authorization": "Bearer sk-mpkey",
|
||||||
"x-routstr-model-path": encode_model_path("http://localhost", 1, MODEL_ID),
|
"x-routstr-model-path": encode_model_path("http://localhost", MODEL_ID),
|
||||||
},
|
},
|
||||||
json.dumps({"model": MODEL_ID}).encode(),
|
json.dumps({"model": MODEL_ID}).encode(),
|
||||||
)
|
)
|
||||||
@@ -146,12 +205,14 @@ async def test_model_path_failure_is_returned_without_falling_back(
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_unknown_provider_in_model_path_is_rejected() -> None:
|
async def test_unknown_legacy_provider_in_model_path_is_rejected() -> None:
|
||||||
upstream = _make_upstream(1)
|
upstream = _make_upstream(1)
|
||||||
request = _make_request(
|
request = _make_request(
|
||||||
{
|
{
|
||||||
"authorization": "Bearer sk-mpkey",
|
"authorization": "Bearer sk-mpkey",
|
||||||
"x-routstr-model-path": encode_model_path("http://localhost", 99, MODEL_ID),
|
"x-routstr-model-path": (
|
||||||
|
f"url=http%3A%2F%2Flocalhost&provider-id=99&model-id={MODEL_ID}"
|
||||||
|
),
|
||||||
},
|
},
|
||||||
json.dumps({"model": MODEL_ID}).encode(),
|
json.dumps({"model": MODEL_ID}).encode(),
|
||||||
)
|
)
|
||||||
@@ -170,7 +231,7 @@ async def test_model_path_disagreeing_with_the_body_model_is_rejected() -> None:
|
|||||||
{
|
{
|
||||||
"authorization": "Bearer sk-mpkey",
|
"authorization": "Bearer sk-mpkey",
|
||||||
"x-routstr-model-path": encode_model_path(
|
"x-routstr-model-path": encode_model_path(
|
||||||
"http://localhost", 1, "other-model"
|
"http://localhost", "other-model"
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
json.dumps({"model": MODEL_ID}).encode(),
|
json.dumps({"model": MODEL_ID}).encode(),
|
||||||
@@ -207,7 +268,7 @@ async def test_endpoint_tag_pins_the_upstream_subprovider() -> None:
|
|||||||
{
|
{
|
||||||
"authorization": "Bearer sk-mpkey",
|
"authorization": "Bearer sk-mpkey",
|
||||||
"x-routstr-model-path": encode_model_path(
|
"x-routstr-model-path": encode_model_path(
|
||||||
"https://openrouter.ai/api/v1", 1, MODEL_ID, "deepinfra/fp8"
|
"https://openrouter.ai/api/v1", MODEL_ID, "deepinfra/fp8"
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
json.dumps({"model": MODEL_ID}).encode(),
|
json.dumps({"model": MODEL_ID}).encode(),
|
||||||
@@ -245,7 +306,7 @@ async def test_selector_url_must_match_configured_provider() -> None:
|
|||||||
{
|
{
|
||||||
"authorization": "Bearer key",
|
"authorization": "Bearer key",
|
||||||
"x-routstr-model-path": encode_model_path(
|
"x-routstr-model-path": encode_model_path(
|
||||||
"http://169.254.169.254", 1, MODEL_ID
|
"http://169.254.169.254", MODEL_ID
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
json.dumps({"model": MODEL_ID}).encode(),
|
json.dumps({"model": MODEL_ID}).encode(),
|
||||||
@@ -266,7 +327,7 @@ async def test_endpoint_pin_cannot_be_stripped_on_retry() -> None:
|
|||||||
{
|
{
|
||||||
"authorization": "Bearer key",
|
"authorization": "Bearer key",
|
||||||
"x-routstr-model-path": encode_model_path(
|
"x-routstr-model-path": encode_model_path(
|
||||||
upstream.base_url, 1, MODEL_ID, "deepinfra/fp8"
|
upstream.base_url, MODEL_ID, "deepinfra/fp8"
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
json.dumps({"model": MODEL_ID}).encode(),
|
json.dumps({"model": MODEL_ID}).encode(),
|
||||||
@@ -298,7 +359,7 @@ async def test_cashu_receives_endpoint_pinned_body(path: str, handler: str) -> N
|
|||||||
{
|
{
|
||||||
"x-cashu": "test-token",
|
"x-cashu": "test-token",
|
||||||
"x-routstr-model-path": encode_model_path(
|
"x-routstr-model-path": encode_model_path(
|
||||||
upstream.base_url, 1, MODEL_ID, "deepinfra/fp8"
|
upstream.base_url, MODEL_ID, "deepinfra/fp8"
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
json.dumps(
|
json.dumps(
|
||||||
@@ -354,7 +415,7 @@ async def test_cashu_pin_reaches_http_transport(path: str, status_code: int) ->
|
|||||||
{
|
{
|
||||||
"x-cashu": "test-token",
|
"x-cashu": "test-token",
|
||||||
"x-routstr-model-path": encode_model_path(
|
"x-routstr-model-path": encode_model_path(
|
||||||
upstream.base_url, 1, MODEL_ID, "deepinfra/fp8"
|
upstream.base_url, MODEL_ID, "deepinfra/fp8"
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
json.dumps({"model": MODEL_ID, "provider": {"allow_fallbacks": True}}).encode(),
|
json.dumps({"model": MODEL_ID, "provider": {"allow_fallbacks": True}}).encode(),
|
||||||
@@ -418,7 +479,7 @@ async def test_pinned_exception_does_not_fall_back() -> None:
|
|||||||
request = _make_request(
|
request = _make_request(
|
||||||
{
|
{
|
||||||
"authorization": "Bearer key",
|
"authorization": "Bearer key",
|
||||||
"x-routstr-model-path": encode_model_path(first.base_url, 1, MODEL_ID),
|
"x-routstr-model-path": encode_model_path(first.base_url, MODEL_ID),
|
||||||
},
|
},
|
||||||
json.dumps({"model": MODEL_ID}).encode(),
|
json.dumps({"model": MODEL_ID}).encode(),
|
||||||
)
|
)
|
||||||
@@ -442,7 +503,7 @@ async def test_unsupported_endpoint_pins_fail_before_payment(
|
|||||||
headers = {
|
headers = {
|
||||||
"x-cashu": "test-token",
|
"x-cashu": "test-token",
|
||||||
"x-routstr-model-path": encode_model_path(
|
"x-routstr-model-path": encode_model_path(
|
||||||
upstream.base_url, 1, MODEL_ID, "deepinfra/fp8"
|
upstream.base_url, MODEL_ID, "deepinfra/fp8"
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
if is_ehbp:
|
if is_ehbp:
|
||||||
@@ -471,7 +532,7 @@ async def test_ehbp_pin_does_not_fall_back(cashu: bool) -> None:
|
|||||||
headers = {
|
headers = {
|
||||||
"ehbp-encapsulated-key": "sealed",
|
"ehbp-encapsulated-key": "sealed",
|
||||||
"x-routstr-model": MODEL_ID,
|
"x-routstr-model": MODEL_ID,
|
||||||
"x-routstr-model-path": encode_model_path(selected.base_url, 1, MODEL_ID),
|
"x-routstr-model-path": encode_model_path(selected.base_url, MODEL_ID),
|
||||||
}
|
}
|
||||||
headers.update({"x-cashu": "token"} if cashu else {"authorization": "Bearer key"})
|
headers.update({"x-cashu": "token"} if cashu else {"authorization": "Bearer key"})
|
||||||
request = _make_request(headers, b"encrypted-body")
|
request = _make_request(headers, b"encrypted-body")
|
||||||
@@ -495,7 +556,7 @@ async def test_duplicate_header_fields_are_rejected() -> None:
|
|||||||
from starlette.datastructures import Headers
|
from starlette.datastructures import Headers
|
||||||
|
|
||||||
selected = _make_upstream(1)
|
selected = _make_upstream(1)
|
||||||
route = encode_model_path(selected.base_url, 1, MODEL_ID).encode()
|
route = encode_model_path(selected.base_url, MODEL_ID).encode()
|
||||||
request = _make_request({}, json.dumps({"model": MODEL_ID}).encode())
|
request = _make_request({}, json.dumps({"model": MODEL_ID}).encode())
|
||||||
request.headers = Headers(
|
request.headers = Headers(
|
||||||
raw=[
|
raw=[
|
||||||
@@ -512,7 +573,7 @@ async def test_duplicate_header_fields_are_rejected() -> None:
|
|||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_attestation_does_not_ignore_model_path() -> None:
|
async def test_attestation_does_not_ignore_model_path() -> None:
|
||||||
request = _make_request(
|
request = _make_request(
|
||||||
{"x-routstr-model-path": encode_model_path("http://localhost", 1, MODEL_ID)},
|
{"x-routstr-model-path": encode_model_path("http://localhost", MODEL_ID)},
|
||||||
b"",
|
b"",
|
||||||
)
|
)
|
||||||
request.method = "GET"
|
request.method = "GET"
|
||||||
@@ -528,7 +589,7 @@ async def test_model_fallback_list_is_rejected_when_pinned() -> None:
|
|||||||
request = _make_request(
|
request = _make_request(
|
||||||
{
|
{
|
||||||
"authorization": "Bearer key",
|
"authorization": "Bearer key",
|
||||||
"x-routstr-model-path": encode_model_path(selected.base_url, 1, MODEL_ID),
|
"x-routstr-model-path": encode_model_path(selected.base_url, MODEL_ID),
|
||||||
},
|
},
|
||||||
json.dumps({"model": MODEL_ID, "models": ["other-model"]}).encode(),
|
json.dumps({"model": MODEL_ID, "models": ["other-model"]}).encode(),
|
||||||
)
|
)
|
||||||
@@ -564,7 +625,7 @@ async def test_pinned_recovery_stays_on_selected_provider(
|
|||||||
{
|
{
|
||||||
"authorization": "Bearer key",
|
"authorization": "Bearer key",
|
||||||
"x-routstr-model-path": encode_model_path(
|
"x-routstr-model-path": encode_model_path(
|
||||||
selected.base_url, 1, MODEL_ID, endpoint
|
selected.base_url, MODEL_ID, endpoint
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
json.dumps(
|
json.dumps(
|
||||||
@@ -608,7 +669,7 @@ async def test_pinned_recovery_preserves_routing_fields(
|
|||||||
{
|
{
|
||||||
"authorization": "Bearer key",
|
"authorization": "Bearer key",
|
||||||
"x-routstr-model-path": encode_model_path(
|
"x-routstr-model-path": encode_model_path(
|
||||||
selected.base_url, 1, MODEL_ID, endpoint
|
selected.base_url, MODEL_ID, endpoint
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
json.dumps(
|
json.dumps(
|
||||||
|
|||||||
@@ -272,6 +272,17 @@ async def _set_provider_fee(engine: AsyncEngine, provider_id: int, fee: float) -
|
|||||||
await session.commit()
|
await session.commit()
|
||||||
|
|
||||||
|
|
||||||
|
async def _set_provider_base_url(
|
||||||
|
engine: AsyncEngine, provider_id: int, base_url: str
|
||||||
|
) -> None:
|
||||||
|
async with AsyncSession(engine) as session:
|
||||||
|
provider = await session.get(UpstreamProviderRow, provider_id)
|
||||||
|
assert provider is not None
|
||||||
|
provider.base_url = base_url
|
||||||
|
session.add(provider)
|
||||||
|
await session.commit()
|
||||||
|
|
||||||
|
|
||||||
def _paths_of(payload: dict, model_id: str) -> set[str]:
|
def _paths_of(payload: dict, model_id: str) -> set[str]:
|
||||||
for entry in payload["data"]:
|
for entry in payload["data"]:
|
||||||
if entry["id"] == model_id:
|
if entry["id"] == model_id:
|
||||||
@@ -289,7 +300,7 @@ def _expected_path(
|
|||||||
endpoint_tag: str | None = None,
|
endpoint_tag: str | None = None,
|
||||||
) -> str:
|
) -> str:
|
||||||
return mp.encode_model_path(
|
return mp.encode_model_path(
|
||||||
f"https://provider-{provider_id}", provider_id, model_id, endpoint_tag
|
f"https://provider-{provider_id}", model_id, endpoint_tag
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -308,7 +319,6 @@ def _path_entry(
|
|||||||
return {
|
return {
|
||||||
"path": _expected_path(provider_id, model_id, endpoint_tag),
|
"path": _expected_path(provider_id, model_id, endpoint_tag),
|
||||||
"provider": {
|
"provider": {
|
||||||
"id": provider_id,
|
|
||||||
"slug": provider_slug or f"p{provider_id}",
|
"slug": provider_slug or f"p{provider_id}",
|
||||||
"type": provider_type
|
"type": provider_type
|
||||||
or ("anthropic" if provider_id == 1 else "openrouter"),
|
or ("anthropic" if provider_id == 1 else "openrouter"),
|
||||||
@@ -368,21 +378,20 @@ def test_public_provider_url_preserves_public_urls_without_ports() -> None:
|
|||||||
assert mp.public_provider_url("http://localhost") == "http://localhost"
|
assert mp.public_provider_url("http://localhost") == "http://localhost"
|
||||||
|
|
||||||
|
|
||||||
def test_encode_model_path_includes_complete_route_identity() -> None:
|
def test_encode_model_path_omits_provider_id() -> None:
|
||||||
assert mp.encode_model_path(
|
assert mp.encode_model_path(
|
||||||
"https://openrouter.ai/api/v1", 42, "anthropic/claude-sonnet-4"
|
"https://openrouter.ai/api/v1", "anthropic/claude-sonnet-4"
|
||||||
) == (
|
) == (
|
||||||
"url=https%3A%2F%2Fopenrouter.ai%2Fapi%2Fv1"
|
"url=https%3A%2F%2Fopenrouter.ai%2Fapi%2Fv1"
|
||||||
"&provider-id=42&model-id=anthropic%2Fclaude-sonnet-4"
|
"&model-id=anthropic%2Fclaude-sonnet-4"
|
||||||
)
|
)
|
||||||
assert mp.encode_model_path(
|
assert mp.encode_model_path(
|
||||||
"https://openrouter.ai/api/v1",
|
"https://openrouter.ai/api/v1",
|
||||||
42,
|
|
||||||
"anthropic/claude-sonnet-4",
|
"anthropic/claude-sonnet-4",
|
||||||
"google-vertex/us-east5",
|
"google-vertex/us-east5",
|
||||||
) == (
|
) == (
|
||||||
"url=https%3A%2F%2Fopenrouter.ai%2Fapi%2Fv1"
|
"url=https%3A%2F%2Fopenrouter.ai%2Fapi%2Fv1"
|
||||||
"&provider-id=42&model-id=anthropic%2Fclaude-sonnet-4"
|
"&model-id=anthropic%2Fclaude-sonnet-4"
|
||||||
"&endpoint=google-vertex%2Fus-east5"
|
"&endpoint=google-vertex%2Fus-east5"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -506,7 +515,7 @@ async def test_direct_path_masks_private_configured_provider_url(
|
|||||||
await mp.refresh_model_paths([provider])
|
await mp.refresh_model_paths([provider])
|
||||||
|
|
||||||
assert _paths_of(await mp.get_all_model_paths(), "local-model") == {
|
assert _paths_of(await mp.get_all_model_paths(), "local-model") == {
|
||||||
mp.encode_model_path("http://localhost", 1, "local-model")
|
mp.encode_model_path("http://localhost", "local-model")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -886,7 +895,7 @@ async def test_openrouter_provider_adds_endpoint_paths(
|
|||||||
assert {
|
assert {
|
||||||
item["endpoint"]["name"] for item in payload["data"] if item["endpoint"]
|
item["endpoint"]["name"] for item in payload["data"] if item["endpoint"]
|
||||||
} == {"Google"}
|
} == {"Google"}
|
||||||
assert {item["provider"]["id"] for item in payload["data"]} == {2}
|
assert {item["provider"]["slug"] for item in payload["data"]} == {"p2"}
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
@@ -984,6 +993,97 @@ async def test_path_pricing_uses_the_provider_fee_of_its_own_provider(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_providers_sharing_a_url_collapse_onto_the_cheapest_path(
|
||||||
|
patched_session: AsyncEngine, sats_quote: float
|
||||||
|
) -> None:
|
||||||
|
"""One selector per URL, describing the provider routing will actually use."""
|
||||||
|
shared_url = "https://shared.example/v1"
|
||||||
|
await _set_provider_base_url(patched_session, 1, shared_url)
|
||||||
|
await _set_provider_base_url(patched_session, 2, shared_url)
|
||||||
|
await _set_provider_fee(patched_session, 1, 2.0)
|
||||||
|
await _set_provider_fee(patched_session, 2, 1.1)
|
||||||
|
|
||||||
|
await mp.refresh_model_paths(
|
||||||
|
[
|
||||||
|
_FakeProvider(
|
||||||
|
provider_type="anthropic",
|
||||||
|
base_url=shared_url,
|
||||||
|
models=[_model("shared-model")],
|
||||||
|
db_id=1,
|
||||||
|
),
|
||||||
|
_FakeProvider(
|
||||||
|
provider_type="openrouter",
|
||||||
|
base_url=shared_url,
|
||||||
|
models=[_model("shared-model")],
|
||||||
|
db_id=2,
|
||||||
|
),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
payload = await mp.get_paths_for_model("shared-model")
|
||||||
|
assert [item["path"] for item in payload["data"]] == [
|
||||||
|
mp.encode_model_path(shared_url, "shared-model")
|
||||||
|
]
|
||||||
|
assert payload["data"][0]["provider"] == {"slug": "p2", "type": "openrouter"}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_collapsed_path_advertises_the_cheapest_provider_pricing(
|
||||||
|
patched_session: AsyncEngine, sats_quote: float
|
||||||
|
) -> None:
|
||||||
|
shared_url = "https://shared.example/v1"
|
||||||
|
await _set_provider_base_url(patched_session, 1, shared_url)
|
||||||
|
await _set_provider_base_url(patched_session, 2, shared_url)
|
||||||
|
await _set_provider_fee(patched_session, 1, 2.0)
|
||||||
|
await _set_provider_fee(patched_session, 2, 1.1)
|
||||||
|
|
||||||
|
priced = _model("shared-model")
|
||||||
|
priced.pricing = {"prompt": "0.000005", "completion": "0.000005"}
|
||||||
|
|
||||||
|
await mp.refresh_model_paths(
|
||||||
|
[
|
||||||
|
_FakeProvider(
|
||||||
|
provider_type="anthropic",
|
||||||
|
base_url=shared_url,
|
||||||
|
models=[priced],
|
||||||
|
db_id=1,
|
||||||
|
),
|
||||||
|
_FakeProvider(
|
||||||
|
provider_type="openrouter",
|
||||||
|
base_url=shared_url,
|
||||||
|
models=[priced],
|
||||||
|
db_id=2,
|
||||||
|
),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
payload = await mp.get_paths_for_model("shared-model")
|
||||||
|
assert len(payload["data"]) == 1
|
||||||
|
assert payload["data"][0]["model"]["pricing"]["prompt"] == pytest.approx(
|
||||||
|
0.000005 * 1.1
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_paths_advertise_no_provider_id(patched_session: AsyncEngine) -> None:
|
||||||
|
await mp.refresh_model_paths(
|
||||||
|
[
|
||||||
|
_FakeProvider(
|
||||||
|
provider_type="anthropic",
|
||||||
|
base_url="https://provider-1",
|
||||||
|
models=[_model("claude-x")],
|
||||||
|
db_id=1,
|
||||||
|
)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
payload = await mp.get_all_model_paths()
|
||||||
|
entry = payload["data"][0]["paths"][0]
|
||||||
|
assert "provider-id" not in entry["path"]
|
||||||
|
assert "id" not in entry["provider"]
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_paths_keep_upstream_pricing_when_the_quote_is_unavailable(
|
async def test_paths_keep_upstream_pricing_when_the_quote_is_unavailable(
|
||||||
patched_session: AsyncEngine, monkeypatch: pytest.MonkeyPatch
|
patched_session: AsyncEngine, monkeypatch: pytest.MonkeyPatch
|
||||||
|
|||||||
Reference in New Issue
Block a user