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",
|
||||
"paths": [
|
||||
{
|
||||
"path": "url=https%3A%2F%2Fapi.anthropic.com%2Fv1&provider-id=12&model-id=anthropic%2Fclaude-sonnet-4",
|
||||
"provider": {"id": 12, "slug": "anthropic-primary", "type": "anthropic"},
|
||||
"path": "url=https%3A%2F%2Fapi.anthropic.com%2Fv1&model-id=anthropic%2Fclaude-sonnet-4",
|
||||
"provider": {"slug": "anthropic-primary", "type": "anthropic"},
|
||||
"endpoint": null
|
||||
},
|
||||
{
|
||||
"path": "url=https%3A%2F%2Fopenrouter.ai%2Fapi%2Fv1&provider-id=42&model-id=anthropic%2Fclaude-sonnet-4&endpoint=google-vertex%2Fus",
|
||||
"provider": {"id": 42, "slug": "openrouter-main", "type": "openrouter"},
|
||||
"path": "url=https%3A%2F%2Fopenrouter.ai%2Fapi%2Fv1&model-id=anthropic%2Fclaude-sonnet-4&endpoint=google-vertex%2Fus",
|
||||
"provider": {"slug": "openrouter-main", "type": "openrouter"},
|
||||
"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
|
||||
unchanged rather than parsing or reconstructing it. It identifies the exact
|
||||
configured route with `url`, `provider-id`, and `model-id`. To avoid exposing
|
||||
private network details, a configured private IP address or any URL with an
|
||||
explicit port is advertised as `http://localhost`. OpenRouter routes additionally
|
||||
preserve the exact machine-readable endpoint `tag`. Provider slugs/types and
|
||||
endpoint names remain display data. When request-side selection is implemented,
|
||||
an endpoint tag must not silently fall back to another backend.
|
||||
unchanged rather than parsing or reconstructing it. It identifies the route with
|
||||
`url` and `model-id`. To avoid exposing private network details, a configured
|
||||
private IP address or any URL with an explicit port is advertised as
|
||||
`http://localhost`. OpenRouter routes additionally preserve the exact
|
||||
machine-readable endpoint `tag`. Provider slugs/types and endpoint names remain
|
||||
display data. When request-side selection is implemented, an endpoint tag must
|
||||
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
|
||||
|
||||
|
||||
+16
-6
@@ -132,11 +132,17 @@ def _candidate_for_selector(
|
||||
selector: ModelPathSelector,
|
||||
candidates: list[tuple[Model, BaseUpstreamProvider]],
|
||||
) -> 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:
|
||||
if (
|
||||
upstream.db_id == selector.provider_id
|
||||
and public_provider_url(upstream.base_url) == selector.base_url
|
||||
):
|
||||
if public_provider_url(upstream.base_url) != selector.base_url:
|
||||
continue
|
||||
if selector.provider_id is not None and upstream.db_id != selector.provider_id:
|
||||
continue
|
||||
return model_obj, upstream
|
||||
return None
|
||||
|
||||
@@ -540,10 +546,14 @@ async def _proxy(
|
||||
if selector is not None:
|
||||
pinned = _candidate_for_selector(selector, candidates)
|
||||
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(
|
||||
"invalid_model_path",
|
||||
f"Model '{selector.model_id}' is not routable through provider "
|
||||
f"{selector.provider_id}",
|
||||
f"Model '{selector.model_id}' is not routable through {target}",
|
||||
404,
|
||||
request=request,
|
||||
)
|
||||
|
||||
@@ -2,11 +2,16 @@
|
||||
|
||||
Exposes every selectable upstream route a Routstr model is reachable through.
|
||||
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
|
||||
endpoint, its machine-readable tag::
|
||||
upstream URL, client-visible model ID and, for an exact OpenRouter endpoint,
|
||||
its machine-readable tag::
|
||||
|
||||
url=https%3A%2F%2Fapi.anthropic.com%2Fv1&provider-id=12&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%2Fapi.anthropic.com%2Fv1&model-id=claude-sonnet-4
|
||||
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
|
||||
@@ -16,6 +21,7 @@ import ipaddress
|
||||
import json
|
||||
import random
|
||||
import time
|
||||
from collections.abc import Iterable
|
||||
from dataclasses import dataclass
|
||||
from typing import TYPE_CHECKING, Any, Callable
|
||||
from urllib.parse import parse_qsl, urlencode, urlsplit
|
||||
@@ -117,16 +123,11 @@ def public_provider_url(base_url: str) -> str:
|
||||
|
||||
def encode_model_path(
|
||||
base_url: str,
|
||||
provider_id: int,
|
||||
model_id: str,
|
||||
endpoint_tag: str | None = None,
|
||||
) -> str:
|
||||
"""Encode the complete upstream route selector advertised to clients."""
|
||||
components: list[tuple[str, str | int]] = [
|
||||
("url", base_url),
|
||||
("provider-id", provider_id),
|
||||
("model-id", model_id),
|
||||
]
|
||||
"""Encode the upstream route selector advertised to clients."""
|
||||
components = [("url", base_url), ("model-id", model_id)]
|
||||
if endpoint_tag:
|
||||
components.append(("endpoint", endpoint_tag))
|
||||
return urlencode(components)
|
||||
@@ -137,9 +138,9 @@ class ModelPathSelector:
|
||||
"""Decoded client-supplied route selector."""
|
||||
|
||||
base_url: str
|
||||
provider_id: int
|
||||
model_id: str
|
||||
endpoint_tag: str | None = None
|
||||
provider_id: int | None = None
|
||||
|
||||
|
||||
def decode_model_path(path: str) -> ModelPathSelector | None:
|
||||
@@ -166,9 +167,10 @@ def decode_model_path(path: str) -> ModelPathSelector | None:
|
||||
return None
|
||||
base_url = params.get("url", "")
|
||||
model_id = params.get("model-id", "")
|
||||
raw_provider_id = params.get("provider-id", "")
|
||||
if not base_url or not model_id or not raw_provider_id:
|
||||
if not base_url or not model_id:
|
||||
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:
|
||||
@@ -177,9 +179,9 @@ def decode_model_path(path: str) -> ModelPathSelector | None:
|
||||
return None
|
||||
return ModelPathSelector(
|
||||
base_url=base_url,
|
||||
provider_id=provider_id,
|
||||
model_id=model_id,
|
||||
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)
|
||||
return DiscoveredPath(
|
||||
model_id=model_id,
|
||||
path=encode_model_path(
|
||||
provider_identity.base_url, provider_identity.id, model_id
|
||||
),
|
||||
path=encode_model_path(provider_identity.base_url, model_id),
|
||||
provider=provider_identity,
|
||||
model_metadata=_serialize_model_metadata(model, model_id),
|
||||
)
|
||||
@@ -529,7 +529,6 @@ async def _collect_provider_paths(
|
||||
model_id=model_id,
|
||||
path=encode_model_path(
|
||||
provider_identity.base_url,
|
||||
provider_identity.id,
|
||||
model_id,
|
||||
endpoint.tag,
|
||||
),
|
||||
@@ -900,7 +899,6 @@ def _serialize_path(row: ModelPathRow, provider_fee: float) -> dict[str, Any]:
|
||||
return {
|
||||
"path": row.path,
|
||||
"provider": {
|
||||
"id": row.upstream_provider_id,
|
||||
"slug": row.provider_slug,
|
||||
"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}
|
||||
|
||||
|
||||
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:
|
||||
"""All models with exact routes and provider-specific model metadata."""
|
||||
async with create_session() as session:
|
||||
@@ -929,16 +946,9 @@ async def get_all_model_paths() -> dict:
|
||||
fees = await _provider_fees(session)
|
||||
|
||||
grouped: dict[str, list[dict[str, Any]]] = {}
|
||||
seen_paths: dict[str, set[str]] = {}
|
||||
updated_at = 0
|
||||
for row in rows:
|
||||
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))
|
||||
)
|
||||
updated_at = max((row.updated_at for row in rows), default=0)
|
||||
for row, fee in _cheapest_rows_by_path(rows, fees):
|
||||
grouped.setdefault(row.model_id, []).append(_serialize_path(row, fee))
|
||||
data = [
|
||||
{
|
||||
"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)
|
||||
fees = await _provider_fees(session)
|
||||
|
||||
seen: set[str] = set()
|
||||
paths: list[dict] = []
|
||||
updated_at = 0
|
||||
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)))
|
||||
updated_at = max((row.updated_at for row in rows), default=0)
|
||||
paths = [
|
||||
_serialize_path(row, fee) for row, fee in _cheapest_rows_by_path(rows, fees)
|
||||
]
|
||||
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:
|
||||
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.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.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:
|
||||
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.endpoint_tag is None
|
||||
|
||||
@@ -94,10 +108,12 @@ def test_decode_model_path_without_endpoint_has_no_tag() -> None:
|
||||
"path",
|
||||
[
|
||||
"",
|
||||
"url=http://localhost&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",
|
||||
"url=http://localhost&provider-id=1",
|
||||
"model-id=test-model",
|
||||
"url=http://localhost",
|
||||
],
|
||||
)
|
||||
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
|
||||
async def test_model_path_routes_to_the_selected_provider() -> None:
|
||||
first, selected = _make_upstream(1), _make_upstream(2)
|
||||
async def test_model_path_routes_to_the_cheapest_provider_sharing_the_url() -> None:
|
||||
# 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(
|
||||
{
|
||||
"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(),
|
||||
)
|
||||
|
||||
await _run_proxy(request, [(MagicMock(), first), (MagicMock(), selected)])
|
||||
await _run_proxy(request, [(MagicMock(), cheapest), (MagicMock(), pricier)])
|
||||
|
||||
selected.forward_request.assert_awaited_once()
|
||||
first.forward_request.assert_not_awaited()
|
||||
cheapest.forward_request.assert_awaited_once()
|
||||
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
|
||||
@@ -131,7 +190,7 @@ async def test_model_path_failure_is_returned_without_falling_back(
|
||||
request = _make_request(
|
||||
{
|
||||
"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(),
|
||||
)
|
||||
@@ -146,12 +205,14 @@ async def test_model_path_failure_is_returned_without_falling_back(
|
||||
|
||||
|
||||
@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)
|
||||
request = _make_request(
|
||||
{
|
||||
"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(),
|
||||
)
|
||||
@@ -170,7 +231,7 @@ async def test_model_path_disagreeing_with_the_body_model_is_rejected() -> None:
|
||||
{
|
||||
"authorization": "Bearer sk-mpkey",
|
||||
"x-routstr-model-path": encode_model_path(
|
||||
"http://localhost", 1, "other-model"
|
||||
"http://localhost", "other-model"
|
||||
),
|
||||
},
|
||||
json.dumps({"model": MODEL_ID}).encode(),
|
||||
@@ -207,7 +268,7 @@ async def test_endpoint_tag_pins_the_upstream_subprovider() -> None:
|
||||
{
|
||||
"authorization": "Bearer sk-mpkey",
|
||||
"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(),
|
||||
@@ -245,7 +306,7 @@ async def test_selector_url_must_match_configured_provider() -> None:
|
||||
{
|
||||
"authorization": "Bearer key",
|
||||
"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(),
|
||||
@@ -266,7 +327,7 @@ async def test_endpoint_pin_cannot_be_stripped_on_retry() -> None:
|
||||
{
|
||||
"authorization": "Bearer key",
|
||||
"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(),
|
||||
@@ -298,7 +359,7 @@ async def test_cashu_receives_endpoint_pinned_body(path: str, handler: str) -> N
|
||||
{
|
||||
"x-cashu": "test-token",
|
||||
"x-routstr-model-path": encode_model_path(
|
||||
upstream.base_url, 1, MODEL_ID, "deepinfra/fp8"
|
||||
upstream.base_url, MODEL_ID, "deepinfra/fp8"
|
||||
),
|
||||
},
|
||||
json.dumps(
|
||||
@@ -354,7 +415,7 @@ async def test_cashu_pin_reaches_http_transport(path: str, status_code: int) ->
|
||||
{
|
||||
"x-cashu": "test-token",
|
||||
"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(),
|
||||
@@ -418,7 +479,7 @@ async def test_pinned_exception_does_not_fall_back() -> None:
|
||||
request = _make_request(
|
||||
{
|
||||
"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(),
|
||||
)
|
||||
@@ -442,7 +503,7 @@ async def test_unsupported_endpoint_pins_fail_before_payment(
|
||||
headers = {
|
||||
"x-cashu": "test-token",
|
||||
"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:
|
||||
@@ -471,7 +532,7 @@ async def test_ehbp_pin_does_not_fall_back(cashu: bool) -> None:
|
||||
headers = {
|
||||
"ehbp-encapsulated-key": "sealed",
|
||||
"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"})
|
||||
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
|
||||
|
||||
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.headers = Headers(
|
||||
raw=[
|
||||
@@ -512,7 +573,7 @@ async def test_duplicate_header_fields_are_rejected() -> None:
|
||||
@pytest.mark.asyncio
|
||||
async def test_attestation_does_not_ignore_model_path() -> None:
|
||||
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"",
|
||||
)
|
||||
request.method = "GET"
|
||||
@@ -528,7 +589,7 @@ async def test_model_fallback_list_is_rejected_when_pinned() -> None:
|
||||
request = _make_request(
|
||||
{
|
||||
"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(),
|
||||
)
|
||||
@@ -564,7 +625,7 @@ async def test_pinned_recovery_stays_on_selected_provider(
|
||||
{
|
||||
"authorization": "Bearer key",
|
||||
"x-routstr-model-path": encode_model_path(
|
||||
selected.base_url, 1, MODEL_ID, endpoint
|
||||
selected.base_url, MODEL_ID, endpoint
|
||||
),
|
||||
},
|
||||
json.dumps(
|
||||
@@ -608,7 +669,7 @@ async def test_pinned_recovery_preserves_routing_fields(
|
||||
{
|
||||
"authorization": "Bearer key",
|
||||
"x-routstr-model-path": encode_model_path(
|
||||
selected.base_url, 1, MODEL_ID, endpoint
|
||||
selected.base_url, MODEL_ID, endpoint
|
||||
),
|
||||
},
|
||||
json.dumps(
|
||||
|
||||
@@ -272,6 +272,17 @@ async def _set_provider_fee(engine: AsyncEngine, provider_id: int, fee: float) -
|
||||
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]:
|
||||
for entry in payload["data"]:
|
||||
if entry["id"] == model_id:
|
||||
@@ -289,7 +300,7 @@ def _expected_path(
|
||||
endpoint_tag: str | None = None,
|
||||
) -> str:
|
||||
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 {
|
||||
"path": _expected_path(provider_id, model_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"),
|
||||
@@ -368,21 +378,20 @@ def test_public_provider_url_preserves_public_urls_without_ports() -> None:
|
||||
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(
|
||||
"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"
|
||||
"&provider-id=42&model-id=anthropic%2Fclaude-sonnet-4"
|
||||
"&model-id=anthropic%2Fclaude-sonnet-4"
|
||||
)
|
||||
assert mp.encode_model_path(
|
||||
"https://openrouter.ai/api/v1",
|
||||
42,
|
||||
"anthropic/claude-sonnet-4",
|
||||
"google-vertex/us-east5",
|
||||
) == (
|
||||
"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"
|
||||
)
|
||||
|
||||
@@ -506,7 +515,7 @@ async def test_direct_path_masks_private_configured_provider_url(
|
||||
await mp.refresh_model_paths([provider])
|
||||
|
||||
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 {
|
||||
item["endpoint"]["name"] for item in payload["data"] if item["endpoint"]
|
||||
} == {"Google"}
|
||||
assert {item["provider"]["id"] for item in payload["data"]} == {2}
|
||||
assert {item["provider"]["slug"] for item in payload["data"]} == {"p2"}
|
||||
|
||||
|
||||
@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
|
||||
async def test_paths_keep_upstream_pricing_when_the_quote_is_unavailable(
|
||||
patched_session: AsyncEngine, monkeypatch: pytest.MonkeyPatch
|
||||
|
||||
Reference in New Issue
Block a user