1014 lines
58 KiB
Python
Executable File
1014 lines
58 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Catálogos auditáveis e preços de referência para o roteador de modelos.
|
|
|
|
A lista pública não comprova autenticação, entitlement ou tarifa da conta. Este
|
|
módulo nunca invoca modelos nem lê/imprime credenciais. Artificial Analysis é
|
|
aceita somente por snapshot manual v1.5 validado; não há scraping nem API
|
|
presumida.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import csv
|
|
import datetime as dt
|
|
import hashlib
|
|
import io
|
|
import json
|
|
import os
|
|
import re
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
import urllib.error
|
|
import urllib.request
|
|
from urllib.parse import urlsplit
|
|
from decimal import Decimal, InvalidOperation
|
|
from pathlib import Path
|
|
from typing import Any, Callable, Iterable
|
|
|
|
ZEN_URL = "https://opencode.ai/zen/v1/models"
|
|
GO_URL = "https://opencode.ai/zen/go/v1/models"
|
|
OPENROUTER_URL = "https://openrouter.ai/api/v1/models"
|
|
AA_URL = "https://artificialanalysis.ai/agents/coding-agents"
|
|
AA_METRIC = "aa_coding_agent_index"
|
|
AA_VERSION = "1.5"
|
|
OMP_THINKING_VARIANTS = frozenset({"off", "minimal", "low", "medium", "high", "xhigh", "max", "auto"})
|
|
CACHE_DIR = Path(os.environ.get("DEV_WORKFLOWS_MODEL_CACHE", "~/.cache/dev-workflows/model-catalog")).expanduser()
|
|
|
|
# Exact reviewed links only. A family/name resemblance is never a join key.
|
|
_CURATED: dict[tuple[str, str, str | None], tuple[str, str]] = {
|
|
("opencode", "glm-5.3", None): ("z-ai/glm-5.3", "OpenRouter cross-reference reviewed against spec sample"),
|
|
("opencode-go", "glm-5.3", None): ("z-ai/glm-5.3", "OpenRouter cross-reference reviewed against spec sample"),
|
|
("opencode", "gpt-6-luna", None): ("openai/gpt-6-luna", "OpenRouter cross-reference reviewed against spec sample"),
|
|
("opencode-go", "gpt-6-luna", None): ("openai/gpt-6-luna", "OpenRouter cross-reference reviewed against spec sample"),
|
|
("opencode", "gpt-6-sol", None): ("openai/gpt-6-sol", "OpenRouter cross-reference reviewed against spec sample"),
|
|
("opencode", "claude-opus-5-5", None): ("anthropic/claude-opus-5.5", "OpenRouter cross-reference reviewed against spec sample"),
|
|
("opencode", "kimi-k3", None): ("moonshotai/kimi-k3", "OpenRouter cross-reference reviewed against spec sample"),
|
|
("opencode-go", "kimi-k3", None): ("moonshotai/kimi-k3", "OpenRouter cross-reference reviewed against spec sample"),
|
|
}
|
|
|
|
|
|
def utc_now() -> str:
|
|
return dt.datetime.now(dt.timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z")
|
|
|
|
|
|
def canonical_json(value: Any) -> bytes:
|
|
return json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":")).encode("utf-8")
|
|
|
|
|
|
def sha256(value: bytes | str) -> str:
|
|
if isinstance(value, str):
|
|
value = value.encode("utf-8")
|
|
return hashlib.sha256(value).hexdigest()
|
|
|
|
|
|
def _secure_url(value: Any, hostname: str | None = None) -> bool:
|
|
if not isinstance(value, str):
|
|
return False
|
|
parsed = urlsplit(value)
|
|
return parsed.scheme == "https" and (hostname is None or parsed.hostname in (hostname, "www." + hostname)) and not parsed.username and not parsed.password and not parsed.query and not parsed.fragment
|
|
|
|
|
|
def decimal_value(value: Any) -> Decimal | None:
|
|
if value is None or value == "":
|
|
return None
|
|
if isinstance(value, bool) or isinstance(value, float):
|
|
raise ValueError("Valores monetários devem ser strings decimais ou inteiros, não float/bool")
|
|
try:
|
|
result = Decimal(str(value))
|
|
except (InvalidOperation, ValueError) as exc:
|
|
raise ValueError(f"decimal inválido: {value!r}") from exc
|
|
if not result.is_finite():
|
|
raise ValueError("decimal deve ser finito")
|
|
return result
|
|
|
|
|
|
def decimal_text(value: Decimal | None) -> str | None:
|
|
return None if value is None else format(value, "f")
|
|
|
|
|
|
def _json_request(url: str, timeout: float = 20) -> tuple[Any, str, str]:
|
|
request = urllib.request.Request(url, headers={"Accept": "application/json", "User-Agent": "dev-workflows-model-catalog/1"})
|
|
with urllib.request.urlopen(request, timeout=timeout) as response:
|
|
body = response.read()
|
|
final_url = response.geturl()
|
|
return json.loads(body), final_url, sha256(body)
|
|
|
|
|
|
def _unwrap_catalog(payload: Any) -> list[dict[str, Any]]:
|
|
if isinstance(payload, list):
|
|
models = payload
|
|
elif isinstance(payload, dict):
|
|
models = next((payload[k] for k in ("data", "models", "items") if isinstance(payload.get(k), list)), None)
|
|
if models is None:
|
|
raise ValueError("formato de catálogo não reconhecido")
|
|
else:
|
|
raise ValueError("catálogo não é JSON de objetos")
|
|
if any(not isinstance(item, dict) for item in models):
|
|
raise ValueError("catálogo contém item não-objeto")
|
|
return models
|
|
|
|
|
|
def parse_provider_catalog(payload: Any, provider: str, source_url: str, captured_at: str | None = None) -> list[dict[str, Any]]:
|
|
"""Converte catálogos Zen/Go sem sintetizar IDs ou variantes."""
|
|
captured_at = captured_at or utc_now()
|
|
result: list[dict[str, Any]] = []
|
|
seen: set[tuple[str, str | None]] = set()
|
|
for item in _unwrap_catalog(payload):
|
|
raw_id = item.get("id") or item.get("model_id")
|
|
if not isinstance(raw_id, str) or not raw_id.strip() or "/" in raw_id or "#" in raw_id:
|
|
raise ValueError(f"ID inválido no catálogo {provider}")
|
|
model_id = raw_id.strip()
|
|
variants = item.get("variants")
|
|
if variants is None:
|
|
variants = [None]
|
|
elif isinstance(variants, dict):
|
|
variants = list(variants)
|
|
if not isinstance(variants, list) or any(v is not None and (not isinstance(v, str) or not v) for v in variants):
|
|
raise ValueError(f"variantes inválidas para {provider}/{model_id}")
|
|
for variant in variants:
|
|
key = (model_id, variant)
|
|
if key in seen:
|
|
continue
|
|
seen.add(key)
|
|
model_key = f"{provider}/{model_id}" + (f"#{variant}" if variant else "")
|
|
result.append({
|
|
"name": str(item.get("name") or model_id), "tool": "opencode", "provider": provider,
|
|
"model_id": model_id, "variant": variant, "model_key": model_key,
|
|
"context_length": item.get("context_length") or item.get("context_window"),
|
|
"modalities": item.get("modalities"), "tool_calling": item.get("tool_calling"),
|
|
"billing_mode": "zen_balance" if provider == "opencode" else "go_quota",
|
|
"availability": {"listed": True, "authenticated": None, "entitled": None, "verified_at": None},
|
|
"public_source": source_url, "captured_at": captured_at,
|
|
})
|
|
return result
|
|
|
|
|
|
_OR_PRICE_FIELDS = {
|
|
"input": "prompt", "output": "completion", "cache_read": "input_cache_read",
|
|
"cache_write": "input_cache_write", "cache_write_5m": "input_cache_write_5m",
|
|
"cache_write_1h": "input_cache_write_1h", "cache_write_24h": "input_cache_write_24h",
|
|
}
|
|
|
|
|
|
def _public_context_tiers(overrides: Any, prices: dict[str, Any], source_url: str,
|
|
captured_at: str) -> list[dict[str, Any]]:
|
|
if not isinstance(overrides, list):
|
|
return []
|
|
tiers = []
|
|
for override in overrides:
|
|
if not isinstance(override, dict):
|
|
continue
|
|
threshold = override.get("min_prompt_tokens")
|
|
if isinstance(threshold, bool) or not isinstance(threshold, int) or threshold < 0:
|
|
continue
|
|
tier_prices = dict(prices)
|
|
for target, source in _OR_PRICE_FIELDS.items():
|
|
if source in override:
|
|
parsed = decimal_value(override[source])
|
|
tier_prices[target] = decimal_text(parsed) if parsed is not None and parsed >= 0 else None
|
|
tiers.append({"min_context_tokens": threshold, "price_per_token_usd": tier_prices,
|
|
"source_url": source_url, "captured_at": captured_at})
|
|
return sorted(tiers, key=lambda tier: tier["min_context_tokens"])
|
|
|
|
|
|
def parse_openrouter(payload: Any, overrides: dict[str, Any] | None = None, source_url: str = OPENROUTER_URL,
|
|
captured_at: str | None = None) -> dict[str, dict[str, Any]]:
|
|
"""Preserva tarifas USD/token, variantes de cache e condições de preço."""
|
|
captured_at = captured_at or utc_now()
|
|
overrides = overrides or {}
|
|
output = {}
|
|
fields = _OR_PRICE_FIELDS
|
|
for item in _unwrap_catalog(payload):
|
|
model_id = item.get("id")
|
|
if not isinstance(model_id, str) or not model_id or any(char.isspace() for char in model_id):
|
|
raise ValueError("OpenRouter contém ID inválido")
|
|
if model_id in output:
|
|
raise ValueError(f"ID OpenRouter duplicado: {model_id}")
|
|
pricing = item.get("pricing") or {}
|
|
if not isinstance(pricing, dict):
|
|
raise ValueError(f"pricing inválido para {model_id}")
|
|
prices = {}
|
|
unsupported_prices = {}
|
|
for target, source in fields.items():
|
|
raw = pricing.get(source)
|
|
price = decimal_value(raw) if raw is not None else None
|
|
if price is not None and price < 0:
|
|
unsupported_prices[target] = str(raw)
|
|
price = None # Credit/rebate is not a usable nonnegative cost estimate.
|
|
prices[target] = decimal_text(price)
|
|
base = {
|
|
"id": model_id, "name": item.get("name"), "canonical_slug": item.get("canonical_slug"),
|
|
"price_per_token_usd": prices, "price_unit": "USD/token",
|
|
"display_usd_per_1m": {k: decimal_text(decimal_value(v) * Decimal(1_000_000)) if v is not None else None for k, v in prices.items()},
|
|
"conditions": {
|
|
"context_length": item.get("context_length"), "top_provider": item.get("top_provider"),
|
|
"per_request_limits": item.get("per_request_limits"),
|
|
"pricing_metadata": {k: v for k, v in pricing.items() if k not in fields.values()},
|
|
"unsupported_negative_prices": unsupported_prices,
|
|
},
|
|
"source_url": source_url, "captured_at": captured_at, "source_hash": sha256(canonical_json(item)),
|
|
}
|
|
public_tiers = _public_context_tiers(pricing.get("overrides"), prices, source_url, captured_at)
|
|
if public_tiers:
|
|
base["conditions"]["context_tiers"] = public_tiers
|
|
override = overrides.get(model_id)
|
|
if override is not None:
|
|
if not isinstance(override, dict):
|
|
raise ValueError(f"override deve ser objeto: {model_id}")
|
|
allowed = {"price_per_token_usd", "display_usd_per_1m", "context_length", "ttl", "endpoint",
|
|
"price_conditions", "context_tiers", "source_url", "captured_at"}
|
|
if set(override) - allowed:
|
|
raise ValueError(f"override contém campo não permitido: {model_id}")
|
|
has_base_prices = "price_per_token_usd" in override or "display_usd_per_1m" in override
|
|
if has_base_prices:
|
|
if not _secure_url(override.get("source_url")) or _parse_utc(override.get("captured_at")) is None:
|
|
raise ValueError(f"override de preço exige source_url https e captured_at: {model_id}")
|
|
base["source_url"], base["captured_at"] = override["source_url"], override["captured_at"]
|
|
for unit_field, divisor in (("price_per_token_usd", Decimal(1)), ("display_usd_per_1m", Decimal(1_000_000))):
|
|
if unit_field not in override:
|
|
continue
|
|
override_prices = override[unit_field]
|
|
if not isinstance(override_prices, dict):
|
|
raise ValueError(f"{unit_field} deve ser objeto: {model_id}")
|
|
for key, value in override_prices.items():
|
|
if key not in prices:
|
|
raise ValueError(f"campo de tarifa desconhecido: {key}")
|
|
parsed = decimal_value(value)
|
|
if parsed is not None and parsed < 0:
|
|
raise ValueError(f"preço override negativo: {model_id}")
|
|
prices[key] = decimal_text(parsed / divisor) if parsed is not None else None
|
|
for field in ("context_length", "ttl", "endpoint", "price_conditions"):
|
|
if field in override:
|
|
base["conditions"][field] = override[field]
|
|
if "endpoint" in override and not _secure_url(override["endpoint"]):
|
|
raise ValueError(f"endpoint override deve ser HTTPS sem credenciais/query: {model_id}")
|
|
if "context_tiers" in override:
|
|
raw_tiers = override["context_tiers"]
|
|
if not isinstance(raw_tiers, list):
|
|
raise ValueError(f"context_tiers deve ser lista: {model_id}")
|
|
tiers = []
|
|
thresholds = set()
|
|
for tier in raw_tiers:
|
|
tier_fields = {"min_context_tokens", "price_per_token_usd", "source_url", "captured_at"}
|
|
if not isinstance(tier, dict) or set(tier) != tier_fields:
|
|
raise ValueError(f"context tier inválido: {model_id}")
|
|
threshold = tier["min_context_tokens"]
|
|
if isinstance(threshold, bool) or not isinstance(threshold, int) or threshold < 0 or threshold in thresholds:
|
|
raise ValueError(f"limite de contexto inválido/duplicado: {model_id}")
|
|
thresholds.add(threshold)
|
|
if not _secure_url(tier["source_url"]) or _parse_utc(tier["captured_at"]) is None:
|
|
raise ValueError(f"context tier requer origem e data auditáveis: {model_id}")
|
|
tier_prices = tier["price_per_token_usd"]
|
|
if not isinstance(tier_prices, dict) or not tier_prices or set(tier_prices) - set(prices):
|
|
raise ValueError(f"tarifas do context tier inválidas: {model_id}")
|
|
normalized = dict(prices)
|
|
for key, value in tier_prices.items():
|
|
parsed = decimal_value(value)
|
|
if parsed is None or parsed < 0:
|
|
raise ValueError(f"tarifa de contexto inválida: {model_id}")
|
|
normalized[key] = decimal_text(parsed)
|
|
tiers.append({"min_context_tokens": threshold, "price_per_token_usd": normalized,
|
|
"source_url": tier["source_url"], "captured_at": tier["captured_at"]})
|
|
base["conditions"]["context_tiers"] = sorted(
|
|
tiers + base["conditions"].get("context_tiers", []),
|
|
key=lambda x: x["min_context_tokens"])
|
|
base["override"] = {k: v for k, v in override.items() if k in allowed}
|
|
base["price_per_token_usd"] = {k: decimal_text(decimal_value(v)) if v is not None else None for k, v in prices.items()}
|
|
base["display_usd_per_1m"] = {
|
|
k: decimal_text(decimal_value(v) * Decimal(1_000_000)) if v is not None else None
|
|
for k, v in base["price_per_token_usd"].items()
|
|
}
|
|
base["source_hash"] = sha256(canonical_json({"api_model": item, "override": override}))
|
|
output[model_id] = base
|
|
if set(overrides) - set(output):
|
|
raise ValueError("override refere ID OpenRouter ausente")
|
|
return output
|
|
|
|
|
|
def openrouter_price_for_profile(record: dict[str, Any], tokens: dict[str, Any]) -> dict[str, Any]:
|
|
selected = dict(record)
|
|
conditions = record.get("conditions") or {}
|
|
tiers = conditions.get("context_tiers") or []
|
|
context_raw = tokens.get("context_length", tokens.get("input"))
|
|
context = decimal_value(context_raw) if context_raw is not None else None
|
|
if context is not None and not tiers:
|
|
metadata = conditions.get("pricing_metadata")
|
|
if isinstance(metadata, dict):
|
|
tiers = _public_context_tiers(metadata.get("overrides"), record["price_per_token_usd"],
|
|
record["source_url"], record["captured_at"])
|
|
if context is not None:
|
|
matching = [tier for tier in tiers if Decimal(tier["min_context_tokens"]) <= context]
|
|
if matching:
|
|
tier = max(matching, key=lambda item: item["min_context_tokens"])
|
|
selected["price_per_token_usd"] = tier["price_per_token_usd"]
|
|
selected["source_url"] = tier["source_url"]
|
|
selected["captured_at"] = tier["captured_at"]
|
|
selected["applied_context_tier"] = tier["min_context_tokens"]
|
|
return selected
|
|
|
|
|
|
def reference_cost(price_per_token_usd: dict[str, Any], tokens: dict[str, Any]) -> Decimal | None:
|
|
"""C_OR em USD; None se qualquer tarifa necessária ao perfil estiver ausente."""
|
|
aliases = {"input": ("input", "prompt"), "output": ("output", "completion"),
|
|
"cache_read": ("cache_read", "input_cache_read"), "cache_write": ("cache_write", "input_cache_write")}
|
|
total = Decimal(0)
|
|
for kind, names in aliases.items():
|
|
raw_count = tokens.get(kind, 0)
|
|
if isinstance(raw_count, bool) or isinstance(raw_count, float):
|
|
raise ValueError("contagem de tokens deve ser inteiro ou decimal exato")
|
|
count = Decimal(str(raw_count))
|
|
if not count.is_finite() or count < 0:
|
|
raise ValueError(f"contagem de tokens inválida: {kind}")
|
|
if count == 0:
|
|
continue
|
|
names_for_cost = names
|
|
if kind == "cache_write" and tokens.get("cache_write_ttl"):
|
|
ttl = tokens["cache_write_ttl"]
|
|
if ttl not in ("5m", "1h", "24h"):
|
|
raise ValueError("cache_write_ttl precisa ser 5m, 1h ou 24h")
|
|
names_for_cost = (f"cache_write_{ttl}", f"input_cache_write_{ttl}")
|
|
raw_price = next((price_per_token_usd[name] for name in names_for_cost if name in price_per_token_usd), None)
|
|
price = decimal_value(raw_price) if raw_price is not None else None
|
|
if price is None:
|
|
return None
|
|
if price < 0:
|
|
raise ValueError("preço por token não pode ser negativo")
|
|
total += count * price
|
|
return total
|
|
|
|
|
|
def validate_aa_snapshot(payload: Any) -> list[dict[str, Any]]:
|
|
"""Aceita exclusivamente export manual auditável do Coding Agent Index v1.5."""
|
|
if not isinstance(payload, dict):
|
|
raise ValueError("snapshot AA deve ser objeto")
|
|
allowed_root = {"metric_id", "version", "ingestion", "terms_accepted", "source_url", "attribution", "captured_at", "records"}
|
|
if set(payload) - allowed_root:
|
|
raise ValueError("snapshot AA contém campo não permitido")
|
|
if payload.get("metric_id") != AA_METRIC or str(payload.get("version")) != AA_VERSION:
|
|
raise ValueError("somente aa_coding_agent_index v1.5 é aceito; índices não são intercambiáveis")
|
|
if payload.get("ingestion") != "manual_audited" or payload.get("terms_accepted") is not True:
|
|
raise ValueError("snapshot precisa ser importação manual consentida (terms_accepted=true)")
|
|
source_url = payload.get("source_url")
|
|
if not _secure_url(source_url, "artificialanalysis.ai"):
|
|
raise ValueError("source_url oficial da Artificial Analysis é obrigatório")
|
|
if not isinstance(payload.get("attribution"), str) or not payload["attribution"]:
|
|
raise ValueError("atribuição AA obrigatória")
|
|
captured_at = payload.get("captured_at")
|
|
captured_time = _parse_utc(captured_at)
|
|
if captured_time is None or captured_time > dt.datetime.now(dt.timezone.utc):
|
|
raise ValueError("captured_at obrigatório, UTC/offset-aware e não futuro")
|
|
records = payload.get("records")
|
|
if not isinstance(records, list):
|
|
raise ValueError("records deve ser lista")
|
|
validated = []
|
|
seen_keys: set[tuple[str, str, str | None]] = set()
|
|
allowed_record = {"metric_id", "version", "provider", "model_id", "variant", "score", "agent", "harness",
|
|
"settings", "match_grade", "evidence_url", "components", "fallback", "effort", "reasoning", "configuration"}
|
|
for record in records:
|
|
if not isinstance(record, dict) or set(record) - allowed_record:
|
|
raise ValueError("registro AA inválido ou contém campo não permitido")
|
|
if record.get("metric_id", AA_METRIC) != AA_METRIC or str(record.get("version", AA_VERSION)) != AA_VERSION:
|
|
raise ValueError("índice/versão diferente não pode ser misturado")
|
|
provider, model_id = record.get("provider"), record.get("model_id")
|
|
if not isinstance(provider, str) or not isinstance(model_id, str) or not model_id or "/" in model_id or "#" in model_id:
|
|
raise ValueError("mapeamento provider/model_id exato obrigatório")
|
|
variant = record.get("variant")
|
|
if variant is not None and (not isinstance(variant, str) or not variant):
|
|
raise ValueError("variante AA inválida")
|
|
key = (provider, model_id, variant)
|
|
if key in seen_keys:
|
|
raise ValueError("benchmark inconsistente: modelo/variante duplicado no snapshot")
|
|
seen_keys.add(key)
|
|
score = decimal_value(record.get("score"))
|
|
if score is None or score < 0 or score > 100:
|
|
raise ValueError("score AA precisa estar no intervalo 0..100")
|
|
grade = record.get("match_grade")
|
|
if grade not in ("exact", "proxy"):
|
|
raise ValueError("match_grade deve ser exact ou proxy, sem fuzzy match")
|
|
for key in ("agent", "harness", "settings", "evidence_url"):
|
|
if key not in record or record[key] in (None, ""):
|
|
raise ValueError(f"campo de proveniência AA obrigatório: {key}")
|
|
if not _secure_url(record["evidence_url"], "artificialanalysis.ai"):
|
|
raise ValueError("evidence_url AA inválida")
|
|
validated.append({
|
|
**record, "metric_id": AA_METRIC, "version": AA_VERSION, "score": decimal_text(score),
|
|
"source_url": source_url, "attribution": payload["attribution"], "observed_at": captured_at,
|
|
"snapshot_hash": sha256(canonical_json(payload)),
|
|
})
|
|
return validated
|
|
|
|
|
|
def validate_availability_snapshot(payload: Any) -> dict[tuple[str, str, str | None], dict[str, Any]]:
|
|
"""Validates explicit account evidence without accepting or retaining secrets."""
|
|
if not isinstance(payload, dict) or payload.get("tool") not in ("opencode", "omp") or payload.get("ingestion") != "manual_audited":
|
|
raise ValueError("availability precisa ser snapshot manual auditado de opencode ou omp")
|
|
tool = payload["tool"]
|
|
if payload.get("user_confirmed") is not True:
|
|
raise ValueError("snapshot de disponibilidade exige user_confirmed=true")
|
|
captured_at = payload.get("captured_at")
|
|
if not isinstance(captured_at, str):
|
|
raise ValueError("captured_at de disponibilidade obrigatório")
|
|
try:
|
|
captured_dt = dt.datetime.fromisoformat(captured_at.replace("Z", "+00:00"))
|
|
except ValueError as exc:
|
|
raise ValueError("captured_at de disponibilidade inválido") from exc
|
|
if captured_dt.tzinfo is None or captured_dt > dt.datetime.now(dt.timezone.utc):
|
|
raise ValueError("captured_at precisa ser UTC/offset-aware e não futuro")
|
|
records = payload.get("records")
|
|
if not isinstance(records, list):
|
|
raise ValueError("records de disponibilidade deve ser lista")
|
|
result = {}
|
|
source_hash = sha256(canonical_json(payload))
|
|
allowed_fields = {"provider", "model_id", "variant", "local_listed", "authenticated", "entitled",
|
|
"verified_at", "source", "balance_confirmed", "balance_verified_at"}
|
|
for record in records:
|
|
if not isinstance(record, dict) or set(record) - allowed_fields:
|
|
raise ValueError("snapshot de disponibilidade contém campos não permitidos")
|
|
provider, model_id, variant = record.get("provider"), record.get("model_id"), record.get("variant")
|
|
if not isinstance(provider, str) or not isinstance(model_id, str) or not model_id or "/" in model_id or "#" in model_id or (variant is not None and not isinstance(variant, str)):
|
|
raise ValueError("provider/model_id/variant exatos são obrigatórios")
|
|
if any(field not in record for field in ("local_listed", "authenticated", "entitled", "verified_at", "source")):
|
|
raise ValueError("snapshot deve declarar todos os campos de disponibilidade")
|
|
key = (provider, model_id, variant)
|
|
if key in result:
|
|
raise ValueError("snapshot de disponibilidade contém ID/variante duplicado")
|
|
for field in ("local_listed", "authenticated", "entitled"):
|
|
if record[field] is not None and not isinstance(record[field], bool):
|
|
raise ValueError(f"{field} deve ser bool ou null")
|
|
balance_confirmed = record.get("balance_confirmed")
|
|
if balance_confirmed is not None and not isinstance(balance_confirmed, bool):
|
|
raise ValueError("balance_confirmed deve ser bool ou null")
|
|
balance_verified_at = record.get("balance_verified_at")
|
|
balance_time = _parse_utc(balance_verified_at) if balance_verified_at else None
|
|
if balance_time is not None and (balance_time > dt.datetime.now(dt.timezone.utc) or balance_confirmed is not True):
|
|
raise ValueError("balance_verified_at requer confirmação explícita e não pode ser futuro")
|
|
if balance_confirmed is True and balance_time is None:
|
|
raise ValueError("saldo confirmado exige balance_verified_at UTC válido")
|
|
verified_at = _parse_utc(record["verified_at"])
|
|
if verified_at is None or verified_at > dt.datetime.now(dt.timezone.utc) or not isinstance(record["source"], str) or not record["source"]:
|
|
raise ValueError("source e verified_at UTC válido são necessários para auditar a disponibilidade")
|
|
result[key] = {
|
|
"local_listed": record["local_listed"], "authenticated": record["authenticated"],
|
|
"entitled": record["entitled"], "verified_at": record["verified_at"],
|
|
"source": "manual_audited", "source_hash": source_hash,
|
|
"balance_confirmed": balance_confirmed, "balance_verified_at": balance_verified_at,
|
|
}
|
|
return result
|
|
|
|
|
|
|
|
def validate_native_tariff_overrides(payload: Any) -> dict[str, dict[str, Any]]:
|
|
"""Valida cobrança nativa/quotas manuais; preços OpenRouter nunca são tarifa."""
|
|
if not isinstance(payload, dict):
|
|
raise ValueError("native_tariffs deve ser objeto JSON")
|
|
result = {}
|
|
allowed = {"billing_mode", "price_per_token_usd", "quota_window_remaining", "quota_window_debit",
|
|
"use_balance_fallback", "source_url", "captured_at", "account_confirmed", "available_balance_usd"}
|
|
for model_key, value in payload.items():
|
|
if not isinstance(model_key, str) or not isinstance(value, dict) or set(value) - allowed:
|
|
raise ValueError("override native contém estrutura/campo inválido")
|
|
if value.get("account_confirmed") is not True:
|
|
raise ValueError("tarifa/cota native exige confirmação explícita da conta")
|
|
mode = value.get("billing_mode")
|
|
if mode not in ("zen_balance", "go_quota", "omp_payg"):
|
|
raise ValueError("billing_mode native inválido")
|
|
if not _secure_url(value.get("source_url")):
|
|
raise ValueError("source_url HTTPS de tarifa/cota native obrigatório")
|
|
captured_at = value.get("captured_at")
|
|
try:
|
|
captured = dt.datetime.fromisoformat(captured_at.replace("Z", "+00:00"))
|
|
except (AttributeError, ValueError) as exc:
|
|
raise ValueError("captured_at de tarifa/cota native inválido") from exc
|
|
if captured.tzinfo is None or captured > dt.datetime.now(dt.timezone.utc):
|
|
raise ValueError("captured_at de tarifa/cota native precisa ser aware e não futuro")
|
|
record = {k: value[k] for k in ("billing_mode", "source_url", "captured_at", "account_confirmed")}
|
|
if mode in ("zen_balance", "omp_payg"):
|
|
rates = value.get("price_per_token_usd")
|
|
if not isinstance(rates, dict) or not rates:
|
|
raise ValueError("tarifa nativa precisa de price_per_token_usd")
|
|
record["price_per_token_usd"] = {
|
|
k: decimal_text(decimal_value(v)) for k, v in rates.items()
|
|
if k in ("input", "output", "cache_read", "cache_write", "cache_write_5m", "cache_write_1h", "cache_write_24h")
|
|
}
|
|
if len(record["price_per_token_usd"]) != len(rates) or any(v is None or Decimal(v) < 0 for v in record["price_per_token_usd"].values()):
|
|
raise ValueError("componentes de tarifa nativa inválidos")
|
|
balance = decimal_value(value.get("available_balance_usd"))
|
|
if balance is None or balance < 0:
|
|
raise ValueError("Zen/OMP payg exige saldo disponível confirmado")
|
|
record["available_balance_usd"] = decimal_text(balance)
|
|
else:
|
|
remaining, debit = value.get("quota_window_remaining"), value.get("quota_window_debit")
|
|
if not isinstance(remaining, dict) or not isinstance(debit, dict) or any(k not in remaining or k not in debit for k in ("5h", "weekly", "monthly")):
|
|
raise ValueError("cota Go exige valores por modelo em 5h/weekly/monthly")
|
|
record["quota_window_remaining"] = {k: decimal_text(decimal_value(remaining[k])) for k in ("5h", "weekly", "monthly")}
|
|
record["quota_window_debit"] = {k: decimal_text(decimal_value(debit[k])) for k in ("5h", "weekly", "monthly")}
|
|
if any(record[key][window] is None or Decimal(record[key][window]) < 0 for key in ("quota_window_remaining", "quota_window_debit") for window in ("5h", "weekly", "monthly")):
|
|
raise ValueError("quota Go precisa ter todos os valores decimais não negativos")
|
|
fallback = value.get("use_balance_fallback", False)
|
|
if not isinstance(fallback, bool):
|
|
raise ValueError("use_balance_fallback precisa ser bool")
|
|
record["use_balance_fallback"] = fallback
|
|
result[model_key] = record
|
|
return result
|
|
|
|
def discover_local_models(provider: str, executable: str = "opencode", runner: Callable[..., Any] = subprocess.run) -> dict[str, Any]:
|
|
"""Lista IDs pela CLI sem persistir stdout bruto, stderr, environment ou credenciais."""
|
|
try:
|
|
result = runner([executable, "models", provider, "--refresh"], capture_output=True, text=True, timeout=30, check=False)
|
|
except (OSError, subprocess.TimeoutExpired):
|
|
return {"ok": False, "models": [], "reason": "cli_local_indisponivel"}
|
|
if result.returncode != 0:
|
|
return {"ok": False, "models": [], "reason": "descoberta_local_falhou"}
|
|
found: set[str] = set()
|
|
# Apenas linhas integralmente compostas por um ID de modelo permitido.
|
|
for line in result.stdout.splitlines():
|
|
candidate = line.strip().strip("`'\" ")
|
|
if candidate.startswith(provider + "/"):
|
|
candidate = candidate[len(provider) + 1:]
|
|
if re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9._:+/-]*(?:#[A-Za-z0-9._+-]+)?", candidate):
|
|
if " " not in candidate and not candidate.startswith("-"):
|
|
found.add(candidate)
|
|
return {"ok": True, "models": sorted(found), "reason": None}
|
|
|
|
|
|
def parse_omp_models(payload: Any, provider: str, captured_at: str | None = None) -> list[dict[str, Any]]:
|
|
"""Converte a listagem JSON local do OMP; ela prova listagem, nunca entitlement."""
|
|
captured_at = captured_at or utc_now()
|
|
if isinstance(payload, list):
|
|
models = payload
|
|
elif isinstance(payload, dict):
|
|
models = next((payload[key] for key in ("models", "data", "items") if isinstance(payload.get(key), list)), None)
|
|
if models is None:
|
|
raise ValueError("formato de listagem OMP não reconhecido")
|
|
else:
|
|
raise ValueError("listagem OMP não é JSON de modelos")
|
|
if any(not isinstance(item, dict) for item in models):
|
|
raise ValueError("listagem OMP contém modelo não-objeto")
|
|
rows = []
|
|
seen: set[tuple[str, str | None]] = set()
|
|
for item in models:
|
|
item_provider = item.get("provider", provider)
|
|
raw_id = item.get("id") or item.get("model_id") or item.get("model")
|
|
if not isinstance(item_provider, str) or item_provider != provider or not isinstance(raw_id, str):
|
|
raise ValueError("provider e ID exatos são obrigatórios na listagem OMP")
|
|
model_id = raw_id
|
|
selector = item.get("selector")
|
|
if selector is not None:
|
|
if not isinstance(selector, str) or not selector.startswith(provider + "/") or selector[len(provider) + 1:] != model_id:
|
|
raise ValueError("selector OMP qualificado diverge do provider/ID")
|
|
if not re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9._:+/-]*", model_id):
|
|
raise ValueError("ID inválido na listagem OMP")
|
|
thinking = item.get("thinking")
|
|
if thinking is not None and (not isinstance(thinking, list) or any(
|
|
not isinstance(level, str) or level not in OMP_THINKING_VARIANTS for level in thinking)):
|
|
raise ValueError("níveis de thinking inválidos na listagem OMP")
|
|
key = (model_id, None)
|
|
if key in seen:
|
|
continue
|
|
seen.add(key)
|
|
model_key = f"{provider}/{model_id}"
|
|
rows.append({
|
|
"name": model_id, "tool": "omp", "provider": provider, "model_id": model_id,
|
|
"variant": None, "thinking": thinking, "model_key": model_key, "context_length": None,
|
|
"modalities": None, "tool_calling": None, "billing_mode": "unknown",
|
|
"availability": {"listed": True, "authenticated": None, "entitled": None, "verified_at": None},
|
|
"public_source": "omp models --json (listagem local)", "captured_at": captured_at,
|
|
})
|
|
return rows
|
|
|
|
|
|
def discover_omp_models(provider: str, executable: str = "omp",
|
|
runner: Callable[..., Any] = subprocess.run) -> dict[str, Any]:
|
|
"""Lê IDs OMP localmente e descarta stdout/stderr brutos e dados de conta."""
|
|
if not re.fullmatch(r"[A-Za-z0-9_.-]+", provider):
|
|
return {"ok": False, "models": [], "reason": "provider_omp_invalido"}
|
|
try:
|
|
result = runner([executable, "models", provider, "--json"], capture_output=True, text=True, timeout=30, check=False)
|
|
except (OSError, subprocess.TimeoutExpired):
|
|
return {"ok": False, "models": [], "reason": "cli_omp_indisponivel"}
|
|
if result.returncode != 0 or not isinstance(result.stdout, str):
|
|
return {"ok": False, "models": [], "reason": "listagem_omp_indisponivel"}
|
|
try:
|
|
payload = json.loads(result.stdout)
|
|
rows = parse_omp_models(payload, provider)
|
|
except (json.JSONDecodeError, ValueError):
|
|
return {"ok": False, "models": [], "reason": "listagem_omp_invalida"}
|
|
selectors = {row["model_key"] for row in rows}
|
|
for row in rows:
|
|
if row.get("variant") is None and isinstance(row.get("thinking"), list):
|
|
selectors.update(f'{row["provider"]}/{row["model_id"]}#{level}' for level in row["thinking"])
|
|
return {
|
|
"ok": True, "models": sorted(selectors), "rows": rows,
|
|
"source_hash": sha256(result.stdout.encode("utf-8")), "verified_at": utc_now(), "reason": None,
|
|
}
|
|
|
|
|
|
def curated_mapping(provider: str, model_id: str, variant: str | None = None) -> dict[str, str] | None:
|
|
match = _CURATED.get((provider, model_id, variant))
|
|
if match is None:
|
|
return None
|
|
return {"openrouter_id": match[0], "provenance": match[1], "match_method": "exact_curated"}
|
|
|
|
|
|
def _atomic_json(path: Path, payload: Any) -> None:
|
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
fd, temporary = tempfile.mkstemp(prefix=path.name + ".", dir=path.parent)
|
|
try:
|
|
with os.fdopen(fd, "w", encoding="utf-8") as stream:
|
|
json.dump(payload, stream, ensure_ascii=False, sort_keys=True, indent=2)
|
|
stream.write("\n")
|
|
stream.flush()
|
|
os.fsync(stream.fileno())
|
|
os.replace(temporary, path)
|
|
finally:
|
|
try:
|
|
os.unlink(temporary)
|
|
except FileNotFoundError:
|
|
pass
|
|
|
|
|
|
def _read_json(path: Path, default: Any) -> Any:
|
|
try:
|
|
with path.open(encoding="utf-8") as stream:
|
|
return json.load(stream)
|
|
except FileNotFoundError:
|
|
return default
|
|
|
|
|
|
def _http_fetch(url: str) -> tuple[Any, str, str]:
|
|
return _json_request(url)
|
|
|
|
|
|
def sync(tool: str = "opencode", providers: Iterable[str] | None = None,
|
|
cache_dir: str | Path | None = None, fetcher: Callable[[str], tuple[Any, str, str]] = _http_fetch,
|
|
local_discoverer: Callable[[str], dict[str, Any]] = discover_local_models,
|
|
aa_snapshot: str | Path | None = None, price_overrides: str | Path | None = None,
|
|
availability_snapshot: str | Path | None = None,
|
|
omp_discoverer: Callable[[str], dict[str, Any]] = discover_omp_models) -> dict[str, Any]:
|
|
"""Atualiza catálogos; listas OMP são locais e nunca provam entitlement."""
|
|
if tool not in ("opencode", "omp"):
|
|
raise ValueError("sync público/local suportado apenas para opencode ou omp")
|
|
cache = Path(cache_dir) if cache_dir is not None else CACHE_DIR
|
|
requested = tuple(dict.fromkeys(providers if providers is not None else
|
|
(() if tool == "omp" else ("opencode", "opencode-go"))))
|
|
if tool == "omp":
|
|
if not requested:
|
|
raise ValueError("OMP exige providers explicitamente autorizados")
|
|
if any(not isinstance(provider, str) or not re.fullmatch(r"[A-Za-z0-9_.-]+", provider) for provider in requested):
|
|
raise ValueError("provider OMP inválido")
|
|
provider_urls = {}
|
|
else:
|
|
provider_urls = {"opencode": ZEN_URL, "opencode-go": GO_URL}
|
|
if not requested or any(provider not in provider_urls for provider in requested):
|
|
raise ValueError("provider desconhecido; use opencode,opencode-go")
|
|
source_hashes: dict[str, str] = {}
|
|
override_bytes = Path(price_overrides).read_bytes() if price_overrides else b""
|
|
override_data = json.loads(override_bytes) if override_bytes else {}
|
|
if not isinstance(override_data, dict):
|
|
raise ValueError("arquivo de overrides deve ser objeto JSON")
|
|
reserved = {"openrouter", "native_tariffs"}
|
|
or_overrides = override_data.get("openrouter", {k: v for k, v in override_data.items() if k not in reserved})
|
|
native_overrides = validate_native_tariff_overrides(override_data.get("native_tariffs", {}))
|
|
if override_bytes:
|
|
source_hashes["overrides"] = sha256(override_bytes)
|
|
if not isinstance(or_overrides, dict):
|
|
raise ValueError("overrides OpenRouter deve ser objeto")
|
|
availability_map: dict[tuple[str, str, str | None], dict[str, Any]] = {}
|
|
if availability_snapshot:
|
|
availability_bytes = Path(availability_snapshot).read_bytes()
|
|
availability_payload = json.loads(availability_bytes)
|
|
if not isinstance(availability_payload, dict) or availability_payload.get("tool") != tool:
|
|
raise ValueError("tool do snapshot de disponibilidade diverge do catálogo")
|
|
availability_map = validate_availability_snapshot(availability_payload)
|
|
source_hashes["availability"] = sha256(availability_bytes)
|
|
|
|
catalog_rows = []
|
|
captured = utc_now()
|
|
if aa_snapshot:
|
|
snapshot_bytes = Path(aa_snapshot).read_bytes()
|
|
aa_payload = json.loads(snapshot_bytes)
|
|
aa_records = validate_aa_snapshot(aa_payload)
|
|
_atomic_json(cache / f"aa-{AA_METRIC}-v{AA_VERSION}.json", {
|
|
"metric_id": AA_METRIC, "version": AA_VERSION, "captured_at": aa_payload["captured_at"],
|
|
"source_hash": sha256(snapshot_bytes), "records": aa_records,
|
|
})
|
|
source_hashes["aa"] = sha256(snapshot_bytes)
|
|
aa_stored = _read_json(cache / f"aa-{AA_METRIC}-v{AA_VERSION}.json", {"records": []})
|
|
scores = aa_stored.get("records", []) if isinstance(aa_stored, dict) else []
|
|
scores_by_key = {(r.get("provider"), r.get("model_id"), r.get("variant")): r for r in scores}
|
|
|
|
if tool == "opencode":
|
|
for provider in requested:
|
|
payload, source_url, digest = fetcher(provider_urls[provider])
|
|
catalog_rows.extend(parse_provider_catalog(payload, provider, source_url, captured))
|
|
source_hashes[provider] = digest
|
|
local_result = {p: local_discoverer(p) for p in requested}
|
|
else:
|
|
local_result = {p: omp_discoverer(p) for p in requested}
|
|
for provider, result in local_result.items():
|
|
if result.get("ok"):
|
|
catalog_rows.extend(result.get("rows", []))
|
|
source_hashes[provider] = result["source_hash"]
|
|
|
|
if tool == "omp":
|
|
base_rows = {(row["provider"], row["model_id"]): row for row in catalog_rows if row.get("variant") is None}
|
|
present = {row["model_key"] for row in catalog_rows}
|
|
for (provider, model_id, variant), _score in scores_by_key.items():
|
|
base_key = f"{provider}/{model_id}"
|
|
model_key = base_key + (f"#{variant}" if variant else "")
|
|
supported = base_rows.get((provider, model_id), {}).get("thinking")
|
|
if (variant not in OMP_THINKING_VARIANTS or model_key in present or
|
|
not isinstance(supported, list) or variant not in supported or
|
|
base_key not in local_result.get(provider, {}).get("models", [])):
|
|
continue
|
|
base = base_rows.get((provider, model_id))
|
|
if base is None:
|
|
continue
|
|
variant_row = dict(base)
|
|
variant_row["variant"] = variant
|
|
variant_row["model_key"] = model_key
|
|
variant_row["availability"] = dict(base.get("availability") or {})
|
|
catalog_rows.append(variant_row)
|
|
present.add(model_key)
|
|
|
|
applied_availability = set()
|
|
for row in catalog_rows:
|
|
provider = row["provider"]
|
|
local = local_result.get(provider, {"ok": False, "models": []})
|
|
local_ids = set(local.get("models", [])) if local.get("ok") else set()
|
|
catalog_key = (provider, row["model_id"], row.get("variant"))
|
|
manual = availability_map.get(catalog_key)
|
|
if manual:
|
|
applied_availability.add(catalog_key)
|
|
if tool == "omp":
|
|
local_listed = row["model_key"] in local_ids if local.get("ok") else None
|
|
else:
|
|
local_key = row["model_id"] + (f"#{row['variant']}" if row.get("variant") else "")
|
|
local_listed = local_key in local_ids if local.get("ok") else None
|
|
if manual and local.get("ok"):
|
|
local_listed = local_listed is True and manual["local_listed"] is True
|
|
row["availability"].update({
|
|
"local_listed": local_listed,
|
|
"authenticated": manual["authenticated"] if manual else None,
|
|
"entitled": manual["entitled"] if manual else None,
|
|
"verified_at": manual["verified_at"] if manual else (captured if local.get("ok") else None),
|
|
"local_verified_at": local.get("verified_at", captured) if local.get("ok") else None,
|
|
"account_verified_at": manual["verified_at"] if manual else None,
|
|
"balance_confirmed": manual.get("balance_confirmed") if manual else None,
|
|
"balance_verified_at": manual.get("balance_verified_at") if manual else None,
|
|
"source": "manual_audited" if manual else ("omp_models_cli" if tool == "omp" and local.get("ok") else "opencode_models_cli" if local.get("ok") else None),
|
|
"evidence_hash": manual["source_hash"] if manual else None,
|
|
})
|
|
if local_listed is not True:
|
|
row["availability_reason"] = "nao_listado_localmente" if local.get("ok") else local.get("reason", "verificacao_local_indisponivel")
|
|
elif manual is None:
|
|
row["availability_reason"] = "conta_nao_verificada"
|
|
elif manual["authenticated"] is not True:
|
|
row["availability_reason"] = "autenticacao_nao_confirmada"
|
|
elif manual["entitled"] is not True:
|
|
row["availability_reason"] = "entitlement_nao_confirmado"
|
|
else:
|
|
row["availability_reason"] = None
|
|
row["source_hash"] = source_hashes.get(provider)
|
|
row["mapping"] = curated_mapping(provider, row["model_id"], row.get("variant"))
|
|
native = native_overrides.get(row["model_key"])
|
|
if native:
|
|
if tool == "opencode" and native["billing_mode"] != row["billing_mode"]:
|
|
raise ValueError("billing_mode do override diverge do provider publicado")
|
|
if tool != "omp" and native["billing_mode"] == "omp_payg":
|
|
raise ValueError("OMP payg não pode ser roteado por outra ferramenta")
|
|
row["billing_mode"] = native["billing_mode"]
|
|
tariff_fields = ("billing_mode", "source_url", "captured_at", "account_confirmed",
|
|
"price_per_token_usd", "available_balance_usd")
|
|
row["native_tariff"] = {k: native[k] for k in tariff_fields if k in native}
|
|
if native["billing_mode"] == "go_quota":
|
|
row["quota_window_remaining"] = native["quota_window_remaining"]
|
|
row["quota_window_debit"] = native["quota_window_debit"]
|
|
row["quota_verified_at"] = native["captured_at"]
|
|
row["quota_source_url"] = native["source_url"]
|
|
row["use_balance_fallback"] = native["use_balance_fallback"]
|
|
invalid_availability = {key for key in set(availability_map) - applied_availability if key[0] in requested}
|
|
if invalid_availability:
|
|
raise ValueError("snapshot de disponibilidade referencia modelo/variante fora da listagem local")
|
|
invalid_native = {key for key in native_overrides if key not in {r["model_key"] for r in catalog_rows}}
|
|
if invalid_native:
|
|
raise ValueError("tarifa native referencia modelo/variante fora do catálogo")
|
|
|
|
prices_payload, price_url, price_hash = fetcher(OPENROUTER_URL)
|
|
prices = parse_openrouter(prices_payload, or_overrides, price_url, captured)
|
|
source_hashes["openrouter"] = price_hash
|
|
for row in catalog_rows:
|
|
row["score"] = scores_by_key.get((row["provider"], row["model_id"], row.get("variant")))
|
|
mapping = row.get("mapping")
|
|
price_id = mapping["openrouter_id"] if mapping else None
|
|
row["openrouter"] = prices.get(price_id) if price_id else None
|
|
row["or_price_per_token"] = row["openrouter"]["price_per_token_usd"] if row["openrouter"] else {"input": None, "output": None, "cache_read": None, "cache_write": None}
|
|
row["price_conditions"] = row["openrouter"]["conditions"] if row["openrouter"] else None
|
|
snapshot = {"schema_version": 1, "tool": tool, "captured_at": captured, "source_hashes": source_hashes,
|
|
"providers": list(requested), "models": catalog_rows, "openrouter": prices,
|
|
"benchmark": {"metric_id": AA_METRIC, "version": AA_VERSION, "records": scores}}
|
|
snapshot["snapshot_hash"] = sha256(canonical_json({k: v for k, v in snapshot.items() if k != "snapshot_hash"}))
|
|
_atomic_json(cache / f"catalog-{tool}.json", snapshot)
|
|
return {
|
|
"tool": tool, "model_count": len(catalog_rows), "provider_count": len(requested),
|
|
"local_discovery": {p: {"ok": v["ok"], "count": len(v["models"]), "reason": v.get("reason")}
|
|
for p, v in local_result.items()},
|
|
"snapshot_hash": snapshot["snapshot_hash"], "captured_at": captured, "source_hashes": source_hashes,
|
|
}
|
|
|
|
|
|
def load_snapshot(tool: str = "opencode", cache_dir: str | Path | None = None) -> dict[str, Any] | None:
|
|
cache = Path(cache_dir) if cache_dir is not None else CACHE_DIR
|
|
snapshot = _read_json(cache / f"catalog-{tool}.json", None)
|
|
return snapshot if isinstance(snapshot, dict) else None
|
|
|
|
|
|
def band_for_score(score: Any) -> str | None:
|
|
value = decimal_value(score)
|
|
if value is None:
|
|
return None
|
|
if Decimal(10) <= value < Decimal(20):
|
|
return "leve"
|
|
if Decimal(20) <= value < Decimal(50):
|
|
return "media"
|
|
if Decimal(50) <= value <= Decimal(100):
|
|
return "alta"
|
|
return None
|
|
|
|
|
|
def list_models(tool: str = "opencode", cache_dir: str | Path | None = None, show_missing: bool = True,
|
|
as_of_utc: str | None = None) -> list[dict[str, Any]]:
|
|
snapshot = load_snapshot(tool, cache_dir)
|
|
if snapshot is None:
|
|
return []
|
|
now = _parse_utc(as_of_utc) if as_of_utc is not None else dt.datetime.now(dt.timezone.utc)
|
|
if now is None:
|
|
raise ValueError("as_of_utc deve ser data/hora UTC ISO 8601")
|
|
result = []
|
|
for model in snapshot.get("models", []):
|
|
score = model.get("score") or {}
|
|
price = model.get("or_price_per_token") or {}
|
|
availability = model.get("availability") or {}
|
|
local_listed = availability.get("local_listed")
|
|
if not show_missing and local_listed is not True:
|
|
continue
|
|
display = model.get("openrouter", {}).get("display_usd_per_1m", {}) if model.get("openrouter") else {}
|
|
stale_reasons = []
|
|
or_snapshot = model.get("openrouter") or {}
|
|
price_time = _parse_utc(or_snapshot.get("captured_at")) if or_snapshot else None
|
|
if price_time is None:
|
|
stale_reasons.append("preco_referencial_ausente")
|
|
elif now - price_time > dt.timedelta(hours=24):
|
|
stale_reasons.append("preco_referencial_obsoleto")
|
|
if score:
|
|
score_time = _parse_utc(score.get("observed_at"))
|
|
if score_time is None or now - score_time > dt.timedelta(days=30):
|
|
stale_reasons.append("benchmark_obsoleto")
|
|
local_time = _parse_utc(availability.get("verified_at"))
|
|
if local_listed is True and (local_time is None or now - local_time > dt.timedelta(hours=24)):
|
|
stale_reasons.append("descoberta_local_precisa_revalidacao")
|
|
account_available = True if all(availability.get(k) is True for k in ("local_listed", "authenticated", "entitled")) else (False if any(availability.get(k) is False for k in ("local_listed", "authenticated", "entitled")) else "n/d")
|
|
native_or_quota = model.get("native_tariff") or "n/d"
|
|
if model.get("billing_mode") == "go_quota" and isinstance(native_or_quota, dict):
|
|
native_or_quota = {
|
|
**native_or_quota,
|
|
"quota_window_remaining": model.get("quota_window_remaining"),
|
|
"quota_window_debit": model.get("quota_window_debit"),
|
|
"use_balance_fallback": model.get("use_balance_fallback"),
|
|
}
|
|
result.append({
|
|
"nome": model.get("name") or "n/d", "tool": model.get("tool") or "n/d", "model_key": model.get("model_key") or "n/d",
|
|
"openrouter_id": model.get("mapping", {}).get("openrouter_id") if model.get("mapping") else "n/d",
|
|
"preco_or_entrada_usd_1m": display.get("input") if display.get("input") is not None else "n/d",
|
|
"preco_or_saida_usd_1m": display.get("output") if display.get("output") is not None else "n/d",
|
|
"preco_or_cache_leitura_usd_1m": display.get("cache_read") if display.get("cache_read") is not None else "n/d",
|
|
"preco_or_cache_escrita_usd_1m": display.get("cache_write") if display.get("cache_write") is not None else "n/d",
|
|
"preco_or_cache_escrita_1h_usd_1m": display.get("cache_write_1h") if display.get("cache_write_1h") is not None else "n/d",
|
|
"condicao_de_preco": model.get("price_conditions") or "n/d", "coding_index_versao": score.get("version") or "n/d",
|
|
"coding_index_nota": score.get("score") or "n/d", "benchmark_por_componente_se_publicado": score.get("components") or "n/d",
|
|
"harness_aa": score.get("harness") or "n/d", "reasoning_aa": score.get("settings") or "n/d",
|
|
"grau_do_match": score.get("match_grade") or "n/d", "faixa": band_for_score(score.get("score")) or "n/d",
|
|
"preco_native_ou_cota": native_or_quota,
|
|
"disponivel_na_conta": account_available,
|
|
"motivo_da_exclusao": model.get("availability_reason") or ("sem_score_compatível" if not score else "n/d"),
|
|
"fonte": {"catalog": model.get("public_source"), "benchmark": score.get("source_url"), "openrouter": or_snapshot.get("source_url")},
|
|
"capturado_em": model.get("captured_at") or "n/d", "stale": bool(stale_reasons), "stale_reasons": stale_reasons,
|
|
"availability": availability, "billing_mode": model.get("billing_mode") or "n/d",
|
|
"source_hash": model.get("source_hash") or "n/d", "or_price_per_token": price, "score": score or "n/d",
|
|
})
|
|
return sorted(result, key=lambda r: (r.get("tool") or "", r.get("model_key") or ""))
|
|
|
|
|
|
def _parse_utc(value: Any) -> dt.datetime | None:
|
|
if not isinstance(value, str) or not value:
|
|
return None
|
|
try:
|
|
result = dt.datetime.fromisoformat(value.replace("Z", "+00:00"))
|
|
except ValueError:
|
|
return None
|
|
if result.tzinfo is None:
|
|
return None
|
|
return result.astimezone(dt.timezone.utc)
|
|
|
|
|
|
def coverage(rows: list[dict[str, Any]]) -> dict[str, dict[str, int]]:
|
|
counts: dict[str, dict[str, int]] = {}
|
|
for row in rows:
|
|
provider = row.get("model_key", "n/d").split("/", 1)[0]
|
|
band = row.get("faixa", "n/d")
|
|
group = counts.setdefault(f"{provider}/{band}", {"scored": 0, "total": 0})
|
|
group["total"] += 1
|
|
group["scored"] += row.get("coding_index_nota") != "n/d"
|
|
return counts
|
|
|
|
|
|
LIST_FIELDS = (
|
|
"nome", "tool", "model_key", "openrouter_id", "preco_or_entrada_usd_1m", "preco_or_saida_usd_1m",
|
|
"preco_or_cache_leitura_usd_1m", "preco_or_cache_escrita_usd_1m", "preco_or_cache_escrita_1h_usd_1m", "condicao_de_preco",
|
|
"coding_index_versao", "coding_index_nota", "benchmark_por_componente_se_publicado", "harness_aa",
|
|
"reasoning_aa", "grau_do_match", "faixa", "preco_native_ou_cota", "disponivel_na_conta",
|
|
"motivo_da_exclusao", "fonte", "capturado_em", "stale", "stale_reasons", "availability",
|
|
"billing_mode", "source_hash", "or_price_per_token", "score",
|
|
)
|
|
|
|
|
|
def _csv_text(rows: list[dict[str, Any]]) -> str:
|
|
output = io.StringIO(newline="")
|
|
writer = csv.DictWriter(output, fieldnames=LIST_FIELDS, extrasaction="ignore", lineterminator="\n")
|
|
writer.writeheader()
|
|
for row in rows:
|
|
writer.writerow({k: json.dumps(v, ensure_ascii=False, sort_keys=True) if isinstance(v, (dict, list)) else ("n/d" if v is None else v) for k, v in row.items()})
|
|
return output.getvalue()
|
|
|
|
|
|
def main(argv: list[str] | None = None) -> int:
|
|
parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
|
|
sub = parser.add_subparsers(dest="command", required=True)
|
|
p_sync = sub.add_parser("sync")
|
|
p_sync.add_argument("--tool", choices=("opencode", "omp"), default="opencode")
|
|
p_sync.add_argument("--providers")
|
|
p_sync.add_argument("--aa-snapshot")
|
|
p_sync.add_argument("--price-overrides")
|
|
p_sync.add_argument("--availability-snapshot")
|
|
p_sync.add_argument("--cache-dir")
|
|
p_list = sub.add_parser("list")
|
|
p_list.add_argument("--tool", choices=("opencode", "claude", "omp"), default="opencode")
|
|
p_list.add_argument("--show-missing", action="store_true", default=True)
|
|
p_list.add_argument("--format", choices=("json", "csv"), default="json")
|
|
p_list.add_argument("--as-of-utc", help="instante UTC ISO 8601 para reproduzir staleness arquivado")
|
|
p_list.add_argument("--cache-dir")
|
|
p_route = sub.add_parser("route")
|
|
p_route.add_argument("--tool", choices=("opencode", "claude", "omp"), default="opencode")
|
|
p_route.add_argument("--role", required=True)
|
|
p_route.add_argument("--providers", help="providers explicitamente autorizados, separados por vírgula")
|
|
p_route.add_argument("--format", choices=("json",), default="json")
|
|
p_route.add_argument("--tokens", default="{}", help="perfil JSON de tokens por tarefa")
|
|
p_route.add_argument("--budget-usd")
|
|
p_route.add_argument("--allow-proxy-score", "--allow-proxy", dest="allow_proxy", action="store_true")
|
|
p_route.add_argument("--max-loss-points", "--delta-points", dest="delta_points")
|
|
p_route.add_argument("--max-price-age-hours", default="24")
|
|
p_route.add_argument("--max-score-age-days", default="30")
|
|
p_route.add_argument("--benchmark-metric", default="aa_coding_agent_index_v1.5")
|
|
p_route.add_argument("--band", choices=("leve", "media", "alta"))
|
|
p_route.add_argument("--bands-json")
|
|
p_route.add_argument("--explain", action="store_true")
|
|
p_route.add_argument("--cache-dir")
|
|
args = parser.parse_args(argv)
|
|
try:
|
|
if args.command == "sync":
|
|
providers = tuple(x for x in args.providers.split(",") if x) if args.providers else None
|
|
data = sync(args.tool, providers, args.cache_dir, aa_snapshot=args.aa_snapshot,
|
|
price_overrides=args.price_overrides, availability_snapshot=args.availability_snapshot)
|
|
print(json.dumps({"ok": True, "command": "sync", "data": data}, ensure_ascii=False))
|
|
elif args.command == "list":
|
|
rows = list_models(args.tool, args.cache_dir, show_missing=args.show_missing, as_of_utc=args.as_of_utc)
|
|
if args.format == "csv":
|
|
print(_csv_text(rows), end="")
|
|
else:
|
|
print(json.dumps({"ok": True, "command": "list", "snapshot_available": load_snapshot(args.tool, args.cache_dir) is not None,
|
|
"coverage": coverage(rows), "models": rows}, ensure_ascii=False))
|
|
else:
|
|
from model_router import route_from_catalog
|
|
token_profile = json.loads(args.tokens)
|
|
if not isinstance(token_profile, dict):
|
|
raise ValueError("--tokens deve ser objeto JSON")
|
|
bands = json.loads(args.bands_json) if args.bands_json else None
|
|
if bands is not None and not isinstance(bands, dict):
|
|
raise ValueError("--bands-json deve ser objeto JSON")
|
|
result = route_from_catalog(args.tool, args.role, args.providers.split(",") if args.providers else [],
|
|
args.cache_dir, explain=args.explain, tokens=token_profile,
|
|
budget_usd=args.budget_usd, allow_proxy=args.allow_proxy,
|
|
delta_points=args.delta_points, max_price_age_hours=args.max_price_age_hours,
|
|
max_score_age_days=args.max_score_age_days, band_override=args.band, bands=bands,
|
|
benchmark_metric=args.benchmark_metric)
|
|
print(json.dumps({"ok": result["selected"] is not None, **result}, ensure_ascii=False))
|
|
except (ValueError, OSError, urllib.error.URLError, json.JSONDecodeError) as exc:
|
|
print(json.dumps({"ok": False, "command": args.command, "error": {"code": type(exc).__name__, "message": "operação indisponível; consulte o snapshot local e a origem configurada"}}, ensure_ascii=False))
|
|
return 2
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|