Files
opensquilla--opensquilla/scripts/live_provider_profile_gateway_e2e.py
2026-07-13 13:12:33 +08:00

753 lines
26 KiB
Python

#!/usr/bin/env python3
"""Run live gateway E2E checks for direct provider tier profiles.
The check starts a temporary OpenSquilla gateway per provider, enables the
matching ``squilla_router.tier_profile``, sends one turn for each text tier,
and records routed model, response usage, and local cost estimates. Secrets are
kept in environment variables and are not written to the output artifact.
"""
from __future__ import annotations
import argparse
import json
import os
import subprocess
import sys
import tempfile
import time
from pathlib import Path
from typing import Any
REPO_ROOT = Path(__file__).resolve().parents[1]
SRC_DIR = REPO_ROOT / "src"
if str(REPO_ROOT) not in sys.path:
sys.path.insert(0, str(REPO_ROOT))
if str(SRC_DIR) not in sys.path:
sys.path.insert(0, str(SRC_DIR))
from opensquilla.engine.pricing import lookup_price # noqa: E402
from opensquilla.gateway.config import GatewayConfig # noqa: E402
from opensquilla.provider.registry import get_provider_spec # noqa: E402
from scripts.smoke_v4_phase3_router import ( # noqa: E402
_free_port,
_post_json,
_read_turn_call_records,
_stop_gateway,
_usage_from_llm_responses,
_wait_for_assistant_reply,
_wait_for_gateway_health,
)
DEFAULT_PROVIDERS = [
"openrouter",
"dashscope",
"deepseek",
"gemini",
"volcengine",
"byteplus",
"openai",
"zhipu",
"moonshot",
]
BASE_ENV = {
"openrouter": "OPENROUTER_BASE_URL",
"openai": "OPENAI_BASE_URL",
"dashscope": "DASHSCOPE_BASE_URL",
"deepseek": "DEEPSEEK_BASE_URL",
"gemini": "GEMINI_BASE_URL",
"volcengine": "VOLCENGINE_BASE_URL",
"byteplus": "BYTEPLUS_BASE_URL",
"moonshot": "MOONSHOT_BASE_URL",
"zhipu": "ZAI_BASE_URL",
}
TEXT_PROFILE_SLOTS = ("c0", "c1", "c2", "c3")
LIVE_AGENT_MAX_ITERATIONS = 6
LIVE_AGENT_RUNTIME_TIMEOUT_SECONDS = 75.0
LIVE_TURN_HARD_DEADLINE_SECONDS = 90.0
TIER_CASES = [
{
"tier": "c0",
"id": "r0_short_ack",
"message": "谢谢。不要调用工具,请只回复一个短句,包含 {marker}。",
},
{
"tier": "c1",
"id": "r1_structured_compare",
"message": (
"不要调用工具,只输出 Markdown 表格和 marker。用不超过 4 行的表格比较 "
"PostgreSQL 和 MySQL 在事务、索引、复制方面的差异,每格不超过 12 个字。"
"最后一行单独写 {marker}。"
),
},
{
"tier": "c2",
"id": "r2_debugging",
"message": (
"下面是异步服务偶发超时的日志片段:连接池耗尽、慢查询、重试风暴、队列积压。"
"不要调用工具,请用不超过三条短句定位可能原因并给出排查动作。"
"最后一行单独写 {marker}。"
),
},
{
"tier": "c3",
"id": "r3_architecture",
"message": (
"请设计跨机房分布式任务调度系统,解释一致性、故障恢复和容量评估。"
"不要调用工具,回答不超过五句,并包含 {marker}。"
),
},
]
def _toml_value(value: Any) -> str:
if isinstance(value, bool):
return "true" if value else "false"
if isinstance(value, int | float):
return str(value)
return json.dumps(str(value), ensure_ascii=False)
def _marker_component(value: str) -> str:
raw = "".join(ch if ch.isalnum() else "_" for ch in value.upper())
return "_".join(part for part in raw.split("_") if part)
def _case_marker(provider: str, slot: str, case_id: str) -> str:
return (
f"E2E_{_marker_component(provider)}_"
f"{_marker_component(slot)}_{_marker_component(case_id)}"
)
def _load_env_quietly(path: Path = REPO_ROOT / ".env") -> None:
if not path.exists():
return
for raw_line in path.read_text(encoding="utf-8").splitlines():
line = raw_line.strip()
if not line or line.startswith("#") or "=" not in line:
continue
key, value = line.split("=", 1)
key = key.strip()
value = value.strip().strip('"').strip("'")
if key and key not in os.environ:
os.environ[key] = value
def _profile_tiers(provider: str) -> dict[str, dict[str, Any]]:
cfg = GatewayConfig.model_validate(
{
"llm": {"provider": provider},
"squilla_router": {"tier_profile": provider},
}
)
return {
name: dict(tier)
for name, tier in cfg.squilla_router.tiers.items()
if isinstance(tier, dict) and not tier.get("image_only")
}
def _profile_slot_targets(tiers: dict[str, dict[str, Any]]) -> dict[str, dict[str, Any]]:
return {
slot: dict(tiers[slot])
for slot in TEXT_PROFILE_SLOTS
if isinstance(tiers.get(slot), dict) and not tiers[slot].get("image_only")
}
def _covered_profile_slots(rows: list[dict[str, Any]]) -> list[str]:
covered: list[str] = []
for row in rows:
slot = str(row.get("actual_slot_covered") or "")
if row.get("ok") is True and slot and slot not in covered:
covered.append(slot)
return covered
def _missing_profile_slots(
tiers: dict[str, dict[str, Any]],
rows: list[dict[str, Any]],
) -> list[str]:
covered = set(_covered_profile_slots(rows))
return [slot for slot in _profile_slot_targets(tiers) if slot not in covered]
def _forced_tier_overrides_for_slot(
tiers: dict[str, dict[str, Any]],
slot: str,
) -> dict[str, dict[str, Any]]:
target = dict(tiers[slot])
overrides: dict[str, dict[str, Any]] = {}
for text_slot in TEXT_PROFILE_SLOTS:
if text_slot == slot:
forced = dict(target)
forced["image_only"] = False
overrides[text_slot] = forced
else:
hidden = dict(tiers.get(text_slot, target))
hidden["image_only"] = True
overrides[text_slot] = hidden
return overrides
def _render_tier_overrides(tiers: dict[str, dict[str, Any]] | None) -> str:
if not tiers:
return ""
lines: list[str] = []
for slot in TEXT_PROFILE_SLOTS:
cfg = tiers.get(slot)
if not isinstance(cfg, dict):
continue
lines.append("")
lines.append(f"[squilla_router.tiers.{slot}]")
for key in (
"provider",
"model",
"description",
"supports_image",
"image_only",
"thinking_level",
"thinking",
"supports_thinking",
):
if key in cfg and cfg[key] is not None:
lines.append(f"{key} = {_toml_value(cfg[key])}")
return "\n".join(lines)
def _write_config(
path: Path,
provider: str,
base_url: str,
model: str,
*,
max_tokens: int,
default_tier: str = "c1",
tier_overrides: dict[str, dict[str, Any]] | None = None,
) -> None:
tier_override_toml = _render_tier_overrides(tier_overrides)
path.write_text(
f"""
host = "127.0.0.1"
debug = false
llm_request_timeout_seconds = 90
agent_runtime_timeout_seconds = {LIVE_AGENT_RUNTIME_TIMEOUT_SECONDS}
agent_max_iterations = {LIVE_AGENT_MAX_ITERATIONS}
[auth]
mode = "none"
[control_ui]
enabled = false
[rate_limit]
enabled = false
[task_runtime]
turn_hard_deadline_s = {LIVE_TURN_HARD_DEADLINE_SECONDS}
[memory]
source = "state"
[llm]
provider = "{provider}"
model = "{model}"
base_url = "{base_url}"
max_tokens = {max_tokens}
[squilla_router]
enabled = true
auto_thinking = true
rollout_phase = "full"
strategy = "v4_phase3"
tier_profile = "{provider}"
default_tier = "{default_tier}"
confidence_threshold = 0.5
kv_cache_anti_downgrade_enabled = true
kv_cache_anti_downgrade_window_seconds = 600
complaint_upgrade_enabled = true
complaint_upgrade_steps = 1
require_router_runtime = true
{tier_override_toml}
""".strip()
+ "\n",
encoding="utf-8",
)
def _first_record(records: list[dict[str, Any]], *, session_key: str, kind: str) -> dict[str, Any]:
for record in records:
if record.get("session_key") == session_key and record.get("kind") == kind:
return record
return {}
def _read_decision_records(state_root: Path) -> list[dict[str, Any]]:
records: list[dict[str, Any]] = []
for path in sorted((state_root / "logs").glob("decisions-*.jsonl")):
for line in path.read_text(encoding="utf-8").splitlines():
if not line.strip():
continue
try:
records.append(json.loads(line))
except json.JSONDecodeError:
continue
return records
def _decision_for_session(
records: list[dict[str, Any]],
*,
session_key: str,
) -> dict[str, Any]:
for record in records:
if record.get("session_key") == session_key:
return record
return {}
def _router_step_from_decision(decision: dict[str, Any]) -> dict[str, Any]:
for step in decision.get("pipeline_steps") or []:
if step.get("step_name") == "apply_squilla_router":
return step
return {}
def _estimate_cost(
model: str,
usage: dict[str, Any],
*,
provider: str | None = None,
) -> dict[str, Any]:
input_tokens = int(usage.get("input_tokens") or usage.get("prompt_tokens") or 0)
output_tokens = int(usage.get("output_tokens") or usage.get("completion_tokens") or 0)
price = lookup_price(model)
estimate = (
input_tokens * price.input_per_m + output_tokens * price.output_per_m
) / 1_000_000
raw_billed_cost = usage.get("billed_cost")
provider_billed_cost = None
cost_source = "opensquilla_static_estimate"
billing_scope = "static_estimate"
if (
provider == "openrouter"
and isinstance(raw_billed_cost, int | float)
and raw_billed_cost > 0
):
provider_billed_cost = float(raw_billed_cost)
cost_source = "provider_billed"
billing_scope = "provider_response"
return {
"provider_billed_cost_usd": provider_billed_cost,
"opensquilla_estimated_cost_usd": estimate,
"cost_source": cost_source,
"billing_scope": billing_scope,
"raw_gateway_usage_billed_cost_usd": usage.get("billed_cost"),
"provider_billed": provider_billed_cost,
"opensquilla_estimate": estimate,
"input_per_m": price.input_per_m,
"output_per_m": price.output_per_m,
"source": cost_source,
}
def _failure_kind(
row: dict[str, Any],
actual_model: str,
actual_routed_tier: str | None,
) -> str | None:
error = str(row.get("turn_error") or "")
if error:
lowered = error.lower()
if "401" in lowered or "authentication" in lowered or "unauthorized" in lowered:
return "auth_failed"
if "429" in lowered or "quota" in lowered or "billing" in lowered:
return "quota_or_billing_blocked"
if "timeout" in lowered or "timed out" in lowered:
return "gateway_turn_timeout"
if "model" in lowered and ("not" in lowered or "invalid" in lowered):
return "model_unavailable"
return "unknown_provider_error"
if not row.get("assistant_excerpt"):
return "gateway_turn_timeout"
if not row.get("assistant_marker_present"):
return "content_marker_missing"
if actual_routed_tier != row.get("expected_slot"):
return "router_selected_unexpected_tier"
if actual_model != row.get("expected_model"):
return "model_unavailable"
return None
def _actual_model_from_records(
request: dict[str, Any],
response: dict[str, Any],
) -> str:
request_payload = request.get("payload") or {}
response_payload = response.get("payload") or {}
request_config = request_payload.get("config") or {}
usage = response_payload.get("usage") or {}
return str(
request_payload.get("model")
or request_config.get("model")
or request.get("model")
or usage.get("model")
or response.get("model")
or ""
)
def _run_gateway_case_batch(
*,
provider: str,
api_key: str,
base_url: str,
tiers: dict[str, dict[str, Any]],
cases: list[dict[str, Any]],
max_tokens: int,
timeout_seconds: float,
case_mode: str,
default_tier: str = "c1",
tier_overrides: dict[str, dict[str, Any]] | None = None,
) -> dict[str, Any]:
active_tiers = tier_overrides or tiers
default_model = str(
active_tiers.get(default_tier, {}).get("model")
or tiers.get(default_tier, {}).get("model")
or next(iter(_profile_slot_targets(tiers).values())).get("model")
or ""
)
port = _free_port()
tmp_path = Path(tempfile.mkdtemp(prefix=f"opensquilla-{provider}-profile-e2e-"))
config_path = tmp_path / "gateway.toml"
turn_log_dir = tmp_path / "turn-calls"
_write_config(
config_path,
provider,
base_url,
default_model,
max_tokens=max_tokens,
default_tier=default_tier,
tier_overrides=tier_overrides,
)
env = os.environ.copy()
env["PYTHONPATH"] = str(SRC_DIR) + os.pathsep + env.get("PYTHONPATH", "")
env["OPENSQUILLA_GATEWAY_CONFIG_PATH"] = str(config_path)
env["OPENSQUILLA_STATE_DIR"] = str(tmp_path / "state")
env["OPENSQUILLA_MEMORY_DREAM_DISABLED"] = "1"
env["OPENSQUILLA_TOOL_PROFILE"] = "channel_default"
env["OPENSQUILLA_TURN_CALL_LOG"] = "1"
env["OPENSQUILLA_TURN_CALL_LOG_DIR"] = str(turn_log_dir)
env["OPENSQUILLA_LLM_PROVIDER"] = provider
env["OPENSQUILLA_LLM_MODEL"] = default_model
env["OPENSQUILLA_LLM_API_KEY"] = api_key
env["OPENSQUILLA_LLM_BASE_URL"] = base_url
if provider != "openrouter":
# build_services still gives OPENROUTER_API_KEY special precedence for
# legacy paths. Keep it empty for direct-provider profiles so dotenv
# loading cannot override the selected provider key.
env["OPENROUTER_API_KEY"] = ""
env["OPENROUTER_BASE_URL"] = ""
stdout_path = tmp_path / "gateway.stdout.log"
stderr_path = tmp_path / "gateway.stderr.log"
with stdout_path.open("w", encoding="utf-8") as stdout_file, stderr_path.open(
"w", encoding="utf-8"
) as stderr_file:
proc = subprocess.Popen(
[
sys.executable,
"-m",
"opensquilla.cli.main",
"gateway",
"run",
"--port",
str(port),
"--bind",
"127.0.0.1",
],
cwd=REPO_ROOT,
env=env,
stdout=stdout_file,
stderr=stderr_file,
text=True,
)
health: dict[str, Any] | None = None
error: str | None = None
rows: list[dict[str, Any]] = []
try:
health, error = _wait_for_gateway_health(proc, port)
if error is None:
for case in cases:
slot = str(case.get("slot") or case.get("tier") or default_tier)
marker = _case_marker(provider, slot, str(case["id"]))
session_key = (
f"profile-e2e:{provider}:{case['id']}:{int(time.time() * 1000)}"
)
message = case["message"].format(marker=marker)
try:
accepted = _post_json(
f"http://127.0.0.1:{port}/api/chat",
{
"sessionKey": session_key,
"message": message,
"intent": "new_chat",
},
timeout=10.0,
)
assistant, history, turn_error = _wait_for_assistant_reply(
port=port,
session_key=session_key,
previous_assistant_count=0,
timeout_seconds=timeout_seconds,
)
except Exception as exc: # noqa: BLE001 - compact E2E diagnostic
accepted = {}
assistant = None
history = None
turn_error = f"{type(exc).__name__}: {exc}"
assistant_text = str((assistant or {}).get("text", "")).strip()
rows.append(
{
"case_id": case["id"],
"case_mode": case_mode,
"expected_slot": slot,
"expected_tier": slot,
"expected_model": str(tiers.get(slot, {}).get("model") or ""),
"marker": marker,
"session_key": session_key,
"accepted": accepted,
"assistant_excerpt": assistant_text[:240],
"assistant_marker_present": marker in assistant_text,
"history_message_count": len((history or {}).get("messages", [])),
"turn_error": turn_error,
}
)
finally:
_stop_gateway(proc)
stdout_file.flush()
stderr_file.flush()
records = _read_turn_call_records(turn_log_dir)
decisions = _read_decision_records(tmp_path / "state")
stdout_tail = stdout_path.read_text(encoding="utf-8", errors="replace")[-2000:]
stderr_tail = stderr_path.read_text(encoding="utf-8", errors="replace")[-4000:]
enriched: list[dict[str, Any]] = []
for row in rows:
request = _first_record(records, session_key=row["session_key"], kind="llm_request")
response = _first_record(records, session_key=row["session_key"], kind="llm_response")
decision = _decision_for_session(decisions, session_key=row["session_key"])
router_step = _router_step_from_decision(decision)
request_payload = request.get("payload") or {}
response_payload = response.get("payload") or {}
request_config = request_payload.get("config") or {}
usage = response_payload.get("usage") or {}
actual_model = _actual_model_from_records(request, response)
actual_routed_tier = (
router_step.get("routed_tier")
or request_payload.get("routed_tier")
or request_payload.get("squilla_router_tier")
or request_config.get("routed_tier")
)
if actual_routed_tier is not None:
actual_routed_tier = str(actual_routed_tier)
failure_kind = _failure_kind(row, actual_model, actual_routed_tier)
row_ok = (
failure_kind is None
and bool(row.get("assistant_excerpt"))
and actual_model == row["expected_model"]
and actual_routed_tier == row["expected_slot"]
)
enriched.append(
{
**row,
"ok": row_ok,
"failure_kind": failure_kind,
"error": row.get("turn_error"),
"actual_routed_tier": actual_routed_tier,
"routing_source": router_step.get("routing_source"),
"routing_confidence": router_step.get("confidence"),
"actual_slot_covered": row["expected_slot"] if row_ok else None,
"actual_request_model": actual_model or request.get("model"),
"actual_response_model": usage.get("model"),
"request_thinking": request_config.get("thinking"),
"request_thinking_level": request_config.get("thinking_level"),
"usage": {
"input_tokens": usage.get("input_tokens"),
"output_tokens": usage.get("output_tokens"),
"reasoning_tokens": usage.get("reasoning_tokens"),
"cached_tokens": usage.get("cached_tokens"),
"billed_cost": usage.get("billed_cost"),
},
"cost": _estimate_cost(
actual_model or row["expected_model"],
usage,
provider=provider,
),
}
)
llm_responses = [record for record in records if record.get("kind") == "llm_response"]
batch_ok = error is None and bool(enriched) and all(row["ok"] for row in enriched)
return {
"case_mode": case_mode,
"ok": batch_ok,
"health": health or {},
"cases": enriched,
"usage_from_turn_logs": _usage_from_llm_responses(llm_responses),
"error": error,
"stdout_tail": stdout_tail,
"stderr_tail": stderr_tail,
}
def _run_provider(provider: str, *, max_tokens: int, timeout_seconds: float) -> dict[str, Any]:
spec = get_provider_spec(provider)
api_key = os.environ.get(spec.env_key, "").strip()
base_url = os.environ.get(BASE_ENV.get(provider, ""), "").strip() or spec.default_base_url
tiers = _profile_tiers(provider)
slot_targets = _profile_slot_targets(tiers)
if not api_key:
return {
"provider": provider,
"ok": False,
"provider_ok": False,
"skipped": True,
"failure_kind": "skipped_missing_key",
"env_key": spec.env_key,
"base_url": base_url,
"key_present": False,
"tier_profile": provider,
"tier_models": {slot: cfg.get("model") for slot, cfg in slot_targets.items()},
"profile_slots_covered": [],
"profile_slots_missing": list(slot_targets),
"models_covered": [],
"error": f"{spec.env_key} is empty",
}
natural = _run_gateway_case_batch(
provider=provider,
api_key=api_key,
base_url=base_url,
tiers=tiers,
cases=TIER_CASES,
max_tokens=max_tokens,
timeout_seconds=timeout_seconds,
case_mode="natural_router",
)
all_cases = list(natural.get("cases") or [])
coverage_batches: list[dict[str, Any]] = []
for missing_slot in _missing_profile_slots(tiers, all_cases):
target_case = {
"slot": missing_slot,
"id": f"coverage_{missing_slot}",
"message": (
"不要调用工具,请只回复一句中文短句并包含 {marker}。"
),
}
batch = _run_gateway_case_batch(
provider=provider,
api_key=api_key,
base_url=base_url,
tiers=tiers,
cases=[target_case],
max_tokens=max_tokens,
timeout_seconds=timeout_seconds,
case_mode="coverage_compensation",
default_tier=missing_slot,
tier_overrides=_forced_tier_overrides_for_slot(tiers, missing_slot),
)
coverage_batches.append(batch)
all_cases.extend(batch.get("cases") or [])
covered_slots = _covered_profile_slots(all_cases)
missing_slots = _missing_profile_slots(tiers, all_cases)
models_covered = sorted(
{
str(row.get("actual_request_model") or row.get("expected_model") or "")
for row in all_cases
if row.get("ok") is True
}
- {""}
)
natural_cases = [row for row in all_cases if row.get("case_mode") == "natural_router"]
coverage_cases = [
row for row in all_cases if row.get("case_mode") == "coverage_compensation"
]
provider_ok = not missing_slots and any(
row.get("case_mode") == "natural_router" and row.get("assistant_excerpt")
for row in all_cases
)
failure_kinds = sorted(
{str(row.get("failure_kind")) for row in all_cases if row.get("failure_kind")}
)
return {
"provider": provider,
"ok": provider_ok,
"provider_ok": provider_ok,
"env_key": spec.env_key,
"base_url": base_url,
"key_present": bool(api_key),
"tier_profile": provider,
"tier_models": {slot: cfg.get("model") for slot, cfg in slot_targets.items()},
"profile_slots_covered": covered_slots,
"profile_slots_missing": missing_slots,
"models_covered": models_covered,
"natural_cases_ok": bool(natural_cases)
and all(
row.get("failure_kind") in (None, "router_selected_unexpected_tier")
for row in natural_cases
),
"coverage_cases_ok": bool(coverage_cases) and all(row.get("ok") for row in coverage_cases)
if coverage_cases
else True,
"health": natural.get("health") or {},
"cases": all_cases,
"batches": [natural, *coverage_batches],
"usage_from_turn_logs": natural.get("usage_from_turn_logs"),
"failure_kinds": failure_kinds,
"error": "; ".join(failure_kinds) or natural.get("error"),
}
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--providers", nargs="+", default=DEFAULT_PROVIDERS)
parser.add_argument("--output", required=True)
parser.add_argument("--max-tokens", type=int, default=768)
parser.add_argument("--timeout-seconds", type=float, default=120.0)
args = parser.parse_args()
_load_env_quietly()
results = [
_run_provider(
provider,
max_tokens=args.max_tokens,
timeout_seconds=args.timeout_seconds,
)
for provider in args.providers
]
payload = {
"generated_at": time.strftime("%Y-%m-%dT%H:%M:%S%z"),
"ok": all(result.get("ok") is True for result in results),
"note": (
"provider_billed_cost_usd is unavailable here; "
"opensquilla_estimated_cost_usd is a static local estimate computed "
"from returned token usage."
),
"results": results,
}
output = Path(args.output)
output.parent.mkdir(parents=True, exist_ok=True)
output.write_text(json.dumps(payload, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
print(json.dumps(payload, ensure_ascii=False, indent=2))
return 0 if payload["ok"] else 1
if __name__ == "__main__":
raise SystemExit(main())