Every failed auxiliary model_route row read error_class=unknown because the aux logical scope only carried outcome/model/provider and the projection hard-coded unknown for any non-success. Every exception escaping call_llm, including Hermes' own aborts (Ctrl+C / /stop mid-compression, interrupted aux streams, task cancellation), was recorded as failed. - classify the error that ended an aux call (and the last one a successful call recovered from) with agent.error_classifier; HTTP-200 bodies that carry a provider error object are classified from that object - Hermes aborts (non-Exception BaseExceptions, InterruptedError) close the call as cancelled/none, matching the contract - MoA aggregator streams report the same way - aux calls that finish under the turn's own live scopes (title generation runs beside the turn) were never counted: the scope could not be popped and the turn-close drain closed it as an orphan without a route. The drain now closes it with the stored real result.
649 lines
21 KiB
Python
649 lines
21 KiB
Python
from types import SimpleNamespace
|
|
|
|
import pytest
|
|
|
|
pytest.importorskip("nemo_relay")
|
|
|
|
from agent import auxiliary_client, relay_llm, relay_runtime
|
|
from hermes_cli.observability.shared_metrics import SharedMetricsStore
|
|
from hermes_cli.observability.shared_metrics_contract import MODEL_ROUTE_METRIC
|
|
from hermes_cli.observability.shared_metrics_subscriber import SharedMetricsSubscriber
|
|
|
|
|
|
@pytest.fixture()
|
|
def relay_turn(tmp_path, monkeypatch):
|
|
monkeypatch.setenv("HERMES_HOME", str(tmp_path / "profile"))
|
|
relay_runtime._reset_for_tests()
|
|
lease = relay_runtime.SESSION_COORDINATOR.acquire_conversation(
|
|
profile_key=relay_runtime.current_profile_key(),
|
|
session_id="session-1",
|
|
platform="cli",
|
|
)
|
|
turn = relay_runtime.SESSION_COORDINATOR.begin_turn(
|
|
lease,
|
|
turn_id="turn-1",
|
|
task_id="task-1",
|
|
)
|
|
try:
|
|
yield lease.host.relay, turn
|
|
finally:
|
|
relay_runtime.SESSION_COORDINATOR.end_turn(turn, outcome="success")
|
|
relay_runtime.SESSION_COORDINATOR.release_conversation(lease)
|
|
relay_runtime._reset_for_tests()
|
|
|
|
|
|
def test_auxiliary_retries_share_logical_relay_identity(monkeypatch):
|
|
attempts = []
|
|
logical_completions = []
|
|
responses = iter([
|
|
SimpleNamespace(choices=[]),
|
|
SimpleNamespace(
|
|
choices=[SimpleNamespace(message=SimpleNamespace(content="ok"))]
|
|
),
|
|
])
|
|
client = SimpleNamespace(
|
|
chat=SimpleNamespace(
|
|
completions=SimpleNamespace(
|
|
create=lambda **_kwargs: next(responses),
|
|
)
|
|
)
|
|
)
|
|
|
|
def execute_current(request, callback, **kwargs):
|
|
attempts.append(kwargs)
|
|
return callback(request)
|
|
|
|
monkeypatch.setattr(relay_llm, "execute_current", execute_current)
|
|
monkeypatch.setattr(
|
|
relay_llm,
|
|
"complete_logical_call",
|
|
lambda request_id, *, outcome, model_name, provider_name, response_model_name, error_class: (
|
|
logical_completions.append(
|
|
(request_id, outcome, model_name, provider_name, response_model_name, error_class)
|
|
)
|
|
),
|
|
)
|
|
|
|
@auxiliary_client._relay_auxiliary_call
|
|
def run(task):
|
|
auxiliary_client._set_relay_auxiliary_route(
|
|
"openrouter",
|
|
"test-model",
|
|
"chat_completions",
|
|
)
|
|
with pytest.raises(RuntimeError, match="invalid response"):
|
|
auxiliary_client._validate_llm_response(
|
|
auxiliary_client._relay_sync_completion(
|
|
client,
|
|
{"model": "test-model", "messages": []},
|
|
),
|
|
task,
|
|
)
|
|
return auxiliary_client._validate_llm_response(
|
|
auxiliary_client._relay_sync_completion(
|
|
client,
|
|
{"model": "test-model", "messages": []},
|
|
),
|
|
task,
|
|
)
|
|
|
|
result = run("compression")
|
|
|
|
assert result.choices[0].message.content == "ok"
|
|
assert attempts[0]["metadata"]["api_request_id"] == (
|
|
attempts[1]["metadata"]["api_request_id"]
|
|
)
|
|
assert [attempt["metadata"]["retry_count"] for attempt in attempts] == [0, 1]
|
|
assert attempts[0]["metadata"]["call_role"] == "auxiliary:compression"
|
|
assert all(attempt["defer_logical_completion"] is True for attempt in attempts)
|
|
assert logical_completions == [
|
|
(
|
|
attempts[0]["metadata"]["api_request_id"],
|
|
"success",
|
|
"test-model",
|
|
"openrouter",
|
|
None,
|
|
"unknown", # recovered from the unclassifiable invalid first response
|
|
)
|
|
]
|
|
|
|
|
|
def test_auxiliary_provider_fallback_closes_one_real_logical_call(
|
|
relay_turn,
|
|
monkeypatch,
|
|
):
|
|
relay, turn = relay_turn
|
|
consumer = "test.auxiliary-provider-fallback"
|
|
turn.lease.host.retain_managed_execution(consumer)
|
|
logical_outputs = []
|
|
original_pop = relay.scope.pop
|
|
|
|
def record_pop(*args, **kwargs):
|
|
logical_outputs.append(kwargs.get("output") or {})
|
|
return original_pop(*args, **kwargs)
|
|
|
|
monkeypatch.setattr(relay.scope, "pop", record_pop)
|
|
responses = iter([
|
|
SimpleNamespace(choices=[]),
|
|
SimpleNamespace(
|
|
choices=[SimpleNamespace(message=SimpleNamespace(content="recovered"))]
|
|
),
|
|
])
|
|
client = SimpleNamespace(
|
|
chat=SimpleNamespace(
|
|
completions=SimpleNamespace(
|
|
create=lambda **_kwargs: next(responses),
|
|
)
|
|
)
|
|
)
|
|
|
|
@auxiliary_client._relay_auxiliary_call
|
|
def run(task):
|
|
auxiliary_client._set_relay_auxiliary_route(
|
|
"nvidia",
|
|
"nvidia/test-model",
|
|
"chat_completions",
|
|
)
|
|
with pytest.raises(RuntimeError, match="invalid response"):
|
|
auxiliary_client._validate_llm_response(
|
|
auxiliary_client._relay_sync_completion(
|
|
client,
|
|
{"model": "nvidia/test-model", "messages": []},
|
|
),
|
|
task,
|
|
)
|
|
assert len(turn.logical_llm_calls) == 1
|
|
|
|
auxiliary_client._set_relay_auxiliary_route(
|
|
"openrouter",
|
|
"openrouter/test-model",
|
|
"chat_completions",
|
|
)
|
|
return auxiliary_client._validate_llm_response(
|
|
auxiliary_client._relay_sync_completion(
|
|
client,
|
|
{"model": "openrouter/test-model", "messages": []},
|
|
),
|
|
task,
|
|
)
|
|
|
|
try:
|
|
result = run("compression")
|
|
finally:
|
|
turn.lease.host.release_managed_execution(consumer)
|
|
|
|
assert result.choices[0].message.content == "recovered"
|
|
assert turn.logical_llm_calls == {}
|
|
assert logical_outputs == [
|
|
{
|
|
"error_class": "unknown",
|
|
"model": "openrouter/test-model",
|
|
"outcome": "success",
|
|
"provider": "openrouter",
|
|
}
|
|
]
|
|
|
|
|
|
def test_auxiliary_provider_fallback_records_one_terminal_model_route(
|
|
relay_turn,
|
|
tmp_path,
|
|
):
|
|
relay, turn = relay_turn
|
|
store = SharedMetricsStore(
|
|
tmp_path / "metrics.sqlite3",
|
|
tmp_path / "outbox",
|
|
)
|
|
subscriber = SharedMetricsSubscriber(
|
|
store,
|
|
"test-version",
|
|
runtime_id=turn.lease.host.runtime_id,
|
|
)
|
|
subscriber_name = "test.auxiliary-model-route"
|
|
relay.subscribers.register(subscriber_name, subscriber)
|
|
turn.lease.host.retain_managed_execution(subscriber_name)
|
|
responses = iter([
|
|
SimpleNamespace(model="failed/model", choices=[]),
|
|
SimpleNamespace(
|
|
model="Accepted/Model",
|
|
choices=[SimpleNamespace(message=SimpleNamespace(content="recovered"))],
|
|
),
|
|
])
|
|
client = SimpleNamespace(
|
|
chat=SimpleNamespace(
|
|
completions=SimpleNamespace(
|
|
create=lambda **_kwargs: next(responses),
|
|
)
|
|
)
|
|
)
|
|
|
|
@auxiliary_client._relay_auxiliary_call
|
|
def run(task):
|
|
auxiliary_client._set_relay_auxiliary_route(
|
|
"nvidia",
|
|
"failed/configured-model",
|
|
"chat_completions",
|
|
)
|
|
with pytest.raises(RuntimeError, match="invalid response"):
|
|
auxiliary_client._validate_llm_response(
|
|
auxiliary_client._relay_sync_completion(
|
|
client,
|
|
{"model": "failed/configured-model", "messages": []},
|
|
),
|
|
task,
|
|
)
|
|
auxiliary_client._set_relay_auxiliary_route(
|
|
"OpenRouter",
|
|
"fallback/configured-model",
|
|
"chat_completions",
|
|
)
|
|
return auxiliary_client._validate_llm_response(
|
|
auxiliary_client._relay_sync_completion(
|
|
client,
|
|
{"model": "fallback/configured-model", "messages": []},
|
|
),
|
|
task,
|
|
)
|
|
|
|
try:
|
|
result = run("compression")
|
|
relay.subscribers.flush()
|
|
finally:
|
|
turn.lease.host.release_managed_execution(subscriber_name)
|
|
relay.subscribers.deregister(subscriber_name)
|
|
|
|
assert result.choices[0].message.content == "recovered"
|
|
snapshot = store.counter_snapshot()
|
|
assert len(snapshot) == 1
|
|
assert snapshot[0]["metric_name"] == MODEL_ROUTE_METRIC
|
|
assert snapshot[0]["resource"]["hermes_version"] == "test-version"
|
|
assert snapshot[0]["dimensions"] == {
|
|
"call_role": "auxiliary",
|
|
# The success keeps the error it recovered from; an invalid shape classifies as unknown.
|
|
"error_class": "unknown",
|
|
"model": "accepted/model",
|
|
"outcome": "success",
|
|
"provider": "openrouter",
|
|
"ttft_bucket": "unknown",
|
|
}
|
|
assert snapshot[0]["value"] == 1
|
|
assert snapshot[0]["packaged_value"] == 0
|
|
|
|
|
|
class _ProviderStatusError(Exception):
|
|
def __init__(self, status_code, message):
|
|
super().__init__(message)
|
|
self.status_code = status_code
|
|
|
|
|
|
def _aux_route_rows(relay_turn, tmp_path, run):
|
|
"""Run ``run`` inside the turn with a real shared-metrics subscriber; the stored route rows."""
|
|
relay, turn = relay_turn
|
|
store = SharedMetricsStore(tmp_path / "metrics.sqlite3", tmp_path / "outbox")
|
|
subscriber = SharedMetricsSubscriber(store, "test-version", runtime_id=turn.lease.host.runtime_id)
|
|
relay.subscribers.register("test.auxiliary-route-rows", subscriber)
|
|
turn.lease.host.retain_managed_execution("test.auxiliary-route-rows")
|
|
try:
|
|
run()
|
|
relay.subscribers.flush()
|
|
finally:
|
|
turn.lease.host.release_managed_execution("test.auxiliary-route-rows")
|
|
relay.subscribers.deregister("test.auxiliary-route-rows")
|
|
return sorted(
|
|
(row["dimensions"]["outcome"], row["dimensions"]["error_class"], row["value"])
|
|
for row in store.counter_snapshot() if row["metric_name"] == MODEL_ROUTE_METRIC
|
|
)
|
|
|
|
|
|
def _aux_attempts(*outcomes):
|
|
"""A client whose successive attempts raise or answer, in order."""
|
|
pending = iter(outcomes)
|
|
|
|
def create(**_kwargs):
|
|
outcome = next(pending)
|
|
if isinstance(outcome, BaseException):
|
|
raise outcome
|
|
return SimpleNamespace(model="m", choices=[SimpleNamespace(message=SimpleNamespace(content=outcome))])
|
|
|
|
return SimpleNamespace(chat=SimpleNamespace(completions=SimpleNamespace(create=create)))
|
|
|
|
|
|
@auxiliary_client._relay_auxiliary_call
|
|
def _aux_call(task, client, attempts):
|
|
auxiliary_client._set_relay_auxiliary_route("openrouter", "m", "chat_completions")
|
|
for _ in range(attempts - 1):
|
|
try:
|
|
return auxiliary_client._validate_llm_response(
|
|
auxiliary_client._relay_sync_completion(client, {"model": "m", "messages": []}), task)
|
|
except Exception:
|
|
continue
|
|
return auxiliary_client._validate_llm_response(
|
|
auxiliary_client._relay_sync_completion(client, {"model": "m", "messages": []}), task)
|
|
|
|
|
|
def test_auxiliary_model_routes_carry_classified_error_class(relay_turn, tmp_path):
|
|
"""A failed aux call reports the classifier's reason for the error that ended it, and a
|
|
recovered one the error it recovered from, never a blanket ``unknown``/``none``."""
|
|
def run():
|
|
_aux_call("title_generation", _aux_attempts(_ProviderStatusError(429, "Rate limit exceeded"), "ok"), 2)
|
|
with pytest.raises(_ProviderStatusError):
|
|
_aux_call("title_generation", _aux_attempts(
|
|
_ProviderStatusError(404, "The model `m` does not exist")), 1)
|
|
|
|
assert _aux_route_rows(relay_turn, tmp_path, run) == [
|
|
("failed", "model_not_found", 1),
|
|
("success", "rate_limit", 1),
|
|
]
|
|
|
|
|
|
@pytest.mark.parametrize("abort", [
|
|
auxiliary_client.AuxiliaryExplicitCancellation(), InterruptedError("aux stream interrupted"), KeyboardInterrupt(),
|
|
])
|
|
def test_aborted_auxiliary_call_is_cancelled_not_failed(relay_turn, tmp_path, abort):
|
|
"""A Hermes abort (/stop, interrupt, Ctrl+C) mid aux call is not a provider failure."""
|
|
def run():
|
|
with pytest.raises(type(abort)):
|
|
_aux_call("compression", _aux_attempts(abort), 1)
|
|
|
|
assert _aux_route_rows(relay_turn, tmp_path, run) == [("cancelled", "none", 1)]
|
|
|
|
|
|
def test_auxiliary_call_finishing_under_the_turns_live_scope_is_counted(relay_turn, tmp_path):
|
|
"""Title generation runs beside the turn, so the turn's own work is usually on top of its
|
|
logical scope when it finishes. Its result must still become the route row."""
|
|
relay, turn = relay_turn
|
|
host, session = turn.lease.host, turn.lease.session
|
|
turn_work = []
|
|
|
|
@auxiliary_client._relay_auxiliary_call
|
|
def title(task):
|
|
auxiliary_client._set_relay_auxiliary_route("openrouter", "m", "chat_completions")
|
|
response = auxiliary_client._relay_sync_completion(_aux_attempts("A title"), {"model": "m", "messages": []})
|
|
turn_work.append(host.run_in_session(
|
|
session, relay.scope.push, "test.turn_work", relay.ScopeType.Function, handle=turn.handle, input={}))
|
|
return auxiliary_client._validate_llm_response(response, task)
|
|
|
|
def run():
|
|
title("title_generation")
|
|
host.run_in_session(session, relay_runtime.pop_relay_scope, relay, turn_work[0], output={})
|
|
relay_runtime.SESSION_COORDINATOR.end_turn(turn, outcome="success")
|
|
|
|
assert _aux_route_rows(relay_turn, tmp_path, run) == [("success", "none", 1)]
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_async_auxiliary_attempt_uses_inherited_relay_adapter(monkeypatch):
|
|
captured = {}
|
|
logical_completions = []
|
|
|
|
async def create(**kwargs):
|
|
return SimpleNamespace(
|
|
request=kwargs,
|
|
choices=[SimpleNamespace(message=SimpleNamespace(content="ok"))],
|
|
)
|
|
|
|
client = SimpleNamespace(
|
|
chat=SimpleNamespace(completions=SimpleNamespace(create=create))
|
|
)
|
|
|
|
async def execute_current_async(request, callback, **kwargs):
|
|
captured.update(kwargs)
|
|
return await callback(request)
|
|
|
|
monkeypatch.setattr(
|
|
relay_llm,
|
|
"execute_current_async",
|
|
execute_current_async,
|
|
)
|
|
monkeypatch.setattr(
|
|
relay_llm,
|
|
"complete_logical_call",
|
|
lambda request_id, *, outcome, model_name, provider_name, response_model_name, error_class: (
|
|
logical_completions.append(
|
|
(request_id, outcome, model_name, provider_name, response_model_name, error_class)
|
|
)
|
|
),
|
|
)
|
|
|
|
@auxiliary_client._relay_auxiliary_call_async
|
|
async def run(task):
|
|
auxiliary_client._set_relay_auxiliary_route(
|
|
"anthropic",
|
|
"claude-test",
|
|
"chat_completions",
|
|
)
|
|
return auxiliary_client._validate_llm_response(
|
|
await auxiliary_client._relay_async_completion(
|
|
client,
|
|
{"model": "claude-test", "messages": []},
|
|
),
|
|
task,
|
|
)
|
|
|
|
result = await run("title_generation")
|
|
|
|
assert result.request["model"] == "claude-test"
|
|
assert captured["name"] == "anthropic"
|
|
assert captured["metadata"]["call_role"] == "auxiliary:title_generation"
|
|
assert captured["defer_logical_completion"] is True
|
|
assert logical_completions == [
|
|
(
|
|
captured["metadata"]["api_request_id"],
|
|
"success",
|
|
"claude-test",
|
|
"anthropic",
|
|
None,
|
|
"none",
|
|
)
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_partial_auxiliary_stream_failure_closes_before_recovery(
|
|
relay_turn, monkeypatch
|
|
):
|
|
_relay, turn = relay_turn
|
|
consumer = "test.partial-auxiliary-stream-failure"
|
|
turn.lease.host.retain_managed_execution(consumer)
|
|
logical_outputs = []
|
|
original_pop = turn.lease.host.relay.scope.pop
|
|
|
|
def record_pop(*args, **kwargs):
|
|
logical_outputs.append(kwargs.get("output") or {})
|
|
return original_pop(*args, **kwargs)
|
|
|
|
monkeypatch.setattr(turn.lease.host.relay.scope, "pop", record_pop)
|
|
|
|
class ProviderError(Exception):
|
|
pass
|
|
|
|
provider_error = ProviderError("stream failed")
|
|
partial_chunk = SimpleNamespace(
|
|
model="test-model",
|
|
choices=[
|
|
SimpleNamespace(
|
|
delta=SimpleNamespace(content="partial", tool_calls=None),
|
|
finish_reason=None,
|
|
)
|
|
],
|
|
usage=None,
|
|
)
|
|
|
|
def partial_stream():
|
|
yield partial_chunk
|
|
raise provider_error
|
|
|
|
stream_client = SimpleNamespace(
|
|
chat=SimpleNamespace(
|
|
completions=SimpleNamespace(
|
|
create=lambda **_kwargs: partial_stream(),
|
|
)
|
|
)
|
|
)
|
|
recovery_client = SimpleNamespace(
|
|
chat=SimpleNamespace(
|
|
completions=SimpleNamespace(
|
|
create=lambda **_kwargs: SimpleNamespace(
|
|
choices=[
|
|
SimpleNamespace(message=SimpleNamespace(content="recovered"))
|
|
]
|
|
),
|
|
)
|
|
)
|
|
)
|
|
|
|
@auxiliary_client._relay_auxiliary_call
|
|
def start_stream(task):
|
|
auxiliary_client._set_relay_auxiliary_route(
|
|
"openrouter",
|
|
"test-model",
|
|
"chat_completions",
|
|
)
|
|
return auxiliary_client._relay_sync_stream(
|
|
stream_client,
|
|
{"model": "test-model", "messages": [], "stream": True},
|
|
)
|
|
|
|
@auxiliary_client._relay_auxiliary_call
|
|
def recover(task):
|
|
auxiliary_client._set_relay_auxiliary_route(
|
|
"openrouter",
|
|
"test-model",
|
|
"chat_completions",
|
|
)
|
|
return auxiliary_client._validate_llm_response(
|
|
auxiliary_client._relay_sync_completion(
|
|
recovery_client,
|
|
{"model": "test-model", "messages": []},
|
|
),
|
|
task,
|
|
)
|
|
|
|
try:
|
|
stream = start_stream("moa")
|
|
assert next(stream) is partial_chunk
|
|
|
|
with pytest.raises(ProviderError) as caught:
|
|
next(stream)
|
|
|
|
assert caught.value is provider_error
|
|
assert logical_outputs == [
|
|
{
|
|
"error_class": "unknown",
|
|
"model": "test-model",
|
|
"outcome": "failed",
|
|
"provider": "openrouter",
|
|
}
|
|
]
|
|
assert turn.logical_llm_calls == {}
|
|
|
|
result = recover("moa")
|
|
|
|
assert result.choices[0].message.content == "recovered"
|
|
assert logical_outputs == [
|
|
{
|
|
"error_class": "unknown",
|
|
"model": "test-model",
|
|
"outcome": "failed",
|
|
"provider": "openrouter",
|
|
},
|
|
{
|
|
"error_class": "none",
|
|
"model": "test-model",
|
|
"outcome": "success",
|
|
"provider": "openrouter",
|
|
},
|
|
]
|
|
assert turn.logical_llm_calls == {}
|
|
finally:
|
|
turn.lease.host.release_managed_execution(consumer)
|
|
def test_auxiliary_stream_unwraps_completed_response(relay_turn):
|
|
"""MoA aggregator on an Anthropic-protocol provider: the client returns a
|
|
completed response for ``stream=True`` (the adapter ignores the flag), so
|
|
``_relay_sync_stream`` must surface it raw for the consumer's
|
|
``hasattr(stream, "choices")`` handling — regression of #11732/#55933 via
|
|
the Relay integration (SimpleNamespace is not iterable)."""
|
|
_relay, _turn = relay_turn
|
|
completed = SimpleNamespace(
|
|
choices=[
|
|
SimpleNamespace(
|
|
message=SimpleNamespace(content="aggregated"),
|
|
finish_reason="stop",
|
|
)
|
|
],
|
|
model="kimi-k3",
|
|
)
|
|
client = SimpleNamespace(
|
|
chat=SimpleNamespace(
|
|
completions=SimpleNamespace(create=lambda **_kwargs: completed)
|
|
)
|
|
)
|
|
|
|
@auxiliary_client._relay_auxiliary_call
|
|
def run(task):
|
|
auxiliary_client._set_relay_auxiliary_route(
|
|
"kimi-coding",
|
|
"kimi-k3",
|
|
"chat_completions",
|
|
)
|
|
return auxiliary_client._relay_sync_stream(
|
|
client,
|
|
{"model": "kimi-k3", "messages": [], "stream": True},
|
|
)
|
|
|
|
assert run("moa_aggregator") is completed
|
|
|
|
|
|
|
|
def test_call_llm_stream_unwraps_completed_response(relay_turn, monkeypatch):
|
|
"""Outermost seam: ``call_llm(stream=True)`` — decorated with
|
|
``@_relay_auxiliary_call`` in production, so the Relay context is always
|
|
set — with an Anthropic-shaped client that ignores ``stream=True`` and
|
|
returns a completed response (the MoA aggregator on kimi-coding /
|
|
MiniMax / ZAI / any /anthropic gateway). Must return the raw response for
|
|
the consumer's ``hasattr(stream, "choices")`` handling, not crash with
|
|
``TypeError: 'types.SimpleNamespace' object is not iterable``."""
|
|
_relay, _turn = relay_turn
|
|
captured = {}
|
|
completed = SimpleNamespace(
|
|
choices=[
|
|
SimpleNamespace(
|
|
message=SimpleNamespace(content="aggregated"),
|
|
finish_reason="stop",
|
|
)
|
|
],
|
|
model="kimi-k3",
|
|
)
|
|
|
|
def fake_create(**kwargs):
|
|
captured.update(kwargs)
|
|
return completed
|
|
|
|
client = SimpleNamespace(
|
|
base_url="https://api.kimi.com/coding/v1",
|
|
chat=SimpleNamespace(completions=SimpleNamespace(create=fake_create)),
|
|
)
|
|
monkeypatch.setattr(
|
|
auxiliary_client,
|
|
"_get_cached_client",
|
|
lambda *args, **kwargs: (client, "kimi-k3"),
|
|
)
|
|
|
|
result = auxiliary_client.call_llm(
|
|
"moa_aggregator",
|
|
provider="kimi-coding",
|
|
model="kimi-k3",
|
|
api_key="sk-test",
|
|
messages=[{"role": "user", "content": "q"}],
|
|
stream=True,
|
|
stream_options={"include_usage": True},
|
|
)
|
|
|
|
assert result is completed
|
|
assert captured["stream"] is True
|
|
assert captured["stream_options"] == {"include_usage": True}
|