feat: add venice upstream provider

This commit is contained in:
9qeklajc
2026-09-25 00:19:17 +02:00
parent 74881f6d59
commit f77896f19a
3 changed files with 417 additions and 0 deletions
+2
View File
@@ -13,6 +13,7 @@ from .ppqai import PPQAIUpstreamProvider
from .routstr import RoutstrUpstreamProvider
from .tinfoil import TinfoilUpstreamProvider
from .typesafe import TypeSafeUpstreamProvider
from .venice import VeniceUpstreamProvider
from .xai import XAIUpstreamProvider
upstream_provider_classes: list[type[BaseUpstreamProvider]] = [
@@ -30,6 +31,7 @@ upstream_provider_classes: list[type[BaseUpstreamProvider]] = [
RoutstrUpstreamProvider,
TinfoilUpstreamProvider,
TypeSafeUpstreamProvider,
VeniceUpstreamProvider,
XAIUpstreamProvider,
]
"""List of all upstream classes"""
+194
View File
@@ -0,0 +1,194 @@
from __future__ import annotations
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__)
# ``GET /models`` defaults to ``type=text``, which is why a Venice account
# configured as a generic upstream never sees the rest of its catalog.
_MODELS_TYPE_PARAM = "all"
# Families this proxy can both route and price. Image, audio, music and video
# are billed per clip or per second and return no usage object to settle
# against, so exposing them would hand out unpriced inference.
_SUPPORTED_TYPES = frozenset({"text", "embedding"})
# Venice prices text in USD per million tokens; Routstr prices per token.
_USD_PER_MILLION = 1_000_000.0
_ARCHITECTURES: dict[str, tuple[str, list[str], list[str]]] = {
"text": ("text->text", ["text"], ["text"]),
"embedding": ("text->embedding", ["text"], ["embedding"]),
}
def _usd(entry: Any) -> float | None:
"""Read the USD leg of a Venice ``{usd, diem}`` price pair."""
if isinstance(entry, dict):
value = entry.get("usd")
if isinstance(value, (int, float)) and not isinstance(value, bool):
return float(value)
return None
class VeniceUpstreamProvider(BaseUpstreamProvider):
"""Upstream provider for the Venice.ai API.
Venice publishes a complete price book on its own catalog, so models are
built from that rather than matched against OpenRouter, which has never
heard of most of Venice's catalog.
"""
provider_type = "venice"
default_base_url = "https://api.venice.ai/api/v1"
platform_url = "https://venice.ai/settings/api"
def __init__(self, api_key: str, provider_fee: float = 1.01):
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"
) -> "VeniceUpstreamProvider":
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": "Venice AI",
"default_base_url": cls.default_base_url,
"fixed_base_url": True,
"platform_url": cls.platform_url,
}
def transform_model_name(self, model_id: str) -> str:
return model_id.removeprefix("venice/")
async def _fetch_provider_models(self) -> dict:
url = f"{self.base_url.rstrip('/')}/models"
headers = {"Authorization": f"Bearer {self.api_key}"} if self.api_key else None
async with httpx.AsyncClient(timeout=30.0) as client:
response = await client.get(
url, params={"type": _MODELS_TYPE_PARAM}, headers=headers
)
response.raise_for_status()
return response.json()
async def fetch_models(self) -> list[Model]:
try:
payload = await self._fetch_provider_models()
except Exception as e:
logger.error(
"Error fetching Venice models",
extra={"error": str(e), "error_type": type(e).__name__},
)
return []
models: list[Model] = []
skipped: list[str] = []
for entry in payload.get("data", []):
if not isinstance(entry, dict):
continue
try:
model = self._parse_model(entry)
except Exception as e:
logger.warning(
"Failed to parse Venice model",
extra={
"model_id": entry.get("id", "unknown"),
"error": str(e),
"error_type": type(e).__name__,
},
)
continue
if model is None:
skipped.append(str(entry.get("id", "unknown")))
continue
models.append(model)
if skipped:
logger.debug(
f"({len(skipped)}) Venice models skipped as unsupported or unpriced",
extra={"skipped_models": skipped},
)
return models
def _parse_model(self, entry: dict[str, Any]) -> Model | None:
model_type = entry.get("type")
model_id = entry.get("id")
spec = entry.get("model_spec")
if not model_id or model_type not in _SUPPORTED_TYPES:
return None
if not isinstance(spec, dict) or spec.get("offline"):
return None
pricing = self._parse_pricing(spec.get("pricing"))
if pricing is None:
return None
modality, input_modalities, output_modalities = _ARCHITECTURES[str(model_type)]
capabilities = spec.get("capabilities")
if (
model_type == "text"
and isinstance(capabilities, dict)
and capabilities.get("supportsVision")
):
input_modalities = [*input_modalities, "image"]
modality = "text+image->text"
context_length = spec.get("availableContextTokens")
max_completion_tokens = spec.get("maxCompletionTokens")
name = spec.get("name") or str(model_id)
return Model(
id=str(model_id),
name=str(name),
created=int(entry.get("created") or 0),
description=str(spec.get("description") or f"Venice {model_type} model"),
context_length=int(context_length) if context_length else 0,
architecture=Architecture(
modality=modality,
input_modalities=input_modalities,
output_modalities=output_modalities,
tokenizer="Unknown",
instruct_type=None,
),
pricing=pricing,
top_provider=TopProvider(
context_length=int(context_length) if context_length else None,
max_completion_tokens=int(max_completion_tokens)
if max_completion_tokens
else None,
),
)
def _parse_pricing(self, raw: Any) -> Pricing | None:
if not isinstance(raw, dict):
return None
# The ``extended`` tier some models charge past a context threshold is
# ignored: billing it would overcharge every request staying under it.
input_usd = _usd(raw.get("input"))
if input_usd is None:
return None
return Pricing(
prompt=input_usd / _USD_PER_MILLION,
completion=(_usd(raw.get("output")) or 0.0) / _USD_PER_MILLION,
input_cache_read=(_usd(raw.get("cache_input")) or 0.0) / _USD_PER_MILLION,
input_cache_write=(_usd(raw.get("cache_write")) or 0.0) / _USD_PER_MILLION,
)
+221
View File
@@ -0,0 +1,221 @@
"""Unit tests for ``VeniceUpstreamProvider.fetch_models``.
Venice answers ``/models`` with only its text catalog unless ``type`` is
passed, which is why the same account configured as a generic upstream sees a
different catalog. These tests pin that query parameter, the per-token pricing
shape, and the families dropped as unpriceable.
"""
from __future__ import annotations
from typing import Any
from unittest.mock import patch
import pytest
from routstr.upstream.venice import VeniceUpstreamProvider
class _FakeResponse:
def __init__(self, payload: dict[str, Any]) -> None:
self._payload = payload
def raise_for_status(self) -> None:
return None
def json(self) -> dict[str, Any]:
return self._payload
class _FakeAsyncClient:
def __init__(self, payload: dict[str, Any], calls: list[dict[str, Any]]) -> None:
self._payload = payload
self._calls = calls
async def __aenter__(self) -> "_FakeAsyncClient":
return self
async def __aexit__(self, *_: object) -> None:
return None
async def get(
self,
url: str,
params: dict[str, Any] | None = None,
headers: dict[str, str] | None = None,
) -> _FakeResponse:
self._calls.append({"url": url, "params": params, "headers": headers})
return _FakeResponse(self._payload)
CATALOG: dict[str, Any] = {
"object": "list",
"data": [
{
"id": "venice-uncensored-1-2",
"type": "text",
"created": 1727966436,
"model_spec": {
"name": "Venice Uncensored 1.2",
"availableContextTokens": 128000,
"maxCompletionTokens": 8192,
"capabilities": {"supportsVision": True},
"pricing": {
"input": {"usd": 0.2, "diem": 0.2},
"output": {"usd": 0.9, "diem": 0.9},
"cache_input": {"usd": 0.02, "diem": 0.02},
"cache_write": {"usd": 0.25, "diem": 0.25},
},
},
},
{
"id": "text-embedding-bge-m3",
"type": "embedding",
"created": 1727966436,
"model_spec": {
"name": "BGE m3",
"availableContextTokens": 8192,
"pricing": {"input": {"usd": 0.01, "diem": 0.01}},
},
},
{
"id": "unpriced-text",
"type": "text",
"created": 1727966436,
"model_spec": {"name": "Unpriced", "pricing": {}},
},
{
"id": "offline-model",
"type": "text",
"created": 1727966436,
"model_spec": {
"name": "Offline",
"offline": True,
"pricing": {"input": {"usd": 0.2, "diem": 0.2}},
},
},
{
"id": "venice-sd35",
"type": "image",
"created": 1727966436,
"model_spec": {
"name": "Venice SD35",
"pricing": {"generation": {"usd": 0.01, "diem": 0.01}},
},
},
{
"id": "flux-2-max-edit",
"type": "inpaint",
"created": 1727966436,
"model_spec": {
"name": "FLUX.2 Max Edit",
"pricing": {"inpaint": {"usd": 0.12, "diem": 0.12}},
},
},
{
"id": "tts-kokoro",
"type": "tts",
"created": 1727966436,
"model_spec": {
"name": "Kokoro",
"pricing": {"input": {"usd": 3.5, "diem": 3.5}},
},
},
{
"id": "unpriced-video",
"type": "video",
"created": 1727966436,
"model_spec": {"name": "Video"},
},
],
}
def _fetch(payload: dict[str, Any] = CATALOG) -> tuple[list[Any], list[dict[str, Any]]]:
import asyncio
calls: list[dict[str, Any]] = []
provider = VeniceUpstreamProvider(api_key="sk-test")
with patch(
"routstr.upstream.venice.httpx.AsyncClient",
lambda *a, **kw: _FakeAsyncClient(payload, calls),
):
models = asyncio.run(provider.fetch_models())
return models, calls
def test_requests_every_model_family() -> None:
_, calls = _fetch()
assert calls[0]["params"] == {"type": "all"}
assert calls[0]["url"] == "https://api.venice.ai/api/v1/models"
assert calls[0]["headers"] == {"Authorization": "Bearer sk-test"}
def test_text_pricing_is_per_token() -> None:
models, _ = _fetch()
model = next(m for m in models if m.id == "venice-uncensored-1-2")
assert model.pricing.prompt == pytest.approx(0.2 / 1_000_000)
assert model.pricing.completion == pytest.approx(0.9 / 1_000_000)
assert model.pricing.input_cache_read == pytest.approx(0.02 / 1_000_000)
assert model.pricing.input_cache_write == pytest.approx(0.25 / 1_000_000)
assert model.context_length == 128000
assert model.top_provider is not None
assert model.top_provider.max_completion_tokens == 8192
assert model.architecture.input_modalities == ["text", "image"]
assert model.architecture.modality == "text+image->text"
def test_embedding_models_are_listed() -> None:
models, _ = _fetch()
model = next(m for m in models if m.id == "text-embedding-bge-m3")
assert model.architecture.output_modalities == ["embedding"]
assert model.pricing.prompt == pytest.approx(0.01 / 1_000_000)
assert model.pricing.completion == 0.0
def test_families_billed_per_clip_are_dropped() -> None:
"""Image, audio and video return no usage to settle against, so listing
them here would hand out inference this provider cannot price."""
models, _ = _fetch()
ids = {m.id for m in models}
assert "venice-sd35" not in ids
assert "flux-2-max-edit" not in ids
assert "tts-kokoro" not in ids
assert "unpriced-video" not in ids
def test_offline_and_unpriced_models_are_dropped() -> None:
models, _ = _fetch()
ids = {m.id for m in models}
assert "offline-model" not in ids
assert "unpriced-text" not in ids
def test_model_name_drops_the_venice_prefix() -> None:
provider = VeniceUpstreamProvider(api_key="sk-test")
assert provider.transform_model_name("venice/venice-uncensored-1-2") == (
"venice-uncensored-1-2"
)
assert provider.transform_model_name("venice-uncensored-1-2") == (
"venice-uncensored-1-2"
)
def test_provider_metadata_pins_the_base_url() -> None:
metadata = VeniceUpstreamProvider.get_provider_metadata()
assert metadata["id"] == "venice"
assert metadata["default_base_url"] == "https://api.venice.ai/api/v1"
assert metadata["fixed_base_url"] is True
def test_fetch_returns_empty_on_upstream_failure() -> None:
provider = VeniceUpstreamProvider(api_key="sk-test")
with patch.object(
VeniceUpstreamProvider,
"_fetch_provider_models",
side_effect=RuntimeError("boom"),
):
import asyncio
assert asyncio.run(provider.fetch_models()) == []