feat(llm): unified client for OpenRouter (Qwen) + Anthropic (Sonnet)

LLMClient instrada richieste in base a ModelTier del genome:
- Tier C -> Qwen 2.5 72B via OpenRouter (chat completions)
- Tier B -> Sonnet 4.6 via Anthropic (messages API)
CompletionResult dataclass frozen con text, tokens, tier, model.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-09 19:54:39 +02:00
parent 93d0a9e848
commit a9261452e0
3 changed files with 133 additions and 0 deletions
View File
+80
View File
@@ -0,0 +1,80 @@
from __future__ import annotations
from dataclasses import dataclass
from anthropic import Anthropic
from openai import OpenAI
from ..genome.hypothesis import HypothesisAgentGenome, ModelTier
# Modelli configurati per Phase 1
MODEL_TIER_C = "qwen/qwen-2.5-72b-instruct" # via OpenRouter
MODEL_TIER_B = "claude-sonnet-4-6" # via Anthropic
OPENROUTER_BASE_URL = "https://openrouter.ai/api/v1"
@dataclass(frozen=True)
class CompletionResult:
text: str
input_tokens: int
output_tokens: int
tier: ModelTier
model: str
class LLMClient:
def __init__(
self,
openrouter_api_key: str,
anthropic_api_key: str | None = None,
) -> None:
self._openrouter = OpenAI(api_key=openrouter_api_key, base_url=OPENROUTER_BASE_URL)
self._anthropic = Anthropic(api_key=anthropic_api_key) if anthropic_api_key else None
def complete(
self,
genome: HypothesisAgentGenome,
system: str,
user: str,
max_tokens: int = 2000,
) -> CompletionResult:
if genome.model_tier == ModelTier.C:
resp = self._openrouter.chat.completions.create(
model=MODEL_TIER_C,
messages=[
{"role": "system", "content": system},
{"role": "user", "content": user},
],
temperature=genome.temperature,
top_p=genome.top_p,
max_tokens=max_tokens,
)
usage = resp.usage
assert usage is not None
return CompletionResult(
text=resp.choices[0].message.content or "",
input_tokens=usage.prompt_tokens,
output_tokens=usage.completion_tokens,
tier=ModelTier.C,
model=MODEL_TIER_C,
)
if self._anthropic is None:
raise RuntimeError("ANTHROPIC_API_KEY required for tier B genomes")
msg = self._anthropic.messages.create(
model=MODEL_TIER_B,
system=system,
messages=[{"role": "user", "content": user}],
temperature=genome.temperature,
top_p=genome.top_p,
max_tokens=max_tokens,
)
text = "".join(block.text for block in msg.content if hasattr(block, "text"))
return CompletionResult(
text=text,
input_tokens=msg.usage.input_tokens,
output_tokens=msg.usage.output_tokens,
tier=ModelTier.B,
model=MODEL_TIER_B,
)