chore(lint): auto-fix 253 of 283 ruff issues (F401, I001, E402, RUF100, UP037, SIM105)
Mass ruff auto-fix:
- ruff check --fix: 109 issues fixed (F401 unused imports,
I001 unsorted imports, UP037 quoted annotations, SIM105
suppressible exception, RUF100 unused-noqa)
- ruff check --fix --unsafe-fixes: 22 additional issues
- ruff format: 70 files reformatted
- Manual pass: fix 16 misplaced import httpx lines
- Manual pass: fix remaining E402 (import-after-docstring)
Result: 283 errors -> 30 errors.
The remaining 30 are real issues that need manual review:
5 F401 unused-import (likely auto-generated stubs)
5 F821 undefined-name (real bugs in code that references
redis/pydantic/LLMRegistry without imports)
3 BLE001 (the compliance LLM fallback is intentional; the
other two are real)
3 RUF012 mutable-class-default
3 SIM105, 3 SIM117, 2 E722, 2 E741
1 B007, 1 B025, 1 E402, 1 RUF200 (pyproject.toml issue)
Tests: 436/437 pass (1 pre-existing SSE sandbox failure).
format check + import sort: now clean.
make ci: still gated on the 30 remaining real issues.
Follow-up: triage the 30 issues file-by-file.
This commit is contained in:
parent
e60a62a07a
commit
a7c30b12cd
85 changed files with 2374 additions and 1071 deletions
|
|
@ -19,6 +19,7 @@ logger = logging.getLogger(__name__)
|
|||
@dataclass
|
||||
class LLMResponse:
|
||||
"""Standard response from any LLM provider."""
|
||||
|
||||
text: str
|
||||
model: str
|
||||
provider: str
|
||||
|
|
@ -33,6 +34,7 @@ class LLMResponse:
|
|||
@dataclass
|
||||
class ReferralConfig:
|
||||
"""Referral/affiliate config for revenue sharing."""
|
||||
|
||||
enabled: bool = True
|
||||
program_id: str = "pry-default"
|
||||
# Provider-specific referral links (with our affiliate ID)
|
||||
|
|
@ -43,6 +45,7 @@ class ReferralConfig:
|
|||
def __post_init__(self):
|
||||
if not self.referral_links:
|
||||
from referrals import PROVIDER_CATALOG
|
||||
|
||||
for _category, providers in PROVIDER_CATALOG.items():
|
||||
for p in providers:
|
||||
self.referral_links[p["tag"]] = p["url"]
|
||||
|
|
@ -58,8 +61,14 @@ class LLMProvider(ABC):
|
|||
referral_url: str = ""
|
||||
|
||||
@abstractmethod
|
||||
async def complete(self, prompt: str, system: str = "", max_tokens: int = 1024,
|
||||
temperature: float = 0.7, model: str = "") -> LLMResponse:
|
||||
async def complete(
|
||||
self,
|
||||
prompt: str,
|
||||
system: str = "",
|
||||
max_tokens: int = 1024,
|
||||
temperature: float = 0.7,
|
||||
model: str = "",
|
||||
) -> LLMResponse:
|
||||
"""Send completion request to provider."""
|
||||
raise NotImplementedError
|
||||
|
||||
|
|
@ -69,4 +78,6 @@ class LLMProvider(ABC):
|
|||
raise NotImplementedError
|
||||
|
||||
def estimate_cost(self, input_tokens: int, output_tokens: int) -> float:
|
||||
return (input_tokens / 1000) * self.cost_per_1k_input + (output_tokens / 1000) * self.cost_per_1k_output
|
||||
return (input_tokens / 1000) * self.cost_per_1k_input + (
|
||||
output_tokens / 1000
|
||||
) * self.cost_per_1k_output
|
||||
|
|
|
|||
|
|
@ -22,27 +22,48 @@ class OpenAIProvider(LLMProvider):
|
|||
def __init__(self, api_key: str = ""):
|
||||
self.api_key = api_key or os.getenv("OPENAI_API_KEY", "")
|
||||
|
||||
async def complete(self, prompt, system="", max_tokens=1024, temperature=0.7, model="gpt-4o-mini"):
|
||||
async def complete(
|
||||
self, prompt, system="", max_tokens=1024, temperature=0.7, model="gpt-4o-mini"
|
||||
):
|
||||
from client import get_client
|
||||
|
||||
client = await get_client()
|
||||
messages = []
|
||||
if system: messages.append({"role": "system", "content": system})
|
||||
if system:
|
||||
messages.append({"role": "system", "content": system})
|
||||
messages.append({"role": "user", "content": prompt})
|
||||
resp = await client.post("https://api.openai.com/v1/chat/completions",
|
||||
json={"model": model, "messages": messages, "max_tokens": max_tokens, "temperature": temperature},
|
||||
headers={"Authorization": f"Bearer {self.api_key}"}, timeout=60)
|
||||
resp = await client.post(
|
||||
"https://api.openai.com/v1/chat/completions",
|
||||
json={
|
||||
"model": model,
|
||||
"messages": messages,
|
||||
"max_tokens": max_tokens,
|
||||
"temperature": temperature,
|
||||
},
|
||||
headers={"Authorization": f"Bearer {self.api_key}"},
|
||||
timeout=60,
|
||||
)
|
||||
data = resp.json()
|
||||
choice = data["choices"][0]
|
||||
return LLMResponse(text=choice["message"]["content"], model=model, provider=self.name,
|
||||
input_tokens=data["usage"]["prompt_tokens"],
|
||||
output_tokens=data["usage"]["completion_tokens"], raw=data)
|
||||
return LLMResponse(
|
||||
text=choice["message"]["content"],
|
||||
model=model,
|
||||
provider=self.name,
|
||||
input_tokens=data["usage"]["prompt_tokens"],
|
||||
output_tokens=data["usage"]["completion_tokens"],
|
||||
raw=data,
|
||||
)
|
||||
|
||||
async def embed(self, text, model="text-embedding-3-small"):
|
||||
from client import get_client
|
||||
|
||||
client = await get_client()
|
||||
resp = await client.post("https://api.openai.com/v1/embeddings",
|
||||
resp = await client.post(
|
||||
"https://api.openai.com/v1/embeddings",
|
||||
json={"input": text, "model": model},
|
||||
headers={"Authorization": f"Bearer {self.api_key}"}, timeout=30)
|
||||
headers={"Authorization": f"Bearer {self.api_key}"},
|
||||
timeout=30,
|
||||
)
|
||||
return resp.json()["data"][0]["embedding"]
|
||||
|
||||
|
||||
|
|
@ -55,18 +76,35 @@ class AnthropicProvider(LLMProvider):
|
|||
def __init__(self, api_key: str = ""):
|
||||
self.api_key = api_key or os.getenv("ANTHROPIC_API_KEY", "")
|
||||
|
||||
async def complete(self, prompt, system="", max_tokens=1024, temperature=0.7, model="claude-3-haiku-20240307"):
|
||||
async def complete(
|
||||
self, prompt, system="", max_tokens=1024, temperature=0.7, model="claude-3-haiku-20240307"
|
||||
):
|
||||
from client import get_client
|
||||
|
||||
client = await get_client()
|
||||
body = {"model": model, "max_tokens": max_tokens, "temperature": temperature,
|
||||
"messages": [{"role": "user", "content": prompt}]}
|
||||
if system: body["system"] = system
|
||||
resp = await client.post("https://api.anthropic.com/v1/messages",
|
||||
json=body, headers={"x-api-key": self.api_key, "anthropic-version": "2023-06-01"}, timeout=60)
|
||||
body = {
|
||||
"model": model,
|
||||
"max_tokens": max_tokens,
|
||||
"temperature": temperature,
|
||||
"messages": [{"role": "user", "content": prompt}],
|
||||
}
|
||||
if system:
|
||||
body["system"] = system
|
||||
resp = await client.post(
|
||||
"https://api.anthropic.com/v1/messages",
|
||||
json=body,
|
||||
headers={"x-api-key": self.api_key, "anthropic-version": "2023-06-01"},
|
||||
timeout=60,
|
||||
)
|
||||
data = resp.json()
|
||||
return LLMResponse(text=data["content"][0]["text"], model=model, provider=self.name,
|
||||
input_tokens=data["usage"]["input_tokens"],
|
||||
output_tokens=data["usage"]["output_tokens"], raw=data)
|
||||
return LLMResponse(
|
||||
text=data["content"][0]["text"],
|
||||
model=model,
|
||||
provider=self.name,
|
||||
input_tokens=data["usage"]["input_tokens"],
|
||||
output_tokens=data["usage"]["output_tokens"],
|
||||
raw=data,
|
||||
)
|
||||
|
||||
async def embed(self, text, model=""):
|
||||
raise NotImplementedError("Anthropic doesn't have a public embedding API yet")
|
||||
|
|
@ -81,23 +119,36 @@ class GoogleProvider(LLMProvider):
|
|||
def __init__(self, api_key: str = ""):
|
||||
self.api_key = api_key or os.getenv("GOOGLE_API_KEY", "")
|
||||
|
||||
async def complete(self, prompt, system="", max_tokens=1024, temperature=0.7, model="gemini-1.5-flash"):
|
||||
async def complete(
|
||||
self, prompt, system="", max_tokens=1024, temperature=0.7, model="gemini-1.5-flash"
|
||||
):
|
||||
from client import get_client
|
||||
|
||||
client = await get_client()
|
||||
url = f"https://generativelanguage.googleapis.com/v1beta/models/{model}:generateContent?key={self.api_key}"
|
||||
contents = [{"role": "user", "parts": [{"text": prompt}]}]
|
||||
body = {"contents": contents, "generationConfig": {"maxOutputTokens": max_tokens, "temperature": temperature}}
|
||||
if system: body["systemInstruction"] = {"parts": [{"text": system}]}
|
||||
body = {
|
||||
"contents": contents,
|
||||
"generationConfig": {"maxOutputTokens": max_tokens, "temperature": temperature},
|
||||
}
|
||||
if system:
|
||||
body["systemInstruction"] = {"parts": [{"text": system}]}
|
||||
resp = await client.post(url, json=body, timeout=60)
|
||||
data = resp.json()
|
||||
text = data["candidates"][0]["content"]["parts"][0]["text"]
|
||||
usage = data.get("usageMetadata", {})
|
||||
return LLMResponse(text=text, model=model, provider=self.name,
|
||||
input_tokens=usage.get("promptTokenCount", 0),
|
||||
output_tokens=usage.get("candidatesTokenCount", 0), raw=data)
|
||||
return LLMResponse(
|
||||
text=text,
|
||||
model=model,
|
||||
provider=self.name,
|
||||
input_tokens=usage.get("promptTokenCount", 0),
|
||||
output_tokens=usage.get("candidatesTokenCount", 0),
|
||||
raw=data,
|
||||
)
|
||||
|
||||
async def embed(self, text, model="text-embedding-004"):
|
||||
from client import get_client
|
||||
|
||||
client = await get_client()
|
||||
url = f"https://generativelanguage.googleapis.com/v1beta/models/{model}:embedContent?key={self.api_key}"
|
||||
resp = await client.post(url, json={"content": {"parts": [{"text": text}]}}, timeout=30)
|
||||
|
|
@ -113,22 +164,39 @@ class CohereProvider(LLMProvider):
|
|||
def __init__(self, api_key: str = ""):
|
||||
self.api_key = api_key or os.getenv("COHERE_API_KEY", "")
|
||||
|
||||
async def complete(self, prompt, system="", max_tokens=1024, temperature=0.7, model="command-r"):
|
||||
async def complete(
|
||||
self, prompt, system="", max_tokens=1024, temperature=0.7, model="command-r"
|
||||
):
|
||||
from client import get_client
|
||||
|
||||
client = await get_client()
|
||||
body = {"model": model, "message": prompt, "max_tokens": max_tokens, "temperature": temperature}
|
||||
if system: body["preamble"] = system
|
||||
resp = await client.post("https://api.cohere.ai/v1/chat",
|
||||
json=body, headers={"Authorization": f"Bearer {self.api_key}"}, timeout=60)
|
||||
body = {
|
||||
"model": model,
|
||||
"message": prompt,
|
||||
"max_tokens": max_tokens,
|
||||
"temperature": temperature,
|
||||
}
|
||||
if system:
|
||||
body["preamble"] = system
|
||||
resp = await client.post(
|
||||
"https://api.cohere.ai/v1/chat",
|
||||
json=body,
|
||||
headers={"Authorization": f"Bearer {self.api_key}"},
|
||||
timeout=60,
|
||||
)
|
||||
data = resp.json()
|
||||
return LLMResponse(text=data["text"], model=model, provider=self.name, raw=data)
|
||||
|
||||
async def embed(self, text, model="embed-english-v3.0"):
|
||||
from client import get_client
|
||||
|
||||
client = await get_client()
|
||||
resp = await client.post("https://api.cohere.ai/v1/embed",
|
||||
resp = await client.post(
|
||||
"https://api.cohere.ai/v1/embed",
|
||||
json={"texts": [text], "model": model, "input_type": "search_document"},
|
||||
headers={"Authorization": f"Bearer {self.api_key}"}, timeout=30)
|
||||
headers={"Authorization": f"Bearer {self.api_key}"},
|
||||
timeout=30,
|
||||
)
|
||||
return resp.json()["embeddings"][0]
|
||||
|
||||
|
||||
|
|
@ -141,26 +209,47 @@ class MistralProvider(LLMProvider):
|
|||
def __init__(self, api_key: str = ""):
|
||||
self.api_key = api_key or os.getenv("MISTRAL_API_KEY", "")
|
||||
|
||||
async def complete(self, prompt, system="", max_tokens=1024, temperature=0.7, model="mistral-small-latest"):
|
||||
async def complete(
|
||||
self, prompt, system="", max_tokens=1024, temperature=0.7, model="mistral-small-latest"
|
||||
):
|
||||
from client import get_client
|
||||
|
||||
client = await get_client()
|
||||
messages = []
|
||||
if system: messages.append({"role": "system", "content": system})
|
||||
if system:
|
||||
messages.append({"role": "system", "content": system})
|
||||
messages.append({"role": "user", "content": prompt})
|
||||
resp = await client.post("https://api.mistral.ai/v1/chat/completions",
|
||||
json={"model": model, "messages": messages, "max_tokens": max_tokens, "temperature": temperature},
|
||||
headers={"Authorization": f"Bearer {self.api_key}"}, timeout=60)
|
||||
resp = await client.post(
|
||||
"https://api.mistral.ai/v1/chat/completions",
|
||||
json={
|
||||
"model": model,
|
||||
"messages": messages,
|
||||
"max_tokens": max_tokens,
|
||||
"temperature": temperature,
|
||||
},
|
||||
headers={"Authorization": f"Bearer {self.api_key}"},
|
||||
timeout=60,
|
||||
)
|
||||
data = resp.json()
|
||||
return LLMResponse(text=data["choices"][0]["message"]["content"], model=model, provider=self.name,
|
||||
input_tokens=data["usage"]["prompt_tokens"],
|
||||
output_tokens=data["usage"]["completion_tokens"], raw=data)
|
||||
return LLMResponse(
|
||||
text=data["choices"][0]["message"]["content"],
|
||||
model=model,
|
||||
provider=self.name,
|
||||
input_tokens=data["usage"]["prompt_tokens"],
|
||||
output_tokens=data["usage"]["completion_tokens"],
|
||||
raw=data,
|
||||
)
|
||||
|
||||
async def embed(self, text, model="mistral-embed"):
|
||||
from client import get_client
|
||||
|
||||
client = await get_client()
|
||||
resp = await client.post("https://api.mistral.ai/v1/embeddings",
|
||||
resp = await client.post(
|
||||
"https://api.mistral.ai/v1/embeddings",
|
||||
json={"model": model, "input": [text]},
|
||||
headers={"Authorization": f"Bearer {self.api_key}"}, timeout=30)
|
||||
headers={"Authorization": f"Bearer {self.api_key}"},
|
||||
timeout=30,
|
||||
)
|
||||
return resp.json()["data"][0]["embedding"]
|
||||
|
||||
|
||||
|
|
@ -176,22 +265,36 @@ class OllamaProvider(LLMProvider):
|
|||
|
||||
async def complete(self, prompt, system="", max_tokens=1024, temperature=0.7, model=""):
|
||||
from client import get_client
|
||||
|
||||
client = await get_client()
|
||||
model = model or self.default_model
|
||||
body = {"model": model, "prompt": prompt, "stream": False, "options": {"temperature": temperature, "num_predict": max_tokens}}
|
||||
if system: body["system"] = system
|
||||
body = {
|
||||
"model": model,
|
||||
"prompt": prompt,
|
||||
"stream": False,
|
||||
"options": {"temperature": temperature, "num_predict": max_tokens},
|
||||
}
|
||||
if system:
|
||||
body["system"] = system
|
||||
resp = await client.post(f"{self.base_url}/api/generate", json=body, timeout=300)
|
||||
data = resp.json()
|
||||
return LLMResponse(text=data["response"], model=model, provider=self.name,
|
||||
input_tokens=data.get("prompt_eval_count", 0),
|
||||
output_tokens=data.get("eval_count", 0), raw=data)
|
||||
return LLMResponse(
|
||||
text=data["response"],
|
||||
model=model,
|
||||
provider=self.name,
|
||||
input_tokens=data.get("prompt_eval_count", 0),
|
||||
output_tokens=data.get("eval_count", 0),
|
||||
raw=data,
|
||||
)
|
||||
|
||||
async def embed(self, text, model=""):
|
||||
from client import get_client
|
||||
|
||||
client = await get_client()
|
||||
model = model or "nomic-embed-text"
|
||||
resp = await client.post(f"{self.base_url}/api/embeddings",
|
||||
json={"model": model, "prompt": text}, timeout=60)
|
||||
resp = await client.post(
|
||||
f"{self.base_url}/api/embeddings", json={"model": model, "prompt": text}, timeout=60
|
||||
)
|
||||
return resp.json()["embedding"]
|
||||
|
||||
|
||||
|
|
@ -204,20 +307,42 @@ class OpenRouterProvider(LLMProvider):
|
|||
def __init__(self, api_key: str = ""):
|
||||
self.api_key = api_key or os.getenv("OPENROUTER_API_KEY", "")
|
||||
|
||||
async def complete(self, prompt, system="", max_tokens=1024, temperature=0.7, model="meta-llama/llama-3.2-3b-instruct:free"):
|
||||
async def complete(
|
||||
self,
|
||||
prompt,
|
||||
system="",
|
||||
max_tokens=1024,
|
||||
temperature=0.7,
|
||||
model="meta-llama/llama-3.2-3b-instruct:free",
|
||||
):
|
||||
from client import get_client
|
||||
|
||||
client = await get_client()
|
||||
messages = []
|
||||
if system: messages.append({"role": "system", "content": system})
|
||||
if system:
|
||||
messages.append({"role": "system", "content": system})
|
||||
messages.append({"role": "user", "content": prompt})
|
||||
resp = await client.post("https://openrouter.ai/api/v1/chat/completions",
|
||||
json={"model": model, "messages": messages, "max_tokens": max_tokens, "temperature": temperature},
|
||||
headers={"Authorization": f"Bearer {self.api_key}"}, timeout=60)
|
||||
resp = await client.post(
|
||||
"https://openrouter.ai/api/v1/chat/completions",
|
||||
json={
|
||||
"model": model,
|
||||
"messages": messages,
|
||||
"max_tokens": max_tokens,
|
||||
"temperature": temperature,
|
||||
},
|
||||
headers={"Authorization": f"Bearer {self.api_key}"},
|
||||
timeout=60,
|
||||
)
|
||||
data = resp.json()
|
||||
usage = data.get("usage", {})
|
||||
return LLMResponse(text=data["choices"][0]["message"]["content"], model=model, provider=self.name,
|
||||
input_tokens=usage.get("prompt_tokens", 0),
|
||||
output_tokens=usage.get("completion_tokens", 0), raw=data)
|
||||
return LLMResponse(
|
||||
text=data["choices"][0]["message"]["content"],
|
||||
model=model,
|
||||
provider=self.name,
|
||||
input_tokens=usage.get("prompt_tokens", 0),
|
||||
output_tokens=usage.get("completion_tokens", 0),
|
||||
raw=data,
|
||||
)
|
||||
|
||||
async def embed(self, text, model=""):
|
||||
raise NotImplementedError("OpenRouter focuses on chat; use dedicated embedding providers")
|
||||
|
|
@ -234,8 +359,12 @@ def register_default_providers(registry: "LLMRegistry") -> None:
|
|||
"openrouter": os.getenv("OPENROUTER_API_KEY"),
|
||||
}
|
||||
provider_classes = {
|
||||
"openai": OpenAIProvider, "anthropic": AnthropicProvider, "google": GoogleProvider,
|
||||
"cohere": CohereProvider, "mistral": MistralProvider, "openrouter": OpenRouterProvider,
|
||||
"openai": OpenAIProvider,
|
||||
"anthropic": AnthropicProvider,
|
||||
"google": GoogleProvider,
|
||||
"cohere": CohereProvider,
|
||||
"mistral": MistralProvider,
|
||||
"openrouter": OpenRouterProvider,
|
||||
}
|
||||
for name, cls in provider_classes.items():
|
||||
key = api_key_map.get(name)
|
||||
|
|
|
|||
|
|
@ -7,16 +7,14 @@
|
|||
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import time
|
||||
from collections import defaultdict
|
||||
from datetime import UTC, datetime
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from llm_providers.base import LLMProvider, LLMResponse, ReferralConfig
|
||||
|
||||
from paths import PRY_DATA_DIR
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
USAGE_DIR = PRY_DATA_DIR / "llm_usage"
|
||||
|
|
@ -31,10 +29,15 @@ class LLMRegistry:
|
|||
self.referral = referral or ReferralConfig()
|
||||
self.fallback_chain: list[str] = []
|
||||
# Usage tracking
|
||||
self.usage: dict[str, dict[str, Any]] = defaultdict(lambda: {
|
||||
"calls": 0, "input_tokens": 0, "output_tokens": 0, "cost_usd": 0.0,
|
||||
"last_used": None,
|
||||
})
|
||||
self.usage: dict[str, dict[str, Any]] = defaultdict(
|
||||
lambda: {
|
||||
"calls": 0,
|
||||
"input_tokens": 0,
|
||||
"output_tokens": 0,
|
||||
"cost_usd": 0.0,
|
||||
"last_used": None,
|
||||
}
|
||||
)
|
||||
self._load_usage()
|
||||
|
||||
def register(self, provider: LLMProvider) -> None:
|
||||
|
|
@ -46,13 +49,26 @@ class LLMRegistry:
|
|||
def set_fallback_chain(self, chain: list[str]) -> None:
|
||||
self.fallback_chain = chain
|
||||
|
||||
async def complete(self, prompt: str, system: str = "", provider_name: str = "",
|
||||
max_tokens: int = 1024, temperature: float = 0.7, model: str = "",
|
||||
fallback: bool = True) -> LLMResponse:
|
||||
async def complete(
|
||||
self,
|
||||
prompt: str,
|
||||
system: str = "",
|
||||
provider_name: str = "",
|
||||
max_tokens: int = 1024,
|
||||
temperature: float = 0.7,
|
||||
model: str = "",
|
||||
fallback: bool = True,
|
||||
) -> LLMResponse:
|
||||
"""Complete via specified provider or fallback chain."""
|
||||
names = [provider_name] if provider_name else list(self.fallback_chain)
|
||||
if not fallback:
|
||||
names = [provider_name] if provider_name else [self.fallback_chain[0]] if self.fallback_chain else []
|
||||
names = (
|
||||
[provider_name]
|
||||
if provider_name
|
||||
else [self.fallback_chain[0]]
|
||||
if self.fallback_chain
|
||||
else []
|
||||
)
|
||||
|
||||
last_error = ""
|
||||
for name in names:
|
||||
|
|
@ -63,14 +79,17 @@ class LLMRegistry:
|
|||
start = time.time()
|
||||
response = await provider.complete(prompt, system, max_tokens, temperature, model)
|
||||
response.latency_ms = int((time.time() - start) * 1000)
|
||||
response.cost_usd = provider.estimate_cost(response.input_tokens, response.output_tokens)
|
||||
response.cost_usd = provider.estimate_cost(
|
||||
response.input_tokens, response.output_tokens
|
||||
)
|
||||
response.referral_id = self.referral.program_id
|
||||
self._track(response)
|
||||
return response
|
||||
except Exception as e: # noqa: BLE001
|
||||
last_error = str(e)
|
||||
logger.warning("llm_provider_failed",
|
||||
extra={"provider": name, "error": str(e)[:100]})
|
||||
logger.warning(
|
||||
"llm_provider_failed", extra={"provider": name, "error": str(e)[:100]}
|
||||
)
|
||||
if not fallback:
|
||||
break
|
||||
raise Exception(f"All LLM providers failed. Last: {last_error}")
|
||||
|
|
@ -84,7 +103,9 @@ class LLMRegistry:
|
|||
try:
|
||||
return await provider.embed(text, model)
|
||||
except Exception as e: # noqa: BLE001
|
||||
logger.warning("embed_provider_failed", extra={"provider": name, "error": str(e)[:80]})
|
||||
logger.warning(
|
||||
"embed_provider_failed", extra={"provider": name, "error": str(e)[:80]}
|
||||
)
|
||||
raise Exception("All embedding providers failed")
|
||||
|
||||
def _track(self, response: LLMResponse) -> None:
|
||||
|
|
@ -97,7 +118,8 @@ class LLMRegistry:
|
|||
# NEW: track in-app LLM usage as referral revenue
|
||||
try:
|
||||
from referrals import ReferralTracker
|
||||
if not hasattr(self, '_referral_tracker'):
|
||||
|
||||
if not hasattr(self, "_referral_tracker"):
|
||||
self._referral_tracker = ReferralTracker()
|
||||
# Estimate revenue at 5% of user cost (typical affiliate share)
|
||||
self._referral_tracker.track_in_app_usage(response.provider, response.cost_usd * 0.05)
|
||||
|
|
@ -108,14 +130,20 @@ class LLMRegistry:
|
|||
path = USAGE_DIR / f"usage_{today}.jsonl"
|
||||
try:
|
||||
with open(path, "a") as f:
|
||||
f.write(json.dumps({
|
||||
"ts": datetime.now(UTC).isoformat(),
|
||||
"provider": response.provider, "model": response.model,
|
||||
"input_tokens": response.input_tokens,
|
||||
"output_tokens": response.output_tokens,
|
||||
"cost_usd": response.cost_usd,
|
||||
"referral_id": response.referral_id,
|
||||
}) + "\n")
|
||||
f.write(
|
||||
json.dumps(
|
||||
{
|
||||
"ts": datetime.now(UTC).isoformat(),
|
||||
"provider": response.provider,
|
||||
"model": response.model,
|
||||
"input_tokens": response.input_tokens,
|
||||
"output_tokens": response.output_tokens,
|
||||
"cost_usd": response.cost_usd,
|
||||
"referral_id": response.referral_id,
|
||||
}
|
||||
)
|
||||
+ "\n"
|
||||
)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
|
|
@ -157,5 +185,6 @@ def get_registry() -> LLMRegistry:
|
|||
_registry = LLMRegistry()
|
||||
# Register providers lazily (only if API keys are set)
|
||||
from llm_providers.providers import register_default_providers
|
||||
|
||||
register_default_providers(_registry)
|
||||
return _registry
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue