Files
2026-07-13 13:12:00 +08:00

408 lines
13 KiB
Python

"""Tests for the server-side intelligent model routing module.
Covers model inference, the RoutingClient protocol, the default
LLMRoutingClient, and the public ``route_turn`` entry point.
"""
from __future__ import annotations
import json
from dataclasses import dataclass
from typing import Any
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from omnigent.server.smart_routing import (
LLMRoutingClient,
RoutingResult,
_build_rubric,
fetch_runner_models,
infer_models,
route_turn,
)
# ── Stubs ───────────────────────────────────────────────────────────
@dataclass
class _FakeOutputText:
text: str
type: str = "output_text"
@dataclass
class _FakeMessageOutput:
content: list[_FakeOutputText]
type: str = "message"
@dataclass
class _FakeResponse:
"""Minimal stub matching omnigent.llms.types.Response."""
output: list[_FakeMessageOutput]
class _FakeLLMClient:
"""Fake PolicyLLMClient that returns a canned verdict."""
def __init__(self, verdict: dict[str, Any]) -> None:
self._verdict = verdict
async def create(self, **kwargs: Any) -> _FakeResponse:
text = json.dumps(self._verdict)
return _FakeResponse(
output=[_FakeMessageOutput(content=[_FakeOutputText(text=text)])],
)
class _FakeRoutingClient:
"""Stub RoutingClient for route_turn integration tests."""
def __init__(self, result: RoutingResult | None) -> None:
self._result = result
async def route(
self, message: str, available_models: dict[str, list[str]]
) -> RoutingResult | None:
del message, available_models
return self._result
# ── infer_models ────────────────────────────────────────────────────
def test_infer_models_claude_sdk() -> None:
"""claude-sdk returns the claude model list."""
models = infer_models("claude-sdk")
assert models is not None
assert any("haiku" in m for m in models)
assert any("opus" in m for m in models)
# Ordered cheapest → most powerful
haiku_idx = next(i for i, m in enumerate(models) if "haiku" in m)
opus_idx = next(i for i, m in enumerate(models) if "opus" in m)
assert haiku_idx < opus_idx
def test_infer_models_native_harnesses() -> None:
assert infer_models("claude-native") is not None
assert infer_models("codex-native") is not None
def test_infer_models_codex() -> None:
models = infer_models("codex")
assert models is not None
assert any("gpt" in m for m in models)
def test_infer_models_openai_agents() -> None:
assert infer_models("openai-agents") is not None
def test_infer_models_pi() -> None:
"""pi is multi-model — both Claude and GPT."""
models = infer_models("pi")
assert models is not None
assert any("haiku" in m for m in models)
assert any("gpt" in m for m in models)
def test_infer_models_unknown_harness() -> None:
assert infer_models("cursor") is None
assert infer_models("antigravity") is None
assert infer_models(None) is None
# ── _build_rubric ───────────────────────────────────────────────────
def test_build_rubric_includes_all_models() -> None:
available = {
"claude-sdk": ["databricks-claude-haiku-4-5", "databricks-claude-opus-4-8"],
}
rubric = _build_rubric(available)
assert "databricks-claude-haiku-4-5" in rubric
assert "databricks-claude-opus-4-8" in rubric
assert "strict JSON" in rubric
assert "haiku" in rubric and "opus" in rubric
def test_build_rubric_shows_harness_names() -> None:
available = {
"claude-sdk": ["databricks-claude-haiku-4-5"],
"codex": ["databricks-gpt-5-4-nano"],
}
rubric = _build_rubric(available)
assert "claude-sdk" in rubric
assert "codex" in rubric
assert "databricks-claude-haiku-4-5" in rubric
assert "databricks-gpt-5-4-nano" in rubric
# ── LLMRoutingClient ───────────────────────────────────────────────
@pytest.mark.asyncio
async def test_llm_routing_client_returns_result() -> None:
verdict = {
"harness": "claude-sdk",
"model": "databricks-claude-opus-4-8",
"rationale": "hard refactor",
}
client = LLMRoutingClient(_FakeLLMClient(verdict))
models = infer_models("claude-sdk")
assert models is not None
result = await client.route("refactor auth", {"claude-sdk": models})
assert result is not None
assert result.model == "databricks-claude-opus-4-8"
assert result.rationale == "hard refactor"
assert result.harness == "claude-sdk"
@pytest.mark.asyncio
async def test_llm_routing_client_harness_mismatch_re_resolves() -> None:
"""If the judge picks a harness that doesn't own the model, fall back."""
claude_models = infer_models("claude-sdk")
assert claude_models is not None
verdict = {
"harness": "codex", # codex doesn't have claude models
"model": "databricks-claude-opus-4-8",
"rationale": "deep reasoning",
}
client = LLMRoutingClient(_FakeLLMClient(verdict))
result = await client.route(
"hard task", {"claude-sdk": claude_models, "codex": ["databricks-gpt-5-4"]}
)
assert result is not None
assert result.model == "databricks-claude-opus-4-8"
# harness re-resolved to the one that owns the model
assert result.harness == "claude-sdk"
@pytest.mark.asyncio
async def test_llm_routing_client_unknown_harness_re_resolves() -> None:
"""If the judge returns an unrecognised harness, fall back to model ownership."""
models = infer_models("claude-sdk")
assert models is not None
verdict = {
"harness": "hallucinated-harness",
"model": "databricks-claude-haiku-4-5",
"rationale": "simple task",
}
client = LLMRoutingClient(_FakeLLMClient(verdict))
result = await client.route("hello", {"claude-sdk": models})
assert result is not None
assert result.model == "databricks-claude-haiku-4-5"
assert result.harness == "claude-sdk"
@pytest.mark.asyncio
async def test_llm_routing_client_clamps_hallucinated_model() -> None:
verdict = {"harness": "claude-sdk", "model": "hallucinated-model", "rationale": "hard"}
client = LLMRoutingClient(_FakeLLMClient(verdict))
models = infer_models("claude-sdk")
assert models is not None
result = await client.route("hard task", {"claude-sdk": models})
assert result is not None
assert result.model == models[0] # clamped to cheapest
@pytest.mark.asyncio
async def test_llm_routing_client_rejects_empty_model() -> None:
verdict = {"harness": "claude-sdk", "model": "", "rationale": "x"}
client = LLMRoutingClient(_FakeLLMClient(verdict))
models = infer_models("claude-sdk")
assert models is not None
result = await client.route("hello", {"claude-sdk": models})
assert result is None
@pytest.mark.asyncio
async def test_llm_routing_client_returns_none_on_error() -> None:
class _BrokenLLM:
async def create(self, **kwargs: Any) -> None:
raise TypeError("boom")
client = LLMRoutingClient(_BrokenLLM())
models = infer_models("claude-sdk")
assert models is not None
result = await client.route("hello", {"claude-sdk": models})
assert result is None
# ── fetch_runner_models ────────────────────────────────────────────
@pytest.mark.asyncio
async def test_fetch_runner_models_parses_catalog() -> None:
catalog_payload = {
"workers": {
"self": {
"source": "catalog",
"verified": True,
"models": [
{"id": "databricks-claude-haiku-4-5", "family": "claude"},
{"id": "databricks-claude-opus-4-8", "family": "claude"},
],
"note": "",
},
"claude_code": {
"source": "catalog",
"verified": True,
"models": [
{"id": "databricks-claude-haiku-4-5", "family": "claude"},
{"id": "databricks-claude-sonnet-4-6", "family": "claude"},
],
"note": "",
},
}
}
mock_response = MagicMock()
mock_response.json.return_value = catalog_payload
mock_response.raise_for_status = MagicMock()
mock_client = MagicMock()
mock_client.get = AsyncMock(return_value=mock_response)
result = await fetch_runner_models("conv_123", mock_client)
assert result is not None
assert "databricks-claude-haiku-4-5" in result["self"]
assert "databricks-claude-opus-4-8" in result["self"]
assert "databricks-claude-sonnet-4-6" in result["claude_code"]
@pytest.mark.asyncio
async def test_fetch_runner_models_returns_none_on_http_error() -> None:
import httpx
mock_client = MagicMock()
mock_client.get = AsyncMock(side_effect=httpx.HTTPError("connection refused"))
result = await fetch_runner_models("conv_123", mock_client)
assert result is None
@pytest.mark.asyncio
async def test_fetch_runner_models_returns_none_on_empty_workers() -> None:
mock_response = MagicMock()
mock_response.json.return_value = {"workers": {}}
mock_response.raise_for_status = MagicMock()
mock_client = MagicMock()
mock_client.get = AsyncMock(return_value=mock_response)
result = await fetch_runner_models("conv_123", mock_client)
assert result is None
# ── route_turn (integration) ───────────────────────────────────────
@dataclass
class _FakeCaps:
routing_client: Any = None # type: ignore[explicit-any]
@pytest.mark.asyncio
async def test_route_turn_uses_caps_routing_client() -> None:
expected = RoutingResult(
model="databricks-claude-haiku-4-5",
rationale="trivial",
harness="claude-sdk",
)
caps = _FakeCaps(routing_client=_FakeRoutingClient(expected))
with patch(
"omnigent.runtime._globals._caps",
new=caps,
):
model, v = await route_turn("claude-sdk", "hello")
assert model == "databricks-claude-haiku-4-5"
assert v is not None
assert "tier" not in v
@pytest.mark.asyncio
async def test_route_turn_returns_none_when_no_client() -> None:
caps = _FakeCaps(routing_client=None)
with patch(
"omnigent.runtime._globals._caps",
new=caps,
):
model, _v = await route_turn("claude-sdk", "hello")
assert model is None
@pytest.mark.asyncio
async def test_route_turn_unknown_harness() -> None:
model, _v = await route_turn("cursor", "hello")
assert model is None
assert _v is None
@pytest.mark.asyncio
async def test_route_turn_uses_runner_catalog_when_available() -> None:
"""route_turn uses live runner catalog instead of static table when provided."""
expected = RoutingResult(
model="databricks-claude-opus-4-8",
rationale="complex task",
harness="self",
)
mock_response = MagicMock()
mock_response.json.return_value = {
"workers": {
"self": {
"source": "catalog",
"verified": True,
"models": [
{"id": "databricks-claude-haiku-4-5"},
{"id": "databricks-claude-opus-4-8"},
],
"note": "",
}
}
}
mock_response.raise_for_status = MagicMock()
mock_client = MagicMock()
mock_client.get = AsyncMock(return_value=mock_response)
caps = _FakeCaps(routing_client=_FakeRoutingClient(expected))
with patch("omnigent.runtime._globals._caps", new=caps):
model, _v = await route_turn(
"claude-sdk",
"complex task",
session_id="conv_123",
runner_client=mock_client,
)
assert model == "databricks-claude-opus-4-8"
# Runner endpoint was called
mock_client.get.assert_called_once()
call_url = mock_client.get.call_args[0][0]
assert "conv_123" in call_url and "models" in call_url
@pytest.mark.asyncio
async def test_route_turn_falls_back_to_static_when_runner_unavailable() -> None:
"""Falls back to infer_models when runner catalog fetch fails."""
import httpx
mock_client = MagicMock()
mock_client.get = AsyncMock(side_effect=httpx.HTTPError("runner down"))
expected = RoutingResult(
model="databricks-claude-haiku-4-5",
rationale="simple",
harness="claude-sdk",
)
caps = _FakeCaps(routing_client=_FakeRoutingClient(expected))
with patch("omnigent.runtime._globals._caps", new=caps):
model, _v = await route_turn(
"claude-sdk",
"hello",
session_id="conv_123",
runner_client=mock_client,
)
# Still routes — fell back to static infer_models
assert model == "databricks-claude-haiku-4-5"