Files
routstr-core/routstr/upstream/anthropic.py
T
Jeroen UbbinkandClaude Opus 4.8 434283c58d refactor(upstream): resolve a provider's own row by primary key
A live upstream provider re-finds its own database row in two places —
PPQ.AI's insufficient-balance self-disable and the base
refresh_models_cache — with WHERE base_url == self.base_url AND
api_key == self.api_key. That uses a rotatable secret as a self-handle:
if the row's key rotates under a live object, it can no longer find
itself.

Carry the row's primary key on the instance as db_id, stamped centrally
by from_db_row via a _build_from_row construction hook that subclasses
override, and look the row up with session.get(UpstreamProviderRow,
db_id). This also closes a latent gap where providers built outside the
init path (auto-topup) never received db_id.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 12:53:31 +02:00

73 lines
2.8 KiB
Python

from typing import TYPE_CHECKING
from ..payment.models import Model, async_fetch_openrouter_models
from .base import BaseUpstreamProvider
if TYPE_CHECKING:
from ..core.db import UpstreamProviderRow
class AnthropicUpstreamProvider(BaseUpstreamProvider):
"""Upstream provider specifically configured for Anthropic API."""
provider_type = "anthropic"
default_base_url = "https://api.anthropic.com/v1"
platform_url = "https://console.anthropic.com/settings/keys"
supports_anthropic_messages = True
litellm_provider_prefix = "anthropic/"
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"
) -> "AnthropicUpstreamProvider":
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": "Anthropic",
"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:
"""Strip 'anthropic/' prefix for Anthropic API compatibility and transform model names."""
if model_id.startswith("anthropic/"):
model_id = model_id[len("anthropic/") :]
fixed_transforms = {
"claude-haiku-4.5": "claude-haiku-4-5-20251001",
"claude-sonnet-4.5": "claude-sonnet-4-5-20250929",
"claude-opus-4.1": "claude-opus-4-1-20250805",
"claude-opus-4": "claude-opus-4-20250514",
"claude-sonnet-4": "claude-sonnet-4-20250514",
"claude-3.5-haiku": "claude-3-5-haiku-20241022",
"claude-3-haiku": "claude-3-haiku-20240307",
"claude-haiku-4-5": "claude-haiku-4-5-20251001",
"claude-sonnet-4-5": "claude-sonnet-4-5-20250929",
"claude-opus-4-1": "claude-opus-4-1-20250805",
"claude-3-5-haiku": "claude-3-5-haiku-20241022",
}
if model_id in fixed_transforms:
model_id = fixed_transforms[model_id]
return model_id
async def fetch_models(self) -> list[Model]:
"""Fetch Anthropic models from OpenRouter API filtered by anthropic source."""
models_data = await async_fetch_openrouter_models(source_filter="anthropic")
models = [Model(**model) for model in models_data] # type: ignore
for model in models:
model.alias_ids = [self.transform_model_name(model.id)]
return models