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

922 lines
32 KiB
Python

from __future__ import annotations
import asyncio
import json
import time
from collections.abc import AsyncIterator
from dataclasses import dataclass, field
from typing import Any
import pytest
from opensquilla.provider import (
ChatConfig,
DoneEvent,
ErrorEvent,
Message,
ProviderHeartbeatEvent,
TextDeltaEvent,
ToolDefinition,
ToolInputSchema,
)
from opensquilla.provider.ensemble import EnsembleMemberConfig, EnsembleProvider
from opensquilla.provider.selector import ProviderConfig
from opensquilla.provider.types import EnsembleProgressEvent, StreamEvent
@dataclass
class _FakePlan:
events: list[StreamEvent]
delay: float = 0.0
gate: asyncio.Event | None = None
@dataclass
class _FakeRegistry:
plans: dict[str, _FakePlan]
calls: list[dict[str, Any]] = field(default_factory=list)
def provider_for(self, cfg: ProviderConfig) -> _FakeProvider:
return _FakeProvider(cfg, self)
class _FakeProvider:
provider_name = "fake"
def __init__(self, cfg: ProviderConfig, registry: _FakeRegistry) -> None:
self._cfg = cfg
self._registry = registry
def chat(
self,
messages: list[Message],
tools: list[ToolDefinition] | None = None,
config: ChatConfig | None = None,
) -> AsyncIterator[StreamEvent]:
return self._chat(messages, tools=tools, config=config)
async def _chat(
self,
messages: list[Message],
*,
tools: list[ToolDefinition] | None,
config: ChatConfig | None,
) -> AsyncIterator[StreamEvent]:
self._registry.calls.append(
{
"model": self._cfg.model,
"messages": messages,
"tools": tools,
"config": config,
"started_at": time.monotonic(),
}
)
plan = self._registry.plans[self._cfg.model]
if plan.delay > 0:
await asyncio.sleep(plan.delay)
if plan.gate is not None:
await plan.gate.wait()
for event in plan.events:
yield event
async def list_models(self) -> list[Any]:
return []
def _member(model: str, *, thinking: str | None = "high") -> EnsembleMemberConfig:
return EnsembleMemberConfig(
provider_config=ProviderConfig(provider="fake", model=model),
label=model,
thinking=thinking,
)
def _openrouter_member(model: str, *, thinking: str | None = "high") -> EnsembleMemberConfig:
return EnsembleMemberConfig(
provider_config=ProviderConfig(
provider="openrouter",
model=model,
base_url="https://openrouter.ai/api/v1",
),
label=model,
thinking=thinking,
)
@pytest.mark.asyncio
async def test_ensemble_emits_heartbeat_while_waiting_for_slow_proposers(
monkeypatch: pytest.MonkeyPatch,
) -> None:
registry = _FakeRegistry(
{
"p1": _FakePlan(
[TextDeltaEvent(text="draft"), DoneEvent(model="p1")],
delay=0.05,
),
"agg": _FakePlan([TextDeltaEvent(text="final"), DoneEvent(model="agg")]),
}
)
monkeypatch.setattr("opensquilla.provider.ensemble._build_provider", registry.provider_for)
monkeypatch.setattr(
"opensquilla.provider.ensemble._ENSEMBLE_HEARTBEAT_INTERVAL_SECONDS",
0.01,
raising=False,
)
provider = EnsembleProvider(
profile_name="default",
proposers=[_member("p1")],
aggregator=_member("agg"),
proposer_timeout_seconds=1,
aggregator_timeout_seconds=1,
shuffle_candidates=False,
)
events = await _collect(provider)
assert any(
isinstance(event, ProviderHeartbeatEvent)
and event.phase == "ensemble_proposers_wait"
for event in events
)
@pytest.mark.asyncio
async def test_ensemble_emits_heartbeat_while_waiting_for_slow_aggregator(
monkeypatch: pytest.MonkeyPatch,
) -> None:
registry = _FakeRegistry(
{
"p1": _FakePlan([TextDeltaEvent(text="draft"), DoneEvent(model="p1")]),
"agg": _FakePlan(
[TextDeltaEvent(text="final"), DoneEvent(model="agg")],
delay=0.05,
),
}
)
monkeypatch.setattr("opensquilla.provider.ensemble._build_provider", registry.provider_for)
monkeypatch.setattr(
"opensquilla.provider.ensemble._ENSEMBLE_HEARTBEAT_INTERVAL_SECONDS",
0.01,
raising=False,
)
provider = EnsembleProvider(
profile_name="default",
proposers=[_member("p1")],
aggregator=_member("agg"),
proposer_timeout_seconds=1,
aggregator_timeout_seconds=1,
shuffle_candidates=False,
)
events = await _collect(provider)
assert any(
isinstance(event, ProviderHeartbeatEvent)
and event.phase == "ensemble_aggregator_wait"
for event in events
)
def _tool() -> ToolDefinition:
return ToolDefinition(
name="lookup",
description="Lookup test data",
input_schema=ToolInputSchema(),
)
async def _collect(provider: EnsembleProvider) -> list[StreamEvent]:
return [
event
async for event in provider.chat(
[Message(role="user", content="answer this")],
tools=[_tool()],
config=ChatConfig(max_tokens=99, thinking=False),
)
]
@pytest.mark.asyncio
async def test_ensemble_runs_proposers_concurrently_and_tools_only_reach_aggregator(
monkeypatch: pytest.MonkeyPatch,
) -> None:
registry = _FakeRegistry(
{
"p1": _FakePlan(
[
TextDeltaEvent(text="draft one"),
DoneEvent(input_tokens=1, output_tokens=2, model="p1"),
],
delay=0.1,
),
"p2": _FakePlan(
[
TextDeltaEvent(text="draft two"),
DoneEvent(input_tokens=3, output_tokens=4, model="p2"),
],
delay=0.1,
),
"agg": _FakePlan(
[
TextDeltaEvent(text="final"),
DoneEvent(
input_tokens=5,
output_tokens=6,
billed_cost=0.25,
model="agg",
cost_source="provider_billed",
),
]
),
}
)
monkeypatch.setattr("opensquilla.provider.ensemble._build_provider", registry.provider_for)
provider = EnsembleProvider(
profile_name="default",
proposers=[_member("p1"), _member("p2")],
aggregator=_member("agg"),
proposer_timeout_seconds=1,
aggregator_timeout_seconds=1,
shuffle_candidates=False,
)
started = time.monotonic()
events = await _collect(provider)
elapsed = time.monotonic() - started
assert elapsed < 0.18
assert [call["model"] for call in registry.calls] == ["p1", "p2", "agg"]
assert abs(registry.calls[0]["started_at"] - registry.calls[1]["started_at"]) < 0.05
assert registry.calls[0]["tools"] is None
assert registry.calls[1]["tools"] is None
assert registry.calls[2]["tools"] is not None
assert "draft one" in str(registry.calls[2]["messages"][-1].content)
assert "draft two" in str(registry.calls[2]["messages"][-1].content)
assert any(isinstance(event, TextDeltaEvent) and event.text == "final" for event in events)
done = next(event for event in events if isinstance(event, DoneEvent))
assert done.input_tokens == 9
assert done.output_tokens == 12
assert done.billed_cost == 0.25
assert done.model == "agg"
assert done.model_usage_breakdown == [
{
"role": "proposer",
"profile": "default",
"label": "p1",
"provider": "fake",
"model": "p1",
"sample_index": 0,
"input_tokens": 1,
"output_tokens": 2,
"reasoning_tokens": 0,
"cached_tokens": 0,
"cache_write_tokens": 0,
"billed_cost": 0.0,
"cost_source": "none",
},
{
"role": "proposer",
"profile": "default",
"label": "p2",
"provider": "fake",
"model": "p2",
"sample_index": 0,
"input_tokens": 3,
"output_tokens": 4,
"reasoning_tokens": 0,
"cached_tokens": 0,
"cache_write_tokens": 0,
"billed_cost": 0.0,
"cost_source": "none",
},
{
"role": "aggregator",
"profile": "default",
"label": "aggregator",
"provider": "fake",
"model": "agg",
"sample_index": 0,
"input_tokens": 5,
"output_tokens": 6,
"reasoning_tokens": 0,
"cached_tokens": 0,
"cache_write_tokens": 0,
"billed_cost": 0.25,
"cost_source": "provider_billed",
},
]
assert done.ensemble_trace is not None
assert done.ensemble_trace["profile"] == "default"
assert done.ensemble_trace["successful_proposers"] == 2
assert done.ensemble_trace["fallback_used"] is False
assert done.ensemble_trace["llm_request_count"] == 3
assert done.ensemble_trace["content_max_chars"] == 8000
first_candidate = done.ensemble_trace["candidates"][0]
assert first_candidate["execution"]["role"] == "proposer"
assert first_candidate["execution"]["model"] == "p1"
assert first_candidate["execution"]["thinking_override"] == "high"
assert first_candidate["execution"]["tools_enabled"] is False
assert first_candidate["execution"]["effective_max_tokens"] == 16384
assert first_candidate["content"]["text"] == "draft one"
assert first_candidate["content"]["truncated"] is False
final_request = done.ensemble_trace["final_request"]
assert final_request["role"] == "aggregator"
assert final_request["execution"]["model"] == "agg"
assert final_request["execution"]["tools_enabled"] is True
assert final_request["execution"]["tool_names"] == ["lookup"]
assert final_request["execution"]["effective_max_tokens"] == 16384
assert "draft one" in final_request["input"]["messages"][-1]["content"]["text"]
assert final_request["output"]["text"] == "final"
assert final_request["usage"]["model"] == "agg"
json.dumps(done.ensemble_trace)
@pytest.mark.asyncio
async def test_ensemble_resolves_max_tokens_per_openrouter_member(
monkeypatch: pytest.MonkeyPatch,
) -> None:
models = [
"deepseek/deepseek-v4-pro",
"z-ai/glm-5.2",
"moonshotai/kimi-k2.7-code",
"qwen/qwen3.7-max",
]
registry = _FakeRegistry(
{
**{
model: _FakePlan(
[
TextDeltaEvent(text=f"draft from {model}"),
DoneEvent(input_tokens=1, output_tokens=1, model=model),
]
)
for model in models
},
"agg": _FakePlan(
[
TextDeltaEvent(text="final"),
DoneEvent(input_tokens=1, output_tokens=1, model="agg"),
]
),
}
)
monkeypatch.setattr("opensquilla.provider.ensemble._build_provider", registry.provider_for)
provider = EnsembleProvider(
profile_name="static_openrouter_b5",
proposers=[_openrouter_member(model, thinking=None) for model in models],
aggregator=EnsembleMemberConfig(
provider_config=ProviderConfig(
provider="openrouter",
model="agg",
base_url="https://openrouter.ai/api/v1",
),
label="aggregator",
max_tokens=123,
thinking=None,
),
proposer_timeout_seconds=1,
aggregator_timeout_seconds=1,
shuffle_candidates=False,
)
events = [
event
async for event in provider.chat(
[Message(role="user", content="answer this")],
config=ChatConfig(max_tokens=384000, thinking=False),
)
]
by_model = {call["model"]: call["config"].max_tokens for call in registry.calls}
assert by_model == {
"deepseek/deepseek-v4-pro": 384000,
# models.dev's 2026-07-08 refresh lowered openrouter z-ai/glm-5.2 max
# output from 131072 to 32768.
"z-ai/glm-5.2": 32768,
"moonshotai/kimi-k2.7-code": 16384,
"qwen/qwen3.7-max": 65536,
"agg": 123,
}
done = next(event for event in events if isinstance(event, DoneEvent))
assert done.ensemble_trace is not None
traced = {
candidate["execution"]["model"]: candidate["execution"]["effective_max_tokens"]
for candidate in done.ensemble_trace["candidates"]
}
assert traced["moonshotai/kimi-k2.7-code"] == 16384
assert done.ensemble_trace["final_request"]["execution"]["effective_max_tokens"] == 123
@pytest.mark.asyncio
async def test_ensemble_uses_fallback_when_too_few_proposers_succeed(
monkeypatch: pytest.MonkeyPatch,
) -> None:
registry = _FakeRegistry(
{
"p1": _FakePlan(
[
TextDeltaEvent(text="draft one"),
DoneEvent(input_tokens=1, output_tokens=2, model="p1"),
]
),
"p2": _FakePlan([ErrorEvent(message="nope", code="boom")]),
}
)
monkeypatch.setattr("opensquilla.provider.ensemble._build_provider", registry.provider_for)
class _FallbackProvider:
provider_name = "fallback"
def chat(
self,
messages: list[Message],
tools: list[ToolDefinition] | None = None,
config: ChatConfig | None = None,
) -> AsyncIterator[StreamEvent]:
async def _stream() -> AsyncIterator[StreamEvent]:
yield TextDeltaEvent(text="single")
yield DoneEvent(input_tokens=7, output_tokens=8, model="single")
return _stream()
async def list_models(self) -> list[Any]:
return []
provider = EnsembleProvider(
profile_name="default",
proposers=[_member("p1"), _member("p2")],
aggregator=_member("agg"),
fallback_provider=_FallbackProvider(),
min_successful_proposers=2,
proposer_timeout_seconds=1,
aggregator_timeout_seconds=1,
shuffle_candidates=False,
)
events = await _collect(provider)
assert [call["model"] for call in registry.calls] == ["p1", "p2"]
assert any(isinstance(event, TextDeltaEvent) and event.text == "single" for event in events)
done = next(event for event in events if isinstance(event, DoneEvent))
assert done.input_tokens == 8
assert done.output_tokens == 10
assert done.model_usage_breakdown[-1]["role"] == "fallback_single"
assert done.ensemble_trace is not None
assert done.ensemble_trace["fallback_used"] is True
assert "requires 2" in done.ensemble_trace["fallback_reason"]
assert done.ensemble_trace["final_request"]["role"] == "fallback_single"
assert done.ensemble_trace["final_request"]["output"]["text"] == "single"
assert done.ensemble_trace["final_request"]["usage"]["model"] == "single"
@pytest.mark.asyncio
async def test_openrouter_members_get_member_specific_reasoning_capabilities(
monkeypatch: pytest.MonkeyPatch,
) -> None:
registry = _FakeRegistry(
{
"z-ai/glm-5.2": _FakePlan(
[TextDeltaEvent(text="draft"), DoneEvent(model="z-ai/glm-5.2")]
),
"qwen/qwen3.7-plus": _FakePlan(
[TextDeltaEvent(text="final"), DoneEvent(model="qwen/qwen3.7-plus")]
),
}
)
monkeypatch.setattr("opensquilla.provider.ensemble._build_provider", registry.provider_for)
provider = EnsembleProvider(
profile_name="default",
proposers=[_openrouter_member("z-ai/glm-5.2")],
aggregator=_openrouter_member("qwen/qwen3.7-plus"),
proposer_timeout_seconds=1,
aggregator_timeout_seconds=1,
shuffle_candidates=False,
)
await _collect(provider)
proposer_cfg = registry.calls[0]["config"]
aggregator_cfg = registry.calls[1]["config"]
assert proposer_cfg.thinking is True
assert proposer_cfg.thinking_level == "high"
assert proposer_cfg.model_capabilities.supports_reasoning is True
assert proposer_cfg.model_capabilities.reasoning_format == "openrouter"
assert aggregator_cfg.thinking is True
assert aggregator_cfg.thinking_level == "high"
assert aggregator_cfg.model_capabilities.supports_reasoning is True
assert aggregator_cfg.model_capabilities.reasoning_format == "openrouter"
@pytest.mark.asyncio
async def test_ensemble_emits_proposer_progress_events(
monkeypatch: pytest.MonkeyPatch,
) -> None:
registry = _FakeRegistry(
{
"p1": _FakePlan(
[TextDeltaEvent(text="d1"), DoneEvent(input_tokens=1, output_tokens=2, model="p1")]
),
"p2": _FakePlan(
[TextDeltaEvent(text="d2"), DoneEvent(input_tokens=3, output_tokens=4, model="p2")]
),
"agg": _FakePlan(
[TextDeltaEvent(text="f"), DoneEvent(input_tokens=5, output_tokens=6, model="agg")]
),
}
)
monkeypatch.setattr("opensquilla.provider.ensemble._build_provider", registry.provider_for)
provider = EnsembleProvider(
profile_name="default",
proposers=[_member("p1"), _member("p2")],
aggregator=_member("agg"),
proposer_timeout_seconds=1,
aggregator_timeout_seconds=1,
shuffle_candidates=False,
)
events = await _collect(provider)
progress = [event for event in events if isinstance(event, EnsembleProgressEvent)]
# Each proposer announces a start and a finish so the UI can reveal it live.
starts = {p.proposer_model for p in progress if p.event_type == "proposer_start"}
finishes = {p.proposer_model for p in progress if p.event_type == "proposer_finish"}
assert starts == {"p1", "p2"}
assert finishes == {"p1", "p2"}
# The finish delta carries the proposer's usage/cost so the UI can render
# per-member tokens live (not just at the terminal breakdown).
p1_finish = next(
p
for p in progress
if p.event_type == "proposer_finish" and p.proposer_model == "p1"
)
assert p1_finish.input_tokens == 1
assert p1_finish.output_tokens == 2
# Progress is delivered before the terminal DoneEvent that carries the breakdown.
last_progress = max(i for i, e in enumerate(events) if isinstance(e, EnsembleProgressEvent))
done_index = max(i for i, e in enumerate(events) if isinstance(e, DoneEvent))
assert last_progress < done_index
@pytest.mark.asyncio
async def test_ensemble_streams_proposer_progress_live_not_buffered(
monkeypatch: pytest.MonkeyPatch,
) -> None:
# p2 blocks until `gate` is set. The consumer sets the gate only AFTER it has
# received p1's proposer_finish from the LIVE stream. If progress were buffered
# until gather() completed, p1's finish would never surface (p2 stays blocked,
# gather never returns) → deadlock. Live streaming completes within the timeout.
gate = asyncio.Event()
registry = _FakeRegistry(
{
"p1": _FakePlan([DoneEvent(input_tokens=1, output_tokens=1, model="p1")]),
"p2": _FakePlan([DoneEvent(input_tokens=1, output_tokens=1, model="p2")], gate=gate),
"agg": _FakePlan(
[TextDeltaEvent(text="f"), DoneEvent(input_tokens=1, output_tokens=1, model="agg")]
),
}
)
monkeypatch.setattr("opensquilla.provider.ensemble._build_provider", registry.provider_for)
provider = EnsembleProvider(
profile_name="default",
proposers=[_member("p1"), _member("p2")],
aggregator=_member("agg"),
proposer_timeout_seconds=2,
aggregator_timeout_seconds=2,
shuffle_candidates=False,
)
async def consume() -> list[StreamEvent]:
collected: list[StreamEvent] = []
async for event in provider.chat(
[Message(role="user", content="q")],
config=ChatConfig(max_tokens=8, thinking=False),
):
collected.append(event)
if (
isinstance(event, EnsembleProgressEvent)
and event.event_type == "proposer_finish"
and event.proposer_model == "p1"
):
gate.set() # reachable only if p1's finish streamed live
return collected
events = await asyncio.wait_for(consume(), timeout=3.0)
finishes = {
e.proposer_model
for e in events
if isinstance(e, EnsembleProgressEvent) and e.event_type == "proposer_finish"
}
assert finishes == {"p1", "p2"}
@pytest.mark.asyncio
async def test_static_openrouter_b5_quorum_cancels_slow_proposer(
monkeypatch: pytest.MonkeyPatch,
) -> None:
slow_gate = asyncio.Event()
registry = _FakeRegistry(
{
"p1": _FakePlan([TextDeltaEvent(text="d1"), DoneEvent(model="p1")]),
"p2": _FakePlan([TextDeltaEvent(text="d2"), DoneEvent(model="p2")]),
"p3": _FakePlan([TextDeltaEvent(text="d3"), DoneEvent(model="p3")]),
"p4": _FakePlan(
[TextDeltaEvent(text="d4"), DoneEvent(model="p4")],
gate=slow_gate,
),
"agg": _FakePlan(
[
TextDeltaEvent(text="final"),
DoneEvent(input_tokens=1, output_tokens=1, model="agg"),
]
),
}
)
monkeypatch.setattr("opensquilla.provider.ensemble._build_provider", registry.provider_for)
provider = EnsembleProvider(
profile_name="static_openrouter_b5",
proposers=[_member("p1"), _member("p2"), _member("p3"), _member("p4")],
aggregator=_member("agg"),
min_successful_proposers=3,
proposer_timeout_seconds=10,
aggregator_timeout_seconds=1,
quorum_grace_seconds=0.02,
shuffle_candidates=False,
)
started = time.monotonic()
events = await _collect(provider)
elapsed = time.monotonic() - started
assert elapsed < 0.5
assert [call["model"] for call in registry.calls] == ["p1", "p2", "p3", "p4", "agg"]
done = next(event for event in events if isinstance(event, DoneEvent))
assert done.ensemble_trace is not None
assert done.ensemble_trace["successful_proposers"] == 3
assert done.ensemble_trace["selected_candidate_count"] == 3
assert done.ensemble_trace["selected_candidate_indexes"] == [0, 1, 2]
assert done.ensemble_trace["llm_request_count"] == 5
assert done.ensemble_trace["quorum_grace_seconds"] == 0.02
p4 = done.ensemble_trace["candidates"][3]
assert p4["model"] == "p4"
assert p4["ok"] is False
assert p4["error_code"] == "quorum_cancelled"
assert "quorum grace" in p4["error"]
assert "d1" in str(registry.calls[-1]["messages"][-1].content)
assert "d2" in str(registry.calls[-1]["messages"][-1].content)
assert "d3" in str(registry.calls[-1]["messages"][-1].content)
assert "d4" not in str(registry.calls[-1]["messages"][-1].content)
@pytest.mark.asyncio
async def test_default_ensemble_waits_for_all_proposers_without_quorum(
monkeypatch: pytest.MonkeyPatch,
) -> None:
slow_gate = asyncio.Event()
registry = _FakeRegistry(
{
"p1": _FakePlan([TextDeltaEvent(text="d1"), DoneEvent(model="p1")]),
"p2": _FakePlan(
[TextDeltaEvent(text="d2"), DoneEvent(model="p2")],
gate=slow_gate,
),
"agg": _FakePlan([TextDeltaEvent(text="final"), DoneEvent(model="agg")]),
}
)
monkeypatch.setattr("opensquilla.provider.ensemble._build_provider", registry.provider_for)
provider = EnsembleProvider(
profile_name="router_dynamic/c1",
proposers=[_member("p1"), _member("p2")],
aggregator=_member("agg"),
min_successful_proposers=1,
proposer_timeout_seconds=2,
aggregator_timeout_seconds=1,
quorum_grace_seconds=0.0,
shuffle_candidates=False,
)
consume_task = asyncio.create_task(_collect(provider))
await asyncio.sleep(0.05)
assert "agg" not in [call["model"] for call in registry.calls]
slow_gate.set()
events = await asyncio.wait_for(consume_task, timeout=1.0)
assert [call["model"] for call in registry.calls] == ["p1", "p2", "agg"]
done = next(event for event in events if isinstance(event, DoneEvent))
assert done.ensemble_trace is not None
assert done.ensemble_trace["successful_proposers"] == 2
assert done.ensemble_trace["quorum_grace_seconds"] == 0.0
def test_runtime_wrap_is_after_selector_resolution() -> None:
import inspect
from opensquilla.engine.runtime import TurnRunner
source = inspect.getsource(TurnRunner._run_pipeline)
resolve_index = source.index("provider = apply_model_override(")
wrap_index = source.index("build_ensemble_provider_from_config")
assert wrap_index > resolve_index
assert "routed_model_before_ensemble" in source
assert "current_provider_config" in source
@pytest.mark.asyncio
async def test_runtime_normalizes_provider_ensemble_progress_event() -> None:
from opensquilla.engine.runtime import _SelectorFallbackProvider
from opensquilla.engine.types import EnsembleProgressEvent as EngineEnsembleProgressEvent
class _Provider:
provider_name = "openrouter"
def chat(
self,
messages: list[Any],
tools: Any = None,
config: Any = None,
) -> AsyncIterator[StreamEvent]:
return self._chat(messages, tools=tools, config=config)
async def _chat(
self,
messages: list[Any],
*,
tools: Any = None,
config: Any = None,
) -> AsyncIterator[StreamEvent]:
yield EnsembleProgressEvent(
event_type="proposer_start",
proposer_index=2,
proposer_label="proposer_3",
proposer_model="qwen/qwen3.7-max",
proposer_provider="openrouter",
sample_index=0,
elapsed_ms=123,
input_tokens=11,
output_tokens=22,
cost_usd=0.003,
error="",
)
yield DoneEvent(model="qwen/qwen3.7-max")
async def list_models(self) -> list[Any]:
return []
class _Selector:
current_config = ProviderConfig(provider="openrouter", model="qwen/qwen3.7-max")
provider = _SelectorFallbackProvider(_Provider(), _Selector())
events = [event async for event in provider.chat([])]
assert isinstance(events[0], EngineEnsembleProgressEvent)
assert events[0].event_type == "proposer_start"
assert events[0].proposer_index == 2
assert events[0].proposer_label == "proposer_3"
assert events[0].proposer_model == "qwen/qwen3.7-max"
assert events[0].proposer_provider == "openrouter"
assert events[0].sample_index == 0
assert events[0].elapsed_ms == 123
assert events[0].input_tokens == 11
assert events[0].output_tokens == 22
assert events[0].cost_usd == 0.003
assert events[0].error == ""
def _static_b5_gateway_config() -> Any:
from opensquilla.gateway.config import GatewayConfig
return GatewayConfig(
llm_ensemble={"enabled": True, "selection_mode": "static_openrouter_b5"},
)
def test_static_b5_credential_unavailable_for_keyless_non_openrouter_provider(
monkeypatch: pytest.MonkeyPatch,
) -> None:
from opensquilla.provider.ensemble import static_b5_credential_available
monkeypatch.delenv("OPENROUTER_API_KEY", raising=False)
inherited = ProviderConfig(provider="groq", model="m", api_key="sk-groq-synthetic")
assert static_b5_credential_available(_static_b5_gateway_config(), inherited) is (
False
)
def test_static_b5_credential_env_key_is_an_opt_in_for_other_providers(
monkeypatch: pytest.MonkeyPatch,
) -> None:
from opensquilla.provider.ensemble import static_b5_credential_available
monkeypatch.setenv("OPENROUTER_API_KEY", "sk-or-synthetic")
inherited = ProviderConfig(provider="groq", model="m", api_key="sk-groq-synthetic")
assert static_b5_credential_available(_static_b5_gateway_config(), inherited) is (
True
)
def test_static_b5_credential_resolves_from_inherited_openrouter_key(
monkeypatch: pytest.MonkeyPatch,
) -> None:
from opensquilla.provider.ensemble import static_b5_credential_available
monkeypatch.delenv("OPENROUTER_API_KEY", raising=False)
inherited = ProviderConfig(provider="openrouter", model="m", api_key="sk-or-synthetic")
assert static_b5_credential_available(_static_b5_gateway_config(), inherited) is (
True
)
def test_static_b5_credential_unavailable_for_keyless_openrouter_provider(
monkeypatch: pytest.MonkeyPatch,
) -> None:
from opensquilla.provider.ensemble import static_b5_credential_available
monkeypatch.delenv("OPENROUTER_API_KEY", raising=False)
inherited = ProviderConfig(provider="openrouter", model="m", api_key="")
assert static_b5_credential_available(_static_b5_gateway_config(), inherited) is (
False
)
def test_static_b5_credential_accepts_non_selector_provider_config_shapes(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""The gateway floor/doctor call sites pass ``config.llm`` (no org_id field)."""
from opensquilla.gateway.config import LlmProviderConfig
from opensquilla.provider.ensemble import static_b5_credential_available
monkeypatch.delenv("OPENROUTER_API_KEY", raising=False)
config = _static_b5_gateway_config()
keyless = LlmProviderConfig(provider="groq", model="m", api_key="sk-groq-synthetic")
assert static_b5_credential_available(config, keyless) is False
keyed = LlmProviderConfig(provider="openrouter", model="m", api_key="sk-or-synthetic")
assert static_b5_credential_available(config, keyed) is True
def test_static_tokenrhythm_b5_credential_resolution(
monkeypatch: pytest.MonkeyPatch,
) -> None:
from opensquilla.gateway.config import GatewayConfig
from opensquilla.provider.ensemble import static_b5_credential_available
config = GatewayConfig(
llm_ensemble={"enabled": True, "selection_mode": "static_tokenrhythm_b5"},
)
mode = "static_tokenrhythm_b5"
# Inherited tokenrhythm key satisfies the profile.
monkeypatch.delenv("TOKENRHYTHM_API_KEY", raising=False)
inherited = ProviderConfig(provider="tokenrhythm", model="m", api_key="sk-tr-synthetic")
assert static_b5_credential_available(config, inherited, mode) is True
# An OpenRouter key never satisfies the tokenrhythm profile.
monkeypatch.setenv("OPENROUTER_API_KEY", "sk-or-synthetic")
keyless = ProviderConfig(provider="groq", model="m", api_key="sk-groq-synthetic")
assert static_b5_credential_available(config, keyless, mode) is False
# The registry env key is an opt-in for other active providers.
monkeypatch.setenv("TOKENRHYTHM_API_KEY", "sk-tr-synthetic")
assert static_b5_credential_available(config, keyless, mode) is True
# Unknown selection modes resolve to no credential.
assert static_b5_credential_available(config, inherited, "static_unknown_b5") is False
def test_static_b5_credential_gate_agrees_with_config_side_floor_gate(
monkeypatch: pytest.MonkeyPatch,
) -> None:
from opensquilla.gateway.config import (
GatewayConfig,
static_b5_ensemble_active,
static_b5_ensemble_enabled,
)
from opensquilla.provider.ensemble import static_b5_credential_available
monkeypatch.delenv("OPENROUTER_API_KEY", raising=False)
configs = [
GatewayConfig(llm={"provider": "groq", "api_key": "sk-groq-synthetic"}),
GatewayConfig(llm={"provider": "openrouter", "api_key": "sk-or-synthetic"}),
GatewayConfig(llm={"provider": "openrouter", "api_key": ""}),
GatewayConfig(
llm={"provider": "groq", "api_key": ""},
llm_ensemble={"enabled": True, "selection_mode": "router_dynamic"},
),
]
for config in configs:
selection_mode = str(config.llm_ensemble.selection_mode or "")
expected = static_b5_ensemble_enabled(config) and static_b5_credential_available(
config, config.llm, selection_mode
)
assert static_b5_ensemble_active(config) is expected