mirror of
https://github.com/Routstr/routstr-core.git
synced 2026-10-05 12:28:22 +00:00
fix: preserve PPQ model identity across catalog aliases
This commit is contained in:
@@ -221,6 +221,7 @@ class PPQAIUpstreamProvider(BaseUpstreamProvider):
|
|||||||
]
|
]
|
||||||
|
|
||||||
models = []
|
models = []
|
||||||
|
seen_ids: set[str] = set()
|
||||||
for model_data in models_data:
|
for model_data in models_data:
|
||||||
try:
|
try:
|
||||||
ppqai_model = PPQAIModel.parse_obj(model_data)
|
ppqai_model = PPQAIModel.parse_obj(model_data)
|
||||||
@@ -259,13 +260,16 @@ class PPQAIUpstreamProvider(BaseUpstreamProvider):
|
|||||||
None,
|
None,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
model_id = or_model.id if or_model else ppqai_model.id
|
||||||
|
if model_id.lower() in seen_ids:
|
||||||
|
continue
|
||||||
|
|
||||||
if or_model:
|
if or_model:
|
||||||
# OpenRouter supplies metadata, not PPQ billing rates.
|
# Keep the stored model identity, but not foreign billing rates.
|
||||||
models.append(
|
models.append(
|
||||||
or_model.copy(
|
or_model.copy(
|
||||||
deep=True,
|
deep=True,
|
||||||
update={
|
update={
|
||||||
"id": ppqai_model.id,
|
|
||||||
"pricing": pricing,
|
"pricing": pricing,
|
||||||
"sats_pricing": None,
|
"sats_pricing": None,
|
||||||
"context_length": ppqai_model.context_length
|
"context_length": ppqai_model.context_length
|
||||||
@@ -291,6 +295,7 @@ class PPQAIUpstreamProvider(BaseUpstreamProvider):
|
|||||||
pricing=pricing,
|
pricing=pricing,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
seen_ids.add(model_id.lower())
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
"Failed to parse PPQ.AI model",
|
"Failed to parse PPQ.AI model",
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ from unittest.mock import AsyncMock, patch
|
|||||||
import httpx
|
import httpx
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
from routstr.algorithm import create_model_mappings
|
||||||
|
from routstr.core.db import ModelRow
|
||||||
from routstr.payment.models import Architecture, Model, Pricing
|
from routstr.payment.models import Architecture, Model, Pricing
|
||||||
from routstr.upstream.ppqai import PPQAIUpstreamProvider
|
from routstr.upstream.ppqai import PPQAIUpstreamProvider
|
||||||
from routstr.upstream.venice import VeniceUpstreamProvider
|
from routstr.upstream.venice import VeniceUpstreamProvider
|
||||||
@@ -75,16 +77,77 @@ async def test_ppq_does_not_inherit_other_provider_cache_or_request_rates() -> N
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_ppq_alias_matches_do_not_share_mutated_prices() -> None:
|
async def test_ppq_alias_matches_emit_first_stable_id_once() -> None:
|
||||||
models = await _fetch(
|
models = await _fetch(
|
||||||
[
|
[
|
||||||
_entry("vendor/model", {"api": {"input_per_1M": 4, "output_per_1M": 8}}),
|
_entry("vendor/model", {"api": {"input_per_1M": 4, "output_per_1M": 8}}),
|
||||||
_entry("model", {"api": {"input_per_1M": 6, "output_per_1M": 9}}),
|
_entry("model", {"api": {"input_per_1M": 6, "output_per_1M": 9}}),
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
assert [m.id for m in models] == ["vendor/model", "model"]
|
assert [m.id for m in models] == ["vendor/model"]
|
||||||
assert [m.pricing.prompt for m in models] == [4e-6, 6e-6]
|
assert models[0].pricing.prompt == 4e-6
|
||||||
assert models[0] is not models[1]
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_ppq_suffix_match_keeps_disabled_model_unroutable() -> None:
|
||||||
|
metadata = _model().copy(
|
||||||
|
update={"id": "openai/gpt-4o", "canonical_slug": "openai/gpt-4o"}
|
||||||
|
)
|
||||||
|
discovered = await _fetch(
|
||||||
|
[_entry("gpt-4o", {"api": {"input_per_1M": 4, "output_per_1M": 8}})],
|
||||||
|
[metadata.dict()],
|
||||||
|
)
|
||||||
|
provider = PPQAIUpstreamProvider("test-only")
|
||||||
|
provider.db_id = 7
|
||||||
|
with patch.object(provider, "get_cached_models", return_value=discovered):
|
||||||
|
_, provider_map, unique_models = create_model_mappings(
|
||||||
|
[provider], {}, {("openai/gpt-4o", 7)}
|
||||||
|
)
|
||||||
|
assert provider_map == {}
|
||||||
|
assert unique_models == {}
|
||||||
|
assert discovered[0].id == metadata.id
|
||||||
|
assert provider.transform_model_name(discovered[0].id) == metadata.id
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_ppq_suffix_match_keeps_override_as_only_candidate() -> None:
|
||||||
|
metadata = _model().copy(
|
||||||
|
update={"id": "openai/gpt-4o", "canonical_slug": "openai/gpt-4o"}
|
||||||
|
)
|
||||||
|
discovered = await _fetch(
|
||||||
|
[_entry("gpt-4o", {"api": {"input_per_1M": 4, "output_per_1M": 8}})],
|
||||||
|
[metadata.dict()],
|
||||||
|
)
|
||||||
|
provider = PPQAIUpstreamProvider("test-only")
|
||||||
|
provider.db_id = 7
|
||||||
|
override = ModelRow(
|
||||||
|
id=metadata.id,
|
||||||
|
name=metadata.name,
|
||||||
|
created=0,
|
||||||
|
description="",
|
||||||
|
context_length=8192,
|
||||||
|
architecture=metadata.architecture.json(),
|
||||||
|
pricing=Pricing(prompt=9e-6, completion=18e-6).json(),
|
||||||
|
enabled=True,
|
||||||
|
upstream_provider_id=7,
|
||||||
|
forwarded_model_id="operator-alias",
|
||||||
|
)
|
||||||
|
with (
|
||||||
|
patch.object(provider, "get_cached_models", return_value=discovered),
|
||||||
|
patch("routstr.payment.models.sats_usd_price", return_value=0.001),
|
||||||
|
):
|
||||||
|
_, provider_map, _ = create_model_mappings(
|
||||||
|
[provider], {(metadata.id, 7): (override, 1.0)}, set()
|
||||||
|
)
|
||||||
|
assert metadata.id in provider_map
|
||||||
|
assert "operator-alias" in provider_map
|
||||||
|
for candidates in provider_map.values():
|
||||||
|
assert len(candidates) == 1
|
||||||
|
candidate, serving = candidates[0]
|
||||||
|
assert serving is provider
|
||||||
|
assert candidate.id == metadata.id
|
||||||
|
assert candidate.forwarded_model_id == "operator-alias"
|
||||||
|
assert candidate.pricing.prompt == 9e-6
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
|
|||||||
Reference in New Issue
Block a user