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:
@@ -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,
|
||||||
|
)
|
||||||
@@ -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
|
||||||
Reference in New Issue
Block a user