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

536 lines
19 KiB
Python

#!/usr/bin/env python3
"""Live smoke selected provider profiles without printing secrets."""
from __future__ import annotations
import argparse
import asyncio
import json
import os
import time
from dataclasses import asdict, dataclass
from pathlib import Path
from typing import Any
import httpx
from opensquilla.engine.pricing import lookup_price
from opensquilla.provider.registry import get_provider_spec
from opensquilla.provider.selector import ProviderConfig, _build_provider
from opensquilla.provider.types import ChatConfig, DoneEvent, ErrorEvent, Message, TextDeltaEvent
@dataclass
class SmokeResult:
provider: str
model: str
base_url: str
env_key: str
key_present: bool
direct_status: str
stream_status: str
response_model: str
content_match: str
usage: dict[str, Any]
cost: dict[str, Any]
error: str
latency_ms: int
_MODEL_ENV = {
"openai": "OPENAI_MODEL",
"dashscope": "DASHSCOPE_MODEL",
"deepseek": "DEEPSEEK_MODEL",
"gemini": "GEMINI_MODEL",
"volcengine": "VOLCENGINE_MODEL",
"volcengine_coding_plan": "VOLCENGINE_CODING_MODEL",
"byteplus": "BYTEPLUS_MODEL",
"bailian_coding": "BAILIAN_CODING_MODEL",
"moonshot": "MOONSHOT_MODEL",
"kimi_coding_openai": "KIMI_CODING_MODEL",
"kimi_coding_anthropic": "KIMI_CODING_MODEL",
"zhipu": "ZAI_MODEL",
"qianfan": "QIANFAN_MODEL",
"minimax": "MINIMAX_MODEL",
"minimax_openai": "MINIMAX_MODEL",
"minimax_coding_openai": "MINIMAX_CODING_MODEL",
"minimax_coding_anthropic": "MINIMAX_CODING_MODEL",
"minimax_cn": "MINIMAX_CN_MODEL",
"minimax_global": "MINIMAX_GLOBAL_MODEL",
"mimo_openai": "MIMO_MODEL",
"mimo_anthropic": "MIMO_MODEL",
"tencent_tokenhub": "TENCENT_TOKENHUB_MODEL",
"tencent_tokenhub_anthropic": "TENCENT_TOKENHUB_MODEL",
"tencent_tokenhub_intl": "TENCENT_TOKENHUB_INTL_MODEL",
"tencent_token_plan": "TENCENT_TOKEN_PLAN_MODEL",
"tencent_token_plan_anthropic": "TENCENT_TOKEN_PLAN_MODEL",
"tokenrhythm": "TOKENRHYTHM_MODEL",
}
_BASE_ENV = {
"openai": "OPENAI_BASE_URL",
"dashscope": "DASHSCOPE_BASE_URL",
"deepseek": "DEEPSEEK_BASE_URL",
"gemini": "GEMINI_BASE_URL",
"volcengine": "VOLCENGINE_BASE_URL",
"volcengine_coding_plan": "VOLCENGINE_CODING_BASE_URL",
"byteplus": "BYTEPLUS_BASE_URL",
"bailian_coding": "BAILIAN_CODING_BASE_URL",
"moonshot": "MOONSHOT_BASE_URL",
"kimi_coding_openai": "KIMI_CODING_OPENAI_BASE_URL",
"kimi_coding_anthropic": "KIMI_CODING_ANTHROPIC_BASE_URL",
"zhipu": "ZAI_BASE_URL",
"qianfan": "QIANFAN_BASE_URL",
"minimax": "MINIMAX_BASE_URL",
"minimax_openai": "MINIMAX_OPENAI_BASE_URL",
"minimax_coding_openai": "MINIMAX_CODING_OPENAI_BASE_URL",
"minimax_coding_anthropic": "MINIMAX_CODING_ANTHROPIC_BASE_URL",
"minimax_cn": "MINIMAX_CN_BASE_URL",
"minimax_global": "MINIMAX_GLOBAL_BASE_URL",
"mimo_openai": "MIMO_OPENAI_BASE_URL",
"mimo_anthropic": "MIMO_ANTHROPIC_BASE_URL",
"tencent_tokenhub": "TENCENT_TOKENHUB_BASE_URL",
"tencent_tokenhub_anthropic": "TENCENT_TOKENHUB_ANTHROPIC_BASE_URL",
"tencent_tokenhub_intl": "TENCENT_TOKENHUB_INTL_BASE_URL",
"tencent_token_plan": "TENCENT_TOKEN_PLAN_BASE_URL",
"tencent_token_plan_anthropic": "TENCENT_TOKEN_PLAN_ANTHROPIC_BASE_URL",
"tokenrhythm": "TOKENRHYTHM_BASE_URL",
}
_DEFAULT_MODELS = {
"openai": "gpt-5.4-mini",
"dashscope": "qwen3.7-plus",
"deepseek": "deepseek-v4-flash",
"gemini": "gemini-3.5-flash",
"volcengine": "doubao-seed-2-0-lite-260215",
"volcengine_coding_plan": "doubao-seed-2.0-pro",
"byteplus": "seed-2-0-lite-260228",
"bailian_coding": "kimi-k2.5",
"moonshot": "kimi-k2.6",
"kimi_coding_openai": "kimi-for-coding",
"kimi_coding_anthropic": "kimi-for-coding",
"zhipu": "glm-5",
"qianfan": "ernie-4.5-turbo-128k",
"minimax": "MiniMax-M2.7",
"minimax_openai": "MiniMax-M2.7",
"minimax_coding_openai": "MiniMax-M2.7",
"minimax_coding_anthropic": "MiniMax-M2.7",
"minimax_cn": "MiniMax-M2.7",
"minimax_global": "MiniMax-M2.7",
"mimo_openai": "mimo-v2.5",
"mimo_anthropic": "mimo-v2.5-pro",
"tencent_tokenhub": "hy3",
"tencent_tokenhub_anthropic": "hy3",
"tencent_tokenhub_intl": "deepseek-v3.2",
"tencent_token_plan": "hy3",
"tencent_token_plan_anthropic": "hy3",
"tokenrhythm": "deepseek-v4-flash",
}
# Providers whose models spend reasoning tokens out of max_tokens before any
# text: the CLI default budget of 64 would come back as empty content with
# finish_reason "length", failing the smoke for provider-independent reasons.
_MIN_MAX_TOKENS = {
"tokenrhythm": 1024,
}
def _csv_values(raw: str | None) -> list[str]:
if not raw:
return []
return [part.strip() for part in raw.split(",") if part.strip()]
def _load_env_quietly(path: Path = Path(".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 _headers_for_openai(api_key: str) -> dict[str, str]:
# Keyless local providers must not send an empty Bearer value (httpx
# rejects it as an illegal header).
headers = {"Content-Type": "application/json"}
if api_key:
headers["Authorization"] = f"Bearer {api_key}"
return headers
def _headers_for_anthropic(api_key: str) -> dict[str, str]:
return {
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json",
"anthropic-version": "2023-06-01",
}
def _versioned_chat_url(base_url: str) -> str:
base = base_url.rstrip("/")
if base.endswith(("/v1", "/v2", "/v3", "/v4")):
return f"{base}/chat/completions"
return f"{base}/v1/chat/completions"
def _direct_openai_temperature(provider: str, model: str) -> int:
if provider == "kimi_coding_openai" and model == "kimi-for-coding":
return 1
if provider == "moonshot" and model.lower().startswith("kimi-k2."):
return 1
return 0
def _direct_openai_token_limit_field(provider: str, model: str) -> str:
if provider == "openai" and model.lower().startswith(("gpt-5", "o1", "o3", "o4")):
return "max_completion_tokens"
return "max_tokens"
async def _direct_openai(
provider: str,
model: str,
api_key: str,
base_url: str,
expected: str,
max_tokens: int,
) -> tuple[str, str, str, dict[str, Any], int]:
start = time.perf_counter()
payload = {
"model": model,
"messages": [
{
"role": "user",
"content": f"Reply exactly with: {expected}",
}
],
"temperature": _direct_openai_temperature(provider, model),
}
payload[_direct_openai_token_limit_field(provider, model)] = max_tokens
try:
async with httpx.AsyncClient(timeout=30.0, trust_env=False) as client:
resp = await client.post(
_versioned_chat_url(base_url),
headers=_headers_for_openai(api_key),
json=payload,
)
latency = int((time.perf_counter() - start) * 1000)
if resp.status_code >= 400:
return "failed", "", _error_summary(resp), {}, latency
data = resp.json()
content = data.get("choices", [{}])[0].get("message", {}).get("content", "")
response_model = str(data.get("model") or "")
status = "passed" if expected in content else "content_mismatch"
return status, response_model, content, _usage_summary(data.get("usage")), latency
except Exception as exc: # noqa: BLE001 - smoke reports compact diagnostic
latency = int((time.perf_counter() - start) * 1000)
return "failed", "", f"{type(exc).__name__}: {exc}", {}, latency
async def _direct_anthropic(
model: str,
api_key: str,
base_url: str,
expected: str,
max_tokens: int,
) -> tuple[str, str, str, dict[str, Any], int]:
start = time.perf_counter()
payload = {
"model": model,
"messages": [{"role": "user", "content": f"Reply exactly with: {expected}"}],
"max_tokens": max_tokens,
"temperature": 1,
}
try:
async with httpx.AsyncClient(timeout=30.0, trust_env=False) as client:
resp = await client.post(
f"{base_url.rstrip('/')}/v1/messages",
headers=_headers_for_anthropic(api_key),
json=payload,
)
latency = int((time.perf_counter() - start) * 1000)
if resp.status_code >= 400:
return "failed", "", _error_summary(resp), {}, latency
data = resp.json()
text_parts = [
block.get("text", "")
for block in data.get("content", [])
if isinstance(block, dict) and block.get("type") == "text"
]
content = "".join(text_parts)
response_model = str(data.get("model") or "")
status = "passed" if expected in content else "content_mismatch"
return status, response_model, content, _usage_summary(data.get("usage")), latency
except Exception as exc: # noqa: BLE001 - smoke reports compact diagnostic
latency = int((time.perf_counter() - start) * 1000)
return "failed", "", f"{type(exc).__name__}: {exc}", {}, latency
async def _stream_opensquilla(
provider: str,
model: str,
api_key: str,
base_url: str,
expected: str,
max_tokens: int,
) -> tuple[str, str, dict[str, Any], int]:
start = time.perf_counter()
try:
built = _build_provider(
ProviderConfig(provider=provider, model=model, api_key=api_key, base_url=base_url)
)
chunks: list[str] = []
done: DoneEvent | None = None
async for event in built.chat(
[Message(role="user", content=f"Reply exactly with: {expected}")],
config=ChatConfig(max_tokens=max_tokens, temperature=1, timeout=30.0),
):
if isinstance(event, TextDeltaEvent):
chunks.append(event.text)
elif isinstance(event, DoneEvent):
done = event
elif isinstance(event, ErrorEvent):
latency = int((time.perf_counter() - start) * 1000)
return "failed", event.message or event.code, {}, latency
latency = int((time.perf_counter() - start) * 1000)
content = "".join(chunks)
if done is None:
return "failed", "missing DoneEvent", {}, latency
usage = {
"input_tokens": done.input_tokens,
"output_tokens": done.output_tokens,
"cached_tokens": done.cached_tokens,
"cache_write_tokens": done.cache_write_tokens,
"reasoning_tokens": done.reasoning_tokens,
"model": done.model,
"billed_cost": done.billed_cost,
"cost_source": done.cost_source,
}
status = "passed" if expected in content else "content_mismatch"
return status, content, usage, latency
except Exception as exc: # noqa: BLE001 - smoke reports compact diagnostic
latency = int((time.perf_counter() - start) * 1000)
return "failed", f"{type(exc).__name__}: {exc}", {}, latency
def _usage_summary(usage: Any) -> dict[str, Any]:
if not isinstance(usage, dict):
return {}
keys = (
"prompt_tokens",
"completion_tokens",
"total_tokens",
"input_tokens",
"output_tokens",
"cache_read_input_tokens",
"cache_creation_input_tokens",
)
return {key: usage[key] for key in keys if key in usage}
def _cost_estimate(model: str, usage: dict[str, Any]) -> dict[str, Any]:
direct_usage = usage.get("direct") if isinstance(usage.get("direct"), dict) else {}
stream_usage = usage.get("stream") if isinstance(usage.get("stream"), dict) else {}
prompt_tokens = direct_usage.get("prompt_tokens") or stream_usage.get("input_tokens") or 0
completion_tokens = (
direct_usage.get("completion_tokens") or stream_usage.get("output_tokens") or 0
)
price = lookup_price(model)
estimate = (
prompt_tokens * price.input_per_m + completion_tokens * price.output_per_m
) / 1_000_000
# The stream DoneEvent carries the provider-billed cost when the upstream
# reports one (OpenRouter usage.cost); surface it instead of pretending
# only static estimates exist.
billed = stream_usage.get("billed_cost") or 0.0
billed_source = str(stream_usage.get("cost_source") or "")
provider_billed = billed if billed > 0 and billed_source == "provider_billed" else None
cost_source = billed_source if provider_billed is not None else "opensquilla_static_estimate"
return {
"provider_billed_cost_usd": provider_billed,
"opensquilla_estimated_cost_usd": estimate,
"cost_source": cost_source,
"billing_scope": "provider_billed" if provider_billed is not None else "static_estimate",
"provider_billed": provider_billed,
"opensquilla_estimate": estimate,
"input_per_m": price.input_per_m,
"output_per_m": price.output_per_m,
"source": cost_source,
}
def _error_summary(resp: httpx.Response) -> str:
try:
body = resp.json()
except ValueError:
body = resp.text[:300]
return f"HTTP {resp.status_code}: {body}"
async def smoke_provider(
provider: str,
*,
include_stream: bool = True,
model_override: str | None = None,
base_url_override: str | None = None,
max_tokens: int = 64,
) -> SmokeResult:
spec = get_provider_spec(provider)
env_key = spec.env_key
api_key = os.environ.get(env_key, "").strip()
max_tokens = max(max_tokens, _MIN_MAX_TOKENS.get(provider, 0))
model = (
model_override
or os.environ.get(_MODEL_ENV.get(provider, ""), "").strip()
or _DEFAULT_MODELS.get(provider, "")
)
if not model:
raise SystemExit(
f"no model configured for provider {provider!r}: pass --model or set "
f"{_MODEL_ENV.get(provider) or 'a model env override'}"
)
base_url = (
base_url_override
or os.environ.get(_BASE_ENV.get(provider, ""), "").strip()
or spec.default_base_url
)
expected = f"opensquilla {provider} smoke ok"
# Local providers (ollama, lm_studio, ovms) declare their key optional in
# the registry; only skip when the spec actually requires one.
if not api_key and spec.requires_api_key():
return SmokeResult(
provider=provider,
model=model,
base_url=base_url,
env_key=env_key,
key_present=False,
direct_status="skipped",
stream_status="skipped",
response_model="",
content_match="not_run",
usage={},
cost={
"provider_billed_cost_usd": None,
"opensquilla_estimated_cost_usd": None,
"cost_source": "unavailable",
"billing_scope": "none",
"provider_billed": None,
"opensquilla_estimate": None,
"source": "unavailable",
},
error=f"{env_key} is empty",
latency_ms=0,
)
if spec.backend == "anthropic":
(
direct_status,
response_model,
direct_content,
usage,
direct_latency,
) = await _direct_anthropic(model, api_key, base_url, expected, max_tokens)
else:
direct_status, response_model, direct_content, usage, direct_latency = await _direct_openai(
provider, model, api_key, base_url, expected, max_tokens
)
if include_stream:
stream_status, stream_content, stream_usage, stream_latency = await _stream_opensquilla(
provider, model, api_key, base_url, expected, max_tokens
)
else:
stream_status = "skipped"
stream_content = ""
stream_usage = {}
stream_latency = 0
errors = []
if direct_status == "failed":
errors.append(f"direct={direct_content}")
if stream_status == "failed":
errors.append(f"stream={stream_content}")
content_match = (
"exact" if direct_status == "passed" and stream_status == "passed" else "not_validated"
)
if direct_status == "passed" and stream_status == "skipped":
content_match = "direct_exact"
merged_usage = {"direct": usage, "stream": stream_usage}
return SmokeResult(
provider=provider,
model=model,
base_url=base_url,
env_key=env_key,
key_present=bool(api_key),
direct_status=direct_status,
stream_status=stream_status,
response_model=response_model,
content_match=content_match,
usage=merged_usage,
cost=_cost_estimate(response_model or model, merged_usage),
error="; ".join(errors),
latency_ms=direct_latency + stream_latency,
)
async def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--provider")
parser.add_argument(
"--providers",
nargs="+",
default=["dashscope", "deepseek", "gemini", "volcengine", "byteplus"],
)
parser.add_argument("--models")
parser.add_argument("--model")
parser.add_argument("--base-url")
parser.add_argument("--max-tokens", type=int, default=64)
parser.add_argument("--skip-stream", action="store_true")
parser.add_argument("--output", required=True)
args = parser.parse_args()
_load_env_quietly()
providers = [args.provider] if args.provider else list(args.providers)
models = _csv_values(args.models)
if args.model and models:
parser.error("--model and --models are mutually exclusive")
if models and len(providers) != 1:
parser.error("--models requires exactly one provider")
jobs: list[tuple[str, str | None]] = []
if models:
jobs = [(providers[0], model) for model in models]
else:
jobs = [(provider, args.model) for provider in providers]
results = [
await smoke_provider(
provider,
include_stream=not args.skip_stream,
model_override=model,
base_url_override=args.base_url,
max_tokens=args.max_tokens,
)
for provider, model in jobs
]
payload = {
"generated_at": time.strftime("%Y-%m-%dT%H:%M:%S%z"),
"results": [asdict(result) for result in results],
}
output = Path(args.output)
output.parent.mkdir(parents=True, exist_ok=True)
output.write_text(json.dumps(payload, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
print(json.dumps(payload, indent=2, ensure_ascii=False))
return 0
if __name__ == "__main__":
raise SystemExit(asyncio.run(main()))