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,
)
+53
View File
@@ -0,0 +1,53 @@
from multi_swarm.genome.hypothesis import HypothesisAgentGenome, ModelTier
from multi_swarm.llm.client import CompletionResult, LLMClient
def make_genome(tier: ModelTier) -> HypothesisAgentGenome:
return HypothesisAgentGenome(
system_prompt="x",
feature_access=["close"],
temperature=0.9,
top_p=0.95,
model_tier=tier,
lookback_window=200,
cognitive_style="physicist",
)
def test_completion_tier_c_uses_openrouter(mocker):
fake_openai = mocker.MagicMock()
fake_response = mocker.MagicMock()
fake_response.choices = [mocker.MagicMock(message=mocker.MagicMock(content="(strategy ...)"))]
fake_response.usage = mocker.MagicMock(prompt_tokens=100, completion_tokens=200)
fake_openai.chat.completions.create.return_value = fake_response
mocker.patch("multi_swarm.llm.client.OpenAI", return_value=fake_openai)
client = LLMClient(openrouter_api_key="or-x", anthropic_api_key=None)
g = make_genome(ModelTier.C)
out = client.complete(g, system="sys", user="usr")
assert isinstance(out, CompletionResult)
assert out.text == "(strategy ...)"
assert out.input_tokens == 100
assert out.output_tokens == 200
assert out.tier == ModelTier.C
fake_openai.chat.completions.create.assert_called_once()
def test_completion_tier_b_uses_anthropic(mocker):
fake_anthropic = mocker.MagicMock()
fake_msg = mocker.MagicMock()
fake_msg.content = [mocker.MagicMock(text="(strategy ...)")]
fake_msg.usage = mocker.MagicMock(input_tokens=80, output_tokens=150)
fake_anthropic.messages.create.return_value = fake_msg
mocker.patch("multi_swarm.llm.client.Anthropic", return_value=fake_anthropic)
client = LLMClient(openrouter_api_key="or-x", anthropic_api_key="an-x")
g = make_genome(ModelTier.B)
out = client.complete(g, system="sys", user="usr")
assert out.text == "(strategy ...)"
assert out.input_tokens == 80
assert out.output_tokens == 150
assert out.tier == ModelTier.B