mirror of
https://github.com/Routstr/routstr-core.git
synced 2026-10-05 12:28:22 +00:00
Merge pull request #747 from Routstr/feat/systemone-typesafe
feat(systemone): add TypeSafe System One decision endpoint
This commit is contained in:
@@ -200,6 +200,84 @@ POST /v1/embeddings
|
||||
}
|
||||
```
|
||||
|
||||
## System One (TypeSafe Decisions)
|
||||
|
||||
### Evaluate State
|
||||
|
||||
Evaluate a state against typed questions (noul / choice / score) on a TypeSafe
|
||||
System One decision model (e.g. `jev-latest`). Requires a `typesafe` upstream
|
||||
provider on the node.
|
||||
|
||||
```http
|
||||
POST /v1/systemone
|
||||
```
|
||||
|
||||
**Request Body:**
|
||||
|
||||
```json
|
||||
{
|
||||
"model": "jev-latest",
|
||||
"state": "Help! My payouts have been failing for 3 days.",
|
||||
"questions": {
|
||||
"is_urgent": {
|
||||
"type": "noul",
|
||||
"instructions": "Does this convey urgency?"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Parameters:**
|
||||
|
||||
| Parameter | Type | Required | Default | Description |
|
||||
|-----------|------|----------|---------|-------------|
|
||||
| `model` | string | Yes | - | TypeSafe alias (`jev-latest`, `jev-preview`) or versioned id (`jev-1.13.0`) |
|
||||
| `state` | string/object/array | Yes | - | Content to evaluate |
|
||||
| `questions` | map<string, Question> | Yes | - | Typed questions; answers keyed identically |
|
||||
|
||||
**Response:**
|
||||
|
||||
```json
|
||||
{
|
||||
"model": "jev-latest",
|
||||
"answers": {
|
||||
"is_urgent": {
|
||||
"type": "noul",
|
||||
"noul": 0.95
|
||||
}
|
||||
},
|
||||
"usage": {
|
||||
"input_tokens": 312,
|
||||
"output_tokens": 48
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Billing is input-token based (output tokens are free on Jev); the response's
|
||||
`usage` is the settlement seam, exactly like embeddings.
|
||||
|
||||
**Notes:**
|
||||
|
||||
- The response `model` echoes the id you requested (e.g. `jev-latest`), not the
|
||||
resolved build (`jev-1.13.0`) TypeSafe returns. Routstr also adds its standard
|
||||
`id`, `cost`, `metadata.routstr` and `usage.*_msats` fields.
|
||||
- TypeSafe's `GET /v1/models` lists aliases only; the node additionally seeds
|
||||
the known versioned ids so they can be requested directly.
|
||||
- TypeSafe answers `429 Too Many Requests` and `529 Overloaded` when throttled.
|
||||
Both are forwarded as upstream errors; retry with exponential backoff.
|
||||
|
||||
**Enabling the provider:**
|
||||
|
||||
1. Admin UI → Providers → *TypeSafe* (base URL is fixed to
|
||||
`https://api.typesafe.ai/v1`), paste your `api.typesafe.ai` key. Or
|
||||
`POST /admin/api/upstream-providers` with
|
||||
`{"provider_type": "typesafe", "api_key": "<key>"}`.
|
||||
2. On a node with an empty provider table, setting `TYPESAFE_API_KEY` seeds
|
||||
the provider automatically.
|
||||
3. `jev-latest`, `jev-preview` and `jev-1.13.0` are catalogued with the
|
||||
published rate ($0.042 per million input tokens, output free). Override the
|
||||
model row if TypeSafe changes pricing.
|
||||
|
||||
## Images (Coming Soon)
|
||||
|
||||
### Create Image
|
||||
|
||||
@@ -104,6 +104,7 @@ Standard OpenAI-compatible endpoints:
|
||||
- **Responses**: `/v1/responses`
|
||||
- **Chat Completions**: `/v1/chat/completions`
|
||||
- **Embeddings**: `/v1/embeddings`
|
||||
- **System One**: `/v1/systemone` (TypeSafe decision models)
|
||||
- **Completions**: `/v1/completions` *(planned)*
|
||||
- **Images**: `/v1/images/generations` *(planned)*
|
||||
- **Audio**: `/v1/audio/transcriptions` *(planned)*
|
||||
|
||||
@@ -262,6 +262,10 @@ _ALLOWED_ENDPOINTS: dict[str, frozenset[str]] = {
|
||||
"responses": frozenset({"POST"}),
|
||||
"messages": frozenset({"POST"}),
|
||||
"embeddings": frozenset({"POST"}),
|
||||
# TypeSafe System One decision endpoint: POST {state, model, questions}
|
||||
# -> {answers, usage}. Non-streaming, JSON in/out; billed from the
|
||||
# response's usage exactly like embeddings.
|
||||
"systemone": frozenset({"POST"}),
|
||||
"models": frozenset({"GET"}),
|
||||
"attestation": frozenset({"GET"}),
|
||||
"tee/attestation": frozenset({"GET"}),
|
||||
|
||||
@@ -12,6 +12,7 @@ from .perplexity import PerplexityUpstreamProvider
|
||||
from .ppqai import PPQAIUpstreamProvider
|
||||
from .routstr import RoutstrUpstreamProvider
|
||||
from .tinfoil import TinfoilUpstreamProvider
|
||||
from .typesafe import TypeSafeUpstreamProvider
|
||||
from .xai import XAIUpstreamProvider
|
||||
|
||||
upstream_provider_classes: list[type[BaseUpstreamProvider]] = [
|
||||
@@ -28,6 +29,7 @@ upstream_provider_classes: list[type[BaseUpstreamProvider]] = [
|
||||
PPQAIUpstreamProvider,
|
||||
RoutstrUpstreamProvider,
|
||||
TinfoilUpstreamProvider,
|
||||
TypeSafeUpstreamProvider,
|
||||
XAIUpstreamProvider,
|
||||
]
|
||||
"""List of all upstream classes"""
|
||||
|
||||
@@ -303,7 +303,7 @@ def _openai_completion_path(path: str) -> str | None:
|
||||
def _x_cashu_path_has_settlement_handler(path: str) -> bool:
|
||||
canonical = path.rstrip("/")
|
||||
return _openai_completion_path(canonical) is not None or canonical.endswith(
|
||||
("embeddings", "messages", "messages/count_tokens")
|
||||
("embeddings", "messages", "messages/count_tokens", "systemone")
|
||||
)
|
||||
|
||||
|
||||
@@ -3155,6 +3155,7 @@ class BaseUpstreamProvider:
|
||||
or path.endswith("embeddings")
|
||||
or path.endswith("messages")
|
||||
or path.endswith("messages/count_tokens")
|
||||
or path.endswith("systemone")
|
||||
):
|
||||
if path.endswith("messages"):
|
||||
client_wants_streaming = False
|
||||
|
||||
@@ -273,6 +273,7 @@ async def _seed_providers_from_settings(
|
||||
("FIREWORKS_API_KEY", "fireworks", None, None),
|
||||
("XAI_API_KEY", "xai", None, None),
|
||||
("TINFOIL_API_KEY", "tinfoil", None, None),
|
||||
("TYPESAFE_API_KEY", "typesafe", None, None),
|
||||
]
|
||||
|
||||
for env_key, provider_type, _, _ in env_mappings:
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
"""Upstream provider for the TypeSafe System One API.
|
||||
|
||||
``POST /v1/systemone`` takes ``{state, model, questions}`` and returns
|
||||
``{model, answers, usage}``. The ``usage`` shape (``input_tokens`` /
|
||||
``output_tokens``) is what :func:`routstr.payment.usage.normalize_usage`
|
||||
already parses, so billing needs no dialect handling. This provider only
|
||||
assembles the catalog: ``GET /v1/models`` lists names without prices.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
import httpx
|
||||
|
||||
from ..core.logging import get_logger
|
||||
from ..payment.models import Architecture, Model, Pricing, TopProvider
|
||||
from .base import BaseUpstreamProvider
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..core.db import UpstreamProviderRow
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
# USD per token (https://docs.typesafe.ai/models). Output is free.
|
||||
_INPUT_RATE_USD = 0.042 / 1_000_000
|
||||
_OUTPUT_RATE_USD = 0.0
|
||||
|
||||
# The listing returns aliases only; versioned ids are accepted but unlisted.
|
||||
_VERSIONED_MODEL_IDS = ("jev-1.13.0",)
|
||||
|
||||
_CONTEXT_LENGTH = 64_000
|
||||
_MODELS_TIMEOUT_SECONDS = 30.0
|
||||
|
||||
|
||||
def _parse_release_date(value: object) -> int:
|
||||
if not isinstance(value, str) or not value:
|
||||
return 0
|
||||
try:
|
||||
return int(datetime.fromisoformat(value.replace("Z", "+00:00")).timestamp())
|
||||
except ValueError:
|
||||
return 0
|
||||
|
||||
|
||||
def _build_model(name: str, entry: dict[str, Any] | None = None) -> Model:
|
||||
entry = entry or {}
|
||||
description = entry.get("description")
|
||||
if not isinstance(description, str) or not description:
|
||||
description = f"TypeSafe System One model {name}"
|
||||
|
||||
return Model(
|
||||
id=name,
|
||||
name=name,
|
||||
created=_parse_release_date(entry.get("release_date")),
|
||||
description=description,
|
||||
context_length=_CONTEXT_LENGTH,
|
||||
architecture=Architecture(
|
||||
modality="text->decisions",
|
||||
input_modalities=["text"],
|
||||
output_modalities=["decisions"],
|
||||
tokenizer="Other",
|
||||
instruct_type=None,
|
||||
),
|
||||
pricing=Pricing(prompt=_INPUT_RATE_USD, completion=_OUTPUT_RATE_USD),
|
||||
top_provider=TopProvider(context_length=_CONTEXT_LENGTH),
|
||||
)
|
||||
|
||||
|
||||
def _models_from_listing(data: object) -> list[Model]:
|
||||
entries = data.get("models", []) if isinstance(data, dict) else data
|
||||
if not isinstance(entries, list):
|
||||
entries = []
|
||||
|
||||
models: dict[str, Model] = {}
|
||||
for entry in entries:
|
||||
name = entry.get("name") if isinstance(entry, dict) else None
|
||||
if isinstance(name, str) and name and name not in models:
|
||||
models[name] = _build_model(name, entry)
|
||||
|
||||
for name in _VERSIONED_MODEL_IDS:
|
||||
if name not in models:
|
||||
models[name] = _build_model(name)
|
||||
return list(models.values())
|
||||
|
||||
|
||||
class TypeSafeUpstreamProvider(BaseUpstreamProvider):
|
||||
"""Upstream provider for the TypeSafe System One decision API."""
|
||||
|
||||
provider_type = "typesafe"
|
||||
default_base_url = "https://api.typesafe.ai/v1"
|
||||
platform_url = "https://docs.typesafe.ai"
|
||||
|
||||
def __init__(self, api_key: str, provider_fee: float = 1.0):
|
||||
super().__init__(
|
||||
base_url=self.default_base_url,
|
||||
api_key=api_key,
|
||||
provider_fee=provider_fee,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def _build_from_row(
|
||||
cls, provider_row: "UpstreamProviderRow"
|
||||
) -> "TypeSafeUpstreamProvider":
|
||||
return cls(
|
||||
api_key=provider_row.api_key,
|
||||
provider_fee=provider_row.provider_fee,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def get_provider_metadata(cls) -> dict[str, object]:
|
||||
return {
|
||||
"id": cls.provider_type,
|
||||
"name": "TypeSafe",
|
||||
"default_base_url": cls.default_base_url,
|
||||
"fixed_base_url": True,
|
||||
"platform_url": cls.platform_url,
|
||||
"can_create_account": False,
|
||||
"can_topup": False,
|
||||
"can_show_balance": False,
|
||||
}
|
||||
|
||||
def transform_model_name(self, model_id: str) -> str:
|
||||
return model_id.removeprefix("typesafe/")
|
||||
|
||||
async def fetch_models(self) -> list[Model]:
|
||||
"""Fetch the catalog; return an empty list on failure so init never breaks."""
|
||||
url = f"{self.base_url}/models"
|
||||
headers = {"Authorization": f"Bearer {self.api_key}"}
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=_MODELS_TIMEOUT_SECONDS) as client:
|
||||
response = await client.get(url, headers=headers)
|
||||
response.raise_for_status()
|
||||
data = response.json()
|
||||
except Exception as exc:
|
||||
logger.warning(
|
||||
"Failed to fetch TypeSafe model catalog",
|
||||
extra={"url": url, "error": str(exc)},
|
||||
exc_info=True,
|
||||
)
|
||||
return []
|
||||
|
||||
return _models_from_listing(data)
|
||||
@@ -0,0 +1,221 @@
|
||||
"""Integration test: POST /v1/systemone proxied and billed end-to-end.
|
||||
|
||||
The TypeSafe decision endpoint shares the request plumbing with embeddings:
|
||||
model id in the top-level ``model`` field, flat ``usage`` in the response.
|
||||
This test pins the whole path — allowlist, auth, forwarding, and settlement
|
||||
billed from the response's usage — with only the network hop mocked.
|
||||
"""
|
||||
|
||||
import json
|
||||
from typing import Any, AsyncGenerator
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
import httpx
|
||||
import pytest
|
||||
from httpx import AsyncClient
|
||||
from sqlmodel.ext.asyncio.session import AsyncSession
|
||||
|
||||
from routstr.payment.models import Architecture, Model, Pricing
|
||||
from routstr.proxy import refresh_model_maps
|
||||
from routstr.upstream.base import BaseUpstreamProvider
|
||||
|
||||
TYPESAFE_BASE_URL = "https://api.typesafe.ai/v1"
|
||||
|
||||
SYSTEMONE_REQUEST = {
|
||||
"model": "jev-latest",
|
||||
"state": "Help! My payouts have been failing for 3 days.",
|
||||
"questions": {
|
||||
"is_urgent": {"type": "noul", "instructions": "Does this convey urgency?"}
|
||||
},
|
||||
}
|
||||
|
||||
SYSTEMONE_RESPONSE = {
|
||||
"model": "jev-latest",
|
||||
"answers": {
|
||||
"is_urgent": {"type": "noul", "noul": 0.95},
|
||||
},
|
||||
"usage": {"input_tokens": 1000, "output_tokens": 200},
|
||||
}
|
||||
|
||||
|
||||
class _StaticTypeSafeProvider(BaseUpstreamProvider):
|
||||
"""Upstream provider with a fixed TypeSafe model catalog."""
|
||||
|
||||
def __init__(self, base_url: str, api_key: str, fee: float, model: Model) -> None:
|
||||
super().__init__(base_url, api_key, fee)
|
||||
self.provider_type = "typesafe"
|
||||
self._static_model = model
|
||||
|
||||
def get_cached_models(self) -> list[Model]:
|
||||
return [self._static_model]
|
||||
|
||||
async def refresh_models_cache(self) -> None:
|
||||
pass
|
||||
|
||||
|
||||
def _jev_model(prompt_sats: float = 0.001, completion_sats: float = 0.0) -> Model:
|
||||
return Model(
|
||||
id="jev-latest",
|
||||
name="jev-latest",
|
||||
created=1,
|
||||
description="TypeSafe System One decision model",
|
||||
context_length=64_000,
|
||||
architecture=Architecture(
|
||||
modality="text->decisions",
|
||||
input_modalities=["text"],
|
||||
output_modalities=["decisions"],
|
||||
tokenizer="Other",
|
||||
instruct_type=None,
|
||||
),
|
||||
pricing=Pricing(
|
||||
prompt=prompt_sats, completion=completion_sats, max_cost=50.0
|
||||
),
|
||||
sats_pricing=Pricing(
|
||||
prompt=prompt_sats, completion=completion_sats, max_cost=50.0
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def typesafe_provider_maps(
|
||||
patched_db_engine: None,
|
||||
) -> AsyncGenerator[_StaticTypeSafeProvider, None]:
|
||||
"""Install a TypeSafe provider with a priced jev-latest model."""
|
||||
provider = _StaticTypeSafeProvider(
|
||||
TYPESAFE_BASE_URL,
|
||||
"key-typesafe",
|
||||
1.0,
|
||||
_jev_model(),
|
||||
)
|
||||
|
||||
from routstr import proxy
|
||||
|
||||
original_upstreams = proxy.get_upstreams()
|
||||
with patch("routstr.proxy._upstreams", [provider]):
|
||||
await refresh_model_maps()
|
||||
yield provider
|
||||
with patch("routstr.proxy._upstreams", original_upstreams):
|
||||
await refresh_model_maps()
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
@pytest.mark.asyncio
|
||||
async def test_systemone_forwarded_and_billed_from_usage(
|
||||
authenticated_client: AsyncClient,
|
||||
typesafe_provider_maps: _StaticTypeSafeProvider,
|
||||
integration_session: AsyncSession,
|
||||
) -> None:
|
||||
"""A systemone request reaches api.typesafe.ai and bills input tokens."""
|
||||
sent_requests: list[httpx.Request] = []
|
||||
|
||||
async def fake_transport(
|
||||
request: httpx.Request, *args: Any, **kwargs: Any
|
||||
) -> httpx.Response:
|
||||
sent_requests.append(request)
|
||||
return httpx.Response(
|
||||
200,
|
||||
content=json.dumps(SYSTEMONE_RESPONSE).encode(),
|
||||
headers={"content-type": "application/json"},
|
||||
)
|
||||
|
||||
with (
|
||||
patch(
|
||||
"httpx.AsyncHTTPTransport.handle_async_request",
|
||||
side_effect=fake_transport,
|
||||
),
|
||||
patch(
|
||||
"routstr.payment.cost_calculation.sats_usd_price",
|
||||
return_value=0.0005,
|
||||
),
|
||||
):
|
||||
response = await authenticated_client.post(
|
||||
"/v1/systemone",
|
||||
json=SYSTEMONE_REQUEST,
|
||||
)
|
||||
|
||||
assert response.status_code == 200, response.text
|
||||
payload = response.json()
|
||||
|
||||
# The answers pass through untouched.
|
||||
assert payload["answers"]["is_urgent"]["noul"] == pytest.approx(0.95)
|
||||
|
||||
# Exactly one upstream hop, aimed at TypeSafe's endpoint with the
|
||||
# provider's model spelling.
|
||||
assert len(sent_requests) == 1
|
||||
sent = sent_requests[0]
|
||||
assert str(sent.url) == "https://api.typesafe.ai/v1/systemone"
|
||||
assert json.loads(sent.content)["model"] == "jev-latest"
|
||||
|
||||
# Billed from usage: 1000 input tokens at 0.001 sats/token = 1000 msats;
|
||||
# 200 output tokens at 0 sats = 0. Settled cost is exposed on the body.
|
||||
assert payload["cost"]["input_msats"] == 1000
|
||||
assert payload["cost"]["output_msats"] == 0
|
||||
assert payload["cost"]["total_msats"] == 1000
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
@pytest.mark.asyncio
|
||||
async def test_systemone_with_x_cashu_settles(
|
||||
authenticated_client: AsyncClient,
|
||||
typesafe_provider_maps: _StaticTypeSafeProvider,
|
||||
testmint_wallet: Any,
|
||||
) -> None:
|
||||
"""The X-Cashu settlement gate admits systemone and refunds the delta."""
|
||||
token = await testmint_wallet.mint_tokens(10_000)
|
||||
|
||||
async def fake_transport(
|
||||
request: httpx.Request, *args: Any, **kwargs: Any
|
||||
) -> httpx.Response:
|
||||
return httpx.Response(
|
||||
200,
|
||||
content=json.dumps(SYSTEMONE_RESPONSE).encode(),
|
||||
headers={"content-type": "application/json"},
|
||||
)
|
||||
|
||||
# The redemption and refund helpers are bound into base.py at import time,
|
||||
# so the app fixture's wallet patches do not reach the proxy's x-cashu path.
|
||||
with (
|
||||
patch(
|
||||
"httpx.AsyncHTTPTransport.handle_async_request",
|
||||
side_effect=fake_transport,
|
||||
),
|
||||
patch(
|
||||
"routstr.upstream.base.recieve_token",
|
||||
AsyncMock(return_value=(10_000, "sat", "https://mint.test")),
|
||||
),
|
||||
patch(
|
||||
"routstr.upstream.base.send_token",
|
||||
AsyncMock(return_value="cashuBrefundtoken"),
|
||||
),
|
||||
# send_refund derives the mint from the token it just created; the
|
||||
# fake token has no mint to parse.
|
||||
patch(
|
||||
"routstr.upstream.base.token_mint_url",
|
||||
return_value="https://mint.test",
|
||||
),
|
||||
# cost_calculation binds sats_usd_price at import time, so the price
|
||||
# patch in the app fixture does not reach it.
|
||||
patch(
|
||||
"routstr.payment.cost_calculation.sats_usd_price",
|
||||
return_value=0.0005,
|
||||
),
|
||||
# Mint trust is node state that other suites mutate; this test is
|
||||
# about the endpoint gate, not mint policy.
|
||||
patch(
|
||||
"routstr.payment.helpers.is_trusted_source_mint",
|
||||
return_value=True,
|
||||
),
|
||||
):
|
||||
response = await authenticated_client.post(
|
||||
"/v1/systemone",
|
||||
json=SYSTEMONE_REQUEST,
|
||||
headers={"X-Cashu": token},
|
||||
)
|
||||
|
||||
# Not rejected as an unsupported endpoint (that returns 400 with
|
||||
# x_cashu_unsupported_endpoint), and settled rather than streamed raw.
|
||||
assert response.status_code == 200, response.text
|
||||
payload = response.json()
|
||||
assert payload["answers"]["is_urgent"]["type"] == "noul"
|
||||
# A refund header exists when the token exceeded the settled cost.
|
||||
assert response.headers.get("X-Cashu") is not None
|
||||
@@ -0,0 +1,178 @@
|
||||
"""Unit tests for the TypeSafe System One provider integration.
|
||||
|
||||
Covers the three integration seams the systemone endpoint touches:
|
||||
|
||||
* the proxy path allowlist admits ``systemone`` (POST only) and nothing that
|
||||
merely looks like it;
|
||||
* the X-Cashu settlement gate treats ``systemone`` as a settleable endpoint;
|
||||
* the provider maps TypeSafe's pricing-less model listing onto priced
|
||||
``Model`` objects with usable rates.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from typing import Any
|
||||
|
||||
os.environ.setdefault("UPSTREAM_BASE_URL", "http://test")
|
||||
os.environ.setdefault("UPSTREAM_API_KEY", "test")
|
||||
|
||||
from unittest.mock import AsyncMock, MagicMock, patch # noqa: E402
|
||||
|
||||
import pytest # noqa: E402
|
||||
|
||||
from routstr.proxy import _forwarding_allowed # noqa: E402
|
||||
from routstr.upstream.base import _x_cashu_path_has_settlement_handler # noqa: E402
|
||||
from routstr.upstream.typesafe import TypeSafeUpstreamProvider # noqa: E402
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("path", "method"),
|
||||
[
|
||||
("v1/systemone", "POST"),
|
||||
("systemone", "POST"),
|
||||
("v1/systemone/", "POST"),
|
||||
],
|
||||
)
|
||||
def test_systemone_is_forwarded(path: str, method: str) -> None:
|
||||
assert _forwarding_allowed(path, method) is True
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("path", "method"),
|
||||
[
|
||||
("v1/systemone", "GET"), # billed endpoint is POST-only
|
||||
("v1/systemone", "DELETE"),
|
||||
("systemonedump", "POST"), # longer segment must not match
|
||||
("v1/systemone/secret", "POST"), # trailing id segment widens nothing
|
||||
("v1/systemone/../admin", "POST"), # traversal spelling is screened
|
||||
],
|
||||
)
|
||||
def test_systemone_lookalikes_are_refused(path: str, method: str) -> None:
|
||||
assert _forwarding_allowed(path, method) is False
|
||||
|
||||
|
||||
def test_systemone_has_x_cashu_settlement_handler() -> None:
|
||||
assert _x_cashu_path_has_settlement_handler("v1/systemone") is True
|
||||
assert _x_cashu_path_has_settlement_handler("systemone/") is True
|
||||
|
||||
|
||||
def test_provider_metadata_is_complete() -> None:
|
||||
meta = TypeSafeUpstreamProvider.get_provider_metadata()
|
||||
assert meta["id"] == "typesafe"
|
||||
assert meta["default_base_url"] == "https://api.typesafe.ai/v1"
|
||||
assert meta["fixed_base_url"] is True
|
||||
|
||||
|
||||
def test_provider_transform_model_name() -> None:
|
||||
provider = TypeSafeUpstreamProvider(api_key="test")
|
||||
assert provider.transform_model_name("typesafe/jev-latest") == "jev-latest"
|
||||
assert provider.transform_model_name("jev-latest") == "jev-latest"
|
||||
|
||||
|
||||
def _mock_models_response(payload: dict) -> MagicMock:
|
||||
mock_response = MagicMock()
|
||||
mock_response.status_code = 200
|
||||
mock_response.raise_for_status = MagicMock()
|
||||
mock_response.json.return_value = payload
|
||||
return mock_response
|
||||
|
||||
|
||||
def _patch_client(mock_response: MagicMock) -> Any:
|
||||
mock_client = MagicMock()
|
||||
mock_client.__aenter__ = AsyncMock(return_value=mock_client)
|
||||
mock_client.__aexit__ = AsyncMock(return_value=None)
|
||||
mock_client.get = AsyncMock(return_value=mock_response)
|
||||
return patch(
|
||||
"routstr.upstream.typesafe.httpx.AsyncClient",
|
||||
return_value=mock_client,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_fetch_models_prices_the_listing() -> None:
|
||||
provider = TypeSafeUpstreamProvider(api_key="test")
|
||||
payload = {
|
||||
"models": [
|
||||
{
|
||||
"name": "jev-latest",
|
||||
"description": "The most recent stable release.",
|
||||
"release_date": "2026-09-17",
|
||||
},
|
||||
{
|
||||
"name": "jev-preview",
|
||||
"description": "The most recent release.",
|
||||
"release_date": "2026-09-17",
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
with _patch_client(_mock_models_response(payload)):
|
||||
models = await provider.fetch_models()
|
||||
|
||||
assert [m.id for m in models] == ["jev-latest", "jev-preview", "jev-1.13.0"]
|
||||
for model in models:
|
||||
# Input priced, output free; rates usable (zero is a real price).
|
||||
assert model.pricing.prompt == pytest.approx(0.042 / 1_000_000)
|
||||
assert model.pricing.completion == 0.0
|
||||
assert model.context_length == 64_000
|
||||
assert model.architecture.input_modalities == ["text"]
|
||||
assert model.architecture.output_modalities == ["decisions"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_fetch_models_handles_error() -> None:
|
||||
provider = TypeSafeUpstreamProvider(api_key="test")
|
||||
mock_response = MagicMock()
|
||||
mock_response.raise_for_status = MagicMock(
|
||||
side_effect=RuntimeError("upstream down")
|
||||
)
|
||||
|
||||
with (
|
||||
_patch_client(mock_response),
|
||||
patch("routstr.upstream.typesafe.logger") as mock_logger,
|
||||
):
|
||||
models = await provider.fetch_models()
|
||||
|
||||
assert models == []
|
||||
mock_logger.warning.assert_called_once()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_fetch_models_defaults_unknown_model_rates() -> None:
|
||||
"""A newly listed model still gets a usable default rate."""
|
||||
provider = TypeSafeUpstreamProvider(api_key="test")
|
||||
payload = {
|
||||
"models": [
|
||||
{"name": "jev-2.0", "description": "Future model", "release_date": None}
|
||||
]
|
||||
}
|
||||
|
||||
with _patch_client(_mock_models_response(payload)):
|
||||
models = await provider.fetch_models()
|
||||
|
||||
by_id = {m.id: m for m in models}
|
||||
assert "jev-2.0" in by_id
|
||||
assert by_id["jev-2.0"].pricing.prompt == pytest.approx(0.042 / 1_000_000)
|
||||
assert by_id["jev-2.0"].created == 0
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_fetch_models_does_not_duplicate_listed_versioned_id() -> None:
|
||||
provider = TypeSafeUpstreamProvider(api_key="test")
|
||||
payload = {
|
||||
"models": [
|
||||
{
|
||||
"name": "jev-1.13.0",
|
||||
"description": "Jev 1.13",
|
||||
"release_date": "2026-09-17T00:00:00Z",
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
with _patch_client(_mock_models_response(payload)):
|
||||
models = await provider.fetch_models()
|
||||
|
||||
assert [m.id for m in models] == ["jev-1.13.0"]
|
||||
assert models[0].description == "Jev 1.13"
|
||||
assert models[0].created > 0
|
||||
Reference in New Issue
Block a user