#!/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 _AA_ROOT_FIELDS = {"metric_id", "version", "ingestion", "terms_accepted", "source_url", "attribution", "captured_at", "records"} _AA_RECORD_FIELDS = {"metric_id", "version", "provider", "model_id", "variant", "score", "agent", "harness", "settings", "match_grade", "evidence_url", "components", "fallback", "effort", "reasoning", "configuration"} _AA_PROVENANCE = ("agent", "harness", "settings", "evidence_url") def _check_aa_provenance(payload: dict[str, Any]) -> None: """Consentimento, fonte oficial, atribuição e data de captura do export AA.""" 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)") if not _secure_url(payload.get("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_time = _parse_utc(payload.get("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") def _aa_records(payload: Any) -> list[Any]: """Valida o cabeçalho do export AA (índice e proveniência) e devolve os registros.""" if not isinstance(payload, dict): raise ValueError("snapshot AA deve ser objeto") if set(payload) - _AA_ROOT_FIELDS: 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") _check_aa_provenance(payload) records = payload.get("records") if not isinstance(records, list): raise ValueError("records deve ser lista") return records def _aa_record_key(record: Any) -> tuple[str, str, str | None]: """Valida a estrutura e o mapeamento exato de um registro AA; devolve (provider, model_id, variant).""" if not isinstance(record, dict) or set(record) - _AA_RECORD_FIELDS: 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 _is_exact_model_ref(provider, 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") return provider, model_id, variant def _aa_record_score(record: dict[str, Any]) -> Decimal: """Valida nota, grau de correspondência e proveniência de um registro AA; devolve a nota.""" 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") if record.get("match_grade") not in ("exact", "proxy"): raise ValueError("match_grade deve ser exact ou proxy, sem fuzzy match") for field in _AA_PROVENANCE: if field not in record or record[field] in (None, ""): raise ValueError(f"campo de proveniência AA obrigatório: {field}") if not _secure_url(record["evidence_url"], "artificialanalysis.ai"): raise ValueError("evidence_url AA inválida") return score def validate_aa_snapshot(payload: Any) -> list[dict[str, Any]]: """Aceita exclusivamente export manual auditável do Coding Agent Index v1.5.""" records = _aa_records(payload) validated = [] seen_keys: set[tuple[str, str, str | None]] = set() for record in records: key = _aa_record_key(record) if key in seen_keys: raise ValueError("benchmark inconsistente: modelo/variante duplicado no snapshot") seen_keys.add(key) score = _aa_record_score(record) validated.append({ **record, "metric_id": AA_METRIC, "version": AA_VERSION, "score": decimal_text(score), "source_url": payload["source_url"], "attribution": payload["attribution"], "observed_at": payload["captured_at"], "snapshot_hash": sha256(canonical_json(payload)), }) return validated _AVAILABILITY_FIELDS = {"provider", "model_id", "variant", "local_listed", "authenticated", "entitled", "verified_at", "source", "balance_confirmed", "balance_verified_at"} _AVAILABILITY_REQUIRED = ("local_listed", "authenticated", "entitled", "verified_at", "source") def _availability_records(payload: Any) -> list[Any]: """Valida o cabeçalho do snapshot de disponibilidade e devolve a lista de registros.""" 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") 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") return records def _is_exact_model_ref(provider: Any, model_id: Any) -> bool: return isinstance(provider, str) and isinstance(model_id, str) and bool(model_id) and "/" not in model_id and "#" not in model_id def _availability_key(record: Any) -> tuple[str, str, str | None]: """Valida a estrutura de um registro e devolve a identidade (provider, model_id, variant).""" if not isinstance(record, dict) or set(record) - _AVAILABILITY_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 _is_exact_model_ref(provider, 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 _AVAILABILITY_REQUIRED): raise ValueError("snapshot deve declarar todos os campos de disponibilidade") return provider, model_id, variant def _check_availability_balance(record: dict[str, Any], now: dt.datetime) -> None: 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 > now 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") def _check_availability_evidence(record: dict[str, Any]) -> None: """Valida flags booleanas, saldo confirmado e a evidência (verified_at/source) de um registro.""" now = dt.datetime.now(dt.timezone.utc) 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") _check_availability_balance(record, now) verified_at = _parse_utc(record["verified_at"]) if verified_at is None or verified_at > now 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") def validate_availability_snapshot(payload: Any) -> dict[tuple[str, str, str | None], dict[str, Any]]: """Validates explicit account evidence without accepting or retaining secrets.""" records = _availability_records(payload) result = {} source_hash = sha256(canonical_json(payload)) for record in records: key = _availability_key(record) if key in result: raise ValueError("snapshot de disponibilidade contém ID/variante duplicado") _check_availability_evidence(record) 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": record.get("balance_confirmed"), "balance_verified_at": record.get("balance_verified_at"), } return result _NATIVE_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"} _NATIVE_BILLING_MODES = ("zen_balance", "go_quota", "omp_payg") _NATIVE_RATE_KEYS = ("input", "output", "cache_read", "cache_write", "cache_write_5m", "cache_write_1h", "cache_write_24h") _QUOTA_WINDOWS = ("5h", "weekly", "monthly") def _check_native_header(model_key: Any, value: Any) -> str: """Valida estrutura, confirmação, modo, URL e data comuns a todo override; devolve o modo.""" if not isinstance(model_key, str) or not isinstance(value, dict) or set(value) - _NATIVE_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 _NATIVE_BILLING_MODES: 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") try: captured = dt.datetime.fromisoformat(value.get("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") return mode def _non_negative_text(text: str | None) -> bool: return text is not None and Decimal(text) >= 0 def _native_balance_fields(value: dict[str, Any]) -> dict[str, Any]: """Campos de tarifa por token e saldo (zen_balance e 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") prices = {k: decimal_text(decimal_value(v)) for k, v in rates.items() if k in _NATIVE_RATE_KEYS} if len(prices) != len(rates) or not all(_non_negative_text(v) for v in prices.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") return {"price_per_token_usd": prices, "available_balance_usd": decimal_text(balance)} def _quota_window_texts(source: dict[str, Any]) -> dict[str, str | None]: return {k: decimal_text(decimal_value(source[k])) for k in _QUOTA_WINDOWS} def _native_quota_fields(value: dict[str, Any]) -> dict[str, Any]: """Campos de cota por janela e fallback de saldo (go_quota).""" remaining, debit = value.get("quota_window_remaining"), value.get("quota_window_debit") if not isinstance(remaining, dict) or not isinstance(debit, dict) or not set(_QUOTA_WINDOWS) <= set(remaining) & set(debit): raise ValueError("cota Go exige valores por modelo em 5h/weekly/monthly") fields = { "quota_window_remaining": _quota_window_texts(remaining), "quota_window_debit": _quota_window_texts(debit), } if not all(_non_negative_text(v) for window_values in fields.values() for v in window_values.values()): 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") fields["use_balance_fallback"] = fallback return fields 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 = {} for model_key, value in payload.items(): mode = _check_native_header(model_key, value) record = {k: value[k] for k in ("billing_mode", "source_url", "captured_at", "account_confirmed")} record.update(_native_quota_fields(value) if mode == "go_quota" else _native_balance_fields(value)) 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())