mirror of
https://github.com/rennf93/roboco.git
synced 2026-08-03 07:23:24 +02:00
* feat(kimi): Kimi K3 provider on the official kimi-code CLI (Wave 1) ModelProvider.KIMI routes through KimiCliProvider driving Moonshot's kimi CLI on a Kimi subscription (OAuth device-code, no metered key). One-shot delivery roles only (V1), interactive ban wired in both guard lists. Auth: one shared RW auth mount; containers symlink credentials/ and oauth/ (the CLI's cross-process refresh-lock dir) into a container-local KIMI_CODE_HOME so every container and the host redeem the SAME rotating refresh chain - live-verified that per-copy chains cross-invalidate after the reuse-grace window. No orchestrator refresh daemon; an expires_at preflight exits 78. Config renderer mirrors the login-managed provider/model blocks field-for-field (live-captured; the model value is the CLI-side name, never the raw API id), plus per-role deny rules and the bash-guard as a PreToolUse hook via a wrapper script (an env key on a hooks entry makes the CLI silently drop ALL hooks - live-verified). Usage capture sums wire.jsonl usage.record 4-bucket events; sniff classifies rate-limit/auth from structured error text only, mapped to the shared 75/78 park contract. Image installs the CLI latest-at-build (no version pin, by policy) with the resolved version stamped as provenance, binary split to /usr/local away from mutable state. Migrations 090 (enum) + 091 (provider seed); catalog, pricing, routing mode, and orchestrator park/usage wiring mirror the codex integration. * feat(kimi): surface sweep + fleet-wide pin drop (Wave 2) Compose x3 gain the agent-kimi-image service and the orchestrator's read-write ~/.kimi-code mount + kimi-usage dir; .env.example documents the Kimi block. Panel mirrors ModelProvider.KIMI and adds the kimi routing mode (catalog filter, mode button, mix-picker group, badge) with tests; provider routes gain the kimi remediation entry. CLAUDE.md and docs/map document the runtime. Per the no-pins policy, agent-grok/ gemini/codex Dockerfiles drop their version pins for latest-at-build with resolved-version provenance stamps (grok resolves 0.2.112 vs the old 0.2.56 pin - verified by real builds of all four images). --------- Co-authored-by: Renn F <rennf93@users.noreply.github.com>
187 lines
6.5 KiB
Python
187 lines
6.5 KiB
Python
"""kimi_cli_sniff — classify a Kimi run from ONLY its machine-relevant text.
|
|
|
|
The structural guarantee under test: the model's own on-topic prose (which
|
|
can legitimately contain the words "quota-limited" or a "429"/"401" substring
|
|
inside a commit hash / id) must NEVER reach the classifier, because
|
|
extraction only pulls a structured ``error`` field off error-bearing JSONL
|
|
events plus raw stderr — never ``role: assistant`` / ``role: tool`` content.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
from typing import TYPE_CHECKING
|
|
|
|
from roboco.llm.providers import kimi_cli_sniff as sniff
|
|
|
|
if TYPE_CHECKING:
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
|
|
def _write_jsonl(path: Path, lines: list[str]) -> None:
|
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
path.write_text("\n".join(lines) + "\n", encoding="utf-8")
|
|
|
|
|
|
def _error_event(message: str) -> str:
|
|
return json.dumps({"type": "error", "error": {"message": message}})
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# extract_error_text — structural isolation
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_extract_error_text_pulls_only_structured_error_field(tmp_path: Path) -> None:
|
|
log = tmp_path / "run.jsonl"
|
|
_write_jsonl(
|
|
log,
|
|
[
|
|
json.dumps(
|
|
{
|
|
"role": "assistant",
|
|
"content": "the quota-limited rollout ships this sprint",
|
|
}
|
|
),
|
|
_error_event("real error text"),
|
|
],
|
|
)
|
|
assert sniff.extract_error_text(log) == "real error text"
|
|
|
|
|
|
def test_extract_error_text_accepts_bare_string_error() -> None:
|
|
assert sniff._error_text_from_event({"error": "bare string error"}) == (
|
|
"bare string error"
|
|
)
|
|
|
|
|
|
def test_extract_error_text_empty_for_missing_or_error_less_log(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
assert sniff.extract_error_text(tmp_path / "nope.jsonl") == ""
|
|
log = tmp_path / "run.jsonl"
|
|
_write_jsonl(log, [json.dumps({"role": "assistant", "content": "hi"})])
|
|
assert sniff.extract_error_text(log) == ""
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# The false-positive class this module exists to kill
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_benign_transcript_never_false_parks(tmp_path: Path) -> None:
|
|
"""A transcript whose ONLY content is benign on-topic prose — mentioning
|
|
"quota-limited" work and a commit hash containing "429"/"401" — must
|
|
classify as "" (no park), because none of it lives in a structured error
|
|
field the extractor even looks at."""
|
|
log = tmp_path / "run.jsonl"
|
|
_write_jsonl(
|
|
log,
|
|
[
|
|
json.dumps(
|
|
{
|
|
"role": "assistant",
|
|
"content": (
|
|
"Fixed the quota-limited rollout gate. Committed as "
|
|
"abc4291f, also touched item 40199."
|
|
),
|
|
}
|
|
),
|
|
json.dumps({"role": "tool", "tool_call_id": "1", "content": "ok"}),
|
|
],
|
|
)
|
|
err_log = tmp_path / "run.err"
|
|
err_log.write_text("", encoding="utf-8")
|
|
assert sniff.classify(log, err_log) == ""
|
|
|
|
|
|
def test_word_boundary_prevents_429_substring_false_positive() -> None:
|
|
assert not sniff.is_rate_limited("commit abc14293 deployed to prod")
|
|
assert not sniff.is_rate_limited("fix4297abc landed")
|
|
|
|
|
|
def test_word_boundary_prevents_401_substring_false_positive() -> None:
|
|
assert not sniff.is_auth_failure("item 40199 was resolved")
|
|
assert not sniff.is_auth_failure("ticket 14012 closed")
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# True positives — the live-verified error text shapes from the spike
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_status_code_429_classifies_rate_limit(tmp_path: Path) -> None:
|
|
log = tmp_path / "run.jsonl"
|
|
_write_jsonl(log, [_error_event("request failed with status code: 429")])
|
|
assert sniff.classify(log) == "rate_limit"
|
|
|
|
|
|
def test_engine_overloaded_classifies_rate_limit(tmp_path: Path) -> None:
|
|
log = tmp_path / "run.jsonl"
|
|
_write_jsonl(log, [_error_event("the engine is currently overloaded")])
|
|
assert sniff.classify(log) == "rate_limit"
|
|
|
|
|
|
def test_usage_limit_for_period_classifies_rate_limit(tmp_path: Path) -> None:
|
|
log = tmp_path / "run.jsonl"
|
|
_write_jsonl(log, [_error_event("usage limit for this period exceeded")])
|
|
assert sniff.classify(log) == "rate_limit"
|
|
|
|
|
|
def test_usage_limit_for_billing_cycle_classifies_rate_limit(tmp_path: Path) -> None:
|
|
log = tmp_path / "run.jsonl"
|
|
_write_jsonl(log, [_error_event("usage limit for this billing cycle reached")])
|
|
assert sniff.classify(log) == "rate_limit"
|
|
|
|
|
|
def test_api_key_invalid_classifies_auth(tmp_path: Path) -> None:
|
|
log = tmp_path / "run.jsonl"
|
|
_write_jsonl(log, [_error_event("API Key appears to be invalid")])
|
|
assert sniff.classify(log) == "auth"
|
|
|
|
|
|
def test_membership_benefits_classifies_auth(tmp_path: Path) -> None:
|
|
log = tmp_path / "run.jsonl"
|
|
_write_jsonl(
|
|
log,
|
|
[_error_event("We're unable to verify your membership benefits at this time.")],
|
|
)
|
|
assert sniff.classify(log) == "auth"
|
|
|
|
|
|
def test_classify_reads_stderr_too(tmp_path: Path) -> None:
|
|
log = tmp_path / "run.jsonl"
|
|
_write_jsonl(log, [json.dumps({"role": "assistant", "content": "ok"})])
|
|
err_log = tmp_path / "run.err"
|
|
err_log.write_text("fatal: status code: 429\n", encoding="utf-8")
|
|
assert sniff.classify(log, err_log) == "rate_limit"
|
|
|
|
|
|
def test_classify_missing_files_returns_empty(tmp_path: Path) -> None:
|
|
assert sniff.classify(tmp_path / "nope.jsonl", tmp_path / "nope.err") == ""
|
|
|
|
|
|
def test_rate_limit_checked_before_auth_when_both_present(tmp_path: Path) -> None:
|
|
log = tmp_path / "run.jsonl"
|
|
_write_jsonl(
|
|
log,
|
|
[_error_event("status code: 429, and API Key appears to be invalid too")],
|
|
)
|
|
assert sniff.classify(log) == "rate_limit"
|
|
|
|
|
|
def test_main_cli_prints_classification(
|
|
tmp_path: Path, capsys: pytest.CaptureFixture[str]
|
|
) -> None:
|
|
log = tmp_path / "run.jsonl"
|
|
_write_jsonl(log, [_error_event("status code: 429")])
|
|
assert sniff.main([str(log)]) == 0
|
|
assert capsys.readouterr().out.strip() == "rate_limit"
|
|
|
|
|
|
def test_main_cli_no_args_prints_empty(capsys: pytest.CaptureFixture[str]) -> None:
|
|
assert sniff.main([]) == 0
|
|
assert capsys.readouterr().out.strip() == ""
|