Files
hack/tools/scripts/llm-key-hunter/results/_cache/content/ailinone_collective-intelligence.json
T

1 line
1.1 MiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{"repo": "ailinone/collective-intelligence", "files": {"api/src/config/load-secrets-into-env.ts": {"b748363822c6e602b7a1e3d6c098e8ea9eefcf3b": {"t": 1785595200.1435797, "n": 64471, "h": "0136561c854731e8", "c": "// Copyright (C) 2026 Ailin One, Inc.\n//\n// This file is part of Collective Intelligence Engine (ci).\n// Licensed under the GNU Affero General Public License v3.0 or later.\n// See LICENSE in the repository root, or <https://www.gnu.org/licenses/>.\n//\n// SPDX-License-Identifier: AGPL-3.0-or-later\n// Source: https://github.com/ailinone/collective-intelligence\n\n/**\n * Load Secrets into Environment\n *\n * This module loads critical secrets from GCP Secret Manager\n * and injects them into process.env BEFORE config validation.\n *\n * This allows the existing config system to work without refactoring,\n * while still using GCP Secret Manager as the source of truth.\n *\n * Call order:\n * 1. initializeSecretsManager()\n * 2. loadSecretsIntoEnv() ← This file\n * 3. validateConfig()\n */\n\nimport { logger } from '@/utils/logger';\nimport { getErrorMessage } from '@/utils/type-guards';\nimport { loadSecret } from './secrets-loader';\n\n/**\n * Critical secrets that must be loaded from GCP\n * These are injected into process.env for config compatibility\n */\ninterface CriticalSecret {\n envVar: string;\n secretKey: string;\n required: boolean;\n defaultValue?: string;\n}\n\nconst CRITICAL_SECRETS: CriticalSecret[] = [\n { envVar: 'DATABASE_URL', secretKey: 'database-url', required: true },\n { envVar: 'JWT_SECRET', secretKey: 'jwt-secret', required: true },\n { envVar: 'REDIS_PASSWORD', secretKey: 'redis-password', required: false, defaultValue: '' },\n { envVar: 'GCS_FILES_BUCKET', secretKey: 'gcs-files-bucket', required: false },\n { envVar: 'SMTP_PASS', secretKey: 'smtp-pass', required: false },\n { envVar: 'STRIPE_SECRET_KEY', secretKey: 'stripe-secret-key', required: false },\n { envVar: 'STRIPE_WEBHOOK_SECRET', secretKey: 'stripe-webhook-secret', required: false },\n { envVar: 'STRIPE_PUBLISHABLE_KEY', secretKey: 'stripe-publishable-key', required: false },\n // Phase 2c shadow ensemble bearer token. Optional because the coord-\n // stable wire is disabled by default in production\n // (CI_ENSEMBLE_COORDINATOR_ENABLED=false). When operator flips it on,\n // the GCP secret `coord-ensemble-token` MUST also be set with the\n // value to share with coord-serving's COORD_SERVING_AUTH_TOKEN.\n { envVar: 'CI_ENSEMBLE_COORDINATOR_TOKEN', secretKey: 'coord-ensemble-token', required: false },\n // Prometheus scrape bearer token for /metrics + /metrics/prompts\n // (metrics-route.ts `authorizeScrape`). Optional here — the route itself\n // is fail-closed when unset: it denies (403) in production and stays\n // open for local/dev scraping otherwise. Wired via CRITICAL_SECRETS\n // (not PROVIDER_SECRETS) because this is a general app credential, not\n // a provider API key.\n { envVar: 'PROMETHEUS_SCRAPE_TOKEN', secretKey: 'prometheus-scrape-token', required: false },\n // Sentry error-tracking DSN (utils/error-tracking.ts `initializeErrorTracking`).\n // Optional — the module itself no-ops with a warning log when unset, both\n // at API boot (index.ts) and worker boot (workers/queue-runner.ts), so\n // there is no fail-closed/fail-open behavior riding on this like the\n // Prometheus token above. Wired via CRITICAL_SECRETS (not PROVIDER_SECRETS)\n // because this is a general app credential, not a provider API key. No GCP\n // secret exists yet as of this commit — an operator must create one\n // (`ailin-sentry-dsn`) with a real DSN from Sentry's project settings\n // before this ever resolves to a non-empty value.\n { envVar: 'SENTRY_DSN', secretKey: 'sentry-dsn', required: false },\n];\n\n/**\n * Provider API keys (loaded from GCP if not in env)\n * CRITICAL: GCP provider automatically adds prefix 'ailin-' to secret keys\n * So 'openai-key' becomes 'ailin-openai-key' in GCP Secret Manager\n * These keys match the actual secret names in GCP (without the prefix, provider adds it)\n */\ninterface ProviderSecretBinding {\n envVar: string;\n secretKeys: readonly string[];\n}\n\nexport const PROVIDER_SECRETS: readonly ProviderSecretBinding[] = [\n { envVar: 'OPENAI_API_KEY', secretKeys: ['openai-key'] },\n { envVar: 'ANTHROPIC_API_KEY', secretKeys: ['anthropic-key'] },\n { envVar: 'GOOGLE_API_KEY', secretKeys: ['google-key'] },\n { envVar: 'DEEPSEEK_API_KEY', secretKeys: ['deepseek-key'] },\n { envVar: 'XAI_API_KEY', secretKeys: ['xai-key'] },\n { envVar: 'MISTRAL_API_KEY', secretKeys: ['mistral-key'] },\n { envVar: 'COHERE_API_KEY', secretKeys: ['cohere-key'] },\n { envVar: 'NVIDIA_API_KEY', secretKeys: ['nvidia-key', 'nvidia-api-key'] },\n { envVar: 'AIHUBMIX_API_KEY', secretKeys: ['aihubmix-key', 'aihubmix-api-key'] },\n { envVar: 'NOVITA_API_KEY', secretKeys: ['novita-key', 'novita-api-key'] },\n { envVar: 'MOONSHOT_API_KEY', secretKeys: ['moonshot-key', 'moonshot-api-key'] },\n { envVar: 'MINIMAX_API_KEY', secretKeys: ['minimax-key', 'minimax-api-key'] },\n { envVar: 'JINA_API_KEY', secretKeys: ['jina-key', 'jina-api-key'] },\n { envVar: 'GOOGLE_MAPS_API_KEY', secretKeys: ['google-maps-key', 'google-maps-api-key'] },\n { envVar: 'FRIENDLI_API_KEY', secretKeys: ['friendli-key', 'friendli-api-key'] },\n { envVar: 'FRIENDLI_TEAM_ID', secretKeys: ['friendli-team-id'] },\n { envVar: 'AIML_API_KEY', secretKeys: ['aiml-key', 'aiml-api-key'] },\n { envVar: 'IMAGEROUTER_API_KEY', secretKeys: ['imagerouter-key', 'imagerouter-api-key'] },\n { envVar: 'OPENROUTER_API_KEY', secretKeys: ['openrouter-key'] },\n { envVar: 'COMETAPI_API_KEY', secretKeys: ['cometapi-key', 'cometapi-api-key'] },\n { envVar: 'NANOGPT_API_KEY', secretKeys: ['nanogpt-key', 'nanogpt-api-key'] },\n { envVar: 'REQUESTY_API_KEY', secretKeys: ['requesty-key', 'requesty-api-key'] },\n { envVar: 'AI302_API_KEY', secretKeys: ['302-key', '302ai-key'] },\n { envVar: 'POE_API_KEY', secretKeys: ['poe-key', 'poe-api-key'] },\n { envVar: 'ROUTEWAY_API_KEY', secretKeys: ['routeway-key', 'routeway-api-key'] },\n // Hub providers support both key naming conventions for compatibility.\n { envVar: 'ORQAI_API_KEY', secretKeys: ['orqai-key', 'orqai-api-key'] },\n { envVar: 'ORQAI_MODEL_DENYLIST', secretKeys: ['orqai-model-denylist'] },\n { envVar: 'EDENAI_API_KEY', secretKeys: ['edenai-key', 'edenai-api-key'] },\n { envVar: 'HELICONEAI_API_KEY', secretKeys: ['heliconeai-key', 'heliconeai-api-key'] },\n { envVar: 'VERTEX_AI_API_KEY', secretKeys: ['vertex-key'] },\n { envVar: 'VERTEX_AI_PROJECT_ID', secretKeys: ['vertex-project-id'] },\n { envVar: 'QWEN_API_KEY', secretKeys: ['qwen-key-2'] },\n { envVar: 'ALIBABA_ACCESS_KEY_ID', secretKeys: ['alibaba-key-id'] },\n { envVar: 'ALIBABA_ACCESS_KEY_SECRET', secretKeys: ['alibaba-key-secret'] },\n { envVar: 'ERNIE_API_KEY', secretKeys: ['baidu-key'] },\n { envVar: 'ERNIE_SECRET_KEY', secretKeys: ['baidu-secret'] },\n { envVar: 'BAIDU_BASE_URL', secretKeys: ['baidu-base-url'] },\n { envVar: 'AWS_ACCESS_KEY_ID', secretKeys: ['aws-key-id'] },\n { envVar: 'AWS_SECRET_ACCESS_KEY', secretKeys: ['aws-secret'] },\n { envVar: 'AWS_BEARER_TOKEN_BEDROCK', secretKeys: ['aws-bearer-token'] },\n // Batch 7.1: Bedrock region + inference-profile ARN can now be set via\n // GCP Secret Manager. Useful for deploys that route Bedrock to a specific\n // region (e.g. us-west-2) or via cross-region inference profiles without\n // baking these into the docker image.\n { envVar: 'AWS_BEDROCK_REGION', secretKeys: ['aws-bedrock-region'] },\n {\n envVar: 'AWS_BEDROCK_INFERENCE_PROFILE_ARN',\n secretKeys: ['aws-bedrock-inference-profile-arn'],\n },\n // Batch 8.1: SageMaker endpoint routing. A deploy can point this adapter\n // at a specific deployed endpoint via a secret rather than baking the\n // endpoint name into the image. The region override lets the adapter\n // hit an endpoint in a different region from the general AWS_REGION\n // used by other services (SES, S3).\n { envVar: 'AWS_SAGEMAKER_REGION', secretKeys: ['aws-sagemaker-region'] },\n { envVar: 'AWS_SAGEMAKER_ENDPOINT_NAME', secretKeys: ['aws-sagemaker-endpoint-name'] },\n { envVar: 'AWS_SAGEMAKER_PAYLOAD_SCHEMA', secretKeys: ['aws-sagemaker-payload-schema'] },\n { envVar: 'AWS_SAGEMAKER_CUSTOM_ATTRIBUTES', secretKeys: ['aws-sagemaker-custom-attributes'] },\n { envVar: 'AZURE_OPENAI_API_KEY', secretKeys: ['azure-openai-api-key'] },\n { envVar: 'AZURE_OPENAI_ENDPOINT', secretKeys: ['azure-openai-endpoint'] },\n { envVar: 'AZURE_OPENAI_DEPLOYMENT', secretKeys: ['azure-openai-deployment'] },\n // Audio-first providers (STT, TTS, STS)\n { envVar: 'DEEPGRAM_API_KEY', secretKeys: ['deepgram-key'] },\n { envVar: 'CARTESIA_API_KEY', secretKeys: ['cartesia-key'] },\n { envVar: 'ELEVENLABS_API_KEY', secretKeys: ['elevenlabs-key'] },\n // Translation provider\n { envVar: 'PALABRAAI_CLIENT_ID', secretKeys: ['palabraai-id'] },\n { envVar: 'PALABRAAI_CLIENT_SECRET', secretKeys: ['palabraai-key'] },\n { envVar: 'OCI_TENANCY_ID', secretKeys: ['oci-tenancy-id'] },\n { envVar: 'OCI_USER_ID', secretKeys: ['oci-user-id'] },\n { envVar: 'OCI_FINGERPRINT', secretKeys: ['oci-fingerprint'] },\n { envVar: 'OCI_PRIVATE_KEY', secretKeys: ['oci-private-key'] },\n { envVar: 'OCI_REGION', secretKeys: ['oci-region'] },\n // 2026-04-22: New GCP secrets (5 providers). The GCP provider auto-prefixes\n // 'ailin-', so 'wandb-key' maps to ailin-wandb-key in Secret Manager.\n // Voyage lists both 'voyageai-key' (current upstream name) and 'voyage-key'\n // (conventional fallback) because the canonical spelling is still unstable.\n { envVar: 'WANDB_API_KEY', secretKeys: ['wandb-key', 'wandb-api-key'] },\n { envVar: 'VOYAGE_API_KEY', secretKeys: ['voyageai-key', 'voyage-key', 'voyage-api-key'] },\n { envVar: 'BYTEZ_API_KEY', secretKeys: ['bytez-key', 'bytez-api-key'] },\n { envVar: 'CEREBRAS_API_KEY', secretKeys: ['cerebras-key', 'cerebras-api-key'] },\n { envVar: 'FEATHERLESS_AI_API_KEY', secretKeys: ['featherless-key', 'featherless-api-key'] },\n // Batch 6 — enterprise hyperscaler gateways (2026-04-22).\n // Azure OpenAI already has API_KEY/ENDPOINT/DEPLOYMENT wired above;\n // these fill in the remaining URL-template parameters.\n {\n envVar: 'AZURE_OPENAI_RESOURCE_NAME',\n secretKeys: ['azure-openai-resource-name', 'azure-openai-resource'],\n },\n { envVar: 'AZURE_OPENAI_API_VERSION', secretKeys: ['azure-openai-api-version'] },\n // Gemini OAI-compat shares the API key with the native `google` provider.\n // Two env-var aliases: GEMINI_API_KEY (canonical upstream) and\n // GOOGLE_AI_STUDIO_API_KEY (legacy). The adapter reads either.\n //\n // 2026-04-23: `vertex-key` added as a fallback source because it was\n // verified working against generativelanguage.googleapis.com (HTTP 200,\n // 50 Gemini models) while `google-key` is suspended (403\n // CONSUMER_SUSPENDED). The Vertex/Gemini API-key split is mostly\n // historical — a Google Cloud API key scoped for Gemini works equally on\n // aistudio and on the generativelanguage surface, as long as the key's\n // GCP project has the API enabled.\n //\n // 2026-07-25: `vertex-key` was originally placed LAST on the (incorrect)\n // assumption that the loop below tries each candidate live and keeps the\n // first one that WORKS. It doesn't — it takes the first candidate that\n // merely EXISTS in Secret Manager (see the resolution loop a few hundred\n // lines down), so the suspended `google-key` always won and gemini-openai\n // stayed broken. Moved `vertex-key` first since it's the only one\n // confirmed live-working; the others remain as fallbacks if it's ever\n // rotated out.\n {\n envVar: 'GEMINI_API_KEY',\n secretKeys: ['vertex-key', 'gemini-key', 'google-ai-studio-key', 'google-key'],\n },\n // GitHub Models uses a GitHub PAT. Operators typically already have this\n // in env (it's the same PAT used for gh CLI); the GCP path is a fallback\n // for service accounts that need isolated tokens.\n { envVar: 'GITHUB_TOKEN', secretKeys: ['github-models-token', 'github-token', 'github-pat'] },\n // Databricks workspace token + config.\n { envVar: 'DATABRICKS_TOKEN', secretKeys: ['databricks-token', 'databricks-pat'] },\n { envVar: 'DATABRICKS_HOST', secretKeys: ['databricks-host'] },\n { envVar: 'DATABRICKS_SERVING_ENDPOINT', secretKeys: ['databricks-serving-endpoint'] },\n // Inworld AI — orphan closure 2026-04-22. Key is already base64-encoded\n // in GCP (132 chars) because Inworld uses Basic auth, not Bearer. The\n // adapter does NOT re-encode; it passes the secret verbatim into the\n // `Basic ` header. Never base64-encode this value at load time.\n { envVar: 'INWORLD_API_KEY', secretKeys: ['inworld-key', 'inworld-api-key'] },\n // ── Orphan-closure Lot B (2026-04-23) ──────────────────────────────────\n // Three providers closed in a single lot because they share identical\n // shape (thin OAI-compat wrappers) and were all already present in GCP\n // Secret Manager but not wired into env loading. Each tuple lists the\n // preferred secret key first and legacy-or-alternate names as fallbacks.\n { envVar: 'WRITER_API_KEY', secretKeys: ['writer-key', 'writer-api-key'] },\n { envVar: 'UPSTAGE_API_KEY', secretKeys: ['upstage-key', 'upstage-api-key'] },\n // Reka's canonical upstream name is \"reka\" but the key is stored in GCP\n // as `ailin-rekaai-key` (matching the catalog providerId, not the upstream\n // name). `reka-key` is kept as a fallback in case the secret is ever\n // renamed to match upstream naming later.\n { envVar: 'REKA_API_KEY', secretKeys: ['rekaai-key', 'reka-key', 'reka-api-key'] },\n // ── LOTE M (2026-04-23) — complement lot: 13 new env-var mappings. ─────\n // Every provider below landed a catalog row this lot and is classified\n // in CONSOLIDATION_MATRIX['credentials-missing'] (secret-absent sub-class\n // for 12, auth-incomplete for qianfan). Each mapping is wired NOW so the\n // moment an operator provisions the corresponding GCP secret, the key\n // propagates to process.env without a code change. The first element of\n // each secretKeys array is the preferred upstream-convention name; any\n // follow-up entries are fallbacks for historical or legacy aliases.\n { envVar: 'ARCEE_API_KEY', secretKeys: ['arcee-api-key', 'arcee-key'] },\n {\n envVar: 'ATLASCLOUD_API_KEY',\n secretKeys: ['atlascloud-key', 'atlascloud-api-key', 'atlas-cloud-key'],\n },\n { envVar: 'AVIAN_API_KEY', secretKeys: ['avian-key', 'avian-api-key', 'avian-io-key'] },\n // Qianfan (Baidu) v2 bce-v3 bearer. Distinct from the legacy v1 AK+SK\n // material in ERNIE_API_KEY / ERNIE_SECRET_KEY / BAIDU_BASE_URL above —\n // both mappings are intentionally kept so an operator can populate either\n // path (v1 OAuth or v2 bearer) depending on which runtime surface is\n // used. The canonical catalog row `qianfan` reads QIANFAN_API_KEY only.\n {\n envVar: 'QIANFAN_API_KEY',\n secretKeys: ['qianfan-key', 'qianfan-api-key', 'baidu-qianfan-key'],\n },\n { envVar: 'GMI_API_KEY', secretKeys: ['gmi-key', 'gmi-api-key', 'gmicloud-key'] },\n {\n envVar: 'INFERMATIC_API_KEY',\n secretKeys: ['infermatic-api-key', 'infermatic-key', 'totalgpt-key'],\n },\n {\n envVar: 'INFLECTION_API_KEY',\n secretKeys: ['inflection-key', 'inflection-api-key', 'inflection-ai-key'],\n },\n // Mancer (tagged contentPolicyClass=uncensored in catalog; per the universal\n // \"habilitado e nunca censurado\" policy from Phase 4b, 2026-04-28, the\n // catalog row is fully admitted to routing — the tag is informational so\n // downstream surfaces can opt to filter it from a moderated default list).\n { envVar: 'MANCER_API_KEY', secretKeys: ['mancer-key', 'mancer-api-key'] },\n // Phala / RedPill TEE — upstream docs use REDPILL_API_KEY but we wire\n // PHALA_API_KEY as the canonical env var (see apiKeyEnvVarOverrideReason\n // in catalog row `phala`). Legacy `redpill-*` secret names are accepted\n // as fallbacks so an operator storing under either convention works.\n {\n envVar: 'PHALA_API_KEY',\n secretKeys: ['phala-key', 'phala-api-key', 'redpill-key', 'redpill-api-key'],\n },\n { envVar: 'RELACE_API_KEY', secretKeys: ['relace-key', 'relace-api-key'] },\n {\n envVar: 'SILICONFLOW_API_KEY',\n secretKeys: ['siliconflow-api-key', 'siliconflow-key', 'silicon-flow-key'],\n },\n { envVar: 'STEPFUN_API_KEY', secretKeys: ['stepfun-api-key', 'stepfun-key', 'step-key'] },\n // Venice (tagged contentPolicyClass=uncensored in catalog). Same Phase-4b\n // treatment as Mancer — admitted, never censored, with the tag as the\n // audit trail for the policy choice.\n { envVar: 'VENICE_API_KEY', secretKeys: ['venice-key', 'venice-api-key', 'venice-ai-key'] },\n // ── Sublote E1 (2026-04-24) — runtime-wiring closure for D1 matrix ─────\n // Sublote D1 promoted 10 providers to CONSOLIDATION_MATRIX['live-validation']\n // based on direct-probe evidence (via gcloud secrets versions access,\n // bypassing the runtime env loader). 7 of those had no GCP→ENV tuple here,\n // so the catalog-loader could never surface `process.env.<X>_API_KEY` at\n // boot — meaning live-validated providers were silently \"not runnable\".\n // Wired pre-emptively: the moment an operator provisions the GCP secret\n // AND the container restarts, propagation happens with zero code change.\n // The 3 already-mapped providers (heliconeai, github-models, infermatic)\n // are NOT duplicated — they appear above under their respective batches.\n { envVar: 'GROQ_API_KEY', secretKeys: ['groq-api-key', 'groq-key'] },\n { envVar: 'DEEPINFRA_API_KEY', secretKeys: ['deepinfra-api-key', 'deepinfra-key'] },\n // Hugging Face: upstream canonical env var is `HF_TOKEN` (not\n // HUGGINGFACE_API_KEY); the catalog row's apiKeyEnvVar matches. Primary\n // GCP secret name follows the upstream convention; legacy huggingface-*\n // aliases accepted for operators who stored it under the provider slug.\n {\n envVar: 'HF_TOKEN',\n secretKeys: ['huggingface-api-key', 'huggingface-token', 'hf-token', 'huggingface-key'],\n },\n // Cloudflare Workers AI — requires BOTH api-token AND account-id. The\n // CloudflareWorkersAIAdapter factory (default-adapter-factories.ts line\n // 229-246) reads CLOUDFLARE_ACCOUNT_ID from env and substitutes it into\n // the `{account_id}` URL template. Without the account-id side-car the\n // adapter ships the literal template to the wire → 404 on every call.\n // Side-car is wired here as a separate tuple because it's not an auth\n // credential — it's a URL parameter — so it doesn't belong in\n // ENV_VAR_TO_PROVIDER or LLM_PROVIDER_ENV_VARS below.\n {\n envVar: 'CLOUDFLARE_API_TOKEN',\n secretKeys: [\n 'cloudflare-workers-ai-api-key',\n 'cloudflare-workers-ai-token',\n 'cloudflare-api-token',\n 'cloudflare-workers-ai-key',\n ],\n },\n {\n envVar: 'CLOUDFLARE_ACCOUNT_ID',\n secretKeys: [\n 'cloudflare-workers-ai-id',\n 'cloudflare-account-id',\n 'cloudflare-workers-ai-account-id',\n ],\n },\n { envVar: 'PERPLEXITY_API_KEY', secretKeys: ['perplexity-api-key', 'perplexity-key'] },\n // Fireworks AI — note the catalog providerId is `fireworks-ai` (hyphenated)\n // but upstream brand/docs spell it \"Fireworks\" (no suffix). Four fallbacks\n // cover both conventions + api-key suffix variant.\n {\n envVar: 'FIREWORKS_AI_API_KEY',\n secretKeys: ['fireworks-ai-api-key', 'fireworks-ai-key', 'fireworks-key', 'fireworks-api-key'],\n },\n { envVar: 'SAMBANOVA_API_KEY', secretKeys: ['sambanova-api-key', 'sambanova-key'] },\n // Replicate — flagged by the sublote-e1-runtime-wiring invariant as a\n // pre-existing gap. `replicate` has been in CONSOLIDATION_MATRIX\n // ['live-validation'] since before D1, with adapterClass 'ReplicateAdapter'\n // and apiKeyEnvVar 'REPLICATE_API_KEY', but no tuple was ever added here.\n // Even though integrationMode is 'execution-only' (no /v1/models\n // discovery), the adapter still requires the env var at /predictions\n // call time. Wired here for closure; the GCP secret name follows the\n // provider-slug convention with optional `-api-key` fallback.\n { envVar: 'REPLICATE_API_KEY', secretKeys: ['replicate-key', 'replicate-api-key'] },\n // ── LOTE N (2026-04-27) — catalog↔loader closure for the remaining 22 ─\n // apiKeyEnvVar entries that had catalog rows but NO PROVIDER_SECRETS\n // tuple. Surfaced by `catalog-loader-wiring-completeness.test.ts` (J6\n // invariant). All 22 are real provider rows with adapter classes (or\n // generic OAI-compat hub registration); they were classified in\n // CONSOLIDATION_MATRIX as `credentials-missing` or `disabled-by-default`,\n // and their absence here meant that even after an operator provisioned\n // the GCP secret, `process.env.<X>_API_KEY` would stay empty at boot.\n //\n // Pattern: preferred secret name first (provider-slug `-key`), then\n // upstream-convention `-api-key` fallback, then any historical aliases.\n // The provider-side `aliases` array in providers.catalog.ts is NOT\n // duplicated here — secret-key aliases are about HOW the operator\n // stored the credential, NOT how callers reach the provider.\n //\n // Image/video-only providers (bfl, recraft, runwayml, topaz) are wired\n // here too but are intentionally absent from LLM_PROVIDER_ENV_VARS below\n // because their presence does NOT count toward the \"at least one LLM\n // key present\" boot-mode gate.\n {\n envVar: 'TOGETHERAI_API_KEY',\n secretKeys: ['togetherai-api-key', 'togetherai-key', 'together-key', 'together-api-key'],\n },\n { envVar: 'ANYSCALE_API_KEY', secretKeys: ['anyscale-api-key', 'anyscale-key'] },\n { envVar: 'HYPERBOLIC_API_KEY', secretKeys: ['hyperbolic-api-key', 'hyperbolic-key'] },\n { envVar: 'CHUTES_API_KEY', secretKeys: ['chutes-api-key', 'chutes-key', 'chutes-ai-key'] },\n { envVar: 'NSCALE_API_KEY', secretKeys: ['nscale-key', 'nscale-api-key'] },\n { envVar: 'NEBIUS_API_KEY', secretKeys: ['nebius-key', 'nebius-api-key'] },\n // Lambda Cloud canonical provider slug is `lambda-ai`; brand is\n // \"Lambda\" (without -ai). Both spellings tried as fallbacks.\n {\n envVar: 'LAMBDA_AI_API_KEY',\n secretKeys: ['lambda-ai-key', 'lambda-key', 'lambda-cloud-key', 'lambda-ai-api-key'],\n },\n { envVar: 'SCALEWAY_API_KEY', secretKeys: ['scaleway-key', 'scaleway-api-key'] },\n {\n envVar: 'SYNTHETIC_API_KEY',\n secretKeys: ['synthetic-key', 'synthetic-api-key', 'synthetic-ai-key'],\n },\n { envVar: 'MORPH_API_KEY', secretKeys: ['morph-key', 'morph-api-key', 'morph-llm-key'] },\n // Z.ai (catalog providerId = `zai`). Upstream domain z.ai resolves to\n // bigmodel.cn — the GLM family operator. Legacy spelling z-ai accepted.\n { envVar: 'ZAI_API_KEY', secretKeys: ['zai-key', 'zai-api-key', 'z-ai-key', 'glm-key'] },\n {\n envVar: 'XIAOMI_MIMO_API_KEY',\n secretKeys: ['xiaomi-mimo-key', 'mimo-key', 'xiaomi-mimo-api-key'],\n },\n // v0 (Vercel) — code-gen LLM. The catalog providerId is `v0` (one\n // character) so the env var name is `V0_API_KEY` not `V0AI_API_KEY`.\n { envVar: 'V0_API_KEY', secretKeys: ['v0-key', 'v0-api-key', 'vercel-v0-key'] },\n // Vercel AI Gateway — DIFFERENT from v0 above (gateway over many LLMs).\n // The canonical secret name follows the catalog providerId.\n {\n envVar: 'VERCEL_AI_GATEWAY_API_KEY',\n secretKeys: ['vercel-ai-gateway-key', 'vercel-ai-gateway-api-key', 'vercel-gateway-key'],\n },\n // Volcano (Volcengine — ByteDance). Canonical slug `volcano`; brand\n // includes Volcengine + Doubao + Ark. Multiple aliases accepted.\n {\n envVar: 'VOLCANO_API_KEY',\n secretKeys: ['volcano-key', 'volcano-api-key', 'volcengine-key', 'doubao-key', 'ark-key'],\n },\n // IBM watsonx — note the env var is `WATSONX_APIKEY` (no underscore\n // between API and KEY) to match IBM's canonical SDK convention.\n // Side-car env vars (WATSONX_PROJECT_ID, WATSONX_URL) are wired below\n // because they're documented in the catalog row's `extraEnvVars` and\n // adapter requires them at runtime.\n {\n envVar: 'WATSONX_APIKEY',\n secretKeys: ['watsonx-apikey', 'watsonx-key', 'watsonx-api-key', 'ibm-watsonx-key'],\n },\n { envVar: 'WATSONX_PROJECT_ID', secretKeys: ['watsonx-project-id', 'ibm-watsonx-project-id'] },\n { envVar: 'WATSONX_URL', secretKeys: ['watsonx-url', 'ibm-watsonx-url'] },\n // Snowflake Cortex — uses Personal Access Token (PAT), not an API key.\n // Multiple side-cars required by the SnowflakeCortexAdapter:\n // SNOWFLAKE_ACCOUNT (orgname-accountname), SNOWFLAKE_USER (key-pair\n // auth user), SNOWFLAKE_BASE_URL (account-specific host). Side-cars\n // wired here so a single GCP provisioning round materializes the full\n // tuple at boot.\n { envVar: 'SNOWFLAKE_PAT', secretKeys: ['snowflake-pat', 'snowflake-token', 'snowflake-key'] },\n { envVar: 'SNOWFLAKE_ACCOUNT', secretKeys: ['snowflake-account'] },\n { envVar: 'SNOWFLAKE_USER', secretKeys: ['snowflake-user', 'snowflake-username'] },\n { envVar: 'SNOWFLAKE_BASE_URL', secretKeys: ['snowflake-base-url', 'snowflake-url'] },\n // SAP AI Core — OAuth2 client_credentials flow. Catalog providerId is\n // `sap`. The apiKeyEnvVar is `SAP_AI_CORE_CLIENT_ID` (the OAuth client\n // id); the matching client_secret + token URL + base URL are all\n // documented in the catalog row's `extraEnvVars` and required by the\n // SapAiCoreAdapter. Wiring all four together so a single provisioning\n // round closes the loop.\n { envVar: 'SAP_AI_CORE_CLIENT_ID', secretKeys: ['sap-ai-core-client-id', 'sap-client-id'] },\n {\n envVar: 'SAP_AI_CORE_CLIENT_SECRET',\n secretKeys: ['sap-ai-core-client-secret', 'sap-client-secret'],\n },\n { envVar: 'SAP_AI_CORE_AUTH_URL', secretKeys: ['sap-ai-core-auth-url', 'sap-auth-url'] },\n { envVar: 'SAP_AI_CORE_BASE_URL', secretKeys: ['sap-ai-core-base-url', 'sap-base-url'] },\n // W&B Inference side-car — WANDB_API_KEY (auth) is already wired in\n // Batch 7 above; WANDB_PROJECT is the project slug header required on\n // every chat call. Wiring here closes the side-car gap.\n { envVar: 'WANDB_PROJECT', secretKeys: ['wandb-project', 'wandb-project-slug'] },\n // Image-only providers (no LLM gate participation, see comment below).\n // bfl auth header is `x-key` (not Bearer) — see catalog `authHeaderName`.\n {\n envVar: 'BFL_API_KEY',\n secretKeys: ['bfl-key', 'bfl-api-key', 'flux-key', 'black-forest-labs-key'],\n },\n { envVar: 'RECRAFT_API_KEY', secretKeys: ['recraft-key', 'recraft-api-key'] },\n { envVar: 'RUNWAYML_API_KEY', secretKeys: ['runwayml-key', 'runwayml-api-key', 'runway-key'] },\n { envVar: 'TOPAZ_API_KEY', secretKeys: ['topaz-key', 'topaz-api-key', 'topaz-labs-key'] },\n // ── LOTE O (2026-07-10) — Apertis + Inception Labs onboarding ─────────\n // Operator pre-provisioned both GCP secrets ahead of the catalog rows\n // (ailin-apertis-key, ailin-inception-key). secretKeys match exactly —\n // the GCP provider auto-prepends the `ailin-` prefix (see file header).\n { envVar: 'APERTIS_API_KEY', secretKeys: ['apertis-key', 'apertis-api-key'] },\n { envVar: 'INCEPTION_API_KEY', secretKeys: ['inception-key', 'inception-api-key'] },\n // ── LOTE P (2026-07-11) — EmpirioLabs AI, live-validated after gate ────\n { envVar: 'EMPIRIOLABS_API_KEY', secretKeys: ['empiriolabs-key', 'empiriolabs-api-key'] },\n // ── LOTE Q (2026-07-12) — Concentrate AI ───────────────────────────────\n { envVar: 'CONCENTRATE_API_KEY', secretKeys: ['concentrate-key', 'concentrate-api-key'] },\n // ── LOTE R (2026-07-13) — FastRouter ────────────────────────────────────\n { envVar: 'FASTROUTER_API_KEY', secretKeys: ['fastrouter-key', 'fastrouter-api-key'] },\n // ── LOTE S (2026-07-13) — Perplexity Agent API ──────────────────────────\n // Same physical credential as PERPLEXITY_API_KEY above (one Perplexity\n // account, two API surfaces) — deliberately sources from the SAME GCP\n // secret names, not a new one.\n { envVar: 'PERPLEXITY_AGENT_API_KEY', secretKeys: ['perplexity-api-key', 'perplexity-key'] },\n // ── LOTE T (2026-07-13) — Ailin gateway (self-referential meta-provider) ──\n // No GCP secret provisioned yet — inert until an operator sets AILIN_API_KEY\n // for a self-hosted deployment. secretKeys follow the same naming\n // convention as the rest of this file in case one is provisioned later.\n { envVar: 'AILIN_API_KEY', secretKeys: ['ailin-key', 'ailin-api-key', 'gateway-key'] },\n // ── LOTE U (2026-07-29) — Sakana AI (Fugu), chat-capable ────────────────\n // 'sakana-ai-key' listed first: it's the actual GCP secret name\n // (`ailin-sakana-ai-key`, prefix auto-added) confirmed live this lot.\n { envVar: 'SAKANA_AI_API_KEY', secretKeys: ['sakana-ai-key', 'sakana-key'] },\n] as const;\n\n// ─── Provider Key Status Tracking ──────────────────────────────────────────\n// Tracks which provider API keys were successfully loaded, from which source,\n// and when. Used by the Self-Healing Discovery (L1) to know when to retry\n// discovery for native providers whose keys arrived late (GCP delay/failure).\n\nexport interface ProviderKeyStatus {\n loaded: boolean;\n source: 'gcp' | 'env' | 'none';\n timestamp: Date;\n envVar: string;\n}\n\n/**\n * Module-level state: populated during loadSecretsIntoEnv() for each provider.\n * Key = normalized provider name (lowercase, e.g., 'openai', 'anthropic').\n */\nconst providerKeyStatusMap = new Map<string, ProviderKeyStatus>();\n\n/**\n * Mapping from env var name to normalized provider name.\n * Allows the discovery service to map env vars to provider sources.\n */\nconst ENV_VAR_TO_PROVIDER: Record<string, string> = {\n // Native API providers\n OPENAI_API_KEY: 'openai',\n ANTHROPIC_API_KEY: 'anthropic',\n GOOGLE_API_KEY: 'google',\n DEEPSEEK_API_KEY: 'deepseek',\n XAI_API_KEY: 'xai',\n MISTRAL_API_KEY: 'mistral',\n COHERE_API_KEY: 'cohere',\n JINA_API_KEY: 'jina',\n // Cloud hubs — 2026-04-23: use canonical providerId 'nvidia' (legacy\n // alias 'nvidia-hub' was absorbed into the nvidia catalog row per Lot B;\n // discovery/status consumers read providerKeyStatus by canonical id).\n NVIDIA_API_KEY: 'nvidia',\n AIHUBMIX_API_KEY: 'aihubmix',\n NOVITA_API_KEY: 'novita',\n MOONSHOT_API_KEY: 'moonshot',\n MINIMAX_API_KEY: 'minimax',\n FRIENDLI_API_KEY: 'friendli',\n AIML_API_KEY: 'aiml',\n IMAGEROUTER_API_KEY: 'imagerouter',\n COMETAPI_API_KEY: 'cometapi',\n NANOGPT_API_KEY: 'nanogpt',\n EDENAI_API_KEY: 'edenai',\n HELICONEAI_API_KEY: 'heliconeai',\n POE_API_KEY: 'poe',\n ROUTEWAY_API_KEY: 'routeway',\n REQUESTY_API_KEY: 'requesty',\n // Post-migration canonical providerId is `ai302` (see providers.catalog.ts\n // and the residue-closure note in provider-registry.ts). Legacy `302ai`\n // survives as a catalog alias for pre-migration user configs.\n AI302_API_KEY: 'ai302',\n ORQAI_API_KEY: 'orqai',\n // Routers/aggregators\n OPENROUTER_API_KEY: 'openrouter',\n // Cloud providers\n VERTEX_AI_API_KEY: 'vertex-ai',\n AWS_ACCESS_KEY_ID: 'aws-bedrock',\n AWS_BEARER_TOKEN_BEDROCK: 'aws-bedrock',\n // SageMaker uses the same shared AWS creds as Bedrock, so `AWS_ACCESS_KEY_ID`\n // keeps mapping to `aws-bedrock` (first-listed wins) — but the endpoint-\n // specific env var is only meaningful for SageMaker. When operators wire\n // this, Self-Healing Discovery will attribute the key presence to the\n // SageMaker provider specifically.\n AWS_SAGEMAKER_ENDPOINT_NAME: 'aws-sagemaker',\n AZURE_OPENAI_API_KEY: 'azure-openai',\n // Regional/specialized\n QWEN_API_KEY: 'qwen',\n ERNIE_API_KEY: 'ernie',\n // Audio providers\n DEEPGRAM_API_KEY: 'deepgram',\n CARTESIA_API_KEY: 'cartesia',\n ELEVENLABS_API_KEY: 'elevenlabs',\n // Inference + embedding providers newly onboarded 2026-04-22\n WANDB_API_KEY: 'wandb',\n VOYAGE_API_KEY: 'voyage',\n BYTEZ_API_KEY: 'bytez',\n CEREBRAS_API_KEY: 'cerebras',\n FEATHERLESS_AI_API_KEY: 'featherless-ai', // canonical catalog id (2026-04-23 drift fix)\n // Batch 6 — enterprise hyperscaler gateways (2026-04-22). Gemini OAI-compat\n // is registered as a SEPARATE provider from the native `google` entry so\n // the self-healing discovery service can track them independently (they\n // have different failure modes — native uses @google/generative-ai SDK\n // with its own error surfaces; OAI-compat uses standard fetch).\n GEMINI_API_KEY: 'gemini-openai',\n GITHUB_TOKEN: 'github-models',\n DATABRICKS_TOKEN: 'databricks',\n // Orphan closure (2026-04-22) — Inworld now has a catalog row + adapter\n // factory. Mapping the env var to the provider slug lets Self-Healing\n // Discovery attribute key presence to the correct provider.\n INWORLD_API_KEY: 'inworld',\n // Orphan-closure Lot B (2026-04-23). Slug = catalog providerId. Writer\n // and Upstage are self-evident; Reka's slug is `rekaai` (the catalog\n // row's canonical providerId — aliases 'reka' + 'reka-ai' resolve to it).\n WRITER_API_KEY: 'writer',\n UPSTAGE_API_KEY: 'upstage',\n REKA_API_KEY: 'rekaai',\n // ── LOTE M (2026-04-23) — complement lot, 13 providers ──────────────\n // Slug = catalog providerId (NOT the upstream brand for phala/qianfan\n // which intentionally diverge — see catalog rows for rationale):\n // phala — canonical slug; upstream brand is \"RedPill\"\n // qianfan — canonical slug; upstream brand is \"Baidu Qianfan / ERNIE\"\n // gmi — canonical slug; upstream brand is \"GMICloud\"\n ARCEE_API_KEY: 'arcee',\n ATLASCLOUD_API_KEY: 'atlascloud',\n AVIAN_API_KEY: 'avian',\n QIANFAN_API_KEY: 'qianfan',\n GMI_API_KEY: 'gmi',\n INFERMATIC_API_KEY: 'infermatic',\n INFLECTION_API_KEY: 'inflection',\n MANCER_API_KEY: 'mancer',\n PHALA_API_KEY: 'phala',\n RELACE_API_KEY: 'relace',\n SILICONFLOW_API_KEY: 'siliconflow',\n STEPFUN_API_KEY: 'stepfun',\n VENICE_API_KEY: 'venice',\n // ── Sublote E1 (2026-04-24) — runtime-wiring closure for D1 matrix ───\n // 7 provider slugs whose env vars gained GCP→ENV tuples in this lot.\n // Slug = catalog providerId exactly (so Self-Healing Discovery can\n // attribute key-loaded events to the right provider). CLOUDFLARE_ACCOUNT_ID\n // is intentionally absent — that's a URL parameter, not an auth credential,\n // so mapping it to a provider slug would be semantically wrong (the same\n // account-id could conceivably scope multiple Cloudflare providers).\n GROQ_API_KEY: 'groq',\n DEEPINFRA_API_KEY: 'deepinfra',\n HF_TOKEN: 'huggingface',\n CLOUDFLARE_API_TOKEN: 'cloudflare-workers-ai',\n PERPLEXITY_API_KEY: 'perplexity',\n FIREWORKS_AI_API_KEY: 'fireworks-ai',\n SAMBANOVA_API_KEY: 'sambanova',\n // Replicate — pre-existing live-validation gap closed by sublote E1.\n // See the matching comment on the PROVIDER_SECRETS tuple above.\n REPLICATE_API_KEY: 'replicate',\n // ── LOTE N (2026-04-27) — catalog↔loader closure (22 new env vars) ──\n // Every entry below maps an env var to its canonical providerId so the\n // L1 Self-Healing Discovery Service can attribute key-loaded events\n // when the corresponding GCP secret is provisioned. Slugs match the\n // catalog providerId exactly (NOT upstream brand) — see catalog rows\n // for cases where they diverge (e.g. `sap` not `sap-ai-core`, `v0` not\n // `vercel-v0`, `volcano` not `volcengine`). Side-car env vars\n // (WATSONX_PROJECT_ID, SNOWFLAKE_ACCOUNT, SAP_AI_CORE_CLIENT_SECRET,\n // etc.) are NOT mapped here — they're auth or URL parameters, not\n // credentials, so attributing them to a provider would conflate\n // credential-presence with side-car-completeness in discovery telemetry.\n TOGETHERAI_API_KEY: 'togetherai',\n ANYSCALE_API_KEY: 'anyscale',\n HYPERBOLIC_API_KEY: 'hyperbolic',\n CHUTES_API_KEY: 'chutes',\n NSCALE_API_KEY: 'nscale',\n NEBIUS_API_KEY: 'nebius',\n LAMBDA_AI_API_KEY: 'lambda-ai',\n SCALEWAY_API_KEY: 'scaleway',\n SYNTHETIC_API_KEY: 'synthetic',\n MORPH_API_KEY: 'morph',\n ZAI_API_KEY: 'zai',\n XIAOMI_MIMO_API_KEY: 'xiaomi-mimo',\n V0_API_KEY: 'v0',\n VERCEL_AI_GATEWAY_API_KEY: 'vercel-ai-gateway',\n VOLCANO_API_KEY: 'volcano',\n WATSONX_APIKEY: 'watsonx',\n SNOWFLAKE_PAT: 'snowflake',\n SAP_AI_CORE_CLIENT_ID: 'sap',\n // Image / video-only providers. Still mapped to a slug so Self-Healing\n // Discovery can record key-presence events — but their env vars do NOT\n // appear in LLM_PROVIDER_ENV_VARS (image surfaces don't satisfy the\n // \"at least one LLM key\" boot gate).\n BFL_API_KEY: 'bfl',\n RECRAFT_API_KEY: 'recraft',\n RUNWAYML_API_KEY: 'runwayml',\n TOPAZ_API_KEY: 'topaz',\n // ── LOTE O (2026-07-10) ────────────────────────────────────────────────\n APERTIS_API_KEY: 'apertis',\n INCEPTION_API_KEY: 'inception',\n // ── LOTE P (2026-07-11) ────────────────────────────────────────────────\n EMPIRIOLABS_API_KEY: 'empiriolabs',\n // ── LOTE Q (2026-07-12) ────────────────────────────────────────────────\n CONCENTRATE_API_KEY: 'concentrate',\n // ── LOTE R (2026-07-13) ────────────────────────────────────────────────\n FASTROUTER_API_KEY: 'fastrouter',\n // ── LOTE S (2026-07-13) ────────────────────────────────────────────────\n PERPLEXITY_AGENT_API_KEY: 'perplexity-agent',\n // ── LOTE T (2026-07-13) ────────────────────────────────────────────────\n AILIN_API_KEY: 'ailin',\n // ── LOTE U (2026-07-29) ────────────────────────────────────────────────\n SAKANA_AI_API_KEY: 'sakana-ai',\n};\n\n/**\n * Returns the status of all provider API keys.\n * Used by Self-Healing Discovery to determine which native sources can run.\n */\nexport function getProviderKeyStatus(): ReadonlyMap<string, ProviderKeyStatus> {\n return providerKeyStatusMap;\n}\n\n/**\n * Returns provider names that have valid (non-empty, non-mock) API keys loaded.\n */\nexport function getLoadedProviderNames(): string[] {\n return Array.from(providerKeyStatusMap.entries())\n .filter(([, status]) => status.loaded)\n .map(([name]) => name);\n}\n\n/**\n * Check if a specific provider has a loaded API key.\n */\nexport function isProviderKeyLoaded(providerName: string): boolean {\n return providerKeyStatusMap.get(providerName.toLowerCase())?.loaded ?? false;\n}\n\nconst LLM_PROVIDER_ENV_VARS = [\n 'OPENAI_API_KEY',\n 'ANTHROPIC_API_KEY',\n 'GOOGLE_API_KEY',\n 'DEEPSEEK_API_KEY',\n 'XAI_API_KEY',\n 'MISTRAL_API_KEY',\n 'COHERE_API_KEY',\n 'NVIDIA_API_KEY',\n 'AIHUBMIX_API_KEY',\n 'NOVITA_API_KEY',\n 'MOONSHOT_API_KEY',\n 'MINIMAX_API_KEY',\n 'JINA_API_KEY',\n 'FRIENDLI_API_KEY',\n 'AIML_API_KEY',\n 'IMAGEROUTER_API_KEY',\n 'OPENROUTER_API_KEY',\n 'ORQAI_API_KEY',\n 'EDENAI_API_KEY',\n 'HELICONEAI_API_KEY',\n 'VERTEX_AI_API_KEY',\n 'AWS_BEARER_TOKEN_BEDROCK',\n 'AZURE_OPENAI_API_KEY',\n 'QWEN_API_KEY',\n 'ERNIE_API_KEY',\n 'COMETAPI_API_KEY',\n 'NANOGPT_API_KEY',\n 'REQUESTY_API_KEY',\n 'AI302_API_KEY',\n 'POE_API_KEY',\n 'ROUTEWAY_API_KEY',\n // Added 2026-04-22 — these keys now count toward the \"at least one LLM key\n // present\" gate that avoids DEGRADED_SELF_HOSTED mode at boot.\n 'WANDB_API_KEY',\n 'VOYAGE_API_KEY',\n 'CEREBRAS_API_KEY',\n 'FEATHERLESS_AI_API_KEY',\n // Batch 4 landed BYTEZ catalog row + adapter, so BYTEZ_API_KEY now counts\n // toward the LLM-key-present gate.\n 'BYTEZ_API_KEY',\n // Batch 6 — enterprise hyperscaler gateways (2026-04-22). These keys now\n // count toward the \"at least one LLM key present\" gate; all four have\n // landed catalog rows + adapter classes in this batch.\n 'GEMINI_API_KEY',\n 'GITHUB_TOKEN',\n 'DATABRICKS_TOKEN',\n // Batch 8.1 — AWS SageMaker. Endpoint name is the meaningful trigger:\n // AWS creds alone are ambiguous (could be Bedrock or S3), but an\n // endpoint name implies \"SageMaker is provisioned here\".\n 'AWS_SAGEMAKER_ENDPOINT_NAME',\n // Orphan closure (2026-04-22) — Inworld AI now counts toward the\n // \"at least one LLM key present\" gate now that the catalog row +\n // adapter factory are wired.\n 'INWORLD_API_KEY',\n // Orphan-closure Lot B (2026-04-23). All three are fully canonical now.\n 'WRITER_API_KEY',\n 'UPSTAGE_API_KEY',\n 'REKA_API_KEY',\n // ── LOTE M (2026-04-23) complement lot ──────────────────────────────\n // Each of the 13 keys below counts toward the \"at least one LLM key\n // present\" gate, the same way all catalog-wired keys do. Note that\n // mancer and venice carry contentPolicyClass=uncensored (2026-04-28\n // Phase 4b — the prior denyByDefault gate was removed per the universal\n // \"habilitado e nunca censurado\" policy); their keys count toward the\n // gate exactly like any other LLM key — surface-level filtering is now\n // a downstream concern, not a routing one.\n // qianfan here is QIANFAN_API_KEY (v2 bce-v3 bearer); the legacy v1\n // ERNIE_API_KEY was already listed above this marker.\n 'ARCEE_API_KEY',\n 'ATLASCLOUD_API_KEY',\n 'AVIAN_API_KEY',\n 'QIANFAN_API_KEY',\n 'GMI_API_KEY',\n 'INFERMATIC_API_KEY',\n 'INFLECTION_API_KEY',\n 'MANCER_API_KEY',\n 'PHALA_API_KEY',\n 'RELACE_API_KEY',\n 'SILICONFLOW_API_KEY',\n 'STEPFUN_API_KEY',\n 'VENICE_API_KEY',\n // ── Sublote E1 (2026-04-24) — runtime-wiring closure for D1 matrix ───\n // These 7 providers landed in CONSOLIDATION_MATRIX['live-validation']\n // during Sublote D1 but had no GCP→ENV tuple above → their keys could\n // not count toward the \"at least one LLM key present\" gate even when\n // GCP Secret Manager held the key. Listed here so a restart after the\n // operator's provisioning cycle propagates the key-present signal to\n // the DEGRADED_SELF_HOSTED avoidance logic. CLOUDFLARE_ACCOUNT_ID is\n // intentionally absent — it's a URL parameter, not an LLM credential.\n 'GROQ_API_KEY',\n 'DEEPINFRA_API_KEY',\n 'HF_TOKEN',\n 'CLOUDFLARE_API_TOKEN',\n 'PERPLEXITY_API_KEY',\n 'FIREWORKS_AI_API_KEY',\n 'SAMBANOVA_API_KEY',\n // Replicate — pre-existing live-validation gap closed by sublote E1.\n // See the matching comments on the PROVIDER_SECRETS tuple and\n // ENV_VAR_TO_PROVIDER entry above.\n 'REPLICATE_API_KEY',\n // ── LOTE N (2026-04-27) — catalog↔loader closure ─────────────────────\n // 18 LLM-class providers added to the boot-mode gate. The 4 image/\n // video-only providers (bfl, recraft, runwayml, topaz) are\n // intentionally OMITTED — their key presence does not signal the\n // server has any text-generation capability, so they should not\n // satisfy the \"at least one LLM key present\" check.\n 'TOGETHERAI_API_KEY',\n 'ANYSCALE_API_KEY',\n 'HYPERBOLIC_API_KEY',\n 'CHUTES_API_KEY',\n 'NSCALE_API_KEY',\n 'NEBIUS_API_KEY',\n 'LAMBDA_AI_API_KEY',\n 'SCALEWAY_API_KEY',\n 'SYNTHETIC_API_KEY',\n 'MORPH_API_KEY',\n 'ZAI_API_KEY',\n 'XIAOMI_MIMO_API_KEY',\n 'V0_API_KEY',\n 'VERCEL_AI_GATEWAY_API_KEY',\n 'VOLCANO_API_KEY',\n 'WATSONX_APIKEY',\n 'SNOWFLAKE_PAT',\n 'SAP_AI_CORE_CLIENT_ID',\n // ── LOTE O (2026-07-10) — Apertis + Inception Labs, both chat-capable ──\n 'APERTIS_API_KEY',\n 'INCEPTION_API_KEY',\n // ── LOTE P (2026-07-11) — EmpirioLabs AI, chat-capable ─────────────────\n 'EMPIRIOLABS_API_KEY',\n // ── LOTE Q (2026-07-12) — Concentrate AI, chat-capable ─────────────────\n 'CONCENTRATE_API_KEY',\n // ── LOTE R (2026-07-13) — FastRouter, chat-capable ─────────────────────\n 'FASTROUTER_API_KEY',\n // ── LOTE S (2026-07-13) — Perplexity Agent API, chat-capable ───────────\n 'PERPLEXITY_AGENT_API_KEY',\n // ── LOTE T (2026-07-13) — Ailin gateway, chat-capable ──────────────────\n 'AILIN_API_KEY',\n // ── LOTE U (2026-07-29) — Sakana AI (Fugu), chat-capable ────────────────\n 'SAKANA_AI_API_KEY',\n] as const;\n\n/**\n * Self-hosted / sidecar endpoints that can keep the server alive in a\n * DEGRADED mode when every external LLM API key is missing.\n *\n * Design rationale:\n * Without this fallback, a single failure (expired GCP ADC, rotated\n * service account, network blip during boot) produces an infinite\n * restart loop. With it, the server boots in `degraded_self_hosted`\n * mode: only local/sidecar models are usable, requests are tagged\n * via last-resort-policy.ts (execution_mode=last_resort_self_hosted,\n * excluded_from_benchmark=true), and ops gets a clear warning\n * instead of a crash loop.\n *\n * This ties directly into Bloco P1.10 (self-hosted last-resort policy)\n * but at boot-time scope rather than per-request scope.\n */\nconst SELF_HOSTED_ENV_VARS = [\n 'OLLAMA_URL',\n 'SELF_HOSTED_LLM_URL',\n 'LOCAL_LLAMA_URL',\n 'LOCAL_KOBOLD_URL',\n] as const;\n\n/**\n * Load critical secrets from GCP into process.env\n *\n * This function is called AFTER SecretsManager initialization\n * and BEFORE config validation.\n *\n * It loads secrets from GCP and injects them into process.env,\n * allowing the existing config system to work without modification.\n */\nexport async function loadSecretsIntoEnv(): Promise<void> {\n const log = logger.child({ component: 'secrets-loader' });\n\n log.info('Loading critical secrets from GCP Secret Manager into environment...');\n\n let loaded = 0;\n let skipped = 0;\n let failed = 0;\n let sanitizedKeys = 0;\n let providerSecretsLoaded = 0;\n // Tracks env values that were authoritatively replaced by GCP fetches.\n // Useful diagnostic for operators verifying that GCP is the source of truth.\n let overwrittenFromGcp = 0;\n\n const forceLoadRealKeys = process.env.TEST_USE_REAL_API_KEYS === 'true';\n\n // ── GCP-authoritative mode ─────────────────────────────────────────────\n // When SECRETS_PROVIDER_PRIMARY=gcp the operator has declared GCP as the\n // source of truth. The legacy \"skip if env already populated\" path then\n // contradicts that declaration: env_file / docker-compose / shell exports\n // silently win and GCP fetches are never even attempted, which is exactly\n // the failure mode that hid populated GCP secrets (e.g. HF_TOKEN) from\n // /v1/models. In authoritative mode we ALWAYS attempt the GCP fetch and\n // overwrite the pre-existing env value when GCP returns one. The fallback\n // is symmetric: a GCP miss preserves the existing env value (no zeroing).\n //\n // Operators can override:\n // SECRETS_GCP_AUTHORITATIVE=false → keep legacy skip-if-exists semantics\n // SECRETS_GCP_AUTHORITATIVE=true → force authoritative even with env primary\n const gcpPrimaryEnv = (process.env.SECRETS_PROVIDER_PRIMARY || '').trim().toLowerCase() === 'gcp';\n const explicitAuthoritative = (process.env.SECRETS_GCP_AUTHORITATIVE || '').trim().toLowerCase();\n const gcpAuthoritative =\n explicitAuthoritative === 'true' || (explicitAuthoritative !== 'false' && gcpPrimaryEnv);\n\n // Tests already use TEST_USE_REAL_API_KEYS=true to force GCP fetches even\n // when local mocks are present. The two flags are independent triggers\n // for the same effect; either one disables skip-if-exists.\n const authoritativeFetch = gcpAuthoritative || forceLoadRealKeys;\n\n log.info(\n {\n gcpPrimary: gcpPrimaryEnv,\n gcpAuthoritative,\n forceLoadRealKeys,\n authoritativeFetch,\n },\n 'Secret loading mode resolved'\n );\n\n // Load critical secrets (required)\n for (const { envVar, secretKey, required, defaultValue } of CRITICAL_SECRETS) {\n const existingValue = process.env[envVar];\n const shouldOverrideMockStripe =\n forceLoadRealKeys &&\n envVar.startsWith('STRIPE_') &&\n typeof existingValue === 'string' &&\n (existingValue.includes('mock') || existingValue.includes('test'));\n\n // Skip if already in env, UNLESS:\n // - we're in GCP-authoritative mode (operator declared GCP as source of truth), OR\n // - we're forcing real keys and detected mock Stripe keys.\n if (existingValue && !shouldOverrideMockStripe && !gcpAuthoritative) {\n log.debug({ envVar, source: 'env' }, 'Secret already in environment, skipping GCP load');\n skipped++;\n continue;\n }\n\n try {\n const value = await loadSecret(secretKey, required);\n if (value) {\n const wasOverwrite = Boolean(existingValue) && existingValue !== value;\n process.env[envVar] = value;\n if (wasOverwrite) overwrittenFromGcp++;\n log.debug(\n { envVar, secretKey, replaced: Boolean(existingValue), overwrittenFromGcp: wasOverwrite },\n 'Secret loaded from GCP into environment'\n );\n loaded++;\n } else if (existingValue && gcpAuthoritative) {\n // Authoritative fetch missed in GCP — fall back to the existing env\n // value rather than zeroing it. This keeps the system bootable when\n // a single secret is absent from GCP without losing the env-provided\n // value.\n log.debug(\n { envVar, secretKey },\n 'GCP miss in authoritative mode; preserving existing env value'\n );\n skipped++;\n } else if (defaultValue !== undefined) {\n // Use default value if secret not found and default provided\n process.env[envVar] = defaultValue;\n log.debug({ envVar, secretKey, defaultValue }, 'Secret not found, using default value');\n skipped++;\n } else if (!required) {\n log.debug({ envVar, secretKey }, 'Optional secret not found, skipping');\n skipped++;\n }\n } catch (error) {\n if (defaultValue !== undefined) {\n // Use default value on error if provided\n process.env[envVar] = defaultValue;\n log.warn(\n { envVar, secretKey, defaultValue, error: getErrorMessage(error) },\n 'Failed to load secret, using default value'\n );\n skipped++;\n } else if (required) {\n const errorMsg = getErrorMessage(error);\n log.error({ envVar, secretKey, error: errorMsg }, 'Failed to load required secret');\n failed++;\n throw new Error(`Failed to load required secret ${secretKey}: ${errorMsg}`);\n } else {\n log.warn(\n { envVar, secretKey, error: getErrorMessage(error) },\n 'Failed to load optional secret'\n );\n skipped++;\n }\n }\n }\n\n // Keep compatibility with components that still read GCS_BUCKET_NAME.\n if (!process.env.GCS_BUCKET_NAME && process.env.GCS_FILES_BUCKET) {\n process.env.GCS_BUCKET_NAME = process.env.GCS_FILES_BUCKET;\n }\n\n // Load provider API keys (optional)\n // CRITICAL: If GCP auth is available, ALWAYS load real keys (API Keys NUNCA DEVEM FALHAR)\n // If TEST_USE_REAL_API_KEYS=true, force load from GCP even if mock keys exist\n // Always replace mock keys with real keys from GCP when available\n // (reuses forceLoadRealKeys defined above)\n\n for (const { envVar, secretKeys } of PROVIDER_SECRETS) {\n const existingValue = process.env[envVar];\n const providerName = ENV_VAR_TO_PROVIDER[envVar];\n\n // Check if existing value looks like a mock key\n const isMockKey =\n existingValue && (existingValue.includes('mock') || existingValue.includes('test-'));\n\n // Skip if real key already exists, UNLESS:\n // - we're in GCP-authoritative mode (operator declared GCP as source of truth), OR\n // - test forces real keys (TEST_USE_REAL_API_KEYS=true), OR\n // - the existing value looks like a mock.\n // The authoritative path lets GCP overwrite stale env_file / shell-exported\n // values, which is the entire point of SECRETS_PROVIDER_PRIMARY=gcp.\n if (existingValue && !authoritativeFetch && !isMockKey) {\n // Record as loaded from env\n if (providerName) {\n providerKeyStatusMap.set(providerName, {\n loaded: true,\n source: 'env',\n timestamp: new Date(),\n envVar,\n });\n }\n skipped++;\n continue;\n }\n\n try {\n let loadedSecretValue: string | undefined;\n let resolvedSecretKey = secretKeys[0];\n\n for (const candidateKey of secretKeys) {\n try {\n const candidateValue = await loadSecret(candidateKey, false);\n if (candidateValue) {\n loadedSecretValue = candidateValue;\n resolvedSecretKey = candidateKey;\n log.debug(\n { envVar, secretKey: candidateKey, valueLen: candidateValue.length },\n 'Secret loaded from GCP'\n );\n break;\n } else {\n log.debug({ envVar, secretKey: candidateKey }, 'Secret returned empty/null from GCP');\n }\n } catch (loadErr) {\n log.warn(\n {\n envVar,\n secretKey: candidateKey,\n error: loadErr instanceof Error ? loadErr.message : String(loadErr),\n },\n 'Failed to load secret from GCP'\n );\n continue;\n }\n }\n\n // GCP provider automatically adds prefix 'ailin-' to secretKey\n // So 'openai-key' becomes 'ailin-openai-key' in GCP Secret Manager\n const value = loadedSecretValue;\n if (value) {\n // Sanitize control characters and invisible Unicode.\n // PEM-format keys (-----BEGIN ...) legitimately contain newlines,\n // so use a lighter regex that preserves \\n and \\r for those values.\n const isPemKey = value.startsWith('-----BEGIN');\n let sanitizedValue = isPemKey\n ? value\n .replace(/[\\x00-\\x09\\x0B\\x0C\\x0E-\\x1F\\x7F]/g, '') // Strip control chars EXCEPT \\n (0x0A) and \\r (0x0D)\n .replace(/[\\u200B-\\u200D\\uFEFF]/g, '') // Remove zero-width spaces\n .trim()\n : value\n .replace(/[\\x00-\\x1F\\x7F]/g, '') // Strip ALL control characters\n .replace(/[\\u200B-\\u200D\\uFEFF]/g, '') // Remove zero-width spaces\n .trim();\n\n // Log detailed debugging information if sanitization changed the value\n if (sanitizedValue !== value) {\n const removedChars = value.length - sanitizedValue.length;\n sanitizedKeys++;\n log.debug(\n {\n envVar,\n secretKey: resolvedSecretKey,\n originalLength: value.length,\n sanitizedLength: sanitizedValue.length,\n removedChars,\n },\n 'API key contained invalid characters and was sanitized'\n );\n }\n\n // Always replace mock keys with real keys\n const wasReplaced = !!existingValue;\n const wasOverwriteOfRealValue =\n wasReplaced && !isMockKey && existingValue !== sanitizedValue;\n\n // CRITICAL: Set process.env and immediately validate\n // Use Object.defineProperty as fallback if direct assignment doesn't work\n // This ensures the value is actually set in the environment\n const _previousValue = process.env[envVar];\n process.env[envVar] = sanitizedValue;\n\n // Immediately read back to verify (handles any potential async issues)\n const readBackValue = process.env[envVar];\n\n // Deep validation: check exact match, type, and length\n const isValidMatch = readBackValue === sanitizedValue;\n const isValidType = typeof readBackValue === 'string';\n const isValidLength =\n readBackValue !== undefined && readBackValue.length === sanitizedValue.length;\n\n if (!isValidMatch || !isValidType || !isValidLength) {\n log.error(\n {\n envVar,\n secretKey: resolvedSecretKey,\n sanitizedValueLength: sanitizedValue.length,\n readBackValueLength: readBackValue?.length,\n },\n 'Secret was loaded but process.env assignment did not persist'\n );\n failed++;\n } else {\n // Success - log and increment counter\n if (wasOverwriteOfRealValue) overwrittenFromGcp++;\n log.debug(\n {\n envVar,\n secretKey: resolvedSecretKey,\n replaced: wasReplaced,\n wasMock: isMockKey,\n overwrittenFromGcp: wasOverwriteOfRealValue,\n },\n 'Provider secret loaded from GCP'\n );\n loaded++;\n providerSecretsLoaded++;\n // Record successful load\n if (providerName) {\n providerKeyStatusMap.set(providerName, {\n loaded: true,\n source: 'gcp',\n timestamp: new Date(),\n envVar,\n });\n }\n }\n } else {\n // GCP returned no value. Decide what's actually in process.env right\n // now: if env had a non-mock value before this loop iteration we\n // preserved it (the authoritative path entered this block but found\n // no GCP value to overwrite with), so we're effectively env-loaded.\n // If env had a mock value, we keep the mock as a degraded fallback\n // and warn loudly. If env had nothing, the provider stays orphaned.\n if (existingValue && !isMockKey) {\n // Authoritative-mode GCP miss with real env value already set:\n // the env value is still in process.env, so report source=env.\n if (providerName) {\n providerKeyStatusMap.set(providerName, {\n loaded: true,\n source: 'env',\n timestamp: new Date(),\n envVar,\n });\n }\n log.debug(\n { envVar, secretKeys },\n 'GCP miss in authoritative mode; preserving existing env value for provider'\n );\n } else {\n if (isMockKey) {\n log.warn(\n { envVar, secretKeys },\n 'Mock API key detected but no real key found in GCP - using mock key (this may cause API failures)'\n );\n }\n // Record as not loaded (no GCP value, no usable env value)\n if (providerName) {\n providerKeyStatusMap.set(providerName, {\n loaded: false,\n source: 'none',\n timestamp: new Date(),\n envVar,\n });\n }\n }\n skipped++;\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n // Only warn if not forcing real keys - if forcing, this is expected to work\n if (forceLoadRealKeys) {\n log.error(\n { envVar, secretKeys, error: errorMessage },\n 'Failed to load provider secret from GCP (TEST_USE_REAL_API_KEYS=true)'\n );\n failed++;\n } else {\n if (isMockKey) {\n log.warn(\n { envVar, secretKeys, error: errorMessage },\n 'Failed to load provider secret from GCP - will use mock key (this may cause API failures)'\n );\n } else {\n log.debug(\n { envVar, secretKeys, error: errorMessage },\n 'Failed to load provider secret from GCP (optional)'\n );\n }\n // Record failure\n if (providerName) {\n providerKeyStatusMap.set(providerName, {\n loaded: false,\n source: 'none',\n timestamp: new Date(),\n envVar,\n });\n }\n skipped++;\n }\n }\n }\n\n // ── Env-var aliases (2026-04-28) ────────────────────────────────────────\n //\n // Some operators set a non-canonical name in `.env` for historical reasons\n // (e.g. `REKAAI_API_KEY` instead of the catalog-canonical `REKA_API_KEY`).\n // When GCP is reachable the secret loader populates the canonical name and\n // it just works; when GCP is unreachable the alias would be the only thing\n // set and the catalog row reads the canonical → empty key → silent skip.\n //\n // This pass copies known aliases into their canonical env var if the\n // canonical is empty. It is purely additive — never overwrites an\n // existing canonical value, never logs a warning unless an alias was\n // applied. Add new entries here when a similar mismatch is discovered.\n const ENV_ALIASES: ReadonlyArray<{ canonical: string; alias: string }> = [\n { canonical: 'REKA_API_KEY', alias: 'REKAAI_API_KEY' },\n ];\n for (const { canonical, alias } of ENV_ALIASES) {\n const canonicalValue = process.env[canonical];\n const aliasValue = process.env[alias];\n if (\n (canonicalValue == null || canonicalValue.trim() === '') &&\n aliasValue &&\n aliasValue.trim() !== ''\n ) {\n process.env[canonical] = aliasValue;\n log.info({ canonical, alias }, 'Promoted alias env var to canonical name');\n }\n }\n\n const gcpPrimary = (process.env.SECRETS_PROVIDER_PRIMARY || '').trim().toLowerCase() === 'gcp';\n const failFastEnv = (process.env.SECRETS_GCP_FAIL_FAST || '').trim().toLowerCase();\n const gcpFailFast = failFastEnv === 'true' || (failFastEnv !== 'false' && gcpPrimary);\n const hasAnyLlmProviderKey = LLM_PROVIDER_ENV_VARS.some((envVar) => {\n const value = process.env[envVar];\n return typeof value === 'string' && value.trim().length > 0;\n });\n\n // Hardening (P1.10 at boot scope): even when zero external API keys are\n // loaded, the server can still boot in DEGRADED mode if a self-hosted\n // endpoint is reachable. Every request will then be tagged via the\n // last-resort-policy as execution_mode=last_resort_self_hosted.\n const availableSelfHostedEndpoints = SELF_HOSTED_ENV_VARS.filter((envVar) => {\n const value = process.env[envVar];\n return typeof value === 'string' && value.trim().length > 0;\n });\n const hasAnySelfHostedFallback = availableSelfHostedEndpoints.length > 0;\n\n // Opt-out for strict production deployments that must never run in\n // degraded mode. Default: allow degraded boot (safer than crash loops).\n const allowDegradedBoot =\n (process.env.ALLOW_DEGRADED_BOOT || 'true').trim().toLowerCase() !== 'false';\n\n if (gcpFailFast && !hasAnyLlmProviderKey) {\n if (hasAnySelfHostedFallback && allowDegradedBoot) {\n log.warn(\n {\n gcpPrimary,\n gcpFailFast,\n providerSecretsLoaded,\n selfHostedEndpoints: availableSelfHostedEndpoints,\n mode: 'degraded_self_hosted',\n docs: 'docs/hardening/last-resort-policy',\n },\n '⚠️ DEGRADED BOOT: zero external LLM credentials, but self-hosted fallback is available. ' +\n 'Server will start in last-resort mode. Requests will be tagged excluded_from_benchmark. ' +\n 'Fix ADC/GCP credentials to restore full operation.'\n );\n // Intentionally fall through — boot continues\n } else {\n const message = hasAnySelfHostedFallback\n ? 'SECRETS_PROVIDER_PRIMARY=gcp, no LLM provider credentials loaded, and ALLOW_DEGRADED_BOOT=false.'\n : 'SECRETS_PROVIDER_PRIMARY=gcp but no LLM provider credentials are available after secrets bootstrap, ' +\n 'and no self-hosted fallback endpoints (OLLAMA_URL, SELF_HOSTED_LLM_URL, LOCAL_LLAMA_URL, LOCAL_KOBOLD_URL) are configured.';\n log.error(\n {\n gcpPrimary,\n gcpFailFast,\n providerSecretsLoaded,\n checkedEnvVars: LLM_PROVIDER_ENV_VARS,\n selfHostedEndpointsChecked: SELF_HOSTED_ENV_VARS,\n allowDegradedBoot,\n },\n message\n );\n throw new Error(\n `${message} Verify GCP Secret Manager access (ADC/Workload Identity) and secret names/prefix, ` +\n `or set OLLAMA_URL for degraded self-hosted boot, or run 'gcloud auth application-default login' if local.`\n );\n }\n }\n\n // Warn on partial provider key loading — helps detect GCP issues early\n if (hasAnyLlmProviderKey && providerSecretsLoaded > 0 && providerSecretsLoaded < 3) {\n log.warn(\n { providerSecretsLoaded, total: PROVIDER_SECRETS.length },\n `⚠️ Only ${providerSecretsLoaded} provider secrets loaded — possible partial GCP failure`\n );\n }\n\n // Audio provider diagnostics\n const audioProviders = {\n DEEPGRAM_API_KEY: !!process.env.DEEPGRAM_API_KEY,\n CARTESIA_API_KEY: !!process.env.CARTESIA_API_KEY,\n ELEVENLABS_API_KEY: !!process.env.ELEVENLABS_API_KEY,\n OLLAMA_URL: process.env.OLLAMA_URL || '',\n SELF_HOSTED_STT_URL: process.env.SELF_HOSTED_STT_URL || '',\n LOCAL_NLLB_URL: process.env.LOCAL_NLLB_URL || '',\n };\n log.info({ audioProviders }, 'Audio provider env vars after secrets loading');\n\n log.info(\n {\n loaded,\n skipped,\n failed,\n sanitizedKeys,\n providerSecretsLoaded,\n overwrittenFromGcp,\n gcpAuthoritative,\n total: CRITICAL_SECRETS.length + PROVIDER_SECRETS.length,\n },\n '✅ Secrets loading complete'\n );\n\n if (failed > 0) {\n throw new Error(`Failed to load ${failed} required secrets`);\n }\n}\n\n/**\n * Get summary of secrets loaded.\n *\n * Diagnostic helper for admin endpoints (e.g. /v1/admin/providers/...) that\n * surfaces exactly which provider envVars are populated and from where, so\n * operators can verify SECRETS_PROVIDER_PRIMARY=gcp is actually authoritative\n * and not silently shadowed by stale env values.\n */\nexport function getSecretsLoadSummary(): {\n fromGCP: string[];\n fromEnv: string[];\n notLoaded: string[];\n} {\n const fromGCP: string[] = [];\n const fromEnv: string[] = [];\n const notLoaded: string[] = [];\n\n for (const status of providerKeyStatusMap.values()) {\n if (!status.loaded) {\n notLoaded.push(status.envVar);\n continue;\n }\n if (status.source === 'gcp') {\n fromGCP.push(status.envVar);\n } else if (status.source === 'env') {\n fromEnv.push(status.envVar);\n }\n }\n\n return { fromGCP, fromEnv, notLoaded };\n}\n"}, "6ff5594f823d436d8790cbaff441cc3b2e82b54a": {"t": 1785745596.8415034, "n": 64471, "h": "0136561c854731e8", "c": "// Copyright (C) 2026 Ailin One, Inc.\n//\n// This file is part of Collective Intelligence Engine (ci).\n// Licensed under the GNU Affero General Public License v3.0 or later.\n// See LICENSE in the repository root, or <https://www.gnu.org/licenses/>.\n//\n// SPDX-License-Identifier: AGPL-3.0-or-later\n// Source: https://github.com/ailinone/collective-intelligence\n\n/**\n * Load Secrets into Environment\n *\n * This module loads critical secrets from GCP Secret Manager\n * and injects them into process.env BEFORE config validation.\n *\n * This allows the existing config system to work without refactoring,\n * while still using GCP Secret Manager as the source of truth.\n *\n * Call order:\n * 1. initializeSecretsManager()\n * 2. loadSecretsIntoEnv() ← This file\n * 3. validateConfig()\n */\n\nimport { logger } from '@/utils/logger';\nimport { getErrorMessage } from '@/utils/type-guards';\nimport { loadSecret } from './secrets-loader';\n\n/**\n * Critical secrets that must be loaded from GCP\n * These are injected into process.env for config compatibility\n */\ninterface CriticalSecret {\n envVar: string;\n secretKey: string;\n required: boolean;\n defaultValue?: string;\n}\n\nconst CRITICAL_SECRETS: CriticalSecret[] = [\n { envVar: 'DATABASE_URL', secretKey: 'database-url', required: true },\n { envVar: 'JWT_SECRET', secretKey: 'jwt-secret', required: true },\n { envVar: 'REDIS_PASSWORD', secretKey: 'redis-password', required: false, defaultValue: '' },\n { envVar: 'GCS_FILES_BUCKET', secretKey: 'gcs-files-bucket', required: false },\n { envVar: 'SMTP_PASS', secretKey: 'smtp-pass', required: false },\n { envVar: 'STRIPE_SECRET_KEY', secretKey: 'stripe-secret-key', required: false },\n { envVar: 'STRIPE_WEBHOOK_SECRET', secretKey: 'stripe-webhook-secret', required: false },\n { envVar: 'STRIPE_PUBLISHABLE_KEY', secretKey: 'stripe-publishable-key', required: false },\n // Phase 2c shadow ensemble bearer token. Optional because the coord-\n // stable wire is disabled by default in production\n // (CI_ENSEMBLE_COORDINATOR_ENABLED=false). When operator flips it on,\n // the GCP secret `coord-ensemble-token` MUST also be set with the\n // value to share with coord-serving's COORD_SERVING_AUTH_TOKEN.\n { envVar: 'CI_ENSEMBLE_COORDINATOR_TOKEN', secretKey: 'coord-ensemble-token', required: false },\n // Prometheus scrape bearer token for /metrics + /metrics/prompts\n // (metrics-route.ts `authorizeScrape`). Optional here — the route itself\n // is fail-closed when unset: it denies (403) in production and stays\n // open for local/dev scraping otherwise. Wired via CRITICAL_SECRETS\n // (not PROVIDER_SECRETS) because this is a general app credential, not\n // a provider API key.\n { envVar: 'PROMETHEUS_SCRAPE_TOKEN', secretKey: 'prometheus-scrape-token', required: false },\n // Sentry error-tracking DSN (utils/error-tracking.ts `initializeErrorTracking`).\n // Optional — the module itself no-ops with a warning log when unset, both\n // at API boot (index.ts) and worker boot (workers/queue-runner.ts), so\n // there is no fail-closed/fail-open behavior riding on this like the\n // Prometheus token above. Wired via CRITICAL_SECRETS (not PROVIDER_SECRETS)\n // because this is a general app credential, not a provider API key. No GCP\n // secret exists yet as of this commit — an operator must create one\n // (`ailin-sentry-dsn`) with a real DSN from Sentry's project settings\n // before this ever resolves to a non-empty value.\n { envVar: 'SENTRY_DSN', secretKey: 'sentry-dsn', required: false },\n];\n\n/**\n * Provider API keys (loaded from GCP if not in env)\n * CRITICAL: GCP provider automatically adds prefix 'ailin-' to secret keys\n * So 'openai-key' becomes 'ailin-openai-key' in GCP Secret Manager\n * These keys match the actual secret names in GCP (without the prefix, provider adds it)\n */\ninterface ProviderSecretBinding {\n envVar: string;\n secretKeys: readonly string[];\n}\n\nexport const PROVIDER_SECRETS: readonly ProviderSecretBinding[] = [\n { envVar: 'OPENAI_API_KEY', secretKeys: ['openai-key'] },\n { envVar: 'ANTHROPIC_API_KEY', secretKeys: ['anthropic-key'] },\n { envVar: 'GOOGLE_API_KEY', secretKeys: ['google-key'] },\n { envVar: 'DEEPSEEK_API_KEY', secretKeys: ['deepseek-key'] },\n { envVar: 'XAI_API_KEY', secretKeys: ['xai-key'] },\n { envVar: 'MISTRAL_API_KEY', secretKeys: ['mistral-key'] },\n { envVar: 'COHERE_API_KEY', secretKeys: ['cohere-key'] },\n { envVar: 'NVIDIA_API_KEY', secretKeys: ['nvidia-key', 'nvidia-api-key'] },\n { envVar: 'AIHUBMIX_API_KEY', secretKeys: ['aihubmix-key', 'aihubmix-api-key'] },\n { envVar: 'NOVITA_API_KEY', secretKeys: ['novita-key', 'novita-api-key'] },\n { envVar: 'MOONSHOT_API_KEY', secretKeys: ['moonshot-key', 'moonshot-api-key'] },\n { envVar: 'MINIMAX_API_KEY', secretKeys: ['minimax-key', 'minimax-api-key'] },\n { envVar: 'JINA_API_KEY', secretKeys: ['jina-key', 'jina-api-key'] },\n { envVar: 'GOOGLE_MAPS_API_KEY', secretKeys: ['google-maps-key', 'google-maps-api-key'] },\n { envVar: 'FRIENDLI_API_KEY', secretKeys: ['friendli-key', 'friendli-api-key'] },\n { envVar: 'FRIENDLI_TEAM_ID', secretKeys: ['friendli-team-id'] },\n { envVar: 'AIML_API_KEY', secretKeys: ['aiml-key', 'aiml-api-key'] },\n { envVar: 'IMAGEROUTER_API_KEY', secretKeys: ['imagerouter-key', 'imagerouter-api-key'] },\n { envVar: 'OPENROUTER_API_KEY', secretKeys: ['openrouter-key'] },\n { envVar: 'COMETAPI_API_KEY', secretKeys: ['cometapi-key', 'cometapi-api-key'] },\n { envVar: 'NANOGPT_API_KEY', secretKeys: ['nanogpt-key', 'nanogpt-api-key'] },\n { envVar: 'REQUESTY_API_KEY', secretKeys: ['requesty-key', 'requesty-api-key'] },\n { envVar: 'AI302_API_KEY', secretKeys: ['302-key', '302ai-key'] },\n { envVar: 'POE_API_KEY', secretKeys: ['poe-key', 'poe-api-key'] },\n { envVar: 'ROUTEWAY_API_KEY', secretKeys: ['routeway-key', 'routeway-api-key'] },\n // Hub providers support both key naming conventions for compatibility.\n { envVar: 'ORQAI_API_KEY', secretKeys: ['orqai-key', 'orqai-api-key'] },\n { envVar: 'ORQAI_MODEL_DENYLIST', secretKeys: ['orqai-model-denylist'] },\n { envVar: 'EDENAI_API_KEY', secretKeys: ['edenai-key', 'edenai-api-key'] },\n { envVar: 'HELICONEAI_API_KEY', secretKeys: ['heliconeai-key', 'heliconeai-api-key'] },\n { envVar: 'VERTEX_AI_API_KEY', secretKeys: ['vertex-key'] },\n { envVar: 'VERTEX_AI_PROJECT_ID', secretKeys: ['vertex-project-id'] },\n { envVar: 'QWEN_API_KEY', secretKeys: ['qwen-key-2'] },\n { envVar: 'ALIBABA_ACCESS_KEY_ID', secretKeys: ['alibaba-key-id'] },\n { envVar: 'ALIBABA_ACCESS_KEY_SECRET', secretKeys: ['alibaba-key-secret'] },\n { envVar: 'ERNIE_API_KEY', secretKeys: ['baidu-key'] },\n { envVar: 'ERNIE_SECRET_KEY', secretKeys: ['baidu-secret'] },\n { envVar: 'BAIDU_BASE_URL', secretKeys: ['baidu-base-url'] },\n { envVar: 'AWS_ACCESS_KEY_ID', secretKeys: ['aws-key-id'] },\n { envVar: 'AWS_SECRET_ACCESS_KEY', secretKeys: ['aws-secret'] },\n { envVar: 'AWS_BEARER_TOKEN_BEDROCK', secretKeys: ['aws-bearer-token'] },\n // Batch 7.1: Bedrock region + inference-profile ARN can now be set via\n // GCP Secret Manager. Useful for deploys that route Bedrock to a specific\n // region (e.g. us-west-2) or via cross-region inference profiles without\n // baking these into the docker image.\n { envVar: 'AWS_BEDROCK_REGION', secretKeys: ['aws-bedrock-region'] },\n {\n envVar: 'AWS_BEDROCK_INFERENCE_PROFILE_ARN',\n secretKeys: ['aws-bedrock-inference-profile-arn'],\n },\n // Batch 8.1: SageMaker endpoint routing. A deploy can point this adapter\n // at a specific deployed endpoint via a secret rather than baking the\n // endpoint name into the image. The region override lets the adapter\n // hit an endpoint in a different region from the general AWS_REGION\n // used by other services (SES, S3).\n { envVar: 'AWS_SAGEMAKER_REGION', secretKeys: ['aws-sagemaker-region'] },\n { envVar: 'AWS_SAGEMAKER_ENDPOINT_NAME', secretKeys: ['aws-sagemaker-endpoint-name'] },\n { envVar: 'AWS_SAGEMAKER_PAYLOAD_SCHEMA', secretKeys: ['aws-sagemaker-payload-schema'] },\n { envVar: 'AWS_SAGEMAKER_CUSTOM_ATTRIBUTES', secretKeys: ['aws-sagemaker-custom-attributes'] },\n { envVar: 'AZURE_OPENAI_API_KEY', secretKeys: ['azure-openai-api-key'] },\n { envVar: 'AZURE_OPENAI_ENDPOINT', secretKeys: ['azure-openai-endpoint'] },\n { envVar: 'AZURE_OPENAI_DEPLOYMENT', secretKeys: ['azure-openai-deployment'] },\n // Audio-first providers (STT, TTS, STS)\n { envVar: 'DEEPGRAM_API_KEY', secretKeys: ['deepgram-key'] },\n { envVar: 'CARTESIA_API_KEY', secretKeys: ['cartesia-key'] },\n { envVar: 'ELEVENLABS_API_KEY', secretKeys: ['elevenlabs-key'] },\n // Translation provider\n { envVar: 'PALABRAAI_CLIENT_ID', secretKeys: ['palabraai-id'] },\n { envVar: 'PALABRAAI_CLIENT_SECRET', secretKeys: ['palabraai-key'] },\n { envVar: 'OCI_TENANCY_ID', secretKeys: ['oci-tenancy-id'] },\n { envVar: 'OCI_USER_ID', secretKeys: ['oci-user-id'] },\n { envVar: 'OCI_FINGERPRINT', secretKeys: ['oci-fingerprint'] },\n { envVar: 'OCI_PRIVATE_KEY', secretKeys: ['oci-private-key'] },\n { envVar: 'OCI_REGION', secretKeys: ['oci-region'] },\n // 2026-04-22: New GCP secrets (5 providers). The GCP provider auto-prefixes\n // 'ailin-', so 'wandb-key' maps to ailin-wandb-key in Secret Manager.\n // Voyage lists both 'voyageai-key' (current upstream name) and 'voyage-key'\n // (conventional fallback) because the canonical spelling is still unstable.\n { envVar: 'WANDB_API_KEY', secretKeys: ['wandb-key', 'wandb-api-key'] },\n { envVar: 'VOYAGE_API_KEY', secretKeys: ['voyageai-key', 'voyage-key', 'voyage-api-key'] },\n { envVar: 'BYTEZ_API_KEY', secretKeys: ['bytez-key', 'bytez-api-key'] },\n { envVar: 'CEREBRAS_API_KEY', secretKeys: ['cerebras-key', 'cerebras-api-key'] },\n { envVar: 'FEATHERLESS_AI_API_KEY', secretKeys: ['featherless-key', 'featherless-api-key'] },\n // Batch 6 — enterprise hyperscaler gateways (2026-04-22).\n // Azure OpenAI already has API_KEY/ENDPOINT/DEPLOYMENT wired above;\n // these fill in the remaining URL-template parameters.\n {\n envVar: 'AZURE_OPENAI_RESOURCE_NAME',\n secretKeys: ['azure-openai-resource-name', 'azure-openai-resource'],\n },\n { envVar: 'AZURE_OPENAI_API_VERSION', secretKeys: ['azure-openai-api-version'] },\n // Gemini OAI-compat shares the API key with the native `google` provider.\n // Two env-var aliases: GEMINI_API_KEY (canonical upstream) and\n // GOOGLE_AI_STUDIO_API_KEY (legacy). The adapter reads either.\n //\n // 2026-04-23: `vertex-key` added as a fallback source because it was\n // verified working against generativelanguage.googleapis.com (HTTP 200,\n // 50 Gemini models) while `google-key` is suspended (403\n // CONSUMER_SUSPENDED). The Vertex/Gemini API-key split is mostly\n // historical — a Google Cloud API key scoped for Gemini works equally on\n // aistudio and on the generativelanguage surface, as long as the key's\n // GCP project has the API enabled.\n //\n // 2026-07-25: `vertex-key` was originally placed LAST on the (incorrect)\n // assumption that the loop below tries each candidate live and keeps the\n // first one that WORKS. It doesn't — it takes the first candidate that\n // merely EXISTS in Secret Manager (see the resolution loop a few hundred\n // lines down), so the suspended `google-key` always won and gemini-openai\n // stayed broken. Moved `vertex-key` first since it's the only one\n // confirmed live-working; the others remain as fallbacks if it's ever\n // rotated out.\n {\n envVar: 'GEMINI_API_KEY',\n secretKeys: ['vertex-key', 'gemini-key', 'google-ai-studio-key', 'google-key'],\n },\n // GitHub Models uses a GitHub PAT. Operators typically already have this\n // in env (it's the same PAT used for gh CLI); the GCP path is a fallback\n // for service accounts that need isolated tokens.\n { envVar: 'GITHUB_TOKEN', secretKeys: ['github-models-token', 'github-token', 'github-pat'] },\n // Databricks workspace token + config.\n { envVar: 'DATABRICKS_TOKEN', secretKeys: ['databricks-token', 'databricks-pat'] },\n { envVar: 'DATABRICKS_HOST', secretKeys: ['databricks-host'] },\n { envVar: 'DATABRICKS_SERVING_ENDPOINT', secretKeys: ['databricks-serving-endpoint'] },\n // Inworld AI — orphan closure 2026-04-22. Key is already base64-encoded\n // in GCP (132 chars) because Inworld uses Basic auth, not Bearer. The\n // adapter does NOT re-encode; it passes the secret verbatim into the\n // `Basic ` header. Never base64-encode this value at load time.\n { envVar: 'INWORLD_API_KEY', secretKeys: ['inworld-key', 'inworld-api-key'] },\n // ── Orphan-closure Lot B (2026-04-23) ──────────────────────────────────\n // Three providers closed in a single lot because they share identical\n // shape (thin OAI-compat wrappers) and were all already present in GCP\n // Secret Manager but not wired into env loading. Each tuple lists the\n // preferred secret key first and legacy-or-alternate names as fallbacks.\n { envVar: 'WRITER_API_KEY', secretKeys: ['writer-key', 'writer-api-key'] },\n { envVar: 'UPSTAGE_API_KEY', secretKeys: ['upstage-key', 'upstage-api-key'] },\n // Reka's canonical upstream name is \"reka\" but the key is stored in GCP\n // as `ailin-rekaai-key` (matching the catalog providerId, not the upstream\n // name). `reka-key` is kept as a fallback in case the secret is ever\n // renamed to match upstream naming later.\n { envVar: 'REKA_API_KEY', secretKeys: ['rekaai-key', 'reka-key', 'reka-api-key'] },\n // ── LOTE M (2026-04-23) — complement lot: 13 new env-var mappings. ─────\n // Every provider below landed a catalog row this lot and is classified\n // in CONSOLIDATION_MATRIX['credentials-missing'] (secret-absent sub-class\n // for 12, auth-incomplete for qianfan). Each mapping is wired NOW so the\n // moment an operator provisions the corresponding GCP secret, the key\n // propagates to process.env without a code change. The first element of\n // each secretKeys array is the preferred upstream-convention name; any\n // follow-up entries are fallbacks for historical or legacy aliases.\n { envVar: 'ARCEE_API_KEY', secretKeys: ['arcee-api-key', 'arcee-key'] },\n {\n envVar: 'ATLASCLOUD_API_KEY',\n secretKeys: ['atlascloud-key', 'atlascloud-api-key', 'atlas-cloud-key'],\n },\n { envVar: 'AVIAN_API_KEY', secretKeys: ['avian-key', 'avian-api-key', 'avian-io-key'] },\n // Qianfan (Baidu) v2 bce-v3 bearer. Distinct from the legacy v1 AK+SK\n // material in ERNIE_API_KEY / ERNIE_SECRET_KEY / BAIDU_BASE_URL above —\n // both mappings are intentionally kept so an operator can populate either\n // path (v1 OAuth or v2 bearer) depending on which runtime surface is\n // used. The canonical catalog row `qianfan` reads QIANFAN_API_KEY only.\n {\n envVar: 'QIANFAN_API_KEY',\n secretKeys: ['qianfan-key', 'qianfan-api-key', 'baidu-qianfan-key'],\n },\n { envVar: 'GMI_API_KEY', secretKeys: ['gmi-key', 'gmi-api-key', 'gmicloud-key'] },\n {\n envVar: 'INFERMATIC_API_KEY',\n secretKeys: ['infermatic-api-key', 'infermatic-key', 'totalgpt-key'],\n },\n {\n envVar: 'INFLECTION_API_KEY',\n secretKeys: ['inflection-key', 'inflection-api-key', 'inflection-ai-key'],\n },\n // Mancer (tagged contentPolicyClass=uncensored in catalog; per the universal\n // \"habilitado e nunca censurado\" policy from Phase 4b, 2026-04-28, the\n // catalog row is fully admitted to routing — the tag is informational so\n // downstream surfaces can opt to filter it from a moderated default list).\n { envVar: 'MANCER_API_KEY', secretKeys: ['mancer-key', 'mancer-api-key'] },\n // Phala / RedPill TEE — upstream docs use REDPILL_API_KEY but we wire\n // PHALA_API_KEY as the canonical env var (see apiKeyEnvVarOverrideReason\n // in catalog row `phala`). Legacy `redpill-*` secret names are accepted\n // as fallbacks so an operator storing under either convention works.\n {\n envVar: 'PHALA_API_KEY',\n secretKeys: ['phala-key', 'phala-api-key', 'redpill-key', 'redpill-api-key'],\n },\n { envVar: 'RELACE_API_KEY', secretKeys: ['relace-key', 'relace-api-key'] },\n {\n envVar: 'SILICONFLOW_API_KEY',\n secretKeys: ['siliconflow-api-key', 'siliconflow-key', 'silicon-flow-key'],\n },\n { envVar: 'STEPFUN_API_KEY', secretKeys: ['stepfun-api-key', 'stepfun-key', 'step-key'] },\n // Venice (tagged contentPolicyClass=uncensored in catalog). Same Phase-4b\n // treatment as Mancer — admitted, never censored, with the tag as the\n // audit trail for the policy choice.\n { envVar: 'VENICE_API_KEY', secretKeys: ['venice-key', 'venice-api-key', 'venice-ai-key'] },\n // ── Sublote E1 (2026-04-24) — runtime-wiring closure for D1 matrix ─────\n // Sublote D1 promoted 10 providers to CONSOLIDATION_MATRIX['live-validation']\n // based on direct-probe evidence (via gcloud secrets versions access,\n // bypassing the runtime env loader). 7 of those had no GCP→ENV tuple here,\n // so the catalog-loader could never surface `process.env.<X>_API_KEY` at\n // boot — meaning live-validated providers were silently \"not runnable\".\n // Wired pre-emptively: the moment an operator provisions the GCP secret\n // AND the container restarts, propagation happens with zero code change.\n // The 3 already-mapped providers (heliconeai, github-models, infermatic)\n // are NOT duplicated — they appear above under their respective batches.\n { envVar: 'GROQ_API_KEY', secretKeys: ['groq-api-key', 'groq-key'] },\n { envVar: 'DEEPINFRA_API_KEY', secretKeys: ['deepinfra-api-key', 'deepinfra-key'] },\n // Hugging Face: upstream canonical env var is `HF_TOKEN` (not\n // HUGGINGFACE_API_KEY); the catalog row's apiKeyEnvVar matches. Primary\n // GCP secret name follows the upstream convention; legacy huggingface-*\n // aliases accepted for operators who stored it under the provider slug.\n {\n envVar: 'HF_TOKEN',\n secretKeys: ['huggingface-api-key', 'huggingface-token', 'hf-token', 'huggingface-key'],\n },\n // Cloudflare Workers AI — requires BOTH api-token AND account-id. The\n // CloudflareWorkersAIAdapter factory (default-adapter-factories.ts line\n // 229-246) reads CLOUDFLARE_ACCOUNT_ID from env and substitutes it into\n // the `{account_id}` URL template. Without the account-id side-car the\n // adapter ships the literal template to the wire → 404 on every call.\n // Side-car is wired here as a separate tuple because it's not an auth\n // credential — it's a URL parameter — so it doesn't belong in\n // ENV_VAR_TO_PROVIDER or LLM_PROVIDER_ENV_VARS below.\n {\n envVar: 'CLOUDFLARE_API_TOKEN',\n secretKeys: [\n 'cloudflare-workers-ai-api-key',\n 'cloudflare-workers-ai-token',\n 'cloudflare-api-token',\n 'cloudflare-workers-ai-key',\n ],\n },\n {\n envVar: 'CLOUDFLARE_ACCOUNT_ID',\n secretKeys: [\n 'cloudflare-workers-ai-id',\n 'cloudflare-account-id',\n 'cloudflare-workers-ai-account-id',\n ],\n },\n { envVar: 'PERPLEXITY_API_KEY', secretKeys: ['perplexity-api-key', 'perplexity-key'] },\n // Fireworks AI — note the catalog providerId is `fireworks-ai` (hyphenated)\n // but upstream brand/docs spell it \"Fireworks\" (no suffix). Four fallbacks\n // cover both conventions + api-key suffix variant.\n {\n envVar: 'FIREWORKS_AI_API_KEY',\n secretKeys: ['fireworks-ai-api-key', 'fireworks-ai-key', 'fireworks-key', 'fireworks-api-key'],\n },\n { envVar: 'SAMBANOVA_API_KEY', secretKeys: ['sambanova-api-key', 'sambanova-key'] },\n // Replicate — flagged by the sublote-e1-runtime-wiring invariant as a\n // pre-existing gap. `replicate` has been in CONSOLIDATION_MATRIX\n // ['live-validation'] since before D1, with adapterClass 'ReplicateAdapter'\n // and apiKeyEnvVar 'REPLICATE_API_KEY', but no tuple was ever added here.\n // Even though integrationMode is 'execution-only' (no /v1/models\n // discovery), the adapter still requires the env var at /predictions\n // call time. Wired here for closure; the GCP secret name follows the\n // provider-slug convention with optional `-api-key` fallback.\n { envVar: 'REPLICATE_API_KEY', secretKeys: ['replicate-key', 'replicate-api-key'] },\n // ── LOTE N (2026-04-27) — catalog↔loader closure for the remaining 22 ─\n // apiKeyEnvVar entries that had catalog rows but NO PROVIDER_SECRETS\n // tuple. Surfaced by `catalog-loader-wiring-completeness.test.ts` (J6\n // invariant). All 22 are real provider rows with adapter classes (or\n // generic OAI-compat hub registration); they were classified in\n // CONSOLIDATION_MATRIX as `credentials-missing` or `disabled-by-default`,\n // and their absence here meant that even after an operator provisioned\n // the GCP secret, `process.env.<X>_API_KEY` would stay empty at boot.\n //\n // Pattern: preferred secret name first (provider-slug `-key`), then\n // upstream-convention `-api-key` fallback, then any historical aliases.\n // The provider-side `aliases` array in providers.catalog.ts is NOT\n // duplicated here — secret-key aliases are about HOW the operator\n // stored the credential, NOT how callers reach the provider.\n //\n // Image/video-only providers (bfl, recraft, runwayml, topaz) are wired\n // here too but are intentionally absent from LLM_PROVIDER_ENV_VARS below\n // because their presence does NOT count toward the \"at least one LLM\n // key present\" boot-mode gate.\n {\n envVar: 'TOGETHERAI_API_KEY',\n secretKeys: ['togetherai-api-key', 'togetherai-key', 'together-key', 'together-api-key'],\n },\n { envVar: 'ANYSCALE_API_KEY', secretKeys: ['anyscale-api-key', 'anyscale-key'] },\n { envVar: 'HYPERBOLIC_API_KEY', secretKeys: ['hyperbolic-api-key', 'hyperbolic-key'] },\n { envVar: 'CHUTES_API_KEY', secretKeys: ['chutes-api-key', 'chutes-key', 'chutes-ai-key'] },\n { envVar: 'NSCALE_API_KEY', secretKeys: ['nscale-key', 'nscale-api-key'] },\n { envVar: 'NEBIUS_API_KEY', secretKeys: ['nebius-key', 'nebius-api-key'] },\n // Lambda Cloud canonical provider slug is `lambda-ai`; brand is\n // \"Lambda\" (without -ai). Both spellings tried as fallbacks.\n {\n envVar: 'LAMBDA_AI_API_KEY',\n secretKeys: ['lambda-ai-key', 'lambda-key', 'lambda-cloud-key', 'lambda-ai-api-key'],\n },\n { envVar: 'SCALEWAY_API_KEY', secretKeys: ['scaleway-key', 'scaleway-api-key'] },\n {\n envVar: 'SYNTHETIC_API_KEY',\n secretKeys: ['synthetic-key', 'synthetic-api-key', 'synthetic-ai-key'],\n },\n { envVar: 'MORPH_API_KEY', secretKeys: ['morph-key', 'morph-api-key', 'morph-llm-key'] },\n // Z.ai (catalog providerId = `zai`). Upstream domain z.ai resolves to\n // bigmodel.cn — the GLM family operator. Legacy spelling z-ai accepted.\n { envVar: 'ZAI_API_KEY', secretKeys: ['zai-key', 'zai-api-key', 'z-ai-key', 'glm-key'] },\n {\n envVar: 'XIAOMI_MIMO_API_KEY',\n secretKeys: ['xiaomi-mimo-key', 'mimo-key', 'xiaomi-mimo-api-key'],\n },\n // v0 (Vercel) — code-gen LLM. The catalog providerId is `v0` (one\n // character) so the env var name is `V0_API_KEY` not `V0AI_API_KEY`.\n { envVar: 'V0_API_KEY', secretKeys: ['v0-key', 'v0-api-key', 'vercel-v0-key'] },\n // Vercel AI Gateway — DIFFERENT from v0 above (gateway over many LLMs).\n // The canonical secret name follows the catalog providerId.\n {\n envVar: 'VERCEL_AI_GATEWAY_API_KEY',\n secretKeys: ['vercel-ai-gateway-key', 'vercel-ai-gateway-api-key', 'vercel-gateway-key'],\n },\n // Volcano (Volcengine — ByteDance). Canonical slug `volcano`; brand\n // includes Volcengine + Doubao + Ark. Multiple aliases accepted.\n {\n envVar: 'VOLCANO_API_KEY',\n secretKeys: ['volcano-key', 'volcano-api-key', 'volcengine-key', 'doubao-key', 'ark-key'],\n },\n // IBM watsonx — note the env var is `WATSONX_APIKEY` (no underscore\n // between API and KEY) to match IBM's canonical SDK convention.\n // Side-car env vars (WATSONX_PROJECT_ID, WATSONX_URL) are wired below\n // because they're documented in the catalog row's `extraEnvVars` and\n // adapter requires them at runtime.\n {\n envVar: 'WATSONX_APIKEY',\n secretKeys: ['watsonx-apikey', 'watsonx-key', 'watsonx-api-key', 'ibm-watsonx-key'],\n },\n { envVar: 'WATSONX_PROJECT_ID', secretKeys: ['watsonx-project-id', 'ibm-watsonx-project-id'] },\n { envVar: 'WATSONX_URL', secretKeys: ['watsonx-url', 'ibm-watsonx-url'] },\n // Snowflake Cortex — uses Personal Access Token (PAT), not an API key.\n // Multiple side-cars required by the SnowflakeCortexAdapter:\n // SNOWFLAKE_ACCOUNT (orgname-accountname), SNOWFLAKE_USER (key-pair\n // auth user), SNOWFLAKE_BASE_URL (account-specific host). Side-cars\n // wired here so a single GCP provisioning round materializes the full\n // tuple at boot.\n { envVar: 'SNOWFLAKE_PAT', secretKeys: ['snowflake-pat', 'snowflake-token', 'snowflake-key'] },\n { envVar: 'SNOWFLAKE_ACCOUNT', secretKeys: ['snowflake-account'] },\n { envVar: 'SNOWFLAKE_USER', secretKeys: ['snowflake-user', 'snowflake-username'] },\n { envVar: 'SNOWFLAKE_BASE_URL', secretKeys: ['snowflake-base-url', 'snowflake-url'] },\n // SAP AI Core — OAuth2 client_credentials flow. Catalog providerId is\n // `sap`. The apiKeyEnvVar is `SAP_AI_CORE_CLIENT_ID` (the OAuth client\n // id); the matching client_secret + token URL + base URL are all\n // documented in the catalog row's `extraEnvVars` and required by the\n // SapAiCoreAdapter. Wiring all four together so a single provisioning\n // round closes the loop.\n { envVar: 'SAP_AI_CORE_CLIENT_ID', secretKeys: ['sap-ai-core-client-id', 'sap-client-id'] },\n {\n envVar: 'SAP_AI_CORE_CLIENT_SECRET',\n secretKeys: ['sap-ai-core-client-secret', 'sap-client-secret'],\n },\n { envVar: 'SAP_AI_CORE_AUTH_URL', secretKeys: ['sap-ai-core-auth-url', 'sap-auth-url'] },\n { envVar: 'SAP_AI_CORE_BASE_URL', secretKeys: ['sap-ai-core-base-url', 'sap-base-url'] },\n // W&B Inference side-car — WANDB_API_KEY (auth) is already wired in\n // Batch 7 above; WANDB_PROJECT is the project slug header required on\n // every chat call. Wiring here closes the side-car gap.\n { envVar: 'WANDB_PROJECT', secretKeys: ['wandb-project', 'wandb-project-slug'] },\n // Image-only providers (no LLM gate participation, see comment below).\n // bfl auth header is `x-key` (not Bearer) — see catalog `authHeaderName`.\n {\n envVar: 'BFL_API_KEY',\n secretKeys: ['bfl-key', 'bfl-api-key', 'flux-key', 'black-forest-labs-key'],\n },\n { envVar: 'RECRAFT_API_KEY', secretKeys: ['recraft-key', 'recraft-api-key'] },\n { envVar: 'RUNWAYML_API_KEY', secretKeys: ['runwayml-key', 'runwayml-api-key', 'runway-key'] },\n { envVar: 'TOPAZ_API_KEY', secretKeys: ['topaz-key', 'topaz-api-key', 'topaz-labs-key'] },\n // ── LOTE O (2026-07-10) — Apertis + Inception Labs onboarding ─────────\n // Operator pre-provisioned both GCP secrets ahead of the catalog rows\n // (ailin-apertis-key, ailin-inception-key). secretKeys match exactly —\n // the GCP provider auto-prepends the `ailin-` prefix (see file header).\n { envVar: 'APERTIS_API_KEY', secretKeys: ['apertis-key', 'apertis-api-key'] },\n { envVar: 'INCEPTION_API_KEY', secretKeys: ['inception-key', 'inception-api-key'] },\n // ── LOTE P (2026-07-11) — EmpirioLabs AI, live-validated after gate ────\n { envVar: 'EMPIRIOLABS_API_KEY', secretKeys: ['empiriolabs-key', 'empiriolabs-api-key'] },\n // ── LOTE Q (2026-07-12) — Concentrate AI ───────────────────────────────\n { envVar: 'CONCENTRATE_API_KEY', secretKeys: ['concentrate-key', 'concentrate-api-key'] },\n // ── LOTE R (2026-07-13) — FastRouter ────────────────────────────────────\n { envVar: 'FASTROUTER_API_KEY', secretKeys: ['fastrouter-key', 'fastrouter-api-key'] },\n // ── LOTE S (2026-07-13) — Perplexity Agent API ──────────────────────────\n // Same physical credential as PERPLEXITY_API_KEY above (one Perplexity\n // account, two API surfaces) — deliberately sources from the SAME GCP\n // secret names, not a new one.\n { envVar: 'PERPLEXITY_AGENT_API_KEY', secretKeys: ['perplexity-api-key', 'perplexity-key'] },\n // ── LOTE T (2026-07-13) — Ailin gateway (self-referential meta-provider) ──\n // No GCP secret provisioned yet — inert until an operator sets AILIN_API_KEY\n // for a self-hosted deployment. secretKeys follow the same naming\n // convention as the rest of this file in case one is provisioned later.\n { envVar: 'AILIN_API_KEY', secretKeys: ['ailin-key', 'ailin-api-key', 'gateway-key'] },\n // ── LOTE U (2026-07-29) — Sakana AI (Fugu), chat-capable ────────────────\n // 'sakana-ai-key' listed first: it's the actual GCP secret name\n // (`ailin-sakana-ai-key`, prefix auto-added) confirmed live this lot.\n { envVar: 'SAKANA_AI_API_KEY', secretKeys: ['sakana-ai-key', 'sakana-key'] },\n] as const;\n\n// ─── Provider Key Status Tracking ──────────────────────────────────────────\n// Tracks which provider API keys were successfully loaded, from which source,\n// and when. Used by the Self-Healing Discovery (L1) to know when to retry\n// discovery for native providers whose keys arrived late (GCP delay/failure).\n\nexport interface ProviderKeyStatus {\n loaded: boolean;\n source: 'gcp' | 'env' | 'none';\n timestamp: Date;\n envVar: string;\n}\n\n/**\n * Module-level state: populated during loadSecretsIntoEnv() for each provider.\n * Key = normalized provider name (lowercase, e.g., 'openai', 'anthropic').\n */\nconst providerKeyStatusMap = new Map<string, ProviderKeyStatus>();\n\n/**\n * Mapping from env var name to normalized provider name.\n * Allows the discovery service to map env vars to provider sources.\n */\nconst ENV_VAR_TO_PROVIDER: Record<string, string> = {\n // Native API providers\n OPENAI_API_KEY: 'openai',\n ANTHROPIC_API_KEY: 'anthropic',\n GOOGLE_API_KEY: 'google',\n DEEPSEEK_API_KEY: 'deepseek',\n XAI_API_KEY: 'xai',\n MISTRAL_API_KEY: 'mistral',\n COHERE_API_KEY: 'cohere',\n JINA_API_KEY: 'jina',\n // Cloud hubs — 2026-04-23: use canonical providerId 'nvidia' (legacy\n // alias 'nvidia-hub' was absorbed into the nvidia catalog row per Lot B;\n // discovery/status consumers read providerKeyStatus by canonical id).\n NVIDIA_API_KEY: 'nvidia',\n AIHUBMIX_API_KEY: 'aihubmix',\n NOVITA_API_KEY: 'novita',\n MOONSHOT_API_KEY: 'moonshot',\n MINIMAX_API_KEY: 'minimax',\n FRIENDLI_API_KEY: 'friendli',\n AIML_API_KEY: 'aiml',\n IMAGEROUTER_API_KEY: 'imagerouter',\n COMETAPI_API_KEY: 'cometapi',\n NANOGPT_API_KEY: 'nanogpt',\n EDENAI_API_KEY: 'edenai',\n HELICONEAI_API_KEY: 'heliconeai',\n POE_API_KEY: 'poe',\n ROUTEWAY_API_KEY: 'routeway',\n REQUESTY_API_KEY: 'requesty',\n // Post-migration canonical providerId is `ai302` (see providers.catalog.ts\n // and the residue-closure note in provider-registry.ts). Legacy `302ai`\n // survives as a catalog alias for pre-migration user configs.\n AI302_API_KEY: 'ai302',\n ORQAI_API_KEY: 'orqai',\n // Routers/aggregators\n OPENROUTER_API_KEY: 'openrouter',\n // Cloud providers\n VERTEX_AI_API_KEY: 'vertex-ai',\n AWS_ACCESS_KEY_ID: 'aws-bedrock',\n AWS_BEARER_TOKEN_BEDROCK: 'aws-bedrock',\n // SageMaker uses the same shared AWS creds as Bedrock, so `AWS_ACCESS_KEY_ID`\n // keeps mapping to `aws-bedrock` (first-listed wins) — but the endpoint-\n // specific env var is only meaningful for SageMaker. When operators wire\n // this, Self-Healing Discovery will attribute the key presence to the\n // SageMaker provider specifically.\n AWS_SAGEMAKER_ENDPOINT_NAME: 'aws-sagemaker',\n AZURE_OPENAI_API_KEY: 'azure-openai',\n // Regional/specialized\n QWEN_API_KEY: 'qwen',\n ERNIE_API_KEY: 'ernie',\n // Audio providers\n DEEPGRAM_API_KEY: 'deepgram',\n CARTESIA_API_KEY: 'cartesia',\n ELEVENLABS_API_KEY: 'elevenlabs',\n // Inference + embedding providers newly onboarded 2026-04-22\n WANDB_API_KEY: 'wandb',\n VOYAGE_API_KEY: 'voyage',\n BYTEZ_API_KEY: 'bytez',\n CEREBRAS_API_KEY: 'cerebras',\n FEATHERLESS_AI_API_KEY: 'featherless-ai', // canonical catalog id (2026-04-23 drift fix)\n // Batch 6 — enterprise hyperscaler gateways (2026-04-22). Gemini OAI-compat\n // is registered as a SEPARATE provider from the native `google` entry so\n // the self-healing discovery service can track them independently (they\n // have different failure modes — native uses @google/generative-ai SDK\n // with its own error surfaces; OAI-compat uses standard fetch).\n GEMINI_API_KEY: 'gemini-openai',\n GITHUB_TOKEN: 'github-models',\n DATABRICKS_TOKEN: 'databricks',\n // Orphan closure (2026-04-22) — Inworld now has a catalog row + adapter\n // factory. Mapping the env var to the provider slug lets Self-Healing\n // Discovery attribute key presence to the correct provider.\n INWORLD_API_KEY: 'inworld',\n // Orphan-closure Lot B (2026-04-23). Slug = catalog providerId. Writer\n // and Upstage are self-evident; Reka's slug is `rekaai` (the catalog\n // row's canonical providerId — aliases 'reka' + 'reka-ai' resolve to it).\n WRITER_API_KEY: 'writer',\n UPSTAGE_API_KEY: 'upstage',\n REKA_API_KEY: 'rekaai',\n // ── LOTE M (2026-04-23) — complement lot, 13 providers ──────────────\n // Slug = catalog providerId (NOT the upstream brand for phala/qianfan\n // which intentionally diverge — see catalog rows for rationale):\n // phala — canonical slug; upstream brand is \"RedPill\"\n // qianfan — canonical slug; upstream brand is \"Baidu Qianfan / ERNIE\"\n // gmi — canonical slug; upstream brand is \"GMICloud\"\n ARCEE_API_KEY: 'arcee',\n ATLASCLOUD_API_KEY: 'atlascloud',\n AVIAN_API_KEY: 'avian',\n QIANFAN_API_KEY: 'qianfan',\n GMI_API_KEY: 'gmi',\n INFERMATIC_API_KEY: 'infermatic',\n INFLECTION_API_KEY: 'inflection',\n MANCER_API_KEY: 'mancer',\n PHALA_API_KEY: 'phala',\n RELACE_API_KEY: 'relace',\n SILICONFLOW_API_KEY: 'siliconflow',\n STEPFUN_API_KEY: 'stepfun',\n VENICE_API_KEY: 'venice',\n // ── Sublote E1 (2026-04-24) — runtime-wiring closure for D1 matrix ───\n // 7 provider slugs whose env vars gained GCP→ENV tuples in this lot.\n // Slug = catalog providerId exactly (so Self-Healing Discovery can\n // attribute key-loaded events to the right provider). CLOUDFLARE_ACCOUNT_ID\n // is intentionally absent — that's a URL parameter, not an auth credential,\n // so mapping it to a provider slug would be semantically wrong (the same\n // account-id could conceivably scope multiple Cloudflare providers).\n GROQ_API_KEY: 'groq',\n DEEPINFRA_API_KEY: 'deepinfra',\n HF_TOKEN: 'huggingface',\n CLOUDFLARE_API_TOKEN: 'cloudflare-workers-ai',\n PERPLEXITY_API_KEY: 'perplexity',\n FIREWORKS_AI_API_KEY: 'fireworks-ai',\n SAMBANOVA_API_KEY: 'sambanova',\n // Replicate — pre-existing live-validation gap closed by sublote E1.\n // See the matching comment on the PROVIDER_SECRETS tuple above.\n REPLICATE_API_KEY: 'replicate',\n // ── LOTE N (2026-04-27) — catalog↔loader closure (22 new env vars) ──\n // Every entry below maps an env var to its canonical providerId so the\n // L1 Self-Healing Discovery Service can attribute key-loaded events\n // when the corresponding GCP secret is provisioned. Slugs match the\n // catalog providerId exactly (NOT upstream brand) — see catalog rows\n // for cases where they diverge (e.g. `sap` not `sap-ai-core`, `v0` not\n // `vercel-v0`, `volcano` not `volcengine`). Side-car env vars\n // (WATSONX_PROJECT_ID, SNOWFLAKE_ACCOUNT, SAP_AI_CORE_CLIENT_SECRET,\n // etc.) are NOT mapped here — they're auth or URL parameters, not\n // credentials, so attributing them to a provider would conflate\n // credential-presence with side-car-completeness in discovery telemetry.\n TOGETHERAI_API_KEY: 'togetherai',\n ANYSCALE_API_KEY: 'anyscale',\n HYPERBOLIC_API_KEY: 'hyperbolic',\n CHUTES_API_KEY: 'chutes',\n NSCALE_API_KEY: 'nscale',\n NEBIUS_API_KEY: 'nebius',\n LAMBDA_AI_API_KEY: 'lambda-ai',\n SCALEWAY_API_KEY: 'scaleway',\n SYNTHETIC_API_KEY: 'synthetic',\n MORPH_API_KEY: 'morph',\n ZAI_API_KEY: 'zai',\n XIAOMI_MIMO_API_KEY: 'xiaomi-mimo',\n V0_API_KEY: 'v0',\n VERCEL_AI_GATEWAY_API_KEY: 'vercel-ai-gateway',\n VOLCANO_API_KEY: 'volcano',\n WATSONX_APIKEY: 'watsonx',\n SNOWFLAKE_PAT: 'snowflake',\n SAP_AI_CORE_CLIENT_ID: 'sap',\n // Image / video-only providers. Still mapped to a slug so Self-Healing\n // Discovery can record key-presence events — but their env vars do NOT\n // appear in LLM_PROVIDER_ENV_VARS (image surfaces don't satisfy the\n // \"at least one LLM key\" boot gate).\n BFL_API_KEY: 'bfl',\n RECRAFT_API_KEY: 'recraft',\n RUNWAYML_API_KEY: 'runwayml',\n TOPAZ_API_KEY: 'topaz',\n // ── LOTE O (2026-07-10) ────────────────────────────────────────────────\n APERTIS_API_KEY: 'apertis',\n INCEPTION_API_KEY: 'inception',\n // ── LOTE P (2026-07-11) ────────────────────────────────────────────────\n EMPIRIOLABS_API_KEY: 'empiriolabs',\n // ── LOTE Q (2026-07-12) ────────────────────────────────────────────────\n CONCENTRATE_API_KEY: 'concentrate',\n // ── LOTE R (2026-07-13) ────────────────────────────────────────────────\n FASTROUTER_API_KEY: 'fastrouter',\n // ── LOTE S (2026-07-13) ────────────────────────────────────────────────\n PERPLEXITY_AGENT_API_KEY: 'perplexity-agent',\n // ── LOTE T (2026-07-13) ────────────────────────────────────────────────\n AILIN_API_KEY: 'ailin',\n // ── LOTE U (2026-07-29) ────────────────────────────────────────────────\n SAKANA_AI_API_KEY: 'sakana-ai',\n};\n\n/**\n * Returns the status of all provider API keys.\n * Used by Self-Healing Discovery to determine which native sources can run.\n */\nexport function getProviderKeyStatus(): ReadonlyMap<string, ProviderKeyStatus> {\n return providerKeyStatusMap;\n}\n\n/**\n * Returns provider names that have valid (non-empty, non-mock) API keys loaded.\n */\nexport function getLoadedProviderNames(): string[] {\n return Array.from(providerKeyStatusMap.entries())\n .filter(([, status]) => status.loaded)\n .map(([name]) => name);\n}\n\n/**\n * Check if a specific provider has a loaded API key.\n */\nexport function isProviderKeyLoaded(providerName: string): boolean {\n return providerKeyStatusMap.get(providerName.toLowerCase())?.loaded ?? false;\n}\n\nconst LLM_PROVIDER_ENV_VARS = [\n 'OPENAI_API_KEY',\n 'ANTHROPIC_API_KEY',\n 'GOOGLE_API_KEY',\n 'DEEPSEEK_API_KEY',\n 'XAI_API_KEY',\n 'MISTRAL_API_KEY',\n 'COHERE_API_KEY',\n 'NVIDIA_API_KEY',\n 'AIHUBMIX_API_KEY',\n 'NOVITA_API_KEY',\n 'MOONSHOT_API_KEY',\n 'MINIMAX_API_KEY',\n 'JINA_API_KEY',\n 'FRIENDLI_API_KEY',\n 'AIML_API_KEY',\n 'IMAGEROUTER_API_KEY',\n 'OPENROUTER_API_KEY',\n 'ORQAI_API_KEY',\n 'EDENAI_API_KEY',\n 'HELICONEAI_API_KEY',\n 'VERTEX_AI_API_KEY',\n 'AWS_BEARER_TOKEN_BEDROCK',\n 'AZURE_OPENAI_API_KEY',\n 'QWEN_API_KEY',\n 'ERNIE_API_KEY',\n 'COMETAPI_API_KEY',\n 'NANOGPT_API_KEY',\n 'REQUESTY_API_KEY',\n 'AI302_API_KEY',\n 'POE_API_KEY',\n 'ROUTEWAY_API_KEY',\n // Added 2026-04-22 — these keys now count toward the \"at least one LLM key\n // present\" gate that avoids DEGRADED_SELF_HOSTED mode at boot.\n 'WANDB_API_KEY',\n 'VOYAGE_API_KEY',\n 'CEREBRAS_API_KEY',\n 'FEATHERLESS_AI_API_KEY',\n // Batch 4 landed BYTEZ catalog row + adapter, so BYTEZ_API_KEY now counts\n // toward the LLM-key-present gate.\n 'BYTEZ_API_KEY',\n // Batch 6 — enterprise hyperscaler gateways (2026-04-22). These keys now\n // count toward the \"at least one LLM key present\" gate; all four have\n // landed catalog rows + adapter classes in this batch.\n 'GEMINI_API_KEY',\n 'GITHUB_TOKEN',\n 'DATABRICKS_TOKEN',\n // Batch 8.1 — AWS SageMaker. Endpoint name is the meaningful trigger:\n // AWS creds alone are ambiguous (could be Bedrock or S3), but an\n // endpoint name implies \"SageMaker is provisioned here\".\n 'AWS_SAGEMAKER_ENDPOINT_NAME',\n // Orphan closure (2026-04-22) — Inworld AI now counts toward the\n // \"at least one LLM key present\" gate now that the catalog row +\n // adapter factory are wired.\n 'INWORLD_API_KEY',\n // Orphan-closure Lot B (2026-04-23). All three are fully canonical now.\n 'WRITER_API_KEY',\n 'UPSTAGE_API_KEY',\n 'REKA_API_KEY',\n // ── LOTE M (2026-04-23) complement lot ──────────────────────────────\n // Each of the 13 keys below counts toward the \"at least one LLM key\n // present\" gate, the same way all catalog-wired keys do. Note that\n // mancer and venice carry contentPolicyClass=uncensored (2026-04-28\n // Phase 4b — the prior denyByDefault gate was removed per the universal\n // \"habilitado e nunca censurado\" policy); their keys count toward the\n // gate exactly like any other LLM key — surface-level filtering is now\n // a downstream concern, not a routing one.\n // qianfan here is QIANFAN_API_KEY (v2 bce-v3 bearer); the legacy v1\n // ERNIE_API_KEY was already listed above this marker.\n 'ARCEE_API_KEY',\n 'ATLASCLOUD_API_KEY',\n 'AVIAN_API_KEY',\n 'QIANFAN_API_KEY',\n 'GMI_API_KEY',\n 'INFERMATIC_API_KEY',\n 'INFLECTION_API_KEY',\n 'MANCER_API_KEY',\n 'PHALA_API_KEY',\n 'RELACE_API_KEY',\n 'SILICONFLOW_API_KEY',\n 'STEPFUN_API_KEY',\n 'VENICE_API_KEY',\n // ── Sublote E1 (2026-04-24) — runtime-wiring closure for D1 matrix ───\n // These 7 providers landed in CONSOLIDATION_MATRIX['live-validation']\n // during Sublote D1 but had no GCP→ENV tuple above → their keys could\n // not count toward the \"at least one LLM key present\" gate even when\n // GCP Secret Manager held the key. Listed here so a restart after the\n // operator's provisioning cycle propagates the key-present signal to\n // the DEGRADED_SELF_HOSTED avoidance logic. CLOUDFLARE_ACCOUNT_ID is\n // intentionally absent — it's a URL parameter, not an LLM credential.\n 'GROQ_API_KEY',\n 'DEEPINFRA_API_KEY',\n 'HF_TOKEN',\n 'CLOUDFLARE_API_TOKEN',\n 'PERPLEXITY_API_KEY',\n 'FIREWORKS_AI_API_KEY',\n 'SAMBANOVA_API_KEY',\n // Replicate — pre-existing live-validation gap closed by sublote E1.\n // See the matching comments on the PROVIDER_SECRETS tuple and\n // ENV_VAR_TO_PROVIDER entry above.\n 'REPLICATE_API_KEY',\n // ── LOTE N (2026-04-27) — catalog↔loader closure ─────────────────────\n // 18 LLM-class providers added to the boot-mode gate. The 4 image/\n // video-only providers (bfl, recraft, runwayml, topaz) are\n // intentionally OMITTED — their key presence does not signal the\n // server has any text-generation capability, so they should not\n // satisfy the \"at least one LLM key present\" check.\n 'TOGETHERAI_API_KEY',\n 'ANYSCALE_API_KEY',\n 'HYPERBOLIC_API_KEY',\n 'CHUTES_API_KEY',\n 'NSCALE_API_KEY',\n 'NEBIUS_API_KEY',\n 'LAMBDA_AI_API_KEY',\n 'SCALEWAY_API_KEY',\n 'SYNTHETIC_API_KEY',\n 'MORPH_API_KEY',\n 'ZAI_API_KEY',\n 'XIAOMI_MIMO_API_KEY',\n 'V0_API_KEY',\n 'VERCEL_AI_GATEWAY_API_KEY',\n 'VOLCANO_API_KEY',\n 'WATSONX_APIKEY',\n 'SNOWFLAKE_PAT',\n 'SAP_AI_CORE_CLIENT_ID',\n // ── LOTE O (2026-07-10) — Apertis + Inception Labs, both chat-capable ──\n 'APERTIS_API_KEY',\n 'INCEPTION_API_KEY',\n // ── LOTE P (2026-07-11) — EmpirioLabs AI, chat-capable ─────────────────\n 'EMPIRIOLABS_API_KEY',\n // ── LOTE Q (2026-07-12) — Concentrate AI, chat-capable ─────────────────\n 'CONCENTRATE_API_KEY',\n // ── LOTE R (2026-07-13) — FastRouter, chat-capable ─────────────────────\n 'FASTROUTER_API_KEY',\n // ── LOTE S (2026-07-13) — Perplexity Agent API, chat-capable ───────────\n 'PERPLEXITY_AGENT_API_KEY',\n // ── LOTE T (2026-07-13) — Ailin gateway, chat-capable ──────────────────\n 'AILIN_API_KEY',\n // ── LOTE U (2026-07-29) — Sakana AI (Fugu), chat-capable ────────────────\n 'SAKANA_AI_API_KEY',\n] as const;\n\n/**\n * Self-hosted / sidecar endpoints that can keep the server alive in a\n * DEGRADED mode when every external LLM API key is missing.\n *\n * Design rationale:\n * Without this fallback, a single failure (expired GCP ADC, rotated\n * service account, network blip during boot) produces an infinite\n * restart loop. With it, the server boots in `degraded_self_hosted`\n * mode: only local/sidecar models are usable, requests are tagged\n * via last-resort-policy.ts (execution_mode=last_resort_self_hosted,\n * excluded_from_benchmark=true), and ops gets a clear warning\n * instead of a crash loop.\n *\n * This ties directly into Bloco P1.10 (self-hosted last-resort policy)\n * but at boot-time scope rather than per-request scope.\n */\nconst SELF_HOSTED_ENV_VARS = [\n 'OLLAMA_URL',\n 'SELF_HOSTED_LLM_URL',\n 'LOCAL_LLAMA_URL',\n 'LOCAL_KOBOLD_URL',\n] as const;\n\n/**\n * Load critical secrets from GCP into process.env\n *\n * This function is called AFTER SecretsManager initialization\n * and BEFORE config validation.\n *\n * It loads secrets from GCP and injects them into process.env,\n * allowing the existing config system to work without modification.\n */\nexport async function loadSecretsIntoEnv(): Promise<void> {\n const log = logger.child({ component: 'secrets-loader' });\n\n log.info('Loading critical secrets from GCP Secret Manager into environment...');\n\n let loaded = 0;\n let skipped = 0;\n let failed = 0;\n let sanitizedKeys = 0;\n let providerSecretsLoaded = 0;\n // Tracks env values that were authoritatively replaced by GCP fetches.\n // Useful diagnostic for operators verifying that GCP is the source of truth.\n let overwrittenFromGcp = 0;\n\n const forceLoadRealKeys = process.env.TEST_USE_REAL_API_KEYS === 'true';\n\n // ── GCP-authoritative mode ─────────────────────────────────────────────\n // When SECRETS_PROVIDER_PRIMARY=gcp the operator has declared GCP as the\n // source of truth. The legacy \"skip if env already populated\" path then\n // contradicts that declaration: env_file / docker-compose / shell exports\n // silently win and GCP fetches are never even attempted, which is exactly\n // the failure mode that hid populated GCP secrets (e.g. HF_TOKEN) from\n // /v1/models. In authoritative mode we ALWAYS attempt the GCP fetch and\n // overwrite the pre-existing env value when GCP returns one. The fallback\n // is symmetric: a GCP miss preserves the existing env value (no zeroing).\n //\n // Operators can override:\n // SECRETS_GCP_AUTHORITATIVE=false → keep legacy skip-if-exists semantics\n // SECRETS_GCP_AUTHORITATIVE=true → force authoritative even with env primary\n const gcpPrimaryEnv = (process.env.SECRETS_PROVIDER_PRIMARY || '').trim().toLowerCase() === 'gcp';\n const explicitAuthoritative = (process.env.SECRETS_GCP_AUTHORITATIVE || '').trim().toLowerCase();\n const gcpAuthoritative =\n explicitAuthoritative === 'true' || (explicitAuthoritative !== 'false' && gcpPrimaryEnv);\n\n // Tests already use TEST_USE_REAL_API_KEYS=true to force GCP fetches even\n // when local mocks are present. The two flags are independent triggers\n // for the same effect; either one disables skip-if-exists.\n const authoritativeFetch = gcpAuthoritative || forceLoadRealKeys;\n\n log.info(\n {\n gcpPrimary: gcpPrimaryEnv,\n gcpAuthoritative,\n forceLoadRealKeys,\n authoritativeFetch,\n },\n 'Secret loading mode resolved'\n );\n\n // Load critical secrets (required)\n for (const { envVar, secretKey, required, defaultValue } of CRITICAL_SECRETS) {\n const existingValue = process.env[envVar];\n const shouldOverrideMockStripe =\n forceLoadRealKeys &&\n envVar.startsWith('STRIPE_') &&\n typeof existingValue === 'string' &&\n (existingValue.includes('mock') || existingValue.includes('test'));\n\n // Skip if already in env, UNLESS:\n // - we're in GCP-authoritative mode (operator declared GCP as source of truth), OR\n // - we're forcing real keys and detected mock Stripe keys.\n if (existingValue && !shouldOverrideMockStripe && !gcpAuthoritative) {\n log.debug({ envVar, source: 'env' }, 'Secret already in environment, skipping GCP load');\n skipped++;\n continue;\n }\n\n try {\n const value = await loadSecret(secretKey, required);\n if (value) {\n const wasOverwrite = Boolean(existingValue) && existingValue !== value;\n process.env[envVar] = value;\n if (wasOverwrite) overwrittenFromGcp++;\n log.debug(\n { envVar, secretKey, replaced: Boolean(existingValue), overwrittenFromGcp: wasOverwrite },\n 'Secret loaded from GCP into environment'\n );\n loaded++;\n } else if (existingValue && gcpAuthoritative) {\n // Authoritative fetch missed in GCP — fall back to the existing env\n // value rather than zeroing it. This keeps the system bootable when\n // a single secret is absent from GCP without losing the env-provided\n // value.\n log.debug(\n { envVar, secretKey },\n 'GCP miss in authoritative mode; preserving existing env value'\n );\n skipped++;\n } else if (defaultValue !== undefined) {\n // Use default value if secret not found and default provided\n process.env[envVar] = defaultValue;\n log.debug({ envVar, secretKey, defaultValue }, 'Secret not found, using default value');\n skipped++;\n } else if (!required) {\n log.debug({ envVar, secretKey }, 'Optional secret not found, skipping');\n skipped++;\n }\n } catch (error) {\n if (defaultValue !== undefined) {\n // Use default value on error if provided\n process.env[envVar] = defaultValue;\n log.warn(\n { envVar, secretKey, defaultValue, error: getErrorMessage(error) },\n 'Failed to load secret, using default value'\n );\n skipped++;\n } else if (required) {\n const errorMsg = getErrorMessage(error);\n log.error({ envVar, secretKey, error: errorMsg }, 'Failed to load required secret');\n failed++;\n throw new Error(`Failed to load required secret ${secretKey}: ${errorMsg}`);\n } else {\n log.warn(\n { envVar, secretKey, error: getErrorMessage(error) },\n 'Failed to load optional secret'\n );\n skipped++;\n }\n }\n }\n\n // Keep compatibility with components that still read GCS_BUCKET_NAME.\n if (!process.env.GCS_BUCKET_NAME && process.env.GCS_FILES_BUCKET) {\n process.env.GCS_BUCKET_NAME = process.env.GCS_FILES_BUCKET;\n }\n\n // Load provider API keys (optional)\n // CRITICAL: If GCP auth is available, ALWAYS load real keys (API Keys NUNCA DEVEM FALHAR)\n // If TEST_USE_REAL_API_KEYS=true, force load from GCP even if mock keys exist\n // Always replace mock keys with real keys from GCP when available\n // (reuses forceLoadRealKeys defined above)\n\n for (const { envVar, secretKeys } of PROVIDER_SECRETS) {\n const existingValue = process.env[envVar];\n const providerName = ENV_VAR_TO_PROVIDER[envVar];\n\n // Check if existing value looks like a mock key\n const isMockKey =\n existingValue && (existingValue.includes('mock') || existingValue.includes('test-'));\n\n // Skip if real key already exists, UNLESS:\n // - we're in GCP-authoritative mode (operator declared GCP as source of truth), OR\n // - test forces real keys (TEST_USE_REAL_API_KEYS=true), OR\n // - the existing value looks like a mock.\n // The authoritative path lets GCP overwrite stale env_file / shell-exported\n // values, which is the entire point of SECRETS_PROVIDER_PRIMARY=gcp.\n if (existingValue && !authoritativeFetch && !isMockKey) {\n // Record as loaded from env\n if (providerName) {\n providerKeyStatusMap.set(providerName, {\n loaded: true,\n source: 'env',\n timestamp: new Date(),\n envVar,\n });\n }\n skipped++;\n continue;\n }\n\n try {\n let loadedSecretValue: string | undefined;\n let resolvedSecretKey = secretKeys[0];\n\n for (const candidateKey of secretKeys) {\n try {\n const candidateValue = await loadSecret(candidateKey, false);\n if (candidateValue) {\n loadedSecretValue = candidateValue;\n resolvedSecretKey = candidateKey;\n log.debug(\n { envVar, secretKey: candidateKey, valueLen: candidateValue.length },\n 'Secret loaded from GCP'\n );\n break;\n } else {\n log.debug({ envVar, secretKey: candidateKey }, 'Secret returned empty/null from GCP');\n }\n } catch (loadErr) {\n log.warn(\n {\n envVar,\n secretKey: candidateKey,\n error: loadErr instanceof Error ? loadErr.message : String(loadErr),\n },\n 'Failed to load secret from GCP'\n );\n continue;\n }\n }\n\n // GCP provider automatically adds prefix 'ailin-' to secretKey\n // So 'openai-key' becomes 'ailin-openai-key' in GCP Secret Manager\n const value = loadedSecretValue;\n if (value) {\n // Sanitize control characters and invisible Unicode.\n // PEM-format keys (-----BEGIN ...) legitimately contain newlines,\n // so use a lighter regex that preserves \\n and \\r for those values.\n const isPemKey = value.startsWith('-----BEGIN');\n let sanitizedValue = isPemKey\n ? value\n .replace(/[\\x00-\\x09\\x0B\\x0C\\x0E-\\x1F\\x7F]/g, '') // Strip control chars EXCEPT \\n (0x0A) and \\r (0x0D)\n .replace(/[\\u200B-\\u200D\\uFEFF]/g, '') // Remove zero-width spaces\n .trim()\n : value\n .replace(/[\\x00-\\x1F\\x7F]/g, '') // Strip ALL control characters\n .replace(/[\\u200B-\\u200D\\uFEFF]/g, '') // Remove zero-width spaces\n .trim();\n\n // Log detailed debugging information if sanitization changed the value\n if (sanitizedValue !== value) {\n const removedChars = value.length - sanitizedValue.length;\n sanitizedKeys++;\n log.debug(\n {\n envVar,\n secretKey: resolvedSecretKey,\n originalLength: value.length,\n sanitizedLength: sanitizedValue.length,\n removedChars,\n },\n 'API key contained invalid characters and was sanitized'\n );\n }\n\n // Always replace mock keys with real keys\n const wasReplaced = !!existingValue;\n const wasOverwriteOfRealValue =\n wasReplaced && !isMockKey && existingValue !== sanitizedValue;\n\n // CRITICAL: Set process.env and immediately validate\n // Use Object.defineProperty as fallback if direct assignment doesn't work\n // This ensures the value is actually set in the environment\n const _previousValue = process.env[envVar];\n process.env[envVar] = sanitizedValue;\n\n // Immediately read back to verify (handles any potential async issues)\n const readBackValue = process.env[envVar];\n\n // Deep validation: check exact match, type, and length\n const isValidMatch = readBackValue === sanitizedValue;\n const isValidType = typeof readBackValue === 'string';\n const isValidLength =\n readBackValue !== undefined && readBackValue.length === sanitizedValue.length;\n\n if (!isValidMatch || !isValidType || !isValidLength) {\n log.error(\n {\n envVar,\n secretKey: resolvedSecretKey,\n sanitizedValueLength: sanitizedValue.length,\n readBackValueLength: readBackValue?.length,\n },\n 'Secret was loaded but process.env assignment did not persist'\n );\n failed++;\n } else {\n // Success - log and increment counter\n if (wasOverwriteOfRealValue) overwrittenFromGcp++;\n log.debug(\n {\n envVar,\n secretKey: resolvedSecretKey,\n replaced: wasReplaced,\n wasMock: isMockKey,\n overwrittenFromGcp: wasOverwriteOfRealValue,\n },\n 'Provider secret loaded from GCP'\n );\n loaded++;\n providerSecretsLoaded++;\n // Record successful load\n if (providerName) {\n providerKeyStatusMap.set(providerName, {\n loaded: true,\n source: 'gcp',\n timestamp: new Date(),\n envVar,\n });\n }\n }\n } else {\n // GCP returned no value. Decide what's actually in process.env right\n // now: if env had a non-mock value before this loop iteration we\n // preserved it (the authoritative path entered this block but found\n // no GCP value to overwrite with), so we're effectively env-loaded.\n // If env had a mock value, we keep the mock as a degraded fallback\n // and warn loudly. If env had nothing, the provider stays orphaned.\n if (existingValue && !isMockKey) {\n // Authoritative-mode GCP miss with real env value already set:\n // the env value is still in process.env, so report source=env.\n if (providerName) {\n providerKeyStatusMap.set(providerName, {\n loaded: true,\n source: 'env',\n timestamp: new Date(),\n envVar,\n });\n }\n log.debug(\n { envVar, secretKeys },\n 'GCP miss in authoritative mode; preserving existing env value for provider'\n );\n } else {\n if (isMockKey) {\n log.warn(\n { envVar, secretKeys },\n 'Mock API key detected but no real key found in GCP - using mock key (this may cause API failures)'\n );\n }\n // Record as not loaded (no GCP value, no usable env value)\n if (providerName) {\n providerKeyStatusMap.set(providerName, {\n loaded: false,\n source: 'none',\n timestamp: new Date(),\n envVar,\n });\n }\n }\n skipped++;\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n // Only warn if not forcing real keys - if forcing, this is expected to work\n if (forceLoadRealKeys) {\n log.error(\n { envVar, secretKeys, error: errorMessage },\n 'Failed to load provider secret from GCP (TEST_USE_REAL_API_KEYS=true)'\n );\n failed++;\n } else {\n if (isMockKey) {\n log.warn(\n { envVar, secretKeys, error: errorMessage },\n 'Failed to load provider secret from GCP - will use mock key (this may cause API failures)'\n );\n } else {\n log.debug(\n { envVar, secretKeys, error: errorMessage },\n 'Failed to load provider secret from GCP (optional)'\n );\n }\n // Record failure\n if (providerName) {\n providerKeyStatusMap.set(providerName, {\n loaded: false,\n source: 'none',\n timestamp: new Date(),\n envVar,\n });\n }\n skipped++;\n }\n }\n }\n\n // ── Env-var aliases (2026-04-28) ────────────────────────────────────────\n //\n // Some operators set a non-canonical name in `.env` for historical reasons\n // (e.g. `REKAAI_API_KEY` instead of the catalog-canonical `REKA_API_KEY`).\n // When GCP is reachable the secret loader populates the canonical name and\n // it just works; when GCP is unreachable the alias would be the only thing\n // set and the catalog row reads the canonical → empty key → silent skip.\n //\n // This pass copies known aliases into their canonical env var if the\n // canonical is empty. It is purely additive — never overwrites an\n // existing canonical value, never logs a warning unless an alias was\n // applied. Add new entries here when a similar mismatch is discovered.\n const ENV_ALIASES: ReadonlyArray<{ canonical: string; alias: string }> = [\n { canonical: 'REKA_API_KEY', alias: 'REKAAI_API_KEY' },\n ];\n for (const { canonical, alias } of ENV_ALIASES) {\n const canonicalValue = process.env[canonical];\n const aliasValue = process.env[alias];\n if (\n (canonicalValue == null || canonicalValue.trim() === '') &&\n aliasValue &&\n aliasValue.trim() !== ''\n ) {\n process.env[canonical] = aliasValue;\n log.info({ canonical, alias }, 'Promoted alias env var to canonical name');\n }\n }\n\n const gcpPrimary = (process.env.SECRETS_PROVIDER_PRIMARY || '').trim().toLowerCase() === 'gcp';\n const failFastEnv = (process.env.SECRETS_GCP_FAIL_FAST || '').trim().toLowerCase();\n const gcpFailFast = failFastEnv === 'true' || (failFastEnv !== 'false' && gcpPrimary);\n const hasAnyLlmProviderKey = LLM_PROVIDER_ENV_VARS.some((envVar) => {\n const value = process.env[envVar];\n return typeof value === 'string' && value.trim().length > 0;\n });\n\n // Hardening (P1.10 at boot scope): even when zero external API keys are\n // loaded, the server can still boot in DEGRADED mode if a self-hosted\n // endpoint is reachable. Every request will then be tagged via the\n // last-resort-policy as execution_mode=last_resort_self_hosted.\n const availableSelfHostedEndpoints = SELF_HOSTED_ENV_VARS.filter((envVar) => {\n const value = process.env[envVar];\n return typeof value === 'string' && value.trim().length > 0;\n });\n const hasAnySelfHostedFallback = availableSelfHostedEndpoints.length > 0;\n\n // Opt-out for strict production deployments that must never run in\n // degraded mode. Default: allow degraded boot (safer than crash loops).\n const allowDegradedBoot =\n (process.env.ALLOW_DEGRADED_BOOT || 'true').trim().toLowerCase() !== 'false';\n\n if (gcpFailFast && !hasAnyLlmProviderKey) {\n if (hasAnySelfHostedFallback && allowDegradedBoot) {\n log.warn(\n {\n gcpPrimary,\n gcpFailFast,\n providerSecretsLoaded,\n selfHostedEndpoints: availableSelfHostedEndpoints,\n mode: 'degraded_self_hosted',\n docs: 'docs/hardening/last-resort-policy',\n },\n '⚠️ DEGRADED BOOT: zero external LLM credentials, but self-hosted fallback is available. ' +\n 'Server will start in last-resort mode. Requests will be tagged excluded_from_benchmark. ' +\n 'Fix ADC/GCP credentials to restore full operation.'\n );\n // Intentionally fall through — boot continues\n } else {\n const message = hasAnySelfHostedFallback\n ? 'SECRETS_PROVIDER_PRIMARY=gcp, no LLM provider credentials loaded, and ALLOW_DEGRADED_BOOT=false.'\n : 'SECRETS_PROVIDER_PRIMARY=gcp but no LLM provider credentials are available after secrets bootstrap, ' +\n 'and no self-hosted fallback endpoints (OLLAMA_URL, SELF_HOSTED_LLM_URL, LOCAL_LLAMA_URL, LOCAL_KOBOLD_URL) are configured.';\n log.error(\n {\n gcpPrimary,\n gcpFailFast,\n providerSecretsLoaded,\n checkedEnvVars: LLM_PROVIDER_ENV_VARS,\n selfHostedEndpointsChecked: SELF_HOSTED_ENV_VARS,\n allowDegradedBoot,\n },\n message\n );\n throw new Error(\n `${message} Verify GCP Secret Manager access (ADC/Workload Identity) and secret names/prefix, ` +\n `or set OLLAMA_URL for degraded self-hosted boot, or run 'gcloud auth application-default login' if local.`\n );\n }\n }\n\n // Warn on partial provider key loading — helps detect GCP issues early\n if (hasAnyLlmProviderKey && providerSecretsLoaded > 0 && providerSecretsLoaded < 3) {\n log.warn(\n { providerSecretsLoaded, total: PROVIDER_SECRETS.length },\n `⚠️ Only ${providerSecretsLoaded} provider secrets loaded — possible partial GCP failure`\n );\n }\n\n // Audio provider diagnostics\n const audioProviders = {\n DEEPGRAM_API_KEY: !!process.env.DEEPGRAM_API_KEY,\n CARTESIA_API_KEY: !!process.env.CARTESIA_API_KEY,\n ELEVENLABS_API_KEY: !!process.env.ELEVENLABS_API_KEY,\n OLLAMA_URL: process.env.OLLAMA_URL || '',\n SELF_HOSTED_STT_URL: process.env.SELF_HOSTED_STT_URL || '',\n LOCAL_NLLB_URL: process.env.LOCAL_NLLB_URL || '',\n };\n log.info({ audioProviders }, 'Audio provider env vars after secrets loading');\n\n log.info(\n {\n loaded,\n skipped,\n failed,\n sanitizedKeys,\n providerSecretsLoaded,\n overwrittenFromGcp,\n gcpAuthoritative,\n total: CRITICAL_SECRETS.length + PROVIDER_SECRETS.length,\n },\n '✅ Secrets loading complete'\n );\n\n if (failed > 0) {\n throw new Error(`Failed to load ${failed} required secrets`);\n }\n}\n\n/**\n * Get summary of secrets loaded.\n *\n * Diagnostic helper for admin endpoints (e.g. /v1/admin/providers/...) that\n * surfaces exactly which provider envVars are populated and from where, so\n * operators can verify SECRETS_PROVIDER_PRIMARY=gcp is actually authoritative\n * and not silently shadowed by stale env values.\n */\nexport function getSecretsLoadSummary(): {\n fromGCP: string[];\n fromEnv: string[];\n notLoaded: string[];\n} {\n const fromGCP: string[] = [];\n const fromEnv: string[] = [];\n const notLoaded: string[] = [];\n\n for (const status of providerKeyStatusMap.values()) {\n if (!status.loaded) {\n notLoaded.push(status.envVar);\n continue;\n }\n if (status.source === 'gcp') {\n fromGCP.push(status.envVar);\n } else if (status.source === 'env') {\n fromEnv.push(status.envVar);\n }\n }\n\n return { fromGCP, fromEnv, notLoaded };\n}\n"}, "2d85636ff28d58279833f79d52590dad3f36922f": {"t": 1785754619.3275213, "n": 64471, "h": "0136561c854731e8", "c": "// Copyright (C) 2026 Ailin One, Inc.\n//\n// This file is part of Collective Intelligence Engine (ci).\n// Licensed under the GNU Affero General Public License v3.0 or later.\n// See LICENSE in the repository root, or <https://www.gnu.org/licenses/>.\n//\n// SPDX-License-Identifier: AGPL-3.0-or-later\n// Source: https://github.com/ailinone/collective-intelligence\n\n/**\n * Load Secrets into Environment\n *\n * This module loads critical secrets from GCP Secret Manager\n * and injects them into process.env BEFORE config validation.\n *\n * This allows the existing config system to work without refactoring,\n * while still using GCP Secret Manager as the source of truth.\n *\n * Call order:\n * 1. initializeSecretsManager()\n * 2. loadSecretsIntoEnv() ← This file\n * 3. validateConfig()\n */\n\nimport { logger } from '@/utils/logger';\nimport { getErrorMessage } from '@/utils/type-guards';\nimport { loadSecret } from './secrets-loader';\n\n/**\n * Critical secrets that must be loaded from GCP\n * These are injected into process.env for config compatibility\n */\ninterface CriticalSecret {\n envVar: string;\n secretKey: string;\n required: boolean;\n defaultValue?: string;\n}\n\nconst CRITICAL_SECRETS: CriticalSecret[] = [\n { envVar: 'DATABASE_URL', secretKey: 'database-url', required: true },\n { envVar: 'JWT_SECRET', secretKey: 'jwt-secret', required: true },\n { envVar: 'REDIS_PASSWORD', secretKey: 'redis-password', required: false, defaultValue: '' },\n { envVar: 'GCS_FILES_BUCKET', secretKey: 'gcs-files-bucket', required: false },\n { envVar: 'SMTP_PASS', secretKey: 'smtp-pass', required: false },\n { envVar: 'STRIPE_SECRET_KEY', secretKey: 'stripe-secret-key', required: false },\n { envVar: 'STRIPE_WEBHOOK_SECRET', secretKey: 'stripe-webhook-secret', required: false },\n { envVar: 'STRIPE_PUBLISHABLE_KEY', secretKey: 'stripe-publishable-key', required: false },\n // Phase 2c shadow ensemble bearer token. Optional because the coord-\n // stable wire is disabled by default in production\n // (CI_ENSEMBLE_COORDINATOR_ENABLED=false). When operator flips it on,\n // the GCP secret `coord-ensemble-token` MUST also be set with the\n // value to share with coord-serving's COORD_SERVING_AUTH_TOKEN.\n { envVar: 'CI_ENSEMBLE_COORDINATOR_TOKEN', secretKey: 'coord-ensemble-token', required: false },\n // Prometheus scrape bearer token for /metrics + /metrics/prompts\n // (metrics-route.ts `authorizeScrape`). Optional here — the route itself\n // is fail-closed when unset: it denies (403) in production and stays\n // open for local/dev scraping otherwise. Wired via CRITICAL_SECRETS\n // (not PROVIDER_SECRETS) because this is a general app credential, not\n // a provider API key.\n { envVar: 'PROMETHEUS_SCRAPE_TOKEN', secretKey: 'prometheus-scrape-token', required: false },\n // Sentry error-tracking DSN (utils/error-tracking.ts `initializeErrorTracking`).\n // Optional — the module itself no-ops with a warning log when unset, both\n // at API boot (index.ts) and worker boot (workers/queue-runner.ts), so\n // there is no fail-closed/fail-open behavior riding on this like the\n // Prometheus token above. Wired via CRITICAL_SECRETS (not PROVIDER_SECRETS)\n // because this is a general app credential, not a provider API key. No GCP\n // secret exists yet as of this commit — an operator must create one\n // (`ailin-sentry-dsn`) with a real DSN from Sentry's project settings\n // before this ever resolves to a non-empty value.\n { envVar: 'SENTRY_DSN', secretKey: 'sentry-dsn', required: false },\n];\n\n/**\n * Provider API keys (loaded from GCP if not in env)\n * CRITICAL: GCP provider automatically adds prefix 'ailin-' to secret keys\n * So 'openai-key' becomes 'ailin-openai-key' in GCP Secret Manager\n * These keys match the actual secret names in GCP (without the prefix, provider adds it)\n */\ninterface ProviderSecretBinding {\n envVar: string;\n secretKeys: readonly string[];\n}\n\nexport const PROVIDER_SECRETS: readonly ProviderSecretBinding[] = [\n { envVar: 'OPENAI_API_KEY', secretKeys: ['openai-key'] },\n { envVar: 'ANTHROPIC_API_KEY', secretKeys: ['anthropic-key'] },\n { envVar: 'GOOGLE_API_KEY', secretKeys: ['google-key'] },\n { envVar: 'DEEPSEEK_API_KEY', secretKeys: ['deepseek-key'] },\n { envVar: 'XAI_API_KEY', secretKeys: ['xai-key'] },\n { envVar: 'MISTRAL_API_KEY', secretKeys: ['mistral-key'] },\n { envVar: 'COHERE_API_KEY', secretKeys: ['cohere-key'] },\n { envVar: 'NVIDIA_API_KEY', secretKeys: ['nvidia-key', 'nvidia-api-key'] },\n { envVar: 'AIHUBMIX_API_KEY', secretKeys: ['aihubmix-key', 'aihubmix-api-key'] },\n { envVar: 'NOVITA_API_KEY', secretKeys: ['novita-key', 'novita-api-key'] },\n { envVar: 'MOONSHOT_API_KEY', secretKeys: ['moonshot-key', 'moonshot-api-key'] },\n { envVar: 'MINIMAX_API_KEY', secretKeys: ['minimax-key', 'minimax-api-key'] },\n { envVar: 'JINA_API_KEY', secretKeys: ['jina-key', 'jina-api-key'] },\n { envVar: 'GOOGLE_MAPS_API_KEY', secretKeys: ['google-maps-key', 'google-maps-api-key'] },\n { envVar: 'FRIENDLI_API_KEY', secretKeys: ['friendli-key', 'friendli-api-key'] },\n { envVar: 'FRIENDLI_TEAM_ID', secretKeys: ['friendli-team-id'] },\n { envVar: 'AIML_API_KEY', secretKeys: ['aiml-key', 'aiml-api-key'] },\n { envVar: 'IMAGEROUTER_API_KEY', secretKeys: ['imagerouter-key', 'imagerouter-api-key'] },\n { envVar: 'OPENROUTER_API_KEY', secretKeys: ['openrouter-key'] },\n { envVar: 'COMETAPI_API_KEY', secretKeys: ['cometapi-key', 'cometapi-api-key'] },\n { envVar: 'NANOGPT_API_KEY', secretKeys: ['nanogpt-key', 'nanogpt-api-key'] },\n { envVar: 'REQUESTY_API_KEY', secretKeys: ['requesty-key', 'requesty-api-key'] },\n { envVar: 'AI302_API_KEY', secretKeys: ['302-key', '302ai-key'] },\n { envVar: 'POE_API_KEY', secretKeys: ['poe-key', 'poe-api-key'] },\n { envVar: 'ROUTEWAY_API_KEY', secretKeys: ['routeway-key', 'routeway-api-key'] },\n // Hub providers support both key naming conventions for compatibility.\n { envVar: 'ORQAI_API_KEY', secretKeys: ['orqai-key', 'orqai-api-key'] },\n { envVar: 'ORQAI_MODEL_DENYLIST', secretKeys: ['orqai-model-denylist'] },\n { envVar: 'EDENAI_API_KEY', secretKeys: ['edenai-key', 'edenai-api-key'] },\n { envVar: 'HELICONEAI_API_KEY', secretKeys: ['heliconeai-key', 'heliconeai-api-key'] },\n { envVar: 'VERTEX_AI_API_KEY', secretKeys: ['vertex-key'] },\n { envVar: 'VERTEX_AI_PROJECT_ID', secretKeys: ['vertex-project-id'] },\n { envVar: 'QWEN_API_KEY', secretKeys: ['qwen-key-2'] },\n { envVar: 'ALIBABA_ACCESS_KEY_ID', secretKeys: ['alibaba-key-id'] },\n { envVar: 'ALIBABA_ACCESS_KEY_SECRET', secretKeys: ['alibaba-key-secret'] },\n { envVar: 'ERNIE_API_KEY', secretKeys: ['baidu-key'] },\n { envVar: 'ERNIE_SECRET_KEY', secretKeys: ['baidu-secret'] },\n { envVar: 'BAIDU_BASE_URL', secretKeys: ['baidu-base-url'] },\n { envVar: 'AWS_ACCESS_KEY_ID', secretKeys: ['aws-key-id'] },\n { envVar: 'AWS_SECRET_ACCESS_KEY', secretKeys: ['aws-secret'] },\n { envVar: 'AWS_BEARER_TOKEN_BEDROCK', secretKeys: ['aws-bearer-token'] },\n // Batch 7.1: Bedrock region + inference-profile ARN can now be set via\n // GCP Secret Manager. Useful for deploys that route Bedrock to a specific\n // region (e.g. us-west-2) or via cross-region inference profiles without\n // baking these into the docker image.\n { envVar: 'AWS_BEDROCK_REGION', secretKeys: ['aws-bedrock-region'] },\n {\n envVar: 'AWS_BEDROCK_INFERENCE_PROFILE_ARN',\n secretKeys: ['aws-bedrock-inference-profile-arn'],\n },\n // Batch 8.1: SageMaker endpoint routing. A deploy can point this adapter\n // at a specific deployed endpoint via a secret rather than baking the\n // endpoint name into the image. The region override lets the adapter\n // hit an endpoint in a different region from the general AWS_REGION\n // used by other services (SES, S3).\n { envVar: 'AWS_SAGEMAKER_REGION', secretKeys: ['aws-sagemaker-region'] },\n { envVar: 'AWS_SAGEMAKER_ENDPOINT_NAME', secretKeys: ['aws-sagemaker-endpoint-name'] },\n { envVar: 'AWS_SAGEMAKER_PAYLOAD_SCHEMA', secretKeys: ['aws-sagemaker-payload-schema'] },\n { envVar: 'AWS_SAGEMAKER_CUSTOM_ATTRIBUTES', secretKeys: ['aws-sagemaker-custom-attributes'] },\n { envVar: 'AZURE_OPENAI_API_KEY', secretKeys: ['azure-openai-api-key'] },\n { envVar: 'AZURE_OPENAI_ENDPOINT', secretKeys: ['azure-openai-endpoint'] },\n { envVar: 'AZURE_OPENAI_DEPLOYMENT', secretKeys: ['azure-openai-deployment'] },\n // Audio-first providers (STT, TTS, STS)\n { envVar: 'DEEPGRAM_API_KEY', secretKeys: ['deepgram-key'] },\n { envVar: 'CARTESIA_API_KEY', secretKeys: ['cartesia-key'] },\n { envVar: 'ELEVENLABS_API_KEY', secretKeys: ['elevenlabs-key'] },\n // Translation provider\n { envVar: 'PALABRAAI_CLIENT_ID', secretKeys: ['palabraai-id'] },\n { envVar: 'PALABRAAI_CLIENT_SECRET', secretKeys: ['palabraai-key'] },\n { envVar: 'OCI_TENANCY_ID', secretKeys: ['oci-tenancy-id'] },\n { envVar: 'OCI_USER_ID', secretKeys: ['oci-user-id'] },\n { envVar: 'OCI_FINGERPRINT', secretKeys: ['oci-fingerprint'] },\n { envVar: 'OCI_PRIVATE_KEY', secretKeys: ['oci-private-key'] },\n { envVar: 'OCI_REGION', secretKeys: ['oci-region'] },\n // 2026-04-22: New GCP secrets (5 providers). The GCP provider auto-prefixes\n // 'ailin-', so 'wandb-key' maps to ailin-wandb-key in Secret Manager.\n // Voyage lists both 'voyageai-key' (current upstream name) and 'voyage-key'\n // (conventional fallback) because the canonical spelling is still unstable.\n { envVar: 'WANDB_API_KEY', secretKeys: ['wandb-key', 'wandb-api-key'] },\n { envVar: 'VOYAGE_API_KEY', secretKeys: ['voyageai-key', 'voyage-key', 'voyage-api-key'] },\n { envVar: 'BYTEZ_API_KEY', secretKeys: ['bytez-key', 'bytez-api-key'] },\n { envVar: 'CEREBRAS_API_KEY', secretKeys: ['cerebras-key', 'cerebras-api-key'] },\n { envVar: 'FEATHERLESS_AI_API_KEY', secretKeys: ['featherless-key', 'featherless-api-key'] },\n // Batch 6 — enterprise hyperscaler gateways (2026-04-22).\n // Azure OpenAI already has API_KEY/ENDPOINT/DEPLOYMENT wired above;\n // these fill in the remaining URL-template parameters.\n {\n envVar: 'AZURE_OPENAI_RESOURCE_NAME',\n secretKeys: ['azure-openai-resource-name', 'azure-openai-resource'],\n },\n { envVar: 'AZURE_OPENAI_API_VERSION', secretKeys: ['azure-openai-api-version'] },\n // Gemini OAI-compat shares the API key with the native `google` provider.\n // Two env-var aliases: GEMINI_API_KEY (canonical upstream) and\n // GOOGLE_AI_STUDIO_API_KEY (legacy). The adapter reads either.\n //\n // 2026-04-23: `vertex-key` added as a fallback source because it was\n // verified working against generativelanguage.googleapis.com (HTTP 200,\n // 50 Gemini models) while `google-key` is suspended (403\n // CONSUMER_SUSPENDED). The Vertex/Gemini API-key split is mostly\n // historical — a Google Cloud API key scoped for Gemini works equally on\n // aistudio and on the generativelanguage surface, as long as the key's\n // GCP project has the API enabled.\n //\n // 2026-07-25: `vertex-key` was originally placed LAST on the (incorrect)\n // assumption that the loop below tries each candidate live and keeps the\n // first one that WORKS. It doesn't — it takes the first candidate that\n // merely EXISTS in Secret Manager (see the resolution loop a few hundred\n // lines down), so the suspended `google-key` always won and gemini-openai\n // stayed broken. Moved `vertex-key` first since it's the only one\n // confirmed live-working; the others remain as fallbacks if it's ever\n // rotated out.\n {\n envVar: 'GEMINI_API_KEY',\n secretKeys: ['vertex-key', 'gemini-key', 'google-ai-studio-key', 'google-key'],\n },\n // GitHub Models uses a GitHub PAT. Operators typically already have this\n // in env (it's the same PAT used for gh CLI); the GCP path is a fallback\n // for service accounts that need isolated tokens.\n { envVar: 'GITHUB_TOKEN', secretKeys: ['github-models-token', 'github-token', 'github-pat'] },\n // Databricks workspace token + config.\n { envVar: 'DATABRICKS_TOKEN', secretKeys: ['databricks-token', 'databricks-pat'] },\n { envVar: 'DATABRICKS_HOST', secretKeys: ['databricks-host'] },\n { envVar: 'DATABRICKS_SERVING_ENDPOINT', secretKeys: ['databricks-serving-endpoint'] },\n // Inworld AI — orphan closure 2026-04-22. Key is already base64-encoded\n // in GCP (132 chars) because Inworld uses Basic auth, not Bearer. The\n // adapter does NOT re-encode; it passes the secret verbatim into the\n // `Basic ` header. Never base64-encode this value at load time.\n { envVar: 'INWORLD_API_KEY', secretKeys: ['inworld-key', 'inworld-api-key'] },\n // ── Orphan-closure Lot B (2026-04-23) ──────────────────────────────────\n // Three providers closed in a single lot because they share identical\n // shape (thin OAI-compat wrappers) and were all already present in GCP\n // Secret Manager but not wired into env loading. Each tuple lists the\n // preferred secret key first and legacy-or-alternate names as fallbacks.\n { envVar: 'WRITER_API_KEY', secretKeys: ['writer-key', 'writer-api-key'] },\n { envVar: 'UPSTAGE_API_KEY', secretKeys: ['upstage-key', 'upstage-api-key'] },\n // Reka's canonical upstream name is \"reka\" but the key is stored in GCP\n // as `ailin-rekaai-key` (matching the catalog providerId, not the upstream\n // name). `reka-key` is kept as a fallback in case the secret is ever\n // renamed to match upstream naming later.\n { envVar: 'REKA_API_KEY', secretKeys: ['rekaai-key', 'reka-key', 'reka-api-key'] },\n // ── LOTE M (2026-04-23) — complement lot: 13 new env-var mappings. ─────\n // Every provider below landed a catalog row this lot and is classified\n // in CONSOLIDATION_MATRIX['credentials-missing'] (secret-absent sub-class\n // for 12, auth-incomplete for qianfan). Each mapping is wired NOW so the\n // moment an operator provisions the corresponding GCP secret, the key\n // propagates to process.env without a code change. The first element of\n // each secretKeys array is the preferred upstream-convention name; any\n // follow-up entries are fallbacks for historical or legacy aliases.\n { envVar: 'ARCEE_API_KEY', secretKeys: ['arcee-api-key', 'arcee-key'] },\n {\n envVar: 'ATLASCLOUD_API_KEY',\n secretKeys: ['atlascloud-key', 'atlascloud-api-key', 'atlas-cloud-key'],\n },\n { envVar: 'AVIAN_API_KEY', secretKeys: ['avian-key', 'avian-api-key', 'avian-io-key'] },\n // Qianfan (Baidu) v2 bce-v3 bearer. Distinct from the legacy v1 AK+SK\n // material in ERNIE_API_KEY / ERNIE_SECRET_KEY / BAIDU_BASE_URL above —\n // both mappings are intentionally kept so an operator can populate either\n // path (v1 OAuth or v2 bearer) depending on which runtime surface is\n // used. The canonical catalog row `qianfan` reads QIANFAN_API_KEY only.\n {\n envVar: 'QIANFAN_API_KEY',\n secretKeys: ['qianfan-key', 'qianfan-api-key', 'baidu-qianfan-key'],\n },\n { envVar: 'GMI_API_KEY', secretKeys: ['gmi-key', 'gmi-api-key', 'gmicloud-key'] },\n {\n envVar: 'INFERMATIC_API_KEY',\n secretKeys: ['infermatic-api-key', 'infermatic-key', 'totalgpt-key'],\n },\n {\n envVar: 'INFLECTION_API_KEY',\n secretKeys: ['inflection-key', 'inflection-api-key', 'inflection-ai-key'],\n },\n // Mancer (tagged contentPolicyClass=uncensored in catalog; per the universal\n // \"habilitado e nunca censurado\" policy from Phase 4b, 2026-04-28, the\n // catalog row is fully admitted to routing — the tag is informational so\n // downstream surfaces can opt to filter it from a moderated default list).\n { envVar: 'MANCER_API_KEY', secretKeys: ['mancer-key', 'mancer-api-key'] },\n // Phala / RedPill TEE — upstream docs use REDPILL_API_KEY but we wire\n // PHALA_API_KEY as the canonical env var (see apiKeyEnvVarOverrideReason\n // in catalog row `phala`). Legacy `redpill-*` secret names are accepted\n // as fallbacks so an operator storing under either convention works.\n {\n envVar: 'PHALA_API_KEY',\n secretKeys: ['phala-key', 'phala-api-key', 'redpill-key', 'redpill-api-key'],\n },\n { envVar: 'RELACE_API_KEY', secretKeys: ['relace-key', 'relace-api-key'] },\n {\n envVar: 'SILICONFLOW_API_KEY',\n secretKeys: ['siliconflow-api-key', 'siliconflow-key', 'silicon-flow-key'],\n },\n { envVar: 'STEPFUN_API_KEY', secretKeys: ['stepfun-api-key', 'stepfun-key', 'step-key'] },\n // Venice (tagged contentPolicyClass=uncensored in catalog). Same Phase-4b\n // treatment as Mancer — admitted, never censored, with the tag as the\n // audit trail for the policy choice.\n { envVar: 'VENICE_API_KEY', secretKeys: ['venice-key', 'venice-api-key', 'venice-ai-key'] },\n // ── Sublote E1 (2026-04-24) — runtime-wiring closure for D1 matrix ─────\n // Sublote D1 promoted 10 providers to CONSOLIDATION_MATRIX['live-validation']\n // based on direct-probe evidence (via gcloud secrets versions access,\n // bypassing the runtime env loader). 7 of those had no GCP→ENV tuple here,\n // so the catalog-loader could never surface `process.env.<X>_API_KEY` at\n // boot — meaning live-validated providers were silently \"not runnable\".\n // Wired pre-emptively: the moment an operator provisions the GCP secret\n // AND the container restarts, propagation happens with zero code change.\n // The 3 already-mapped providers (heliconeai, github-models, infermatic)\n // are NOT duplicated — they appear above under their respective batches.\n { envVar: 'GROQ_API_KEY', secretKeys: ['groq-api-key', 'groq-key'] },\n { envVar: 'DEEPINFRA_API_KEY', secretKeys: ['deepinfra-api-key', 'deepinfra-key'] },\n // Hugging Face: upstream canonical env var is `HF_TOKEN` (not\n // HUGGINGFACE_API_KEY); the catalog row's apiKeyEnvVar matches. Primary\n // GCP secret name follows the upstream convention; legacy huggingface-*\n // aliases accepted for operators who stored it under the provider slug.\n {\n envVar: 'HF_TOKEN',\n secretKeys: ['huggingface-api-key', 'huggingface-token', 'hf-token', 'huggingface-key'],\n },\n // Cloudflare Workers AI — requires BOTH api-token AND account-id. The\n // CloudflareWorkersAIAdapter factory (default-adapter-factories.ts line\n // 229-246) reads CLOUDFLARE_ACCOUNT_ID from env and substitutes it into\n // the `{account_id}` URL template. Without the account-id side-car the\n // adapter ships the literal template to the wire → 404 on every call.\n // Side-car is wired here as a separate tuple because it's not an auth\n // credential — it's a URL parameter — so it doesn't belong in\n // ENV_VAR_TO_PROVIDER or LLM_PROVIDER_ENV_VARS below.\n {\n envVar: 'CLOUDFLARE_API_TOKEN',\n secretKeys: [\n 'cloudflare-workers-ai-api-key',\n 'cloudflare-workers-ai-token',\n 'cloudflare-api-token',\n 'cloudflare-workers-ai-key',\n ],\n },\n {\n envVar: 'CLOUDFLARE_ACCOUNT_ID',\n secretKeys: [\n 'cloudflare-workers-ai-id',\n 'cloudflare-account-id',\n 'cloudflare-workers-ai-account-id',\n ],\n },\n { envVar: 'PERPLEXITY_API_KEY', secretKeys: ['perplexity-api-key', 'perplexity-key'] },\n // Fireworks AI — note the catalog providerId is `fireworks-ai` (hyphenated)\n // but upstream brand/docs spell it \"Fireworks\" (no suffix). Four fallbacks\n // cover both conventions + api-key suffix variant.\n {\n envVar: 'FIREWORKS_AI_API_KEY',\n secretKeys: ['fireworks-ai-api-key', 'fireworks-ai-key', 'fireworks-key', 'fireworks-api-key'],\n },\n { envVar: 'SAMBANOVA_API_KEY', secretKeys: ['sambanova-api-key', 'sambanova-key'] },\n // Replicate — flagged by the sublote-e1-runtime-wiring invariant as a\n // pre-existing gap. `replicate` has been in CONSOLIDATION_MATRIX\n // ['live-validation'] since before D1, with adapterClass 'ReplicateAdapter'\n // and apiKeyEnvVar 'REPLICATE_API_KEY', but no tuple was ever added here.\n // Even though integrationMode is 'execution-only' (no /v1/models\n // discovery), the adapter still requires the env var at /predictions\n // call time. Wired here for closure; the GCP secret name follows the\n // provider-slug convention with optional `-api-key` fallback.\n { envVar: 'REPLICATE_API_KEY', secretKeys: ['replicate-key', 'replicate-api-key'] },\n // ── LOTE N (2026-04-27) — catalog↔loader closure for the remaining 22 ─\n // apiKeyEnvVar entries that had catalog rows but NO PROVIDER_SECRETS\n // tuple. Surfaced by `catalog-loader-wiring-completeness.test.ts` (J6\n // invariant). All 22 are real provider rows with adapter classes (or\n // generic OAI-compat hub registration); they were classified in\n // CONSOLIDATION_MATRIX as `credentials-missing` or `disabled-by-default`,\n // and their absence here meant that even after an operator provisioned\n // the GCP secret, `process.env.<X>_API_KEY` would stay empty at boot.\n //\n // Pattern: preferred secret name first (provider-slug `-key`), then\n // upstream-convention `-api-key` fallback, then any historical aliases.\n // The provider-side `aliases` array in providers.catalog.ts is NOT\n // duplicated here — secret-key aliases are about HOW the operator\n // stored the credential, NOT how callers reach the provider.\n //\n // Image/video-only providers (bfl, recraft, runwayml, topaz) are wired\n // here too but are intentionally absent from LLM_PROVIDER_ENV_VARS below\n // because their presence does NOT count toward the \"at least one LLM\n // key present\" boot-mode gate.\n {\n envVar: 'TOGETHERAI_API_KEY',\n secretKeys: ['togetherai-api-key', 'togetherai-key', 'together-key', 'together-api-key'],\n },\n { envVar: 'ANYSCALE_API_KEY', secretKeys: ['anyscale-api-key', 'anyscale-key'] },\n { envVar: 'HYPERBOLIC_API_KEY', secretKeys: ['hyperbolic-api-key', 'hyperbolic-key'] },\n { envVar: 'CHUTES_API_KEY', secretKeys: ['chutes-api-key', 'chutes-key', 'chutes-ai-key'] },\n { envVar: 'NSCALE_API_KEY', secretKeys: ['nscale-key', 'nscale-api-key'] },\n { envVar: 'NEBIUS_API_KEY', secretKeys: ['nebius-key', 'nebius-api-key'] },\n // Lambda Cloud canonical provider slug is `lambda-ai`; brand is\n // \"Lambda\" (without -ai). Both spellings tried as fallbacks.\n {\n envVar: 'LAMBDA_AI_API_KEY',\n secretKeys: ['lambda-ai-key', 'lambda-key', 'lambda-cloud-key', 'lambda-ai-api-key'],\n },\n { envVar: 'SCALEWAY_API_KEY', secretKeys: ['scaleway-key', 'scaleway-api-key'] },\n {\n envVar: 'SYNTHETIC_API_KEY',\n secretKeys: ['synthetic-key', 'synthetic-api-key', 'synthetic-ai-key'],\n },\n { envVar: 'MORPH_API_KEY', secretKeys: ['morph-key', 'morph-api-key', 'morph-llm-key'] },\n // Z.ai (catalog providerId = `zai`). Upstream domain z.ai resolves to\n // bigmodel.cn — the GLM family operator. Legacy spelling z-ai accepted.\n { envVar: 'ZAI_API_KEY', secretKeys: ['zai-key', 'zai-api-key', 'z-ai-key', 'glm-key'] },\n {\n envVar: 'XIAOMI_MIMO_API_KEY',\n secretKeys: ['xiaomi-mimo-key', 'mimo-key', 'xiaomi-mimo-api-key'],\n },\n // v0 (Vercel) — code-gen LLM. The catalog providerId is `v0` (one\n // character) so the env var name is `V0_API_KEY` not `V0AI_API_KEY`.\n { envVar: 'V0_API_KEY', secretKeys: ['v0-key', 'v0-api-key', 'vercel-v0-key'] },\n // Vercel AI Gateway — DIFFERENT from v0 above (gateway over many LLMs).\n // The canonical secret name follows the catalog providerId.\n {\n envVar: 'VERCEL_AI_GATEWAY_API_KEY',\n secretKeys: ['vercel-ai-gateway-key', 'vercel-ai-gateway-api-key', 'vercel-gateway-key'],\n },\n // Volcano (Volcengine — ByteDance). Canonical slug `volcano`; brand\n // includes Volcengine + Doubao + Ark. Multiple aliases accepted.\n {\n envVar: 'VOLCANO_API_KEY',\n secretKeys: ['volcano-key', 'volcano-api-key', 'volcengine-key', 'doubao-key', 'ark-key'],\n },\n // IBM watsonx — note the env var is `WATSONX_APIKEY` (no underscore\n // between API and KEY) to match IBM's canonical SDK convention.\n // Side-car env vars (WATSONX_PROJECT_ID, WATSONX_URL) are wired below\n // because they're documented in the catalog row's `extraEnvVars` and\n // adapter requires them at runtime.\n {\n envVar: 'WATSONX_APIKEY',\n secretKeys: ['watsonx-apikey', 'watsonx-key', 'watsonx-api-key', 'ibm-watsonx-key'],\n },\n { envVar: 'WATSONX_PROJECT_ID', secretKeys: ['watsonx-project-id', 'ibm-watsonx-project-id'] },\n { envVar: 'WATSONX_URL', secretKeys: ['watsonx-url', 'ibm-watsonx-url'] },\n // Snowflake Cortex — uses Personal Access Token (PAT), not an API key.\n // Multiple side-cars required by the SnowflakeCortexAdapter:\n // SNOWFLAKE_ACCOUNT (orgname-accountname), SNOWFLAKE_USER (key-pair\n // auth user), SNOWFLAKE_BASE_URL (account-specific host). Side-cars\n // wired here so a single GCP provisioning round materializes the full\n // tuple at boot.\n { envVar: 'SNOWFLAKE_PAT', secretKeys: ['snowflake-pat', 'snowflake-token', 'snowflake-key'] },\n { envVar: 'SNOWFLAKE_ACCOUNT', secretKeys: ['snowflake-account'] },\n { envVar: 'SNOWFLAKE_USER', secretKeys: ['snowflake-user', 'snowflake-username'] },\n { envVar: 'SNOWFLAKE_BASE_URL', secretKeys: ['snowflake-base-url', 'snowflake-url'] },\n // SAP AI Core — OAuth2 client_credentials flow. Catalog providerId is\n // `sap`. The apiKeyEnvVar is `SAP_AI_CORE_CLIENT_ID` (the OAuth client\n // id); the matching client_secret + token URL + base URL are all\n // documented in the catalog row's `extraEnvVars` and required by the\n // SapAiCoreAdapter. Wiring all four together so a single provisioning\n // round closes the loop.\n { envVar: 'SAP_AI_CORE_CLIENT_ID', secretKeys: ['sap-ai-core-client-id', 'sap-client-id'] },\n {\n envVar: 'SAP_AI_CORE_CLIENT_SECRET',\n secretKeys: ['sap-ai-core-client-secret', 'sap-client-secret'],\n },\n { envVar: 'SAP_AI_CORE_AUTH_URL', secretKeys: ['sap-ai-core-auth-url', 'sap-auth-url'] },\n { envVar: 'SAP_AI_CORE_BASE_URL', secretKeys: ['sap-ai-core-base-url', 'sap-base-url'] },\n // W&B Inference side-car — WANDB_API_KEY (auth) is already wired in\n // Batch 7 above; WANDB_PROJECT is the project slug header required on\n // every chat call. Wiring here closes the side-car gap.\n { envVar: 'WANDB_PROJECT', secretKeys: ['wandb-project', 'wandb-project-slug'] },\n // Image-only providers (no LLM gate participation, see comment below).\n // bfl auth header is `x-key` (not Bearer) — see catalog `authHeaderName`.\n {\n envVar: 'BFL_API_KEY',\n secretKeys: ['bfl-key', 'bfl-api-key', 'flux-key', 'black-forest-labs-key'],\n },\n { envVar: 'RECRAFT_API_KEY', secretKeys: ['recraft-key', 'recraft-api-key'] },\n { envVar: 'RUNWAYML_API_KEY', secretKeys: ['runwayml-key', 'runwayml-api-key', 'runway-key'] },\n { envVar: 'TOPAZ_API_KEY', secretKeys: ['topaz-key', 'topaz-api-key', 'topaz-labs-key'] },\n // ── LOTE O (2026-07-10) — Apertis + Inception Labs onboarding ─────────\n // Operator pre-provisioned both GCP secrets ahead of the catalog rows\n // (ailin-apertis-key, ailin-inception-key). secretKeys match exactly —\n // the GCP provider auto-prepends the `ailin-` prefix (see file header).\n { envVar: 'APERTIS_API_KEY', secretKeys: ['apertis-key', 'apertis-api-key'] },\n { envVar: 'INCEPTION_API_KEY', secretKeys: ['inception-key', 'inception-api-key'] },\n // ── LOTE P (2026-07-11) — EmpirioLabs AI, live-validated after gate ────\n { envVar: 'EMPIRIOLABS_API_KEY', secretKeys: ['empiriolabs-key', 'empiriolabs-api-key'] },\n // ── LOTE Q (2026-07-12) — Concentrate AI ───────────────────────────────\n { envVar: 'CONCENTRATE_API_KEY', secretKeys: ['concentrate-key', 'concentrate-api-key'] },\n // ── LOTE R (2026-07-13) — FastRouter ────────────────────────────────────\n { envVar: 'FASTROUTER_API_KEY', secretKeys: ['fastrouter-key', 'fastrouter-api-key'] },\n // ── LOTE S (2026-07-13) — Perplexity Agent API ──────────────────────────\n // Same physical credential as PERPLEXITY_API_KEY above (one Perplexity\n // account, two API surfaces) — deliberately sources from the SAME GCP\n // secret names, not a new one.\n { envVar: 'PERPLEXITY_AGENT_API_KEY', secretKeys: ['perplexity-api-key', 'perplexity-key'] },\n // ── LOTE T (2026-07-13) — Ailin gateway (self-referential meta-provider) ──\n // No GCP secret provisioned yet — inert until an operator sets AILIN_API_KEY\n // for a self-hosted deployment. secretKeys follow the same naming\n // convention as the rest of this file in case one is provisioned later.\n { envVar: 'AILIN_API_KEY', secretKeys: ['ailin-key', 'ailin-api-key', 'gateway-key'] },\n // ── LOTE U (2026-07-29) — Sakana AI (Fugu), chat-capable ────────────────\n // 'sakana-ai-key' listed first: it's the actual GCP secret name\n // (`ailin-sakana-ai-key`, prefix auto-added) confirmed live this lot.\n { envVar: 'SAKANA_AI_API_KEY', secretKeys: ['sakana-ai-key', 'sakana-key'] },\n] as const;\n\n// ─── Provider Key Status Tracking ──────────────────────────────────────────\n// Tracks which provider API keys were successfully loaded, from which source,\n// and when. Used by the Self-Healing Discovery (L1) to know when to retry\n// discovery for native providers whose keys arrived late (GCP delay/failure).\n\nexport interface ProviderKeyStatus {\n loaded: boolean;\n source: 'gcp' | 'env' | 'none';\n timestamp: Date;\n envVar: string;\n}\n\n/**\n * Module-level state: populated during loadSecretsIntoEnv() for each provider.\n * Key = normalized provider name (lowercase, e.g., 'openai', 'anthropic').\n */\nconst providerKeyStatusMap = new Map<string, ProviderKeyStatus>();\n\n/**\n * Mapping from env var name to normalized provider name.\n * Allows the discovery service to map env vars to provider sources.\n */\nconst ENV_VAR_TO_PROVIDER: Record<string, string> = {\n // Native API providers\n OPENAI_API_KEY: 'openai',\n ANTHROPIC_API_KEY: 'anthropic',\n GOOGLE_API_KEY: 'google',\n DEEPSEEK_API_KEY: 'deepseek',\n XAI_API_KEY: 'xai',\n MISTRAL_API_KEY: 'mistral',\n COHERE_API_KEY: 'cohere',\n JINA_API_KEY: 'jina',\n // Cloud hubs — 2026-04-23: use canonical providerId 'nvidia' (legacy\n // alias 'nvidia-hub' was absorbed into the nvidia catalog row per Lot B;\n // discovery/status consumers read providerKeyStatus by canonical id).\n NVIDIA_API_KEY: 'nvidia',\n AIHUBMIX_API_KEY: 'aihubmix',\n NOVITA_API_KEY: 'novita',\n MOONSHOT_API_KEY: 'moonshot',\n MINIMAX_API_KEY: 'minimax',\n FRIENDLI_API_KEY: 'friendli',\n AIML_API_KEY: 'aiml',\n IMAGEROUTER_API_KEY: 'imagerouter',\n COMETAPI_API_KEY: 'cometapi',\n NANOGPT_API_KEY: 'nanogpt',\n EDENAI_API_KEY: 'edenai',\n HELICONEAI_API_KEY: 'heliconeai',\n POE_API_KEY: 'poe',\n ROUTEWAY_API_KEY: 'routeway',\n REQUESTY_API_KEY: 'requesty',\n // Post-migration canonical providerId is `ai302` (see providers.catalog.ts\n // and the residue-closure note in provider-registry.ts). Legacy `302ai`\n // survives as a catalog alias for pre-migration user configs.\n AI302_API_KEY: 'ai302',\n ORQAI_API_KEY: 'orqai',\n // Routers/aggregators\n OPENROUTER_API_KEY: 'openrouter',\n // Cloud providers\n VERTEX_AI_API_KEY: 'vertex-ai',\n AWS_ACCESS_KEY_ID: 'aws-bedrock',\n AWS_BEARER_TOKEN_BEDROCK: 'aws-bedrock',\n // SageMaker uses the same shared AWS creds as Bedrock, so `AWS_ACCESS_KEY_ID`\n // keeps mapping to `aws-bedrock` (first-listed wins) — but the endpoint-\n // specific env var is only meaningful for SageMaker. When operators wire\n // this, Self-Healing Discovery will attribute the key presence to the\n // SageMaker provider specifically.\n AWS_SAGEMAKER_ENDPOINT_NAME: 'aws-sagemaker',\n AZURE_OPENAI_API_KEY: 'azure-openai',\n // Regional/specialized\n QWEN_API_KEY: 'qwen',\n ERNIE_API_KEY: 'ernie',\n // Audio providers\n DEEPGRAM_API_KEY: 'deepgram',\n CARTESIA_API_KEY: 'cartesia',\n ELEVENLABS_API_KEY: 'elevenlabs',\n // Inference + embedding providers newly onboarded 2026-04-22\n WANDB_API_KEY: 'wandb',\n VOYAGE_API_KEY: 'voyage',\n BYTEZ_API_KEY: 'bytez',\n CEREBRAS_API_KEY: 'cerebras',\n FEATHERLESS_AI_API_KEY: 'featherless-ai', // canonical catalog id (2026-04-23 drift fix)\n // Batch 6 — enterprise hyperscaler gateways (2026-04-22). Gemini OAI-compat\n // is registered as a SEPARATE provider from the native `google` entry so\n // the self-healing discovery service can track them independently (they\n // have different failure modes — native uses @google/generative-ai SDK\n // with its own error surfaces; OAI-compat uses standard fetch).\n GEMINI_API_KEY: 'gemini-openai',\n GITHUB_TOKEN: 'github-models',\n DATABRICKS_TOKEN: 'databricks',\n // Orphan closure (2026-04-22) — Inworld now has a catalog row + adapter\n // factory. Mapping the env var to the provider slug lets Self-Healing\n // Discovery attribute key presence to the correct provider.\n INWORLD_API_KEY: 'inworld',\n // Orphan-closure Lot B (2026-04-23). Slug = catalog providerId. Writer\n // and Upstage are self-evident; Reka's slug is `rekaai` (the catalog\n // row's canonical providerId — aliases 'reka' + 'reka-ai' resolve to it).\n WRITER_API_KEY: 'writer',\n UPSTAGE_API_KEY: 'upstage',\n REKA_API_KEY: 'rekaai',\n // ── LOTE M (2026-04-23) — complement lot, 13 providers ──────────────\n // Slug = catalog providerId (NOT the upstream brand for phala/qianfan\n // which intentionally diverge — see catalog rows for rationale):\n // phala — canonical slug; upstream brand is \"RedPill\"\n // qianfan — canonical slug; upstream brand is \"Baidu Qianfan / ERNIE\"\n // gmi — canonical slug; upstream brand is \"GMICloud\"\n ARCEE_API_KEY: 'arcee',\n ATLASCLOUD_API_KEY: 'atlascloud',\n AVIAN_API_KEY: 'avian',\n QIANFAN_API_KEY: 'qianfan',\n GMI_API_KEY: 'gmi',\n INFERMATIC_API_KEY: 'infermatic',\n INFLECTION_API_KEY: 'inflection',\n MANCER_API_KEY: 'mancer',\n PHALA_API_KEY: 'phala',\n RELACE_API_KEY: 'relace',\n SILICONFLOW_API_KEY: 'siliconflow',\n STEPFUN_API_KEY: 'stepfun',\n VENICE_API_KEY: 'venice',\n // ── Sublote E1 (2026-04-24) — runtime-wiring closure for D1 matrix ───\n // 7 provider slugs whose env vars gained GCP→ENV tuples in this lot.\n // Slug = catalog providerId exactly (so Self-Healing Discovery can\n // attribute key-loaded events to the right provider). CLOUDFLARE_ACCOUNT_ID\n // is intentionally absent — that's a URL parameter, not an auth credential,\n // so mapping it to a provider slug would be semantically wrong (the same\n // account-id could conceivably scope multiple Cloudflare providers).\n GROQ_API_KEY: 'groq',\n DEEPINFRA_API_KEY: 'deepinfra',\n HF_TOKEN: 'huggingface',\n CLOUDFLARE_API_TOKEN: 'cloudflare-workers-ai',\n PERPLEXITY_API_KEY: 'perplexity',\n FIREWORKS_AI_API_KEY: 'fireworks-ai',\n SAMBANOVA_API_KEY: 'sambanova',\n // Replicate — pre-existing live-validation gap closed by sublote E1.\n // See the matching comment on the PROVIDER_SECRETS tuple above.\n REPLICATE_API_KEY: 'replicate',\n // ── LOTE N (2026-04-27) — catalog↔loader closure (22 new env vars) ──\n // Every entry below maps an env var to its canonical providerId so the\n // L1 Self-Healing Discovery Service can attribute key-loaded events\n // when the corresponding GCP secret is provisioned. Slugs match the\n // catalog providerId exactly (NOT upstream brand) — see catalog rows\n // for cases where they diverge (e.g. `sap` not `sap-ai-core`, `v0` not\n // `vercel-v0`, `volcano` not `volcengine`). Side-car env vars\n // (WATSONX_PROJECT_ID, SNOWFLAKE_ACCOUNT, SAP_AI_CORE_CLIENT_SECRET,\n // etc.) are NOT mapped here — they're auth or URL parameters, not\n // credentials, so attributing them to a provider would conflate\n // credential-presence with side-car-completeness in discovery telemetry.\n TOGETHERAI_API_KEY: 'togetherai',\n ANYSCALE_API_KEY: 'anyscale',\n HYPERBOLIC_API_KEY: 'hyperbolic',\n CHUTES_API_KEY: 'chutes',\n NSCALE_API_KEY: 'nscale',\n NEBIUS_API_KEY: 'nebius',\n LAMBDA_AI_API_KEY: 'lambda-ai',\n SCALEWAY_API_KEY: 'scaleway',\n SYNTHETIC_API_KEY: 'synthetic',\n MORPH_API_KEY: 'morph',\n ZAI_API_KEY: 'zai',\n XIAOMI_MIMO_API_KEY: 'xiaomi-mimo',\n V0_API_KEY: 'v0',\n VERCEL_AI_GATEWAY_API_KEY: 'vercel-ai-gateway',\n VOLCANO_API_KEY: 'volcano',\n WATSONX_APIKEY: 'watsonx',\n SNOWFLAKE_PAT: 'snowflake',\n SAP_AI_CORE_CLIENT_ID: 'sap',\n // Image / video-only providers. Still mapped to a slug so Self-Healing\n // Discovery can record key-presence events — but their env vars do NOT\n // appear in LLM_PROVIDER_ENV_VARS (image surfaces don't satisfy the\n // \"at least one LLM key\" boot gate).\n BFL_API_KEY: 'bfl',\n RECRAFT_API_KEY: 'recraft',\n RUNWAYML_API_KEY: 'runwayml',\n TOPAZ_API_KEY: 'topaz',\n // ── LOTE O (2026-07-10) ────────────────────────────────────────────────\n APERTIS_API_KEY: 'apertis',\n INCEPTION_API_KEY: 'inception',\n // ── LOTE P (2026-07-11) ────────────────────────────────────────────────\n EMPIRIOLABS_API_KEY: 'empiriolabs',\n // ── LOTE Q (2026-07-12) ────────────────────────────────────────────────\n CONCENTRATE_API_KEY: 'concentrate',\n // ── LOTE R (2026-07-13) ────────────────────────────────────────────────\n FASTROUTER_API_KEY: 'fastrouter',\n // ── LOTE S (2026-07-13) ────────────────────────────────────────────────\n PERPLEXITY_AGENT_API_KEY: 'perplexity-agent',\n // ── LOTE T (2026-07-13) ────────────────────────────────────────────────\n AILIN_API_KEY: 'ailin',\n // ── LOTE U (2026-07-29) ────────────────────────────────────────────────\n SAKANA_AI_API_KEY: 'sakana-ai',\n};\n\n/**\n * Returns the status of all provider API keys.\n * Used by Self-Healing Discovery to determine which native sources can run.\n */\nexport function getProviderKeyStatus(): ReadonlyMap<string, ProviderKeyStatus> {\n return providerKeyStatusMap;\n}\n\n/**\n * Returns provider names that have valid (non-empty, non-mock) API keys loaded.\n */\nexport function getLoadedProviderNames(): string[] {\n return Array.from(providerKeyStatusMap.entries())\n .filter(([, status]) => status.loaded)\n .map(([name]) => name);\n}\n\n/**\n * Check if a specific provider has a loaded API key.\n */\nexport function isProviderKeyLoaded(providerName: string): boolean {\n return providerKeyStatusMap.get(providerName.toLowerCase())?.loaded ?? false;\n}\n\nconst LLM_PROVIDER_ENV_VARS = [\n 'OPENAI_API_KEY',\n 'ANTHROPIC_API_KEY',\n 'GOOGLE_API_KEY',\n 'DEEPSEEK_API_KEY',\n 'XAI_API_KEY',\n 'MISTRAL_API_KEY',\n 'COHERE_API_KEY',\n 'NVIDIA_API_KEY',\n 'AIHUBMIX_API_KEY',\n 'NOVITA_API_KEY',\n 'MOONSHOT_API_KEY',\n 'MINIMAX_API_KEY',\n 'JINA_API_KEY',\n 'FRIENDLI_API_KEY',\n 'AIML_API_KEY',\n 'IMAGEROUTER_API_KEY',\n 'OPENROUTER_API_KEY',\n 'ORQAI_API_KEY',\n 'EDENAI_API_KEY',\n 'HELICONEAI_API_KEY',\n 'VERTEX_AI_API_KEY',\n 'AWS_BEARER_TOKEN_BEDROCK',\n 'AZURE_OPENAI_API_KEY',\n 'QWEN_API_KEY',\n 'ERNIE_API_KEY',\n 'COMETAPI_API_KEY',\n 'NANOGPT_API_KEY',\n 'REQUESTY_API_KEY',\n 'AI302_API_KEY',\n 'POE_API_KEY',\n 'ROUTEWAY_API_KEY',\n // Added 2026-04-22 — these keys now count toward the \"at least one LLM key\n // present\" gate that avoids DEGRADED_SELF_HOSTED mode at boot.\n 'WANDB_API_KEY',\n 'VOYAGE_API_KEY',\n 'CEREBRAS_API_KEY',\n 'FEATHERLESS_AI_API_KEY',\n // Batch 4 landed BYTEZ catalog row + adapter, so BYTEZ_API_KEY now counts\n // toward the LLM-key-present gate.\n 'BYTEZ_API_KEY',\n // Batch 6 — enterprise hyperscaler gateways (2026-04-22). These keys now\n // count toward the \"at least one LLM key present\" gate; all four have\n // landed catalog rows + adapter classes in this batch.\n 'GEMINI_API_KEY',\n 'GITHUB_TOKEN',\n 'DATABRICKS_TOKEN',\n // Batch 8.1 — AWS SageMaker. Endpoint name is the meaningful trigger:\n // AWS creds alone are ambiguous (could be Bedrock or S3), but an\n // endpoint name implies \"SageMaker is provisioned here\".\n 'AWS_SAGEMAKER_ENDPOINT_NAME',\n // Orphan closure (2026-04-22) — Inworld AI now counts toward the\n // \"at least one LLM key present\" gate now that the catalog row +\n // adapter factory are wired.\n 'INWORLD_API_KEY',\n // Orphan-closure Lot B (2026-04-23). All three are fully canonical now.\n 'WRITER_API_KEY',\n 'UPSTAGE_API_KEY',\n 'REKA_API_KEY',\n // ── LOTE M (2026-04-23) complement lot ──────────────────────────────\n // Each of the 13 keys below counts toward the \"at least one LLM key\n // present\" gate, the same way all catalog-wired keys do. Note that\n // mancer and venice carry contentPolicyClass=uncensored (2026-04-28\n // Phase 4b — the prior denyByDefault gate was removed per the universal\n // \"habilitado e nunca censurado\" policy); their keys count toward the\n // gate exactly like any other LLM key — surface-level filtering is now\n // a downstream concern, not a routing one.\n // qianfan here is QIANFAN_API_KEY (v2 bce-v3 bearer); the legacy v1\n // ERNIE_API_KEY was already listed above this marker.\n 'ARCEE_API_KEY',\n 'ATLASCLOUD_API_KEY',\n 'AVIAN_API_KEY',\n 'QIANFAN_API_KEY',\n 'GMI_API_KEY',\n 'INFERMATIC_API_KEY',\n 'INFLECTION_API_KEY',\n 'MANCER_API_KEY',\n 'PHALA_API_KEY',\n 'RELACE_API_KEY',\n 'SILICONFLOW_API_KEY',\n 'STEPFUN_API_KEY',\n 'VENICE_API_KEY',\n // ── Sublote E1 (2026-04-24) — runtime-wiring closure for D1 matrix ───\n // These 7 providers landed in CONSOLIDATION_MATRIX['live-validation']\n // during Sublote D1 but had no GCP→ENV tuple above → their keys could\n // not count toward the \"at least one LLM key present\" gate even when\n // GCP Secret Manager held the key. Listed here so a restart after the\n // operator's provisioning cycle propagates the key-present signal to\n // the DEGRADED_SELF_HOSTED avoidance logic. CLOUDFLARE_ACCOUNT_ID is\n // intentionally absent — it's a URL parameter, not an LLM credential.\n 'GROQ_API_KEY',\n 'DEEPINFRA_API_KEY',\n 'HF_TOKEN',\n 'CLOUDFLARE_API_TOKEN',\n 'PERPLEXITY_API_KEY',\n 'FIREWORKS_AI_API_KEY',\n 'SAMBANOVA_API_KEY',\n // Replicate — pre-existing live-validation gap closed by sublote E1.\n // See the matching comments on the PROVIDER_SECRETS tuple and\n // ENV_VAR_TO_PROVIDER entry above.\n 'REPLICATE_API_KEY',\n // ── LOTE N (2026-04-27) — catalog↔loader closure ─────────────────────\n // 18 LLM-class providers added to the boot-mode gate. The 4 image/\n // video-only providers (bfl, recraft, runwayml, topaz) are\n // intentionally OMITTED — their key presence does not signal the\n // server has any text-generation capability, so they should not\n // satisfy the \"at least one LLM key present\" check.\n 'TOGETHERAI_API_KEY',\n 'ANYSCALE_API_KEY',\n 'HYPERBOLIC_API_KEY',\n 'CHUTES_API_KEY',\n 'NSCALE_API_KEY',\n 'NEBIUS_API_KEY',\n 'LAMBDA_AI_API_KEY',\n 'SCALEWAY_API_KEY',\n 'SYNTHETIC_API_KEY',\n 'MORPH_API_KEY',\n 'ZAI_API_KEY',\n 'XIAOMI_MIMO_API_KEY',\n 'V0_API_KEY',\n 'VERCEL_AI_GATEWAY_API_KEY',\n 'VOLCANO_API_KEY',\n 'WATSONX_APIKEY',\n 'SNOWFLAKE_PAT',\n 'SAP_AI_CORE_CLIENT_ID',\n // ── LOTE O (2026-07-10) — Apertis + Inception Labs, both chat-capable ──\n 'APERTIS_API_KEY',\n 'INCEPTION_API_KEY',\n // ── LOTE P (2026-07-11) — EmpirioLabs AI, chat-capable ─────────────────\n 'EMPIRIOLABS_API_KEY',\n // ── LOTE Q (2026-07-12) — Concentrate AI, chat-capable ─────────────────\n 'CONCENTRATE_API_KEY',\n // ── LOTE R (2026-07-13) — FastRouter, chat-capable ─────────────────────\n 'FASTROUTER_API_KEY',\n // ── LOTE S (2026-07-13) — Perplexity Agent API, chat-capable ───────────\n 'PERPLEXITY_AGENT_API_KEY',\n // ── LOTE T (2026-07-13) — Ailin gateway, chat-capable ──────────────────\n 'AILIN_API_KEY',\n // ── LOTE U (2026-07-29) — Sakana AI (Fugu), chat-capable ────────────────\n 'SAKANA_AI_API_KEY',\n] as const;\n\n/**\n * Self-hosted / sidecar endpoints that can keep the server alive in a\n * DEGRADED mode when every external LLM API key is missing.\n *\n * Design rationale:\n * Without this fallback, a single failure (expired GCP ADC, rotated\n * service account, network blip during boot) produces an infinite\n * restart loop. With it, the server boots in `degraded_self_hosted`\n * mode: only local/sidecar models are usable, requests are tagged\n * via last-resort-policy.ts (execution_mode=last_resort_self_hosted,\n * excluded_from_benchmark=true), and ops gets a clear warning\n * instead of a crash loop.\n *\n * This ties directly into Bloco P1.10 (self-hosted last-resort policy)\n * but at boot-time scope rather than per-request scope.\n */\nconst SELF_HOSTED_ENV_VARS = [\n 'OLLAMA_URL',\n 'SELF_HOSTED_LLM_URL',\n 'LOCAL_LLAMA_URL',\n 'LOCAL_KOBOLD_URL',\n] as const;\n\n/**\n * Load critical secrets from GCP into process.env\n *\n * This function is called AFTER SecretsManager initialization\n * and BEFORE config validation.\n *\n * It loads secrets from GCP and injects them into process.env,\n * allowing the existing config system to work without modification.\n */\nexport async function loadSecretsIntoEnv(): Promise<void> {\n const log = logger.child({ component: 'secrets-loader' });\n\n log.info('Loading critical secrets from GCP Secret Manager into environment...');\n\n let loaded = 0;\n let skipped = 0;\n let failed = 0;\n let sanitizedKeys = 0;\n let providerSecretsLoaded = 0;\n // Tracks env values that were authoritatively replaced by GCP fetches.\n // Useful diagnostic for operators verifying that GCP is the source of truth.\n let overwrittenFromGcp = 0;\n\n const forceLoadRealKeys = process.env.TEST_USE_REAL_API_KEYS === 'true';\n\n // ── GCP-authoritative mode ─────────────────────────────────────────────\n // When SECRETS_PROVIDER_PRIMARY=gcp the operator has declared GCP as the\n // source of truth. The legacy \"skip if env already populated\" path then\n // contradicts that declaration: env_file / docker-compose / shell exports\n // silently win and GCP fetches are never even attempted, which is exactly\n // the failure mode that hid populated GCP secrets (e.g. HF_TOKEN) from\n // /v1/models. In authoritative mode we ALWAYS attempt the GCP fetch and\n // overwrite the pre-existing env value when GCP returns one. The fallback\n // is symmetric: a GCP miss preserves the existing env value (no zeroing).\n //\n // Operators can override:\n // SECRETS_GCP_AUTHORITATIVE=false → keep legacy skip-if-exists semantics\n // SECRETS_GCP_AUTHORITATIVE=true → force authoritative even with env primary\n const gcpPrimaryEnv = (process.env.SECRETS_PROVIDER_PRIMARY || '').trim().toLowerCase() === 'gcp';\n const explicitAuthoritative = (process.env.SECRETS_GCP_AUTHORITATIVE || '').trim().toLowerCase();\n const gcpAuthoritative =\n explicitAuthoritative === 'true' || (explicitAuthoritative !== 'false' && gcpPrimaryEnv);\n\n // Tests already use TEST_USE_REAL_API_KEYS=true to force GCP fetches even\n // when local mocks are present. The two flags are independent triggers\n // for the same effect; either one disables skip-if-exists.\n const authoritativeFetch = gcpAuthoritative || forceLoadRealKeys;\n\n log.info(\n {\n gcpPrimary: gcpPrimaryEnv,\n gcpAuthoritative,\n forceLoadRealKeys,\n authoritativeFetch,\n },\n 'Secret loading mode resolved'\n );\n\n // Load critical secrets (required)\n for (const { envVar, secretKey, required, defaultValue } of CRITICAL_SECRETS) {\n const existingValue = process.env[envVar];\n const shouldOverrideMockStripe =\n forceLoadRealKeys &&\n envVar.startsWith('STRIPE_') &&\n typeof existingValue === 'string' &&\n (existingValue.includes('mock') || existingValue.includes('test'));\n\n // Skip if already in env, UNLESS:\n // - we're in GCP-authoritative mode (operator declared GCP as source of truth), OR\n // - we're forcing real keys and detected mock Stripe keys.\n if (existingValue && !shouldOverrideMockStripe && !gcpAuthoritative) {\n log.debug({ envVar, source: 'env' }, 'Secret already in environment, skipping GCP load');\n skipped++;\n continue;\n }\n\n try {\n const value = await loadSecret(secretKey, required);\n if (value) {\n const wasOverwrite = Boolean(existingValue) && existingValue !== value;\n process.env[envVar] = value;\n if (wasOverwrite) overwrittenFromGcp++;\n log.debug(\n { envVar, secretKey, replaced: Boolean(existingValue), overwrittenFromGcp: wasOverwrite },\n 'Secret loaded from GCP into environment'\n );\n loaded++;\n } else if (existingValue && gcpAuthoritative) {\n // Authoritative fetch missed in GCP — fall back to the existing env\n // value rather than zeroing it. This keeps the system bootable when\n // a single secret is absent from GCP without losing the env-provided\n // value.\n log.debug(\n { envVar, secretKey },\n 'GCP miss in authoritative mode; preserving existing env value'\n );\n skipped++;\n } else if (defaultValue !== undefined) {\n // Use default value if secret not found and default provided\n process.env[envVar] = defaultValue;\n log.debug({ envVar, secretKey, defaultValue }, 'Secret not found, using default value');\n skipped++;\n } else if (!required) {\n log.debug({ envVar, secretKey }, 'Optional secret not found, skipping');\n skipped++;\n }\n } catch (error) {\n if (defaultValue !== undefined) {\n // Use default value on error if provided\n process.env[envVar] = defaultValue;\n log.warn(\n { envVar, secretKey, defaultValue, error: getErrorMessage(error) },\n 'Failed to load secret, using default value'\n );\n skipped++;\n } else if (required) {\n const errorMsg = getErrorMessage(error);\n log.error({ envVar, secretKey, error: errorMsg }, 'Failed to load required secret');\n failed++;\n throw new Error(`Failed to load required secret ${secretKey}: ${errorMsg}`);\n } else {\n log.warn(\n { envVar, secretKey, error: getErrorMessage(error) },\n 'Failed to load optional secret'\n );\n skipped++;\n }\n }\n }\n\n // Keep compatibility with components that still read GCS_BUCKET_NAME.\n if (!process.env.GCS_BUCKET_NAME && process.env.GCS_FILES_BUCKET) {\n process.env.GCS_BUCKET_NAME = process.env.GCS_FILES_BUCKET;\n }\n\n // Load provider API keys (optional)\n // CRITICAL: If GCP auth is available, ALWAYS load real keys (API Keys NUNCA DEVEM FALHAR)\n // If TEST_USE_REAL_API_KEYS=true, force load from GCP even if mock keys exist\n // Always replace mock keys with real keys from GCP when available\n // (reuses forceLoadRealKeys defined above)\n\n for (const { envVar, secretKeys } of PROVIDER_SECRETS) {\n const existingValue = process.env[envVar];\n const providerName = ENV_VAR_TO_PROVIDER[envVar];\n\n // Check if existing value looks like a mock key\n const isMockKey =\n existingValue && (existingValue.includes('mock') || existingValue.includes('test-'));\n\n // Skip if real key already exists, UNLESS:\n // - we're in GCP-authoritative mode (operator declared GCP as source of truth), OR\n // - test forces real keys (TEST_USE_REAL_API_KEYS=true), OR\n // - the existing value looks like a mock.\n // The authoritative path lets GCP overwrite stale env_file / shell-exported\n // values, which is the entire point of SECRETS_PROVIDER_PRIMARY=gcp.\n if (existingValue && !authoritativeFetch && !isMockKey) {\n // Record as loaded from env\n if (providerName) {\n providerKeyStatusMap.set(providerName, {\n loaded: true,\n source: 'env',\n timestamp: new Date(),\n envVar,\n });\n }\n skipped++;\n continue;\n }\n\n try {\n let loadedSecretValue: string | undefined;\n let resolvedSecretKey = secretKeys[0];\n\n for (const candidateKey of secretKeys) {\n try {\n const candidateValue = await loadSecret(candidateKey, false);\n if (candidateValue) {\n loadedSecretValue = candidateValue;\n resolvedSecretKey = candidateKey;\n log.debug(\n { envVar, secretKey: candidateKey, valueLen: candidateValue.length },\n 'Secret loaded from GCP'\n );\n break;\n } else {\n log.debug({ envVar, secretKey: candidateKey }, 'Secret returned empty/null from GCP');\n }\n } catch (loadErr) {\n log.warn(\n {\n envVar,\n secretKey: candidateKey,\n error: loadErr instanceof Error ? loadErr.message : String(loadErr),\n },\n 'Failed to load secret from GCP'\n );\n continue;\n }\n }\n\n // GCP provider automatically adds prefix 'ailin-' to secretKey\n // So 'openai-key' becomes 'ailin-openai-key' in GCP Secret Manager\n const value = loadedSecretValue;\n if (value) {\n // Sanitize control characters and invisible Unicode.\n // PEM-format keys (-----BEGIN ...) legitimately contain newlines,\n // so use a lighter regex that preserves \\n and \\r for those values.\n const isPemKey = value.startsWith('-----BEGIN');\n let sanitizedValue = isPemKey\n ? value\n .replace(/[\\x00-\\x09\\x0B\\x0C\\x0E-\\x1F\\x7F]/g, '') // Strip control chars EXCEPT \\n (0x0A) and \\r (0x0D)\n .replace(/[\\u200B-\\u200D\\uFEFF]/g, '') // Remove zero-width spaces\n .trim()\n : value\n .replace(/[\\x00-\\x1F\\x7F]/g, '') // Strip ALL control characters\n .replace(/[\\u200B-\\u200D\\uFEFF]/g, '') // Remove zero-width spaces\n .trim();\n\n // Log detailed debugging information if sanitization changed the value\n if (sanitizedValue !== value) {\n const removedChars = value.length - sanitizedValue.length;\n sanitizedKeys++;\n log.debug(\n {\n envVar,\n secretKey: resolvedSecretKey,\n originalLength: value.length,\n sanitizedLength: sanitizedValue.length,\n removedChars,\n },\n 'API key contained invalid characters and was sanitized'\n );\n }\n\n // Always replace mock keys with real keys\n const wasReplaced = !!existingValue;\n const wasOverwriteOfRealValue =\n wasReplaced && !isMockKey && existingValue !== sanitizedValue;\n\n // CRITICAL: Set process.env and immediately validate\n // Use Object.defineProperty as fallback if direct assignment doesn't work\n // This ensures the value is actually set in the environment\n const _previousValue = process.env[envVar];\n process.env[envVar] = sanitizedValue;\n\n // Immediately read back to verify (handles any potential async issues)\n const readBackValue = process.env[envVar];\n\n // Deep validation: check exact match, type, and length\n const isValidMatch = readBackValue === sanitizedValue;\n const isValidType = typeof readBackValue === 'string';\n const isValidLength =\n readBackValue !== undefined && readBackValue.length === sanitizedValue.length;\n\n if (!isValidMatch || !isValidType || !isValidLength) {\n log.error(\n {\n envVar,\n secretKey: resolvedSecretKey,\n sanitizedValueLength: sanitizedValue.length,\n readBackValueLength: readBackValue?.length,\n },\n 'Secret was loaded but process.env assignment did not persist'\n );\n failed++;\n } else {\n // Success - log and increment counter\n if (wasOverwriteOfRealValue) overwrittenFromGcp++;\n log.debug(\n {\n envVar,\n secretKey: resolvedSecretKey,\n replaced: wasReplaced,\n wasMock: isMockKey,\n overwrittenFromGcp: wasOverwriteOfRealValue,\n },\n 'Provider secret loaded from GCP'\n );\n loaded++;\n providerSecretsLoaded++;\n // Record successful load\n if (providerName) {\n providerKeyStatusMap.set(providerName, {\n loaded: true,\n source: 'gcp',\n timestamp: new Date(),\n envVar,\n });\n }\n }\n } else {\n // GCP returned no value. Decide what's actually in process.env right\n // now: if env had a non-mock value before this loop iteration we\n // preserved it (the authoritative path entered this block but found\n // no GCP value to overwrite with), so we're effectively env-loaded.\n // If env had a mock value, we keep the mock as a degraded fallback\n // and warn loudly. If env had nothing, the provider stays orphaned.\n if (existingValue && !isMockKey) {\n // Authoritative-mode GCP miss with real env value already set:\n // the env value is still in process.env, so report source=env.\n if (providerName) {\n providerKeyStatusMap.set(providerName, {\n loaded: true,\n source: 'env',\n timestamp: new Date(),\n envVar,\n });\n }\n log.debug(\n { envVar, secretKeys },\n 'GCP miss in authoritative mode; preserving existing env value for provider'\n );\n } else {\n if (isMockKey) {\n log.warn(\n { envVar, secretKeys },\n 'Mock API key detected but no real key found in GCP - using mock key (this may cause API failures)'\n );\n }\n // Record as not loaded (no GCP value, no usable env value)\n if (providerName) {\n providerKeyStatusMap.set(providerName, {\n loaded: false,\n source: 'none',\n timestamp: new Date(),\n envVar,\n });\n }\n }\n skipped++;\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n // Only warn if not forcing real keys - if forcing, this is expected to work\n if (forceLoadRealKeys) {\n log.error(\n { envVar, secretKeys, error: errorMessage },\n 'Failed to load provider secret from GCP (TEST_USE_REAL_API_KEYS=true)'\n );\n failed++;\n } else {\n if (isMockKey) {\n log.warn(\n { envVar, secretKeys, error: errorMessage },\n 'Failed to load provider secret from GCP - will use mock key (this may cause API failures)'\n );\n } else {\n log.debug(\n { envVar, secretKeys, error: errorMessage },\n 'Failed to load provider secret from GCP (optional)'\n );\n }\n // Record failure\n if (providerName) {\n providerKeyStatusMap.set(providerName, {\n loaded: false,\n source: 'none',\n timestamp: new Date(),\n envVar,\n });\n }\n skipped++;\n }\n }\n }\n\n // ── Env-var aliases (2026-04-28) ────────────────────────────────────────\n //\n // Some operators set a non-canonical name in `.env` for historical reasons\n // (e.g. `REKAAI_API_KEY` instead of the catalog-canonical `REKA_API_KEY`).\n // When GCP is reachable the secret loader populates the canonical name and\n // it just works; when GCP is unreachable the alias would be the only thing\n // set and the catalog row reads the canonical → empty key → silent skip.\n //\n // This pass copies known aliases into their canonical env var if the\n // canonical is empty. It is purely additive — never overwrites an\n // existing canonical value, never logs a warning unless an alias was\n // applied. Add new entries here when a similar mismatch is discovered.\n const ENV_ALIASES: ReadonlyArray<{ canonical: string; alias: string }> = [\n { canonical: 'REKA_API_KEY', alias: 'REKAAI_API_KEY' },\n ];\n for (const { canonical, alias } of ENV_ALIASES) {\n const canonicalValue = process.env[canonical];\n const aliasValue = process.env[alias];\n if (\n (canonicalValue == null || canonicalValue.trim() === '') &&\n aliasValue &&\n aliasValue.trim() !== ''\n ) {\n process.env[canonical] = aliasValue;\n log.info({ canonical, alias }, 'Promoted alias env var to canonical name');\n }\n }\n\n const gcpPrimary = (process.env.SECRETS_PROVIDER_PRIMARY || '').trim().toLowerCase() === 'gcp';\n const failFastEnv = (process.env.SECRETS_GCP_FAIL_FAST || '').trim().toLowerCase();\n const gcpFailFast = failFastEnv === 'true' || (failFastEnv !== 'false' && gcpPrimary);\n const hasAnyLlmProviderKey = LLM_PROVIDER_ENV_VARS.some((envVar) => {\n const value = process.env[envVar];\n return typeof value === 'string' && value.trim().length > 0;\n });\n\n // Hardening (P1.10 at boot scope): even when zero external API keys are\n // loaded, the server can still boot in DEGRADED mode if a self-hosted\n // endpoint is reachable. Every request will then be tagged via the\n // last-resort-policy as execution_mode=last_resort_self_hosted.\n const availableSelfHostedEndpoints = SELF_HOSTED_ENV_VARS.filter((envVar) => {\n const value = process.env[envVar];\n return typeof value === 'string' && value.trim().length > 0;\n });\n const hasAnySelfHostedFallback = availableSelfHostedEndpoints.length > 0;\n\n // Opt-out for strict production deployments that must never run in\n // degraded mode. Default: allow degraded boot (safer than crash loops).\n const allowDegradedBoot =\n (process.env.ALLOW_DEGRADED_BOOT || 'true').trim().toLowerCase() !== 'false';\n\n if (gcpFailFast && !hasAnyLlmProviderKey) {\n if (hasAnySelfHostedFallback && allowDegradedBoot) {\n log.warn(\n {\n gcpPrimary,\n gcpFailFast,\n providerSecretsLoaded,\n selfHostedEndpoints: availableSelfHostedEndpoints,\n mode: 'degraded_self_hosted',\n docs: 'docs/hardening/last-resort-policy',\n },\n '⚠️ DEGRADED BOOT: zero external LLM credentials, but self-hosted fallback is available. ' +\n 'Server will start in last-resort mode. Requests will be tagged excluded_from_benchmark. ' +\n 'Fix ADC/GCP credentials to restore full operation.'\n );\n // Intentionally fall through — boot continues\n } else {\n const message = hasAnySelfHostedFallback\n ? 'SECRETS_PROVIDER_PRIMARY=gcp, no LLM provider credentials loaded, and ALLOW_DEGRADED_BOOT=false.'\n : 'SECRETS_PROVIDER_PRIMARY=gcp but no LLM provider credentials are available after secrets bootstrap, ' +\n 'and no self-hosted fallback endpoints (OLLAMA_URL, SELF_HOSTED_LLM_URL, LOCAL_LLAMA_URL, LOCAL_KOBOLD_URL) are configured.';\n log.error(\n {\n gcpPrimary,\n gcpFailFast,\n providerSecretsLoaded,\n checkedEnvVars: LLM_PROVIDER_ENV_VARS,\n selfHostedEndpointsChecked: SELF_HOSTED_ENV_VARS,\n allowDegradedBoot,\n },\n message\n );\n throw new Error(\n `${message} Verify GCP Secret Manager access (ADC/Workload Identity) and secret names/prefix, ` +\n `or set OLLAMA_URL for degraded self-hosted boot, or run 'gcloud auth application-default login' if local.`\n );\n }\n }\n\n // Warn on partial provider key loading — helps detect GCP issues early\n if (hasAnyLlmProviderKey && providerSecretsLoaded > 0 && providerSecretsLoaded < 3) {\n log.warn(\n { providerSecretsLoaded, total: PROVIDER_SECRETS.length },\n `⚠️ Only ${providerSecretsLoaded} provider secrets loaded — possible partial GCP failure`\n );\n }\n\n // Audio provider diagnostics\n const audioProviders = {\n DEEPGRAM_API_KEY: !!process.env.DEEPGRAM_API_KEY,\n CARTESIA_API_KEY: !!process.env.CARTESIA_API_KEY,\n ELEVENLABS_API_KEY: !!process.env.ELEVENLABS_API_KEY,\n OLLAMA_URL: process.env.OLLAMA_URL || '',\n SELF_HOSTED_STT_URL: process.env.SELF_HOSTED_STT_URL || '',\n LOCAL_NLLB_URL: process.env.LOCAL_NLLB_URL || '',\n };\n log.info({ audioProviders }, 'Audio provider env vars after secrets loading');\n\n log.info(\n {\n loaded,\n skipped,\n failed,\n sanitizedKeys,\n providerSecretsLoaded,\n overwrittenFromGcp,\n gcpAuthoritative,\n total: CRITICAL_SECRETS.length + PROVIDER_SECRETS.length,\n },\n '✅ Secrets loading complete'\n );\n\n if (failed > 0) {\n throw new Error(`Failed to load ${failed} required secrets`);\n }\n}\n\n/**\n * Get summary of secrets loaded.\n *\n * Diagnostic helper for admin endpoints (e.g. /v1/admin/providers/...) that\n * surfaces exactly which provider envVars are populated and from where, so\n * operators can verify SECRETS_PROVIDER_PRIMARY=gcp is actually authoritative\n * and not silently shadowed by stale env values.\n */\nexport function getSecretsLoadSummary(): {\n fromGCP: string[];\n fromEnv: string[];\n notLoaded: string[];\n} {\n const fromGCP: string[] = [];\n const fromEnv: string[] = [];\n const notLoaded: string[] = [];\n\n for (const status of providerKeyStatusMap.values()) {\n if (!status.loaded) {\n notLoaded.push(status.envVar);\n continue;\n }\n if (status.source === 'gcp') {\n fromGCP.push(status.envVar);\n } else if (status.source === 'env') {\n fromEnv.push(status.envVar);\n }\n }\n\n return { fromGCP, fromEnv, notLoaded };\n}\n"}, "c85a03cf46da149eddfd99f3c8c1c648d57cb866": {"t": 1785754621.0326009, "n": 63719, "h": "782d3a59d6196086", "c": "// Copyright (C) 2026 Ailin One, Inc.\n//\n// This file is part of Collective Intelligence Engine (ci).\n// Licensed under the GNU Affero General Public License v3.0 or later.\n// See LICENSE in the repository root, or <https://www.gnu.org/licenses/>.\n//\n// SPDX-License-Identifier: AGPL-3.0-or-later\n// Source: https://github.com/ailinone/collective-intelligence\n\n/**\n * Load Secrets into Environment\n * \n * This module loads critical secrets from GCP Secret Manager\n * and injects them into process.env BEFORE config validation.\n * \n * This allows the existing config system to work without refactoring,\n * while still using GCP Secret Manager as the source of truth.\n * \n * Call order:\n * 1. initializeSecretsManager()\n * 2. loadSecretsIntoEnv() ← This file\n * 3. validateConfig()\n */\n\nimport { logger } from '@/utils/logger';\nimport { getErrorMessage } from '@/utils/type-guards';\nimport { loadSecret } from './secrets-loader';\n\n/**\n * Critical secrets that must be loaded from GCP\n * These are injected into process.env for config compatibility\n */\ninterface CriticalSecret {\n envVar: string;\n secretKey: string;\n required: boolean;\n defaultValue?: string;\n}\n\nconst CRITICAL_SECRETS: CriticalSecret[] = [\n { envVar: 'DATABASE_URL', secretKey: 'database-url', required: true },\n { envVar: 'JWT_SECRET', secretKey: 'jwt-secret', required: true },\n { envVar: 'REDIS_PASSWORD', secretKey: 'redis-password', required: false, defaultValue: '' },\n { envVar: 'GCS_FILES_BUCKET', secretKey: 'gcs-files-bucket', required: false },\n { envVar: 'SMTP_PASS', secretKey: 'smtp-pass', required: false },\n { envVar: 'STRIPE_SECRET_KEY', secretKey: 'stripe-secret-key', required: false },\n { envVar: 'STRIPE_WEBHOOK_SECRET', secretKey: 'stripe-webhook-secret', required: false },\n { envVar: 'STRIPE_PUBLISHABLE_KEY', secretKey: 'stripe-publishable-key', required: false },\n // Phase 2c shadow ensemble bearer token. Optional because the coord-\n // stable wire is disabled by default in production\n // (CI_ENSEMBLE_COORDINATOR_ENABLED=false). When operator flips it on,\n // the GCP secret `coord-ensemble-token` MUST also be set with the\n // value to share with coord-serving's COORD_SERVING_AUTH_TOKEN.\n { envVar: 'CI_ENSEMBLE_COORDINATOR_TOKEN', secretKey: 'coord-ensemble-token', required: false },\n // Prometheus scrape bearer token for /metrics + /metrics/prompts\n // (metrics-route.ts `authorizeScrape`). Optional here — the route itself\n // is fail-closed when unset: it denies (403) in production and stays\n // open for local/dev scraping otherwise. Wired via CRITICAL_SECRETS\n // (not PROVIDER_SECRETS) because this is a general app credential, not\n // a provider API key.\n { envVar: 'PROMETHEUS_SCRAPE_TOKEN', secretKey: 'prometheus-scrape-token', required: false },\n // Sentry error-tracking DSN (utils/error-tracking.ts `initializeErrorTracking`).\n // Optional — the module itself no-ops with a warning log when unset, both\n // at API boot (index.ts) and worker boot (workers/queue-runner.ts), so\n // there is no fail-closed/fail-open behavior riding on this like the\n // Prometheus token above. Wired via CRITICAL_SECRETS (not PROVIDER_SECRETS)\n // because this is a general app credential, not a provider API key. No GCP\n // secret exists yet as of this commit — an operator must create one\n // (`ailin-sentry-dsn`) with a real DSN from Sentry's project settings\n // before this ever resolves to a non-empty value.\n { envVar: 'SENTRY_DSN', secretKey: 'sentry-dsn', required: false },\n];\n\n/**\n * Provider API keys (loaded from GCP if not in env)\n * CRITICAL: GCP provider automatically adds prefix 'ailin-' to secret keys\n * So 'openai-key' becomes 'ailin-openai-key' in GCP Secret Manager\n * These keys match the actual secret names in GCP (without the prefix, provider adds it)\n */\ninterface ProviderSecretBinding {\n envVar: string;\n secretKeys: readonly string[];\n}\n\nexport const PROVIDER_SECRETS: readonly ProviderSecretBinding[] = [\n { envVar: 'OPENAI_API_KEY', secretKeys: ['openai-key'] },\n { envVar: 'ANTHROPIC_API_KEY', secretKeys: ['anthropic-key'] },\n { envVar: 'GOOGLE_API_KEY', secretKeys: ['google-key'] },\n { envVar: 'DEEPSEEK_API_KEY', secretKeys: ['deepseek-key'] },\n { envVar: 'XAI_API_KEY', secretKeys: ['xai-key'] },\n { envVar: 'MISTRAL_API_KEY', secretKeys: ['mistral-key'] },\n { envVar: 'COHERE_API_KEY', secretKeys: ['cohere-key'] },\n { envVar: 'NVIDIA_API_KEY', secretKeys: ['nvidia-key', 'nvidia-api-key'] },\n { envVar: 'AIHUBMIX_API_KEY', secretKeys: ['aihubmix-key', 'aihubmix-api-key'] },\n { envVar: 'NOVITA_API_KEY', secretKeys: ['novita-key', 'novita-api-key'] },\n { envVar: 'MOONSHOT_API_KEY', secretKeys: ['moonshot-key', 'moonshot-api-key'] },\n { envVar: 'MINIMAX_API_KEY', secretKeys: ['minimax-key', 'minimax-api-key'] },\n { envVar: 'JINA_API_KEY', secretKeys: ['jina-key', 'jina-api-key'] },\n { envVar: 'GOOGLE_MAPS_API_KEY', secretKeys: ['google-maps-key', 'google-maps-api-key'] },\n { envVar: 'FRIENDLI_API_KEY', secretKeys: ['friendli-key', 'friendli-api-key'] },\n { envVar: 'FRIENDLI_TEAM_ID', secretKeys: ['friendli-team-id'] },\n { envVar: 'AIML_API_KEY', secretKeys: ['aiml-key', 'aiml-api-key'] },\n { envVar: 'IMAGEROUTER_API_KEY', secretKeys: ['imagerouter-key', 'imagerouter-api-key'] },\n { envVar: 'OPENROUTER_API_KEY', secretKeys: ['openrouter-key'] },\n { envVar: 'COMETAPI_API_KEY', secretKeys: ['cometapi-key', 'cometapi-api-key'] },\n { envVar: 'NANOGPT_API_KEY', secretKeys: ['nanogpt-key', 'nanogpt-api-key'] },\n { envVar: 'REQUESTY_API_KEY', secretKeys: ['requesty-key', 'requesty-api-key'] },\n { envVar: 'AI302_API_KEY', secretKeys: ['302-key', '302ai-key'] },\n { envVar: 'POE_API_KEY', secretKeys: ['poe-key', 'poe-api-key'] },\n { envVar: 'ROUTEWAY_API_KEY', secretKeys: ['routeway-key', 'routeway-api-key'] },\n // Hub providers support both key naming conventions for compatibility.\n { envVar: 'ORQAI_API_KEY', secretKeys: ['orqai-key', 'orqai-api-key'] },\n { envVar: 'ORQAI_MODEL_DENYLIST', secretKeys: ['orqai-model-denylist'] },\n { envVar: 'EDENAI_API_KEY', secretKeys: ['edenai-key', 'edenai-api-key'] },\n { envVar: 'HELICONEAI_API_KEY', secretKeys: ['heliconeai-key', 'heliconeai-api-key'] },\n { envVar: 'VERTEX_AI_API_KEY', secretKeys: ['vertex-key'] },\n { envVar: 'VERTEX_AI_PROJECT_ID', secretKeys: ['vertex-project-id'] },\n { envVar: 'QWEN_API_KEY', secretKeys: ['qwen-key-2'] },\n { envVar: 'ALIBABA_ACCESS_KEY_ID', secretKeys: ['alibaba-key-id'] },\n { envVar: 'ALIBABA_ACCESS_KEY_SECRET', secretKeys: ['alibaba-key-secret'] },\n { envVar: 'ERNIE_API_KEY', secretKeys: ['baidu-key'] },\n { envVar: 'ERNIE_SECRET_KEY', secretKeys: ['baidu-secret'] },\n { envVar: 'BAIDU_BASE_URL', secretKeys: ['baidu-base-url'] },\n { envVar: 'AWS_ACCESS_KEY_ID', secretKeys: ['aws-key-id'] },\n { envVar: 'AWS_SECRET_ACCESS_KEY', secretKeys: ['aws-secret'] },\n { envVar: 'AWS_BEARER_TOKEN_BEDROCK', secretKeys: ['aws-bearer-token'] },\n // Batch 7.1: Bedrock region + inference-profile ARN can now be set via\n // GCP Secret Manager. Useful for deploys that route Bedrock to a specific\n // region (e.g. us-west-2) or via cross-region inference profiles without\n // baking these into the docker image.\n { envVar: 'AWS_BEDROCK_REGION', secretKeys: ['aws-bedrock-region'] },\n { envVar: 'AWS_BEDROCK_INFERENCE_PROFILE_ARN', secretKeys: ['aws-bedrock-inference-profile-arn'] },\n // Batch 8.1: SageMaker endpoint routing. A deploy can point this adapter\n // at a specific deployed endpoint via a secret rather than baking the\n // endpoint name into the image. The region override lets the adapter\n // hit an endpoint in a different region from the general AWS_REGION\n // used by other services (SES, S3).\n { envVar: 'AWS_SAGEMAKER_REGION', secretKeys: ['aws-sagemaker-region'] },\n { envVar: 'AWS_SAGEMAKER_ENDPOINT_NAME', secretKeys: ['aws-sagemaker-endpoint-name'] },\n { envVar: 'AWS_SAGEMAKER_PAYLOAD_SCHEMA', secretKeys: ['aws-sagemaker-payload-schema'] },\n { envVar: 'AWS_SAGEMAKER_CUSTOM_ATTRIBUTES', secretKeys: ['aws-sagemaker-custom-attributes'] },\n { envVar: 'AZURE_OPENAI_API_KEY', secretKeys: ['azure-openai-api-key'] },\n { envVar: 'AZURE_OPENAI_ENDPOINT', secretKeys: ['azure-openai-endpoint'] },\n { envVar: 'AZURE_OPENAI_DEPLOYMENT', secretKeys: ['azure-openai-deployment'] },\n // Audio-first providers (STT, TTS, STS)\n { envVar: 'DEEPGRAM_API_KEY', secretKeys: ['deepgram-key'] },\n { envVar: 'CARTESIA_API_KEY', secretKeys: ['cartesia-key'] },\n { envVar: 'ELEVENLABS_API_KEY', secretKeys: ['elevenlabs-key'] },\n // Translation provider\n { envVar: 'PALABRAAI_CLIENT_ID', secretKeys: ['palabraai-id'] },\n { envVar: 'PALABRAAI_CLIENT_SECRET', secretKeys: ['palabraai-key'] },\n { envVar: 'OCI_TENANCY_ID', secretKeys: ['oci-tenancy-id'] },\n { envVar: 'OCI_USER_ID', secretKeys: ['oci-user-id'] },\n { envVar: 'OCI_FINGERPRINT', secretKeys: ['oci-fingerprint'] },\n { envVar: 'OCI_PRIVATE_KEY', secretKeys: ['oci-private-key'] },\n { envVar: 'OCI_REGION', secretKeys: ['oci-region'] },\n // 2026-04-22: New GCP secrets (5 providers). The GCP provider auto-prefixes\n // 'ailin-', so 'wandb-key' maps to ailin-wandb-key in Secret Manager.\n // Voyage lists both 'voyageai-key' (current upstream name) and 'voyage-key'\n // (conventional fallback) because the canonical spelling is still unstable.\n { envVar: 'WANDB_API_KEY', secretKeys: ['wandb-key', 'wandb-api-key'] },\n { envVar: 'VOYAGE_API_KEY', secretKeys: ['voyageai-key', 'voyage-key', 'voyage-api-key'] },\n { envVar: 'BYTEZ_API_KEY', secretKeys: ['bytez-key', 'bytez-api-key'] },\n { envVar: 'CEREBRAS_API_KEY', secretKeys: ['cerebras-key', 'cerebras-api-key'] },\n { envVar: 'FEATHERLESS_AI_API_KEY', secretKeys: ['featherless-key', 'featherless-api-key'] },\n // Batch 6 — enterprise hyperscaler gateways (2026-04-22).\n // Azure OpenAI already has API_KEY/ENDPOINT/DEPLOYMENT wired above;\n // these fill in the remaining URL-template parameters.\n { envVar: 'AZURE_OPENAI_RESOURCE_NAME', secretKeys: ['azure-openai-resource-name', 'azure-openai-resource'] },\n { envVar: 'AZURE_OPENAI_API_VERSION', secretKeys: ['azure-openai-api-version'] },\n // Gemini OAI-compat shares the API key with the native `google` provider.\n // Two env-var aliases: GEMINI_API_KEY (canonical upstream) and\n // GOOGLE_AI_STUDIO_API_KEY (legacy). The adapter reads either.\n //\n // 2026-04-23: `vertex-key` added as a fallback source because it was\n // verified working against generativelanguage.googleapis.com (HTTP 200,\n // 50 Gemini models) while `google-key` is suspended (403\n // CONSUMER_SUSPENDED). The Vertex/Gemini API-key split is mostly\n // historical — a Google Cloud API key scoped for Gemini works equally on\n // aistudio and on the generativelanguage surface, as long as the key's\n // GCP project has the API enabled.\n //\n // 2026-07-25: `vertex-key` was originally placed LAST on the (incorrect)\n // assumption that the loop below tries each candidate live and keeps the\n // first one that WORKS. It doesn't — it takes the first candidate that\n // merely EXISTS in Secret Manager (see the resolution loop a few hundred\n // lines down), so the suspended `google-key` always won and gemini-openai\n // stayed broken. Moved `vertex-key` first since it's the only one\n // confirmed live-working; the others remain as fallbacks if it's ever\n // rotated out.\n { envVar: 'GEMINI_API_KEY', secretKeys: ['vertex-key', 'gemini-key', 'google-ai-studio-key', 'google-key'] },\n // GitHub Models uses a GitHub PAT. Operators typically already have this\n // in env (it's the same PAT used for gh CLI); the GCP path is a fallback\n // for service accounts that need isolated tokens.\n { envVar: 'GITHUB_TOKEN', secretKeys: ['github-models-token', 'github-token', 'github-pat'] },\n // Databricks workspace token + config.\n { envVar: 'DATABRICKS_TOKEN', secretKeys: ['databricks-token', 'databricks-pat'] },\n { envVar: 'DATABRICKS_HOST', secretKeys: ['databricks-host'] },\n { envVar: 'DATABRICKS_SERVING_ENDPOINT', secretKeys: ['databricks-serving-endpoint'] },\n // Inworld AI — orphan closure 2026-04-22. Key is already base64-encoded\n // in GCP (132 chars) because Inworld uses Basic auth, not Bearer. The\n // adapter does NOT re-encode; it passes the secret verbatim into the\n // `Basic ` header. Never base64-encode this value at load time.\n { envVar: 'INWORLD_API_KEY', secretKeys: ['inworld-key', 'inworld-api-key'] },\n // ── Orphan-closure Lot B (2026-04-23) ──────────────────────────────────\n // Three providers closed in a single lot because they share identical\n // shape (thin OAI-compat wrappers) and were all already present in GCP\n // Secret Manager but not wired into env loading. Each tuple lists the\n // preferred secret key first and legacy-or-alternate names as fallbacks.\n { envVar: 'WRITER_API_KEY', secretKeys: ['writer-key', 'writer-api-key'] },\n { envVar: 'UPSTAGE_API_KEY', secretKeys: ['upstage-key', 'upstage-api-key'] },\n // Reka's canonical upstream name is \"reka\" but the key is stored in GCP\n // as `ailin-rekaai-key` (matching the catalog providerId, not the upstream\n // name). `reka-key` is kept as a fallback in case the secret is ever\n // renamed to match upstream naming later.\n { envVar: 'REKA_API_KEY', secretKeys: ['rekaai-key', 'reka-key', 'reka-api-key'] },\n // ── LOTE M (2026-04-23) — complement lot: 13 new env-var mappings. ─────\n // Every provider below landed a catalog row this lot and is classified\n // in CONSOLIDATION_MATRIX['credentials-missing'] (secret-absent sub-class\n // for 12, auth-incomplete for qianfan). Each mapping is wired NOW so the\n // moment an operator provisions the corresponding GCP secret, the key\n // propagates to process.env without a code change. The first element of\n // each secretKeys array is the preferred upstream-convention name; any\n // follow-up entries are fallbacks for historical or legacy aliases.\n { envVar: 'ARCEE_API_KEY', secretKeys: ['arcee-api-key', 'arcee-key'] },\n { envVar: 'ATLASCLOUD_API_KEY', secretKeys: ['atlascloud-key', 'atlascloud-api-key', 'atlas-cloud-key'] },\n { envVar: 'AVIAN_API_KEY', secretKeys: ['avian-key', 'avian-api-key', 'avian-io-key'] },\n // Qianfan (Baidu) v2 bce-v3 bearer. Distinct from the legacy v1 AK+SK\n // material in ERNIE_API_KEY / ERNIE_SECRET_KEY / BAIDU_BASE_URL above —\n // both mappings are intentionally kept so an operator can populate either\n // path (v1 OAuth or v2 bearer) depending on which runtime surface is\n // used. The canonical catalog row `qianfan` reads QIANFAN_API_KEY only.\n { envVar: 'QIANFAN_API_KEY', secretKeys: ['qianfan-key', 'qianfan-api-key', 'baidu-qianfan-key'] },\n { envVar: 'GMI_API_KEY', secretKeys: ['gmi-key', 'gmi-api-key', 'gmicloud-key'] },\n { envVar: 'INFERMATIC_API_KEY', secretKeys: ['infermatic-api-key', 'infermatic-key', 'totalgpt-key'] },\n { envVar: 'INFLECTION_API_KEY', secretKeys: ['inflection-key', 'inflection-api-key', 'inflection-ai-key'] },\n // Mancer (tagged contentPolicyClass=uncensored in catalog; per the universal\n // \"habilitado e nunca censurado\" policy from Phase 4b, 2026-04-28, the\n // catalog row is fully admitted to routing — the tag is informational so\n // downstream surfaces can opt to filter it from a moderated default list).\n { envVar: 'MANCER_API_KEY', secretKeys: ['mancer-key', 'mancer-api-key'] },\n // Phala / RedPill TEE — upstream docs use REDPILL_API_KEY but we wire\n // PHALA_API_KEY as the canonical env var (see apiKeyEnvVarOverrideReason\n // in catalog row `phala`). Legacy `redpill-*` secret names are accepted\n // as fallbacks so an operator storing under either convention works.\n { envVar: 'PHALA_API_KEY', secretKeys: ['phala-key', 'phala-api-key', 'redpill-key', 'redpill-api-key'] },\n { envVar: 'RELACE_API_KEY', secretKeys: ['relace-key', 'relace-api-key'] },\n { envVar: 'SILICONFLOW_API_KEY', secretKeys: ['siliconflow-api-key', 'siliconflow-key', 'silicon-flow-key'] },\n { envVar: 'STEPFUN_API_KEY', secretKeys: ['stepfun-api-key', 'stepfun-key', 'step-key'] },\n // Venice (tagged contentPolicyClass=uncensored in catalog). Same Phase-4b\n // treatment as Mancer — admitted, never censored, with the tag as the\n // audit trail for the policy choice.\n { envVar: 'VENICE_API_KEY', secretKeys: ['venice-key', 'venice-api-key', 'venice-ai-key'] },\n // ── Sublote E1 (2026-04-24) — runtime-wiring closure for D1 matrix ─────\n // Sublote D1 promoted 10 providers to CONSOLIDATION_MATRIX['live-validation']\n // based on direct-probe evidence (via gcloud secrets versions access,\n // bypassing the runtime env loader). 7 of those had no GCP→ENV tuple here,\n // so the catalog-loader could never surface `process.env.<X>_API_KEY` at\n // boot — meaning live-validated providers were silently \"not runnable\".\n // Wired pre-emptively: the moment an operator provisions the GCP secret\n // AND the container restarts, propagation happens with zero code change.\n // The 3 already-mapped providers (heliconeai, github-models, infermatic)\n // are NOT duplicated — they appear above under their respective batches.\n { envVar: 'GROQ_API_KEY', secretKeys: ['groq-api-key', 'groq-key'] },\n { envVar: 'DEEPINFRA_API_KEY', secretKeys: ['deepinfra-api-key', 'deepinfra-key'] },\n // Hugging Face: upstream canonical env var is `HF_TOKEN` (not\n // HUGGINGFACE_API_KEY); the catalog row's apiKeyEnvVar matches. Primary\n // GCP secret name follows the upstream convention; legacy huggingface-*\n // aliases accepted for operators who stored it under the provider slug.\n { envVar: 'HF_TOKEN', secretKeys: ['huggingface-api-key', 'huggingface-token', 'hf-token', 'huggingface-key'] },\n // Cloudflare Workers AI — requires BOTH api-token AND account-id. The\n // CloudflareWorkersAIAdapter factory (default-adapter-factories.ts line\n // 229-246) reads CLOUDFLARE_ACCOUNT_ID from env and substitutes it into\n // the `{account_id}` URL template. Without the account-id side-car the\n // adapter ships the literal template to the wire → 404 on every call.\n // Side-car is wired here as a separate tuple because it's not an auth\n // credential — it's a URL parameter — so it doesn't belong in\n // ENV_VAR_TO_PROVIDER or LLM_PROVIDER_ENV_VARS below.\n { envVar: 'CLOUDFLARE_API_TOKEN', secretKeys: ['cloudflare-workers-ai-api-key', 'cloudflare-workers-ai-token', 'cloudflare-api-token', 'cloudflare-workers-ai-key'] },\n { envVar: 'CLOUDFLARE_ACCOUNT_ID', secretKeys: ['cloudflare-workers-ai-id', 'cloudflare-account-id', 'cloudflare-workers-ai-account-id'] },\n { envVar: 'PERPLEXITY_API_KEY', secretKeys: ['perplexity-api-key', 'perplexity-key'] },\n // Fireworks AI — note the catalog providerId is `fireworks-ai` (hyphenated)\n // but upstream brand/docs spell it \"Fireworks\" (no suffix). Four fallbacks\n // cover both conventions + api-key suffix variant.\n { envVar: 'FIREWORKS_AI_API_KEY', secretKeys: ['fireworks-ai-api-key', 'fireworks-ai-key', 'fireworks-key', 'fireworks-api-key'] },\n { envVar: 'SAMBANOVA_API_KEY', secretKeys: ['sambanova-api-key', 'sambanova-key'] },\n // Replicate — flagged by the sublote-e1-runtime-wiring invariant as a\n // pre-existing gap. `replicate` has been in CONSOLIDATION_MATRIX\n // ['live-validation'] since before D1, with adapterClass 'ReplicateAdapter'\n // and apiKeyEnvVar 'REPLICATE_API_KEY', but no tuple was ever added here.\n // Even though integrationMode is 'execution-only' (no /v1/models\n // discovery), the adapter still requires the env var at /predictions\n // call time. Wired here for closure; the GCP secret name follows the\n // provider-slug convention with optional `-api-key` fallback.\n { envVar: 'REPLICATE_API_KEY', secretKeys: ['replicate-key', 'replicate-api-key'] },\n // ── LOTE N (2026-04-27) — catalog↔loader closure for the remaining 22 ─\n // apiKeyEnvVar entries that had catalog rows but NO PROVIDER_SECRETS\n // tuple. Surfaced by `catalog-loader-wiring-completeness.test.ts` (J6\n // invariant). All 22 are real provider rows with adapter classes (or\n // generic OAI-compat hub registration); they were classified in\n // CONSOLIDATION_MATRIX as `credentials-missing` or `disabled-by-default`,\n // and their absence here meant that even after an operator provisioned\n // the GCP secret, `process.env.<X>_API_KEY` would stay empty at boot.\n //\n // Pattern: preferred secret name first (provider-slug `-key`), then\n // upstream-convention `-api-key` fallback, then any historical aliases.\n // The provider-side `aliases` array in providers.catalog.ts is NOT\n // duplicated here — secret-key aliases are about HOW the operator\n // stored the credential, NOT how callers reach the provider.\n //\n // Image/video-only providers (bfl, recraft, runwayml, topaz) are wired\n // here too but are intentionally absent from LLM_PROVIDER_ENV_VARS below\n // because their presence does NOT count toward the \"at least one LLM\n // key present\" boot-mode gate.\n { envVar: 'TOGETHERAI_API_KEY', secretKeys: ['togetherai-api-key', 'togetherai-key', 'together-key', 'together-api-key'] },\n { envVar: 'ANYSCALE_API_KEY', secretKeys: ['anyscale-api-key', 'anyscale-key'] },\n { envVar: 'HYPERBOLIC_API_KEY', secretKeys: ['hyperbolic-api-key', 'hyperbolic-key'] },\n { envVar: 'CHUTES_API_KEY', secretKeys: ['chutes-api-key', 'chutes-key', 'chutes-ai-key'] },\n { envVar: 'NSCALE_API_KEY', secretKeys: ['nscale-key', 'nscale-api-key'] },\n { envVar: 'NEBIUS_API_KEY', secretKeys: ['nebius-key', 'nebius-api-key'] },\n // Lambda Cloud canonical provider slug is `lambda-ai`; brand is\n // \"Lambda\" (without -ai). Both spellings tried as fallbacks.\n { envVar: 'LAMBDA_AI_API_KEY', secretKeys: ['lambda-ai-key', 'lambda-key', 'lambda-cloud-key', 'lambda-ai-api-key'] },\n { envVar: 'SCALEWAY_API_KEY', secretKeys: ['scaleway-key', 'scaleway-api-key'] },\n { envVar: 'SYNTHETIC_API_KEY', secretKeys: ['synthetic-key', 'synthetic-api-key', 'synthetic-ai-key'] },\n { envVar: 'MORPH_API_KEY', secretKeys: ['morph-key', 'morph-api-key', 'morph-llm-key'] },\n // Z.ai (catalog providerId = `zai`). Upstream domain z.ai resolves to\n // bigmodel.cn — the GLM family operator. Legacy spelling z-ai accepted.\n { envVar: 'ZAI_API_KEY', secretKeys: ['zai-key', 'zai-api-key', 'z-ai-key', 'glm-key'] },\n { envVar: 'XIAOMI_MIMO_API_KEY', secretKeys: ['xiaomi-mimo-key', 'mimo-key', 'xiaomi-mimo-api-key'] },\n // v0 (Vercel) — code-gen LLM. The catalog providerId is `v0` (one\n // character) so the env var name is `V0_API_KEY` not `V0AI_API_KEY`.\n { envVar: 'V0_API_KEY', secretKeys: ['v0-key', 'v0-api-key', 'vercel-v0-key'] },\n // Vercel AI Gateway — DIFFERENT from v0 above (gateway over many LLMs).\n // The canonical secret name follows the catalog providerId.\n { envVar: 'VERCEL_AI_GATEWAY_API_KEY', secretKeys: ['vercel-ai-gateway-key', 'vercel-ai-gateway-api-key', 'vercel-gateway-key'] },\n // Volcano (Volcengine — ByteDance). Canonical slug `volcano`; brand\n // includes Volcengine + Doubao + Ark. Multiple aliases accepted.\n { envVar: 'VOLCANO_API_KEY', secretKeys: ['volcano-key', 'volcano-api-key', 'volcengine-key', 'doubao-key', 'ark-key'] },\n // IBM watsonx — note the env var is `WATSONX_APIKEY` (no underscore\n // between API and KEY) to match IBM's canonical SDK convention.\n // Side-car env vars (WATSONX_PROJECT_ID, WATSONX_URL) are wired below\n // because they're documented in the catalog row's `extraEnvVars` and\n // adapter requires them at runtime.\n { envVar: 'WATSONX_APIKEY', secretKeys: ['watsonx-apikey', 'watsonx-key', 'watsonx-api-key', 'ibm-watsonx-key'] },\n { envVar: 'WATSONX_PROJECT_ID', secretKeys: ['watsonx-project-id', 'ibm-watsonx-project-id'] },\n { envVar: 'WATSONX_URL', secretKeys: ['watsonx-url', 'ibm-watsonx-url'] },\n // Snowflake Cortex — uses Personal Access Token (PAT), not an API key.\n // Multiple side-cars required by the SnowflakeCortexAdapter:\n // SNOWFLAKE_ACCOUNT (orgname-accountname), SNOWFLAKE_USER (key-pair\n // auth user), SNOWFLAKE_BASE_URL (account-specific host). Side-cars\n // wired here so a single GCP provisioning round materializes the full\n // tuple at boot.\n { envVar: 'SNOWFLAKE_PAT', secretKeys: ['snowflake-pat', 'snowflake-token', 'snowflake-key'] },\n { envVar: 'SNOWFLAKE_ACCOUNT', secretKeys: ['snowflake-account'] },\n { envVar: 'SNOWFLAKE_USER', secretKeys: ['snowflake-user', 'snowflake-username'] },\n { envVar: 'SNOWFLAKE_BASE_URL', secretKeys: ['snowflake-base-url', 'snowflake-url'] },\n // SAP AI Core — OAuth2 client_credentials flow. Catalog providerId is\n // `sap`. The apiKeyEnvVar is `SAP_AI_CORE_CLIENT_ID` (the OAuth client\n // id); the matching client_secret + token URL + base URL are all\n // documented in the catalog row's `extraEnvVars` and required by the\n // SapAiCoreAdapter. Wiring all four together so a single provisioning\n // round closes the loop.\n { envVar: 'SAP_AI_CORE_CLIENT_ID', secretKeys: ['sap-ai-core-client-id', 'sap-client-id'] },\n { envVar: 'SAP_AI_CORE_CLIENT_SECRET', secretKeys: ['sap-ai-core-client-secret', 'sap-client-secret'] },\n { envVar: 'SAP_AI_CORE_AUTH_URL', secretKeys: ['sap-ai-core-auth-url', 'sap-auth-url'] },\n { envVar: 'SAP_AI_CORE_BASE_URL', secretKeys: ['sap-ai-core-base-url', 'sap-base-url'] },\n // W&B Inference side-car — WANDB_API_KEY (auth) is already wired in\n // Batch 7 above; WANDB_PROJECT is the project slug header required on\n // every chat call. Wiring here closes the side-car gap.\n { envVar: 'WANDB_PROJECT', secretKeys: ['wandb-project', 'wandb-project-slug'] },\n // Image-only providers (no LLM gate participation, see comment below).\n // bfl auth header is `x-key` (not Bearer) — see catalog `authHeaderName`.\n { envVar: 'BFL_API_KEY', secretKeys: ['bfl-key', 'bfl-api-key', 'flux-key', 'black-forest-labs-key'] },\n { envVar: 'RECRAFT_API_KEY', secretKeys: ['recraft-key', 'recraft-api-key'] },\n { envVar: 'RUNWAYML_API_KEY', secretKeys: ['runwayml-key', 'runwayml-api-key', 'runway-key'] },\n { envVar: 'TOPAZ_API_KEY', secretKeys: ['topaz-key', 'topaz-api-key', 'topaz-labs-key'] },\n // ── LOTE O (2026-07-10) — Apertis + Inception Labs onboarding ─────────\n // Operator pre-provisioned both GCP secrets ahead of the catalog rows\n // (ailin-apertis-key, ailin-inception-key). secretKeys match exactly —\n // the GCP provider auto-prepends the `ailin-` prefix (see file header).\n { envVar: 'APERTIS_API_KEY', secretKeys: ['apertis-key', 'apertis-api-key'] },\n { envVar: 'INCEPTION_API_KEY', secretKeys: ['inception-key', 'inception-api-key'] },\n // ── LOTE P (2026-07-11) — EmpirioLabs AI, live-validated after gate ────\n { envVar: 'EMPIRIOLABS_API_KEY', secretKeys: ['empiriolabs-key', 'empiriolabs-api-key'] },\n // ── LOTE Q (2026-07-12) — Concentrate AI ───────────────────────────────\n { envVar: 'CONCENTRATE_API_KEY', secretKeys: ['concentrate-key', 'concentrate-api-key'] },\n // ── LOTE R (2026-07-13) — FastRouter ────────────────────────────────────\n { envVar: 'FASTROUTER_API_KEY', secretKeys: ['fastrouter-key', 'fastrouter-api-key'] },\n // ── LOTE S (2026-07-13) — Perplexity Agent API ──────────────────────────\n // Same physical credential as PERPLEXITY_API_KEY above (one Perplexity\n // account, two API surfaces) — deliberately sources from the SAME GCP\n // secret names, not a new one.\n { envVar: 'PERPLEXITY_AGENT_API_KEY', secretKeys: ['perplexity-api-key', 'perplexity-key'] },\n // ── LOTE T (2026-07-13) — Ailin gateway (self-referential meta-provider) ──\n // No GCP secret provisioned yet — inert until an operator sets AILIN_API_KEY\n // for a self-hosted deployment. secretKeys follow the same naming\n // convention as the rest of this file in case one is provisioned later.\n { envVar: 'AILIN_API_KEY', secretKeys: ['ailin-key', 'ailin-api-key', 'gateway-key'] },\n // ── LOTE U (2026-07-29) — Sakana AI (Fugu), chat-capable ────────────────\n // 'sakana-ai-key' listed first: it's the actual GCP secret name\n // (`ailin-sakana-ai-key`, prefix auto-added) confirmed live this lot.\n { envVar: 'SAKANA_AI_API_KEY', secretKeys: ['sakana-ai-key', 'sakana-key'] },\n] as const;\n\n// ─── Provider Key Status Tracking ──────────────────────────────────────────\n// Tracks which provider API keys were successfully loaded, from which source,\n// and when. Used by the Self-Healing Discovery (L1) to know when to retry\n// discovery for native providers whose keys arrived late (GCP delay/failure).\n\nexport interface ProviderKeyStatus {\n loaded: boolean;\n source: 'gcp' | 'env' | 'none';\n timestamp: Date;\n envVar: string;\n}\n\n/**\n * Module-level state: populated during loadSecretsIntoEnv() for each provider.\n * Key = normalized provider name (lowercase, e.g., 'openai', 'anthropic').\n */\nconst providerKeyStatusMap = new Map<string, ProviderKeyStatus>();\n\n/**\n * Mapping from env var name to normalized provider name.\n * Allows the discovery service to map env vars to provider sources.\n */\nconst ENV_VAR_TO_PROVIDER: Record<string, string> = {\n // Native API providers\n OPENAI_API_KEY: 'openai',\n ANTHROPIC_API_KEY: 'anthropic',\n GOOGLE_API_KEY: 'google',\n DEEPSEEK_API_KEY: 'deepseek',\n XAI_API_KEY: 'xai',\n MISTRAL_API_KEY: 'mistral',\n COHERE_API_KEY: 'cohere',\n JINA_API_KEY: 'jina',\n // Cloud hubs — 2026-04-23: use canonical providerId 'nvidia' (legacy\n // alias 'nvidia-hub' was absorbed into the nvidia catalog row per Lot B;\n // discovery/status consumers read providerKeyStatus by canonical id).\n NVIDIA_API_KEY: 'nvidia',\n AIHUBMIX_API_KEY: 'aihubmix',\n NOVITA_API_KEY: 'novita',\n MOONSHOT_API_KEY: 'moonshot',\n MINIMAX_API_KEY: 'minimax',\n FRIENDLI_API_KEY: 'friendli',\n AIML_API_KEY: 'aiml',\n IMAGEROUTER_API_KEY: 'imagerouter',\n COMETAPI_API_KEY: 'cometapi',\n NANOGPT_API_KEY: 'nanogpt',\n EDENAI_API_KEY: 'edenai',\n HELICONEAI_API_KEY: 'heliconeai',\n POE_API_KEY: 'poe',\n ROUTEWAY_API_KEY: 'routeway',\n REQUESTY_API_KEY: 'requesty',\n // Post-migration canonical providerId is `ai302` (see providers.catalog.ts\n // and the residue-closure note in provider-registry.ts). Legacy `302ai`\n // survives as a catalog alias for pre-migration user configs.\n AI302_API_KEY: 'ai302',\n ORQAI_API_KEY: 'orqai',\n // Routers/aggregators\n OPENROUTER_API_KEY: 'openrouter',\n // Cloud providers\n VERTEX_AI_API_KEY: 'vertex-ai',\n AWS_ACCESS_KEY_ID: 'aws-bedrock',\n AWS_BEARER_TOKEN_BEDROCK: 'aws-bedrock',\n // SageMaker uses the same shared AWS creds as Bedrock, so `AWS_ACCESS_KEY_ID`\n // keeps mapping to `aws-bedrock` (first-listed wins) — but the endpoint-\n // specific env var is only meaningful for SageMaker. When operators wire\n // this, Self-Healing Discovery will attribute the key presence to the\n // SageMaker provider specifically.\n AWS_SAGEMAKER_ENDPOINT_NAME: 'aws-sagemaker',\n AZURE_OPENAI_API_KEY: 'azure-openai',\n // Regional/specialized\n QWEN_API_KEY: 'qwen',\n ERNIE_API_KEY: 'ernie',\n // Audio providers\n DEEPGRAM_API_KEY: 'deepgram',\n CARTESIA_API_KEY: 'cartesia',\n ELEVENLABS_API_KEY: 'elevenlabs',\n // Inference + embedding providers newly onboarded 2026-04-22\n WANDB_API_KEY: 'wandb',\n VOYAGE_API_KEY: 'voyage',\n BYTEZ_API_KEY: 'bytez',\n CEREBRAS_API_KEY: 'cerebras',\n FEATHERLESS_AI_API_KEY: 'featherless-ai', // canonical catalog id (2026-04-23 drift fix)\n // Batch 6 — enterprise hyperscaler gateways (2026-04-22). Gemini OAI-compat\n // is registered as a SEPARATE provider from the native `google` entry so\n // the self-healing discovery service can track them independently (they\n // have different failure modes — native uses @google/generative-ai SDK\n // with its own error surfaces; OAI-compat uses standard fetch).\n GEMINI_API_KEY: 'gemini-openai',\n GITHUB_TOKEN: 'github-models',\n DATABRICKS_TOKEN: 'databricks',\n // Orphan closure (2026-04-22) — Inworld now has a catalog row + adapter\n // factory. Mapping the env var to the provider slug lets Self-Healing\n // Discovery attribute key presence to the correct provider.\n INWORLD_API_KEY: 'inworld',\n // Orphan-closure Lot B (2026-04-23). Slug = catalog providerId. Writer\n // and Upstage are self-evident; Reka's slug is `rekaai` (the catalog\n // row's canonical providerId — aliases 'reka' + 'reka-ai' resolve to it).\n WRITER_API_KEY: 'writer',\n UPSTAGE_API_KEY: 'upstage',\n REKA_API_KEY: 'rekaai',\n // ── LOTE M (2026-04-23) — complement lot, 13 providers ──────────────\n // Slug = catalog providerId (NOT the upstream brand for phala/qianfan\n // which intentionally diverge — see catalog rows for rationale):\n // phala — canonical slug; upstream brand is \"RedPill\"\n // qianfan — canonical slug; upstream brand is \"Baidu Qianfan / ERNIE\"\n // gmi — canonical slug; upstream brand is \"GMICloud\"\n ARCEE_API_KEY: 'arcee',\n ATLASCLOUD_API_KEY: 'atlascloud',\n AVIAN_API_KEY: 'avian',\n QIANFAN_API_KEY: 'qianfan',\n GMI_API_KEY: 'gmi',\n INFERMATIC_API_KEY: 'infermatic',\n INFLECTION_API_KEY: 'inflection',\n MANCER_API_KEY: 'mancer',\n PHALA_API_KEY: 'phala',\n RELACE_API_KEY: 'relace',\n SILICONFLOW_API_KEY: 'siliconflow',\n STEPFUN_API_KEY: 'stepfun',\n VENICE_API_KEY: 'venice',\n // ── Sublote E1 (2026-04-24) — runtime-wiring closure for D1 matrix ───\n // 7 provider slugs whose env vars gained GCP→ENV tuples in this lot.\n // Slug = catalog providerId exactly (so Self-Healing Discovery can\n // attribute key-loaded events to the right provider). CLOUDFLARE_ACCOUNT_ID\n // is intentionally absent — that's a URL parameter, not an auth credential,\n // so mapping it to a provider slug would be semantically wrong (the same\n // account-id could conceivably scope multiple Cloudflare providers).\n GROQ_API_KEY: 'groq',\n DEEPINFRA_API_KEY: 'deepinfra',\n HF_TOKEN: 'huggingface',\n CLOUDFLARE_API_TOKEN: 'cloudflare-workers-ai',\n PERPLEXITY_API_KEY: 'perplexity',\n FIREWORKS_AI_API_KEY: 'fireworks-ai',\n SAMBANOVA_API_KEY: 'sambanova',\n // Replicate — pre-existing live-validation gap closed by sublote E1.\n // See the matching comment on the PROVIDER_SECRETS tuple above.\n REPLICATE_API_KEY: 'replicate',\n // ── LOTE N (2026-04-27) — catalog↔loader closure (22 new env vars) ──\n // Every entry below maps an env var to its canonical providerId so the\n // L1 Self-Healing Discovery Service can attribute key-loaded events\n // when the corresponding GCP secret is provisioned. Slugs match the\n // catalog providerId exactly (NOT upstream brand) — see catalog rows\n // for cases where they diverge (e.g. `sap` not `sap-ai-core`, `v0` not\n // `vercel-v0`, `volcano` not `volcengine`). Side-car env vars\n // (WATSONX_PROJECT_ID, SNOWFLAKE_ACCOUNT, SAP_AI_CORE_CLIENT_SECRET,\n // etc.) are NOT mapped here — they're auth or URL parameters, not\n // credentials, so attributing them to a provider would conflate\n // credential-presence with side-car-completeness in discovery telemetry.\n TOGETHERAI_API_KEY: 'togetherai',\n ANYSCALE_API_KEY: 'anyscale',\n HYPERBOLIC_API_KEY: 'hyperbolic',\n CHUTES_API_KEY: 'chutes',\n NSCALE_API_KEY: 'nscale',\n NEBIUS_API_KEY: 'nebius',\n LAMBDA_AI_API_KEY: 'lambda-ai',\n SCALEWAY_API_KEY: 'scaleway',\n SYNTHETIC_API_KEY: 'synthetic',\n MORPH_API_KEY: 'morph',\n ZAI_API_KEY: 'zai',\n XIAOMI_MIMO_API_KEY: 'xiaomi-mimo',\n V0_API_KEY: 'v0',\n VERCEL_AI_GATEWAY_API_KEY: 'vercel-ai-gateway',\n VOLCANO_API_KEY: 'volcano',\n WATSONX_APIKEY: 'watsonx',\n SNOWFLAKE_PAT: 'snowflake',\n SAP_AI_CORE_CLIENT_ID: 'sap',\n // Image / video-only providers. Still mapped to a slug so Self-Healing\n // Discovery can record key-presence events — but their env vars do NOT\n // appear in LLM_PROVIDER_ENV_VARS (image surfaces don't satisfy the\n // \"at least one LLM key\" boot gate).\n BFL_API_KEY: 'bfl',\n RECRAFT_API_KEY: 'recraft',\n RUNWAYML_API_KEY: 'runwayml',\n TOPAZ_API_KEY: 'topaz',\n // ── LOTE O (2026-07-10) ────────────────────────────────────────────────\n APERTIS_API_KEY: 'apertis',\n INCEPTION_API_KEY: 'inception',\n // ── LOTE P (2026-07-11) ────────────────────────────────────────────────\n EMPIRIOLABS_API_KEY: 'empiriolabs',\n // ── LOTE Q (2026-07-12) ────────────────────────────────────────────────\n CONCENTRATE_API_KEY: 'concentrate',\n // ── LOTE R (2026-07-13) ────────────────────────────────────────────────\n FASTROUTER_API_KEY: 'fastrouter',\n // ── LOTE S (2026-07-13) ────────────────────────────────────────────────\n PERPLEXITY_AGENT_API_KEY: 'perplexity-agent',\n // ── LOTE T (2026-07-13) ────────────────────────────────────────────────\n AILIN_API_KEY: 'ailin',\n // ── LOTE U (2026-07-29) ────────────────────────────────────────────────\n SAKANA_AI_API_KEY: 'sakana-ai',\n};\n\n/**\n * Returns the status of all provider API keys.\n * Used by Self-Healing Discovery to determine which native sources can run.\n */\nexport function getProviderKeyStatus(): ReadonlyMap<string, ProviderKeyStatus> {\n return providerKeyStatusMap;\n}\n\n/**\n * Returns provider names that have valid (non-empty, non-mock) API keys loaded.\n */\nexport function getLoadedProviderNames(): string[] {\n return Array.from(providerKeyStatusMap.entries())\n .filter(([, status]) => status.loaded)\n .map(([name]) => name);\n}\n\n/**\n * Check if a specific provider has a loaded API key.\n */\nexport function isProviderKeyLoaded(providerName: string): boolean {\n return providerKeyStatusMap.get(providerName.toLowerCase())?.loaded ?? false;\n}\n\nconst LLM_PROVIDER_ENV_VARS = [\n 'OPENAI_API_KEY',\n 'ANTHROPIC_API_KEY',\n 'GOOGLE_API_KEY',\n 'DEEPSEEK_API_KEY',\n 'XAI_API_KEY',\n 'MISTRAL_API_KEY',\n 'COHERE_API_KEY',\n 'NVIDIA_API_KEY',\n 'AIHUBMIX_API_KEY',\n 'NOVITA_API_KEY',\n 'MOONSHOT_API_KEY',\n 'MINIMAX_API_KEY',\n 'JINA_API_KEY',\n 'FRIENDLI_API_KEY',\n 'AIML_API_KEY',\n 'IMAGEROUTER_API_KEY',\n 'OPENROUTER_API_KEY',\n 'ORQAI_API_KEY',\n 'EDENAI_API_KEY',\n 'HELICONEAI_API_KEY',\n 'VERTEX_AI_API_KEY',\n 'AWS_BEARER_TOKEN_BEDROCK',\n 'AZURE_OPENAI_API_KEY',\n 'QWEN_API_KEY',\n 'ERNIE_API_KEY',\n 'COMETAPI_API_KEY',\n 'NANOGPT_API_KEY',\n 'REQUESTY_API_KEY',\n 'AI302_API_KEY',\n 'POE_API_KEY',\n 'ROUTEWAY_API_KEY',\n // Added 2026-04-22 — these keys now count toward the \"at least one LLM key\n // present\" gate that avoids DEGRADED_SELF_HOSTED mode at boot.\n 'WANDB_API_KEY',\n 'VOYAGE_API_KEY',\n 'CEREBRAS_API_KEY',\n 'FEATHERLESS_AI_API_KEY',\n // Batch 4 landed BYTEZ catalog row + adapter, so BYTEZ_API_KEY now counts\n // toward the LLM-key-present gate.\n 'BYTEZ_API_KEY',\n // Batch 6 — enterprise hyperscaler gateways (2026-04-22). These keys now\n // count toward the \"at least one LLM key present\" gate; all four have\n // landed catalog rows + adapter classes in this batch.\n 'GEMINI_API_KEY',\n 'GITHUB_TOKEN',\n 'DATABRICKS_TOKEN',\n // Batch 8.1 — AWS SageMaker. Endpoint name is the meaningful trigger:\n // AWS creds alone are ambiguous (could be Bedrock or S3), but an\n // endpoint name implies \"SageMaker is provisioned here\".\n 'AWS_SAGEMAKER_ENDPOINT_NAME',\n // Orphan closure (2026-04-22) — Inworld AI now counts toward the\n // \"at least one LLM key present\" gate now that the catalog row +\n // adapter factory are wired.\n 'INWORLD_API_KEY',\n // Orphan-closure Lot B (2026-04-23). All three are fully canonical now.\n 'WRITER_API_KEY',\n 'UPSTAGE_API_KEY',\n 'REKA_API_KEY',\n // ── LOTE M (2026-04-23) complement lot ──────────────────────────────\n // Each of the 13 keys below counts toward the \"at least one LLM key\n // present\" gate, the same way all catalog-wired keys do. Note that\n // mancer and venice carry contentPolicyClass=uncensored (2026-04-28\n // Phase 4b — the prior denyByDefault gate was removed per the universal\n // \"habilitado e nunca censurado\" policy); their keys count toward the\n // gate exactly like any other LLM key — surface-level filtering is now\n // a downstream concern, not a routing one.\n // qianfan here is QIANFAN_API_KEY (v2 bce-v3 bearer); the legacy v1\n // ERNIE_API_KEY was already listed above this marker.\n 'ARCEE_API_KEY',\n 'ATLASCLOUD_API_KEY',\n 'AVIAN_API_KEY',\n 'QIANFAN_API_KEY',\n 'GMI_API_KEY',\n 'INFERMATIC_API_KEY',\n 'INFLECTION_API_KEY',\n 'MANCER_API_KEY',\n 'PHALA_API_KEY',\n 'RELACE_API_KEY',\n 'SILICONFLOW_API_KEY',\n 'STEPFUN_API_KEY',\n 'VENICE_API_KEY',\n // ── Sublote E1 (2026-04-24) — runtime-wiring closure for D1 matrix ───\n // These 7 providers landed in CONSOLIDATION_MATRIX['live-validation']\n // during Sublote D1 but had no GCP→ENV tuple above → their keys could\n // not count toward the \"at least one LLM key present\" gate even when\n // GCP Secret Manager held the key. Listed here so a restart after the\n // operator's provisioning cycle propagates the key-present signal to\n // the DEGRADED_SELF_HOSTED avoidance logic. CLOUDFLARE_ACCOUNT_ID is\n // intentionally absent — it's a URL parameter, not an LLM credential.\n 'GROQ_API_KEY',\n 'DEEPINFRA_API_KEY',\n 'HF_TOKEN',\n 'CLOUDFLARE_API_TOKEN',\n 'PERPLEXITY_API_KEY',\n 'FIREWORKS_AI_API_KEY',\n 'SAMBANOVA_API_KEY',\n // Replicate — pre-existing live-validation gap closed by sublote E1.\n // See the matching comments on the PROVIDER_SECRETS tuple and\n // ENV_VAR_TO_PROVIDER entry above.\n 'REPLICATE_API_KEY',\n // ── LOTE N (2026-04-27) — catalog↔loader closure ─────────────────────\n // 18 LLM-class providers added to the boot-mode gate. The 4 image/\n // video-only providers (bfl, recraft, runwayml, topaz) are\n // intentionally OMITTED — their key presence does not signal the\n // server has any text-generation capability, so they should not\n // satisfy the \"at least one LLM key present\" check.\n 'TOGETHERAI_API_KEY',\n 'ANYSCALE_API_KEY',\n 'HYPERBOLIC_API_KEY',\n 'CHUTES_API_KEY',\n 'NSCALE_API_KEY',\n 'NEBIUS_API_KEY',\n 'LAMBDA_AI_API_KEY',\n 'SCALEWAY_API_KEY',\n 'SYNTHETIC_API_KEY',\n 'MORPH_API_KEY',\n 'ZAI_API_KEY',\n 'XIAOMI_MIMO_API_KEY',\n 'V0_API_KEY',\n 'VERCEL_AI_GATEWAY_API_KEY',\n 'VOLCANO_API_KEY',\n 'WATSONX_APIKEY',\n 'SNOWFLAKE_PAT',\n 'SAP_AI_CORE_CLIENT_ID',\n // ── LOTE O (2026-07-10) — Apertis + Inception Labs, both chat-capable ──\n 'APERTIS_API_KEY',\n 'INCEPTION_API_KEY',\n // ── LOTE P (2026-07-11) — EmpirioLabs AI, chat-capable ─────────────────\n 'EMPIRIOLABS_API_KEY',\n // ── LOTE Q (2026-07-12) — Concentrate AI, chat-capable ─────────────────\n 'CONCENTRATE_API_KEY',\n // ── LOTE R (2026-07-13) — FastRouter, chat-capable ─────────────────────\n 'FASTROUTER_API_KEY',\n // ── LOTE S (2026-07-13) — Perplexity Agent API, chat-capable ───────────\n 'PERPLEXITY_AGENT_API_KEY',\n // ── LOTE T (2026-07-13) — Ailin gateway, chat-capable ──────────────────\n 'AILIN_API_KEY',\n // ── LOTE U (2026-07-29) — Sakana AI (Fugu), chat-capable ────────────────\n 'SAKANA_AI_API_KEY',\n] as const;\n\n/**\n * Self-hosted / sidecar endpoints that can keep the server alive in a\n * DEGRADED mode when every external LLM API key is missing.\n *\n * Design rationale:\n * Without this fallback, a single failure (expired GCP ADC, rotated\n * service account, network blip during boot) produces an infinite\n * restart loop. With it, the server boots in `degraded_self_hosted`\n * mode: only local/sidecar models are usable, requests are tagged\n * via last-resort-policy.ts (execution_mode=last_resort_self_hosted,\n * excluded_from_benchmark=true), and ops gets a clear warning\n * instead of a crash loop.\n *\n * This ties directly into Bloco P1.10 (self-hosted last-resort policy)\n * but at boot-time scope rather than per-request scope.\n */\nconst SELF_HOSTED_ENV_VARS = [\n 'OLLAMA_URL',\n 'SELF_HOSTED_LLM_URL',\n 'LOCAL_LLAMA_URL',\n 'LOCAL_KOBOLD_URL',\n] as const;\n\n/**\n * Load critical secrets from GCP into process.env\n * \n * This function is called AFTER SecretsManager initialization\n * and BEFORE config validation.\n * \n * It loads secrets from GCP and injects them into process.env,\n * allowing the existing config system to work without modification.\n */\nexport async function loadSecretsIntoEnv(): Promise<void> {\n const log = logger.child({ component: 'secrets-loader' });\n\n log.info('Loading critical secrets from GCP Secret Manager into environment...');\n\n let loaded = 0;\n let skipped = 0;\n let failed = 0;\n let sanitizedKeys = 0;\n let providerSecretsLoaded = 0;\n // Tracks env values that were authoritatively replaced by GCP fetches.\n // Useful diagnostic for operators verifying that GCP is the source of truth.\n let overwrittenFromGcp = 0;\n\n const forceLoadRealKeys = process.env.TEST_USE_REAL_API_KEYS === 'true';\n\n // ── GCP-authoritative mode ─────────────────────────────────────────────\n // When SECRETS_PROVIDER_PRIMARY=gcp the operator has declared GCP as the\n // source of truth. The legacy \"skip if env already populated\" path then\n // contradicts that declaration: env_file / docker-compose / shell exports\n // silently win and GCP fetches are never even attempted, which is exactly\n // the failure mode that hid populated GCP secrets (e.g. HF_TOKEN) from\n // /v1/models. In authoritative mode we ALWAYS attempt the GCP fetch and\n // overwrite the pre-existing env value when GCP returns one. The fallback\n // is symmetric: a GCP miss preserves the existing env value (no zeroing).\n //\n // Operators can override:\n // SECRETS_GCP_AUTHORITATIVE=false → keep legacy skip-if-exists semantics\n // SECRETS_GCP_AUTHORITATIVE=true → force authoritative even with env primary\n const gcpPrimaryEnv = (process.env.SECRETS_PROVIDER_PRIMARY || '').trim().toLowerCase() === 'gcp';\n const explicitAuthoritative = (process.env.SECRETS_GCP_AUTHORITATIVE || '').trim().toLowerCase();\n const gcpAuthoritative =\n explicitAuthoritative === 'true' ||\n (explicitAuthoritative !== 'false' && gcpPrimaryEnv);\n\n // Tests already use TEST_USE_REAL_API_KEYS=true to force GCP fetches even\n // when local mocks are present. The two flags are independent triggers\n // for the same effect; either one disables skip-if-exists.\n const authoritativeFetch = gcpAuthoritative || forceLoadRealKeys;\n\n log.info(\n {\n gcpPrimary: gcpPrimaryEnv,\n gcpAuthoritative,\n forceLoadRealKeys,\n authoritativeFetch,\n },\n 'Secret loading mode resolved'\n );\n\n // Load critical secrets (required)\n for (const { envVar, secretKey, required, defaultValue } of CRITICAL_SECRETS) {\n const existingValue = process.env[envVar];\n const shouldOverrideMockStripe =\n forceLoadRealKeys &&\n envVar.startsWith('STRIPE_') &&\n typeof existingValue === 'string' &&\n (existingValue.includes('mock') || existingValue.includes('test'));\n\n // Skip if already in env, UNLESS:\n // - we're in GCP-authoritative mode (operator declared GCP as source of truth), OR\n // - we're forcing real keys and detected mock Stripe keys.\n if (existingValue && !shouldOverrideMockStripe && !gcpAuthoritative) {\n log.debug({ envVar, source: 'env' }, 'Secret already in environment, skipping GCP load');\n skipped++;\n continue;\n }\n\n try {\n const value = await loadSecret(secretKey, required);\n if (value) {\n const wasOverwrite = Boolean(existingValue) && existingValue !== value;\n process.env[envVar] = value;\n if (wasOverwrite) overwrittenFromGcp++;\n log.debug(\n { envVar, secretKey, replaced: Boolean(existingValue), overwrittenFromGcp: wasOverwrite },\n 'Secret loaded from GCP into environment'\n );\n loaded++;\n } else if (existingValue && gcpAuthoritative) {\n // Authoritative fetch missed in GCP — fall back to the existing env\n // value rather than zeroing it. This keeps the system bootable when\n // a single secret is absent from GCP without losing the env-provided\n // value.\n log.debug(\n { envVar, secretKey },\n 'GCP miss in authoritative mode; preserving existing env value'\n );\n skipped++;\n } else if (defaultValue !== undefined) {\n // Use default value if secret not found and default provided\n process.env[envVar] = defaultValue;\n log.debug({ envVar, secretKey, defaultValue }, 'Secret not found, using default value');\n skipped++;\n } else if (!required) {\n log.debug({ envVar, secretKey }, 'Optional secret not found, skipping');\n skipped++;\n }\n } catch (error) {\n if (defaultValue !== undefined) {\n // Use default value on error if provided\n process.env[envVar] = defaultValue;\n log.warn({ envVar, secretKey, defaultValue, error: getErrorMessage(error) }, 'Failed to load secret, using default value');\n skipped++;\n } else if (required) {\n const errorMsg = getErrorMessage(error);\n log.error(\n { envVar, secretKey, error: errorMsg },\n 'Failed to load required secret'\n );\n failed++;\n throw new Error(`Failed to load required secret ${secretKey}: ${errorMsg}`);\n } else {\n log.warn({ envVar, secretKey, error: getErrorMessage(error) }, 'Failed to load optional secret');\n skipped++;\n }\n }\n }\n\n // Keep compatibility with components that still read GCS_BUCKET_NAME.\n if (!process.env.GCS_BUCKET_NAME && process.env.GCS_FILES_BUCKET) {\n process.env.GCS_BUCKET_NAME = process.env.GCS_FILES_BUCKET;\n }\n\n // Load provider API keys (optional)\n // CRITICAL: If GCP auth is available, ALWAYS load real keys (API Keys NUNCA DEVEM FALHAR)\n // If TEST_USE_REAL_API_KEYS=true, force load from GCP even if mock keys exist\n // Always replace mock keys with real keys from GCP when available\n // (reuses forceLoadRealKeys defined above)\n \n for (const { envVar, secretKeys } of PROVIDER_SECRETS) {\n const existingValue = process.env[envVar];\n const providerName = ENV_VAR_TO_PROVIDER[envVar];\n\n // Check if existing value looks like a mock key\n const isMockKey = existingValue && (existingValue.includes('mock') || existingValue.includes('test-'));\n\n // Skip if real key already exists, UNLESS:\n // - we're in GCP-authoritative mode (operator declared GCP as source of truth), OR\n // - test forces real keys (TEST_USE_REAL_API_KEYS=true), OR\n // - the existing value looks like a mock.\n // The authoritative path lets GCP overwrite stale env_file / shell-exported\n // values, which is the entire point of SECRETS_PROVIDER_PRIMARY=gcp.\n if (existingValue && !authoritativeFetch && !isMockKey) {\n // Record as loaded from env\n if (providerName) {\n providerKeyStatusMap.set(providerName, { loaded: true, source: 'env', timestamp: new Date(), envVar });\n }\n skipped++;\n continue;\n }\n\n try {\n let loadedSecretValue: string | undefined;\n let resolvedSecretKey = secretKeys[0];\n\n for (const candidateKey of secretKeys) {\n try {\n const candidateValue = await loadSecret(candidateKey, false);\n if (candidateValue) {\n loadedSecretValue = candidateValue;\n resolvedSecretKey = candidateKey;\n log.debug({ envVar, secretKey: candidateKey, valueLen: candidateValue.length }, 'Secret loaded from GCP');\n break;\n } else {\n log.debug({ envVar, secretKey: candidateKey }, 'Secret returned empty/null from GCP');\n }\n } catch (loadErr) {\n log.warn({ envVar, secretKey: candidateKey, error: loadErr instanceof Error ? loadErr.message : String(loadErr) }, 'Failed to load secret from GCP');\n continue;\n }\n }\n\n // GCP provider automatically adds prefix 'ailin-' to secretKey\n // So 'openai-key' becomes 'ailin-openai-key' in GCP Secret Manager\n const value = loadedSecretValue;\n if (value) {\n // Sanitize control characters and invisible Unicode.\n // PEM-format keys (-----BEGIN ...) legitimately contain newlines,\n // so use a lighter regex that preserves \\n and \\r for those values.\n const isPemKey = value.startsWith('-----BEGIN');\n let sanitizedValue = isPemKey\n ? value\n .replace(/[\\x00-\\x09\\x0B\\x0C\\x0E-\\x1F\\x7F]/g, '') // Strip control chars EXCEPT \\n (0x0A) and \\r (0x0D)\n .replace(/[\\u200B-\\u200D\\uFEFF]/g, '') // Remove zero-width spaces\n .trim()\n : value\n .replace(/[\\x00-\\x1F\\x7F]/g, '') // Strip ALL control characters\n .replace(/[\\u200B-\\u200D\\uFEFF]/g, '') // Remove zero-width spaces\n .trim();\n \n // Log detailed debugging information if sanitization changed the value\n if (sanitizedValue !== value) {\n const removedChars = value.length - sanitizedValue.length;\n sanitizedKeys++;\n log.debug(\n { \n envVar, \n secretKey: resolvedSecretKey,\n originalLength: value.length, \n sanitizedLength: sanitizedValue.length,\n removedChars\n },\n 'API key contained invalid characters and was sanitized'\n );\n }\n \n // Always replace mock keys with real keys\n const wasReplaced = !!existingValue;\n const wasOverwriteOfRealValue =\n wasReplaced && !isMockKey && existingValue !== sanitizedValue;\n\n // CRITICAL: Set process.env and immediately validate\n // Use Object.defineProperty as fallback if direct assignment doesn't work\n // This ensures the value is actually set in the environment\n const _previousValue = process.env[envVar];\n process.env[envVar] = sanitizedValue;\n\n // Immediately read back to verify (handles any potential async issues)\n const readBackValue = process.env[envVar];\n\n // Deep validation: check exact match, type, and length\n const isValidMatch = readBackValue === sanitizedValue;\n const isValidType = typeof readBackValue === 'string';\n const isValidLength = readBackValue !== undefined && readBackValue.length === sanitizedValue.length;\n\n if (!isValidMatch || !isValidType || !isValidLength) {\n log.error(\n {\n envVar,\n secretKey: resolvedSecretKey,\n sanitizedValueLength: sanitizedValue.length,\n readBackValueLength: readBackValue?.length,\n },\n 'Secret was loaded but process.env assignment did not persist'\n );\n failed++;\n } else {\n // Success - log and increment counter\n if (wasOverwriteOfRealValue) overwrittenFromGcp++;\n log.debug(\n {\n envVar,\n secretKey: resolvedSecretKey,\n replaced: wasReplaced,\n wasMock: isMockKey,\n overwrittenFromGcp: wasOverwriteOfRealValue,\n },\n 'Provider secret loaded from GCP'\n );\n loaded++;\n providerSecretsLoaded++;\n // Record successful load\n if (providerName) {\n providerKeyStatusMap.set(providerName, { loaded: true, source: 'gcp', timestamp: new Date(), envVar });\n }\n }\n } else {\n // GCP returned no value. Decide what's actually in process.env right\n // now: if env had a non-mock value before this loop iteration we\n // preserved it (the authoritative path entered this block but found\n // no GCP value to overwrite with), so we're effectively env-loaded.\n // If env had a mock value, we keep the mock as a degraded fallback\n // and warn loudly. If env had nothing, the provider stays orphaned.\n if (existingValue && !isMockKey) {\n // Authoritative-mode GCP miss with real env value already set:\n // the env value is still in process.env, so report source=env.\n if (providerName) {\n providerKeyStatusMap.set(providerName, { loaded: true, source: 'env', timestamp: new Date(), envVar });\n }\n log.debug(\n { envVar, secretKeys },\n 'GCP miss in authoritative mode; preserving existing env value for provider'\n );\n } else {\n if (isMockKey) {\n log.warn(\n { envVar, secretKeys },\n 'Mock API key detected but no real key found in GCP - using mock key (this may cause API failures)'\n );\n }\n // Record as not loaded (no GCP value, no usable env value)\n if (providerName) {\n providerKeyStatusMap.set(providerName, { loaded: false, source: 'none', timestamp: new Date(), envVar });\n }\n }\n skipped++;\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n // Only warn if not forcing real keys - if forcing, this is expected to work\n if (forceLoadRealKeys) {\n log.error(\n { envVar, secretKeys, error: errorMessage },\n 'Failed to load provider secret from GCP (TEST_USE_REAL_API_KEYS=true)'\n );\n failed++;\n } else {\n if (isMockKey) {\n log.warn(\n { envVar, secretKeys, error: errorMessage },\n 'Failed to load provider secret from GCP - will use mock key (this may cause API failures)'\n );\n } else {\n log.debug(\n { envVar, secretKeys, error: errorMessage },\n 'Failed to load provider secret from GCP (optional)'\n );\n }\n // Record failure\n if (providerName) {\n providerKeyStatusMap.set(providerName, { loaded: false, source: 'none', timestamp: new Date(), envVar });\n }\n skipped++;\n }\n }\n }\n\n // ── Env-var aliases (2026-04-28) ────────────────────────────────────────\n //\n // Some operators set a non-canonical name in `.env` for historical reasons\n // (e.g. `REKAAI_API_KEY` instead of the catalog-canonical `REKA_API_KEY`).\n // When GCP is reachable the secret loader populates the canonical name and\n // it just works; when GCP is unreachable the alias would be the only thing\n // set and the catalog row reads the canonical → empty key → silent skip.\n //\n // This pass copies known aliases into their canonical env var if the\n // canonical is empty. It is purely additive — never overwrites an\n // existing canonical value, never logs a warning unless an alias was\n // applied. Add new entries here when a similar mismatch is discovered.\n const ENV_ALIASES: ReadonlyArray<{ canonical: string; alias: string }> = [\n { canonical: 'REKA_API_KEY', alias: 'REKAAI_API_KEY' },\n ];\n for (const { canonical, alias } of ENV_ALIASES) {\n const canonicalValue = process.env[canonical];\n const aliasValue = process.env[alias];\n if ((canonicalValue == null || canonicalValue.trim() === '') && aliasValue && aliasValue.trim() !== '') {\n process.env[canonical] = aliasValue;\n log.info({ canonical, alias }, 'Promoted alias env var to canonical name');\n }\n }\n\n const gcpPrimary = (process.env.SECRETS_PROVIDER_PRIMARY || '').trim().toLowerCase() === 'gcp';\n const failFastEnv = (process.env.SECRETS_GCP_FAIL_FAST || '').trim().toLowerCase();\n const gcpFailFast = failFastEnv === 'true' || (failFastEnv !== 'false' && gcpPrimary);\n const hasAnyLlmProviderKey = LLM_PROVIDER_ENV_VARS.some((envVar) => {\n const value = process.env[envVar];\n return typeof value === 'string' && value.trim().length > 0;\n });\n\n // Hardening (P1.10 at boot scope): even when zero external API keys are\n // loaded, the server can still boot in DEGRADED mode if a self-hosted\n // endpoint is reachable. Every request will then be tagged via the\n // last-resort-policy as execution_mode=last_resort_self_hosted.\n const availableSelfHostedEndpoints = SELF_HOSTED_ENV_VARS.filter((envVar) => {\n const value = process.env[envVar];\n return typeof value === 'string' && value.trim().length > 0;\n });\n const hasAnySelfHostedFallback = availableSelfHostedEndpoints.length > 0;\n\n // Opt-out for strict production deployments that must never run in\n // degraded mode. Default: allow degraded boot (safer than crash loops).\n const allowDegradedBoot = (process.env.ALLOW_DEGRADED_BOOT || 'true').trim().toLowerCase() !== 'false';\n\n if (gcpFailFast && !hasAnyLlmProviderKey) {\n if (hasAnySelfHostedFallback && allowDegradedBoot) {\n log.warn(\n {\n gcpPrimary,\n gcpFailFast,\n providerSecretsLoaded,\n selfHostedEndpoints: availableSelfHostedEndpoints,\n mode: 'degraded_self_hosted',\n docs: 'docs/hardening/last-resort-policy',\n },\n '⚠️ DEGRADED BOOT: zero external LLM credentials, but self-hosted fallback is available. ' +\n 'Server will start in last-resort mode. Requests will be tagged excluded_from_benchmark. ' +\n 'Fix ADC/GCP credentials to restore full operation.'\n );\n // Intentionally fall through — boot continues\n } else {\n const message = hasAnySelfHostedFallback\n ? 'SECRETS_PROVIDER_PRIMARY=gcp, no LLM provider credentials loaded, and ALLOW_DEGRADED_BOOT=false.'\n : 'SECRETS_PROVIDER_PRIMARY=gcp but no LLM provider credentials are available after secrets bootstrap, ' +\n 'and no self-hosted fallback endpoints (OLLAMA_URL, SELF_HOSTED_LLM_URL, LOCAL_LLAMA_URL, LOCAL_KOBOLD_URL) are configured.';\n log.error(\n {\n gcpPrimary,\n gcpFailFast,\n providerSecretsLoaded,\n checkedEnvVars: LLM_PROVIDER_ENV_VARS,\n selfHostedEndpointsChecked: SELF_HOSTED_ENV_VARS,\n allowDegradedBoot,\n },\n message\n );\n throw new Error(\n `${message} Verify GCP Secret Manager access (ADC/Workload Identity) and secret names/prefix, ` +\n `or set OLLAMA_URL for degraded self-hosted boot, or run 'gcloud auth application-default login' if local.`\n );\n }\n }\n\n // Warn on partial provider key loading — helps detect GCP issues early\n if (hasAnyLlmProviderKey && providerSecretsLoaded > 0 && providerSecretsLoaded < 3) {\n log.warn(\n { providerSecretsLoaded, total: PROVIDER_SECRETS.length },\n `⚠️ Only ${providerSecretsLoaded} provider secrets loaded — possible partial GCP failure`\n );\n }\n\n // Audio provider diagnostics\n const audioProviders = {\n DEEPGRAM_API_KEY: !!process.env.DEEPGRAM_API_KEY,\n CARTESIA_API_KEY: !!process.env.CARTESIA_API_KEY,\n ELEVENLABS_API_KEY: !!process.env.ELEVENLABS_API_KEY,\n OLLAMA_URL: process.env.OLLAMA_URL || '',\n SELF_HOSTED_STT_URL: process.env.SELF_HOSTED_STT_URL || '',\n LOCAL_NLLB_URL: process.env.LOCAL_NLLB_URL || '',\n };\n log.info({ audioProviders }, 'Audio provider env vars after secrets loading');\n\n log.info(\n {\n loaded,\n skipped,\n failed,\n sanitizedKeys,\n providerSecretsLoaded,\n overwrittenFromGcp,\n gcpAuthoritative,\n total: CRITICAL_SECRETS.length + PROVIDER_SECRETS.length,\n },\n '✅ Secrets loading complete'\n );\n\n if (failed > 0) {\n throw new Error(`Failed to load ${failed} required secrets`);\n }\n}\n\n/**\n * Get summary of secrets loaded.\n *\n * Diagnostic helper for admin endpoints (e.g. /v1/admin/providers/...) that\n * surfaces exactly which provider envVars are populated and from where, so\n * operators can verify SECRETS_PROVIDER_PRIMARY=gcp is actually authoritative\n * and not silently shadowed by stale env values.\n */\nexport function getSecretsLoadSummary(): {\n fromGCP: string[];\n fromEnv: string[];\n notLoaded: string[];\n} {\n const fromGCP: string[] = [];\n const fromEnv: string[] = [];\n const notLoaded: string[] = [];\n\n for (const status of providerKeyStatusMap.values()) {\n if (!status.loaded) {\n notLoaded.push(status.envVar);\n continue;\n }\n if (status.source === 'gcp') {\n fromGCP.push(status.envVar);\n } else if (status.source === 'env') {\n fromEnv.push(status.envVar);\n }\n }\n\n return { fromGCP, fromEnv, notLoaded };\n}\n"}, "4c8ccfed421656aae81aaae8c9faefd0f2c98c64": {"t": 1785754624.5358255, "n": 63209, "h": "3e2285044e2c05b9", "c": "// Copyright (C) 2026 Ailin One, Inc.\n//\n// This file is part of Collective Intelligence Engine (ci).\n// Licensed under the GNU Affero General Public License v3.0 or later.\n// See LICENSE in the repository root, or <https://www.gnu.org/licenses/>.\n//\n// SPDX-License-Identifier: AGPL-3.0-or-later\n// Source: https://github.com/ailinone/collective-intelligence\n\n/**\n * Load Secrets into Environment\n * \n * This module loads critical secrets from GCP Secret Manager\n * and injects them into process.env BEFORE config validation.\n * \n * This allows the existing config system to work without refactoring,\n * while still using GCP Secret Manager as the source of truth.\n * \n * Call order:\n * 1. initializeSecretsManager()\n * 2. loadSecretsIntoEnv() ← This file\n * 3. validateConfig()\n */\n\nimport { logger } from '@/utils/logger';\nimport { getErrorMessage } from '@/utils/type-guards';\nimport { loadSecret } from './secrets-loader';\n\n/**\n * Critical secrets that must be loaded from GCP\n * These are injected into process.env for config compatibility\n */\ninterface CriticalSecret {\n envVar: string;\n secretKey: string;\n required: boolean;\n defaultValue?: string;\n}\n\nconst CRITICAL_SECRETS: CriticalSecret[] = [\n { envVar: 'DATABASE_URL', secretKey: 'database-url', required: true },\n { envVar: 'JWT_SECRET', secretKey: 'jwt-secret', required: true },\n { envVar: 'REDIS_PASSWORD', secretKey: 'redis-password', required: false, defaultValue: '' },\n { envVar: 'GCS_FILES_BUCKET', secretKey: 'gcs-files-bucket', required: false },\n { envVar: 'SMTP_PASS', secretKey: 'smtp-pass', required: false },\n { envVar: 'STRIPE_SECRET_KEY', secretKey: 'stripe-secret-key', required: false },\n { envVar: 'STRIPE_WEBHOOK_SECRET', secretKey: 'stripe-webhook-secret', required: false },\n { envVar: 'STRIPE_PUBLISHABLE_KEY', secretKey: 'stripe-publishable-key', required: false },\n // Phase 2c shadow ensemble bearer token. Optional because the coord-\n // stable wire is disabled by default in production\n // (CI_ENSEMBLE_COORDINATOR_ENABLED=false). When operator flips it on,\n // the GCP secret `coord-ensemble-token` MUST also be set with the\n // value to share with coord-serving's COORD_SERVING_AUTH_TOKEN.\n { envVar: 'CI_ENSEMBLE_COORDINATOR_TOKEN', secretKey: 'coord-ensemble-token', required: false },\n // Prometheus scrape bearer token for /metrics + /metrics/prompts\n // (metrics-route.ts `authorizeScrape`). Optional here — the route itself\n // is fail-closed when unset: it denies (403) in production and stays\n // open for local/dev scraping otherwise. Wired via CRITICAL_SECRETS\n // (not PROVIDER_SECRETS) because this is a general app credential, not\n // a provider API key.\n { envVar: 'PROMETHEUS_SCRAPE_TOKEN', secretKey: 'prometheus-scrape-token', required: false },\n // Sentry error-tracking DSN (utils/error-tracking.ts `initializeErrorTracking`).\n // Optional — the module itself no-ops with a warning log when unset, both\n // at API boot (index.ts) and worker boot (workers/queue-runner.ts), so\n // there is no fail-closed/fail-open behavior riding on this like the\n // Prometheus token above. Wired via CRITICAL_SECRETS (not PROVIDER_SECRETS)\n // because this is a general app credential, not a provider API key. No GCP\n // secret exists yet as of this commit — an operator must create one\n // (`ailin-sentry-dsn`) with a real DSN from Sentry's project settings\n // before this ever resolves to a non-empty value.\n { envVar: 'SENTRY_DSN', secretKey: 'sentry-dsn', required: false },\n];\n\n/**\n * Provider API keys (loaded from GCP if not in env)\n * CRITICAL: GCP provider automatically adds prefix 'ailin-' to secret keys\n * So 'openai-key' becomes 'ailin-openai-key' in GCP Secret Manager\n * These keys match the actual secret names in GCP (without the prefix, provider adds it)\n */\ninterface ProviderSecretBinding {\n envVar: string;\n secretKeys: readonly string[];\n}\n\nexport const PROVIDER_SECRETS: readonly ProviderSecretBinding[] = [\n { envVar: 'OPENAI_API_KEY', secretKeys: ['openai-key'] },\n { envVar: 'ANTHROPIC_API_KEY', secretKeys: ['anthropic-key'] },\n { envVar: 'GOOGLE_API_KEY', secretKeys: ['google-key'] },\n { envVar: 'DEEPSEEK_API_KEY', secretKeys: ['deepseek-key'] },\n { envVar: 'XAI_API_KEY', secretKeys: ['xai-key'] },\n { envVar: 'MISTRAL_API_KEY', secretKeys: ['mistral-key'] },\n { envVar: 'COHERE_API_KEY', secretKeys: ['cohere-key'] },\n { envVar: 'NVIDIA_API_KEY', secretKeys: ['nvidia-key', 'nvidia-api-key'] },\n { envVar: 'AIHUBMIX_API_KEY', secretKeys: ['aihubmix-key', 'aihubmix-api-key'] },\n { envVar: 'NOVITA_API_KEY', secretKeys: ['novita-key', 'novita-api-key'] },\n { envVar: 'MOONSHOT_API_KEY', secretKeys: ['moonshot-key', 'moonshot-api-key'] },\n { envVar: 'MINIMAX_API_KEY', secretKeys: ['minimax-key', 'minimax-api-key'] },\n { envVar: 'JINA_API_KEY', secretKeys: ['jina-key', 'jina-api-key'] },\n { envVar: 'GOOGLE_MAPS_API_KEY', secretKeys: ['google-maps-key', 'google-maps-api-key'] },\n { envVar: 'FRIENDLI_API_KEY', secretKeys: ['friendli-key', 'friendli-api-key'] },\n { envVar: 'FRIENDLI_TEAM_ID', secretKeys: ['friendli-team-id'] },\n { envVar: 'AIML_API_KEY', secretKeys: ['aiml-key', 'aiml-api-key'] },\n { envVar: 'IMAGEROUTER_API_KEY', secretKeys: ['imagerouter-key', 'imagerouter-api-key'] },\n { envVar: 'OPENROUTER_API_KEY', secretKeys: ['openrouter-key'] },\n { envVar: 'COMETAPI_API_KEY', secretKeys: ['cometapi-key', 'cometapi-api-key'] },\n { envVar: 'NANOGPT_API_KEY', secretKeys: ['nanogpt-key', 'nanogpt-api-key'] },\n { envVar: 'REQUESTY_API_KEY', secretKeys: ['requesty-key', 'requesty-api-key'] },\n { envVar: 'AI302_API_KEY', secretKeys: ['302-key', '302ai-key'] },\n { envVar: 'POE_API_KEY', secretKeys: ['poe-key', 'poe-api-key'] },\n { envVar: 'ROUTEWAY_API_KEY', secretKeys: ['routeway-key', 'routeway-api-key'] },\n // Hub providers support both key naming conventions for compatibility.\n { envVar: 'ORQAI_API_KEY', secretKeys: ['orqai-key', 'orqai-api-key'] },\n { envVar: 'ORQAI_MODEL_DENYLIST', secretKeys: ['orqai-model-denylist'] },\n { envVar: 'EDENAI_API_KEY', secretKeys: ['edenai-key', 'edenai-api-key'] },\n { envVar: 'HELICONEAI_API_KEY', secretKeys: ['heliconeai-key', 'heliconeai-api-key'] },\n { envVar: 'VERTEX_AI_API_KEY', secretKeys: ['vertex-key'] },\n { envVar: 'VERTEX_AI_PROJECT_ID', secretKeys: ['vertex-project-id'] },\n { envVar: 'QWEN_API_KEY', secretKeys: ['qwen-key-2'] },\n { envVar: 'ALIBABA_ACCESS_KEY_ID', secretKeys: ['alibaba-key-id'] },\n { envVar: 'ALIBABA_ACCESS_KEY_SECRET', secretKeys: ['alibaba-key-secret'] },\n { envVar: 'ERNIE_API_KEY', secretKeys: ['baidu-key'] },\n { envVar: 'ERNIE_SECRET_KEY', secretKeys: ['baidu-secret'] },\n { envVar: 'BAIDU_BASE_URL', secretKeys: ['baidu-base-url'] },\n { envVar: 'AWS_ACCESS_KEY_ID', secretKeys: ['aws-key-id'] },\n { envVar: 'AWS_SECRET_ACCESS_KEY', secretKeys: ['aws-secret'] },\n { envVar: 'AWS_BEARER_TOKEN_BEDROCK', secretKeys: ['aws-bearer-token'] },\n // Batch 7.1: Bedrock region + inference-profile ARN can now be set via\n // GCP Secret Manager. Useful for deploys that route Bedrock to a specific\n // region (e.g. us-west-2) or via cross-region inference profiles without\n // baking these into the docker image.\n { envVar: 'AWS_BEDROCK_REGION', secretKeys: ['aws-bedrock-region'] },\n { envVar: 'AWS_BEDROCK_INFERENCE_PROFILE_ARN', secretKeys: ['aws-bedrock-inference-profile-arn'] },\n // Batch 8.1: SageMaker endpoint routing. A deploy can point this adapter\n // at a specific deployed endpoint via a secret rather than baking the\n // endpoint name into the image. The region override lets the adapter\n // hit an endpoint in a different region from the general AWS_REGION\n // used by other services (SES, S3).\n { envVar: 'AWS_SAGEMAKER_REGION', secretKeys: ['aws-sagemaker-region'] },\n { envVar: 'AWS_SAGEMAKER_ENDPOINT_NAME', secretKeys: ['aws-sagemaker-endpoint-name'] },\n { envVar: 'AWS_SAGEMAKER_PAYLOAD_SCHEMA', secretKeys: ['aws-sagemaker-payload-schema'] },\n { envVar: 'AWS_SAGEMAKER_CUSTOM_ATTRIBUTES', secretKeys: ['aws-sagemaker-custom-attributes'] },\n { envVar: 'AZURE_OPENAI_API_KEY', secretKeys: ['azure-openai-api-key'] },\n { envVar: 'AZURE_OPENAI_ENDPOINT', secretKeys: ['azure-openai-endpoint'] },\n { envVar: 'AZURE_OPENAI_DEPLOYMENT', secretKeys: ['azure-openai-deployment'] },\n // Audio-first providers (STT, TTS, STS)\n { envVar: 'DEEPGRAM_API_KEY', secretKeys: ['deepgram-key'] },\n { envVar: 'CARTESIA_API_KEY', secretKeys: ['cartesia-key'] },\n { envVar: 'ELEVENLABS_API_KEY', secretKeys: ['elevenlabs-key'] },\n // Translation provider\n { envVar: 'PALABRAAI_CLIENT_ID', secretKeys: ['palabraai-id'] },\n { envVar: 'PALABRAAI_CLIENT_SECRET', secretKeys: ['palabraai-key'] },\n { envVar: 'OCI_TENANCY_ID', secretKeys: ['oci-tenancy-id'] },\n { envVar: 'OCI_USER_ID', secretKeys: ['oci-user-id'] },\n { envVar: 'OCI_FINGERPRINT', secretKeys: ['oci-fingerprint'] },\n { envVar: 'OCI_PRIVATE_KEY', secretKeys: ['oci-private-key'] },\n { envVar: 'OCI_REGION', secretKeys: ['oci-region'] },\n // 2026-04-22: New GCP secrets (5 providers). The GCP provider auto-prefixes\n // 'ailin-', so 'wandb-key' maps to ailin-wandb-key in Secret Manager.\n // Voyage lists both 'voyageai-key' (current upstream name) and 'voyage-key'\n // (conventional fallback) because the canonical spelling is still unstable.\n { envVar: 'WANDB_API_KEY', secretKeys: ['wandb-key', 'wandb-api-key'] },\n { envVar: 'VOYAGE_API_KEY', secretKeys: ['voyageai-key', 'voyage-key', 'voyage-api-key'] },\n { envVar: 'BYTEZ_API_KEY', secretKeys: ['bytez-key', 'bytez-api-key'] },\n { envVar: 'CEREBRAS_API_KEY', secretKeys: ['cerebras-key', 'cerebras-api-key'] },\n { envVar: 'FEATHERLESS_AI_API_KEY', secretKeys: ['featherless-key', 'featherless-api-key'] },\n // Batch 6 — enterprise hyperscaler gateways (2026-04-22).\n // Azure OpenAI already has API_KEY/ENDPOINT/DEPLOYMENT wired above;\n // these fill in the remaining URL-template parameters.\n { envVar: 'AZURE_OPENAI_RESOURCE_NAME', secretKeys: ['azure-openai-resource-name', 'azure-openai-resource'] },\n { envVar: 'AZURE_OPENAI_API_VERSION', secretKeys: ['azure-openai-api-version'] },\n // Gemini OAI-compat shares the API key with the native `google` provider.\n // Two env-var aliases: GEMINI_API_KEY (canonical upstream) and\n // GOOGLE_AI_STUDIO_API_KEY (legacy). The adapter reads either.\n //\n // 2026-04-23: `vertex-key` added as a fallback source because it was\n // verified working against generativelanguage.googleapis.com (HTTP 200,\n // 50 Gemini models) while `google-key` is suspended (403\n // CONSUMER_SUSPENDED). The Vertex/Gemini API-key split is mostly\n // historical — a Google Cloud API key scoped for Gemini works equally on\n // aistudio and on the generativelanguage surface, as long as the key's\n // GCP project has the API enabled.\n //\n // 2026-07-25: `vertex-key` was originally placed LAST on the (incorrect)\n // assumption that the loop below tries each candidate live and keeps the\n // first one that WORKS. It doesn't — it takes the first candidate that\n // merely EXISTS in Secret Manager (see the resolution loop a few hundred\n // lines down), so the suspended `google-key` always won and gemini-openai\n // stayed broken. Moved `vertex-key` first since it's the only one\n // confirmed live-working; the others remain as fallbacks if it's ever\n // rotated out.\n { envVar: 'GEMINI_API_KEY', secretKeys: ['vertex-key', 'gemini-key', 'google-ai-studio-key', 'google-key'] },\n // GitHub Models uses a GitHub PAT. Operators typically already have this\n // in env (it's the same PAT used for gh CLI); the GCP path is a fallback\n // for service accounts that need isolated tokens.\n { envVar: 'GITHUB_TOKEN', secretKeys: ['github-models-token', 'github-token', 'github-pat'] },\n // Databricks workspace token + config.\n { envVar: 'DATABRICKS_TOKEN', secretKeys: ['databricks-token', 'databricks-pat'] },\n { envVar: 'DATABRICKS_HOST', secretKeys: ['databricks-host'] },\n { envVar: 'DATABRICKS_SERVING_ENDPOINT', secretKeys: ['databricks-serving-endpoint'] },\n // Inworld AI — orphan closure 2026-04-22. Key is already base64-encoded\n // in GCP (132 chars) because Inworld uses Basic auth, not Bearer. The\n // adapter does NOT re-encode; it passes the secret verbatim into the\n // `Basic ` header. Never base64-encode this value at load time.\n { envVar: 'INWORLD_API_KEY', secretKeys: ['inworld-key', 'inworld-api-key'] },\n // ── Orphan-closure Lot B (2026-04-23) ──────────────────────────────────\n // Three providers closed in a single lot because they share identical\n // shape (thin OAI-compat wrappers) and were all already present in GCP\n // Secret Manager but not wired into env loading. Each tuple lists the\n // preferred secret key first and legacy-or-alternate names as fallbacks.\n { envVar: 'WRITER_API_KEY', secretKeys: ['writer-key', 'writer-api-key'] },\n { envVar: 'UPSTAGE_API_KEY', secretKeys: ['upstage-key', 'upstage-api-key'] },\n // Reka's canonical upstream name is \"reka\" but the key is stored in GCP\n // as `ailin-rekaai-key` (matching the catalog providerId, not the upstream\n // name). `reka-key` is kept as a fallback in case the secret is ever\n // renamed to match upstream naming later.\n { envVar: 'REKA_API_KEY', secretKeys: ['rekaai-key', 'reka-key', 'reka-api-key'] },\n // ── LOTE M (2026-04-23) — complement lot: 13 new env-var mappings. ─────\n // Every provider below landed a catalog row this lot and is classified\n // in CONSOLIDATION_MATRIX['credentials-missing'] (secret-absent sub-class\n // for 12, auth-incomplete for qianfan). Each mapping is wired NOW so the\n // moment an operator provisions the corresponding GCP secret, the key\n // propagates to process.env without a code change. The first element of\n // each secretKeys array is the preferred upstream-convention name; any\n // follow-up entries are fallbacks for historical or legacy aliases.\n { envVar: 'ARCEE_API_KEY', secretKeys: ['arcee-api-key', 'arcee-key'] },\n { envVar: 'ATLASCLOUD_API_KEY', secretKeys: ['atlascloud-key', 'atlascloud-api-key', 'atlas-cloud-key'] },\n { envVar: 'AVIAN_API_KEY', secretKeys: ['avian-key', 'avian-api-key', 'avian-io-key'] },\n // Qianfan (Baidu) v2 bce-v3 bearer. Distinct from the legacy v1 AK+SK\n // material in ERNIE_API_KEY / ERNIE_SECRET_KEY / BAIDU_BASE_URL above —\n // both mappings are intentionally kept so an operator can populate either\n // path (v1 OAuth or v2 bearer) depending on which runtime surface is\n // used. The canonical catalog row `qianfan` reads QIANFAN_API_KEY only.\n { envVar: 'QIANFAN_API_KEY', secretKeys: ['qianfan-key', 'qianfan-api-key', 'baidu-qianfan-key'] },\n { envVar: 'GMI_API_KEY', secretKeys: ['gmi-key', 'gmi-api-key', 'gmicloud-key'] },\n { envVar: 'INFERMATIC_API_KEY', secretKeys: ['infermatic-api-key', 'infermatic-key', 'totalgpt-key'] },\n { envVar: 'INFLECTION_API_KEY', secretKeys: ['inflection-key', 'inflection-api-key', 'inflection-ai-key'] },\n // Mancer (tagged contentPolicyClass=uncensored in catalog; per the universal\n // \"habilitado e nunca censurado\" policy from Phase 4b, 2026-04-28, the\n // catalog row is fully admitted to routing — the tag is informational so\n // downstream surfaces can opt to filter it from a moderated default list).\n { envVar: 'MANCER_API_KEY', secretKeys: ['mancer-key', 'mancer-api-key'] },\n // Phala / RedPill TEE — upstream docs use REDPILL_API_KEY but we wire\n // PHALA_API_KEY as the canonical env var (see apiKeyEnvVarOverrideReason\n // in catalog row `phala`). Legacy `redpill-*` secret names are accepted\n // as fallbacks so an operator storing under either convention works.\n { envVar: 'PHALA_API_KEY', secretKeys: ['phala-key', 'phala-api-key', 'redpill-key', 'redpill-api-key'] },\n { envVar: 'RELACE_API_KEY', secretKeys: ['relace-key', 'relace-api-key'] },\n { envVar: 'SILICONFLOW_API_KEY', secretKeys: ['siliconflow-api-key', 'siliconflow-key', 'silicon-flow-key'] },\n { envVar: 'STEPFUN_API_KEY', secretKeys: ['stepfun-api-key', 'stepfun-key', 'step-key'] },\n // Venice (tagged contentPolicyClass=uncensored in catalog). Same Phase-4b\n // treatment as Mancer — admitted, never censored, with the tag as the\n // audit trail for the policy choice.\n { envVar: 'VENICE_API_KEY', secretKeys: ['venice-key', 'venice-api-key', 'venice-ai-key'] },\n // ── Sublote E1 (2026-04-24) — runtime-wiring closure for D1 matrix ─────\n // Sublote D1 promoted 10 providers to CONSOLIDATION_MATRIX['live-validation']\n // based on direct-probe evidence (via gcloud secrets versions access,\n // bypassing the runtime env loader). 7 of those had no GCP→ENV tuple here,\n // so the catalog-loader could never surface `process.env.<X>_API_KEY` at\n // boot — meaning live-validated providers were silently \"not runnable\".\n // Wired pre-emptively: the moment an operator provisions the GCP secret\n // AND the container restarts, propagation happens with zero code change.\n // The 3 already-mapped providers (heliconeai, github-models, infermatic)\n // are NOT duplicated — they appear above under their respective batches.\n { envVar: 'GROQ_API_KEY', secretKeys: ['groq-api-key', 'groq-key'] },\n { envVar: 'DEEPINFRA_API_KEY', secretKeys: ['deepinfra-api-key', 'deepinfra-key'] },\n // Hugging Face: upstream canonical env var is `HF_TOKEN` (not\n // HUGGINGFACE_API_KEY); the catalog row's apiKeyEnvVar matches. Primary\n // GCP secret name follows the upstream convention; legacy huggingface-*\n // aliases accepted for operators who stored it under the provider slug.\n { envVar: 'HF_TOKEN', secretKeys: ['huggingface-api-key', 'huggingface-token', 'hf-token', 'huggingface-key'] },\n // Cloudflare Workers AI — requires BOTH api-token AND account-id. The\n // CloudflareWorkersAIAdapter factory (default-adapter-factories.ts line\n // 229-246) reads CLOUDFLARE_ACCOUNT_ID from env and substitutes it into\n // the `{account_id}` URL template. Without the account-id side-car the\n // adapter ships the literal template to the wire → 404 on every call.\n // Side-car is wired here as a separate tuple because it's not an auth\n // credential — it's a URL parameter — so it doesn't belong in\n // ENV_VAR_TO_PROVIDER or LLM_PROVIDER_ENV_VARS below.\n { envVar: 'CLOUDFLARE_API_TOKEN', secretKeys: ['cloudflare-workers-ai-api-key', 'cloudflare-workers-ai-token', 'cloudflare-api-token', 'cloudflare-workers-ai-key'] },\n { envVar: 'CLOUDFLARE_ACCOUNT_ID', secretKeys: ['cloudflare-workers-ai-id', 'cloudflare-account-id', 'cloudflare-workers-ai-account-id'] },\n { envVar: 'PERPLEXITY_API_KEY', secretKeys: ['perplexity-api-key', 'perplexity-key'] },\n // Fireworks AI — note the catalog providerId is `fireworks-ai` (hyphenated)\n // but upstream brand/docs spell it \"Fireworks\" (no suffix). Four fallbacks\n // cover both conventions + api-key suffix variant.\n { envVar: 'FIREWORKS_AI_API_KEY', secretKeys: ['fireworks-ai-api-key', 'fireworks-ai-key', 'fireworks-key', 'fireworks-api-key'] },\n { envVar: 'SAMBANOVA_API_KEY', secretKeys: ['sambanova-api-key', 'sambanova-key'] },\n // Replicate — flagged by the sublote-e1-runtime-wiring invariant as a\n // pre-existing gap. `replicate` has been in CONSOLIDATION_MATRIX\n // ['live-validation'] since before D1, with adapterClass 'ReplicateAdapter'\n // and apiKeyEnvVar 'REPLICATE_API_KEY', but no tuple was ever added here.\n // Even though integrationMode is 'execution-only' (no /v1/models\n // discovery), the adapter still requires the env var at /predictions\n // call time. Wired here for closure; the GCP secret name follows the\n // provider-slug convention with optional `-api-key` fallback.\n { envVar: 'REPLICATE_API_KEY', secretKeys: ['replicate-key', 'replicate-api-key'] },\n // ── LOTE N (2026-04-27) — catalog↔loader closure for the remaining 22 ─\n // apiKeyEnvVar entries that had catalog rows but NO PROVIDER_SECRETS\n // tuple. Surfaced by `catalog-loader-wiring-completeness.test.ts` (J6\n // invariant). All 22 are real provider rows with adapter classes (or\n // generic OAI-compat hub registration); they were classified in\n // CONSOLIDATION_MATRIX as `credentials-missing` or `disabled-by-default`,\n // and their absence here meant that even after an operator provisioned\n // the GCP secret, `process.env.<X>_API_KEY` would stay empty at boot.\n //\n // Pattern: preferred secret name first (provider-slug `-key`), then\n // upstream-convention `-api-key` fallback, then any historical aliases.\n // The provider-side `aliases` array in providers.catalog.ts is NOT\n // duplicated here — secret-key aliases are about HOW the operator\n // stored the credential, NOT how callers reach the provider.\n //\n // Image/video-only providers (bfl, recraft, runwayml, topaz) are wired\n // here too but are intentionally absent from LLM_PROVIDER_ENV_VARS below\n // because their presence does NOT count toward the \"at least one LLM\n // key present\" boot-mode gate.\n { envVar: 'TOGETHERAI_API_KEY', secretKeys: ['togetherai-api-key', 'togetherai-key', 'together-key', 'together-api-key'] },\n { envVar: 'ANYSCALE_API_KEY', secretKeys: ['anyscale-api-key', 'anyscale-key'] },\n { envVar: 'HYPERBOLIC_API_KEY', secretKeys: ['hyperbolic-api-key', 'hyperbolic-key'] },\n { envVar: 'CHUTES_API_KEY', secretKeys: ['chutes-api-key', 'chutes-key', 'chutes-ai-key'] },\n { envVar: 'NSCALE_API_KEY', secretKeys: ['nscale-key', 'nscale-api-key'] },\n { envVar: 'NEBIUS_API_KEY', secretKeys: ['nebius-key', 'nebius-api-key'] },\n // Lambda Cloud canonical provider slug is `lambda-ai`; brand is\n // \"Lambda\" (without -ai). Both spellings tried as fallbacks.\n { envVar: 'LAMBDA_AI_API_KEY', secretKeys: ['lambda-ai-key', 'lambda-key', 'lambda-cloud-key', 'lambda-ai-api-key'] },\n { envVar: 'SCALEWAY_API_KEY', secretKeys: ['scaleway-key', 'scaleway-api-key'] },\n { envVar: 'SYNTHETIC_API_KEY', secretKeys: ['synthetic-key', 'synthetic-api-key', 'synthetic-ai-key'] },\n { envVar: 'MORPH_API_KEY', secretKeys: ['morph-key', 'morph-api-key', 'morph-llm-key'] },\n // Z.ai (catalog providerId = `zai`). Upstream domain z.ai resolves to\n // bigmodel.cn — the GLM family operator. Legacy spelling z-ai accepted.\n { envVar: 'ZAI_API_KEY', secretKeys: ['zai-key', 'zai-api-key', 'z-ai-key', 'glm-key'] },\n { envVar: 'XIAOMI_MIMO_API_KEY', secretKeys: ['xiaomi-mimo-key', 'mimo-key', 'xiaomi-mimo-api-key'] },\n // v0 (Vercel) — code-gen LLM. The catalog providerId is `v0` (one\n // character) so the env var name is `V0_API_KEY` not `V0AI_API_KEY`.\n { envVar: 'V0_API_KEY', secretKeys: ['v0-key', 'v0-api-key', 'vercel-v0-key'] },\n // Vercel AI Gateway — DIFFERENT from v0 above (gateway over many LLMs).\n // The canonical secret name follows the catalog providerId.\n { envVar: 'VERCEL_AI_GATEWAY_API_KEY', secretKeys: ['vercel-ai-gateway-key', 'vercel-ai-gateway-api-key', 'vercel-gateway-key'] },\n // Volcano (Volcengine — ByteDance). Canonical slug `volcano`; brand\n // includes Volcengine + Doubao + Ark. Multiple aliases accepted.\n { envVar: 'VOLCANO_API_KEY', secretKeys: ['volcano-key', 'volcano-api-key', 'volcengine-key', 'doubao-key', 'ark-key'] },\n // IBM watsonx — note the env var is `WATSONX_APIKEY` (no underscore\n // between API and KEY) to match IBM's canonical SDK convention.\n // Side-car env vars (WATSONX_PROJECT_ID, WATSONX_URL) are wired below\n // because they're documented in the catalog row's `extraEnvVars` and\n // adapter requires them at runtime.\n { envVar: 'WATSONX_APIKEY', secretKeys: ['watsonx-apikey', 'watsonx-key', 'watsonx-api-key', 'ibm-watsonx-key'] },\n { envVar: 'WATSONX_PROJECT_ID', secretKeys: ['watsonx-project-id', 'ibm-watsonx-project-id'] },\n { envVar: 'WATSONX_URL', secretKeys: ['watsonx-url', 'ibm-watsonx-url'] },\n // Snowflake Cortex — uses Personal Access Token (PAT), not an API key.\n // Multiple side-cars required by the SnowflakeCortexAdapter:\n // SNOWFLAKE_ACCOUNT (orgname-accountname), SNOWFLAKE_USER (key-pair\n // auth user), SNOWFLAKE_BASE_URL (account-specific host). Side-cars\n // wired here so a single GCP provisioning round materializes the full\n // tuple at boot.\n { envVar: 'SNOWFLAKE_PAT', secretKeys: ['snowflake-pat', 'snowflake-token', 'snowflake-key'] },\n { envVar: 'SNOWFLAKE_ACCOUNT', secretKeys: ['snowflake-account'] },\n { envVar: 'SNOWFLAKE_USER', secretKeys: ['snowflake-user', 'snowflake-username'] },\n { envVar: 'SNOWFLAKE_BASE_URL', secretKeys: ['snowflake-base-url', 'snowflake-url'] },\n // SAP AI Core — OAuth2 client_credentials flow. Catalog providerId is\n // `sap`. The apiKeyEnvVar is `SAP_AI_CORE_CLIENT_ID` (the OAuth client\n // id); the matching client_secret + token URL + base URL are all\n // documented in the catalog row's `extraEnvVars` and required by the\n // SapAiCoreAdapter. Wiring all four together so a single provisioning\n // round closes the loop.\n { envVar: 'SAP_AI_CORE_CLIENT_ID', secretKeys: ['sap-ai-core-client-id', 'sap-client-id'] },\n { envVar: 'SAP_AI_CORE_CLIENT_SECRET', secretKeys: ['sap-ai-core-client-secret', 'sap-client-secret'] },\n { envVar: 'SAP_AI_CORE_AUTH_URL', secretKeys: ['sap-ai-core-auth-url', 'sap-auth-url'] },\n { envVar: 'SAP_AI_CORE_BASE_URL', secretKeys: ['sap-ai-core-base-url', 'sap-base-url'] },\n // W&B Inference side-car — WANDB_API_KEY (auth) is already wired in\n // Batch 7 above; WANDB_PROJECT is the project slug header required on\n // every chat call. Wiring here closes the side-car gap.\n { envVar: 'WANDB_PROJECT', secretKeys: ['wandb-project', 'wandb-project-slug'] },\n // Image-only providers (no LLM gate participation, see comment below).\n // bfl auth header is `x-key` (not Bearer) — see catalog `authHeaderName`.\n { envVar: 'BFL_API_KEY', secretKeys: ['bfl-key', 'bfl-api-key', 'flux-key', 'black-forest-labs-key'] },\n { envVar: 'RECRAFT_API_KEY', secretKeys: ['recraft-key', 'recraft-api-key'] },\n { envVar: 'RUNWAYML_API_KEY', secretKeys: ['runwayml-key', 'runwayml-api-key', 'runway-key'] },\n { envVar: 'TOPAZ_API_KEY', secretKeys: ['topaz-key', 'topaz-api-key', 'topaz-labs-key'] },\n // ── LOTE O (2026-07-10) — Apertis + Inception Labs onboarding ─────────\n // Operator pre-provisioned both GCP secrets ahead of the catalog rows\n // (ailin-apertis-key, ailin-inception-key). secretKeys match exactly —\n // the GCP provider auto-prepends the `ailin-` prefix (see file header).\n { envVar: 'APERTIS_API_KEY', secretKeys: ['apertis-key', 'apertis-api-key'] },\n { envVar: 'INCEPTION_API_KEY', secretKeys: ['inception-key', 'inception-api-key'] },\n // ── LOTE P (2026-07-11) — EmpirioLabs AI, live-validated after gate ────\n { envVar: 'EMPIRIOLABS_API_KEY', secretKeys: ['empiriolabs-key', 'empiriolabs-api-key'] },\n // ── LOTE Q (2026-07-12) — Concentrate AI ───────────────────────────────\n { envVar: 'CONCENTRATE_API_KEY', secretKeys: ['concentrate-key', 'concentrate-api-key'] },\n // ── LOTE R (2026-07-13) — FastRouter ────────────────────────────────────\n { envVar: 'FASTROUTER_API_KEY', secretKeys: ['fastrouter-key', 'fastrouter-api-key'] },\n // ── LOTE S (2026-07-13) — Perplexity Agent API ──────────────────────────\n // Same physical credential as PERPLEXITY_API_KEY above (one Perplexity\n // account, two API surfaces) — deliberately sources from the SAME GCP\n // secret names, not a new one.\n { envVar: 'PERPLEXITY_AGENT_API_KEY', secretKeys: ['perplexity-api-key', 'perplexity-key'] },\n // ── LOTE T (2026-07-13) — Ailin gateway (self-referential meta-provider) ──\n // No GCP secret provisioned yet — inert until an operator sets AILIN_API_KEY\n // for a self-hosted deployment. secretKeys follow the same naming\n // convention as the rest of this file in case one is provisioned later.\n { envVar: 'AILIN_API_KEY', secretKeys: ['ailin-key', 'ailin-api-key', 'gateway-key'] },\n] as const;\n\n// ─── Provider Key Status Tracking ──────────────────────────────────────────\n// Tracks which provider API keys were successfully loaded, from which source,\n// and when. Used by the Self-Healing Discovery (L1) to know when to retry\n// discovery for native providers whose keys arrived late (GCP delay/failure).\n\nexport interface ProviderKeyStatus {\n loaded: boolean;\n source: 'gcp' | 'env' | 'none';\n timestamp: Date;\n envVar: string;\n}\n\n/**\n * Module-level state: populated during loadSecretsIntoEnv() for each provider.\n * Key = normalized provider name (lowercase, e.g., 'openai', 'anthropic').\n */\nconst providerKeyStatusMap = new Map<string, ProviderKeyStatus>();\n\n/**\n * Mapping from env var name to normalized provider name.\n * Allows the discovery service to map env vars to provider sources.\n */\nconst ENV_VAR_TO_PROVIDER: Record<string, string> = {\n // Native API providers\n OPENAI_API_KEY: 'openai',\n ANTHROPIC_API_KEY: 'anthropic',\n GOOGLE_API_KEY: 'google',\n DEEPSEEK_API_KEY: 'deepseek',\n XAI_API_KEY: 'xai',\n MISTRAL_API_KEY: 'mistral',\n COHERE_API_KEY: 'cohere',\n JINA_API_KEY: 'jina',\n // Cloud hubs — 2026-04-23: use canonical providerId 'nvidia' (legacy\n // alias 'nvidia-hub' was absorbed into the nvidia catalog row per Lot B;\n // discovery/status consumers read providerKeyStatus by canonical id).\n NVIDIA_API_KEY: 'nvidia',\n AIHUBMIX_API_KEY: 'aihubmix',\n NOVITA_API_KEY: 'novita',\n MOONSHOT_API_KEY: 'moonshot',\n MINIMAX_API_KEY: 'minimax',\n FRIENDLI_API_KEY: 'friendli',\n AIML_API_KEY: 'aiml',\n IMAGEROUTER_API_KEY: 'imagerouter',\n COMETAPI_API_KEY: 'cometapi',\n NANOGPT_API_KEY: 'nanogpt',\n EDENAI_API_KEY: 'edenai',\n HELICONEAI_API_KEY: 'heliconeai',\n POE_API_KEY: 'poe',\n ROUTEWAY_API_KEY: 'routeway',\n REQUESTY_API_KEY: 'requesty',\n // Post-migration canonical providerId is `ai302` (see providers.catalog.ts\n // and the residue-closure note in provider-registry.ts). Legacy `302ai`\n // survives as a catalog alias for pre-migration user configs.\n AI302_API_KEY: 'ai302',\n ORQAI_API_KEY: 'orqai',\n // Routers/aggregators\n OPENROUTER_API_KEY: 'openrouter',\n // Cloud providers\n VERTEX_AI_API_KEY: 'vertex-ai',\n AWS_ACCESS_KEY_ID: 'aws-bedrock',\n AWS_BEARER_TOKEN_BEDROCK: 'aws-bedrock',\n // SageMaker uses the same shared AWS creds as Bedrock, so `AWS_ACCESS_KEY_ID`\n // keeps mapping to `aws-bedrock` (first-listed wins) — but the endpoint-\n // specific env var is only meaningful for SageMaker. When operators wire\n // this, Self-Healing Discovery will attribute the key presence to the\n // SageMaker provider specifically.\n AWS_SAGEMAKER_ENDPOINT_NAME: 'aws-sagemaker',\n AZURE_OPENAI_API_KEY: 'azure-openai',\n // Regional/specialized\n QWEN_API_KEY: 'qwen',\n ERNIE_API_KEY: 'ernie',\n // Audio providers\n DEEPGRAM_API_KEY: 'deepgram',\n CARTESIA_API_KEY: 'cartesia',\n ELEVENLABS_API_KEY: 'elevenlabs',\n // Inference + embedding providers newly onboarded 2026-04-22\n WANDB_API_KEY: 'wandb',\n VOYAGE_API_KEY: 'voyage',\n BYTEZ_API_KEY: 'bytez',\n CEREBRAS_API_KEY: 'cerebras',\n FEATHERLESS_AI_API_KEY: 'featherless-ai', // canonical catalog id (2026-04-23 drift fix)\n // Batch 6 — enterprise hyperscaler gateways (2026-04-22). Gemini OAI-compat\n // is registered as a SEPARATE provider from the native `google` entry so\n // the self-healing discovery service can track them independently (they\n // have different failure modes — native uses @google/generative-ai SDK\n // with its own error surfaces; OAI-compat uses standard fetch).\n GEMINI_API_KEY: 'gemini-openai',\n GITHUB_TOKEN: 'github-models',\n DATABRICKS_TOKEN: 'databricks',\n // Orphan closure (2026-04-22) — Inworld now has a catalog row + adapter\n // factory. Mapping the env var to the provider slug lets Self-Healing\n // Discovery attribute key presence to the correct provider.\n INWORLD_API_KEY: 'inworld',\n // Orphan-closure Lot B (2026-04-23). Slug = catalog providerId. Writer\n // and Upstage are self-evident; Reka's slug is `rekaai` (the catalog\n // row's canonical providerId — aliases 'reka' + 'reka-ai' resolve to it).\n WRITER_API_KEY: 'writer',\n UPSTAGE_API_KEY: 'upstage',\n REKA_API_KEY: 'rekaai',\n // ── LOTE M (2026-04-23) — complement lot, 13 providers ──────────────\n // Slug = catalog providerId (NOT the upstream brand for phala/qianfan\n // which intentionally diverge — see catalog rows for rationale):\n // phala — canonical slug; upstream brand is \"RedPill\"\n // qianfan — canonical slug; upstream brand is \"Baidu Qianfan / ERNIE\"\n // gmi — canonical slug; upstream brand is \"GMICloud\"\n ARCEE_API_KEY: 'arcee',\n ATLASCLOUD_API_KEY: 'atlascloud',\n AVIAN_API_KEY: 'avian',\n QIANFAN_API_KEY: 'qianfan',\n GMI_API_KEY: 'gmi',\n INFERMATIC_API_KEY: 'infermatic',\n INFLECTION_API_KEY: 'inflection',\n MANCER_API_KEY: 'mancer',\n PHALA_API_KEY: 'phala',\n RELACE_API_KEY: 'relace',\n SILICONFLOW_API_KEY: 'siliconflow',\n STEPFUN_API_KEY: 'stepfun',\n VENICE_API_KEY: 'venice',\n // ── Sublote E1 (2026-04-24) — runtime-wiring closure for D1 matrix ───\n // 7 provider slugs whose env vars gained GCP→ENV tuples in this lot.\n // Slug = catalog providerId exactly (so Self-Healing Discovery can\n // attribute key-loaded events to the right provider). CLOUDFLARE_ACCOUNT_ID\n // is intentionally absent — that's a URL parameter, not an auth credential,\n // so mapping it to a provider slug would be semantically wrong (the same\n // account-id could conceivably scope multiple Cloudflare providers).\n GROQ_API_KEY: 'groq',\n DEEPINFRA_API_KEY: 'deepinfra',\n HF_TOKEN: 'huggingface',\n CLOUDFLARE_API_TOKEN: 'cloudflare-workers-ai',\n PERPLEXITY_API_KEY: 'perplexity',\n FIREWORKS_AI_API_KEY: 'fireworks-ai',\n SAMBANOVA_API_KEY: 'sambanova',\n // Replicate — pre-existing live-validation gap closed by sublote E1.\n // See the matching comment on the PROVIDER_SECRETS tuple above.\n REPLICATE_API_KEY: 'replicate',\n // ── LOTE N (2026-04-27) — catalog↔loader closure (22 new env vars) ──\n // Every entry below maps an env var to its canonical providerId so the\n // L1 Self-Healing Discovery Service can attribute key-loaded events\n // when the corresponding GCP secret is provisioned. Slugs match the\n // catalog providerId exactly (NOT upstream brand) — see catalog rows\n // for cases where they diverge (e.g. `sap` not `sap-ai-core`, `v0` not\n // `vercel-v0`, `volcano` not `volcengine`). Side-car env vars\n // (WATSONX_PROJECT_ID, SNOWFLAKE_ACCOUNT, SAP_AI_CORE_CLIENT_SECRET,\n // etc.) are NOT mapped here — they're auth or URL parameters, not\n // credentials, so attributing them to a provider would conflate\n // credential-presence with side-car-completeness in discovery telemetry.\n TOGETHERAI_API_KEY: 'togetherai',\n ANYSCALE_API_KEY: 'anyscale',\n HYPERBOLIC_API_KEY: 'hyperbolic',\n CHUTES_API_KEY: 'chutes',\n NSCALE_API_KEY: 'nscale',\n NEBIUS_API_KEY: 'nebius',\n LAMBDA_AI_API_KEY: 'lambda-ai',\n SCALEWAY_API_KEY: 'scaleway',\n SYNTHETIC_API_KEY: 'synthetic',\n MORPH_API_KEY: 'morph',\n ZAI_API_KEY: 'zai',\n XIAOMI_MIMO_API_KEY: 'xiaomi-mimo',\n V0_API_KEY: 'v0',\n VERCEL_AI_GATEWAY_API_KEY: 'vercel-ai-gateway',\n VOLCANO_API_KEY: 'volcano',\n WATSONX_APIKEY: 'watsonx',\n SNOWFLAKE_PAT: 'snowflake',\n SAP_AI_CORE_CLIENT_ID: 'sap',\n // Image / video-only providers. Still mapped to a slug so Self-Healing\n // Discovery can record key-presence events — but their env vars do NOT\n // appear in LLM_PROVIDER_ENV_VARS (image surfaces don't satisfy the\n // \"at least one LLM key\" boot gate).\n BFL_API_KEY: 'bfl',\n RECRAFT_API_KEY: 'recraft',\n RUNWAYML_API_KEY: 'runwayml',\n TOPAZ_API_KEY: 'topaz',\n // ── LOTE O (2026-07-10) ────────────────────────────────────────────────\n APERTIS_API_KEY: 'apertis',\n INCEPTION_API_KEY: 'inception',\n // ── LOTE P (2026-07-11) ────────────────────────────────────────────────\n EMPIRIOLABS_API_KEY: 'empiriolabs',\n // ── LOTE Q (2026-07-12) ────────────────────────────────────────────────\n CONCENTRATE_API_KEY: 'concentrate',\n // ── LOTE R (2026-07-13) ────────────────────────────────────────────────\n FASTROUTER_API_KEY: 'fastrouter',\n // ── LOTE S (2026-07-13) ────────────────────────────────────────────────\n PERPLEXITY_AGENT_API_KEY: 'perplexity-agent',\n // ── LOTE T (2026-07-13) ────────────────────────────────────────────────\n AILIN_API_KEY: 'ailin',\n};\n\n/**\n * Returns the status of all provider API keys.\n * Used by Self-Healing Discovery to determine which native sources can run.\n */\nexport function getProviderKeyStatus(): ReadonlyMap<string, ProviderKeyStatus> {\n return providerKeyStatusMap;\n}\n\n/**\n * Returns provider names that have valid (non-empty, non-mock) API keys loaded.\n */\nexport function getLoadedProviderNames(): string[] {\n return Array.from(providerKeyStatusMap.entries())\n .filter(([, status]) => status.loaded)\n .map(([name]) => name);\n}\n\n/**\n * Check if a specific provider has a loaded API key.\n */\nexport function isProviderKeyLoaded(providerName: string): boolean {\n return providerKeyStatusMap.get(providerName.toLowerCase())?.loaded ?? false;\n}\n\nconst LLM_PROVIDER_ENV_VARS = [\n 'OPENAI_API_KEY',\n 'ANTHROPIC_API_KEY',\n 'GOOGLE_API_KEY',\n 'DEEPSEEK_API_KEY',\n 'XAI_API_KEY',\n 'MISTRAL_API_KEY',\n 'COHERE_API_KEY',\n 'NVIDIA_API_KEY',\n 'AIHUBMIX_API_KEY',\n 'NOVITA_API_KEY',\n 'MOONSHOT_API_KEY',\n 'MINIMAX_API_KEY',\n 'JINA_API_KEY',\n 'FRIENDLI_API_KEY',\n 'AIML_API_KEY',\n 'IMAGEROUTER_API_KEY',\n 'OPENROUTER_API_KEY',\n 'ORQAI_API_KEY',\n 'EDENAI_API_KEY',\n 'HELICONEAI_API_KEY',\n 'VERTEX_AI_API_KEY',\n 'AWS_BEARER_TOKEN_BEDROCK',\n 'AZURE_OPENAI_API_KEY',\n 'QWEN_API_KEY',\n 'ERNIE_API_KEY',\n 'COMETAPI_API_KEY',\n 'NANOGPT_API_KEY',\n 'REQUESTY_API_KEY',\n 'AI302_API_KEY',\n 'POE_API_KEY',\n 'ROUTEWAY_API_KEY',\n // Added 2026-04-22 — these keys now count toward the \"at least one LLM key\n // present\" gate that avoids DEGRADED_SELF_HOSTED mode at boot.\n 'WANDB_API_KEY',\n 'VOYAGE_API_KEY',\n 'CEREBRAS_API_KEY',\n 'FEATHERLESS_AI_API_KEY',\n // Batch 4 landed BYTEZ catalog row + adapter, so BYTEZ_API_KEY now counts\n // toward the LLM-key-present gate.\n 'BYTEZ_API_KEY',\n // Batch 6 — enterprise hyperscaler gateways (2026-04-22). These keys now\n // count toward the \"at least one LLM key present\" gate; all four have\n // landed catalog rows + adapter classes in this batch.\n 'GEMINI_API_KEY',\n 'GITHUB_TOKEN',\n 'DATABRICKS_TOKEN',\n // Batch 8.1 — AWS SageMaker. Endpoint name is the meaningful trigger:\n // AWS creds alone are ambiguous (could be Bedrock or S3), but an\n // endpoint name implies \"SageMaker is provisioned here\".\n 'AWS_SAGEMAKER_ENDPOINT_NAME',\n // Orphan closure (2026-04-22) — Inworld AI now counts toward the\n // \"at least one LLM key present\" gate now that the catalog row +\n // adapter factory are wired.\n 'INWORLD_API_KEY',\n // Orphan-closure Lot B (2026-04-23). All three are fully canonical now.\n 'WRITER_API_KEY',\n 'UPSTAGE_API_KEY',\n 'REKA_API_KEY',\n // ── LOTE M (2026-04-23) complement lot ──────────────────────────────\n // Each of the 13 keys below counts toward the \"at least one LLM key\n // present\" gate, the same way all catalog-wired keys do. Note that\n // mancer and venice carry contentPolicyClass=uncensored (2026-04-28\n // Phase 4b — the prior denyByDefault gate was removed per the universal\n // \"habilitado e nunca censurado\" policy); their keys count toward the\n // gate exactly like any other LLM key — surface-level filtering is now\n // a downstream concern, not a routing one.\n // qianfan here is QIANFAN_API_KEY (v2 bce-v3 bearer); the legacy v1\n // ERNIE_API_KEY was already listed above this marker.\n 'ARCEE_API_KEY',\n 'ATLASCLOUD_API_KEY',\n 'AVIAN_API_KEY',\n 'QIANFAN_API_KEY',\n 'GMI_API_KEY',\n 'INFERMATIC_API_KEY',\n 'INFLECTION_API_KEY',\n 'MANCER_API_KEY',\n 'PHALA_API_KEY',\n 'RELACE_API_KEY',\n 'SILICONFLOW_API_KEY',\n 'STEPFUN_API_KEY',\n 'VENICE_API_KEY',\n // ── Sublote E1 (2026-04-24) — runtime-wiring closure for D1 matrix ───\n // These 7 providers landed in CONSOLIDATION_MATRIX['live-validation']\n // during Sublote D1 but had no GCP→ENV tuple above → their keys could\n // not count toward the \"at least one LLM key present\" gate even when\n // GCP Secret Manager held the key. Listed here so a restart after the\n // operator's provisioning cycle propagates the key-present signal to\n // the DEGRADED_SELF_HOSTED avoidance logic. CLOUDFLARE_ACCOUNT_ID is\n // intentionally absent — it's a URL parameter, not an LLM credential.\n 'GROQ_API_KEY',\n 'DEEPINFRA_API_KEY',\n 'HF_TOKEN',\n 'CLOUDFLARE_API_TOKEN',\n 'PERPLEXITY_API_KEY',\n 'FIREWORKS_AI_API_KEY',\n 'SAMBANOVA_API_KEY',\n // Replicate — pre-existing live-validation gap closed by sublote E1.\n // See the matching comments on the PROVIDER_SECRETS tuple and\n // ENV_VAR_TO_PROVIDER entry above.\n 'REPLICATE_API_KEY',\n // ── LOTE N (2026-04-27) — catalog↔loader closure ─────────────────────\n // 18 LLM-class providers added to the boot-mode gate. The 4 image/\n // video-only providers (bfl, recraft, runwayml, topaz) are\n // intentionally OMITTED — their key presence does not signal the\n // server has any text-generation capability, so they should not\n // satisfy the \"at least one LLM key present\" check.\n 'TOGETHERAI_API_KEY',\n 'ANYSCALE_API_KEY',\n 'HYPERBOLIC_API_KEY',\n 'CHUTES_API_KEY',\n 'NSCALE_API_KEY',\n 'NEBIUS_API_KEY',\n 'LAMBDA_AI_API_KEY',\n 'SCALEWAY_API_KEY',\n 'SYNTHETIC_API_KEY',\n 'MORPH_API_KEY',\n 'ZAI_API_KEY',\n 'XIAOMI_MIMO_API_KEY',\n 'V0_API_KEY',\n 'VERCEL_AI_GATEWAY_API_KEY',\n 'VOLCANO_API_KEY',\n 'WATSONX_APIKEY',\n 'SNOWFLAKE_PAT',\n 'SAP_AI_CORE_CLIENT_ID',\n // ── LOTE O (2026-07-10) — Apertis + Inception Labs, both chat-capable ──\n 'APERTIS_API_KEY',\n 'INCEPTION_API_KEY',\n // ── LOTE P (2026-07-11) — EmpirioLabs AI, chat-capable ─────────────────\n 'EMPIRIOLABS_API_KEY',\n // ── LOTE Q (2026-07-12) — Concentrate AI, chat-capable ─────────────────\n 'CONCENTRATE_API_KEY',\n // ── LOTE R (2026-07-13) — FastRouter, chat-capable ─────────────────────\n 'FASTROUTER_API_KEY',\n // ── LOTE S (2026-07-13) — Perplexity Agent API, chat-capable ───────────\n 'PERPLEXITY_AGENT_API_KEY',\n // ── LOTE T (2026-07-13) — Ailin gateway, chat-capable ──────────────────\n 'AILIN_API_KEY',\n] as const;\n\n/**\n * Self-hosted / sidecar endpoints that can keep the server alive in a\n * DEGRADED mode when every external LLM API key is missing.\n *\n * Design rationale:\n * Without this fallback, a single failure (expired GCP ADC, rotated\n * service account, network blip during boot) produces an infinite\n * restart loop. With it, the server boots in `degraded_self_hosted`\n * mode: only local/sidecar models are usable, requests are tagged\n * via last-resort-policy.ts (execution_mode=last_resort_self_hosted,\n * excluded_from_benchmark=true), and ops gets a clear warning\n * instead of a crash loop.\n *\n * This ties directly into Bloco P1.10 (self-hosted last-resort policy)\n * but at boot-time scope rather than per-request scope.\n */\nconst SELF_HOSTED_ENV_VARS = [\n 'OLLAMA_URL',\n 'SELF_HOSTED_LLM_URL',\n 'LOCAL_LLAMA_URL',\n 'LOCAL_KOBOLD_URL',\n] as const;\n\n/**\n * Load critical secrets from GCP into process.env\n * \n * This function is called AFTER SecretsManager initialization\n * and BEFORE config validation.\n * \n * It loads secrets from GCP and injects them into process.env,\n * allowing the existing config system to work without modification.\n */\nexport async function loadSecretsIntoEnv(): Promise<void> {\n const log = logger.child({ component: 'secrets-loader' });\n\n log.info('Loading critical secrets from GCP Secret Manager into environment...');\n\n let loaded = 0;\n let skipped = 0;\n let failed = 0;\n let sanitizedKeys = 0;\n let providerSecretsLoaded = 0;\n // Tracks env values that were authoritatively replaced by GCP fetches.\n // Useful diagnostic for operators verifying that GCP is the source of truth.\n let overwrittenFromGcp = 0;\n\n const forceLoadRealKeys = process.env.TEST_USE_REAL_API_KEYS === 'true';\n\n // ── GCP-authoritative mode ─────────────────────────────────────────────\n // When SECRETS_PROVIDER_PRIMARY=gcp the operator has declared GCP as the\n // source of truth. The legacy \"skip if env already populated\" path then\n // contradicts that declaration: env_file / docker-compose / shell exports\n // silently win and GCP fetches are never even attempted, which is exactly\n // the failure mode that hid populated GCP secrets (e.g. HF_TOKEN) from\n // /v1/models. In authoritative mode we ALWAYS attempt the GCP fetch and\n // overwrite the pre-existing env value when GCP returns one. The fallback\n // is symmetric: a GCP miss preserves the existing env value (no zeroing).\n //\n // Operators can override:\n // SECRETS_GCP_AUTHORITATIVE=false → keep legacy skip-if-exists semantics\n // SECRETS_GCP_AUTHORITATIVE=true → force authoritative even with env primary\n const gcpPrimaryEnv = (process.env.SECRETS_PROVIDER_PRIMARY || '').trim().toLowerCase() === 'gcp';\n const explicitAuthoritative = (process.env.SECRETS_GCP_AUTHORITATIVE || '').trim().toLowerCase();\n const gcpAuthoritative =\n explicitAuthoritative === 'true' ||\n (explicitAuthoritative !== 'false' && gcpPrimaryEnv);\n\n // Tests already use TEST_USE_REAL_API_KEYS=true to force GCP fetches even\n // when local mocks are present. The two flags are independent triggers\n // for the same effect; either one disables skip-if-exists.\n const authoritativeFetch = gcpAuthoritative || forceLoadRealKeys;\n\n log.info(\n {\n gcpPrimary: gcpPrimaryEnv,\n gcpAuthoritative,\n forceLoadRealKeys,\n authoritativeFetch,\n },\n 'Secret loading mode resolved'\n );\n\n // Load critical secrets (required)\n for (const { envVar, secretKey, required, defaultValue } of CRITICAL_SECRETS) {\n const existingValue = process.env[envVar];\n const shouldOverrideMockStripe =\n forceLoadRealKeys &&\n envVar.startsWith('STRIPE_') &&\n typeof existingValue === 'string' &&\n (existingValue.includes('mock') || existingValue.includes('test'));\n\n // Skip if already in env, UNLESS:\n // - we're in GCP-authoritative mode (operator declared GCP as source of truth), OR\n // - we're forcing real keys and detected mock Stripe keys.\n if (existingValue && !shouldOverrideMockStripe && !gcpAuthoritative) {\n log.debug({ envVar, source: 'env' }, 'Secret already in environment, skipping GCP load');\n skipped++;\n continue;\n }\n\n try {\n const value = await loadSecret(secretKey, required);\n if (value) {\n const wasOverwrite = Boolean(existingValue) && existingValue !== value;\n process.env[envVar] = value;\n if (wasOverwrite) overwrittenFromGcp++;\n log.debug(\n { envVar, secretKey, replaced: Boolean(existingValue), overwrittenFromGcp: wasOverwrite },\n 'Secret loaded from GCP into environment'\n );\n loaded++;\n } else if (existingValue && gcpAuthoritative) {\n // Authoritative fetch missed in GCP — fall back to the existing env\n // value rather than zeroing it. This keeps the system bootable when\n // a single secret is absent from GCP without losing the env-provided\n // value.\n log.debug(\n { envVar, secretKey },\n 'GCP miss in authoritative mode; preserving existing env value'\n );\n skipped++;\n } else if (defaultValue !== undefined) {\n // Use default value if secret not found and default provided\n process.env[envVar] = defaultValue;\n log.debug({ envVar, secretKey, defaultValue }, 'Secret not found, using default value');\n skipped++;\n } else if (!required) {\n log.debug({ envVar, secretKey }, 'Optional secret not found, skipping');\n skipped++;\n }\n } catch (error) {\n if (defaultValue !== undefined) {\n // Use default value on error if provided\n process.env[envVar] = defaultValue;\n log.warn({ envVar, secretKey, defaultValue, error: getErrorMessage(error) }, 'Failed to load secret, using default value');\n skipped++;\n } else if (required) {\n const errorMsg = getErrorMessage(error);\n log.error(\n { envVar, secretKey, error: errorMsg },\n 'Failed to load required secret'\n );\n failed++;\n throw new Error(`Failed to load required secret ${secretKey}: ${errorMsg}`);\n } else {\n log.warn({ envVar, secretKey, error: getErrorMessage(error) }, 'Failed to load optional secret');\n skipped++;\n }\n }\n }\n\n // Keep compatibility with components that still read GCS_BUCKET_NAME.\n if (!process.env.GCS_BUCKET_NAME && process.env.GCS_FILES_BUCKET) {\n process.env.GCS_BUCKET_NAME = process.env.GCS_FILES_BUCKET;\n }\n\n // Load provider API keys (optional)\n // CRITICAL: If GCP auth is available, ALWAYS load real keys (API Keys NUNCA DEVEM FALHAR)\n // If TEST_USE_REAL_API_KEYS=true, force load from GCP even if mock keys exist\n // Always replace mock keys with real keys from GCP when available\n // (reuses forceLoadRealKeys defined above)\n \n for (const { envVar, secretKeys } of PROVIDER_SECRETS) {\n const existingValue = process.env[envVar];\n const providerName = ENV_VAR_TO_PROVIDER[envVar];\n\n // Check if existing value looks like a mock key\n const isMockKey = existingValue && (existingValue.includes('mock') || existingValue.includes('test-'));\n\n // Skip if real key already exists, UNLESS:\n // - we're in GCP-authoritative mode (operator declared GCP as source of truth), OR\n // - test forces real keys (TEST_USE_REAL_API_KEYS=true), OR\n // - the existing value looks like a mock.\n // The authoritative path lets GCP overwrite stale env_file / shell-exported\n // values, which is the entire point of SECRETS_PROVIDER_PRIMARY=gcp.\n if (existingValue && !authoritativeFetch && !isMockKey) {\n // Record as loaded from env\n if (providerName) {\n providerKeyStatusMap.set(providerName, { loaded: true, source: 'env', timestamp: new Date(), envVar });\n }\n skipped++;\n continue;\n }\n\n try {\n let loadedSecretValue: string | undefined;\n let resolvedSecretKey = secretKeys[0];\n\n for (const candidateKey of secretKeys) {\n try {\n const candidateValue = await loadSecret(candidateKey, false);\n if (candidateValue) {\n loadedSecretValue = candidateValue;\n resolvedSecretKey = candidateKey;\n log.debug({ envVar, secretKey: candidateKey, valueLen: candidateValue.length }, 'Secret loaded from GCP');\n break;\n } else {\n log.debug({ envVar, secretKey: candidateKey }, 'Secret returned empty/null from GCP');\n }\n } catch (loadErr) {\n log.warn({ envVar, secretKey: candidateKey, error: loadErr instanceof Error ? loadErr.message : String(loadErr) }, 'Failed to load secret from GCP');\n continue;\n }\n }\n\n // GCP provider automatically adds prefix 'ailin-' to secretKey\n // So 'openai-key' becomes 'ailin-openai-key' in GCP Secret Manager\n const value = loadedSecretValue;\n if (value) {\n // Sanitize control characters and invisible Unicode.\n // PEM-format keys (-----BEGIN ...) legitimately contain newlines,\n // so use a lighter regex that preserves \\n and \\r for those values.\n const isPemKey = value.startsWith('-----BEGIN');\n let sanitizedValue = isPemKey\n ? value\n .replace(/[\\x00-\\x09\\x0B\\x0C\\x0E-\\x1F\\x7F]/g, '') // Strip control chars EXCEPT \\n (0x0A) and \\r (0x0D)\n .replace(/[\\u200B-\\u200D\\uFEFF]/g, '') // Remove zero-width spaces\n .trim()\n : value\n .replace(/[\\x00-\\x1F\\x7F]/g, '') // Strip ALL control characters\n .replace(/[\\u200B-\\u200D\\uFEFF]/g, '') // Remove zero-width spaces\n .trim();\n \n // Log detailed debugging information if sanitization changed the value\n if (sanitizedValue !== value) {\n const removedChars = value.length - sanitizedValue.length;\n sanitizedKeys++;\n log.debug(\n { \n envVar, \n secretKey: resolvedSecretKey,\n originalLength: value.length, \n sanitizedLength: sanitizedValue.length,\n removedChars\n },\n 'API key contained invalid characters and was sanitized'\n );\n }\n \n // Always replace mock keys with real keys\n const wasReplaced = !!existingValue;\n const wasOverwriteOfRealValue =\n wasReplaced && !isMockKey && existingValue !== sanitizedValue;\n\n // CRITICAL: Set process.env and immediately validate\n // Use Object.defineProperty as fallback if direct assignment doesn't work\n // This ensures the value is actually set in the environment\n const _previousValue = process.env[envVar];\n process.env[envVar] = sanitizedValue;\n\n // Immediately read back to verify (handles any potential async issues)\n const readBackValue = process.env[envVar];\n\n // Deep validation: check exact match, type, and length\n const isValidMatch = readBackValue === sanitizedValue;\n const isValidType = typeof readBackValue === 'string';\n const isValidLength = readBackValue !== undefined && readBackValue.length === sanitizedValue.length;\n\n if (!isValidMatch || !isValidType || !isValidLength) {\n log.error(\n {\n envVar,\n secretKey: resolvedSecretKey,\n sanitizedValueLength: sanitizedValue.length,\n readBackValueLength: readBackValue?.length,\n },\n 'Secret was loaded but process.env assignment did not persist'\n );\n failed++;\n } else {\n // Success - log and increment counter\n if (wasOverwriteOfRealValue) overwrittenFromGcp++;\n log.debug(\n {\n envVar,\n secretKey: resolvedSecretKey,\n replaced: wasReplaced,\n wasMock: isMockKey,\n overwrittenFromGcp: wasOverwriteOfRealValue,\n },\n 'Provider secret loaded from GCP'\n );\n loaded++;\n providerSecretsLoaded++;\n // Record successful load\n if (providerName) {\n providerKeyStatusMap.set(providerName, { loaded: true, source: 'gcp', timestamp: new Date(), envVar });\n }\n }\n } else {\n // GCP returned no value. Decide what's actually in process.env right\n // now: if env had a non-mock value before this loop iteration we\n // preserved it (the authoritative path entered this block but found\n // no GCP value to overwrite with), so we're effectively env-loaded.\n // If env had a mock value, we keep the mock as a degraded fallback\n // and warn loudly. If env had nothing, the provider stays orphaned.\n if (existingValue && !isMockKey) {\n // Authoritative-mode GCP miss with real env value already set:\n // the env value is still in process.env, so report source=env.\n if (providerName) {\n providerKeyStatusMap.set(providerName, { loaded: true, source: 'env', timestamp: new Date(), envVar });\n }\n log.debug(\n { envVar, secretKeys },\n 'GCP miss in authoritative mode; preserving existing env value for provider'\n );\n } else {\n if (isMockKey) {\n log.warn(\n { envVar, secretKeys },\n 'Mock API key detected but no real key found in GCP - using mock key (this may cause API failures)'\n );\n }\n // Record as not loaded (no GCP value, no usable env value)\n if (providerName) {\n providerKeyStatusMap.set(providerName, { loaded: false, source: 'none', timestamp: new Date(), envVar });\n }\n }\n skipped++;\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n // Only warn if not forcing real keys - if forcing, this is expected to work\n if (forceLoadRealKeys) {\n log.error(\n { envVar, secretKeys, error: errorMessage },\n 'Failed to load provider secret from GCP (TEST_USE_REAL_API_KEYS=true)'\n );\n failed++;\n } else {\n if (isMockKey) {\n log.warn(\n { envVar, secretKeys, error: errorMessage },\n 'Failed to load provider secret from GCP - will use mock key (this may cause API failures)'\n );\n } else {\n log.debug(\n { envVar, secretKeys, error: errorMessage },\n 'Failed to load provider secret from GCP (optional)'\n );\n }\n // Record failure\n if (providerName) {\n providerKeyStatusMap.set(providerName, { loaded: false, source: 'none', timestamp: new Date(), envVar });\n }\n skipped++;\n }\n }\n }\n\n // ── Env-var aliases (2026-04-28) ────────────────────────────────────────\n //\n // Some operators set a non-canonical name in `.env` for historical reasons\n // (e.g. `REKAAI_API_KEY` instead of the catalog-canonical `REKA_API_KEY`).\n // When GCP is reachable the secret loader populates the canonical name and\n // it just works; when GCP is unreachable the alias would be the only thing\n // set and the catalog row reads the canonical → empty key → silent skip.\n //\n // This pass copies known aliases into their canonical env var if the\n // canonical is empty. It is purely additive — never overwrites an\n // existing canonical value, never logs a warning unless an alias was\n // applied. Add new entries here when a similar mismatch is discovered.\n const ENV_ALIASES: ReadonlyArray<{ canonical: string; alias: string }> = [\n { canonical: 'REKA_API_KEY', alias: 'REKAAI_API_KEY' },\n ];\n for (const { canonical, alias } of ENV_ALIASES) {\n const canonicalValue = process.env[canonical];\n const aliasValue = process.env[alias];\n if ((canonicalValue == null || canonicalValue.trim() === '') && aliasValue && aliasValue.trim() !== '') {\n process.env[canonical] = aliasValue;\n log.info({ canonical, alias }, 'Promoted alias env var to canonical name');\n }\n }\n\n const gcpPrimary = (process.env.SECRETS_PROVIDER_PRIMARY || '').trim().toLowerCase() === 'gcp';\n const failFastEnv = (process.env.SECRETS_GCP_FAIL_FAST || '').trim().toLowerCase();\n const gcpFailFast = failFastEnv === 'true' || (failFastEnv !== 'false' && gcpPrimary);\n const hasAnyLlmProviderKey = LLM_PROVIDER_ENV_VARS.some((envVar) => {\n const value = process.env[envVar];\n return typeof value === 'string' && value.trim().length > 0;\n });\n\n // Hardening (P1.10 at boot scope): even when zero external API keys are\n // loaded, the server can still boot in DEGRADED mode if a self-hosted\n // endpoint is reachable. Every request will then be tagged via the\n // last-resort-policy as execution_mode=last_resort_self_hosted.\n const availableSelfHostedEndpoints = SELF_HOSTED_ENV_VARS.filter((envVar) => {\n const value = process.env[envVar];\n return typeof value === 'string' && value.trim().length > 0;\n });\n const hasAnySelfHostedFallback = availableSelfHostedEndpoints.length > 0;\n\n // Opt-out for strict production deployments that must never run in\n // degraded mode. Default: allow degraded boot (safer than crash loops).\n const allowDegradedBoot = (process.env.ALLOW_DEGRADED_BOOT || 'true').trim().toLowerCase() !== 'false';\n\n if (gcpFailFast && !hasAnyLlmProviderKey) {\n if (hasAnySelfHostedFallback && allowDegradedBoot) {\n log.warn(\n {\n gcpPrimary,\n gcpFailFast,\n providerSecretsLoaded,\n selfHostedEndpoints: availableSelfHostedEndpoints,\n mode: 'degraded_self_hosted',\n docs: 'docs/hardening/last-resort-policy',\n },\n '⚠️ DEGRADED BOOT: zero external LLM credentials, but self-hosted fallback is available. ' +\n 'Server will start in last-resort mode. Requests will be tagged excluded_from_benchmark. ' +\n 'Fix ADC/GCP credentials to restore full operation.'\n );\n // Intentionally fall through — boot continues\n } else {\n const message = hasAnySelfHostedFallback\n ? 'SECRETS_PROVIDER_PRIMARY=gcp, no LLM provider credentials loaded, and ALLOW_DEGRADED_BOOT=false.'\n : 'SECRETS_PROVIDER_PRIMARY=gcp but no LLM provider credentials are available after secrets bootstrap, ' +\n 'and no self-hosted fallback endpoints (OLLAMA_URL, SELF_HOSTED_LLM_URL, LOCAL_LLAMA_URL, LOCAL_KOBOLD_URL) are configured.';\n log.error(\n {\n gcpPrimary,\n gcpFailFast,\n providerSecretsLoaded,\n checkedEnvVars: LLM_PROVIDER_ENV_VARS,\n selfHostedEndpointsChecked: SELF_HOSTED_ENV_VARS,\n allowDegradedBoot,\n },\n message\n );\n throw new Error(\n `${message} Verify GCP Secret Manager access (ADC/Workload Identity) and secret names/prefix, ` +\n `or set OLLAMA_URL for degraded self-hosted boot, or run 'gcloud auth application-default login' if local.`\n );\n }\n }\n\n // Warn on partial provider key loading — helps detect GCP issues early\n if (hasAnyLlmProviderKey && providerSecretsLoaded > 0 && providerSecretsLoaded < 3) {\n log.warn(\n { providerSecretsLoaded, total: PROVIDER_SECRETS.length },\n `⚠️ Only ${providerSecretsLoaded} provider secrets loaded — possible partial GCP failure`\n );\n }\n\n // Audio provider diagnostics\n const audioProviders = {\n DEEPGRAM_API_KEY: !!process.env.DEEPGRAM_API_KEY,\n CARTESIA_API_KEY: !!process.env.CARTESIA_API_KEY,\n ELEVENLABS_API_KEY: !!process.env.ELEVENLABS_API_KEY,\n OLLAMA_URL: process.env.OLLAMA_URL || '',\n SELF_HOSTED_STT_URL: process.env.SELF_HOSTED_STT_URL || '',\n LOCAL_NLLB_URL: process.env.LOCAL_NLLB_URL || '',\n };\n log.info({ audioProviders }, 'Audio provider env vars after secrets loading');\n\n log.info(\n {\n loaded,\n skipped,\n failed,\n sanitizedKeys,\n providerSecretsLoaded,\n overwrittenFromGcp,\n gcpAuthoritative,\n total: CRITICAL_SECRETS.length + PROVIDER_SECRETS.length,\n },\n '✅ Secrets loading complete'\n );\n\n if (failed > 0) {\n throw new Error(`Failed to load ${failed} required secrets`);\n }\n}\n\n/**\n * Get summary of secrets loaded.\n *\n * Diagnostic helper for admin endpoints (e.g. /v1/admin/providers/...) that\n * surfaces exactly which provider envVars are populated and from where, so\n * operators can verify SECRETS_PROVIDER_PRIMARY=gcp is actually authoritative\n * and not silently shadowed by stale env values.\n */\nexport function getSecretsLoadSummary(): {\n fromGCP: string[];\n fromEnv: string[];\n notLoaded: string[];\n} {\n const fromGCP: string[] = [];\n const fromEnv: string[] = [];\n const notLoaded: string[] = [];\n\n for (const status of providerKeyStatusMap.values()) {\n if (!status.loaded) {\n notLoaded.push(status.envVar);\n continue;\n }\n if (status.source === 'gcp') {\n fromGCP.push(status.envVar);\n } else if (status.source === 'env') {\n fromEnv.push(status.envVar);\n }\n }\n\n return { fromGCP, fromEnv, notLoaded };\n}\n"}, "35c17c7fd0d958b365820b26ba4c14e50719571d": {"t": 1785754626.2556858, "n": 61996, "h": "8c7520874e1ad467", "c": "// Copyright (C) 2026 Ailin One, Inc.\n//\n// This file is part of Collective Intelligence Engine (ci).\n// Licensed under the GNU Affero General Public License v3.0 or later.\n// See LICENSE in the repository root, or <https://www.gnu.org/licenses/>.\n//\n// SPDX-License-Identifier: AGPL-3.0-or-later\n// Source: https://github.com/ailinone/collective-intelligence\n\n/**\n * Load Secrets into Environment\n * \n * This module loads critical secrets from GCP Secret Manager\n * and injects them into process.env BEFORE config validation.\n * \n * This allows the existing config system to work without refactoring,\n * while still using GCP Secret Manager as the source of truth.\n * \n * Call order:\n * 1. initializeSecretsManager()\n * 2. loadSecretsIntoEnv() ← This file\n * 3. validateConfig()\n */\n\nimport { logger } from '@/utils/logger';\nimport { getErrorMessage } from '@/utils/type-guards';\nimport { loadSecret } from './secrets-loader';\n\n/**\n * Critical secrets that must be loaded from GCP\n * These are injected into process.env for config compatibility\n */\ninterface CriticalSecret {\n envVar: string;\n secretKey: string;\n required: boolean;\n defaultValue?: string;\n}\n\nconst CRITICAL_SECRETS: CriticalSecret[] = [\n { envVar: 'DATABASE_URL', secretKey: 'database-url', required: true },\n { envVar: 'JWT_SECRET', secretKey: 'jwt-secret', required: true },\n { envVar: 'REDIS_PASSWORD', secretKey: 'redis-password', required: false, defaultValue: '' },\n { envVar: 'GCS_FILES_BUCKET', secretKey: 'gcs-files-bucket', required: false },\n { envVar: 'SMTP_PASS', secretKey: 'smtp-pass', required: false },\n { envVar: 'STRIPE_SECRET_KEY', secretKey: 'stripe-secret-key', required: false },\n { envVar: 'STRIPE_WEBHOOK_SECRET', secretKey: 'stripe-webhook-secret', required: false },\n { envVar: 'STRIPE_PUBLISHABLE_KEY', secretKey: 'stripe-publishable-key', required: false },\n // Phase 2c shadow ensemble bearer token. Optional because the coord-\n // stable wire is disabled by default in production\n // (CI_ENSEMBLE_COORDINATOR_ENABLED=false). When operator flips it on,\n // the GCP secret `coord-ensemble-token` MUST also be set with the\n // value to share with coord-serving's COORD_SERVING_AUTH_TOKEN.\n { envVar: 'CI_ENSEMBLE_COORDINATOR_TOKEN', secretKey: 'coord-ensemble-token', required: false },\n];\n\n/**\n * Provider API keys (loaded from GCP if not in env)\n * CRITICAL: GCP provider automatically adds prefix 'ailin-' to secret keys\n * So 'openai-key' becomes 'ailin-openai-key' in GCP Secret Manager\n * These keys match the actual secret names in GCP (without the prefix, provider adds it)\n */\ninterface ProviderSecretBinding {\n envVar: string;\n secretKeys: readonly string[];\n}\n\nexport const PROVIDER_SECRETS: readonly ProviderSecretBinding[] = [\n { envVar: 'OPENAI_API_KEY', secretKeys: ['openai-key'] },\n { envVar: 'ANTHROPIC_API_KEY', secretKeys: ['anthropic-key'] },\n { envVar: 'GOOGLE_API_KEY', secretKeys: ['google-key'] },\n { envVar: 'DEEPSEEK_API_KEY', secretKeys: ['deepseek-key'] },\n { envVar: 'XAI_API_KEY', secretKeys: ['xai-key'] },\n { envVar: 'MISTRAL_API_KEY', secretKeys: ['mistral-key'] },\n { envVar: 'COHERE_API_KEY', secretKeys: ['cohere-key'] },\n { envVar: 'NVIDIA_API_KEY', secretKeys: ['nvidia-key', 'nvidia-api-key'] },\n { envVar: 'AIHUBMIX_API_KEY', secretKeys: ['aihubmix-key', 'aihubmix-api-key'] },\n { envVar: 'NOVITA_API_KEY', secretKeys: ['novita-key', 'novita-api-key'] },\n { envVar: 'MOONSHOT_API_KEY', secretKeys: ['moonshot-key', 'moonshot-api-key'] },\n { envVar: 'MINIMAX_API_KEY', secretKeys: ['minimax-key', 'minimax-api-key'] },\n { envVar: 'JINA_API_KEY', secretKeys: ['jina-key', 'jina-api-key'] },\n { envVar: 'GOOGLE_MAPS_API_KEY', secretKeys: ['google-maps-key', 'google-maps-api-key'] },\n { envVar: 'FRIENDLI_API_KEY', secretKeys: ['friendli-key', 'friendli-api-key'] },\n { envVar: 'FRIENDLI_TEAM_ID', secretKeys: ['friendli-team-id'] },\n { envVar: 'AIML_API_KEY', secretKeys: ['aiml-key', 'aiml-api-key'] },\n { envVar: 'IMAGEROUTER_API_KEY', secretKeys: ['imagerouter-key', 'imagerouter-api-key'] },\n { envVar: 'OPENROUTER_API_KEY', secretKeys: ['openrouter-key'] },\n { envVar: 'COMETAPI_API_KEY', secretKeys: ['cometapi-key', 'cometapi-api-key'] },\n { envVar: 'NANOGPT_API_KEY', secretKeys: ['nanogpt-key', 'nanogpt-api-key'] },\n { envVar: 'REQUESTY_API_KEY', secretKeys: ['requesty-key', 'requesty-api-key'] },\n { envVar: 'AI302_API_KEY', secretKeys: ['302-key', '302ai-key'] },\n { envVar: 'POE_API_KEY', secretKeys: ['poe-key', 'poe-api-key'] },\n { envVar: 'ROUTEWAY_API_KEY', secretKeys: ['routeway-key', 'routeway-api-key'] },\n // Hub providers support both key naming conventions for compatibility.\n { envVar: 'ORQAI_API_KEY', secretKeys: ['orqai-key', 'orqai-api-key'] },\n { envVar: 'ORQAI_MODEL_DENYLIST', secretKeys: ['orqai-model-denylist'] },\n { envVar: 'EDENAI_API_KEY', secretKeys: ['edenai-key', 'edenai-api-key'] },\n { envVar: 'HELICONEAI_API_KEY', secretKeys: ['heliconeai-key', 'heliconeai-api-key'] },\n { envVar: 'VERTEX_AI_API_KEY', secretKeys: ['vertex-key'] },\n { envVar: 'VERTEX_AI_PROJECT_ID', secretKeys: ['vertex-project-id'] },\n { envVar: 'QWEN_API_KEY', secretKeys: ['qwen-key-2'] },\n { envVar: 'ALIBABA_ACCESS_KEY_ID', secretKeys: ['alibaba-key-id'] },\n { envVar: 'ALIBABA_ACCESS_KEY_SECRET', secretKeys: ['alibaba-key-secret'] },\n { envVar: 'ERNIE_API_KEY', secretKeys: ['baidu-key'] },\n { envVar: 'ERNIE_SECRET_KEY', secretKeys: ['baidu-secret'] },\n { envVar: 'BAIDU_BASE_URL', secretKeys: ['baidu-base-url'] },\n { envVar: 'AWS_ACCESS_KEY_ID', secretKeys: ['aws-key-id'] },\n { envVar: 'AWS_SECRET_ACCESS_KEY', secretKeys: ['aws-secret'] },\n { envVar: 'AWS_BEARER_TOKEN_BEDROCK', secretKeys: ['aws-bearer-token'] },\n // Batch 7.1: Bedrock region + inference-profile ARN can now be set via\n // GCP Secret Manager. Useful for deploys that route Bedrock to a specific\n // region (e.g. us-west-2) or via cross-region inference profiles without\n // baking these into the docker image.\n { envVar: 'AWS_BEDROCK_REGION', secretKeys: ['aws-bedrock-region'] },\n { envVar: 'AWS_BEDROCK_INFERENCE_PROFILE_ARN', secretKeys: ['aws-bedrock-inference-profile-arn'] },\n // Batch 8.1: SageMaker endpoint routing. A deploy can point this adapter\n // at a specific deployed endpoint via a secret rather than baking the\n // endpoint name into the image. The region override lets the adapter\n // hit an endpoint in a different region from the general AWS_REGION\n // used by other services (SES, S3).\n { envVar: 'AWS_SAGEMAKER_REGION', secretKeys: ['aws-sagemaker-region'] },\n { envVar: 'AWS_SAGEMAKER_ENDPOINT_NAME', secretKeys: ['aws-sagemaker-endpoint-name'] },\n { envVar: 'AWS_SAGEMAKER_PAYLOAD_SCHEMA', secretKeys: ['aws-sagemaker-payload-schema'] },\n { envVar: 'AWS_SAGEMAKER_CUSTOM_ATTRIBUTES', secretKeys: ['aws-sagemaker-custom-attributes'] },\n { envVar: 'AZURE_OPENAI_API_KEY', secretKeys: ['azure-openai-api-key'] },\n { envVar: 'AZURE_OPENAI_ENDPOINT', secretKeys: ['azure-openai-endpoint'] },\n { envVar: 'AZURE_OPENAI_DEPLOYMENT', secretKeys: ['azure-openai-deployment'] },\n // Audio-first providers (STT, TTS, STS)\n { envVar: 'DEEPGRAM_API_KEY', secretKeys: ['deepgram-key'] },\n { envVar: 'CARTESIA_API_KEY', secretKeys: ['cartesia-key'] },\n { envVar: 'ELEVENLABS_API_KEY', secretKeys: ['elevenlabs-key'] },\n // Translation provider\n { envVar: 'PALABRAAI_CLIENT_ID', secretKeys: ['palabraai-id'] },\n { envVar: 'PALABRAAI_CLIENT_SECRET', secretKeys: ['palabraai-key'] },\n { envVar: 'OCI_TENANCY_ID', secretKeys: ['oci-tenancy-id'] },\n { envVar: 'OCI_USER_ID', secretKeys: ['oci-user-id'] },\n { envVar: 'OCI_FINGERPRINT', secretKeys: ['oci-fingerprint'] },\n { envVar: 'OCI_PRIVATE_KEY', secretKeys: ['oci-private-key'] },\n { envVar: 'OCI_REGION', secretKeys: ['oci-region'] },\n // 2026-04-22: New GCP secrets (5 providers). The GCP provider auto-prefixes\n // 'ailin-', so 'wandb-key' maps to ailin-wandb-key in Secret Manager.\n // Voyage lists both 'voyageai-key' (current upstream name) and 'voyage-key'\n // (conventional fallback) because the canonical spelling is still unstable.\n { envVar: 'WANDB_API_KEY', secretKeys: ['wandb-key', 'wandb-api-key'] },\n { envVar: 'VOYAGE_API_KEY', secretKeys: ['voyageai-key', 'voyage-key', 'voyage-api-key'] },\n { envVar: 'BYTEZ_API_KEY', secretKeys: ['bytez-key', 'bytez-api-key'] },\n { envVar: 'CEREBRAS_API_KEY', secretKeys: ['cerebras-key', 'cerebras-api-key'] },\n { envVar: 'FEATHERLESS_AI_API_KEY', secretKeys: ['featherless-key', 'featherless-api-key'] },\n // Batch 6 — enterprise hyperscaler gateways (2026-04-22).\n // Azure OpenAI already has API_KEY/ENDPOINT/DEPLOYMENT wired above;\n // these fill in the remaining URL-template parameters.\n { envVar: 'AZURE_OPENAI_RESOURCE_NAME', secretKeys: ['azure-openai-resource-name', 'azure-openai-resource'] },\n { envVar: 'AZURE_OPENAI_API_VERSION', secretKeys: ['azure-openai-api-version'] },\n // Gemini OAI-compat shares the API key with the native `google` provider.\n // Two env-var aliases: GEMINI_API_KEY (canonical upstream) and\n // GOOGLE_AI_STUDIO_API_KEY (legacy). The adapter reads either.\n //\n // 2026-04-23: `vertex-key` added as a fallback source because it was\n // verified working against generativelanguage.googleapis.com (HTTP 200,\n // 50 Gemini models) while `google-key` is suspended (403\n // CONSUMER_SUSPENDED). The Vertex/Gemini API-key split is mostly\n // historical — a Google Cloud API key scoped for Gemini works equally on\n // aistudio and on the generativelanguage surface, as long as the key's\n // GCP project has the API enabled.\n //\n // 2026-07-25: `vertex-key` was originally placed LAST on the (incorrect)\n // assumption that the loop below tries each candidate live and keeps the\n // first one that WORKS. It doesn't — it takes the first candidate that\n // merely EXISTS in Secret Manager (see the resolution loop a few hundred\n // lines down), so the suspended `google-key` always won and gemini-openai\n // stayed broken. Moved `vertex-key` first since it's the only one\n // confirmed live-working; the others remain as fallbacks if it's ever\n // rotated out.\n { envVar: 'GEMINI_API_KEY', secretKeys: ['vertex-key', 'gemini-key', 'google-ai-studio-key', 'google-key'] },\n // GitHub Models uses a GitHub PAT. Operators typically already have this\n // in env (it's the same PAT used for gh CLI); the GCP path is a fallback\n // for service accounts that need isolated tokens.\n { envVar: 'GITHUB_TOKEN', secretKeys: ['github-models-token', 'github-token', 'github-pat'] },\n // Databricks workspace token + config.\n { envVar: 'DATABRICKS_TOKEN', secretKeys: ['databricks-token', 'databricks-pat'] },\n { envVar: 'DATABRICKS_HOST', secretKeys: ['databricks-host'] },\n { envVar: 'DATABRICKS_SERVING_ENDPOINT', secretKeys: ['databricks-serving-endpoint'] },\n // Inworld AI — orphan closure 2026-04-22. Key is already base64-encoded\n // in GCP (132 chars) because Inworld uses Basic auth, not Bearer. The\n // adapter does NOT re-encode; it passes the secret verbatim into the\n // `Basic ` header. Never base64-encode this value at load time.\n { envVar: 'INWORLD_API_KEY', secretKeys: ['inworld-key', 'inworld-api-key'] },\n // ── Orphan-closure Lot B (2026-04-23) ──────────────────────────────────\n // Three providers closed in a single lot because they share identical\n // shape (thin OAI-compat wrappers) and were all already present in GCP\n // Secret Manager but not wired into env loading. Each tuple lists the\n // preferred secret key first and legacy-or-alternate names as fallbacks.\n { envVar: 'WRITER_API_KEY', secretKeys: ['writer-key', 'writer-api-key'] },\n { envVar: 'UPSTAGE_API_KEY', secretKeys: ['upstage-key', 'upstage-api-key'] },\n // Reka's canonical upstream name is \"reka\" but the key is stored in GCP\n // as `ailin-rekaai-key` (matching the catalog providerId, not the upstream\n // name). `reka-key` is kept as a fallback in case the secret is ever\n // renamed to match upstream naming later.\n { envVar: 'REKA_API_KEY', secretKeys: ['rekaai-key', 'reka-key', 'reka-api-key'] },\n // ── LOTE M (2026-04-23) — complement lot: 13 new env-var mappings. ─────\n // Every provider below landed a catalog row this lot and is classified\n // in CONSOLIDATION_MATRIX['credentials-missing'] (secret-absent sub-class\n // for 12, auth-incomplete for qianfan). Each mapping is wired NOW so the\n // moment an operator provisions the corresponding GCP secret, the key\n // propagates to process.env without a code change. The first element of\n // each secretKeys array is the preferred upstream-convention name; any\n // follow-up entries are fallbacks for historical or legacy aliases.\n { envVar: 'ARCEE_API_KEY', secretKeys: ['arcee-api-key', 'arcee-key'] },\n { envVar: 'ATLASCLOUD_API_KEY', secretKeys: ['atlascloud-key', 'atlascloud-api-key', 'atlas-cloud-key'] },\n { envVar: 'AVIAN_API_KEY', secretKeys: ['avian-key', 'avian-api-key', 'avian-io-key'] },\n // Qianfan (Baidu) v2 bce-v3 bearer. Distinct from the legacy v1 AK+SK\n // material in ERNIE_API_KEY / ERNIE_SECRET_KEY / BAIDU_BASE_URL above —\n // both mappings are intentionally kept so an operator can populate either\n // path (v1 OAuth or v2 bearer) depending on which runtime surface is\n // used. The canonical catalog row `qianfan` reads QIANFAN_API_KEY only.\n { envVar: 'QIANFAN_API_KEY', secretKeys: ['qianfan-key', 'qianfan-api-key', 'baidu-qianfan-key'] },\n { envVar: 'GMI_API_KEY', secretKeys: ['gmi-key', 'gmi-api-key', 'gmicloud-key'] },\n { envVar: 'INFERMATIC_API_KEY', secretKeys: ['infermatic-api-key', 'infermatic-key', 'totalgpt-key'] },\n { envVar: 'INFLECTION_API_KEY', secretKeys: ['inflection-key', 'inflection-api-key', 'inflection-ai-key'] },\n // Mancer (tagged contentPolicyClass=uncensored in catalog; per the universal\n // \"habilitado e nunca censurado\" policy from Phase 4b, 2026-04-28, the\n // catalog row is fully admitted to routing — the tag is informational so\n // downstream surfaces can opt to filter it from a moderated default list).\n { envVar: 'MANCER_API_KEY', secretKeys: ['mancer-key', 'mancer-api-key'] },\n // Phala / RedPill TEE — upstream docs use REDPILL_API_KEY but we wire\n // PHALA_API_KEY as the canonical env var (see apiKeyEnvVarOverrideReason\n // in catalog row `phala`). Legacy `redpill-*` secret names are accepted\n // as fallbacks so an operator storing under either convention works.\n { envVar: 'PHALA_API_KEY', secretKeys: ['phala-key', 'phala-api-key', 'redpill-key', 'redpill-api-key'] },\n { envVar: 'RELACE_API_KEY', secretKeys: ['relace-key', 'relace-api-key'] },\n { envVar: 'SILICONFLOW_API_KEY', secretKeys: ['siliconflow-api-key', 'siliconflow-key', 'silicon-flow-key'] },\n { envVar: 'STEPFUN_API_KEY', secretKeys: ['stepfun-api-key', 'stepfun-key', 'step-key'] },\n // Venice (tagged contentPolicyClass=uncensored in catalog). Same Phase-4b\n // treatment as Mancer — admitted, never censored, with the tag as the\n // audit trail for the policy choice.\n { envVar: 'VENICE_API_KEY', secretKeys: ['venice-key', 'venice-api-key', 'venice-ai-key'] },\n // ── Sublote E1 (2026-04-24) — runtime-wiring closure for D1 matrix ─────\n // Sublote D1 promoted 10 providers to CONSOLIDATION_MATRIX['live-validation']\n // based on direct-probe evidence (via gcloud secrets versions access,\n // bypassing the runtime env loader). 7 of those had no GCP→ENV tuple here,\n // so the catalog-loader could never surface `process.env.<X>_API_KEY` at\n // boot — meaning live-validated providers were silently \"not runnable\".\n // Wired pre-emptively: the moment an operator provisions the GCP secret\n // AND the container restarts, propagation happens with zero code change.\n // The 3 already-mapped providers (heliconeai, github-models, infermatic)\n // are NOT duplicated — they appear above under their respective batches.\n { envVar: 'GROQ_API_KEY', secretKeys: ['groq-api-key', 'groq-key'] },\n { envVar: 'DEEPINFRA_API_KEY', secretKeys: ['deepinfra-api-key', 'deepinfra-key'] },\n // Hugging Face: upstream canonical env var is `HF_TOKEN` (not\n // HUGGINGFACE_API_KEY); the catalog row's apiKeyEnvVar matches. Primary\n // GCP secret name follows the upstream convention; legacy huggingface-*\n // aliases accepted for operators who stored it under the provider slug.\n { envVar: 'HF_TOKEN', secretKeys: ['huggingface-api-key', 'huggingface-token', 'hf-token', 'huggingface-key'] },\n // Cloudflare Workers AI — requires BOTH api-token AND account-id. The\n // CloudflareWorkersAIAdapter factory (default-adapter-factories.ts line\n // 229-246) reads CLOUDFLARE_ACCOUNT_ID from env and substitutes it into\n // the `{account_id}` URL template. Without the account-id side-car the\n // adapter ships the literal template to the wire → 404 on every call.\n // Side-car is wired here as a separate tuple because it's not an auth\n // credential — it's a URL parameter — so it doesn't belong in\n // ENV_VAR_TO_PROVIDER or LLM_PROVIDER_ENV_VARS below.\n { envVar: 'CLOUDFLARE_API_TOKEN', secretKeys: ['cloudflare-workers-ai-api-key', 'cloudflare-workers-ai-token', 'cloudflare-api-token', 'cloudflare-workers-ai-key'] },\n { envVar: 'CLOUDFLARE_ACCOUNT_ID', secretKeys: ['cloudflare-workers-ai-id', 'cloudflare-account-id', 'cloudflare-workers-ai-account-id'] },\n { envVar: 'PERPLEXITY_API_KEY', secretKeys: ['perplexity-api-key', 'perplexity-key'] },\n // Fireworks AI — note the catalog providerId is `fireworks-ai` (hyphenated)\n // but upstream brand/docs spell it \"Fireworks\" (no suffix). Four fallbacks\n // cover both conventions + api-key suffix variant.\n { envVar: 'FIREWORKS_AI_API_KEY', secretKeys: ['fireworks-ai-api-key', 'fireworks-ai-key', 'fireworks-key', 'fireworks-api-key'] },\n { envVar: 'SAMBANOVA_API_KEY', secretKeys: ['sambanova-api-key', 'sambanova-key'] },\n // Replicate — flagged by the sublote-e1-runtime-wiring invariant as a\n // pre-existing gap. `replicate` has been in CONSOLIDATION_MATRIX\n // ['live-validation'] since before D1, with adapterClass 'ReplicateAdapter'\n // and apiKeyEnvVar 'REPLICATE_API_KEY', but no tuple was ever added here.\n // Even though integrationMode is 'execution-only' (no /v1/models\n // discovery), the adapter still requires the env var at /predictions\n // call time. Wired here for closure; the GCP secret name follows the\n // provider-slug convention with optional `-api-key` fallback.\n { envVar: 'REPLICATE_API_KEY', secretKeys: ['replicate-key', 'replicate-api-key'] },\n // ── LOTE N (2026-04-27) — catalog↔loader closure for the remaining 22 ─\n // apiKeyEnvVar entries that had catalog rows but NO PROVIDER_SECRETS\n // tuple. Surfaced by `catalog-loader-wiring-completeness.test.ts` (J6\n // invariant). All 22 are real provider rows with adapter classes (or\n // generic OAI-compat hub registration); they were classified in\n // CONSOLIDATION_MATRIX as `credentials-missing` or `disabled-by-default`,\n // and their absence here meant that even after an operator provisioned\n // the GCP secret, `process.env.<X>_API_KEY` would stay empty at boot.\n //\n // Pattern: preferred secret name first (provider-slug `-key`), then\n // upstream-convention `-api-key` fallback, then any historical aliases.\n // The provider-side `aliases` array in providers.catalog.ts is NOT\n // duplicated here — secret-key aliases are about HOW the operator\n // stored the credential, NOT how callers reach the provider.\n //\n // Image/video-only providers (bfl, recraft, runwayml, topaz) are wired\n // here too but are intentionally absent from LLM_PROVIDER_ENV_VARS below\n // because their presence does NOT count toward the \"at least one LLM\n // key present\" boot-mode gate.\n { envVar: 'TOGETHERAI_API_KEY', secretKeys: ['togetherai-api-key', 'togetherai-key', 'together-key', 'together-api-key'] },\n { envVar: 'ANYSCALE_API_KEY', secretKeys: ['anyscale-api-key', 'anyscale-key'] },\n { envVar: 'HYPERBOLIC_API_KEY', secretKeys: ['hyperbolic-api-key', 'hyperbolic-key'] },\n { envVar: 'CHUTES_API_KEY', secretKeys: ['chutes-api-key', 'chutes-key', 'chutes-ai-key'] },\n { envVar: 'NSCALE_API_KEY', secretKeys: ['nscale-key', 'nscale-api-key'] },\n { envVar: 'NEBIUS_API_KEY', secretKeys: ['nebius-key', 'nebius-api-key'] },\n // Lambda Cloud canonical provider slug is `lambda-ai`; brand is\n // \"Lambda\" (without -ai). Both spellings tried as fallbacks.\n { envVar: 'LAMBDA_AI_API_KEY', secretKeys: ['lambda-ai-key', 'lambda-key', 'lambda-cloud-key', 'lambda-ai-api-key'] },\n { envVar: 'SCALEWAY_API_KEY', secretKeys: ['scaleway-key', 'scaleway-api-key'] },\n { envVar: 'SYNTHETIC_API_KEY', secretKeys: ['synthetic-key', 'synthetic-api-key', 'synthetic-ai-key'] },\n { envVar: 'MORPH_API_KEY', secretKeys: ['morph-key', 'morph-api-key', 'morph-llm-key'] },\n // Z.ai (catalog providerId = `zai`). Upstream domain z.ai resolves to\n // bigmodel.cn — the GLM family operator. Legacy spelling z-ai accepted.\n { envVar: 'ZAI_API_KEY', secretKeys: ['zai-key', 'zai-api-key', 'z-ai-key', 'glm-key'] },\n { envVar: 'XIAOMI_MIMO_API_KEY', secretKeys: ['xiaomi-mimo-key', 'mimo-key', 'xiaomi-mimo-api-key'] },\n // v0 (Vercel) — code-gen LLM. The catalog providerId is `v0` (one\n // character) so the env var name is `V0_API_KEY` not `V0AI_API_KEY`.\n { envVar: 'V0_API_KEY', secretKeys: ['v0-key', 'v0-api-key', 'vercel-v0-key'] },\n // Vercel AI Gateway — DIFFERENT from v0 above (gateway over many LLMs).\n // The canonical secret name follows the catalog providerId.\n { envVar: 'VERCEL_AI_GATEWAY_API_KEY', secretKeys: ['vercel-ai-gateway-key', 'vercel-ai-gateway-api-key', 'vercel-gateway-key'] },\n // Volcano (Volcengine — ByteDance). Canonical slug `volcano`; brand\n // includes Volcengine + Doubao + Ark. Multiple aliases accepted.\n { envVar: 'VOLCANO_API_KEY', secretKeys: ['volcano-key', 'volcano-api-key', 'volcengine-key', 'doubao-key', 'ark-key'] },\n // IBM watsonx — note the env var is `WATSONX_APIKEY` (no underscore\n // between API and KEY) to match IBM's canonical SDK convention.\n // Side-car env vars (WATSONX_PROJECT_ID, WATSONX_URL) are wired below\n // because they're documented in the catalog row's `extraEnvVars` and\n // adapter requires them at runtime.\n { envVar: 'WATSONX_APIKEY', secretKeys: ['watsonx-apikey', 'watsonx-key', 'watsonx-api-key', 'ibm-watsonx-key'] },\n { envVar: 'WATSONX_PROJECT_ID', secretKeys: ['watsonx-project-id', 'ibm-watsonx-project-id'] },\n { envVar: 'WATSONX_URL', secretKeys: ['watsonx-url', 'ibm-watsonx-url'] },\n // Snowflake Cortex — uses Personal Access Token (PAT), not an API key.\n // Multiple side-cars required by the SnowflakeCortexAdapter:\n // SNOWFLAKE_ACCOUNT (orgname-accountname), SNOWFLAKE_USER (key-pair\n // auth user), SNOWFLAKE_BASE_URL (account-specific host). Side-cars\n // wired here so a single GCP provisioning round materializes the full\n // tuple at boot.\n { envVar: 'SNOWFLAKE_PAT', secretKeys: ['snowflake-pat', 'snowflake-token', 'snowflake-key'] },\n { envVar: 'SNOWFLAKE_ACCOUNT', secretKeys: ['snowflake-account'] },\n { envVar: 'SNOWFLAKE_USER', secretKeys: ['snowflake-user', 'snowflake-username'] },\n { envVar: 'SNOWFLAKE_BASE_URL', secretKeys: ['snowflake-base-url', 'snowflake-url'] },\n // SAP AI Core — OAuth2 client_credentials flow. Catalog providerId is\n // `sap`. The apiKeyEnvVar is `SAP_AI_CORE_CLIENT_ID` (the OAuth client\n // id); the matching client_secret + token URL + base URL are all\n // documented in the catalog row's `extraEnvVars` and required by the\n // SapAiCoreAdapter. Wiring all four together so a single provisioning\n // round closes the loop.\n { envVar: 'SAP_AI_CORE_CLIENT_ID', secretKeys: ['sap-ai-core-client-id', 'sap-client-id'] },\n { envVar: 'SAP_AI_CORE_CLIENT_SECRET', secretKeys: ['sap-ai-core-client-secret', 'sap-client-secret'] },\n { envVar: 'SAP_AI_CORE_AUTH_URL', secretKeys: ['sap-ai-core-auth-url', 'sap-auth-url'] },\n { envVar: 'SAP_AI_CORE_BASE_URL', secretKeys: ['sap-ai-core-base-url', 'sap-base-url'] },\n // W&B Inference side-car — WANDB_API_KEY (auth) is already wired in\n // Batch 7 above; WANDB_PROJECT is the project slug header required on\n // every chat call. Wiring here closes the side-car gap.\n { envVar: 'WANDB_PROJECT', secretKeys: ['wandb-project', 'wandb-project-slug'] },\n // Image-only providers (no LLM gate participation, see comment below).\n // bfl auth header is `x-key` (not Bearer) — see catalog `authHeaderName`.\n { envVar: 'BFL_API_KEY', secretKeys: ['bfl-key', 'bfl-api-key', 'flux-key', 'black-forest-labs-key'] },\n { envVar: 'RECRAFT_API_KEY', secretKeys: ['recraft-key', 'recraft-api-key'] },\n { envVar: 'RUNWAYML_API_KEY', secretKeys: ['runwayml-key', 'runwayml-api-key', 'runway-key'] },\n { envVar: 'TOPAZ_API_KEY', secretKeys: ['topaz-key', 'topaz-api-key', 'topaz-labs-key'] },\n // ── LOTE O (2026-07-10) — Apertis + Inception Labs onboarding ─────────\n // Operator pre-provisioned both GCP secrets ahead of the catalog rows\n // (ailin-apertis-key, ailin-inception-key). secretKeys match exactly —\n // the GCP provider auto-prepends the `ailin-` prefix (see file header).\n { envVar: 'APERTIS_API_KEY', secretKeys: ['apertis-key', 'apertis-api-key'] },\n { envVar: 'INCEPTION_API_KEY', secretKeys: ['inception-key', 'inception-api-key'] },\n // ── LOTE P (2026-07-11) — EmpirioLabs AI, live-validated after gate ────\n { envVar: 'EMPIRIOLABS_API_KEY', secretKeys: ['empiriolabs-key', 'empiriolabs-api-key'] },\n // ── LOTE Q (2026-07-12) — Concentrate AI ───────────────────────────────\n { envVar: 'CONCENTRATE_API_KEY', secretKeys: ['concentrate-key', 'concentrate-api-key'] },\n // ── LOTE R (2026-07-13) — FastRouter ────────────────────────────────────\n { envVar: 'FASTROUTER_API_KEY', secretKeys: ['fastrouter-key', 'fastrouter-api-key'] },\n // ── LOTE S (2026-07-13) — Perplexity Agent API ──────────────────────────\n // Same physical credential as PERPLEXITY_API_KEY above (one Perplexity\n // account, two API surfaces) — deliberately sources from the SAME GCP\n // secret names, not a new one.\n { envVar: 'PERPLEXITY_AGENT_API_KEY', secretKeys: ['perplexity-api-key', 'perplexity-key'] },\n // ── LOTE T (2026-07-13) — Ailin gateway (self-referential meta-provider) ──\n // No GCP secret provisioned yet — inert until an operator sets AILIN_API_KEY\n // for a self-hosted deployment. secretKeys follow the same naming\n // convention as the rest of this file in case one is provisioned later.\n { envVar: 'AILIN_API_KEY', secretKeys: ['ailin-key', 'ailin-api-key', 'gateway-key'] },\n] as const;\n\n// ─── Provider Key Status Tracking ──────────────────────────────────────────\n// Tracks which provider API keys were successfully loaded, from which source,\n// and when. Used by the Self-Healing Discovery (L1) to know when to retry\n// discovery for native providers whose keys arrived late (GCP delay/failure).\n\nexport interface ProviderKeyStatus {\n loaded: boolean;\n source: 'gcp' | 'env' | 'none';\n timestamp: Date;\n envVar: string;\n}\n\n/**\n * Module-level state: populated during loadSecretsIntoEnv() for each provider.\n * Key = normalized provider name (lowercase, e.g., 'openai', 'anthropic').\n */\nconst providerKeyStatusMap = new Map<string, ProviderKeyStatus>();\n\n/**\n * Mapping from env var name to normalized provider name.\n * Allows the discovery service to map env vars to provider sources.\n */\nconst ENV_VAR_TO_PROVIDER: Record<string, string> = {\n // Native API providers\n OPENAI_API_KEY: 'openai',\n ANTHROPIC_API_KEY: 'anthropic',\n GOOGLE_API_KEY: 'google',\n DEEPSEEK_API_KEY: 'deepseek',\n XAI_API_KEY: 'xai',\n MISTRAL_API_KEY: 'mistral',\n COHERE_API_KEY: 'cohere',\n JINA_API_KEY: 'jina',\n // Cloud hubs — 2026-04-23: use canonical providerId 'nvidia' (legacy\n // alias 'nvidia-hub' was absorbed into the nvidia catalog row per Lot B;\n // discovery/status consumers read providerKeyStatus by canonical id).\n NVIDIA_API_KEY: 'nvidia',\n AIHUBMIX_API_KEY: 'aihubmix',\n NOVITA_API_KEY: 'novita',\n MOONSHOT_API_KEY: 'moonshot',\n MINIMAX_API_KEY: 'minimax',\n FRIENDLI_API_KEY: 'friendli',\n AIML_API_KEY: 'aiml',\n IMAGEROUTER_API_KEY: 'imagerouter',\n COMETAPI_API_KEY: 'cometapi',\n NANOGPT_API_KEY: 'nanogpt',\n EDENAI_API_KEY: 'edenai',\n HELICONEAI_API_KEY: 'heliconeai',\n POE_API_KEY: 'poe',\n ROUTEWAY_API_KEY: 'routeway',\n REQUESTY_API_KEY: 'requesty',\n // Post-migration canonical providerId is `ai302` (see providers.catalog.ts\n // and the residue-closure note in provider-registry.ts). Legacy `302ai`\n // survives as a catalog alias for pre-migration user configs.\n AI302_API_KEY: 'ai302',\n ORQAI_API_KEY: 'orqai',\n // Routers/aggregators\n OPENROUTER_API_KEY: 'openrouter',\n // Cloud providers\n VERTEX_AI_API_KEY: 'vertex-ai',\n AWS_ACCESS_KEY_ID: 'aws-bedrock',\n AWS_BEARER_TOKEN_BEDROCK: 'aws-bedrock',\n // SageMaker uses the same shared AWS creds as Bedrock, so `AWS_ACCESS_KEY_ID`\n // keeps mapping to `aws-bedrock` (first-listed wins) — but the endpoint-\n // specific env var is only meaningful for SageMaker. When operators wire\n // this, Self-Healing Discovery will attribute the key presence to the\n // SageMaker provider specifically.\n AWS_SAGEMAKER_ENDPOINT_NAME: 'aws-sagemaker',\n AZURE_OPENAI_API_KEY: 'azure-openai',\n // Regional/specialized\n QWEN_API_KEY: 'qwen',\n ERNIE_API_KEY: 'ernie',\n // Audio providers\n DEEPGRAM_API_KEY: 'deepgram',\n CARTESIA_API_KEY: 'cartesia',\n ELEVENLABS_API_KEY: 'elevenlabs',\n // Inference + embedding providers newly onboarded 2026-04-22\n WANDB_API_KEY: 'wandb',\n VOYAGE_API_KEY: 'voyage',\n BYTEZ_API_KEY: 'bytez',\n CEREBRAS_API_KEY: 'cerebras',\n FEATHERLESS_AI_API_KEY: 'featherless-ai', // canonical catalog id (2026-04-23 drift fix)\n // Batch 6 — enterprise hyperscaler gateways (2026-04-22). Gemini OAI-compat\n // is registered as a SEPARATE provider from the native `google` entry so\n // the self-healing discovery service can track them independently (they\n // have different failure modes — native uses @google/generative-ai SDK\n // with its own error surfaces; OAI-compat uses standard fetch).\n GEMINI_API_KEY: 'gemini-openai',\n GITHUB_TOKEN: 'github-models',\n DATABRICKS_TOKEN: 'databricks',\n // Orphan closure (2026-04-22) — Inworld now has a catalog row + adapter\n // factory. Mapping the env var to the provider slug lets Self-Healing\n // Discovery attribute key presence to the correct provider.\n INWORLD_API_KEY: 'inworld',\n // Orphan-closure Lot B (2026-04-23). Slug = catalog providerId. Writer\n // and Upstage are self-evident; Reka's slug is `rekaai` (the catalog\n // row's canonical providerId — aliases 'reka' + 'reka-ai' resolve to it).\n WRITER_API_KEY: 'writer',\n UPSTAGE_API_KEY: 'upstage',\n REKA_API_KEY: 'rekaai',\n // ── LOTE M (2026-04-23) — complement lot, 13 providers ──────────────\n // Slug = catalog providerId (NOT the upstream brand for phala/qianfan\n // which intentionally diverge — see catalog rows for rationale):\n // phala — canonical slug; upstream brand is \"RedPill\"\n // qianfan — canonical slug; upstream brand is \"Baidu Qianfan / ERNIE\"\n // gmi — canonical slug; upstream brand is \"GMICloud\"\n ARCEE_API_KEY: 'arcee',\n ATLASCLOUD_API_KEY: 'atlascloud',\n AVIAN_API_KEY: 'avian',\n QIANFAN_API_KEY: 'qianfan',\n GMI_API_KEY: 'gmi',\n INFERMATIC_API_KEY: 'infermatic',\n INFLECTION_API_KEY: 'inflection',\n MANCER_API_KEY: 'mancer',\n PHALA_API_KEY: 'phala',\n RELACE_API_KEY: 'relace',\n SILICONFLOW_API_KEY: 'siliconflow',\n STEPFUN_API_KEY: 'stepfun',\n VENICE_API_KEY: 'venice',\n // ── Sublote E1 (2026-04-24) — runtime-wiring closure for D1 matrix ───\n // 7 provider slugs whose env vars gained GCP→ENV tuples in this lot.\n // Slug = catalog providerId exactly (so Self-Healing Discovery can\n // attribute key-loaded events to the right provider). CLOUDFLARE_ACCOUNT_ID\n // is intentionally absent — that's a URL parameter, not an auth credential,\n // so mapping it to a provider slug would be semantically wrong (the same\n // account-id could conceivably scope multiple Cloudflare providers).\n GROQ_API_KEY: 'groq',\n DEEPINFRA_API_KEY: 'deepinfra',\n HF_TOKEN: 'huggingface',\n CLOUDFLARE_API_TOKEN: 'cloudflare-workers-ai',\n PERPLEXITY_API_KEY: 'perplexity',\n FIREWORKS_AI_API_KEY: 'fireworks-ai',\n SAMBANOVA_API_KEY: 'sambanova',\n // Replicate — pre-existing live-validation gap closed by sublote E1.\n // See the matching comment on the PROVIDER_SECRETS tuple above.\n REPLICATE_API_KEY: 'replicate',\n // ── LOTE N (2026-04-27) — catalog↔loader closure (22 new env vars) ──\n // Every entry below maps an env var to its canonical providerId so the\n // L1 Self-Healing Discovery Service can attribute key-loaded events\n // when the corresponding GCP secret is provisioned. Slugs match the\n // catalog providerId exactly (NOT upstream brand) — see catalog rows\n // for cases where they diverge (e.g. `sap` not `sap-ai-core`, `v0` not\n // `vercel-v0`, `volcano` not `volcengine`). Side-car env vars\n // (WATSONX_PROJECT_ID, SNOWFLAKE_ACCOUNT, SAP_AI_CORE_CLIENT_SECRET,\n // etc.) are NOT mapped here — they're auth or URL parameters, not\n // credentials, so attributing them to a provider would conflate\n // credential-presence with side-car-completeness in discovery telemetry.\n TOGETHERAI_API_KEY: 'togetherai',\n ANYSCALE_API_KEY: 'anyscale',\n HYPERBOLIC_API_KEY: 'hyperbolic',\n CHUTES_API_KEY: 'chutes',\n NSCALE_API_KEY: 'nscale',\n NEBIUS_API_KEY: 'nebius',\n LAMBDA_AI_API_KEY: 'lambda-ai',\n SCALEWAY_API_KEY: 'scaleway',\n SYNTHETIC_API_KEY: 'synthetic',\n MORPH_API_KEY: 'morph',\n ZAI_API_KEY: 'zai',\n XIAOMI_MIMO_API_KEY: 'xiaomi-mimo',\n V0_API_KEY: 'v0',\n VERCEL_AI_GATEWAY_API_KEY: 'vercel-ai-gateway',\n VOLCANO_API_KEY: 'volcano',\n WATSONX_APIKEY: 'watsonx',\n SNOWFLAKE_PAT: 'snowflake',\n SAP_AI_CORE_CLIENT_ID: 'sap',\n // Image / video-only providers. Still mapped to a slug so Self-Healing\n // Discovery can record key-presence events — but their env vars do NOT\n // appear in LLM_PROVIDER_ENV_VARS (image surfaces don't satisfy the\n // \"at least one LLM key\" boot gate).\n BFL_API_KEY: 'bfl',\n RECRAFT_API_KEY: 'recraft',\n RUNWAYML_API_KEY: 'runwayml',\n TOPAZ_API_KEY: 'topaz',\n // ── LOTE O (2026-07-10) ────────────────────────────────────────────────\n APERTIS_API_KEY: 'apertis',\n INCEPTION_API_KEY: 'inception',\n // ── LOTE P (2026-07-11) ────────────────────────────────────────────────\n EMPIRIOLABS_API_KEY: 'empiriolabs',\n // ── LOTE Q (2026-07-12) ────────────────────────────────────────────────\n CONCENTRATE_API_KEY: 'concentrate',\n // ── LOTE R (2026-07-13) ────────────────────────────────────────────────\n FASTROUTER_API_KEY: 'fastrouter',\n // ── LOTE S (2026-07-13) ────────────────────────────────────────────────\n PERPLEXITY_AGENT_API_KEY: 'perplexity-agent',\n // ── LOTE T (2026-07-13) ────────────────────────────────────────────────\n AILIN_API_KEY: 'ailin',\n};\n\n/**\n * Returns the status of all provider API keys.\n * Used by Self-Healing Discovery to determine which native sources can run.\n */\nexport function getProviderKeyStatus(): ReadonlyMap<string, ProviderKeyStatus> {\n return providerKeyStatusMap;\n}\n\n/**\n * Returns provider names that have valid (non-empty, non-mock) API keys loaded.\n */\nexport function getLoadedProviderNames(): string[] {\n return Array.from(providerKeyStatusMap.entries())\n .filter(([, status]) => status.loaded)\n .map(([name]) => name);\n}\n\n/**\n * Check if a specific provider has a loaded API key.\n */\nexport function isProviderKeyLoaded(providerName: string): boolean {\n return providerKeyStatusMap.get(providerName.toLowerCase())?.loaded ?? false;\n}\n\nconst LLM_PROVIDER_ENV_VARS = [\n 'OPENAI_API_KEY',\n 'ANTHROPIC_API_KEY',\n 'GOOGLE_API_KEY',\n 'DEEPSEEK_API_KEY',\n 'XAI_API_KEY',\n 'MISTRAL_API_KEY',\n 'COHERE_API_KEY',\n 'NVIDIA_API_KEY',\n 'AIHUBMIX_API_KEY',\n 'NOVITA_API_KEY',\n 'MOONSHOT_API_KEY',\n 'MINIMAX_API_KEY',\n 'JINA_API_KEY',\n 'FRIENDLI_API_KEY',\n 'AIML_API_KEY',\n 'IMAGEROUTER_API_KEY',\n 'OPENROUTER_API_KEY',\n 'ORQAI_API_KEY',\n 'EDENAI_API_KEY',\n 'HELICONEAI_API_KEY',\n 'VERTEX_AI_API_KEY',\n 'AWS_BEARER_TOKEN_BEDROCK',\n 'AZURE_OPENAI_API_KEY',\n 'QWEN_API_KEY',\n 'ERNIE_API_KEY',\n 'COMETAPI_API_KEY',\n 'NANOGPT_API_KEY',\n 'REQUESTY_API_KEY',\n 'AI302_API_KEY',\n 'POE_API_KEY',\n 'ROUTEWAY_API_KEY',\n // Added 2026-04-22 — these keys now count toward the \"at least one LLM key\n // present\" gate that avoids DEGRADED_SELF_HOSTED mode at boot.\n 'WANDB_API_KEY',\n 'VOYAGE_API_KEY',\n 'CEREBRAS_API_KEY',\n 'FEATHERLESS_AI_API_KEY',\n // Batch 4 landed BYTEZ catalog row + adapter, so BYTEZ_API_KEY now counts\n // toward the LLM-key-present gate.\n 'BYTEZ_API_KEY',\n // Batch 6 — enterprise hyperscaler gateways (2026-04-22). These keys now\n // count toward the \"at least one LLM key present\" gate; all four have\n // landed catalog rows + adapter classes in this batch.\n 'GEMINI_API_KEY',\n 'GITHUB_TOKEN',\n 'DATABRICKS_TOKEN',\n // Batch 8.1 — AWS SageMaker. Endpoint name is the meaningful trigger:\n // AWS creds alone are ambiguous (could be Bedrock or S3), but an\n // endpoint name implies \"SageMaker is provisioned here\".\n 'AWS_SAGEMAKER_ENDPOINT_NAME',\n // Orphan closure (2026-04-22) — Inworld AI now counts toward the\n // \"at least one LLM key present\" gate now that the catalog row +\n // adapter factory are wired.\n 'INWORLD_API_KEY',\n // Orphan-closure Lot B (2026-04-23). All three are fully canonical now.\n 'WRITER_API_KEY',\n 'UPSTAGE_API_KEY',\n 'REKA_API_KEY',\n // ── LOTE M (2026-04-23) complement lot ──────────────────────────────\n // Each of the 13 keys below counts toward the \"at least one LLM key\n // present\" gate, the same way all catalog-wired keys do. Note that\n // mancer and venice carry contentPolicyClass=uncensored (2026-04-28\n // Phase 4b — the prior denyByDefault gate was removed per the universal\n // \"habilitado e nunca censurado\" policy); their keys count toward the\n // gate exactly like any other LLM key — surface-level filtering is now\n // a downstream concern, not a routing one.\n // qianfan here is QIANFAN_API_KEY (v2 bce-v3 bearer); the legacy v1\n // ERNIE_API_KEY was already listed above this marker.\n 'ARCEE_API_KEY',\n 'ATLASCLOUD_API_KEY',\n 'AVIAN_API_KEY',\n 'QIANFAN_API_KEY',\n 'GMI_API_KEY',\n 'INFERMATIC_API_KEY',\n 'INFLECTION_API_KEY',\n 'MANCER_API_KEY',\n 'PHALA_API_KEY',\n 'RELACE_API_KEY',\n 'SILICONFLOW_API_KEY',\n 'STEPFUN_API_KEY',\n 'VENICE_API_KEY',\n // ── Sublote E1 (2026-04-24) — runtime-wiring closure for D1 matrix ───\n // These 7 providers landed in CONSOLIDATION_MATRIX['live-validation']\n // during Sublote D1 but had no GCP→ENV tuple above → their keys could\n // not count toward the \"at least one LLM key present\" gate even when\n // GCP Secret Manager held the key. Listed here so a restart after the\n // operator's provisioning cycle propagates the key-present signal to\n // the DEGRADED_SELF_HOSTED avoidance logic. CLOUDFLARE_ACCOUNT_ID is\n // intentionally absent — it's a URL parameter, not an LLM credential.\n 'GROQ_API_KEY',\n 'DEEPINFRA_API_KEY',\n 'HF_TOKEN',\n 'CLOUDFLARE_API_TOKEN',\n 'PERPLEXITY_API_KEY',\n 'FIREWORKS_AI_API_KEY',\n 'SAMBANOVA_API_KEY',\n // Replicate — pre-existing live-validation gap closed by sublote E1.\n // See the matching comments on the PROVIDER_SECRETS tuple and\n // ENV_VAR_TO_PROVIDER entry above.\n 'REPLICATE_API_KEY',\n // ── LOTE N (2026-04-27) — catalog↔loader closure ─────────────────────\n // 18 LLM-class providers added to the boot-mode gate. The 4 image/\n // video-only providers (bfl, recraft, runwayml, topaz) are\n // intentionally OMITTED — their key presence does not signal the\n // server has any text-generation capability, so they should not\n // satisfy the \"at least one LLM key present\" check.\n 'TOGETHERAI_API_KEY',\n 'ANYSCALE_API_KEY',\n 'HYPERBOLIC_API_KEY',\n 'CHUTES_API_KEY',\n 'NSCALE_API_KEY',\n 'NEBIUS_API_KEY',\n 'LAMBDA_AI_API_KEY',\n 'SCALEWAY_API_KEY',\n 'SYNTHETIC_API_KEY',\n 'MORPH_API_KEY',\n 'ZAI_API_KEY',\n 'XIAOMI_MIMO_API_KEY',\n 'V0_API_KEY',\n 'VERCEL_AI_GATEWAY_API_KEY',\n 'VOLCANO_API_KEY',\n 'WATSONX_APIKEY',\n 'SNOWFLAKE_PAT',\n 'SAP_AI_CORE_CLIENT_ID',\n // ── LOTE O (2026-07-10) — Apertis + Inception Labs, both chat-capable ──\n 'APERTIS_API_KEY',\n 'INCEPTION_API_KEY',\n // ── LOTE P (2026-07-11) — EmpirioLabs AI, chat-capable ─────────────────\n 'EMPIRIOLABS_API_KEY',\n // ── LOTE Q (2026-07-12) — Concentrate AI, chat-capable ─────────────────\n 'CONCENTRATE_API_KEY',\n // ── LOTE R (2026-07-13) — FastRouter, chat-capable ─────────────────────\n 'FASTROUTER_API_KEY',\n // ── LOTE S (2026-07-13) — Perplexity Agent API, chat-capable ───────────\n 'PERPLEXITY_AGENT_API_KEY',\n // ── LOTE T (2026-07-13) — Ailin gateway, chat-capable ──────────────────\n 'AILIN_API_KEY',\n] as const;\n\n/**\n * Self-hosted / sidecar endpoints that can keep the server alive in a\n * DEGRADED mode when every external LLM API key is missing.\n *\n * Design rationale:\n * Without this fallback, a single failure (expired GCP ADC, rotated\n * service account, network blip during boot) produces an infinite\n * restart loop. With it, the server boots in `degraded_self_hosted`\n * mode: only local/sidecar models are usable, requests are tagged\n * via last-resort-policy.ts (execution_mode=last_resort_self_hosted,\n * excluded_from_benchmark=true), and ops gets a clear warning\n * instead of a crash loop.\n *\n * This ties directly into Bloco P1.10 (self-hosted last-resort policy)\n * but at boot-time scope rather than per-request scope.\n */\nconst SELF_HOSTED_ENV_VARS = [\n 'OLLAMA_URL',\n 'SELF_HOSTED_LLM_URL',\n 'LOCAL_LLAMA_URL',\n 'LOCAL_KOBOLD_URL',\n] as const;\n\n/**\n * Load critical secrets from GCP into process.env\n * \n * This function is called AFTER SecretsManager initialization\n * and BEFORE config validation.\n * \n * It loads secrets from GCP and injects them into process.env,\n * allowing the existing config system to work without modification.\n */\nexport async function loadSecretsIntoEnv(): Promise<void> {\n const log = logger.child({ component: 'secrets-loader' });\n\n log.info('Loading critical secrets from GCP Secret Manager into environment...');\n\n let loaded = 0;\n let skipped = 0;\n let failed = 0;\n let sanitizedKeys = 0;\n let providerSecretsLoaded = 0;\n // Tracks env values that were authoritatively replaced by GCP fetches.\n // Useful diagnostic for operators verifying that GCP is the source of truth.\n let overwrittenFromGcp = 0;\n\n const forceLoadRealKeys = process.env.TEST_USE_REAL_API_KEYS === 'true';\n\n // ── GCP-authoritative mode ─────────────────────────────────────────────\n // When SECRETS_PROVIDER_PRIMARY=gcp the operator has declared GCP as the\n // source of truth. The legacy \"skip if env already populated\" path then\n // contradicts that declaration: env_file / docker-compose / shell exports\n // silently win and GCP fetches are never even attempted, which is exactly\n // the failure mode that hid populated GCP secrets (e.g. HF_TOKEN) from\n // /v1/models. In authoritative mode we ALWAYS attempt the GCP fetch and\n // overwrite the pre-existing env value when GCP returns one. The fallback\n // is symmetric: a GCP miss preserves the existing env value (no zeroing).\n //\n // Operators can override:\n // SECRETS_GCP_AUTHORITATIVE=false → keep legacy skip-if-exists semantics\n // SECRETS_GCP_AUTHORITATIVE=true → force authoritative even with env primary\n const gcpPrimaryEnv = (process.env.SECRETS_PROVIDER_PRIMARY || '').trim().toLowerCase() === 'gcp';\n const explicitAuthoritative = (process.env.SECRETS_GCP_AUTHORITATIVE || '').trim().toLowerCase();\n const gcpAuthoritative =\n explicitAuthoritative === 'true' ||\n (explicitAuthoritative !== 'false' && gcpPrimaryEnv);\n\n // Tests already use TEST_USE_REAL_API_KEYS=true to force GCP fetches even\n // when local mocks are present. The two flags are independent triggers\n // for the same effect; either one disables skip-if-exists.\n const authoritativeFetch = gcpAuthoritative || forceLoadRealKeys;\n\n log.info(\n {\n gcpPrimary: gcpPrimaryEnv,\n gcpAuthoritative,\n forceLoadRealKeys,\n authoritativeFetch,\n },\n 'Secret loading mode resolved'\n );\n\n // Load critical secrets (required)\n for (const { envVar, secretKey, required, defaultValue } of CRITICAL_SECRETS) {\n const existingValue = process.env[envVar];\n const shouldOverrideMockStripe =\n forceLoadRealKeys &&\n envVar.startsWith('STRIPE_') &&\n typeof existingValue === 'string' &&\n (existingValue.includes('mock') || existingValue.includes('test'));\n\n // Skip if already in env, UNLESS:\n // - we're in GCP-authoritative mode (operator declared GCP as source of truth), OR\n // - we're forcing real keys and detected mock Stripe keys.\n if (existingValue && !shouldOverrideMockStripe && !gcpAuthoritative) {\n log.debug({ envVar, source: 'env' }, 'Secret already in environment, skipping GCP load');\n skipped++;\n continue;\n }\n\n try {\n const value = await loadSecret(secretKey, required);\n if (value) {\n const wasOverwrite = Boolean(existingValue) && existingValue !== value;\n process.env[envVar] = value;\n if (wasOverwrite) overwrittenFromGcp++;\n log.debug(\n { envVar, secretKey, replaced: Boolean(existingValue), overwrittenFromGcp: wasOverwrite },\n 'Secret loaded from GCP into environment'\n );\n loaded++;\n } else if (existingValue && gcpAuthoritative) {\n // Authoritative fetch missed in GCP — fall back to the existing env\n // value rather than zeroing it. This keeps the system bootable when\n // a single secret is absent from GCP without losing the env-provided\n // value.\n log.debug(\n { envVar, secretKey },\n 'GCP miss in authoritative mode; preserving existing env value'\n );\n skipped++;\n } else if (defaultValue !== undefined) {\n // Use default value if secret not found and default provided\n process.env[envVar] = defaultValue;\n log.debug({ envVar, secretKey, defaultValue }, 'Secret not found, using default value');\n skipped++;\n } else if (!required) {\n log.debug({ envVar, secretKey }, 'Optional secret not found, skipping');\n skipped++;\n }\n } catch (error) {\n if (defaultValue !== undefined) {\n // Use default value on error if provided\n process.env[envVar] = defaultValue;\n log.warn({ envVar, secretKey, defaultValue, error: getErrorMessage(error) }, 'Failed to load secret, using default value');\n skipped++;\n } else if (required) {\n const errorMsg = getErrorMessage(error);\n log.error(\n { envVar, secretKey, error: errorMsg },\n 'Failed to load required secret'\n );\n failed++;\n throw new Error(`Failed to load required secret ${secretKey}: ${errorMsg}`);\n } else {\n log.warn({ envVar, secretKey, error: getErrorMessage(error) }, 'Failed to load optional secret');\n skipped++;\n }\n }\n }\n\n // Keep compatibility with components that still read GCS_BUCKET_NAME.\n if (!process.env.GCS_BUCKET_NAME && process.env.GCS_FILES_BUCKET) {\n process.env.GCS_BUCKET_NAME = process.env.GCS_FILES_BUCKET;\n }\n\n // Load provider API keys (optional)\n // CRITICAL: If GCP auth is available, ALWAYS load real keys (API Keys NUNCA DEVEM FALHAR)\n // If TEST_USE_REAL_API_KEYS=true, force load from GCP even if mock keys exist\n // Always replace mock keys with real keys from GCP when available\n // (reuses forceLoadRealKeys defined above)\n \n for (const { envVar, secretKeys } of PROVIDER_SECRETS) {\n const existingValue = process.env[envVar];\n const providerName = ENV_VAR_TO_PROVIDER[envVar];\n\n // Check if existing value looks like a mock key\n const isMockKey = existingValue && (existingValue.includes('mock') || existingValue.includes('test-'));\n\n // Skip if real key already exists, UNLESS:\n // - we're in GCP-authoritative mode (operator declared GCP as source of truth), OR\n // - test forces real keys (TEST_USE_REAL_API_KEYS=true), OR\n // - the existing value looks like a mock.\n // The authoritative path lets GCP overwrite stale env_file / shell-exported\n // values, which is the entire point of SECRETS_PROVIDER_PRIMARY=gcp.\n if (existingValue && !authoritativeFetch && !isMockKey) {\n // Record as loaded from env\n if (providerName) {\n providerKeyStatusMap.set(providerName, { loaded: true, source: 'env', timestamp: new Date(), envVar });\n }\n skipped++;\n continue;\n }\n\n try {\n let loadedSecretValue: string | undefined;\n let resolvedSecretKey = secretKeys[0];\n\n for (const candidateKey of secretKeys) {\n try {\n const candidateValue = await loadSecret(candidateKey, false);\n if (candidateValue) {\n loadedSecretValue = candidateValue;\n resolvedSecretKey = candidateKey;\n log.debug({ envVar, secretKey: candidateKey, valueLen: candidateValue.length }, 'Secret loaded from GCP');\n break;\n } else {\n log.debug({ envVar, secretKey: candidateKey }, 'Secret returned empty/null from GCP');\n }\n } catch (loadErr) {\n log.warn({ envVar, secretKey: candidateKey, error: loadErr instanceof Error ? loadErr.message : String(loadErr) }, 'Failed to load secret from GCP');\n continue;\n }\n }\n\n // GCP provider automatically adds prefix 'ailin-' to secretKey\n // So 'openai-key' becomes 'ailin-openai-key' in GCP Secret Manager\n const value = loadedSecretValue;\n if (value) {\n // Sanitize control characters and invisible Unicode.\n // PEM-format keys (-----BEGIN ...) legitimately contain newlines,\n // so use a lighter regex that preserves \\n and \\r for those values.\n const isPemKey = value.startsWith('-----BEGIN');\n let sanitizedValue = isPemKey\n ? value\n .replace(/[\\x00-\\x09\\x0B\\x0C\\x0E-\\x1F\\x7F]/g, '') // Strip control chars EXCEPT \\n (0x0A) and \\r (0x0D)\n .replace(/[\\u200B-\\u200D\\uFEFF]/g, '') // Remove zero-width spaces\n .trim()\n : value\n .replace(/[\\x00-\\x1F\\x7F]/g, '') // Strip ALL control characters\n .replace(/[\\u200B-\\u200D\\uFEFF]/g, '') // Remove zero-width spaces\n .trim();\n \n // Log detailed debugging information if sanitization changed the value\n if (sanitizedValue !== value) {\n const removedChars = value.length - sanitizedValue.length;\n sanitizedKeys++;\n log.debug(\n { \n envVar, \n secretKey: resolvedSecretKey,\n originalLength: value.length, \n sanitizedLength: sanitizedValue.length,\n removedChars\n },\n 'API key contained invalid characters and was sanitized'\n );\n }\n \n // Always replace mock keys with real keys\n const wasReplaced = !!existingValue;\n const wasOverwriteOfRealValue =\n wasReplaced && !isMockKey && existingValue !== sanitizedValue;\n\n // CRITICAL: Set process.env and immediately validate\n // Use Object.defineProperty as fallback if direct assignment doesn't work\n // This ensures the value is actually set in the environment\n const _previousValue = process.env[envVar];\n process.env[envVar] = sanitizedValue;\n\n // Immediately read back to verify (handles any potential async issues)\n const readBackValue = process.env[envVar];\n\n // Deep validation: check exact match, type, and length\n const isValidMatch = readBackValue === sanitizedValue;\n const isValidType = typeof readBackValue === 'string';\n const isValidLength = readBackValue !== undefined && readBackValue.length === sanitizedValue.length;\n\n if (!isValidMatch || !isValidType || !isValidLength) {\n log.error(\n {\n envVar,\n secretKey: resolvedSecretKey,\n sanitizedValueLength: sanitizedValue.length,\n readBackValueLength: readBackValue?.length,\n },\n 'Secret was loaded but process.env assignment did not persist'\n );\n failed++;\n } else {\n // Success - log and increment counter\n if (wasOverwriteOfRealValue) overwrittenFromGcp++;\n log.debug(\n {\n envVar,\n secretKey: resolvedSecretKey,\n replaced: wasReplaced,\n wasMock: isMockKey,\n overwrittenFromGcp: wasOverwriteOfRealValue,\n },\n 'Provider secret loaded from GCP'\n );\n loaded++;\n providerSecretsLoaded++;\n // Record successful load\n if (providerName) {\n providerKeyStatusMap.set(providerName, { loaded: true, source: 'gcp', timestamp: new Date(), envVar });\n }\n }\n } else {\n // GCP returned no value. Decide what's actually in process.env right\n // now: if env had a non-mock value before this loop iteration we\n // preserved it (the authoritative path entered this block but found\n // no GCP value to overwrite with), so we're effectively env-loaded.\n // If env had a mock value, we keep the mock as a degraded fallback\n // and warn loudly. If env had nothing, the provider stays orphaned.\n if (existingValue && !isMockKey) {\n // Authoritative-mode GCP miss with real env value already set:\n // the env value is still in process.env, so report source=env.\n if (providerName) {\n providerKeyStatusMap.set(providerName, { loaded: true, source: 'env', timestamp: new Date(), envVar });\n }\n log.debug(\n { envVar, secretKeys },\n 'GCP miss in authoritative mode; preserving existing env value for provider'\n );\n } else {\n if (isMockKey) {\n log.warn(\n { envVar, secretKeys },\n 'Mock API key detected but no real key found in GCP - using mock key (this may cause API failures)'\n );\n }\n // Record as not loaded (no GCP value, no usable env value)\n if (providerName) {\n providerKeyStatusMap.set(providerName, { loaded: false, source: 'none', timestamp: new Date(), envVar });\n }\n }\n skipped++;\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n // Only warn if not forcing real keys - if forcing, this is expected to work\n if (forceLoadRealKeys) {\n log.error(\n { envVar, secretKeys, error: errorMessage },\n 'Failed to load provider secret from GCP (TEST_USE_REAL_API_KEYS=true)'\n );\n failed++;\n } else {\n if (isMockKey) {\n log.warn(\n { envVar, secretKeys, error: errorMessage },\n 'Failed to load provider secret from GCP - will use mock key (this may cause API failures)'\n );\n } else {\n log.debug(\n { envVar, secretKeys, error: errorMessage },\n 'Failed to load provider secret from GCP (optional)'\n );\n }\n // Record failure\n if (providerName) {\n providerKeyStatusMap.set(providerName, { loaded: false, source: 'none', timestamp: new Date(), envVar });\n }\n skipped++;\n }\n }\n }\n\n // ── Env-var aliases (2026-04-28) ────────────────────────────────────────\n //\n // Some operators set a non-canonical name in `.env` for historical reasons\n // (e.g. `REKAAI_API_KEY` instead of the catalog-canonical `REKA_API_KEY`).\n // When GCP is reachable the secret loader populates the canonical name and\n // it just works; when GCP is unreachable the alias would be the only thing\n // set and the catalog row reads the canonical → empty key → silent skip.\n //\n // This pass copies known aliases into their canonical env var if the\n // canonical is empty. It is purely additive — never overwrites an\n // existing canonical value, never logs a warning unless an alias was\n // applied. Add new entries here when a similar mismatch is discovered.\n const ENV_ALIASES: ReadonlyArray<{ canonical: string; alias: string }> = [\n { canonical: 'REKA_API_KEY', alias: 'REKAAI_API_KEY' },\n ];\n for (const { canonical, alias } of ENV_ALIASES) {\n const canonicalValue = process.env[canonical];\n const aliasValue = process.env[alias];\n if ((canonicalValue == null || canonicalValue.trim() === '') && aliasValue && aliasValue.trim() !== '') {\n process.env[canonical] = aliasValue;\n log.info({ canonical, alias }, 'Promoted alias env var to canonical name');\n }\n }\n\n const gcpPrimary = (process.env.SECRETS_PROVIDER_PRIMARY || '').trim().toLowerCase() === 'gcp';\n const failFastEnv = (process.env.SECRETS_GCP_FAIL_FAST || '').trim().toLowerCase();\n const gcpFailFast = failFastEnv === 'true' || (failFastEnv !== 'false' && gcpPrimary);\n const hasAnyLlmProviderKey = LLM_PROVIDER_ENV_VARS.some((envVar) => {\n const value = process.env[envVar];\n return typeof value === 'string' && value.trim().length > 0;\n });\n\n // Hardening (P1.10 at boot scope): even when zero external API keys are\n // loaded, the server can still boot in DEGRADED mode if a self-hosted\n // endpoint is reachable. Every request will then be tagged via the\n // last-resort-policy as execution_mode=last_resort_self_hosted.\n const availableSelfHostedEndpoints = SELF_HOSTED_ENV_VARS.filter((envVar) => {\n const value = process.env[envVar];\n return typeof value === 'string' && value.trim().length > 0;\n });\n const hasAnySelfHostedFallback = availableSelfHostedEndpoints.length > 0;\n\n // Opt-out for strict production deployments that must never run in\n // degraded mode. Default: allow degraded boot (safer than crash loops).\n const allowDegradedBoot = (process.env.ALLOW_DEGRADED_BOOT || 'true').trim().toLowerCase() !== 'false';\n\n if (gcpFailFast && !hasAnyLlmProviderKey) {\n if (hasAnySelfHostedFallback && allowDegradedBoot) {\n log.warn(\n {\n gcpPrimary,\n gcpFailFast,\n providerSecretsLoaded,\n selfHostedEndpoints: availableSelfHostedEndpoints,\n mode: 'degraded_self_hosted',\n docs: 'docs/hardening/last-resort-policy',\n },\n '⚠️ DEGRADED BOOT: zero external LLM credentials, but self-hosted fallback is available. ' +\n 'Server will start in last-resort mode. Requests will be tagged excluded_from_benchmark. ' +\n 'Fix ADC/GCP credentials to restore full operation.'\n );\n // Intentionally fall through — boot continues\n } else {\n const message = hasAnySelfHostedFallback\n ? 'SECRETS_PROVIDER_PRIMARY=gcp, no LLM provider credentials loaded, and ALLOW_DEGRADED_BOOT=false.'\n : 'SECRETS_PROVIDER_PRIMARY=gcp but no LLM provider credentials are available after secrets bootstrap, ' +\n 'and no self-hosted fallback endpoints (OLLAMA_URL, SELF_HOSTED_LLM_URL, LOCAL_LLAMA_URL, LOCAL_KOBOLD_URL) are configured.';\n log.error(\n {\n gcpPrimary,\n gcpFailFast,\n providerSecretsLoaded,\n checkedEnvVars: LLM_PROVIDER_ENV_VARS,\n selfHostedEndpointsChecked: SELF_HOSTED_ENV_VARS,\n allowDegradedBoot,\n },\n message\n );\n throw new Error(\n `${message} Verify GCP Secret Manager access (ADC/Workload Identity) and secret names/prefix, ` +\n `or set OLLAMA_URL for degraded self-hosted boot, or run 'gcloud auth application-default login' if local.`\n );\n }\n }\n\n // Warn on partial provider key loading — helps detect GCP issues early\n if (hasAnyLlmProviderKey && providerSecretsLoaded > 0 && providerSecretsLoaded < 3) {\n log.warn(\n { providerSecretsLoaded, total: PROVIDER_SECRETS.length },\n `⚠️ Only ${providerSecretsLoaded} provider secrets loaded — possible partial GCP failure`\n );\n }\n\n // Audio provider diagnostics\n const audioProviders = {\n DEEPGRAM_API_KEY: !!process.env.DEEPGRAM_API_KEY,\n CARTESIA_API_KEY: !!process.env.CARTESIA_API_KEY,\n ELEVENLABS_API_KEY: !!process.env.ELEVENLABS_API_KEY,\n OLLAMA_URL: process.env.OLLAMA_URL || '',\n SELF_HOSTED_STT_URL: process.env.SELF_HOSTED_STT_URL || '',\n LOCAL_NLLB_URL: process.env.LOCAL_NLLB_URL || '',\n };\n log.info({ audioProviders }, 'Audio provider env vars after secrets loading');\n\n log.info(\n {\n loaded,\n skipped,\n failed,\n sanitizedKeys,\n providerSecretsLoaded,\n overwrittenFromGcp,\n gcpAuthoritative,\n total: CRITICAL_SECRETS.length + PROVIDER_SECRETS.length,\n },\n '✅ Secrets loading complete'\n );\n\n if (failed > 0) {\n throw new Error(`Failed to load ${failed} required secrets`);\n }\n}\n\n/**\n * Get summary of secrets loaded.\n *\n * Diagnostic helper for admin endpoints (e.g. /v1/admin/providers/...) that\n * surfaces exactly which provider envVars are populated and from where, so\n * operators can verify SECRETS_PROVIDER_PRIMARY=gcp is actually authoritative\n * and not silently shadowed by stale env values.\n */\nexport function getSecretsLoadSummary(): {\n fromGCP: string[];\n fromEnv: string[];\n notLoaded: string[];\n} {\n const fromGCP: string[] = [];\n const fromEnv: string[] = [];\n const notLoaded: string[] = [];\n\n for (const status of providerKeyStatusMap.values()) {\n if (!status.loaded) {\n notLoaded.push(status.envVar);\n continue;\n }\n if (status.source === 'gcp') {\n fromGCP.push(status.envVar);\n } else if (status.source === 'env') {\n fromEnv.push(status.envVar);\n }\n }\n\n return { fromGCP, fromEnv, notLoaded };\n}\n"}, "73e74cbb6d08f64a0e6730e3aaf3375a98d078f3": {"t": 1785754629.6981485, "n": 61706, "h": "7ce28d63950250aa", "c": "// Copyright (C) 2026 Ailin One, Inc.\n//\n// This file is part of Collective Intelligence Engine (ci).\n// Licensed under the GNU Affero General Public License v3.0 or later.\n// See LICENSE in the repository root, or <https://www.gnu.org/licenses/>.\n//\n// SPDX-License-Identifier: AGPL-3.0-or-later\n// Source: https://github.com/ailinone/collective-intelligence\n\n/**\n * Load Secrets into Environment\n * \n * This module loads critical secrets from GCP Secret Manager\n * and injects them into process.env BEFORE config validation.\n * \n * This allows the existing config system to work without refactoring,\n * while still using GCP Secret Manager as the source of truth.\n * \n * Call order:\n * 1. initializeSecretsManager()\n * 2. loadSecretsIntoEnv() ← This file\n * 3. validateConfig()\n */\n\nimport { logger } from '@/utils/logger';\nimport { getErrorMessage } from '@/utils/type-guards';\nimport { loadSecret } from './secrets-loader';\n\n/**\n * Critical secrets that must be loaded from GCP\n * These are injected into process.env for config compatibility\n */\ninterface CriticalSecret {\n envVar: string;\n secretKey: string;\n required: boolean;\n defaultValue?: string;\n}\n\nconst CRITICAL_SECRETS: CriticalSecret[] = [\n { envVar: 'DATABASE_URL', secretKey: 'database-url', required: true },\n { envVar: 'JWT_SECRET', secretKey: 'jwt-secret', required: true },\n { envVar: 'REDIS_PASSWORD', secretKey: 'redis-password', required: false, defaultValue: '' },\n { envVar: 'GCS_FILES_BUCKET', secretKey: 'gcs-files-bucket', required: false },\n { envVar: 'SMTP_PASS', secretKey: 'smtp-pass', required: false },\n { envVar: 'STRIPE_SECRET_KEY', secretKey: 'stripe-secret-key', required: false },\n { envVar: 'STRIPE_WEBHOOK_SECRET', secretKey: 'stripe-webhook-secret', required: false },\n { envVar: 'STRIPE_PUBLISHABLE_KEY', secretKey: 'stripe-publishable-key', required: false },\n // Phase 2c shadow ensemble bearer token. Optional because the coord-\n // stable wire is disabled by default in production\n // (CI_ENSEMBLE_COORDINATOR_ENABLED=false). When operator flips it on,\n // the GCP secret `coord-ensemble-token` MUST also be set with the\n // value to share with coord-serving's COORD_SERVING_AUTH_TOKEN.\n { envVar: 'CI_ENSEMBLE_COORDINATOR_TOKEN', secretKey: 'coord-ensemble-token', required: false },\n];\n\n/**\n * Provider API keys (loaded from GCP if not in env)\n * CRITICAL: GCP provider automatically adds prefix 'ailin-' to secret keys\n * So 'openai-key' becomes 'ailin-openai-key' in GCP Secret Manager\n * These keys match the actual secret names in GCP (without the prefix, provider adds it)\n */\ninterface ProviderSecretBinding {\n envVar: string;\n secretKeys: readonly string[];\n}\n\nexport const PROVIDER_SECRETS: readonly ProviderSecretBinding[] = [\n { envVar: 'OPENAI_API_KEY', secretKeys: ['openai-key'] },\n { envVar: 'ANTHROPIC_API_KEY', secretKeys: ['anthropic-key'] },\n { envVar: 'GOOGLE_API_KEY', secretKeys: ['google-key'] },\n { envVar: 'DEEPSEEK_API_KEY', secretKeys: ['deepseek-key'] },\n { envVar: 'XAI_API_KEY', secretKeys: ['xai-key'] },\n { envVar: 'MISTRAL_API_KEY', secretKeys: ['mistral-key'] },\n { envVar: 'COHERE_API_KEY', secretKeys: ['cohere-key'] },\n { envVar: 'NVIDIA_API_KEY', secretKeys: ['nvidia-key', 'nvidia-api-key'] },\n { envVar: 'AIHUBMIX_API_KEY', secretKeys: ['aihubmix-key', 'aihubmix-api-key'] },\n { envVar: 'NOVITA_API_KEY', secretKeys: ['novita-key', 'novita-api-key'] },\n { envVar: 'MOONSHOT_API_KEY', secretKeys: ['moonshot-key', 'moonshot-api-key'] },\n { envVar: 'MINIMAX_API_KEY', secretKeys: ['minimax-key', 'minimax-api-key'] },\n { envVar: 'JINA_API_KEY', secretKeys: ['jina-key', 'jina-api-key'] },\n { envVar: 'GOOGLE_MAPS_API_KEY', secretKeys: ['google-maps-key', 'google-maps-api-key'] },\n { envVar: 'FRIENDLI_API_KEY', secretKeys: ['friendli-key', 'friendli-api-key'] },\n { envVar: 'FRIENDLI_TEAM_ID', secretKeys: ['friendli-team-id'] },\n { envVar: 'AIML_API_KEY', secretKeys: ['aiml-key', 'aiml-api-key'] },\n { envVar: 'IMAGEROUTER_API_KEY', secretKeys: ['imagerouter-key', 'imagerouter-api-key'] },\n { envVar: 'OPENROUTER_API_KEY', secretKeys: ['openrouter-key'] },\n { envVar: 'COMETAPI_API_KEY', secretKeys: ['cometapi-key', 'cometapi-api-key'] },\n { envVar: 'NANOGPT_API_KEY', secretKeys: ['nanogpt-key', 'nanogpt-api-key'] },\n { envVar: 'REQUESTY_API_KEY', secretKeys: ['requesty-key', 'requesty-api-key'] },\n { envVar: 'AI302_API_KEY', secretKeys: ['302-key', '302ai-key'] },\n { envVar: 'POE_API_KEY', secretKeys: ['poe-key', 'poe-api-key'] },\n { envVar: 'ROUTEWAY_API_KEY', secretKeys: ['routeway-key', 'routeway-api-key'] },\n // Hub providers support both key naming conventions for compatibility.\n { envVar: 'ORQAI_API_KEY', secretKeys: ['orqai-key', 'orqai-api-key'] },\n { envVar: 'ORQAI_MODEL_DENYLIST', secretKeys: ['orqai-model-denylist'] },\n { envVar: 'EDENAI_API_KEY', secretKeys: ['edenai-key', 'edenai-api-key'] },\n { envVar: 'HELICONEAI_API_KEY', secretKeys: ['heliconeai-key', 'heliconeai-api-key'] },\n { envVar: 'VERTEX_AI_API_KEY', secretKeys: ['vertex-key'] },\n { envVar: 'VERTEX_AI_PROJECT_ID', secretKeys: ['vertex-project-id'] },\n { envVar: 'QWEN_API_KEY', secretKeys: ['qwen-key-2'] },\n { envVar: 'ALIBABA_ACCESS_KEY_ID', secretKeys: ['alibaba-key-id'] },\n { envVar: 'ALIBABA_ACCESS_KEY_SECRET', secretKeys: ['alibaba-key-secret'] },\n { envVar: 'ERNIE_API_KEY', secretKeys: ['baidu-key'] },\n { envVar: 'ERNIE_SECRET_KEY', secretKeys: ['baidu-secret'] },\n { envVar: 'BAIDU_BASE_URL', secretKeys: ['baidu-base-url'] },\n { envVar: 'AWS_ACCESS_KEY_ID', secretKeys: ['aws-key-id'] },\n { envVar: 'AWS_SECRET_ACCESS_KEY', secretKeys: ['aws-secret'] },\n { envVar: 'AWS_BEARER_TOKEN_BEDROCK', secretKeys: ['aws-bearer-token'] },\n // Batch 7.1: Bedrock region + inference-profile ARN can now be set via\n // GCP Secret Manager. Useful for deploys that route Bedrock to a specific\n // region (e.g. us-west-2) or via cross-region inference profiles without\n // baking these into the docker image.\n { envVar: 'AWS_BEDROCK_REGION', secretKeys: ['aws-bedrock-region'] },\n { envVar: 'AWS_BEDROCK_INFERENCE_PROFILE_ARN', secretKeys: ['aws-bedrock-inference-profile-arn'] },\n // Batch 8.1: SageMaker endpoint routing. A deploy can point this adapter\n // at a specific deployed endpoint via a secret rather than baking the\n // endpoint name into the image. The region override lets the adapter\n // hit an endpoint in a different region from the general AWS_REGION\n // used by other services (SES, S3).\n { envVar: 'AWS_SAGEMAKER_REGION', secretKeys: ['aws-sagemaker-region'] },\n { envVar: 'AWS_SAGEMAKER_ENDPOINT_NAME', secretKeys: ['aws-sagemaker-endpoint-name'] },\n { envVar: 'AWS_SAGEMAKER_PAYLOAD_SCHEMA', secretKeys: ['aws-sagemaker-payload-schema'] },\n { envVar: 'AWS_SAGEMAKER_CUSTOM_ATTRIBUTES', secretKeys: ['aws-sagemaker-custom-attributes'] },\n { envVar: 'AZURE_OPENAI_API_KEY', secretKeys: ['azure-openai-api-key'] },\n { envVar: 'AZURE_OPENAI_ENDPOINT', secretKeys: ['azure-openai-endpoint'] },\n { envVar: 'AZURE_OPENAI_DEPLOYMENT', secretKeys: ['azure-openai-deployment'] },\n // Audio-first providers (STT, TTS, STS)\n { envVar: 'DEEPGRAM_API_KEY', secretKeys: ['deepgram-key'] },\n { envVar: 'CARTESIA_API_KEY', secretKeys: ['cartesia-key'] },\n { envVar: 'ELEVENLABS_API_KEY', secretKeys: ['elevenlabs-key'] },\n // Translation provider\n { envVar: 'PALABRAAI_CLIENT_ID', secretKeys: ['palabraai-id'] },\n { envVar: 'PALABRAAI_CLIENT_SECRET', secretKeys: ['palabraai-key'] },\n { envVar: 'OCI_TENANCY_ID', secretKeys: ['oci-tenancy-id'] },\n { envVar: 'OCI_USER_ID', secretKeys: ['oci-user-id'] },\n { envVar: 'OCI_FINGERPRINT', secretKeys: ['oci-fingerprint'] },\n { envVar: 'OCI_PRIVATE_KEY', secretKeys: ['oci-private-key'] },\n { envVar: 'OCI_REGION', secretKeys: ['oci-region'] },\n // 2026-04-22: New GCP secrets (5 providers). The GCP provider auto-prefixes\n // 'ailin-', so 'wandb-key' maps to ailin-wandb-key in Secret Manager.\n // Voyage lists both 'voyageai-key' (current upstream name) and 'voyage-key'\n // (conventional fallback) because the canonical spelling is still unstable.\n { envVar: 'WANDB_API_KEY', secretKeys: ['wandb-key', 'wandb-api-key'] },\n { envVar: 'VOYAGE_API_KEY', secretKeys: ['voyageai-key', 'voyage-key', 'voyage-api-key'] },\n { envVar: 'BYTEZ_API_KEY', secretKeys: ['bytez-key', 'bytez-api-key'] },\n { envVar: 'CEREBRAS_API_KEY', secretKeys: ['cerebras-key', 'cerebras-api-key'] },\n { envVar: 'FEATHERLESS_AI_API_KEY', secretKeys: ['featherless-key', 'featherless-api-key'] },\n // Batch 6 — enterprise hyperscaler gateways (2026-04-22).\n // Azure OpenAI already has API_KEY/ENDPOINT/DEPLOYMENT wired above;\n // these fill in the remaining URL-template parameters.\n { envVar: 'AZURE_OPENAI_RESOURCE_NAME', secretKeys: ['azure-openai-resource-name', 'azure-openai-resource'] },\n { envVar: 'AZURE_OPENAI_API_VERSION', secretKeys: ['azure-openai-api-version'] },\n // Gemini OAI-compat shares the API key with the native `google` provider.\n // Two env-var aliases: GEMINI_API_KEY (canonical upstream) and\n // GOOGLE_AI_STUDIO_API_KEY (legacy). The adapter reads either.\n //\n // 2026-04-23 final pass: `vertex-key` added as a fallback source because\n // it was verified working against generativelanguage.googleapis.com this\n // session (HTTP 200, 50 Gemini models) while `google-key` is suspended\n // (403 CONSUMER_SUSPENDED). The Vertex/Gemini API-key split is\n // mostly historical — a Google Cloud API key scoped for Gemini works\n // equally on aistudio and on the generativelanguage surface, as long as\n // the key's GCP project has the API enabled. Putting `vertex-key` at\n // the END of the fallback preserves existing behavior (gemini-key /\n // google-ai-studio-key / google-key are still preferred when present)\n // while letting the vertex-key unlock gemini-openai today.\n { envVar: 'GEMINI_API_KEY', secretKeys: ['google-key', 'gemini-key', 'google-ai-studio-key', 'vertex-key'] },\n // GitHub Models uses a GitHub PAT. Operators typically already have this\n // in env (it's the same PAT used for gh CLI); the GCP path is a fallback\n // for service accounts that need isolated tokens.\n { envVar: 'GITHUB_TOKEN', secretKeys: ['github-models-token', 'github-token', 'github-pat'] },\n // Databricks workspace token + config.\n { envVar: 'DATABRICKS_TOKEN', secretKeys: ['databricks-token', 'databricks-pat'] },\n { envVar: 'DATABRICKS_HOST', secretKeys: ['databricks-host'] },\n { envVar: 'DATABRICKS_SERVING_ENDPOINT', secretKeys: ['databricks-serving-endpoint'] },\n // Inworld AI — orphan closure 2026-04-22. Key is already base64-encoded\n // in GCP (132 chars) because Inworld uses Basic auth, not Bearer. The\n // adapter does NOT re-encode; it passes the secret verbatim into the\n // `Basic ` header. Never base64-encode this value at load time.\n { envVar: 'INWORLD_API_KEY', secretKeys: ['inworld-key', 'inworld-api-key'] },\n // ── Orphan-closure Lot B (2026-04-23) ──────────────────────────────────\n // Three providers closed in a single lot because they share identical\n // shape (thin OAI-compat wrappers) and were all already present in GCP\n // Secret Manager but not wired into env loading. Each tuple lists the\n // preferred secret key first and legacy-or-alternate names as fallbacks.\n { envVar: 'WRITER_API_KEY', secretKeys: ['writer-key', 'writer-api-key'] },\n { envVar: 'UPSTAGE_API_KEY', secretKeys: ['upstage-key', 'upstage-api-key'] },\n // Reka's canonical upstream name is \"reka\" but the key is stored in GCP\n // as `ailin-rekaai-key` (matching the catalog providerId, not the upstream\n // name). `reka-key` is kept as a fallback in case the secret is ever\n // renamed to match upstream naming later.\n { envVar: 'REKA_API_KEY', secretKeys: ['rekaai-key', 'reka-key', 'reka-api-key'] },\n // ── LOTE M (2026-04-23) — complement lot: 13 new env-var mappings. ─────\n // Every provider below landed a catalog row this lot and is classified\n // in CONSOLIDATION_MATRIX['credentials-missing'] (secret-absent sub-class\n // for 12, auth-incomplete for qianfan). Each mapping is wired NOW so the\n // moment an operator provisions the corresponding GCP secret, the key\n // propagates to process.env without a code change. The first element of\n // each secretKeys array is the preferred upstream-convention name; any\n // follow-up entries are fallbacks for historical or legacy aliases.\n { envVar: 'ARCEE_API_KEY', secretKeys: ['arcee-api-key', 'arcee-key'] },\n { envVar: 'ATLASCLOUD_API_KEY', secretKeys: ['atlascloud-key', 'atlascloud-api-key', 'atlas-cloud-key'] },\n { envVar: 'AVIAN_API_KEY', secretKeys: ['avian-key', 'avian-api-key', 'avian-io-key'] },\n // Qianfan (Baidu) v2 bce-v3 bearer. Distinct from the legacy v1 AK+SK\n // material in ERNIE_API_KEY / ERNIE_SECRET_KEY / BAIDU_BASE_URL above —\n // both mappings are intentionally kept so an operator can populate either\n // path (v1 OAuth or v2 bearer) depending on which runtime surface is\n // used. The canonical catalog row `qianfan` reads QIANFAN_API_KEY only.\n { envVar: 'QIANFAN_API_KEY', secretKeys: ['qianfan-key', 'qianfan-api-key', 'baidu-qianfan-key'] },\n { envVar: 'GMI_API_KEY', secretKeys: ['gmi-key', 'gmi-api-key', 'gmicloud-key'] },\n { envVar: 'INFERMATIC_API_KEY', secretKeys: ['infermatic-api-key', 'infermatic-key', 'totalgpt-key'] },\n { envVar: 'INFLECTION_API_KEY', secretKeys: ['inflection-key', 'inflection-api-key', 'inflection-ai-key'] },\n // Mancer (tagged contentPolicyClass=uncensored in catalog; per the universal\n // \"habilitado e nunca censurado\" policy from Phase 4b, 2026-04-28, the\n // catalog row is fully admitted to routing — the tag is informational so\n // downstream surfaces can opt to filter it from a moderated default list).\n { envVar: 'MANCER_API_KEY', secretKeys: ['mancer-key', 'mancer-api-key'] },\n // Phala / RedPill TEE — upstream docs use REDPILL_API_KEY but we wire\n // PHALA_API_KEY as the canonical env var (see apiKeyEnvVarOverrideReason\n // in catalog row `phala`). Legacy `redpill-*` secret names are accepted\n // as fallbacks so an operator storing under either convention works.\n { envVar: 'PHALA_API_KEY', secretKeys: ['phala-key', 'phala-api-key', 'redpill-key', 'redpill-api-key'] },\n { envVar: 'RELACE_API_KEY', secretKeys: ['relace-key', 'relace-api-key'] },\n { envVar: 'SILICONFLOW_API_KEY', secretKeys: ['siliconflow-api-key', 'siliconflow-key', 'silicon-flow-key'] },\n { envVar: 'STEPFUN_API_KEY', secretKeys: ['stepfun-api-key', 'stepfun-key', 'step-key'] },\n // Venice (tagged contentPolicyClass=uncensored in catalog). Same Phase-4b\n // treatment as Mancer — admitted, never censored, with the tag as the\n // audit trail for the policy choice.\n { envVar: 'VENICE_API_KEY', secretKeys: ['venice-key', 'venice-api-key', 'venice-ai-key'] },\n // ── Sublote E1 (2026-04-24) — runtime-wiring closure for D1 matrix ─────\n // Sublote D1 promoted 10 providers to CONSOLIDATION_MATRIX['live-validation']\n // based on direct-probe evidence (via gcloud secrets versions access,\n // bypassing the runtime env loader). 7 of those had no GCP→ENV tuple here,\n // so the catalog-loader could never surface `process.env.<X>_API_KEY` at\n // boot — meaning live-validated providers were silently \"not runnable\".\n // Wired pre-emptively: the moment an operator provisions the GCP secret\n // AND the container restarts, propagation happens with zero code change.\n // The 3 already-mapped providers (heliconeai, github-models, infermatic)\n // are NOT duplicated — they appear above under their respective batches.\n { envVar: 'GROQ_API_KEY', secretKeys: ['groq-api-key', 'groq-key'] },\n { envVar: 'DEEPINFRA_API_KEY', secretKeys: ['deepinfra-api-key', 'deepinfra-key'] },\n // Hugging Face: upstream canonical env var is `HF_TOKEN` (not\n // HUGGINGFACE_API_KEY); the catalog row's apiKeyEnvVar matches. Primary\n // GCP secret name follows the upstream convention; legacy huggingface-*\n // aliases accepted for operators who stored it under the provider slug.\n { envVar: 'HF_TOKEN', secretKeys: ['huggingface-api-key', 'huggingface-token', 'hf-token', 'huggingface-key'] },\n // Cloudflare Workers AI — requires BOTH api-token AND account-id. The\n // CloudflareWorkersAIAdapter factory (default-adapter-factories.ts line\n // 229-246) reads CLOUDFLARE_ACCOUNT_ID from env and substitutes it into\n // the `{account_id}` URL template. Without the account-id side-car the\n // adapter ships the literal template to the wire → 404 on every call.\n // Side-car is wired here as a separate tuple because it's not an auth\n // credential — it's a URL parameter — so it doesn't belong in\n // ENV_VAR_TO_PROVIDER or LLM_PROVIDER_ENV_VARS below.\n { envVar: 'CLOUDFLARE_API_TOKEN', secretKeys: ['cloudflare-workers-ai-api-key', 'cloudflare-workers-ai-token', 'cloudflare-api-token', 'cloudflare-workers-ai-key'] },\n { envVar: 'CLOUDFLARE_ACCOUNT_ID', secretKeys: ['cloudflare-workers-ai-id', 'cloudflare-account-id', 'cloudflare-workers-ai-account-id'] },\n { envVar: 'PERPLEXITY_API_KEY', secretKeys: ['perplexity-api-key', 'perplexity-key'] },\n // Fireworks AI — note the catalog providerId is `fireworks-ai` (hyphenated)\n // but upstream brand/docs spell it \"Fireworks\" (no suffix). Four fallbacks\n // cover both conventions + api-key suffix variant.\n { envVar: 'FIREWORKS_AI_API_KEY', secretKeys: ['fireworks-ai-api-key', 'fireworks-ai-key', 'fireworks-key', 'fireworks-api-key'] },\n { envVar: 'SAMBANOVA_API_KEY', secretKeys: ['sambanova-api-key', 'sambanova-key'] },\n // Replicate — flagged by the sublote-e1-runtime-wiring invariant as a\n // pre-existing gap. `replicate` has been in CONSOLIDATION_MATRIX\n // ['live-validation'] since before D1, with adapterClass 'ReplicateAdapter'\n // and apiKeyEnvVar 'REPLICATE_API_KEY', but no tuple was ever added here.\n // Even though integrationMode is 'execution-only' (no /v1/models\n // discovery), the adapter still requires the env var at /predictions\n // call time. Wired here for closure; the GCP secret name follows the\n // provider-slug convention with optional `-api-key` fallback.\n { envVar: 'REPLICATE_API_KEY', secretKeys: ['replicate-key', 'replicate-api-key'] },\n // ── LOTE N (2026-04-27) — catalog↔loader closure for the remaining 22 ─\n // apiKeyEnvVar entries that had catalog rows but NO PROVIDER_SECRETS\n // tuple. Surfaced by `catalog-loader-wiring-completeness.test.ts` (J6\n // invariant). All 22 are real provider rows with adapter classes (or\n // generic OAI-compat hub registration); they were classified in\n // CONSOLIDATION_MATRIX as `credentials-missing` or `disabled-by-default`,\n // and their absence here meant that even after an operator provisioned\n // the GCP secret, `process.env.<X>_API_KEY` would stay empty at boot.\n //\n // Pattern: preferred secret name first (provider-slug `-key`), then\n // upstream-convention `-api-key` fallback, then any historical aliases.\n // The provider-side `aliases` array in providers.catalog.ts is NOT\n // duplicated here — secret-key aliases are about HOW the operator\n // stored the credential, NOT how callers reach the provider.\n //\n // Image/video-only providers (bfl, recraft, runwayml, topaz) are wired\n // here too but are intentionally absent from LLM_PROVIDER_ENV_VARS below\n // because their presence does NOT count toward the \"at least one LLM\n // key present\" boot-mode gate.\n { envVar: 'TOGETHERAI_API_KEY', secretKeys: ['togetherai-api-key', 'togetherai-key', 'together-key', 'together-api-key'] },\n { envVar: 'ANYSCALE_API_KEY', secretKeys: ['anyscale-api-key', 'anyscale-key'] },\n { envVar: 'HYPERBOLIC_API_KEY', secretKeys: ['hyperbolic-api-key', 'hyperbolic-key'] },\n { envVar: 'CHUTES_API_KEY', secretKeys: ['chutes-api-key', 'chutes-key', 'chutes-ai-key'] },\n { envVar: 'NSCALE_API_KEY', secretKeys: ['nscale-key', 'nscale-api-key'] },\n { envVar: 'NEBIUS_API_KEY', secretKeys: ['nebius-key', 'nebius-api-key'] },\n // Lambda Cloud canonical provider slug is `lambda-ai`; brand is\n // \"Lambda\" (without -ai). Both spellings tried as fallbacks.\n { envVar: 'LAMBDA_AI_API_KEY', secretKeys: ['lambda-ai-key', 'lambda-key', 'lambda-cloud-key', 'lambda-ai-api-key'] },\n { envVar: 'SCALEWAY_API_KEY', secretKeys: ['scaleway-key', 'scaleway-api-key'] },\n { envVar: 'SYNTHETIC_API_KEY', secretKeys: ['synthetic-key', 'synthetic-api-key', 'synthetic-ai-key'] },\n { envVar: 'MORPH_API_KEY', secretKeys: ['morph-key', 'morph-api-key', 'morph-llm-key'] },\n // Z.ai (catalog providerId = `zai`). Upstream domain z.ai resolves to\n // bigmodel.cn — the GLM family operator. Legacy spelling z-ai accepted.\n { envVar: 'ZAI_API_KEY', secretKeys: ['zai-key', 'zai-api-key', 'z-ai-key', 'glm-key'] },\n { envVar: 'XIAOMI_MIMO_API_KEY', secretKeys: ['xiaomi-mimo-key', 'mimo-key', 'xiaomi-mimo-api-key'] },\n // v0 (Vercel) — code-gen LLM. The catalog providerId is `v0` (one\n // character) so the env var name is `V0_API_KEY` not `V0AI_API_KEY`.\n { envVar: 'V0_API_KEY', secretKeys: ['v0-key', 'v0-api-key', 'vercel-v0-key'] },\n // Vercel AI Gateway — DIFFERENT from v0 above (gateway over many LLMs).\n // The canonical secret name follows the catalog providerId.\n { envVar: 'VERCEL_AI_GATEWAY_API_KEY', secretKeys: ['vercel-ai-gateway-key', 'vercel-ai-gateway-api-key', 'vercel-gateway-key'] },\n // Volcano (Volcengine — ByteDance). Canonical slug `volcano`; brand\n // includes Volcengine + Doubao + Ark. Multiple aliases accepted.\n { envVar: 'VOLCANO_API_KEY', secretKeys: ['volcano-key', 'volcano-api-key', 'volcengine-key', 'doubao-key', 'ark-key'] },\n // IBM watsonx — note the env var is `WATSONX_APIKEY` (no underscore\n // between API and KEY) to match IBM's canonical SDK convention.\n // Side-car env vars (WATSONX_PROJECT_ID, WATSONX_URL) are wired below\n // because they're documented in the catalog row's `extraEnvVars` and\n // adapter requires them at runtime.\n { envVar: 'WATSONX_APIKEY', secretKeys: ['watsonx-apikey', 'watsonx-key', 'watsonx-api-key', 'ibm-watsonx-key'] },\n { envVar: 'WATSONX_PROJECT_ID', secretKeys: ['watsonx-project-id', 'ibm-watsonx-project-id'] },\n { envVar: 'WATSONX_URL', secretKeys: ['watsonx-url', 'ibm-watsonx-url'] },\n // Snowflake Cortex — uses Personal Access Token (PAT), not an API key.\n // Multiple side-cars required by the SnowflakeCortexAdapter:\n // SNOWFLAKE_ACCOUNT (orgname-accountname), SNOWFLAKE_USER (key-pair\n // auth user), SNOWFLAKE_BASE_URL (account-specific host). Side-cars\n // wired here so a single GCP provisioning round materializes the full\n // tuple at boot.\n { envVar: 'SNOWFLAKE_PAT', secretKeys: ['snowflake-pat', 'snowflake-token', 'snowflake-key'] },\n { envVar: 'SNOWFLAKE_ACCOUNT', secretKeys: ['snowflake-account'] },\n { envVar: 'SNOWFLAKE_USER', secretKeys: ['snowflake-user', 'snowflake-username'] },\n { envVar: 'SNOWFLAKE_BASE_URL', secretKeys: ['snowflake-base-url', 'snowflake-url'] },\n // SAP AI Core — OAuth2 client_credentials flow. Catalog providerId is\n // `sap`. The apiKeyEnvVar is `SAP_AI_CORE_CLIENT_ID` (the OAuth client\n // id); the matching client_secret + token URL + base URL are all\n // documented in the catalog row's `extraEnvVars` and required by the\n // SapAiCoreAdapter. Wiring all four together so a single provisioning\n // round closes the loop.\n { envVar: 'SAP_AI_CORE_CLIENT_ID', secretKeys: ['sap-ai-core-client-id', 'sap-client-id'] },\n { envVar: 'SAP_AI_CORE_CLIENT_SECRET', secretKeys: ['sap-ai-core-client-secret', 'sap-client-secret'] },\n { envVar: 'SAP_AI_CORE_AUTH_URL', secretKeys: ['sap-ai-core-auth-url', 'sap-auth-url'] },\n { envVar: 'SAP_AI_CORE_BASE_URL', secretKeys: ['sap-ai-core-base-url', 'sap-base-url'] },\n // W&B Inference side-car — WANDB_API_KEY (auth) is already wired in\n // Batch 7 above; WANDB_PROJECT is the project slug header required on\n // every chat call. Wiring here closes the side-car gap.\n { envVar: 'WANDB_PROJECT', secretKeys: ['wandb-project', 'wandb-project-slug'] },\n // Image-only providers (no LLM gate participation, see comment below).\n // bfl auth header is `x-key` (not Bearer) — see catalog `authHeaderName`.\n { envVar: 'BFL_API_KEY', secretKeys: ['bfl-key', 'bfl-api-key', 'flux-key', 'black-forest-labs-key'] },\n { envVar: 'RECRAFT_API_KEY', secretKeys: ['recraft-key', 'recraft-api-key'] },\n { envVar: 'RUNWAYML_API_KEY', secretKeys: ['runwayml-key', 'runwayml-api-key', 'runway-key'] },\n { envVar: 'TOPAZ_API_KEY', secretKeys: ['topaz-key', 'topaz-api-key', 'topaz-labs-key'] },\n // ── LOTE O (2026-07-10) — Apertis + Inception Labs onboarding ─────────\n // Operator pre-provisioned both GCP secrets ahead of the catalog rows\n // (ailin-apertis-key, ailin-inception-key). secretKeys match exactly —\n // the GCP provider auto-prepends the `ailin-` prefix (see file header).\n { envVar: 'APERTIS_API_KEY', secretKeys: ['apertis-key', 'apertis-api-key'] },\n { envVar: 'INCEPTION_API_KEY', secretKeys: ['inception-key', 'inception-api-key'] },\n // ── LOTE P (2026-07-11) — EmpirioLabs AI, live-validated after gate ────\n { envVar: 'EMPIRIOLABS_API_KEY', secretKeys: ['empiriolabs-key', 'empiriolabs-api-key'] },\n // ── LOTE Q (2026-07-12) — Concentrate AI ───────────────────────────────\n { envVar: 'CONCENTRATE_API_KEY', secretKeys: ['concentrate-key', 'concentrate-api-key'] },\n // ── LOTE R (2026-07-13) — FastRouter ────────────────────────────────────\n { envVar: 'FASTROUTER_API_KEY', secretKeys: ['fastrouter-key', 'fastrouter-api-key'] },\n // ── LOTE S (2026-07-13) — Perplexity Agent API ──────────────────────────\n // Same physical credential as PERPLEXITY_API_KEY above (one Perplexity\n // account, two API surfaces) — deliberately sources from the SAME GCP\n // secret names, not a new one.\n { envVar: 'PERPLEXITY_AGENT_API_KEY', secretKeys: ['perplexity-api-key', 'perplexity-key'] },\n // ── LOTE T (2026-07-13) — Ailin gateway (self-referential meta-provider) ──\n // No GCP secret provisioned yet — inert until an operator sets AILIN_API_KEY\n // for a self-hosted deployment. secretKeys follow the same naming\n // convention as the rest of this file in case one is provisioned later.\n { envVar: 'AILIN_API_KEY', secretKeys: ['ailin-key', 'ailin-api-key', 'gateway-key'] },\n] as const;\n\n// ─── Provider Key Status Tracking ──────────────────────────────────────────\n// Tracks which provider API keys were successfully loaded, from which source,\n// and when. Used by the Self-Healing Discovery (L1) to know when to retry\n// discovery for native providers whose keys arrived late (GCP delay/failure).\n\nexport interface ProviderKeyStatus {\n loaded: boolean;\n source: 'gcp' | 'env' | 'none';\n timestamp: Date;\n envVar: string;\n}\n\n/**\n * Module-level state: populated during loadSecretsIntoEnv() for each provider.\n * Key = normalized provider name (lowercase, e.g., 'openai', 'anthropic').\n */\nconst providerKeyStatusMap = new Map<string, ProviderKeyStatus>();\n\n/**\n * Mapping from env var name to normalized provider name.\n * Allows the discovery service to map env vars to provider sources.\n */\nconst ENV_VAR_TO_PROVIDER: Record<string, string> = {\n // Native API providers\n OPENAI_API_KEY: 'openai',\n ANTHROPIC_API_KEY: 'anthropic',\n GOOGLE_API_KEY: 'google',\n DEEPSEEK_API_KEY: 'deepseek',\n XAI_API_KEY: 'xai',\n MISTRAL_API_KEY: 'mistral',\n COHERE_API_KEY: 'cohere',\n JINA_API_KEY: 'jina',\n // Cloud hubs — 2026-04-23: use canonical providerId 'nvidia' (legacy\n // alias 'nvidia-hub' was absorbed into the nvidia catalog row per Lot B;\n // discovery/status consumers read providerKeyStatus by canonical id).\n NVIDIA_API_KEY: 'nvidia',\n AIHUBMIX_API_KEY: 'aihubmix',\n NOVITA_API_KEY: 'novita',\n MOONSHOT_API_KEY: 'moonshot',\n MINIMAX_API_KEY: 'minimax',\n FRIENDLI_API_KEY: 'friendli',\n AIML_API_KEY: 'aiml',\n IMAGEROUTER_API_KEY: 'imagerouter',\n COMETAPI_API_KEY: 'cometapi',\n NANOGPT_API_KEY: 'nanogpt',\n EDENAI_API_KEY: 'edenai',\n HELICONEAI_API_KEY: 'heliconeai',\n POE_API_KEY: 'poe',\n ROUTEWAY_API_KEY: 'routeway',\n REQUESTY_API_KEY: 'requesty',\n // Post-migration canonical providerId is `ai302` (see providers.catalog.ts\n // and the residue-closure note in provider-registry.ts). Legacy `302ai`\n // survives as a catalog alias for pre-migration user configs.\n AI302_API_KEY: 'ai302',\n ORQAI_API_KEY: 'orqai',\n // Routers/aggregators\n OPENROUTER_API_KEY: 'openrouter',\n // Cloud providers\n VERTEX_AI_API_KEY: 'vertex-ai',\n AWS_ACCESS_KEY_ID: 'aws-bedrock',\n AWS_BEARER_TOKEN_BEDROCK: 'aws-bedrock',\n // SageMaker uses the same shared AWS creds as Bedrock, so `AWS_ACCESS_KEY_ID`\n // keeps mapping to `aws-bedrock` (first-listed wins) — but the endpoint-\n // specific env var is only meaningful for SageMaker. When operators wire\n // this, Self-Healing Discovery will attribute the key presence to the\n // SageMaker provider specifically.\n AWS_SAGEMAKER_ENDPOINT_NAME: 'aws-sagemaker',\n AZURE_OPENAI_API_KEY: 'azure-openai',\n // Regional/specialized\n QWEN_API_KEY: 'qwen',\n ERNIE_API_KEY: 'ernie',\n // Audio providers\n DEEPGRAM_API_KEY: 'deepgram',\n CARTESIA_API_KEY: 'cartesia',\n ELEVENLABS_API_KEY: 'elevenlabs',\n // Inference + embedding providers newly onboarded 2026-04-22\n WANDB_API_KEY: 'wandb',\n VOYAGE_API_KEY: 'voyage',\n BYTEZ_API_KEY: 'bytez',\n CEREBRAS_API_KEY: 'cerebras',\n FEATHERLESS_AI_API_KEY: 'featherless-ai', // canonical catalog id (2026-04-23 drift fix)\n // Batch 6 — enterprise hyperscaler gateways (2026-04-22). Gemini OAI-compat\n // is registered as a SEPARATE provider from the native `google` entry so\n // the self-healing discovery service can track them independently (they\n // have different failure modes — native uses @google/generative-ai SDK\n // with its own error surfaces; OAI-compat uses standard fetch).\n GEMINI_API_KEY: 'gemini-openai',\n GITHUB_TOKEN: 'github-models',\n DATABRICKS_TOKEN: 'databricks',\n // Orphan closure (2026-04-22) — Inworld now has a catalog row + adapter\n // factory. Mapping the env var to the provider slug lets Self-Healing\n // Discovery attribute key presence to the correct provider.\n INWORLD_API_KEY: 'inworld',\n // Orphan-closure Lot B (2026-04-23). Slug = catalog providerId. Writer\n // and Upstage are self-evident; Reka's slug is `rekaai` (the catalog\n // row's canonical providerId — aliases 'reka' + 'reka-ai' resolve to it).\n WRITER_API_KEY: 'writer',\n UPSTAGE_API_KEY: 'upstage',\n REKA_API_KEY: 'rekaai',\n // ── LOTE M (2026-04-23) — complement lot, 13 providers ──────────────\n // Slug = catalog providerId (NOT the upstream brand for phala/qianfan\n // which intentionally diverge — see catalog rows for rationale):\n // phala — canonical slug; upstream brand is \"RedPill\"\n // qianfan — canonical slug; upstream brand is \"Baidu Qianfan / ERNIE\"\n // gmi — canonical slug; upstream brand is \"GMICloud\"\n ARCEE_API_KEY: 'arcee',\n ATLASCLOUD_API_KEY: 'atlascloud',\n AVIAN_API_KEY: 'avian',\n QIANFAN_API_KEY: 'qianfan',\n GMI_API_KEY: 'gmi',\n INFERMATIC_API_KEY: 'infermatic',\n INFLECTION_API_KEY: 'inflection',\n MANCER_API_KEY: 'mancer',\n PHALA_API_KEY: 'phala',\n RELACE_API_KEY: 'relace',\n SILICONFLOW_API_KEY: 'siliconflow',\n STEPFUN_API_KEY: 'stepfun',\n VENICE_API_KEY: 'venice',\n // ── Sublote E1 (2026-04-24) — runtime-wiring closure for D1 matrix ───\n // 7 provider slugs whose env vars gained GCP→ENV tuples in this lot.\n // Slug = catalog providerId exactly (so Self-Healing Discovery can\n // attribute key-loaded events to the right provider). CLOUDFLARE_ACCOUNT_ID\n // is intentionally absent — that's a URL parameter, not an auth credential,\n // so mapping it to a provider slug would be semantically wrong (the same\n // account-id could conceivably scope multiple Cloudflare providers).\n GROQ_API_KEY: 'groq',\n DEEPINFRA_API_KEY: 'deepinfra',\n HF_TOKEN: 'huggingface',\n CLOUDFLARE_API_TOKEN: 'cloudflare-workers-ai',\n PERPLEXITY_API_KEY: 'perplexity',\n FIREWORKS_AI_API_KEY: 'fireworks-ai',\n SAMBANOVA_API_KEY: 'sambanova',\n // Replicate — pre-existing live-validation gap closed by sublote E1.\n // See the matching comment on the PROVIDER_SECRETS tuple above.\n REPLICATE_API_KEY: 'replicate',\n // ── LOTE N (2026-04-27) — catalog↔loader closure (22 new env vars) ──\n // Every entry below maps an env var to its canonical providerId so the\n // L1 Self-Healing Discovery Service can attribute key-loaded events\n // when the corresponding GCP secret is provisioned. Slugs match the\n // catalog providerId exactly (NOT upstream brand) — see catalog rows\n // for cases where they diverge (e.g. `sap` not `sap-ai-core`, `v0` not\n // `vercel-v0`, `volcano` not `volcengine`). Side-car env vars\n // (WATSONX_PROJECT_ID, SNOWFLAKE_ACCOUNT, SAP_AI_CORE_CLIENT_SECRET,\n // etc.) are NOT mapped here — they're auth or URL parameters, not\n // credentials, so attributing them to a provider would conflate\n // credential-presence with side-car-completeness in discovery telemetry.\n TOGETHERAI_API_KEY: 'togetherai',\n ANYSCALE_API_KEY: 'anyscale',\n HYPERBOLIC_API_KEY: 'hyperbolic',\n CHUTES_API_KEY: 'chutes',\n NSCALE_API_KEY: 'nscale',\n NEBIUS_API_KEY: 'nebius',\n LAMBDA_AI_API_KEY: 'lambda-ai',\n SCALEWAY_API_KEY: 'scaleway',\n SYNTHETIC_API_KEY: 'synthetic',\n MORPH_API_KEY: 'morph',\n ZAI_API_KEY: 'zai',\n XIAOMI_MIMO_API_KEY: 'xiaomi-mimo',\n V0_API_KEY: 'v0',\n VERCEL_AI_GATEWAY_API_KEY: 'vercel-ai-gateway',\n VOLCANO_API_KEY: 'volcano',\n WATSONX_APIKEY: 'watsonx',\n SNOWFLAKE_PAT: 'snowflake',\n SAP_AI_CORE_CLIENT_ID: 'sap',\n // Image / video-only providers. Still mapped to a slug so Self-Healing\n // Discovery can record key-presence events — but their env vars do NOT\n // appear in LLM_PROVIDER_ENV_VARS (image surfaces don't satisfy the\n // \"at least one LLM key\" boot gate).\n BFL_API_KEY: 'bfl',\n RECRAFT_API_KEY: 'recraft',\n RUNWAYML_API_KEY: 'runwayml',\n TOPAZ_API_KEY: 'topaz',\n // ── LOTE O (2026-07-10) ────────────────────────────────────────────────\n APERTIS_API_KEY: 'apertis',\n INCEPTION_API_KEY: 'inception',\n // ── LOTE P (2026-07-11) ────────────────────────────────────────────────\n EMPIRIOLABS_API_KEY: 'empiriolabs',\n // ── LOTE Q (2026-07-12) ────────────────────────────────────────────────\n CONCENTRATE_API_KEY: 'concentrate',\n // ── LOTE R (2026-07-13) ────────────────────────────────────────────────\n FASTROUTER_API_KEY: 'fastrouter',\n // ── LOTE S (2026-07-13) ────────────────────────────────────────────────\n PERPLEXITY_AGENT_API_KEY: 'perplexity-agent',\n // ── LOTE T (2026-07-13) ────────────────────────────────────────────────\n AILIN_API_KEY: 'ailin',\n};\n\n/**\n * Returns the status of all provider API keys.\n * Used by Self-Healing Discovery to determine which native sources can run.\n */\nexport function getProviderKeyStatus(): ReadonlyMap<string, ProviderKeyStatus> {\n return providerKeyStatusMap;\n}\n\n/**\n * Returns provider names that have valid (non-empty, non-mock) API keys loaded.\n */\nexport function getLoadedProviderNames(): string[] {\n return Array.from(providerKeyStatusMap.entries())\n .filter(([, status]) => status.loaded)\n .map(([name]) => name);\n}\n\n/**\n * Check if a specific provider has a loaded API key.\n */\nexport function isProviderKeyLoaded(providerName: string): boolean {\n return providerKeyStatusMap.get(providerName.toLowerCase())?.loaded ?? false;\n}\n\nconst LLM_PROVIDER_ENV_VARS = [\n 'OPENAI_API_KEY',\n 'ANTHROPIC_API_KEY',\n 'GOOGLE_API_KEY',\n 'DEEPSEEK_API_KEY',\n 'XAI_API_KEY',\n 'MISTRAL_API_KEY',\n 'COHERE_API_KEY',\n 'NVIDIA_API_KEY',\n 'AIHUBMIX_API_KEY',\n 'NOVITA_API_KEY',\n 'MOONSHOT_API_KEY',\n 'MINIMAX_API_KEY',\n 'JINA_API_KEY',\n 'FRIENDLI_API_KEY',\n 'AIML_API_KEY',\n 'IMAGEROUTER_API_KEY',\n 'OPENROUTER_API_KEY',\n 'ORQAI_API_KEY',\n 'EDENAI_API_KEY',\n 'HELICONEAI_API_KEY',\n 'VERTEX_AI_API_KEY',\n 'AWS_BEARER_TOKEN_BEDROCK',\n 'AZURE_OPENAI_API_KEY',\n 'QWEN_API_KEY',\n 'ERNIE_API_KEY',\n 'COMETAPI_API_KEY',\n 'NANOGPT_API_KEY',\n 'REQUESTY_API_KEY',\n 'AI302_API_KEY',\n 'POE_API_KEY',\n 'ROUTEWAY_API_KEY',\n // Added 2026-04-22 — these keys now count toward the \"at least one LLM key\n // present\" gate that avoids DEGRADED_SELF_HOSTED mode at boot.\n 'WANDB_API_KEY',\n 'VOYAGE_API_KEY',\n 'CEREBRAS_API_KEY',\n 'FEATHERLESS_AI_API_KEY',\n // Batch 4 landed BYTEZ catalog row + adapter, so BYTEZ_API_KEY now counts\n // toward the LLM-key-present gate.\n 'BYTEZ_API_KEY',\n // Batch 6 — enterprise hyperscaler gateways (2026-04-22). These keys now\n // count toward the \"at least one LLM key present\" gate; all four have\n // landed catalog rows + adapter classes in this batch.\n 'GEMINI_API_KEY',\n 'GITHUB_TOKEN',\n 'DATABRICKS_TOKEN',\n // Batch 8.1 — AWS SageMaker. Endpoint name is the meaningful trigger:\n // AWS creds alone are ambiguous (could be Bedrock or S3), but an\n // endpoint name implies \"SageMaker is provisioned here\".\n 'AWS_SAGEMAKER_ENDPOINT_NAME',\n // Orphan closure (2026-04-22) — Inworld AI now counts toward the\n // \"at least one LLM key present\" gate now that the catalog row +\n // adapter factory are wired.\n 'INWORLD_API_KEY',\n // Orphan-closure Lot B (2026-04-23). All three are fully canonical now.\n 'WRITER_API_KEY',\n 'UPSTAGE_API_KEY',\n 'REKA_API_KEY',\n // ── LOTE M (2026-04-23) complement lot ──────────────────────────────\n // Each of the 13 keys below counts toward the \"at least one LLM key\n // present\" gate, the same way all catalog-wired keys do. Note that\n // mancer and venice carry contentPolicyClass=uncensored (2026-04-28\n // Phase 4b — the prior denyByDefault gate was removed per the universal\n // \"habilitado e nunca censurado\" policy); their keys count toward the\n // gate exactly like any other LLM key — surface-level filtering is now\n // a downstream concern, not a routing one.\n // qianfan here is QIANFAN_API_KEY (v2 bce-v3 bearer); the legacy v1\n // ERNIE_API_KEY was already listed above this marker.\n 'ARCEE_API_KEY',\n 'ATLASCLOUD_API_KEY',\n 'AVIAN_API_KEY',\n 'QIANFAN_API_KEY',\n 'GMI_API_KEY',\n 'INFERMATIC_API_KEY',\n 'INFLECTION_API_KEY',\n 'MANCER_API_KEY',\n 'PHALA_API_KEY',\n 'RELACE_API_KEY',\n 'SILICONFLOW_API_KEY',\n 'STEPFUN_API_KEY',\n 'VENICE_API_KEY',\n // ── Sublote E1 (2026-04-24) — runtime-wiring closure for D1 matrix ───\n // These 7 providers landed in CONSOLIDATION_MATRIX['live-validation']\n // during Sublote D1 but had no GCP→ENV tuple above → their keys could\n // not count toward the \"at least one LLM key present\" gate even when\n // GCP Secret Manager held the key. Listed here so a restart after the\n // operator's provisioning cycle propagates the key-present signal to\n // the DEGRADED_SELF_HOSTED avoidance logic. CLOUDFLARE_ACCOUNT_ID is\n // intentionally absent — it's a URL parameter, not an LLM credential.\n 'GROQ_API_KEY',\n 'DEEPINFRA_API_KEY',\n 'HF_TOKEN',\n 'CLOUDFLARE_API_TOKEN',\n 'PERPLEXITY_API_KEY',\n 'FIREWORKS_AI_API_KEY',\n 'SAMBANOVA_API_KEY',\n // Replicate — pre-existing live-validation gap closed by sublote E1.\n // See the matching comments on the PROVIDER_SECRETS tuple and\n // ENV_VAR_TO_PROVIDER entry above.\n 'REPLICATE_API_KEY',\n // ── LOTE N (2026-04-27) — catalog↔loader closure ─────────────────────\n // 18 LLM-class providers added to the boot-mode gate. The 4 image/\n // video-only providers (bfl, recraft, runwayml, topaz) are\n // intentionally OMITTED — their key presence does not signal the\n // server has any text-generation capability, so they should not\n // satisfy the \"at least one LLM key present\" check.\n 'TOGETHERAI_API_KEY',\n 'ANYSCALE_API_KEY',\n 'HYPERBOLIC_API_KEY',\n 'CHUTES_API_KEY',\n 'NSCALE_API_KEY',\n 'NEBIUS_API_KEY',\n 'LAMBDA_AI_API_KEY',\n 'SCALEWAY_API_KEY',\n 'SYNTHETIC_API_KEY',\n 'MORPH_API_KEY',\n 'ZAI_API_KEY',\n 'XIAOMI_MIMO_API_KEY',\n 'V0_API_KEY',\n 'VERCEL_AI_GATEWAY_API_KEY',\n 'VOLCANO_API_KEY',\n 'WATSONX_APIKEY',\n 'SNOWFLAKE_PAT',\n 'SAP_AI_CORE_CLIENT_ID',\n // ── LOTE O (2026-07-10) — Apertis + Inception Labs, both chat-capable ──\n 'APERTIS_API_KEY',\n 'INCEPTION_API_KEY',\n // ── LOTE P (2026-07-11) — EmpirioLabs AI, chat-capable ─────────────────\n 'EMPIRIOLABS_API_KEY',\n // ── LOTE Q (2026-07-12) — Concentrate AI, chat-capable ─────────────────\n 'CONCENTRATE_API_KEY',\n // ── LOTE R (2026-07-13) — FastRouter, chat-capable ─────────────────────\n 'FASTROUTER_API_KEY',\n // ── LOTE S (2026-07-13) — Perplexity Agent API, chat-capable ───────────\n 'PERPLEXITY_AGENT_API_KEY',\n // ── LOTE T (2026-07-13) — Ailin gateway, chat-capable ──────────────────\n 'AILIN_API_KEY',\n] as const;\n\n/**\n * Self-hosted / sidecar endpoints that can keep the server alive in a\n * DEGRADED mode when every external LLM API key is missing.\n *\n * Design rationale:\n * Without this fallback, a single failure (expired GCP ADC, rotated\n * service account, network blip during boot) produces an infinite\n * restart loop. With it, the server boots in `degraded_self_hosted`\n * mode: only local/sidecar models are usable, requests are tagged\n * via last-resort-policy.ts (execution_mode=last_resort_self_hosted,\n * excluded_from_benchmark=true), and ops gets a clear warning\n * instead of a crash loop.\n *\n * This ties directly into Bloco P1.10 (self-hosted last-resort policy)\n * but at boot-time scope rather than per-request scope.\n */\nconst SELF_HOSTED_ENV_VARS = [\n 'OLLAMA_URL',\n 'SELF_HOSTED_LLM_URL',\n 'LOCAL_LLAMA_URL',\n 'LOCAL_KOBOLD_URL',\n] as const;\n\n/**\n * Load critical secrets from GCP into process.env\n * \n * This function is called AFTER SecretsManager initialization\n * and BEFORE config validation.\n * \n * It loads secrets from GCP and injects them into process.env,\n * allowing the existing config system to work without modification.\n */\nexport async function loadSecretsIntoEnv(): Promise<void> {\n const log = logger.child({ component: 'secrets-loader' });\n\n log.info('Loading critical secrets from GCP Secret Manager into environment...');\n\n let loaded = 0;\n let skipped = 0;\n let failed = 0;\n let sanitizedKeys = 0;\n let providerSecretsLoaded = 0;\n // Tracks env values that were authoritatively replaced by GCP fetches.\n // Useful diagnostic for operators verifying that GCP is the source of truth.\n let overwrittenFromGcp = 0;\n\n const forceLoadRealKeys = process.env.TEST_USE_REAL_API_KEYS === 'true';\n\n // ── GCP-authoritative mode ─────────────────────────────────────────────\n // When SECRETS_PROVIDER_PRIMARY=gcp the operator has declared GCP as the\n // source of truth. The legacy \"skip if env already populated\" path then\n // contradicts that declaration: env_file / docker-compose / shell exports\n // silently win and GCP fetches are never even attempted, which is exactly\n // the failure mode that hid populated GCP secrets (e.g. HF_TOKEN) from\n // /v1/models. In authoritative mode we ALWAYS attempt the GCP fetch and\n // overwrite the pre-existing env value when GCP returns one. The fallback\n // is symmetric: a GCP miss preserves the existing env value (no zeroing).\n //\n // Operators can override:\n // SECRETS_GCP_AUTHORITATIVE=false → keep legacy skip-if-exists semantics\n // SECRETS_GCP_AUTHORITATIVE=true → force authoritative even with env primary\n const gcpPrimaryEnv = (process.env.SECRETS_PROVIDER_PRIMARY || '').trim().toLowerCase() === 'gcp';\n const explicitAuthoritative = (process.env.SECRETS_GCP_AUTHORITATIVE || '').trim().toLowerCase();\n const gcpAuthoritative =\n explicitAuthoritative === 'true' ||\n (explicitAuthoritative !== 'false' && gcpPrimaryEnv);\n\n // Tests already use TEST_USE_REAL_API_KEYS=true to force GCP fetches even\n // when local mocks are present. The two flags are independent triggers\n // for the same effect; either one disables skip-if-exists.\n const authoritativeFetch = gcpAuthoritative || forceLoadRealKeys;\n\n log.info(\n {\n gcpPrimary: gcpPrimaryEnv,\n gcpAuthoritative,\n forceLoadRealKeys,\n authoritativeFetch,\n },\n 'Secret loading mode resolved'\n );\n\n // Load critical secrets (required)\n for (const { envVar, secretKey, required, defaultValue } of CRITICAL_SECRETS) {\n const existingValue = process.env[envVar];\n const shouldOverrideMockStripe =\n forceLoadRealKeys &&\n envVar.startsWith('STRIPE_') &&\n typeof existingValue === 'string' &&\n (existingValue.includes('mock') || existingValue.includes('test'));\n\n // Skip if already in env, UNLESS:\n // - we're in GCP-authoritative mode (operator declared GCP as source of truth), OR\n // - we're forcing real keys and detected mock Stripe keys.\n if (existingValue && !shouldOverrideMockStripe && !gcpAuthoritative) {\n log.debug({ envVar, source: 'env' }, 'Secret already in environment, skipping GCP load');\n skipped++;\n continue;\n }\n\n try {\n const value = await loadSecret(secretKey, required);\n if (value) {\n const wasOverwrite = Boolean(existingValue) && existingValue !== value;\n process.env[envVar] = value;\n if (wasOverwrite) overwrittenFromGcp++;\n log.debug(\n { envVar, secretKey, replaced: Boolean(existingValue), overwrittenFromGcp: wasOverwrite },\n 'Secret loaded from GCP into environment'\n );\n loaded++;\n } else if (existingValue && gcpAuthoritative) {\n // Authoritative fetch missed in GCP — fall back to the existing env\n // value rather than zeroing it. This keeps the system bootable when\n // a single secret is absent from GCP without losing the env-provided\n // value.\n log.debug(\n { envVar, secretKey },\n 'GCP miss in authoritative mode; preserving existing env value'\n );\n skipped++;\n } else if (defaultValue !== undefined) {\n // Use default value if secret not found and default provided\n process.env[envVar] = defaultValue;\n log.debug({ envVar, secretKey, defaultValue }, 'Secret not found, using default value');\n skipped++;\n } else if (!required) {\n log.debug({ envVar, secretKey }, 'Optional secret not found, skipping');\n skipped++;\n }\n } catch (error) {\n if (defaultValue !== undefined) {\n // Use default value on error if provided\n process.env[envVar] = defaultValue;\n log.warn({ envVar, secretKey, defaultValue, error: getErrorMessage(error) }, 'Failed to load secret, using default value');\n skipped++;\n } else if (required) {\n const errorMsg = getErrorMessage(error);\n log.error(\n { envVar, secretKey, error: errorMsg },\n 'Failed to load required secret'\n );\n failed++;\n throw new Error(`Failed to load required secret ${secretKey}: ${errorMsg}`);\n } else {\n log.warn({ envVar, secretKey, error: getErrorMessage(error) }, 'Failed to load optional secret');\n skipped++;\n }\n }\n }\n\n // Keep compatibility with components that still read GCS_BUCKET_NAME.\n if (!process.env.GCS_BUCKET_NAME && process.env.GCS_FILES_BUCKET) {\n process.env.GCS_BUCKET_NAME = process.env.GCS_FILES_BUCKET;\n }\n\n // Load provider API keys (optional)\n // CRITICAL: If GCP auth is available, ALWAYS load real keys (API Keys NUNCA DEVEM FALHAR)\n // If TEST_USE_REAL_API_KEYS=true, force load from GCP even if mock keys exist\n // Always replace mock keys with real keys from GCP when available\n // (reuses forceLoadRealKeys defined above)\n \n for (const { envVar, secretKeys } of PROVIDER_SECRETS) {\n const existingValue = process.env[envVar];\n const providerName = ENV_VAR_TO_PROVIDER[envVar];\n\n // Check if existing value looks like a mock key\n const isMockKey = existingValue && (existingValue.includes('mock') || existingValue.includes('test-'));\n\n // Skip if real key already exists, UNLESS:\n // - we're in GCP-authoritative mode (operator declared GCP as source of truth), OR\n // - test forces real keys (TEST_USE_REAL_API_KEYS=true), OR\n // - the existing value looks like a mock.\n // The authoritative path lets GCP overwrite stale env_file / shell-exported\n // values, which is the entire point of SECRETS_PROVIDER_PRIMARY=gcp.\n if (existingValue && !authoritativeFetch && !isMockKey) {\n // Record as loaded from env\n if (providerName) {\n providerKeyStatusMap.set(providerName, { loaded: true, source: 'env', timestamp: new Date(), envVar });\n }\n skipped++;\n continue;\n }\n\n try {\n let loadedSecretValue: string | undefined;\n let resolvedSecretKey = secretKeys[0];\n\n for (const candidateKey of secretKeys) {\n try {\n const candidateValue = await loadSecret(candidateKey, false);\n if (candidateValue) {\n loadedSecretValue = candidateValue;\n resolvedSecretKey = candidateKey;\n log.debug({ envVar, secretKey: candidateKey, valueLen: candidateValue.length }, 'Secret loaded from GCP');\n break;\n } else {\n log.debug({ envVar, secretKey: candidateKey }, 'Secret returned empty/null from GCP');\n }\n } catch (loadErr) {\n log.warn({ envVar, secretKey: candidateKey, error: loadErr instanceof Error ? loadErr.message : String(loadErr) }, 'Failed to load secret from GCP');\n continue;\n }\n }\n\n // GCP provider automatically adds prefix 'ailin-' to secretKey\n // So 'openai-key' becomes 'ailin-openai-key' in GCP Secret Manager\n const value = loadedSecretValue;\n if (value) {\n // Sanitize control characters and invisible Unicode.\n // PEM-format keys (-----BEGIN ...) legitimately contain newlines,\n // so use a lighter regex that preserves \\n and \\r for those values.\n const isPemKey = value.startsWith('-----BEGIN');\n let sanitizedValue = isPemKey\n ? value\n .replace(/[\\x00-\\x09\\x0B\\x0C\\x0E-\\x1F\\x7F]/g, '') // Strip control chars EXCEPT \\n (0x0A) and \\r (0x0D)\n .replace(/[\\u200B-\\u200D\\uFEFF]/g, '') // Remove zero-width spaces\n .trim()\n : value\n .replace(/[\\x00-\\x1F\\x7F]/g, '') // Strip ALL control characters\n .replace(/[\\u200B-\\u200D\\uFEFF]/g, '') // Remove zero-width spaces\n .trim();\n \n // Log detailed debugging information if sanitization changed the value\n if (sanitizedValue !== value) {\n const removedChars = value.length - sanitizedValue.length;\n sanitizedKeys++;\n log.debug(\n { \n envVar, \n secretKey: resolvedSecretKey,\n originalLength: value.length, \n sanitizedLength: sanitizedValue.length,\n removedChars\n },\n 'API key contained invalid characters and was sanitized'\n );\n }\n \n // Always replace mock keys with real keys\n const wasReplaced = !!existingValue;\n const wasOverwriteOfRealValue =\n wasReplaced && !isMockKey && existingValue !== sanitizedValue;\n\n // CRITICAL: Set process.env and immediately validate\n // Use Object.defineProperty as fallback if direct assignment doesn't work\n // This ensures the value is actually set in the environment\n const _previousValue = process.env[envVar];\n process.env[envVar] = sanitizedValue;\n\n // Immediately read back to verify (handles any potential async issues)\n const readBackValue = process.env[envVar];\n\n // Deep validation: check exact match, type, and length\n const isValidMatch = readBackValue === sanitizedValue;\n const isValidType = typeof readBackValue === 'string';\n const isValidLength = readBackValue !== undefined && readBackValue.length === sanitizedValue.length;\n\n if (!isValidMatch || !isValidType || !isValidLength) {\n log.error(\n {\n envVar,\n secretKey: resolvedSecretKey,\n sanitizedValueLength: sanitizedValue.length,\n readBackValueLength: readBackValue?.length,\n },\n 'Secret was loaded but process.env assignment did not persist'\n );\n failed++;\n } else {\n // Success - log and increment counter\n if (wasOverwriteOfRealValue) overwrittenFromGcp++;\n log.debug(\n {\n envVar,\n secretKey: resolvedSecretKey,\n replaced: wasReplaced,\n wasMock: isMockKey,\n overwrittenFromGcp: wasOverwriteOfRealValue,\n },\n 'Provider secret loaded from GCP'\n );\n loaded++;\n providerSecretsLoaded++;\n // Record successful load\n if (providerName) {\n providerKeyStatusMap.set(providerName, { loaded: true, source: 'gcp', timestamp: new Date(), envVar });\n }\n }\n } else {\n // GCP returned no value. Decide what's actually in process.env right\n // now: if env had a non-mock value before this loop iteration we\n // preserved it (the authoritative path entered this block but found\n // no GCP value to overwrite with), so we're effectively env-loaded.\n // If env had a mock value, we keep the mock as a degraded fallback\n // and warn loudly. If env had nothing, the provider stays orphaned.\n if (existingValue && !isMockKey) {\n // Authoritative-mode GCP miss with real env value already set:\n // the env value is still in process.env, so report source=env.\n if (providerName) {\n providerKeyStatusMap.set(providerName, { loaded: true, source: 'env', timestamp: new Date(), envVar });\n }\n log.debug(\n { envVar, secretKeys },\n 'GCP miss in authoritative mode; preserving existing env value for provider'\n );\n } else {\n if (isMockKey) {\n log.warn(\n { envVar, secretKeys },\n 'Mock API key detected but no real key found in GCP - using mock key (this may cause API failures)'\n );\n }\n // Record as not loaded (no GCP value, no usable env value)\n if (providerName) {\n providerKeyStatusMap.set(providerName, { loaded: false, source: 'none', timestamp: new Date(), envVar });\n }\n }\n skipped++;\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n // Only warn if not forcing real keys - if forcing, this is expected to work\n if (forceLoadRealKeys) {\n log.error(\n { envVar, secretKeys, error: errorMessage },\n 'Failed to load provider secret from GCP (TEST_USE_REAL_API_KEYS=true)'\n );\n failed++;\n } else {\n if (isMockKey) {\n log.warn(\n { envVar, secretKeys, error: errorMessage },\n 'Failed to load provider secret from GCP - will use mock key (this may cause API failures)'\n );\n } else {\n log.debug(\n { envVar, secretKeys, error: errorMessage },\n 'Failed to load provider secret from GCP (optional)'\n );\n }\n // Record failure\n if (providerName) {\n providerKeyStatusMap.set(providerName, { loaded: false, source: 'none', timestamp: new Date(), envVar });\n }\n skipped++;\n }\n }\n }\n\n // ── Env-var aliases (2026-04-28) ────────────────────────────────────────\n //\n // Some operators set a non-canonical name in `.env` for historical reasons\n // (e.g. `REKAAI_API_KEY` instead of the catalog-canonical `REKA_API_KEY`).\n // When GCP is reachable the secret loader populates the canonical name and\n // it just works; when GCP is unreachable the alias would be the only thing\n // set and the catalog row reads the canonical → empty key → silent skip.\n //\n // This pass copies known aliases into their canonical env var if the\n // canonical is empty. It is purely additive — never overwrites an\n // existing canonical value, never logs a warning unless an alias was\n // applied. Add new entries here when a similar mismatch is discovered.\n const ENV_ALIASES: ReadonlyArray<{ canonical: string; alias: string }> = [\n { canonical: 'REKA_API_KEY', alias: 'REKAAI_API_KEY' },\n ];\n for (const { canonical, alias } of ENV_ALIASES) {\n const canonicalValue = process.env[canonical];\n const aliasValue = process.env[alias];\n if ((canonicalValue == null || canonicalValue.trim() === '') && aliasValue && aliasValue.trim() !== '') {\n process.env[canonical] = aliasValue;\n log.info({ canonical, alias }, 'Promoted alias env var to canonical name');\n }\n }\n\n const gcpPrimary = (process.env.SECRETS_PROVIDER_PRIMARY || '').trim().toLowerCase() === 'gcp';\n const failFastEnv = (process.env.SECRETS_GCP_FAIL_FAST || '').trim().toLowerCase();\n const gcpFailFast = failFastEnv === 'true' || (failFastEnv !== 'false' && gcpPrimary);\n const hasAnyLlmProviderKey = LLM_PROVIDER_ENV_VARS.some((envVar) => {\n const value = process.env[envVar];\n return typeof value === 'string' && value.trim().length > 0;\n });\n\n // Hardening (P1.10 at boot scope): even when zero external API keys are\n // loaded, the server can still boot in DEGRADED mode if a self-hosted\n // endpoint is reachable. Every request will then be tagged via the\n // last-resort-policy as execution_mode=last_resort_self_hosted.\n const availableSelfHostedEndpoints = SELF_HOSTED_ENV_VARS.filter((envVar) => {\n const value = process.env[envVar];\n return typeof value === 'string' && value.trim().length > 0;\n });\n const hasAnySelfHostedFallback = availableSelfHostedEndpoints.length > 0;\n\n // Opt-out for strict production deployments that must never run in\n // degraded mode. Default: allow degraded boot (safer than crash loops).\n const allowDegradedBoot = (process.env.ALLOW_DEGRADED_BOOT || 'true').trim().toLowerCase() !== 'false';\n\n if (gcpFailFast && !hasAnyLlmProviderKey) {\n if (hasAnySelfHostedFallback && allowDegradedBoot) {\n log.warn(\n {\n gcpPrimary,\n gcpFailFast,\n providerSecretsLoaded,\n selfHostedEndpoints: availableSelfHostedEndpoints,\n mode: 'degraded_self_hosted',\n docs: 'docs/hardening/last-resort-policy',\n },\n '⚠️ DEGRADED BOOT: zero external LLM credentials, but self-hosted fallback is available. ' +\n 'Server will start in last-resort mode. Requests will be tagged excluded_from_benchmark. ' +\n 'Fix ADC/GCP credentials to restore full operation.'\n );\n // Intentionally fall through — boot continues\n } else {\n const message = hasAnySelfHostedFallback\n ? 'SECRETS_PROVIDER_PRIMARY=gcp, no LLM provider credentials loaded, and ALLOW_DEGRADED_BOOT=false.'\n : 'SECRETS_PROVIDER_PRIMARY=gcp but no LLM provider credentials are available after secrets bootstrap, ' +\n 'and no self-hosted fallback endpoints (OLLAMA_URL, SELF_HOSTED_LLM_URL, LOCAL_LLAMA_URL, LOCAL_KOBOLD_URL) are configured.';\n log.error(\n {\n gcpPrimary,\n gcpFailFast,\n providerSecretsLoaded,\n checkedEnvVars: LLM_PROVIDER_ENV_VARS,\n selfHostedEndpointsChecked: SELF_HOSTED_ENV_VARS,\n allowDegradedBoot,\n },\n message\n );\n throw new Error(\n `${message} Verify GCP Secret Manager access (ADC/Workload Identity) and secret names/prefix, ` +\n `or set OLLAMA_URL for degraded self-hosted boot, or run 'gcloud auth application-default login' if local.`\n );\n }\n }\n\n // Warn on partial provider key loading — helps detect GCP issues early\n if (hasAnyLlmProviderKey && providerSecretsLoaded > 0 && providerSecretsLoaded < 3) {\n log.warn(\n { providerSecretsLoaded, total: PROVIDER_SECRETS.length },\n `⚠️ Only ${providerSecretsLoaded} provider secrets loaded — possible partial GCP failure`\n );\n }\n\n // Audio provider diagnostics\n const audioProviders = {\n DEEPGRAM_API_KEY: !!process.env.DEEPGRAM_API_KEY,\n CARTESIA_API_KEY: !!process.env.CARTESIA_API_KEY,\n ELEVENLABS_API_KEY: !!process.env.ELEVENLABS_API_KEY,\n OLLAMA_URL: process.env.OLLAMA_URL || '',\n SELF_HOSTED_STT_URL: process.env.SELF_HOSTED_STT_URL || '',\n LOCAL_NLLB_URL: process.env.LOCAL_NLLB_URL || '',\n };\n log.info({ audioProviders }, 'Audio provider env vars after secrets loading');\n\n log.info(\n {\n loaded,\n skipped,\n failed,\n sanitizedKeys,\n providerSecretsLoaded,\n overwrittenFromGcp,\n gcpAuthoritative,\n total: CRITICAL_SECRETS.length + PROVIDER_SECRETS.length,\n },\n '✅ Secrets loading complete'\n );\n\n if (failed > 0) {\n throw new Error(`Failed to load ${failed} required secrets`);\n }\n}\n\n/**\n * Get summary of secrets loaded.\n *\n * Diagnostic helper for admin endpoints (e.g. /v1/admin/providers/...) that\n * surfaces exactly which provider envVars are populated and from where, so\n * operators can verify SECRETS_PROVIDER_PRIMARY=gcp is actually authoritative\n * and not silently shadowed by stale env values.\n */\nexport function getSecretsLoadSummary(): {\n fromGCP: string[];\n fromEnv: string[];\n notLoaded: string[];\n} {\n const fromGCP: string[] = [];\n const fromEnv: string[] = [];\n const notLoaded: string[] = [];\n\n for (const status of providerKeyStatusMap.values()) {\n if (!status.loaded) {\n notLoaded.push(status.envVar);\n continue;\n }\n if (status.source === 'gcp') {\n fromGCP.push(status.envVar);\n } else if (status.source === 'env') {\n fromEnv.push(status.envVar);\n }\n }\n\n return { fromGCP, fromEnv, notLoaded };\n}\n"}, "70e542f201c7de16aef8b6f54ad3a419a4943015": {"t": 1785754631.8858967, "n": 61698, "h": "19aa74e19369ee40", "c": "// Copyright (C) 2026 Ailin One, Inc.\n//\n// This file is part of Collective Intelligence Engine (ci).\n// Licensed under the GNU Affero General Public License v3.0 or later.\n// See LICENSE in the repository root, or <https://www.gnu.org/licenses/>.\n//\n// SPDX-License-Identifier: AGPL-3.0-or-later\n// Source: https://github.com/ailinone/collective-intelligence\n\n/**\n * Load Secrets into Environment\n * \n * This module loads critical secrets from GCP Secret Manager\n * and injects them into process.env BEFORE config validation.\n * \n * This allows the existing config system to work without refactoring,\n * while still using GCP Secret Manager as the source of truth.\n * \n * Call order:\n * 1. initializeSecretsManager()\n * 2. loadSecretsIntoEnv() ← This file\n * 3. validateConfig()\n */\n\nimport { logger } from '@/utils/logger';\nimport { getErrorMessage } from '@/utils/type-guards';\nimport { loadSecret } from './secrets-loader';\n\n/**\n * Critical secrets that must be loaded from GCP\n * These are injected into process.env for config compatibility\n */\ninterface CriticalSecret {\n envVar: string;\n secretKey: string;\n required: boolean;\n defaultValue?: string;\n}\n\nconst CRITICAL_SECRETS: CriticalSecret[] = [\n { envVar: 'DATABASE_URL', secretKey: 'database-url', required: true },\n { envVar: 'JWT_SECRET', secretKey: 'jwt-secret', required: true },\n { envVar: 'REDIS_PASSWORD', secretKey: 'redis-password', required: false, defaultValue: '' },\n { envVar: 'GCS_FILES_BUCKET', secretKey: 'gcs-files-bucket', required: false },\n { envVar: 'SMTP_PASS', secretKey: 'smtp-pass', required: false },\n { envVar: 'STRIPE_SECRET_KEY', secretKey: 'stripe-secret-key', required: false },\n { envVar: 'STRIPE_WEBHOOK_SECRET', secretKey: 'stripe-webhook-secret', required: false },\n { envVar: 'STRIPE_PUBLISHABLE_KEY', secretKey: 'stripe-publishable-key', required: false },\n // Phase 2c shadow ensemble bearer token. Optional because the coord-\n // stable wire is disabled by default in production\n // (CI_ENSEMBLE_COORDINATOR_ENABLED=false). When operator flips it on,\n // the GCP secret `coord-ensemble-token` MUST also be set with the\n // value to share with coord-serving's COORD_SERVING_AUTH_TOKEN.\n { envVar: 'CI_ENSEMBLE_COORDINATOR_TOKEN', secretKey: 'coord-ensemble-token', required: false },\n];\n\n/**\n * Provider API keys (loaded from GCP if not in env)\n * CRITICAL: GCP provider automatically adds prefix 'ailin-' to secret keys\n * So 'openai-key' becomes 'ailin-openai-key' in GCP Secret Manager\n * These keys match the actual secret names in GCP (without the prefix, provider adds it)\n */\ninterface ProviderSecretBinding {\n envVar: string;\n secretKeys: readonly string[];\n}\n\nexport const PROVIDER_SECRETS: readonly ProviderSecretBinding[] = [\n { envVar: 'OPENAI_API_KEY', secretKeys: ['openai-key'] },\n { envVar: 'ANTHROPIC_API_KEY', secretKeys: ['anthropic-key'] },\n { envVar: 'GOOGLE_API_KEY', secretKeys: ['google-key'] },\n { envVar: 'DEEPSEEK_API_KEY', secretKeys: ['deepseek-key'] },\n { envVar: 'XAI_API_KEY', secretKeys: ['xai-key'] },\n { envVar: 'MISTRAL_API_KEY', secretKeys: ['mistral-key'] },\n { envVar: 'COHERE_API_KEY', secretKeys: ['cohere-key'] },\n { envVar: 'NVIDIA_API_KEY', secretKeys: ['nvidia-key', 'nvidia-api-key'] },\n { envVar: 'AIHUBMIX_API_KEY', secretKeys: ['aihubmix-key', 'aihubmix-api-key'] },\n { envVar: 'NOVITA_API_KEY', secretKeys: ['novita-key', 'novita-api-key'] },\n { envVar: 'MOONSHOT_API_KEY', secretKeys: ['moonshot-key', 'moonshot-api-key'] },\n { envVar: 'MINIMAX_API_KEY', secretKeys: ['minimax-key', 'minimax-api-key'] },\n { envVar: 'JINA_API_KEY', secretKeys: ['jina-key', 'jina-api-key'] },\n { envVar: 'GOOGLE_MAPS_API_KEY', secretKeys: ['google-maps-key', 'google-maps-api-key'] },\n { envVar: 'FRIENDLI_API_KEY', secretKeys: ['friendli-key', 'friendli-api-key'] },\n { envVar: 'FRIENDLI_TEAM_ID', secretKeys: ['friendli-team-id'] },\n { envVar: 'AIML_API_KEY', secretKeys: ['aiml-key', 'aiml-api-key'] },\n { envVar: 'IMAGEROUTER_API_KEY', secretKeys: ['imagerouter-key', 'imagerouter-api-key'] },\n { envVar: 'OPENROUTER_API_KEY', secretKeys: ['openrouter-key'] },\n { envVar: 'COMETAPI_API_KEY', secretKeys: ['cometapi-key', 'cometapi-api-key'] },\n { envVar: 'NANOGPT_API_KEY', secretKeys: ['nanogpt-key', 'nanogpt-api-key'] },\n { envVar: 'REQUESTY_API_KEY', secretKeys: ['requesty-key', 'requesty-api-key'] },\n { envVar: 'AI302_API_KEY', secretKeys: ['302-key', '302ai-key'] },\n { envVar: 'POE_API_KEY', secretKeys: ['poe-key', 'poe-api-key'] },\n { envVar: 'ROUTEWAY_API_KEY', secretKeys: ['routeway-key', 'routeway-api-key'] },\n // Hub providers support both key naming conventions for compatibility.\n { envVar: 'ORQAI_API_KEY', secretKeys: ['orqai-key', 'orqai-api-key'] },\n { envVar: 'ORQAI_MODEL_DENYLIST', secretKeys: ['orqai-model-denylist'] },\n { envVar: 'EDENAI_API_KEY', secretKeys: ['edenai-key', 'edenai-api-key'] },\n { envVar: 'HELICONEAI_API_KEY', secretKeys: ['heliconeai-key', 'heliconeai-api-key'] },\n { envVar: 'VERTEX_AI_API_KEY', secretKeys: ['vertex-key'] },\n { envVar: 'VERTEX_AI_PROJECT_ID', secretKeys: ['vertex-project-id'] },\n { envVar: 'QWEN_API_KEY', secretKeys: ['qwen-key-2'] },\n { envVar: 'ALIBABA_KEY_ID', secretKeys: ['alibaba-key-id'] },\n { envVar: 'ALIBABA_KEY_SECRET', secretKeys: ['alibaba-key-secret'] },\n { envVar: 'ERNIE_API_KEY', secretKeys: ['baidu-key'] },\n { envVar: 'ERNIE_SECRET_KEY', secretKeys: ['baidu-secret'] },\n { envVar: 'BAIDU_BASE_URL', secretKeys: ['baidu-base-url'] },\n { envVar: 'AWS_ACCESS_KEY_ID', secretKeys: ['aws-key-id'] },\n { envVar: 'AWS_SECRET_ACCESS_KEY', secretKeys: ['aws-secret'] },\n { envVar: 'AWS_BEARER_TOKEN_BEDROCK', secretKeys: ['aws-bearer-token'] },\n // Batch 7.1: Bedrock region + inference-profile ARN can now be set via\n // GCP Secret Manager. Useful for deploys that route Bedrock to a specific\n // region (e.g. us-west-2) or via cross-region inference profiles without\n // baking these into the docker image.\n { envVar: 'AWS_BEDROCK_REGION', secretKeys: ['aws-bedrock-region'] },\n { envVar: 'AWS_BEDROCK_INFERENCE_PROFILE_ARN', secretKeys: ['aws-bedrock-inference-profile-arn'] },\n // Batch 8.1: SageMaker endpoint routing. A deploy can point this adapter\n // at a specific deployed endpoint via a secret rather than baking the\n // endpoint name into the image. The region override lets the adapter\n // hit an endpoint in a different region from the general AWS_REGION\n // used by other services (SES, S3).\n { envVar: 'AWS_SAGEMAKER_REGION', secretKeys: ['aws-sagemaker-region'] },\n { envVar: 'AWS_SAGEMAKER_ENDPOINT_NAME', secretKeys: ['aws-sagemaker-endpoint-name'] },\n { envVar: 'AWS_SAGEMAKER_PAYLOAD_SCHEMA', secretKeys: ['aws-sagemaker-payload-schema'] },\n { envVar: 'AWS_SAGEMAKER_CUSTOM_ATTRIBUTES', secretKeys: ['aws-sagemaker-custom-attributes'] },\n { envVar: 'AZURE_OPENAI_API_KEY', secretKeys: ['azure-openai-api-key'] },\n { envVar: 'AZURE_OPENAI_ENDPOINT', secretKeys: ['azure-openai-endpoint'] },\n { envVar: 'AZURE_OPENAI_DEPLOYMENT', secretKeys: ['azure-openai-deployment'] },\n // Audio-first providers (STT, TTS, STS)\n { envVar: 'DEEPGRAM_API_KEY', secretKeys: ['deepgram-key'] },\n { envVar: 'CARTESIA_API_KEY', secretKeys: ['cartesia-key'] },\n { envVar: 'ELEVENLABS_API_KEY', secretKeys: ['elevenlabs-key'] },\n // Translation provider\n { envVar: 'PALABRAAI_CLIENT_ID', secretKeys: ['palabraai-id'] },\n { envVar: 'PALABRAAI_CLIENT_SECRET', secretKeys: ['palabraai-key'] },\n { envVar: 'OCI_TENANCY_ID', secretKeys: ['oci-tenancy-id'] },\n { envVar: 'OCI_USER_ID', secretKeys: ['oci-user-id'] },\n { envVar: 'OCI_FINGERPRINT', secretKeys: ['oci-fingerprint'] },\n { envVar: 'OCI_PRIVATE_KEY', secretKeys: ['oci-private-key'] },\n { envVar: 'OCI_REGION', secretKeys: ['oci-region'] },\n // 2026-04-22: New GCP secrets (5 providers). The GCP provider auto-prefixes\n // 'ailin-', so 'wandb-key' maps to ailin-wandb-key in Secret Manager.\n // Voyage lists both 'voyageai-key' (current upstream name) and 'voyage-key'\n // (conventional fallback) because the canonical spelling is still unstable.\n { envVar: 'WANDB_API_KEY', secretKeys: ['wandb-key', 'wandb-api-key'] },\n { envVar: 'VOYAGE_API_KEY', secretKeys: ['voyageai-key', 'voyage-key', 'voyage-api-key'] },\n { envVar: 'BYTEZ_API_KEY', secretKeys: ['bytez-key', 'bytez-api-key'] },\n { envVar: 'CEREBRAS_API_KEY', secretKeys: ['cerebras-key', 'cerebras-api-key'] },\n { envVar: 'FEATHERLESS_AI_API_KEY', secretKeys: ['featherless-key', 'featherless-api-key'] },\n // Batch 6 — enterprise hyperscaler gateways (2026-04-22).\n // Azure OpenAI already has API_KEY/ENDPOINT/DEPLOYMENT wired above;\n // these fill in the remaining URL-template parameters.\n { envVar: 'AZURE_OPENAI_RESOURCE_NAME', secretKeys: ['azure-openai-resource-name', 'azure-openai-resource'] },\n { envVar: 'AZURE_OPENAI_API_VERSION', secretKeys: ['azure-openai-api-version'] },\n // Gemini OAI-compat shares the API key with the native `google` provider.\n // Two env-var aliases: GEMINI_API_KEY (canonical upstream) and\n // GOOGLE_AI_STUDIO_API_KEY (legacy). The adapter reads either.\n //\n // 2026-04-23 final pass: `vertex-key` added as a fallback source because\n // it was verified working against generativelanguage.googleapis.com this\n // session (HTTP 200, 50 Gemini models) while `google-key` is suspended\n // (403 CONSUMER_SUSPENDED). The Vertex/Gemini API-key split is\n // mostly historical — a Google Cloud API key scoped for Gemini works\n // equally on aistudio and on the generativelanguage surface, as long as\n // the key's GCP project has the API enabled. Putting `vertex-key` at\n // the END of the fallback preserves existing behavior (gemini-key /\n // google-ai-studio-key / google-key are still preferred when present)\n // while letting the vertex-key unlock gemini-openai today.\n { envVar: 'GEMINI_API_KEY', secretKeys: ['google-key', 'gemini-key', 'google-ai-studio-key', 'vertex-key'] },\n // GitHub Models uses a GitHub PAT. Operators typically already have this\n // in env (it's the same PAT used for gh CLI); the GCP path is a fallback\n // for service accounts that need isolated tokens.\n { envVar: 'GITHUB_TOKEN', secretKeys: ['github-models-token', 'github-token', 'github-pat'] },\n // Databricks workspace token + config.\n { envVar: 'DATABRICKS_TOKEN', secretKeys: ['databricks-token', 'databricks-pat'] },\n { envVar: 'DATABRICKS_HOST', secretKeys: ['databricks-host'] },\n { envVar: 'DATABRICKS_SERVING_ENDPOINT', secretKeys: ['databricks-serving-endpoint'] },\n // Inworld AI — orphan closure 2026-04-22. Key is already base64-encoded\n // in GCP (132 chars) because Inworld uses Basic auth, not Bearer. The\n // adapter does NOT re-encode; it passes the secret verbatim into the\n // `Basic ` header. Never base64-encode this value at load time.\n { envVar: 'INWORLD_API_KEY', secretKeys: ['inworld-key', 'inworld-api-key'] },\n // ── Orphan-closure Lot B (2026-04-23) ──────────────────────────────────\n // Three providers closed in a single lot because they share identical\n // shape (thin OAI-compat wrappers) and were all already present in GCP\n // Secret Manager but not wired into env loading. Each tuple lists the\n // preferred secret key first and legacy-or-alternate names as fallbacks.\n { envVar: 'WRITER_API_KEY', secretKeys: ['writer-key', 'writer-api-key'] },\n { envVar: 'UPSTAGE_API_KEY', secretKeys: ['upstage-key', 'upstage-api-key'] },\n // Reka's canonical upstream name is \"reka\" but the key is stored in GCP\n // as `ailin-rekaai-key` (matching the catalog providerId, not the upstream\n // name). `reka-key` is kept as a fallback in case the secret is ever\n // renamed to match upstream naming later.\n { envVar: 'REKA_API_KEY', secretKeys: ['rekaai-key', 'reka-key', 'reka-api-key'] },\n // ── LOTE M (2026-04-23) — complement lot: 13 new env-var mappings. ─────\n // Every provider below landed a catalog row this lot and is classified\n // in CONSOLIDATION_MATRIX['credentials-missing'] (secret-absent sub-class\n // for 12, auth-incomplete for qianfan). Each mapping is wired NOW so the\n // moment an operator provisions the corresponding GCP secret, the key\n // propagates to process.env without a code change. The first element of\n // each secretKeys array is the preferred upstream-convention name; any\n // follow-up entries are fallbacks for historical or legacy aliases.\n { envVar: 'ARCEE_API_KEY', secretKeys: ['arcee-api-key', 'arcee-key'] },\n { envVar: 'ATLASCLOUD_API_KEY', secretKeys: ['atlascloud-key', 'atlascloud-api-key', 'atlas-cloud-key'] },\n { envVar: 'AVIAN_API_KEY', secretKeys: ['avian-key', 'avian-api-key', 'avian-io-key'] },\n // Qianfan (Baidu) v2 bce-v3 bearer. Distinct from the legacy v1 AK+SK\n // material in ERNIE_API_KEY / ERNIE_SECRET_KEY / BAIDU_BASE_URL above —\n // both mappings are intentionally kept so an operator can populate either\n // path (v1 OAuth or v2 bearer) depending on which runtime surface is\n // used. The canonical catalog row `qianfan` reads QIANFAN_API_KEY only.\n { envVar: 'QIANFAN_API_KEY', secretKeys: ['qianfan-key', 'qianfan-api-key', 'baidu-qianfan-key'] },\n { envVar: 'GMI_API_KEY', secretKeys: ['gmi-key', 'gmi-api-key', 'gmicloud-key'] },\n { envVar: 'INFERMATIC_API_KEY', secretKeys: ['infermatic-api-key', 'infermatic-key', 'totalgpt-key'] },\n { envVar: 'INFLECTION_API_KEY', secretKeys: ['inflection-key', 'inflection-api-key', 'inflection-ai-key'] },\n // Mancer (tagged contentPolicyClass=uncensored in catalog; per the universal\n // \"habilitado e nunca censurado\" policy from Phase 4b, 2026-04-28, the\n // catalog row is fully admitted to routing — the tag is informational so\n // downstream surfaces can opt to filter it from a moderated default list).\n { envVar: 'MANCER_API_KEY', secretKeys: ['mancer-key', 'mancer-api-key'] },\n // Phala / RedPill TEE — upstream docs use REDPILL_API_KEY but we wire\n // PHALA_API_KEY as the canonical env var (see apiKeyEnvVarOverrideReason\n // in catalog row `phala`). Legacy `redpill-*` secret names are accepted\n // as fallbacks so an operator storing under either convention works.\n { envVar: 'PHALA_API_KEY', secretKeys: ['phala-key', 'phala-api-key', 'redpill-key', 'redpill-api-key'] },\n { envVar: 'RELACE_API_KEY', secretKeys: ['relace-key', 'relace-api-key'] },\n { envVar: 'SILICONFLOW_API_KEY', secretKeys: ['siliconflow-api-key', 'siliconflow-key', 'silicon-flow-key'] },\n { envVar: 'STEPFUN_API_KEY', secretKeys: ['stepfun-api-key', 'stepfun-key', 'step-key'] },\n // Venice (tagged contentPolicyClass=uncensored in catalog). Same Phase-4b\n // treatment as Mancer — admitted, never censored, with the tag as the\n // audit trail for the policy choice.\n { envVar: 'VENICE_API_KEY', secretKeys: ['venice-key', 'venice-api-key', 'venice-ai-key'] },\n // ── Sublote E1 (2026-04-24) — runtime-wiring closure for D1 matrix ─────\n // Sublote D1 promoted 10 providers to CONSOLIDATION_MATRIX['live-validation']\n // based on direct-probe evidence (via gcloud secrets versions access,\n // bypassing the runtime env loader). 7 of those had no GCP→ENV tuple here,\n // so the catalog-loader could never surface `process.env.<X>_API_KEY` at\n // boot — meaning live-validated providers were silently \"not runnable\".\n // Wired pre-emptively: the moment an operator provisions the GCP secret\n // AND the container restarts, propagation happens with zero code change.\n // The 3 already-mapped providers (heliconeai, github-models, infermatic)\n // are NOT duplicated — they appear above under their respective batches.\n { envVar: 'GROQ_API_KEY', secretKeys: ['groq-api-key', 'groq-key'] },\n { envVar: 'DEEPINFRA_API_KEY', secretKeys: ['deepinfra-api-key', 'deepinfra-key'] },\n // Hugging Face: upstream canonical env var is `HF_TOKEN` (not\n // HUGGINGFACE_API_KEY); the catalog row's apiKeyEnvVar matches. Primary\n // GCP secret name follows the upstream convention; legacy huggingface-*\n // aliases accepted for operators who stored it under the provider slug.\n { envVar: 'HF_TOKEN', secretKeys: ['huggingface-api-key', 'huggingface-token', 'hf-token', 'huggingface-key'] },\n // Cloudflare Workers AI — requires BOTH api-token AND account-id. The\n // CloudflareWorkersAIAdapter factory (default-adapter-factories.ts line\n // 229-246) reads CLOUDFLARE_ACCOUNT_ID from env and substitutes it into\n // the `{account_id}` URL template. Without the account-id side-car the\n // adapter ships the literal template to the wire → 404 on every call.\n // Side-car is wired here as a separate tuple because it's not an auth\n // credential — it's a URL parameter — so it doesn't belong in\n // ENV_VAR_TO_PROVIDER or LLM_PROVIDER_ENV_VARS below.\n { envVar: 'CLOUDFLARE_API_TOKEN', secretKeys: ['cloudflare-workers-ai-api-key', 'cloudflare-workers-ai-token', 'cloudflare-api-token', 'cloudflare-workers-ai-key'] },\n { envVar: 'CLOUDFLARE_ACCOUNT_ID', secretKeys: ['cloudflare-workers-ai-id', 'cloudflare-account-id', 'cloudflare-workers-ai-account-id'] },\n { envVar: 'PERPLEXITY_API_KEY', secretKeys: ['perplexity-api-key', 'perplexity-key'] },\n // Fireworks AI — note the catalog providerId is `fireworks-ai` (hyphenated)\n // but upstream brand/docs spell it \"Fireworks\" (no suffix). Four fallbacks\n // cover both conventions + api-key suffix variant.\n { envVar: 'FIREWORKS_AI_API_KEY', secretKeys: ['fireworks-ai-api-key', 'fireworks-ai-key', 'fireworks-key', 'fireworks-api-key'] },\n { envVar: 'SAMBANOVA_API_KEY', secretKeys: ['sambanova-api-key', 'sambanova-key'] },\n // Replicate — flagged by the sublote-e1-runtime-wiring invariant as a\n // pre-existing gap. `replicate` has been in CONSOLIDATION_MATRIX\n // ['live-validation'] since before D1, with adapterClass 'ReplicateAdapter'\n // and apiKeyEnvVar 'REPLICATE_API_KEY', but no tuple was ever added here.\n // Even though integrationMode is 'execution-only' (no /v1/models\n // discovery), the adapter still requires the env var at /predictions\n // call time. Wired here for closure; the GCP secret name follows the\n // provider-slug convention with optional `-api-key` fallback.\n { envVar: 'REPLICATE_API_KEY', secretKeys: ['replicate-key', 'replicate-api-key'] },\n // ── LOTE N (2026-04-27) — catalog↔loader closure for the remaining 22 ─\n // apiKeyEnvVar entries that had catalog rows but NO PROVIDER_SECRETS\n // tuple. Surfaced by `catalog-loader-wiring-completeness.test.ts` (J6\n // invariant). All 22 are real provider rows with adapter classes (or\n // generic OAI-compat hub registration); they were classified in\n // CONSOLIDATION_MATRIX as `credentials-missing` or `disabled-by-default`,\n // and their absence here meant that even after an operator provisioned\n // the GCP secret, `process.env.<X>_API_KEY` would stay empty at boot.\n //\n // Pattern: preferred secret name first (provider-slug `-key`), then\n // upstream-convention `-api-key` fallback, then any historical aliases.\n // The provider-side `aliases` array in providers.catalog.ts is NOT\n // duplicated here — secret-key aliases are about HOW the operator\n // stored the credential, NOT how callers reach the provider.\n //\n // Image/video-only providers (bfl, recraft, runwayml, topaz) are wired\n // here too but are intentionally absent from LLM_PROVIDER_ENV_VARS below\n // because their presence does NOT count toward the \"at least one LLM\n // key present\" boot-mode gate.\n { envVar: 'TOGETHERAI_API_KEY', secretKeys: ['togetherai-api-key', 'togetherai-key', 'together-key', 'together-api-key'] },\n { envVar: 'ANYSCALE_API_KEY', secretKeys: ['anyscale-api-key', 'anyscale-key'] },\n { envVar: 'HYPERBOLIC_API_KEY', secretKeys: ['hyperbolic-api-key', 'hyperbolic-key'] },\n { envVar: 'CHUTES_API_KEY', secretKeys: ['chutes-api-key', 'chutes-key', 'chutes-ai-key'] },\n { envVar: 'NSCALE_API_KEY', secretKeys: ['nscale-key', 'nscale-api-key'] },\n { envVar: 'NEBIUS_API_KEY', secretKeys: ['nebius-key', 'nebius-api-key'] },\n // Lambda Cloud canonical provider slug is `lambda-ai`; brand is\n // \"Lambda\" (without -ai). Both spellings tried as fallbacks.\n { envVar: 'LAMBDA_AI_API_KEY', secretKeys: ['lambda-ai-key', 'lambda-key', 'lambda-cloud-key', 'lambda-ai-api-key'] },\n { envVar: 'SCALEWAY_API_KEY', secretKeys: ['scaleway-key', 'scaleway-api-key'] },\n { envVar: 'SYNTHETIC_API_KEY', secretKeys: ['synthetic-key', 'synthetic-api-key', 'synthetic-ai-key'] },\n { envVar: 'MORPH_API_KEY', secretKeys: ['morph-key', 'morph-api-key', 'morph-llm-key'] },\n // Z.ai (catalog providerId = `zai`). Upstream domain z.ai resolves to\n // bigmodel.cn — the GLM family operator. Legacy spelling z-ai accepted.\n { envVar: 'ZAI_API_KEY', secretKeys: ['zai-key', 'zai-api-key', 'z-ai-key', 'glm-key'] },\n { envVar: 'XIAOMI_MIMO_API_KEY', secretKeys: ['xiaomi-mimo-key', 'mimo-key', 'xiaomi-mimo-api-key'] },\n // v0 (Vercel) — code-gen LLM. The catalog providerId is `v0` (one\n // character) so the env var name is `V0_API_KEY` not `V0AI_API_KEY`.\n { envVar: 'V0_API_KEY', secretKeys: ['v0-key', 'v0-api-key', 'vercel-v0-key'] },\n // Vercel AI Gateway — DIFFERENT from v0 above (gateway over many LLMs).\n // The canonical secret name follows the catalog providerId.\n { envVar: 'VERCEL_AI_GATEWAY_API_KEY', secretKeys: ['vercel-ai-gateway-key', 'vercel-ai-gateway-api-key', 'vercel-gateway-key'] },\n // Volcano (Volcengine — ByteDance). Canonical slug `volcano`; brand\n // includes Volcengine + Doubao + Ark. Multiple aliases accepted.\n { envVar: 'VOLCANO_API_KEY', secretKeys: ['volcano-key', 'volcano-api-key', 'volcengine-key', 'doubao-key', 'ark-key'] },\n // IBM watsonx — note the env var is `WATSONX_APIKEY` (no underscore\n // between API and KEY) to match IBM's canonical SDK convention.\n // Side-car env vars (WATSONX_PROJECT_ID, WATSONX_URL) are wired below\n // because they're documented in the catalog row's `extraEnvVars` and\n // adapter requires them at runtime.\n { envVar: 'WATSONX_APIKEY', secretKeys: ['watsonx-apikey', 'watsonx-key', 'watsonx-api-key', 'ibm-watsonx-key'] },\n { envVar: 'WATSONX_PROJECT_ID', secretKeys: ['watsonx-project-id', 'ibm-watsonx-project-id'] },\n { envVar: 'WATSONX_URL', secretKeys: ['watsonx-url', 'ibm-watsonx-url'] },\n // Snowflake Cortex — uses Personal Access Token (PAT), not an API key.\n // Multiple side-cars required by the SnowflakeCortexAdapter:\n // SNOWFLAKE_ACCOUNT (orgname-accountname), SNOWFLAKE_USER (key-pair\n // auth user), SNOWFLAKE_BASE_URL (account-specific host). Side-cars\n // wired here so a single GCP provisioning round materializes the full\n // tuple at boot.\n { envVar: 'SNOWFLAKE_PAT', secretKeys: ['snowflake-pat', 'snowflake-token', 'snowflake-key'] },\n { envVar: 'SNOWFLAKE_ACCOUNT', secretKeys: ['snowflake-account'] },\n { envVar: 'SNOWFLAKE_USER', secretKeys: ['snowflake-user', 'snowflake-username'] },\n { envVar: 'SNOWFLAKE_BASE_URL', secretKeys: ['snowflake-base-url', 'snowflake-url'] },\n // SAP AI Core — OAuth2 client_credentials flow. Catalog providerId is\n // `sap`. The apiKeyEnvVar is `SAP_AI_CORE_CLIENT_ID` (the OAuth client\n // id); the matching client_secret + token URL + base URL are all\n // documented in the catalog row's `extraEnvVars` and required by the\n // SapAiCoreAdapter. Wiring all four together so a single provisioning\n // round closes the loop.\n { envVar: 'SAP_AI_CORE_CLIENT_ID', secretKeys: ['sap-ai-core-client-id', 'sap-client-id'] },\n { envVar: 'SAP_AI_CORE_CLIENT_SECRET', secretKeys: ['sap-ai-core-client-secret', 'sap-client-secret'] },\n { envVar: 'SAP_AI_CORE_AUTH_URL', secretKeys: ['sap-ai-core-auth-url', 'sap-auth-url'] },\n { envVar: 'SAP_AI_CORE_BASE_URL', secretKeys: ['sap-ai-core-base-url', 'sap-base-url'] },\n // W&B Inference side-car — WANDB_API_KEY (auth) is already wired in\n // Batch 7 above; WANDB_PROJECT is the project slug header required on\n // every chat call. Wiring here closes the side-car gap.\n { envVar: 'WANDB_PROJECT', secretKeys: ['wandb-project', 'wandb-project-slug'] },\n // Image-only providers (no LLM gate participation, see comment below).\n // bfl auth header is `x-key` (not Bearer) — see catalog `authHeaderName`.\n { envVar: 'BFL_API_KEY', secretKeys: ['bfl-key', 'bfl-api-key', 'flux-key', 'black-forest-labs-key'] },\n { envVar: 'RECRAFT_API_KEY', secretKeys: ['recraft-key', 'recraft-api-key'] },\n { envVar: 'RUNWAYML_API_KEY', secretKeys: ['runwayml-key', 'runwayml-api-key', 'runway-key'] },\n { envVar: 'TOPAZ_API_KEY', secretKeys: ['topaz-key', 'topaz-api-key', 'topaz-labs-key'] },\n // ── LOTE O (2026-07-10) — Apertis + Inception Labs onboarding ─────────\n // Operator pre-provisioned both GCP secrets ahead of the catalog rows\n // (ailin-apertis-key, ailin-inception-key). secretKeys match exactly —\n // the GCP provider auto-prepends the `ailin-` prefix (see file header).\n { envVar: 'APERTIS_API_KEY', secretKeys: ['apertis-key', 'apertis-api-key'] },\n { envVar: 'INCEPTION_API_KEY', secretKeys: ['inception-key', 'inception-api-key'] },\n // ── LOTE P (2026-07-11) — EmpirioLabs AI, live-validated after gate ────\n { envVar: 'EMPIRIOLABS_API_KEY', secretKeys: ['empiriolabs-key', 'empiriolabs-api-key'] },\n // ── LOTE Q (2026-07-12) — Concentrate AI ───────────────────────────────\n { envVar: 'CONCENTRATE_API_KEY', secretKeys: ['concentrate-key', 'concentrate-api-key'] },\n // ── LOTE R (2026-07-13) — FastRouter ────────────────────────────────────\n { envVar: 'FASTROUTER_API_KEY', secretKeys: ['fastrouter-key', 'fastrouter-api-key'] },\n // ── LOTE S (2026-07-13) — Perplexity Agent API ──────────────────────────\n // Same physical credential as PERPLEXITY_API_KEY above (one Perplexity\n // account, two API surfaces) — deliberately sources from the SAME GCP\n // secret names, not a new one.\n { envVar: 'PERPLEXITY_AGENT_API_KEY', secretKeys: ['perplexity-api-key', 'perplexity-key'] },\n // ── LOTE T (2026-07-13) — Ailin gateway (self-referential meta-provider) ──\n // No GCP secret provisioned yet — inert until an operator sets AILIN_API_KEY\n // for a self-hosted deployment. secretKeys follow the same naming\n // convention as the rest of this file in case one is provisioned later.\n { envVar: 'AILIN_API_KEY', secretKeys: ['ailin-key', 'ailin-api-key', 'ailin-gateway-key'] },\n] as const;\n\n// ─── Provider Key Status Tracking ──────────────────────────────────────────\n// Tracks which provider API keys were successfully loaded, from which source,\n// and when. Used by the Self-Healing Discovery (L1) to know when to retry\n// discovery for native providers whose keys arrived late (GCP delay/failure).\n\nexport interface ProviderKeyStatus {\n loaded: boolean;\n source: 'gcp' | 'env' | 'none';\n timestamp: Date;\n envVar: string;\n}\n\n/**\n * Module-level state: populated during loadSecretsIntoEnv() for each provider.\n * Key = normalized provider name (lowercase, e.g., 'openai', 'anthropic').\n */\nconst providerKeyStatusMap = new Map<string, ProviderKeyStatus>();\n\n/**\n * Mapping from env var name to normalized provider name.\n * Allows the discovery service to map env vars to provider sources.\n */\nconst ENV_VAR_TO_PROVIDER: Record<string, string> = {\n // Native API providers\n OPENAI_API_KEY: 'openai',\n ANTHROPIC_API_KEY: 'anthropic',\n GOOGLE_API_KEY: 'google',\n DEEPSEEK_API_KEY: 'deepseek',\n XAI_API_KEY: 'xai',\n MISTRAL_API_KEY: 'mistral',\n COHERE_API_KEY: 'cohere',\n JINA_API_KEY: 'jina',\n // Cloud hubs — 2026-04-23: use canonical providerId 'nvidia' (legacy\n // alias 'nvidia-hub' was absorbed into the nvidia catalog row per Lot B;\n // discovery/status consumers read providerKeyStatus by canonical id).\n NVIDIA_API_KEY: 'nvidia',\n AIHUBMIX_API_KEY: 'aihubmix',\n NOVITA_API_KEY: 'novita',\n MOONSHOT_API_KEY: 'moonshot',\n MINIMAX_API_KEY: 'minimax',\n FRIENDLI_API_KEY: 'friendli',\n AIML_API_KEY: 'aiml',\n IMAGEROUTER_API_KEY: 'imagerouter',\n COMETAPI_API_KEY: 'cometapi',\n NANOGPT_API_KEY: 'nanogpt',\n EDENAI_API_KEY: 'edenai',\n HELICONEAI_API_KEY: 'heliconeai',\n POE_API_KEY: 'poe',\n ROUTEWAY_API_KEY: 'routeway',\n REQUESTY_API_KEY: 'requesty',\n // Post-migration canonical providerId is `ai302` (see providers.catalog.ts\n // and the residue-closure note in provider-registry.ts). Legacy `302ai`\n // survives as a catalog alias for pre-migration user configs.\n AI302_API_KEY: 'ai302',\n ORQAI_API_KEY: 'orqai',\n // Routers/aggregators\n OPENROUTER_API_KEY: 'openrouter',\n // Cloud providers\n VERTEX_AI_API_KEY: 'vertex-ai',\n AWS_ACCESS_KEY_ID: 'aws-bedrock',\n AWS_BEARER_TOKEN_BEDROCK: 'aws-bedrock',\n // SageMaker uses the same shared AWS creds as Bedrock, so `AWS_ACCESS_KEY_ID`\n // keeps mapping to `aws-bedrock` (first-listed wins) — but the endpoint-\n // specific env var is only meaningful for SageMaker. When operators wire\n // this, Self-Healing Discovery will attribute the key presence to the\n // SageMaker provider specifically.\n AWS_SAGEMAKER_ENDPOINT_NAME: 'aws-sagemaker',\n AZURE_OPENAI_API_KEY: 'azure-openai',\n // Regional/specialized\n QWEN_API_KEY: 'qwen',\n ERNIE_API_KEY: 'ernie',\n // Audio providers\n DEEPGRAM_API_KEY: 'deepgram',\n CARTESIA_API_KEY: 'cartesia',\n ELEVENLABS_API_KEY: 'elevenlabs',\n // Inference + embedding providers newly onboarded 2026-04-22\n WANDB_API_KEY: 'wandb',\n VOYAGE_API_KEY: 'voyage',\n BYTEZ_API_KEY: 'bytez',\n CEREBRAS_API_KEY: 'cerebras',\n FEATHERLESS_AI_API_KEY: 'featherless-ai', // canonical catalog id (2026-04-23 drift fix)\n // Batch 6 — enterprise hyperscaler gateways (2026-04-22). Gemini OAI-compat\n // is registered as a SEPARATE provider from the native `google` entry so\n // the self-healing discovery service can track them independently (they\n // have different failure modes — native uses @google/generative-ai SDK\n // with its own error surfaces; OAI-compat uses standard fetch).\n GEMINI_API_KEY: 'gemini-openai',\n GITHUB_TOKEN: 'github-models',\n DATABRICKS_TOKEN: 'databricks',\n // Orphan closure (2026-04-22) — Inworld now has a catalog row + adapter\n // factory. Mapping the env var to the provider slug lets Self-Healing\n // Discovery attribute key presence to the correct provider.\n INWORLD_API_KEY: 'inworld',\n // Orphan-closure Lot B (2026-04-23). Slug = catalog providerId. Writer\n // and Upstage are self-evident; Reka's slug is `rekaai` (the catalog\n // row's canonical providerId — aliases 'reka' + 'reka-ai' resolve to it).\n WRITER_API_KEY: 'writer',\n UPSTAGE_API_KEY: 'upstage',\n REKA_API_KEY: 'rekaai',\n // ── LOTE M (2026-04-23) — complement lot, 13 providers ──────────────\n // Slug = catalog providerId (NOT the upstream brand for phala/qianfan\n // which intentionally diverge — see catalog rows for rationale):\n // phala — canonical slug; upstream brand is \"RedPill\"\n // qianfan — canonical slug; upstream brand is \"Baidu Qianfan / ERNIE\"\n // gmi — canonical slug; upstream brand is \"GMICloud\"\n ARCEE_API_KEY: 'arcee',\n ATLASCLOUD_API_KEY: 'atlascloud',\n AVIAN_API_KEY: 'avian',\n QIANFAN_API_KEY: 'qianfan',\n GMI_API_KEY: 'gmi',\n INFERMATIC_API_KEY: 'infermatic',\n INFLECTION_API_KEY: 'inflection',\n MANCER_API_KEY: 'mancer',\n PHALA_API_KEY: 'phala',\n RELACE_API_KEY: 'relace',\n SILICONFLOW_API_KEY: 'siliconflow',\n STEPFUN_API_KEY: 'stepfun',\n VENICE_API_KEY: 'venice',\n // ── Sublote E1 (2026-04-24) — runtime-wiring closure for D1 matrix ───\n // 7 provider slugs whose env vars gained GCP→ENV tuples in this lot.\n // Slug = catalog providerId exactly (so Self-Healing Discovery can\n // attribute key-loaded events to the right provider). CLOUDFLARE_ACCOUNT_ID\n // is intentionally absent — that's a URL parameter, not an auth credential,\n // so mapping it to a provider slug would be semantically wrong (the same\n // account-id could conceivably scope multiple Cloudflare providers).\n GROQ_API_KEY: 'groq',\n DEEPINFRA_API_KEY: 'deepinfra',\n HF_TOKEN: 'huggingface',\n CLOUDFLARE_API_TOKEN: 'cloudflare-workers-ai',\n PERPLEXITY_API_KEY: 'perplexity',\n FIREWORKS_AI_API_KEY: 'fireworks-ai',\n SAMBANOVA_API_KEY: 'sambanova',\n // Replicate — pre-existing live-validation gap closed by sublote E1.\n // See the matching comment on the PROVIDER_SECRETS tuple above.\n REPLICATE_API_KEY: 'replicate',\n // ── LOTE N (2026-04-27) — catalog↔loader closure (22 new env vars) ──\n // Every entry below maps an env var to its canonical providerId so the\n // L1 Self-Healing Discovery Service can attribute key-loaded events\n // when the corresponding GCP secret is provisioned. Slugs match the\n // catalog providerId exactly (NOT upstream brand) — see catalog rows\n // for cases where they diverge (e.g. `sap` not `sap-ai-core`, `v0` not\n // `vercel-v0`, `volcano` not `volcengine`). Side-car env vars\n // (WATSONX_PROJECT_ID, SNOWFLAKE_ACCOUNT, SAP_AI_CORE_CLIENT_SECRET,\n // etc.) are NOT mapped here — they're auth or URL parameters, not\n // credentials, so attributing them to a provider would conflate\n // credential-presence with side-car-completeness in discovery telemetry.\n TOGETHERAI_API_KEY: 'togetherai',\n ANYSCALE_API_KEY: 'anyscale',\n HYPERBOLIC_API_KEY: 'hyperbolic',\n CHUTES_API_KEY: 'chutes',\n NSCALE_API_KEY: 'nscale',\n NEBIUS_API_KEY: 'nebius',\n LAMBDA_AI_API_KEY: 'lambda-ai',\n SCALEWAY_API_KEY: 'scaleway',\n SYNTHETIC_API_KEY: 'synthetic',\n MORPH_API_KEY: 'morph',\n ZAI_API_KEY: 'zai',\n XIAOMI_MIMO_API_KEY: 'xiaomi-mimo',\n V0_API_KEY: 'v0',\n VERCEL_AI_GATEWAY_API_KEY: 'vercel-ai-gateway',\n VOLCANO_API_KEY: 'volcano',\n WATSONX_APIKEY: 'watsonx',\n SNOWFLAKE_PAT: 'snowflake',\n SAP_AI_CORE_CLIENT_ID: 'sap',\n // Image / video-only providers. Still mapped to a slug so Self-Healing\n // Discovery can record key-presence events — but their env vars do NOT\n // appear in LLM_PROVIDER_ENV_VARS (image surfaces don't satisfy the\n // \"at least one LLM key\" boot gate).\n BFL_API_KEY: 'bfl',\n RECRAFT_API_KEY: 'recraft',\n RUNWAYML_API_KEY: 'runwayml',\n TOPAZ_API_KEY: 'topaz',\n // ── LOTE O (2026-07-10) ────────────────────────────────────────────────\n APERTIS_API_KEY: 'apertis',\n INCEPTION_API_KEY: 'inception',\n // ── LOTE P (2026-07-11) ────────────────────────────────────────────────\n EMPIRIOLABS_API_KEY: 'empiriolabs',\n // ── LOTE Q (2026-07-12) ────────────────────────────────────────────────\n CONCENTRATE_API_KEY: 'concentrate',\n // ── LOTE R (2026-07-13) ────────────────────────────────────────────────\n FASTROUTER_API_KEY: 'fastrouter',\n // ── LOTE S (2026-07-13) ────────────────────────────────────────────────\n PERPLEXITY_AGENT_API_KEY: 'perplexity-agent',\n // ── LOTE T (2026-07-13) ────────────────────────────────────────────────\n AILIN_API_KEY: 'ailin',\n};\n\n/**\n * Returns the status of all provider API keys.\n * Used by Self-Healing Discovery to determine which native sources can run.\n */\nexport function getProviderKeyStatus(): ReadonlyMap<string, ProviderKeyStatus> {\n return providerKeyStatusMap;\n}\n\n/**\n * Returns provider names that have valid (non-empty, non-mock) API keys loaded.\n */\nexport function getLoadedProviderNames(): string[] {\n return Array.from(providerKeyStatusMap.entries())\n .filter(([, status]) => status.loaded)\n .map(([name]) => name);\n}\n\n/**\n * Check if a specific provider has a loaded API key.\n */\nexport function isProviderKeyLoaded(providerName: string): boolean {\n return providerKeyStatusMap.get(providerName.toLowerCase())?.loaded ?? false;\n}\n\nconst LLM_PROVIDER_ENV_VARS = [\n 'OPENAI_API_KEY',\n 'ANTHROPIC_API_KEY',\n 'GOOGLE_API_KEY',\n 'DEEPSEEK_API_KEY',\n 'XAI_API_KEY',\n 'MISTRAL_API_KEY',\n 'COHERE_API_KEY',\n 'NVIDIA_API_KEY',\n 'AIHUBMIX_API_KEY',\n 'NOVITA_API_KEY',\n 'MOONSHOT_API_KEY',\n 'MINIMAX_API_KEY',\n 'JINA_API_KEY',\n 'FRIENDLI_API_KEY',\n 'AIML_API_KEY',\n 'IMAGEROUTER_API_KEY',\n 'OPENROUTER_API_KEY',\n 'ORQAI_API_KEY',\n 'EDENAI_API_KEY',\n 'HELICONEAI_API_KEY',\n 'VERTEX_AI_API_KEY',\n 'AWS_BEARER_TOKEN_BEDROCK',\n 'AZURE_OPENAI_API_KEY',\n 'QWEN_API_KEY',\n 'ERNIE_API_KEY',\n 'COMETAPI_API_KEY',\n 'NANOGPT_API_KEY',\n 'REQUESTY_API_KEY',\n 'AI302_API_KEY',\n 'POE_API_KEY',\n 'ROUTEWAY_API_KEY',\n // Added 2026-04-22 — these keys now count toward the \"at least one LLM key\n // present\" gate that avoids DEGRADED_SELF_HOSTED mode at boot.\n 'WANDB_API_KEY',\n 'VOYAGE_API_KEY',\n 'CEREBRAS_API_KEY',\n 'FEATHERLESS_AI_API_KEY',\n // Batch 4 landed BYTEZ catalog row + adapter, so BYTEZ_API_KEY now counts\n // toward the LLM-key-present gate.\n 'BYTEZ_API_KEY',\n // Batch 6 — enterprise hyperscaler gateways (2026-04-22). These keys now\n // count toward the \"at least one LLM key present\" gate; all four have\n // landed catalog rows + adapter classes in this batch.\n 'GEMINI_API_KEY',\n 'GITHUB_TOKEN',\n 'DATABRICKS_TOKEN',\n // Batch 8.1 — AWS SageMaker. Endpoint name is the meaningful trigger:\n // AWS creds alone are ambiguous (could be Bedrock or S3), but an\n // endpoint name implies \"SageMaker is provisioned here\".\n 'AWS_SAGEMAKER_ENDPOINT_NAME',\n // Orphan closure (2026-04-22) — Inworld AI now counts toward the\n // \"at least one LLM key present\" gate now that the catalog row +\n // adapter factory are wired.\n 'INWORLD_API_KEY',\n // Orphan-closure Lot B (2026-04-23). All three are fully canonical now.\n 'WRITER_API_KEY',\n 'UPSTAGE_API_KEY',\n 'REKA_API_KEY',\n // ── LOTE M (2026-04-23) complement lot ──────────────────────────────\n // Each of the 13 keys below counts toward the \"at least one LLM key\n // present\" gate, the same way all catalog-wired keys do. Note that\n // mancer and venice carry contentPolicyClass=uncensored (2026-04-28\n // Phase 4b — the prior denyByDefault gate was removed per the universal\n // \"habilitado e nunca censurado\" policy); their keys count toward the\n // gate exactly like any other LLM key — surface-level filtering is now\n // a downstream concern, not a routing one.\n // qianfan here is QIANFAN_API_KEY (v2 bce-v3 bearer); the legacy v1\n // ERNIE_API_KEY was already listed above this marker.\n 'ARCEE_API_KEY',\n 'ATLASCLOUD_API_KEY',\n 'AVIAN_API_KEY',\n 'QIANFAN_API_KEY',\n 'GMI_API_KEY',\n 'INFERMATIC_API_KEY',\n 'INFLECTION_API_KEY',\n 'MANCER_API_KEY',\n 'PHALA_API_KEY',\n 'RELACE_API_KEY',\n 'SILICONFLOW_API_KEY',\n 'STEPFUN_API_KEY',\n 'VENICE_API_KEY',\n // ── Sublote E1 (2026-04-24) — runtime-wiring closure for D1 matrix ───\n // These 7 providers landed in CONSOLIDATION_MATRIX['live-validation']\n // during Sublote D1 but had no GCP→ENV tuple above → their keys could\n // not count toward the \"at least one LLM key present\" gate even when\n // GCP Secret Manager held the key. Listed here so a restart after the\n // operator's provisioning cycle propagates the key-present signal to\n // the DEGRADED_SELF_HOSTED avoidance logic. CLOUDFLARE_ACCOUNT_ID is\n // intentionally absent — it's a URL parameter, not an LLM credential.\n 'GROQ_API_KEY',\n 'DEEPINFRA_API_KEY',\n 'HF_TOKEN',\n 'CLOUDFLARE_API_TOKEN',\n 'PERPLEXITY_API_KEY',\n 'FIREWORKS_AI_API_KEY',\n 'SAMBANOVA_API_KEY',\n // Replicate — pre-existing live-validation gap closed by sublote E1.\n // See the matching comments on the PROVIDER_SECRETS tuple and\n // ENV_VAR_TO_PROVIDER entry above.\n 'REPLICATE_API_KEY',\n // ── LOTE N (2026-04-27) — catalog↔loader closure ─────────────────────\n // 18 LLM-class providers added to the boot-mode gate. The 4 image/\n // video-only providers (bfl, recraft, runwayml, topaz) are\n // intentionally OMITTED — their key presence does not signal the\n // server has any text-generation capability, so they should not\n // satisfy the \"at least one LLM key present\" check.\n 'TOGETHERAI_API_KEY',\n 'ANYSCALE_API_KEY',\n 'HYPERBOLIC_API_KEY',\n 'CHUTES_API_KEY',\n 'NSCALE_API_KEY',\n 'NEBIUS_API_KEY',\n 'LAMBDA_AI_API_KEY',\n 'SCALEWAY_API_KEY',\n 'SYNTHETIC_API_KEY',\n 'MORPH_API_KEY',\n 'ZAI_API_KEY',\n 'XIAOMI_MIMO_API_KEY',\n 'V0_API_KEY',\n 'VERCEL_AI_GATEWAY_API_KEY',\n 'VOLCANO_API_KEY',\n 'WATSONX_APIKEY',\n 'SNOWFLAKE_PAT',\n 'SAP_AI_CORE_CLIENT_ID',\n // ── LOTE O (2026-07-10) — Apertis + Inception Labs, both chat-capable ──\n 'APERTIS_API_KEY',\n 'INCEPTION_API_KEY',\n // ── LOTE P (2026-07-11) — EmpirioLabs AI, chat-capable ─────────────────\n 'EMPIRIOLABS_API_KEY',\n // ── LOTE Q (2026-07-12) — Concentrate AI, chat-capable ─────────────────\n 'CONCENTRATE_API_KEY',\n // ── LOTE R (2026-07-13) — FastRouter, chat-capable ─────────────────────\n 'FASTROUTER_API_KEY',\n // ── LOTE S (2026-07-13) — Perplexity Agent API, chat-capable ───────────\n 'PERPLEXITY_AGENT_API_KEY',\n // ── LOTE T (2026-07-13) — Ailin gateway, chat-capable ──────────────────\n 'AILIN_API_KEY',\n] as const;\n\n/**\n * Self-hosted / sidecar endpoints that can keep the server alive in a\n * DEGRADED mode when every external LLM API key is missing.\n *\n * Design rationale:\n * Without this fallback, a single failure (expired GCP ADC, rotated\n * service account, network blip during boot) produces an infinite\n * restart loop. With it, the server boots in `degraded_self_hosted`\n * mode: only local/sidecar models are usable, requests are tagged\n * via last-resort-policy.ts (execution_mode=last_resort_self_hosted,\n * excluded_from_benchmark=true), and ops gets a clear warning\n * instead of a crash loop.\n *\n * This ties directly into Bloco P1.10 (self-hosted last-resort policy)\n * but at boot-time scope rather than per-request scope.\n */\nconst SELF_HOSTED_ENV_VARS = [\n 'OLLAMA_URL',\n 'SELF_HOSTED_LLM_URL',\n 'LOCAL_LLAMA_URL',\n 'LOCAL_KOBOLD_URL',\n] as const;\n\n/**\n * Load critical secrets from GCP into process.env\n * \n * This function is called AFTER SecretsManager initialization\n * and BEFORE config validation.\n * \n * It loads secrets from GCP and injects them into process.env,\n * allowing the existing config system to work without modification.\n */\nexport async function loadSecretsIntoEnv(): Promise<void> {\n const log = logger.child({ component: 'secrets-loader' });\n\n log.info('Loading critical secrets from GCP Secret Manager into environment...');\n\n let loaded = 0;\n let skipped = 0;\n let failed = 0;\n let sanitizedKeys = 0;\n let providerSecretsLoaded = 0;\n // Tracks env values that were authoritatively replaced by GCP fetches.\n // Useful diagnostic for operators verifying that GCP is the source of truth.\n let overwrittenFromGcp = 0;\n\n const forceLoadRealKeys = process.env.TEST_USE_REAL_API_KEYS === 'true';\n\n // ── GCP-authoritative mode ─────────────────────────────────────────────\n // When SECRETS_PROVIDER_PRIMARY=gcp the operator has declared GCP as the\n // source of truth. The legacy \"skip if env already populated\" path then\n // contradicts that declaration: env_file / docker-compose / shell exports\n // silently win and GCP fetches are never even attempted, which is exactly\n // the failure mode that hid populated GCP secrets (e.g. HF_TOKEN) from\n // /v1/models. In authoritative mode we ALWAYS attempt the GCP fetch and\n // overwrite the pre-existing env value when GCP returns one. The fallback\n // is symmetric: a GCP miss preserves the existing env value (no zeroing).\n //\n // Operators can override:\n // SECRETS_GCP_AUTHORITATIVE=false → keep legacy skip-if-exists semantics\n // SECRETS_GCP_AUTHORITATIVE=true → force authoritative even with env primary\n const gcpPrimaryEnv = (process.env.SECRETS_PROVIDER_PRIMARY || '').trim().toLowerCase() === 'gcp';\n const explicitAuthoritative = (process.env.SECRETS_GCP_AUTHORITATIVE || '').trim().toLowerCase();\n const gcpAuthoritative =\n explicitAuthoritative === 'true' ||\n (explicitAuthoritative !== 'false' && gcpPrimaryEnv);\n\n // Tests already use TEST_USE_REAL_API_KEYS=true to force GCP fetches even\n // when local mocks are present. The two flags are independent triggers\n // for the same effect; either one disables skip-if-exists.\n const authoritativeFetch = gcpAuthoritative || forceLoadRealKeys;\n\n log.info(\n {\n gcpPrimary: gcpPrimaryEnv,\n gcpAuthoritative,\n forceLoadRealKeys,\n authoritativeFetch,\n },\n 'Secret loading mode resolved'\n );\n\n // Load critical secrets (required)\n for (const { envVar, secretKey, required, defaultValue } of CRITICAL_SECRETS) {\n const existingValue = process.env[envVar];\n const shouldOverrideMockStripe =\n forceLoadRealKeys &&\n envVar.startsWith('STRIPE_') &&\n typeof existingValue === 'string' &&\n (existingValue.includes('mock') || existingValue.includes('test'));\n\n // Skip if already in env, UNLESS:\n // - we're in GCP-authoritative mode (operator declared GCP as source of truth), OR\n // - we're forcing real keys and detected mock Stripe keys.\n if (existingValue && !shouldOverrideMockStripe && !gcpAuthoritative) {\n log.debug({ envVar, source: 'env' }, 'Secret already in environment, skipping GCP load');\n skipped++;\n continue;\n }\n\n try {\n const value = await loadSecret(secretKey, required);\n if (value) {\n const wasOverwrite = Boolean(existingValue) && existingValue !== value;\n process.env[envVar] = value;\n if (wasOverwrite) overwrittenFromGcp++;\n log.debug(\n { envVar, secretKey, replaced: Boolean(existingValue), overwrittenFromGcp: wasOverwrite },\n 'Secret loaded from GCP into environment'\n );\n loaded++;\n } else if (existingValue && gcpAuthoritative) {\n // Authoritative fetch missed in GCP — fall back to the existing env\n // value rather than zeroing it. This keeps the system bootable when\n // a single secret is absent from GCP without losing the env-provided\n // value.\n log.debug(\n { envVar, secretKey },\n 'GCP miss in authoritative mode; preserving existing env value'\n );\n skipped++;\n } else if (defaultValue !== undefined) {\n // Use default value if secret not found and default provided\n process.env[envVar] = defaultValue;\n log.debug({ envVar, secretKey, defaultValue }, 'Secret not found, using default value');\n skipped++;\n } else if (!required) {\n log.debug({ envVar, secretKey }, 'Optional secret not found, skipping');\n skipped++;\n }\n } catch (error) {\n if (defaultValue !== undefined) {\n // Use default value on error if provided\n process.env[envVar] = defaultValue;\n log.warn({ envVar, secretKey, defaultValue, error: getErrorMessage(error) }, 'Failed to load secret, using default value');\n skipped++;\n } else if (required) {\n const errorMsg = getErrorMessage(error);\n log.error(\n { envVar, secretKey, error: errorMsg },\n 'Failed to load required secret'\n );\n failed++;\n throw new Error(`Failed to load required secret ${secretKey}: ${errorMsg}`);\n } else {\n log.warn({ envVar, secretKey, error: getErrorMessage(error) }, 'Failed to load optional secret');\n skipped++;\n }\n }\n }\n\n // Keep compatibility with components that still read GCS_BUCKET_NAME.\n if (!process.env.GCS_BUCKET_NAME && process.env.GCS_FILES_BUCKET) {\n process.env.GCS_BUCKET_NAME = process.env.GCS_FILES_BUCKET;\n }\n\n // Load provider API keys (optional)\n // CRITICAL: If GCP auth is available, ALWAYS load real keys (API Keys NUNCA DEVEM FALHAR)\n // If TEST_USE_REAL_API_KEYS=true, force load from GCP even if mock keys exist\n // Always replace mock keys with real keys from GCP when available\n // (reuses forceLoadRealKeys defined above)\n \n for (const { envVar, secretKeys } of PROVIDER_SECRETS) {\n const existingValue = process.env[envVar];\n const providerName = ENV_VAR_TO_PROVIDER[envVar];\n\n // Check if existing value looks like a mock key\n const isMockKey = existingValue && (existingValue.includes('mock') || existingValue.includes('test-'));\n\n // Skip if real key already exists, UNLESS:\n // - we're in GCP-authoritative mode (operator declared GCP as source of truth), OR\n // - test forces real keys (TEST_USE_REAL_API_KEYS=true), OR\n // - the existing value looks like a mock.\n // The authoritative path lets GCP overwrite stale env_file / shell-exported\n // values, which is the entire point of SECRETS_PROVIDER_PRIMARY=gcp.\n if (existingValue && !authoritativeFetch && !isMockKey) {\n // Record as loaded from env\n if (providerName) {\n providerKeyStatusMap.set(providerName, { loaded: true, source: 'env', timestamp: new Date(), envVar });\n }\n skipped++;\n continue;\n }\n\n try {\n let loadedSecretValue: string | undefined;\n let resolvedSecretKey = secretKeys[0];\n\n for (const candidateKey of secretKeys) {\n try {\n const candidateValue = await loadSecret(candidateKey, false);\n if (candidateValue) {\n loadedSecretValue = candidateValue;\n resolvedSecretKey = candidateKey;\n log.debug({ envVar, secretKey: candidateKey, valueLen: candidateValue.length }, 'Secret loaded from GCP');\n break;\n } else {\n log.debug({ envVar, secretKey: candidateKey }, 'Secret returned empty/null from GCP');\n }\n } catch (loadErr) {\n log.warn({ envVar, secretKey: candidateKey, error: loadErr instanceof Error ? loadErr.message : String(loadErr) }, 'Failed to load secret from GCP');\n continue;\n }\n }\n\n // GCP provider automatically adds prefix 'ailin-' to secretKey\n // So 'openai-key' becomes 'ailin-openai-key' in GCP Secret Manager\n const value = loadedSecretValue;\n if (value) {\n // Sanitize control characters and invisible Unicode.\n // PEM-format keys (-----BEGIN ...) legitimately contain newlines,\n // so use a lighter regex that preserves \\n and \\r for those values.\n const isPemKey = value.startsWith('-----BEGIN');\n let sanitizedValue = isPemKey\n ? value\n .replace(/[\\x00-\\x09\\x0B\\x0C\\x0E-\\x1F\\x7F]/g, '') // Strip control chars EXCEPT \\n (0x0A) and \\r (0x0D)\n .replace(/[\\u200B-\\u200D\\uFEFF]/g, '') // Remove zero-width spaces\n .trim()\n : value\n .replace(/[\\x00-\\x1F\\x7F]/g, '') // Strip ALL control characters\n .replace(/[\\u200B-\\u200D\\uFEFF]/g, '') // Remove zero-width spaces\n .trim();\n \n // Log detailed debugging information if sanitization changed the value\n if (sanitizedValue !== value) {\n const removedChars = value.length - sanitizedValue.length;\n sanitizedKeys++;\n log.debug(\n { \n envVar, \n secretKey: resolvedSecretKey,\n originalLength: value.length, \n sanitizedLength: sanitizedValue.length,\n removedChars\n },\n 'API key contained invalid characters and was sanitized'\n );\n }\n \n // Always replace mock keys with real keys\n const wasReplaced = !!existingValue;\n const wasOverwriteOfRealValue =\n wasReplaced && !isMockKey && existingValue !== sanitizedValue;\n\n // CRITICAL: Set process.env and immediately validate\n // Use Object.defineProperty as fallback if direct assignment doesn't work\n // This ensures the value is actually set in the environment\n const _previousValue = process.env[envVar];\n process.env[envVar] = sanitizedValue;\n\n // Immediately read back to verify (handles any potential async issues)\n const readBackValue = process.env[envVar];\n\n // Deep validation: check exact match, type, and length\n const isValidMatch = readBackValue === sanitizedValue;\n const isValidType = typeof readBackValue === 'string';\n const isValidLength = readBackValue !== undefined && readBackValue.length === sanitizedValue.length;\n\n if (!isValidMatch || !isValidType || !isValidLength) {\n log.error(\n {\n envVar,\n secretKey: resolvedSecretKey,\n sanitizedValueLength: sanitizedValue.length,\n readBackValueLength: readBackValue?.length,\n },\n 'Secret was loaded but process.env assignment did not persist'\n );\n failed++;\n } else {\n // Success - log and increment counter\n if (wasOverwriteOfRealValue) overwrittenFromGcp++;\n log.debug(\n {\n envVar,\n secretKey: resolvedSecretKey,\n replaced: wasReplaced,\n wasMock: isMockKey,\n overwrittenFromGcp: wasOverwriteOfRealValue,\n },\n 'Provider secret loaded from GCP'\n );\n loaded++;\n providerSecretsLoaded++;\n // Record successful load\n if (providerName) {\n providerKeyStatusMap.set(providerName, { loaded: true, source: 'gcp', timestamp: new Date(), envVar });\n }\n }\n } else {\n // GCP returned no value. Decide what's actually in process.env right\n // now: if env had a non-mock value before this loop iteration we\n // preserved it (the authoritative path entered this block but found\n // no GCP value to overwrite with), so we're effectively env-loaded.\n // If env had a mock value, we keep the mock as a degraded fallback\n // and warn loudly. If env had nothing, the provider stays orphaned.\n if (existingValue && !isMockKey) {\n // Authoritative-mode GCP miss with real env value already set:\n // the env value is still in process.env, so report source=env.\n if (providerName) {\n providerKeyStatusMap.set(providerName, { loaded: true, source: 'env', timestamp: new Date(), envVar });\n }\n log.debug(\n { envVar, secretKeys },\n 'GCP miss in authoritative mode; preserving existing env value for provider'\n );\n } else {\n if (isMockKey) {\n log.warn(\n { envVar, secretKeys },\n 'Mock API key detected but no real key found in GCP - using mock key (this may cause API failures)'\n );\n }\n // Record as not loaded (no GCP value, no usable env value)\n if (providerName) {\n providerKeyStatusMap.set(providerName, { loaded: false, source: 'none', timestamp: new Date(), envVar });\n }\n }\n skipped++;\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n // Only warn if not forcing real keys - if forcing, this is expected to work\n if (forceLoadRealKeys) {\n log.error(\n { envVar, secretKeys, error: errorMessage },\n 'Failed to load provider secret from GCP (TEST_USE_REAL_API_KEYS=true)'\n );\n failed++;\n } else {\n if (isMockKey) {\n log.warn(\n { envVar, secretKeys, error: errorMessage },\n 'Failed to load provider secret from GCP - will use mock key (this may cause API failures)'\n );\n } else {\n log.debug(\n { envVar, secretKeys, error: errorMessage },\n 'Failed to load provider secret from GCP (optional)'\n );\n }\n // Record failure\n if (providerName) {\n providerKeyStatusMap.set(providerName, { loaded: false, source: 'none', timestamp: new Date(), envVar });\n }\n skipped++;\n }\n }\n }\n\n // ── Env-var aliases (2026-04-28) ────────────────────────────────────────\n //\n // Some operators set a non-canonical name in `.env` for historical reasons\n // (e.g. `REKAAI_API_KEY` instead of the catalog-canonical `REKA_API_KEY`).\n // When GCP is reachable the secret loader populates the canonical name and\n // it just works; when GCP is unreachable the alias would be the only thing\n // set and the catalog row reads the canonical → empty key → silent skip.\n //\n // This pass copies known aliases into their canonical env var if the\n // canonical is empty. It is purely additive — never overwrites an\n // existing canonical value, never logs a warning unless an alias was\n // applied. Add new entries here when a similar mismatch is discovered.\n const ENV_ALIASES: ReadonlyArray<{ canonical: string; alias: string }> = [\n { canonical: 'REKA_API_KEY', alias: 'REKAAI_API_KEY' },\n ];\n for (const { canonical, alias } of ENV_ALIASES) {\n const canonicalValue = process.env[canonical];\n const aliasValue = process.env[alias];\n if ((canonicalValue == null || canonicalValue.trim() === '') && aliasValue && aliasValue.trim() !== '') {\n process.env[canonical] = aliasValue;\n log.info({ canonical, alias }, 'Promoted alias env var to canonical name');\n }\n }\n\n const gcpPrimary = (process.env.SECRETS_PROVIDER_PRIMARY || '').trim().toLowerCase() === 'gcp';\n const failFastEnv = (process.env.SECRETS_GCP_FAIL_FAST || '').trim().toLowerCase();\n const gcpFailFast = failFastEnv === 'true' || (failFastEnv !== 'false' && gcpPrimary);\n const hasAnyLlmProviderKey = LLM_PROVIDER_ENV_VARS.some((envVar) => {\n const value = process.env[envVar];\n return typeof value === 'string' && value.trim().length > 0;\n });\n\n // Hardening (P1.10 at boot scope): even when zero external API keys are\n // loaded, the server can still boot in DEGRADED mode if a self-hosted\n // endpoint is reachable. Every request will then be tagged via the\n // last-resort-policy as execution_mode=last_resort_self_hosted.\n const availableSelfHostedEndpoints = SELF_HOSTED_ENV_VARS.filter((envVar) => {\n const value = process.env[envVar];\n return typeof value === 'string' && value.trim().length > 0;\n });\n const hasAnySelfHostedFallback = availableSelfHostedEndpoints.length > 0;\n\n // Opt-out for strict production deployments that must never run in\n // degraded mode. Default: allow degraded boot (safer than crash loops).\n const allowDegradedBoot = (process.env.ALLOW_DEGRADED_BOOT || 'true').trim().toLowerCase() !== 'false';\n\n if (gcpFailFast && !hasAnyLlmProviderKey) {\n if (hasAnySelfHostedFallback && allowDegradedBoot) {\n log.warn(\n {\n gcpPrimary,\n gcpFailFast,\n providerSecretsLoaded,\n selfHostedEndpoints: availableSelfHostedEndpoints,\n mode: 'degraded_self_hosted',\n docs: 'docs/hardening/last-resort-policy',\n },\n '⚠️ DEGRADED BOOT: zero external LLM credentials, but self-hosted fallback is available. ' +\n 'Server will start in last-resort mode. Requests will be tagged excluded_from_benchmark. ' +\n 'Fix ADC/GCP credentials to restore full operation.'\n );\n // Intentionally fall through — boot continues\n } else {\n const message = hasAnySelfHostedFallback\n ? 'SECRETS_PROVIDER_PRIMARY=gcp, no LLM provider credentials loaded, and ALLOW_DEGRADED_BOOT=false.'\n : 'SECRETS_PROVIDER_PRIMARY=gcp but no LLM provider credentials are available after secrets bootstrap, ' +\n 'and no self-hosted fallback endpoints (OLLAMA_URL, SELF_HOSTED_LLM_URL, LOCAL_LLAMA_URL, LOCAL_KOBOLD_URL) are configured.';\n log.error(\n {\n gcpPrimary,\n gcpFailFast,\n providerSecretsLoaded,\n checkedEnvVars: LLM_PROVIDER_ENV_VARS,\n selfHostedEndpointsChecked: SELF_HOSTED_ENV_VARS,\n allowDegradedBoot,\n },\n message\n );\n throw new Error(\n `${message} Verify GCP Secret Manager access (ADC/Workload Identity) and secret names/prefix, ` +\n `or set OLLAMA_URL for degraded self-hosted boot, or run 'gcloud auth application-default login' if local.`\n );\n }\n }\n\n // Warn on partial provider key loading — helps detect GCP issues early\n if (hasAnyLlmProviderKey && providerSecretsLoaded > 0 && providerSecretsLoaded < 3) {\n log.warn(\n { providerSecretsLoaded, total: PROVIDER_SECRETS.length },\n `⚠️ Only ${providerSecretsLoaded} provider secrets loaded — possible partial GCP failure`\n );\n }\n\n // Audio provider diagnostics\n const audioProviders = {\n DEEPGRAM_API_KEY: !!process.env.DEEPGRAM_API_KEY,\n CARTESIA_API_KEY: !!process.env.CARTESIA_API_KEY,\n ELEVENLABS_API_KEY: !!process.env.ELEVENLABS_API_KEY,\n OLLAMA_URL: process.env.OLLAMA_URL || '',\n SELF_HOSTED_STT_URL: process.env.SELF_HOSTED_STT_URL || '',\n LOCAL_NLLB_URL: process.env.LOCAL_NLLB_URL || '',\n };\n log.info({ audioProviders }, 'Audio provider env vars after secrets loading');\n\n log.info(\n {\n loaded,\n skipped,\n failed,\n sanitizedKeys,\n providerSecretsLoaded,\n overwrittenFromGcp,\n gcpAuthoritative,\n total: CRITICAL_SECRETS.length + PROVIDER_SECRETS.length,\n },\n '✅ Secrets loading complete'\n );\n\n if (failed > 0) {\n throw new Error(`Failed to load ${failed} required secrets`);\n }\n}\n\n/**\n * Get summary of secrets loaded.\n *\n * Diagnostic helper for admin endpoints (e.g. /v1/admin/providers/...) that\n * surfaces exactly which provider envVars are populated and from where, so\n * operators can verify SECRETS_PROVIDER_PRIMARY=gcp is actually authoritative\n * and not silently shadowed by stale env values.\n */\nexport function getSecretsLoadSummary(): {\n fromGCP: string[];\n fromEnv: string[];\n notLoaded: string[];\n} {\n const fromGCP: string[] = [];\n const fromEnv: string[] = [];\n const notLoaded: string[] = [];\n\n for (const status of providerKeyStatusMap.values()) {\n if (!status.loaded) {\n notLoaded.push(status.envVar);\n continue;\n }\n if (status.source === 'gcp') {\n fromGCP.push(status.envVar);\n } else if (status.source === 'env') {\n fromEnv.push(status.envVar);\n }\n }\n\n return { fromGCP, fromEnv, notLoaded };\n}\n"}}, "api/src/providers/catalog/providers.catalog.ts": {"cb3d697c21c8852d6f51ec5cd9aa1e4a02d90328": {"t": 1785674600.3063216, "n": 160496, "h": "a7f9b98664b2dce8", "c": "// Copyright (C) 2026 Ailin One, Inc.\n//\n// This file is part of Collective Intelligence Engine (ci).\n// Licensed under the GNU Affero General Public License v3.0 or later.\n// See LICENSE in the repository root, or <https://www.gnu.org/licenses/>.\n//\n// SPDX-License-Identifier: AGPL-3.0-or-later\n// Source: https://github.com/ailinone/collective-intelligence\n\n/**\n * Provider Catalog — Data-Driven Entries\n *\n * Each entry registers ONE provider. Adding a new OpenAI-compatible provider\n * typically costs ~15 lines here and ZERO lines in `provider-registry.ts`.\n *\n * AUTHORING RULES (enforced by Zod schema):\n * - `providerId`: lowercase-kebab-case, unique.\n * - `apiKeyEnvVar`: must match `<PROVIDER_ID_UPPER>_API_KEY` unless the\n * authScheme is hmac-sigv4/iam-token/oauth2/custom.\n * - `baseUrl`: https only, unless self-hosted.\n * - `supports` reflects the provider's declared surface. Hints go to\n * `capabilityHints`. The PER-MODEL capability resolution is STILL done\n * by the HCRA merger with full provenance.\n *\n * WHAT THIS CATALOG IS NOT:\n * - It is NOT a per-model capability table.\n * - It is NOT an authoritative pricing source.\n * - It is NOT a replacement for the first-party adapters (OpenAI, Anthropic,\n * Google, etc.) — those remain as dedicated classes in provider-registry.ts.\n *\n * WHEN TO ADD HERE vs WRITE A NEW ADAPTER:\n * - Provider speaks OpenAI `/v1/chat/completions` and `/v1/models` → catalog entry only.\n * - Provider has quirks (citations, custom headers, non-standard list path) →\n * catalog entry with `integrationClass: 'oai-compat-quirks'` + overrides.\n * - Provider has a fundamentally different API shape (Bedrock Converse,\n * Anthropic Messages, Vertex Generate) → dedicated adapter class +\n * `adapterClass` field pointing to it.\n */\n\nimport type { ProviderCatalogEntry } from './provider-catalog.types';\n\n/**\n * Complete catalog. Order is the preferred registration order, which also\n * becomes the default execution priority tiebreaker (earlier = higher).\n */\nexport const PROVIDER_CATALOG: readonly ProviderCatalogEntry[] = [\n // ──────────────────────────────────────────────────────────────────────────\n // LOTE A — OpenAI-compatible pure providers\n // These speak the OpenAI protocol faithfully. Reuse the hub fetcher/adapter.\n // ──────────────────────────────────────────────────────────────────────────\n {\n providerId: 'groq',\n displayName: 'Groq',\n providerFamily: 'groq',\n integrationClass: 'oai-compat-quirks', // reasoning_format / reasoning_effort / service_tier — not pure\n integrationMode: 'discovery+execution',\n baseUrl: 'https://api.groq.com/openai/v1',\n authScheme: 'bearer',\n apiKeyEnvVar: 'GROQ_API_KEY',\n adapterClass: 'GroqAdapter',\n supports: {\n chat: true,\n streaming: true,\n tools: true,\n jsonMode: true,\n vision: true, // llama-3.2 vision family\n },\n capabilityHints: [\n { capability: 'low_latency', rationale: 'provider-class-default', confidence: 0.7 },\n ],\n pricingMode: 'none',\n enabledByDefault: true,\n priority: 50,\n docsUrl: 'https://console.groq.com/docs/api-reference',\n notes:\n 'Custom LPU hardware, sub-second latency for Llama/Mixtral/Gemma. Dedicated adapter injects reasoning_format/reasoning_effort/service_tier for OSS-reasoning models (gpt-oss, deepseek-r1, qwen-qwq, compound-beta).',\n lastReviewedAt: '2026-04-22',\n },\n {\n providerId: 'togetherai',\n displayName: 'Together AI',\n providerFamily: 'togetherai',\n integrationClass: 'oai-compat-pure',\n integrationMode: 'discovery+execution',\n baseUrl: 'https://api.together.xyz/v1',\n authScheme: 'bearer',\n apiKeyEnvVar: 'TOGETHERAI_API_KEY',\n // Live probe 2026-07-17: POST /videos/generations EXISTS and requires the\n // Together-specific body `{model, payload:{...}}` — the flat OAI shape is\n // rejected with \"validation failed for field 'payload': expected\n // required\". Video model ids validated as accepted at the field layer:\n // openai/sora-2, google/veo-3.0-fast, kwaivgI/kling-2.1-standard (their\n // GET /v1/models lists them with type \"video\"). Inner payload fields are\n // per-model; response/poll contract still to be proven by a first real\n // generation.\n videoRequestStyle: 'payload-wrap',\n supports: {\n chat: true,\n embeddings: true,\n streaming: true,\n tools: true,\n jsonMode: true,\n imageGeneration: true, // FLUX, SDXL hosted\n vision: true,\n videoGeneration: true,\n },\n pricingMode: 'remote',\n enabledByDefault: true,\n priority: 40,\n docsUrl: 'https://docs.together.ai/reference',\n notes:\n 'D1 2026-04-24: provisioned key (ailin-togetherai-api-key) returned 401 on /v1/chat — \"key_\" prefix non-canonical; operator to re-issue if chat 401 persists. Probe 2026-07-17: video surface live-validated to the field-validation layer with THIS key (payload-wrap body; see videoRequestStyle) — auth accepted there.',\n lastReviewedAt: '2026-07-17',\n originalProviderField: 'organization',\n },\n {\n providerId: 'fireworks-ai',\n displayName: 'Fireworks AI',\n providerFamily: 'fireworks-ai',\n aliases: ['fireworks', 'fireworks_ai'],\n integrationClass: 'oai-compat-pure',\n integrationMode: 'discovery+execution',\n baseUrl: 'https://api.fireworks.ai/inference/v1',\n authScheme: 'bearer',\n apiKeyEnvVar: 'FIREWORKS_AI_API_KEY',\n supports: {\n chat: true,\n embeddings: true,\n streaming: true,\n tools: true,\n jsonMode: true,\n imageGeneration: true,\n vision: true,\n },\n pricingMode: 'remote',\n enabledByDefault: true,\n priority: 40,\n docsUrl: 'https://docs.fireworks.ai/api-reference',\n lastReviewedAt: '2026-04-21',\n },\n {\n providerId: 'deepinfra',\n displayName: 'DeepInfra',\n providerFamily: 'deepinfra',\n integrationClass: 'oai-compat-pure',\n integrationMode: 'discovery+execution',\n baseUrl: 'https://api.deepinfra.com/v1/openai',\n authScheme: 'bearer',\n apiKeyEnvVar: 'DEEPINFRA_API_KEY',\n supports: {\n chat: true,\n embeddings: true,\n streaming: true,\n tools: true,\n imageGeneration: true,\n speechToText: true,\n textToSpeech: true,\n },\n pricingMode: 'remote',\n enabledByDefault: true,\n priority: 35,\n docsUrl: 'https://deepinfra.com/docs',\n lastReviewedAt: '2026-04-21',\n },\n {\n providerId: 'perplexity',\n displayName: 'Perplexity AI',\n providerFamily: 'perplexity',\n integrationClass: 'oai-compat-quirks',\n integrationMode: 'discovery+execution',\n baseUrl: 'https://api.perplexity.ai',\n authScheme: 'bearer',\n apiKeyEnvVar: 'PERPLEXITY_API_KEY',\n adapterClass: 'PerplexityAdapter',\n paths: {\n // Perplexity has no /models endpoint. The pinnedFallback below carries\n // the canonical Sonar family list (Phase 4d, 2026-04-28 — was staticModels).\n chatCompletions: '/chat/completions',\n },\n pinnedFallback: {\n // Operator-declared capabilities (root-cause refactor 2026-04-28).\n // All Sonar models are research-grounded chat with web_search; the\n // *-reasoning-* family adds extended reasoning. Declared here directly\n // so the catalog-bridge does NOT fall back to name-regex inference.\n models: [\n { id: 'sonar-small-online', capabilities: ['chat', 'streaming', 'web_search'] },\n { id: 'sonar-medium-online', capabilities: ['chat', 'streaming', 'web_search'] },\n { id: 'sonar-large-online', capabilities: ['chat', 'streaming', 'web_search'] },\n { id: 'sonar-pro', capabilities: ['chat', 'streaming', 'web_search'] },\n {\n id: 'sonar-reasoning',\n capabilities: ['chat', 'streaming', 'web_search', 'reasoning', 'thinking_mode'],\n },\n {\n id: 'sonar-reasoning-pro',\n capabilities: ['chat', 'streaming', 'web_search', 'reasoning', 'thinking_mode'],\n },\n ],\n reason: 'no-list-endpoint',\n lastReviewedAt: '2026-04-22',\n },\n supports: {\n chat: true,\n streaming: true,\n },\n capabilityHints: [\n { capability: 'web_search', rationale: 'provider-class-default', confidence: 0.85 },\n { capability: 'deep_research', rationale: 'docs-declared', confidence: 0.7 },\n ],\n pricingMode: 'none',\n enabledByDefault: true,\n priority: 30,\n docsUrl: 'https://docs.perplexity.ai/api-reference/chat-completions',\n notes:\n 'All models are web-search-grounded. Response includes citations[], related_questions[], images[]. Dedicated adapter preserves those extension fields on PerplexityChatResponse.',\n lastReviewedAt: '2026-04-22',\n },\n {\n providerId: 'cerebras',\n displayName: 'Cerebras',\n providerFamily: 'cerebras',\n integrationClass: 'oai-compat-quirks', // max_completion_tokens normalization\n integrationMode: 'discovery+execution',\n baseUrl: 'https://api.cerebras.ai/v1',\n authScheme: 'bearer',\n apiKeyEnvVar: 'CEREBRAS_API_KEY',\n adapterClass: 'CerebrasAdapter',\n supports: {\n chat: true,\n streaming: true,\n tools: true,\n jsonMode: true,\n },\n capabilityHints: [\n { capability: 'low_latency', rationale: 'provider-class-default', confidence: 0.75 },\n ],\n pricingMode: 'none',\n enabledByDefault: true,\n priority: 45,\n docsUrl: 'https://inference-docs.cerebras.ai/api-reference',\n notes:\n 'Wafer-scale inference; fastest Llama-70B in market. Dedicated adapter normalizes max_completion_tokens ↔ max_tokens per Cerebras docs.',\n lastReviewedAt: '2026-04-22',\n },\n {\n providerId: 'hyperbolic',\n displayName: 'Hyperbolic',\n providerFamily: 'hyperbolic',\n integrationClass: 'oai-compat-pure',\n integrationMode: 'discovery+execution',\n baseUrl: 'https://api.hyperbolic.xyz/v1',\n authScheme: 'bearer',\n apiKeyEnvVar: 'HYPERBOLIC_API_KEY',\n supports: {\n chat: true,\n embeddings: true,\n streaming: true,\n imageGeneration: true,\n },\n pricingMode: 'none',\n enabledByDefault: true,\n priority: 20,\n docsUrl: 'https://docs.hyperbolic.xyz/',\n notes:\n 'D1 2026-04-24: key provisioned (ailin-hyperbolic-api-key, 73B \"sk_liv…\") and auth accepted; /v1/chat returns 402 {\"detail\":\"Insufficient funds, please see https://docs.hyperbolic.xyz/docs/hyperbolic-pricing\"}. Classified upstream-suspended (not credentials-missing) because the credential itself is valid — only the account balance is zero. Operator top-up unblocks live-validation.',\n lastReviewedAt: '2026-04-24',\n },\n {\n providerId: 'nscale',\n displayName: 'Nscale (EU)',\n providerFamily: 'nscale',\n integrationClass: 'oai-compat-pure',\n integrationMode: 'discovery+execution',\n baseUrl: 'https://inference.api.nscale.com/v1',\n authScheme: 'bearer',\n apiKeyEnvVar: 'NSCALE_API_KEY',\n supports: {\n chat: true,\n streaming: true,\n tools: true,\n },\n capabilityHints: [\n { capability: 'eu_sovereign', rationale: 'provider-class-default', confidence: 0.85 },\n ],\n pricingMode: 'none',\n enabledByDefault: true,\n priority: 25,\n docsUrl: 'https://docs.nscale.com/docs/inference/chat',\n notes: 'EU data sovereignty; GDPR-compliant inference.',\n lastReviewedAt: '2026-04-21',\n },\n {\n providerId: 'anyscale',\n displayName: 'Anyscale Endpoints',\n providerFamily: 'anyscale',\n integrationClass: 'oai-compat-pure',\n integrationMode: 'discovery+execution',\n baseUrl: 'https://api.endpoints.anyscale.com/v1',\n authScheme: 'bearer',\n apiKeyEnvVar: 'ANYSCALE_API_KEY',\n supports: {\n chat: true,\n embeddings: true,\n streaming: true,\n },\n pricingMode: 'none',\n enabledByDefault: true, // Anyscale deprecated public endpoints mid-2024; secret provisioned but vendor-side suspended. Adapter still resolves for ANYSCALE_BASE_URL private deployments.\n priority: 10,\n notes:\n 'Deprecated for new signups. Entry retained for existing enterprise deployments via ANYSCALE_BASE_URL override. D1 2026-04-24: key provisioned (ailin-anyscale-api-key, 236B \"aph0_C…\") but api.endpoints.anyscale.com returns HTML shutdown notice \"Effective August 1, 2024 ... Multi-tenant access to LLM models has been removed.\" Permanent vendor-side shutdown; classified upstream-suspended. Unblock via private Hosted deployment with ANYSCALE_BASE_URL override.',\n baseUrlEnvVar: 'ANYSCALE_BASE_URL',\n lastReviewedAt: '2026-04-24',\n },\n {\n providerId: 'featherless-ai',\n displayName: 'Featherless AI',\n providerFamily: 'featherless-ai',\n aliases: ['featherless', 'featherless_ai'],\n integrationClass: 'oai-compat-pure',\n integrationMode: 'discovery+execution',\n baseUrl: 'https://api.featherless.ai/v1',\n authScheme: 'bearer',\n apiKeyEnvVar: 'FEATHERLESS_AI_API_KEY',\n adapterClass: 'FeatherlessAdapter',\n supports: {\n chat: true,\n streaming: true,\n },\n pricingMode: 'none',\n enabledByDefault: true,\n priority: 15,\n docsUrl: 'https://featherless.ai/docs/completions',\n notes:\n 'Large long-tail HF-model marketplace. Dedicated thin adapter for per-provider observability (logs/metrics scope to `provider: featherless-ai`). No model identifiers hardcoded — the catalog/discovery service is sole source of truth; the adapter is identity-only over the hub.',\n lastReviewedAt: '2026-04-22',\n },\n {\n providerId: 'nebius',\n displayName: 'Nebius AI Studio',\n providerFamily: 'nebius',\n integrationClass: 'oai-compat-pure',\n integrationMode: 'discovery+execution',\n baseUrl: 'https://api.studio.nebius.ai/v1',\n authScheme: 'bearer',\n apiKeyEnvVar: 'NEBIUS_API_KEY',\n supports: {\n chat: true,\n embeddings: true,\n streaming: true,\n tools: true,\n jsonMode: true,\n imageGeneration: true,\n vision: true,\n },\n pricingMode: 'remote',\n enabledByDefault: true,\n priority: 30,\n docsUrl: 'https://docs.nebius.com/studio/inference',\n lastReviewedAt: '2026-04-21',\n },\n {\n providerId: 'lambda-ai',\n displayName: 'Lambda AI',\n providerFamily: 'lambda-ai',\n aliases: ['lambda', 'lambdalabs'],\n integrationClass: 'oai-compat-pure',\n integrationMode: 'discovery+execution',\n baseUrl: 'https://api.lambdalabs.com/v1',\n authScheme: 'bearer',\n apiKeyEnvVar: 'LAMBDA_AI_API_KEY',\n supports: {\n chat: true,\n streaming: true,\n },\n pricingMode: 'none',\n enabledByDefault: true,\n priority: 20,\n docsUrl: 'https://docs.lambdalabs.com/public-cloud/lambda-inference-api/',\n lastReviewedAt: '2026-04-21',\n },\n {\n providerId: 'sambanova',\n displayName: 'SambaNova Cloud',\n providerFamily: 'sambanova',\n integrationClass: 'oai-compat-pure', // wire protocol is pure OAI; adapter only adds tier hints\n integrationMode: 'discovery+execution',\n baseUrl: 'https://api.sambanova.ai/v1',\n authScheme: 'bearer',\n apiKeyEnvVar: 'SAMBANOVA_API_KEY',\n adapterClass: 'SambanovaAdapter',\n supports: {\n chat: true,\n streaming: true,\n tools: true,\n },\n capabilityHints: [\n { capability: 'low_latency', rationale: 'provider-class-default', confidence: 0.7 },\n ],\n pricingMode: 'none',\n enabledByDefault: true,\n priority: 40,\n docsUrl: 'https://docs.sambanova.ai/cloud/docs/get-started/overview',\n notes:\n 'RDU hardware; fast Llama-405B inference. Dedicated adapter only labels observability and exposes FAST_TIER_MODELS hint.',\n lastReviewedAt: '2026-04-22',\n },\n {\n providerId: 'scaleway',\n displayName: 'Scaleway Generative APIs',\n providerFamily: 'scaleway',\n integrationClass: 'oai-compat-pure',\n integrationMode: 'discovery+execution',\n baseUrl: 'https://api.scaleway.ai/v1',\n authScheme: 'bearer',\n apiKeyEnvVar: 'SCALEWAY_API_KEY',\n supports: {\n chat: true,\n embeddings: true,\n streaming: true,\n tools: true,\n jsonMode: true,\n },\n capabilityHints: [\n { capability: 'eu_sovereign', rationale: 'provider-class-default', confidence: 0.85 },\n ],\n pricingMode: 'none',\n enabledByDefault: true,\n priority: 25,\n docsUrl: 'https://www.scaleway.com/en/docs/generative-apis/',\n lastReviewedAt: '2026-04-21',\n },\n {\n providerId: 'synthetic',\n displayName: 'Synthetic',\n providerFamily: 'synthetic',\n integrationClass: 'oai-compat-pure',\n integrationMode: 'discovery+execution',\n baseUrl: 'https://api.synthetic.new/v1',\n authScheme: 'bearer',\n apiKeyEnvVar: 'SYNTHETIC_API_KEY',\n supports: {\n chat: true,\n streaming: true,\n },\n pricingMode: 'none',\n enabledByDefault: true,\n priority: 10,\n docsUrl: 'https://synthetic.new/landing/home',\n notes: 'Smaller provider; smoke-test with real key before relying on routing.',\n lastReviewedAt: '2026-04-21',\n },\n {\n providerId: 'morph',\n displayName: 'Morph',\n providerFamily: 'morph',\n integrationClass: 'oai-compat-pure',\n integrationMode: 'discovery+execution',\n baseUrl: 'https://api.morphllm.com/v1',\n authScheme: 'bearer',\n apiKeyEnvVar: 'MORPH_API_KEY',\n supports: {\n chat: true,\n streaming: true,\n },\n pricingMode: 'none',\n enabledByDefault: true,\n priority: 10,\n docsUrl: 'https://docs.morphllm.com/',\n notes: 'Fast code-editing specialist. Verify baseUrl before enabling.',\n lastReviewedAt: '2026-04-21',\n },\n {\n providerId: 'chutes',\n displayName: 'Chutes',\n providerFamily: 'chutes',\n integrationClass: 'oai-compat-pure',\n integrationMode: 'discovery+execution',\n baseUrl: 'https://llm.chutes.ai/v1',\n authScheme: 'bearer',\n apiKeyEnvVar: 'CHUTES_API_KEY',\n supports: {\n chat: true,\n streaming: true,\n },\n pricingMode: 'none',\n enabledByDefault: true,\n priority: 15,\n docsUrl: 'https://chutes.ai/',\n notes:\n 'D1 2026-04-24: key provisioned (ailin-chutes-api-key) and auth accepted; /v1/chat returned 402 (account balance $0.0). Live-verified 2026-07-29 after operator top-up: GET /v1/models 200 (15 TEE models, this key is TEE-only by provisioning), POST /v1/chat/completions 200 for Qwen/Qwen3-32B-TEE and unsloth/Mistral-Nemo-Instruct-2407-TEE — zero 402s. No integration changes needed.',\n lastReviewedAt: '2026-07-29',\n },\n\n // ──────────────────────────────────────────────────────────────────────────\n // LOTE B — User-prioritized providers\n // ──────────────────────────────────────────────────────────────────────────\n {\n providerId: 'zai',\n displayName: 'Z.AI (Zhipu)',\n providerFamily: 'zai',\n aliases: ['zhipu', 'zhipuai', 'bigmodel'],\n integrationClass: 'oai-compat-pure',\n integrationMode: 'discovery+execution',\n baseUrl: 'https://open.bigmodel.cn/api/paas/v4',\n authScheme: 'bearer',\n apiKeyEnvVar: 'ZAI_API_KEY',\n supports: {\n chat: true,\n embeddings: true,\n streaming: true,\n tools: true,\n jsonMode: true,\n vision: true,\n imageGeneration: true,\n videoGeneration: true,\n },\n capabilityHints: [\n { capability: 'multilingual_chinese', rationale: 'provider-class-default', confidence: 0.85 },\n ],\n pricingMode: 'none',\n enabledByDefault: true,\n priority: 30,\n docsUrl: 'https://docs.z.ai/devpack/quick-start',\n notes: 'GLM-4 family; strong Chinese + multilingual. Video via CogVideoX.',\n lastReviewedAt: '2026-04-21',\n },\n {\n providerId: 'xiaomi-mimo',\n displayName: 'Xiaomi MiMo',\n providerFamily: 'xiaomi-mimo',\n aliases: ['xiaomi', 'mimo'],\n integrationClass: 'oai-compat-pure',\n // 2026-04-29: live probe of platform.xiaomimimo.com/v1/models returns the\n // platform's HTML homepage (no JSON listing endpoint exists). Switched\n // to execution-only with pinnedFallback so the catalog row materializes.\n // Model IDs sourced from the Vercel AI Gateway listing (xiaomi/mimo-v2-pro,\n // xiaomi/mimo-v2-flash) plus the platform's own docs (Omni multimodal).\n integrationMode: 'execution-only',\n baseUrl: 'https://platform.xiaomimimo.com/v1',\n authScheme: 'bearer',\n apiKeyEnvVar: 'XIAOMI_MIMO_API_KEY',\n supports: {\n chat: true,\n streaming: true,\n },\n pinnedFallback: {\n reason: 'no-list-endpoint',\n // Structured form with capabilities — same rationale as v0 above:\n // `mimo-*` ids don't match any built-in regex, so bare strings would\n // trip the `pinnedFallback-capability-coverage` invariant.\n //\n // Architectural note (2026-04-28): the model's Chinese-language\n // affinity is NOT in this strict capabilities array. It lives in\n // `capabilityHints` below (fuzzy soft hint, confidence 0.85) — the\n // strict capabilities surface answers \"can the model perform action\n // X?\" (boolean), while language strength is a fuzzy quality, not a\n // discrete capability surface. Other Chinese-trained providers\n // (qwen, glm, ernie, deepseek) follow the same convention.\n models: [\n { id: 'mimo-v2-pro', capabilities: ['chat', 'streaming', 'tool_use'] },\n { id: 'mimo-v2-flash', capabilities: ['chat', 'streaming'] },\n { id: 'mimo-omni', capabilities: ['chat', 'streaming', 'vision', 'multimodal'] },\n ],\n lastReviewedAt: '2026-04-29',\n },\n capabilityHints: [\n { capability: 'multilingual_chinese', rationale: 'provider-class-default', confidence: 0.85 },\n ],\n pricingMode: 'none',\n enabledByDefault: true,\n priority: 10,\n docsUrl: 'https://platform.xiaomimimo.com/#/docs/welcome',\n notes:\n 'New platform; no /v1/models listing — pinnedFallback covers the 3 published SKUs (MiMo-V2-Pro, V2-Flash, Omni). Cross-checked against Vercel AI Gateway listing 2026-04-29. Uses dedicated XiaomiMimoAdapter for named identity in metrics + circuit-breaker scoping.',\n adapterClass: 'XiaomiMimoAdapter',\n lastReviewedAt: '2026-04-29',\n },\n {\n providerId: 'v0',\n displayName: 'v0 (Vercel)',\n providerFamily: 'v0',\n integrationClass: 'oai-compat-pure',\n // 2026-04-29: live probe api.v0.dev/v1/models returns 404 (Not Found —\n // {\"error\":{\"type\":\"not_found_error\"}}). v0 has no models-listing endpoint;\n // their public docs (v0.app/docs/api/platform/overview) describe a chats\n // / projects / deployments shape, not a model selector. Falling back to\n // pinned list of advertised SKUs. Note: v0 is also exposed via Vercel AI\n // Gateway as `vercel/v0-...` — but the gateway listing today shows zero\n // vercel-owned entries, so the direct row is the canonical reach.\n integrationMode: 'execution-only',\n baseUrl: 'https://api.v0.dev/v1',\n authScheme: 'bearer',\n apiKeyEnvVar: 'V0_API_KEY',\n supports: {\n chat: true,\n streaming: true,\n tools: true,\n },\n pinnedFallback: {\n reason: 'no-list-endpoint',\n // Explicit capabilities (not bare strings) so the CI invariant\n // `pinnedFallback-capability-coverage` passes — `v0-*` ids don't match\n // any built-in name-regex, so bare strings would fail the coverage check.\n models: [\n { id: 'v0-1.5-md', capabilities: ['chat', 'streaming', 'tool_use', 'code_generation'] },\n { id: 'v0-1.5-sm', capabilities: ['chat', 'streaming', 'code_generation'] },\n { id: 'v0-1.0-md', capabilities: ['chat', 'streaming', 'code_generation'] },\n ],\n lastReviewedAt: '2026-04-29',\n },\n capabilityHints: [\n {\n capability: 'frontend_code_generation',\n rationale: 'provider-class-default',\n confidence: 0.8,\n },\n ],\n pricingMode: 'none',\n enabledByDefault: true,\n priority: 20,\n docsUrl: 'https://v0.app/docs/api/platform/overview',\n notes:\n 'Specialized in frontend/UI code generation. No /v1/models endpoint — pinnedFallback enumerates the published SKUs.',\n lastReviewedAt: '2026-04-29',\n },\n {\n providerId: 'vercel-ai-gateway',\n displayName: 'Vercel AI Gateway',\n providerFamily: 'vercel-ai-gateway',\n aliases: ['vercel_ai_gateway', 'vercel-gateway'],\n integrationClass: 'gateway',\n integrationMode: 'discovery+execution',\n baseUrl: 'https://ai-gateway.vercel.sh/v1',\n authScheme: 'bearer',\n apiKeyEnvVar: 'VERCEL_AI_GATEWAY_API_KEY',\n adapterClass: 'VercelAIGatewayAdapter',\n paths: {\n // Vercel implements the OpenAI surface verbatim — confirmed against\n // https://vercel.com/docs/ai-gateway/capabilities/image-generation/openai\n // (\"Image-only models use the OpenAI Images API (`/v1/images/generations`)\n // for specialized image creation\"). Multimodal LLMs that generate images\n // (Nano Banana, GPT-5 image variants) use `/v1/chat/completions` with\n // images returned in the response's `images` array. Embeddings follow\n // the OpenAI default. Topology: baseUrl already includes `/v1`, so\n // paths here are relative to that.\n //\n // VIDEO: deliberately omitted. The video docs (2026-04-29) only show\n // AI SDK's `experimental_generateVideo` — no published REST shape.\n // When Vercel publishes a REST endpoint, add `videoGenerate` here.\n chatCompletions: '/chat/completions',\n embeddings: '/embeddings',\n imagesGenerate: '/images/generations',\n },\n supports: {\n chat: true,\n embeddings: true,\n streaming: true,\n tools: true,\n jsonMode: true,\n vision: true,\n // 2026-04-29: align with Vercel's capability matrix. Gateway proxies\n // image (Recraft, BFL, Topaz routes), video (Runway, Topaz), reasoning\n // (o1/o3/Claude-thinking), and exposes web-search via the Responses\n // proxy when the upstream supports it. `rerank`/`moderation` follow\n // when the upstream model declares them via owned_by.\n imageGeneration: true,\n // 2026-07-17: videoGeneration REMOVED — it contradicted the VIDEO note\n // in `paths` above (\"deliberately omitted... no published REST shape\")\n // and made all 21 vercel video-tagged models enter the execution pool\n // just to 404 on /v1/videos/generations (live sweep re-confirmed: 404\n // on both /videos/generations and /videos). Re-add together with a\n // paths.videoGenerate when Vercel publishes a REST video endpoint.\n reasoning: true,\n rerank: true,\n moderation: true,\n },\n capabilityHints: [\n // `web_search` is not a `supports` flag (catalog enum stops at modality\n // flags); surface it via capabilityHint so the merger picks it up for\n // OpenAI/xAI/Perplexity routes that expose it through the gateway.\n // Rationale `docs-declared`: vercel.com/docs/ai-gateway/capabilities/web-search.\n { capability: 'web_search', rationale: 'docs-declared', confidence: 0.6 },\n ],\n originalProviderField: 'owned_by',\n pricingMode: 'remote',\n enabledByDefault: true,\n priority: 35,\n docsUrl: 'https://vercel.com/docs/ai-gateway',\n notes:\n 'Gateway routing to OpenAI/Anthropic/xAI/etc. Models namespaced as `provider/model`; adapter exposes parseModelId() + attributeFromDiscovery() so the capability merger attributes to the real owner. Image/video/reasoning/rerank/moderation surface; paths.imagesGenerate=/images/generations matches the OAI Images endpoint. videoGeneration=true reflects model surface, NOT REST availability — Vercel video is AI-SDK-only as of 2026-04-29 (no published REST).',\n lastReviewedAt: '2026-04-29',\n },\n {\n // providerId is `wandb` (not `wandb-inference`) so the convention\n // `WANDB_API_KEY` holds — matches W&B's published SDK/CLI env var name.\n // The longer form lives in aliases for inbound normalization.\n providerId: 'wandb',\n displayName: 'Weights & Biases Inference',\n providerFamily: 'wandb',\n aliases: ['wandb-inference', 'weave'],\n integrationClass: 'oai-compat-quirks',\n integrationMode: 'discovery+execution',\n baseUrl: 'https://api.inference.wandb.ai/v1',\n authScheme: 'bearer',\n apiKeyEnvVar: 'WANDB_API_KEY',\n adapterClass: 'WandbAdapter',\n extraEnvVars: {\n WANDB_PROJECT: 'W&B project slug required on requests (header wandb-project)',\n },\n supports: {\n chat: true,\n streaming: true,\n },\n pricingMode: 'none',\n enabledByDefault: true,\n priority: 15,\n docsUrl: 'https://docs.wandb.ai/weave/quickstart-inference',\n notes:\n 'OAI-compatible chat surface. Dedicated adapter injects the required `wandb-project` header from $WANDB_PROJECT at request time (env hot-swap safe).',\n lastReviewedAt: '2026-04-22',\n },\n {\n providerId: 'voyage',\n displayName: 'Voyage AI',\n providerFamily: 'voyage',\n aliases: ['voyage_ai', 'voyageai'],\n integrationClass: 'embeddings-only',\n // Voyage does NOT expose GET /v1/models (confirmed HTTP 404 on live probe\n // 2026-04-22; cross-checked against docs.voyageai.com — the public API\n // reference covers only /embeddings, /multimodalembeddings, and /rerank).\n // Model identifiers are therefore sourced from the catalog's own\n // enumeration (see MongoDB-hosted Voyage \"Models Overview\" for the\n // canonical family list). Execution-only reflects that truthfully.\n integrationMode: 'execution-only',\n baseUrl: 'https://api.voyageai.com/v1',\n authScheme: 'bearer',\n apiKeyEnvVar: 'VOYAGE_API_KEY',\n adapterClass: 'VoyageAdapter',\n paths: {\n embeddings: '/embeddings',\n rerank: '/rerank',\n },\n // Voyage exposes no /models endpoint (execution-only). The pinnedFallback\n // list below covers the stable families as of 2026-04 per blog.voyageai.com\n // and mongodb.com/docs/voyageai/models:\n // - Voyage-4 series: shared embedding space, interchangeable.\n // - voyage-3 / voyage-3-lite retained as \"previous generation\" for\n // existing indexes; not removed yet per vendor.\n // - Domain-specific: finance-2, law-2, code-2, code-3, multilingual-2.\n // - Multimodal: voyage-multimodal-3.\n // - Rerankers: rerank-2, rerank-2-lite, rerank-2.5, rerank-2.5-lite.\n // Expand when Voyage publishes a new generation; remove deprecated IDs\n // only after the vendor end-of-life window closes.\n // Phase 4d (2026-04-28): renamed staticModels → pinnedFallback with\n // reason='no-list-endpoint' (HTTP 404 confirmed by live probe 2026-04-22).\n pinnedFallback: {\n // Operator-declared (root-cause refactor 2026-04-28). Voyage's surface\n // splits cleanly: voyage-* are embedding models (multimodal-3 also\n // accepts image input); rerank-* are retrieval rerankers. Declared\n // here so the catalog-bridge does NOT regex-infer rerank-* as chat\n // (the regex table previously matched them via `rerank-` only after\n // the moderation/transcription rules were ordered correctly).\n models: [\n { id: 'voyage-4-large', capabilities: ['embedding', 'embeddings'] },\n { id: 'voyage-4', capabilities: ['embedding', 'embeddings'] },\n { id: 'voyage-4-lite', capabilities: ['embedding', 'embeddings'] },\n { id: 'voyage-4-nano', capabilities: ['embedding', 'embeddings'] },\n { id: 'voyage-3', capabilities: ['embedding', 'embeddings'] },\n { id: 'voyage-3-lite', capabilities: ['embedding', 'embeddings'] },\n { id: 'voyage-code-3', capabilities: ['embedding', 'embeddings'] },\n { id: 'voyage-code-2', capabilities: ['embedding', 'embeddings'] },\n { id: 'voyage-finance-2', capabilities: ['embedding', 'embeddings'] },\n { id: 'voyage-law-2', capabilities: ['embedding', 'embeddings'] },\n { id: 'voyage-multilingual-2', capabilities: ['embedding', 'embeddings'] },\n {\n id: 'voyage-multimodal-3',\n capabilities: ['embedding', 'embeddings', 'multimodal', 'vision'],\n },\n { id: 'rerank-2.5', capabilities: ['reranking', 'retrieval'] },\n { id: 'rerank-2.5-lite', capabilities: ['reranking', 'retrieval'] },\n { id: 'rerank-2', capabilities: ['reranking', 'retrieval'] },\n { id: 'rerank-2-lite', capabilities: ['reranking', 'retrieval'] },\n ],\n reason: 'no-list-endpoint',\n lastReviewedAt: '2026-04-28',\n },\n supports: {\n embeddings: true,\n rerank: true,\n },\n capabilityHints: [\n { capability: 'rerank', rationale: 'endpoint-declared', confidence: 0.9 },\n { capability: 'long_context_embedding', rationale: 'docs-declared', confidence: 0.8 },\n ],\n pricingMode: 'none',\n enabledByDefault: true,\n priority: 60,\n docsUrl: 'https://docs.voyageai.com/docs/introduction',\n notes:\n 'Top-tier embeddings + rerank. voyage-3, voyage-code-3, voyage-rerank-2. Voyage does NOT expose GET /v1/models (confirmed 404 via live probe 2026-04-22); discovery is skipped, model IDs are resolved from the catalog/DB. Dedicated adapter implements /v1/embeddings + /v1/rerank and explicitly rejects chat calls.',\n lastReviewedAt: '2026-04-22',\n },\n {\n providerId: 'volcano',\n displayName: 'Volcano Engine (Volcengine)',\n providerFamily: 'volcano',\n aliases: ['volcengine', 'ark', 'bytedance'],\n integrationClass: 'oai-compat-quirks', // model = endpoint id, no bulk /models route\n integrationMode: 'discovery+execution',\n baseUrl: 'https://ark.cn-beijing.volces.com/api/v3',\n authScheme: 'bearer',\n apiKeyEnvVar: 'VOLCANO_API_KEY',\n adapterClass: 'VolcanoAdapter',\n supports: {\n chat: true,\n embeddings: true,\n streaming: true,\n tools: true,\n vision: true,\n },\n capabilityHints: [\n { capability: 'multilingual_chinese', rationale: 'provider-class-default', confidence: 0.85 },\n ],\n pricingMode: 'none',\n enabledByDefault: true,\n priority: 30,\n docsUrl: 'https://www.volcengine.com/docs/82379/1099455',\n notes:\n 'ByteDance/Volcano ARK — Doubao models. Strong Chinese coverage. ARK does NOT expose a bulk /models route — dedicated adapter returns empty getModels() and validates the ep-<timestamp>-<random> endpoint-id format.',\n lastReviewedAt: '2026-04-22',\n },\n {\n providerId: 'watsonx',\n displayName: 'IBM watsonx.ai',\n providerFamily: 'watsonx',\n aliases: ['ibm-watsonx', 'ibm_watsonx'],\n integrationClass: 'first-party-native',\n // Promoted from catalog-only → discovery+execution now that WatsonxAdapter\n // implements the IAM token exchange (+cache), x-watsonx-project-id header,\n // and /ml/v1/text/chat + /ml/v1/text/embeddings routes with version pin.\n integrationMode: 'discovery+execution',\n baseUrl: 'https://us-south.ml.cloud.ibm.com',\n authScheme: 'iam-token',\n apiKeyEnvVar: 'WATSONX_APIKEY',\n baseUrlEnvVar: 'WATSONX_URL',\n extraEnvVars: {\n WATSONX_PROJECT_ID: 'watsonx.ai project ID (required on all generate calls)',\n WATSONX_URL: 'Region-specific host (overrides baseUrl) — us-south, eu-de, jp-tok...',\n WATSONX_ZENAPIKEY: 'Alternative: ZenAPIKey for Cloud Pak for Data',\n },\n supports: {\n chat: true,\n embeddings: true,\n streaming: true,\n },\n pricingMode: 'none',\n enabledByDefault: true,\n priority: 25,\n docsUrl: 'https://www.ibm.com/watsonx/developer/',\n notes:\n 'IAM token exchange against https://iam.cloud.ibm.com/identity/token (grant_type=urn:ibm:params:oauth:grant-type:apikey). Requires WATSONX_APIKEY + WATSONX_PROJECT_ID. API version pinned to 2024-05-31.',\n adapterClass: 'WatsonxAdapter',\n lastReviewedAt: '2026-04-22',\n },\n {\n providerId: 'snowflake',\n displayName: 'Snowflake Cortex',\n providerFamily: 'snowflake',\n integrationClass: 'first-party-native',\n // Promoted from catalog-only → discovery+execution: SnowflakeCortexAdapter\n // (registered in default-adapter-factories) implements JWT key-pair auth,\n // account-scoped baseUrl substitution, and getModels() discovery.\n integrationMode: 'discovery+execution',\n baseUrl: 'https://snowflake.example.snowflakecomputing.com',\n authScheme: 'custom',\n apiKeyEnvVar: 'SNOWFLAKE_PAT',\n extraEnvVars: {\n SNOWFLAKE_ACCOUNT: 'Account identifier (orgname-accountname)',\n SNOWFLAKE_USER: 'Username for key-pair auth',\n SNOWFLAKE_WAREHOUSE: 'Compute warehouse (optional)',\n SNOWFLAKE_BASE_URL: 'Override for baseUrl — https://<account>.snowflakecomputing.com',\n },\n baseUrlEnvVar: 'SNOWFLAKE_BASE_URL',\n supports: {\n chat: true,\n streaming: true,\n embeddings: true,\n },\n pricingMode: 'none',\n enabledByDefault: true,\n priority: 20,\n docsUrl: 'https://docs.snowflake.com/en/user-guide/snowflake-cortex/cortex-rest-api',\n notes:\n 'JWT key-pair auth via SnowflakeCortexAdapter. Requires SNOWFLAKE_PAT + SNOWFLAKE_USER + SNOWFLAKE_ACCOUNT set together (account-scoped baseUrl). Adapter wired 2026-06-15.',\n adapterClass: 'SnowflakeCortexAdapter',\n lastReviewedAt: '2026-04-21',\n },\n {\n providerId: 'sap',\n displayName: 'SAP Generative AI Hub',\n providerFamily: 'sap',\n aliases: ['sap-ai-core', 'sap-genai'],\n integrationClass: 'first-party-native',\n // Promoted from catalog-only → discovery+execution: SapAiCoreAdapter\n // (registered in default-adapter-factories) implements OAuth2 client_credentials\n // token exchange, AI-Resource-Group header injection, and getModels() discovery.\n integrationMode: 'discovery+execution',\n baseUrl: 'https://api.ai.prod.eu-central-1.aws.ml.hana.ondemand.com',\n authScheme: 'oauth2',\n apiKeyEnvVar: 'SAP_AI_CORE_CLIENT_ID',\n extraEnvVars: {\n SAP_AI_CORE_CLIENT_SECRET: 'OAuth2 client secret',\n SAP_AI_CORE_AUTH_URL: 'OAuth2 token endpoint',\n SAP_AI_CORE_RESOURCE_GROUP: 'Resource group (default: default)',\n SAP_AI_CORE_BASE_URL: 'Region-specific base URL',\n },\n baseUrlEnvVar: 'SAP_AI_CORE_BASE_URL',\n supports: {\n chat: true,\n streaming: true,\n embeddings: true,\n },\n pricingMode: 'none',\n enabledByDefault: true,\n priority: 15,\n docsUrl: 'https://help.sap.com/docs/sap-ai-core',\n notes:\n 'OAuth2 client_credentials via SapAiCoreAdapter. Requires SAP_AI_CORE_CLIENT_ID + SAP_AI_CORE_CLIENT_SECRET + SAP_AI_CORE_AUTH_URL set together (+ optional resource group). Adapter wired 2026-06-15.',\n adapterClass: 'SapAiCoreAdapter',\n lastReviewedAt: '2026-04-21',\n },\n\n // ──────────────────────────────────────────────────────────────────────────\n // LOTE C — Image/Video specialties\n // ──────────────────────────────────────────────────────────────────────────\n {\n providerId: 'recraft',\n displayName: 'Recraft',\n providerFamily: 'recraft',\n integrationClass: 'image-only',\n integrationMode: 'execution-only', // no /models endpoint\n baseUrl: 'https://external.api.recraft.ai/v1',\n authScheme: 'bearer',\n apiKeyEnvVar: 'RECRAFT_API_KEY',\n adapterClass: 'RecraftAdapter',\n paths: {\n imagesGenerate: '/images/generations',\n },\n pinnedFallback: {\n // Operator-declared (root-cause refactor 2026-04-28). Recraft v3/v2\n // are pure image-generation models — no /models endpoint exists, so\n // the catalog row IS the source of truth.\n models: [\n { id: 'recraftv3', capabilities: ['image_generation', 'image_editing'] },\n { id: 'recraftv2', capabilities: ['image_generation'] },\n ],\n reason: 'no-list-endpoint',\n lastReviewedAt: '2026-04-28',\n },\n supports: {\n imageGeneration: true,\n },\n capabilityHints: [\n { capability: 'vector_image_generation', rationale: 'docs-declared', confidence: 0.85 },\n { capability: 'brand_style_consistency', rationale: 'docs-declared', confidence: 0.75 },\n ],\n pricingMode: 'none',\n enabledByDefault: true,\n priority: 30,\n docsUrl: 'https://www.recraft.ai/docs/api-reference/getting-started',\n notes:\n 'Vector + raster image gen with style controls. Dedicated adapter validates model × style pairs before the wire (v3 has vector families + any, v2 is raster-only).',\n lastReviewedAt: '2026-04-22',\n },\n {\n providerId: 'runwayml',\n displayName: 'RunwayML',\n providerFamily: 'runwayml',\n integrationClass: 'video-only',\n integrationMode: 'execution-only',\n baseUrl: 'https://api.dev.runwayml.com',\n authScheme: 'bearer',\n apiKeyEnvVar: 'RUNWAYML_API_KEY',\n adapterClass: 'RunwayMLAdapter',\n extraHeaders: {\n 'X-Runway-Version': '2024-11-06',\n },\n paths: {\n videoGenerate: '/v1/image_to_video',\n },\n pinnedFallback: {\n // Operator-declared (root-cause refactor 2026-04-28). Runway gen3*\n // are video generation slots; act-one is video-to-video character\n // performance transfer. No /models endpoint, so these are authoritative.\n models: [\n {\n id: 'gen3a_turbo',\n capabilities: ['video_generation', 'image_to_video'],\n },\n {\n id: 'gen3_alpha',\n capabilities: ['video_generation', 'image_to_video'],\n },\n { id: 'act-one', capabilities: ['video_generation', 'video_to_video'] },\n ],\n reason: 'no-list-endpoint',\n lastReviewedAt: '2026-04-28',\n },\n supports: {\n videoGeneration: true,\n imageGeneration: true,\n },\n capabilityHints: [\n { capability: 'image_to_video', rationale: 'docs-declared', confidence: 0.9 },\n ],\n pricingMode: 'none',\n enabledByDefault: true,\n priority: 35,\n docsUrl: 'https://docs.dev.runwayml.com/',\n notes:\n 'Video-from-image + act-one motion transfer. Requires X-Runway-Version header. Async-job API: POST /v1/image_to_video → poll GET /v1/tasks/{id} until terminal status. Dedicated adapter owns the polling budget.',\n lastReviewedAt: '2026-04-22',\n },\n {\n providerId: 'topaz',\n displayName: 'Topaz Labs',\n providerFamily: 'topaz',\n integrationClass: 'image-only',\n // 2026-05-06: flipped catalog-only → execution-only. The TopazImageAdapter\n // exists at providers/topaz/topaz-adapter.ts AND is wired in the factory\n // registry (see default-adapter-factories.ts: registerAdapterFactory\n // 'TopazImageAdapter'). Topaz exposes no /models listing endpoint —\n // pinnedFallback below is the canonical inventory.\n integrationMode: 'execution-only',\n baseUrl: 'https://api.topazlabs.com/image/v1',\n authScheme: 'api-key-header',\n authHeaderName: 'X-API-Key',\n apiKeyEnvVar: 'TOPAZ_API_KEY',\n pinnedFallback: {\n // Topaz Image API exposes a fixed roster of image-enhance pipelines\n // (upscale, denoise, sharpen, recovery). The 'standard'/'high-fidelity'\n // pair is the canonical chat-routable surface; expand when the API\n // grows new model variants.\n models: [\n { id: 'standard', capabilities: ['image_upscale', 'image_editing', 'image_denoise'] },\n { id: 'high-fidelity', capabilities: ['image_upscale', 'image_editing'] },\n ],\n reason: 'no-list-endpoint',\n lastReviewedAt: '2026-05-06',\n },\n supports: {\n imageEditing: true,\n },\n capabilityHints: [\n { capability: 'image_upscale', rationale: 'docs-declared', confidence: 0.9 },\n { capability: 'image_denoise', rationale: 'docs-declared', confidence: 0.85 },\n ],\n pricingMode: 'none',\n enabledByDefault: true,\n priority: 20,\n docsUrl: 'https://www.topazlabs.com/api',\n notes: 'Image upscale/enhance only — async job API. Wired via TopazImageAdapter factory.',\n adapterClass: 'TopazImageAdapter',\n lastReviewedAt: '2026-05-06',\n },\n {\n // providerId is `bfl` (not `black-forest-labs`) so the convention\n // `BFL_API_KEY` holds — matches BFL's published env var name. The full\n // brand name stays in `displayName` + `aliases` for humans + inbound norm.\n providerId: 'bfl',\n displayName: 'Black Forest Labs (FLUX)',\n providerFamily: 'bfl',\n aliases: ['black-forest-labs', 'flux'],\n integrationClass: 'image-only',\n integrationMode: 'execution-only',\n baseUrl: 'https://api.bfl.ai/v1',\n authScheme: 'api-key-header',\n authHeaderName: 'x-key',\n apiKeyEnvVar: 'BFL_API_KEY',\n pinnedFallback: {\n // Operator-declared (root-cause refactor 2026-04-28). Black Forest Labs\n // FLUX is image-generation + edit. The pro-1.1-ultra variant adds\n // higher-fidelity output but the surface is identical.\n models: [\n { id: 'flux-pro-1.1', capabilities: ['image_generation', 'image_editing'] },\n { id: 'flux-pro', capabilities: ['image_generation', 'image_editing'] },\n { id: 'flux-dev', capabilities: ['image_generation'] },\n { id: 'flux-schnell', capabilities: ['image_generation'] },\n {\n id: 'flux-pro-1.1-ultra',\n capabilities: ['image_generation', 'image_editing'],\n },\n ],\n reason: 'no-list-endpoint',\n lastReviewedAt: '2026-04-28',\n },\n supports: {\n imageGeneration: true,\n imageEditing: true,\n },\n pricingMode: 'none',\n enabledByDefault: true,\n priority: 35,\n docsUrl: 'https://docs.bfl.ai/',\n notes:\n 'FLUX family. Uses x-key header (not Bearer). Async-job protocol — submit → poll → download.',\n adapterClass: 'BflAdapter',\n lastReviewedAt: '2026-04-29',\n },\n {\n // 302.AI — OAI-compatible aggregator at api.302.ai/v1. Migrated out of\n // the provider-registry.ts switch in 2026-04-22 (residue-closure phase).\n //\n // ── Why providerId is `ai302` and not `302ai` ───────────────────────────\n // Our providerId regex requires a leading alpha character\n // (`/^[a-z][a-z0-9]*.../`). The user-facing name \"302ai\" begins with a\n // digit, so it can't be a canonical providerId. We keep `302ai` as an\n // alias so historical `config.providers[].name === '302ai'` still resolves\n // and users don't need to migrate their configs.\n //\n // Live-probed 2026-04-22 via GCP secret `ailin-302-key`: upstream returned\n // HTTP 401 with `Insufficient account balance`. That means auth + routing\n // are correct (the token was accepted) — the account simply has zero credit.\n // Classification: \"integrated, live-auth-accepted, balance-exhausted\".\n providerId: 'ai302',\n displayName: '302.AI',\n providerFamily: 'ai302',\n aliases: ['302ai', '302-ai', '302'],\n integrationClass: 'oai-compat-pure',\n integrationMode: 'discovery+execution',\n baseUrl: 'https://api.302.ai/v1',\n authScheme: 'bearer',\n apiKeyEnvVar: 'AI302_API_KEY',\n supports: {\n chat: true,\n streaming: true,\n tools: true,\n embeddings: true,\n imageGeneration: true,\n },\n pricingMode: 'none',\n enabledByDefault: true,\n priority: 8,\n docsUrl: 'https://302.ai/',\n notes:\n 'OAI-compatible aggregator. Models-list endpoint works when account has balance. Historical user config `{name: \"302ai\"}` still resolves via the alias table.',\n lastReviewedAt: '2026-04-22',\n },\n {\n // Replicate — first-party-native predictions API. Model versions (not names)\n // drive execution. Sync predictions via `Prefer: wait` header. The adapter\n // existed for months before the catalog migration and is fully functional;\n // this row is the missing factory registration that lets it be resolved.\n providerId: 'replicate',\n displayName: 'Replicate',\n providerFamily: 'replicate',\n integrationClass: 'first-party-native',\n integrationMode: 'execution-only',\n baseUrl: 'https://api.replicate.com/v1',\n baseUrlEnvVar: 'REPLICATE_BASE_URL',\n authScheme: 'bearer',\n apiKeyEnvVar: 'REPLICATE_API_KEY',\n supports: {\n chat: true,\n streaming: true,\n imageGeneration: true,\n textToSpeech: true,\n speechToText: true,\n },\n pricingMode: 'none',\n enabledByDefault: true,\n priority: 15,\n // Replicate's /v1/models endpoint returns thousands of public predictions,\n // most of which are non-LLM or private to their authors. A wholesale\n // discovery import is worse than a curated canonical list, so this stays\n // execution-only with a deliberate shortlist. Expand when product wants\n // additional Replicate-hosted families on the benchmark grid.\n // Phase 4d (2026-04-28): renamed staticModels → pinnedFallback with\n // reason='curated-shortlist' — the dedicated reason for \"endpoint works\n // but signal-to-noise ratio favours a hand-picked list\".\n pinnedFallback: {\n // Operator-declared (root-cause refactor 2026-04-28). Replicate routes\n // by `owner/name` slug — capabilities are model-specific, NOT inferable\n // from the slug alone (claude-3.5-sonnet is chat+vision; whisper is\n // STT; flux is image; stable-diffusion is image). Declared here so the\n // catalog-bridge does not regress to regex inference, which would\n // mistype `openai/whisper` (whisper-prefixed) as STT-only and miss\n // claude's vision capability entirely.\n models: [\n {\n id: 'anthropic/claude-3.5-sonnet',\n capabilities: [\n 'chat',\n 'streaming',\n 'vision',\n 'multimodal',\n 'function_calling',\n 'tool_use',\n 'reasoning',\n ],\n },\n {\n id: 'black-forest-labs/flux-pro',\n capabilities: ['image_generation', 'image_editing'],\n },\n {\n id: 'black-forest-labs/flux-schnell',\n capabilities: ['image_generation'],\n },\n {\n id: 'meta/meta-llama-3-70b-instruct',\n capabilities: ['chat', 'streaming', 'function_calling'],\n },\n {\n id: 'meta/meta-llama-3-8b-instruct',\n capabilities: ['chat', 'streaming'],\n },\n {\n id: 'mistralai/mistral-7b-instruct-v0.2',\n capabilities: ['chat', 'streaming'],\n },\n {\n id: 'openai/gpt-4o-mini',\n capabilities: [\n 'chat',\n 'streaming',\n 'vision',\n 'multimodal',\n 'function_calling',\n 'json_mode',\n ],\n },\n {\n id: 'openai/whisper',\n capabilities: ['speech_to_text', 'transcription', 'audio'],\n },\n {\n id: 'stability-ai/stable-diffusion-3',\n capabilities: ['image_generation'],\n },\n ],\n reason: 'curated-shortlist',\n lastReviewedAt: '2026-04-28',\n },\n docsUrl: 'https://replicate.com/docs',\n notes:\n 'Predictions API (async-by-default, sync via Prefer: wait). Models are invoked as owner/name or owner/name:version. Adapter handles SSE streams for LLM models and downloads output URLs for image/audio models.',\n adapterClass: 'ReplicateAdapter',\n lastReviewedAt: '2026-04-22',\n },\n {\n // Bytez — multi-modality hub with a quirky OAI-compat surface.\n // apiKeyEnvVar wired to GCP secret ailin-bytez-key (2026-04-22).\n //\n // URL LAYOUT (documented, confirmed by docs.bytez.com/http-reference):\n // Chat (OAI-compat): POST https://api.bytez.com/models/v2/openai/v1/chat/completions\n // Models list (native shape, NOT OAI):\n // GET https://api.bytez.com/models/v2/list/models?task=chat\n // Response: { error, output: [{ modelId, task, meter, meterPrice, params, ramRequired }] }\n // Auth header format: `Authorization: <token>` (sample in docs omits\n // the `Bearer ` prefix; the hub adapter currently sends `Bearer <token>`,\n // which several OAI-compat gateways also accept — verify during the\n // first live probe).\n //\n // Classification consequences:\n // - `oai-compat-quirks` (not `-pure`) because the OAI path is nested\n // under `/models/v2/openai/v1` instead of the customary `/v1`.\n // - `discovery+execution` (Phase 4d, 2026-04-28): the dedicated\n // `BytezNativeModelFetcher` consumes `/models/v2/list/models`\n // (non-OAI shape `{error, output:[{modelId,task,...}]}`) and\n // transforms it into the discovery pipeline's `ProviderModel`\n // shape. This unlocks Bytez's full ~100k-model HuggingFace surface\n // instead of the 4-row hand-picked list that previously seeded\n // execution-only mode.\n // - Image/speech modalities ride on separate native endpoints (see\n // docs.bytez.com/http-reference/examples/open-source/*) and are\n // intentionally NOT in `supports` here — the OAI hub adapter can't\n // route them without modality-specific transforms. The native\n // fetcher still surfaces those model IDs for visibility, but\n // execution requires the modality-specific transforms to land.\n providerId: 'bytez',\n displayName: 'Bytez',\n providerFamily: 'bytez',\n integrationClass: 'oai-compat-quirks',\n integrationMode: 'discovery+execution',\n baseUrl: 'https://api.bytez.com/models/v2/openai/v1',\n baseUrlEnvVar: 'BYTEZ_BASE_URL',\n authScheme: 'bearer',\n apiKeyEnvVar: 'BYTEZ_API_KEY',\n supports: {\n chat: true,\n embeddings: true,\n streaming: true,\n },\n pricingMode: 'none',\n enabledByDefault: true,\n priority: 12,\n docsUrl: 'https://docs.bytez.com/http-reference/oaiCompliant/chatCompletions',\n notes:\n 'Multi-modality hub over HuggingFace + custom models. OAI-compat chat/embeddings live at /models/v2/openai/v1/... — NOT /v1/... (quirk). Native discovery GET /models/v2/list/models returns non-OAI shape {error, output:[{modelId,task,...}]}; consumed by BytezNativeModelFetcher (Phase 4d 2026-04-28 — was execution-only with 4 staticModels). Confirmed 2026-04-22 via docs + live probe (old /v1 baseUrl returned HTTP 404 on every path).',\n adapterClass: 'BytezAdapter',\n fetcherClass: 'BytezNativeModelFetcher',\n lastReviewedAt: '2026-04-28',\n },\n {\n // Inworld AI — router + TTS/STT specialist with an OAI-compat chat surface.\n // Orphan-closure entry (2026-04-22): InworldAdapter existed on disk but was\n // never registered in the catalog or provider-registry, so the class was\n // never instantiated. This row + the InworldAdapter factory registration\n // in default-adapter-factories.ts close the orphan.\n //\n // QUIRK 1 — Basic auth (not Bearer).\n // The INWORLD_API_KEY is already base64-encoded (132 chars in GCP). The\n // adapter passes `authScheme: 'Basic'` into its hub metadata so the hub's\n // buildRequestHeaders() prepends `Basic ` (not `Bearer `) at every HTTP\n // site. Catalog authScheme is 'custom' because the schema enum has no\n // discrete 'basic' value — the catalog surface is auth-agnostic and the\n // adapter encodes the concrete scheme.\n //\n // QUIRK 2 — discovery broken on both documented paths.\n // Live probe 2026-04-22:\n // GET https://api.inworld.ai/router/v1/models → HTTP 404\n // GET https://api.inworld.ai/v1/models → HTTP 404\n // Upstream docs at https://docs.inworld.ai don't publish a discovery\n // endpoint; the canonical router surface only exposes chat. So this\n // entry is `execution-only` with staticModels — the discovery merger\n // must not probe HTTP for this provider.\n //\n // QUIRK 3 — provider-prefixed model IDs.\n // Inworld's router exposes upstream model IDs verbatim with their\n // family prefix (openai/gpt-4o-mini, anthropic/claude-3-haiku, etc.),\n // which is why the catalog can enumerate a short list without caring\n // about Inworld's private inventory — the adapter forwards the id.\n //\n // QUIRK 4 — TTS/STT/voice-clone endpoints on the same adapter.\n // InworldAdapter overrides textToSpeech/speechToText + adds\n // cloneVoice() on top of the hub. Exposing those in `supports` is\n // deferred — the CapabilityHintSchema doesn't yet carve out voice-clone,\n // and speech tests against a metered endpoint burn quota. Until a\n // capability-level audit happens, `supports.chat` is the safe minimum.\n providerId: 'inworld',\n displayName: 'Inworld AI',\n providerFamily: 'inworld',\n aliases: ['inworld_ai'],\n integrationClass: 'oai-compat-quirks',\n integrationMode: 'execution-only',\n baseUrl: 'https://api.inworld.ai',\n baseUrlEnvVar: 'INWORLD_BASE_URL',\n authScheme: 'custom',\n apiKeyEnvVar: 'INWORLD_API_KEY',\n adapterClass: 'InworldAdapter',\n supports: {\n chat: true,\n streaming: true,\n },\n // Minimum viable pinned fallback — live-probed 2026-04-22 at\n // POST /v1/chat/completions { model: 'openai/gpt-4o-mini', ... }\n // returned HTTP 200 with a valid ChatCompletion (\"Pong!\"). Additional\n // families are listed by provider-prefix convention; the router accepts\n // any upstream model ID it has a route for, so this list is\n // intentionally illustrative rather than exhaustive.\n // Phase 4d (2026-04-28): renamed staticModels → pinnedFallback with\n // reason='no-list-endpoint' (router/v1/models AND /v1/models both 404).\n pinnedFallback: {\n // Operator-declared (root-cause refactor 2026-04-28). Inworld routes\n // upstream models verbatim; capabilities follow the upstream family.\n // GPT-4o + claude-3-5-sonnet carry vision/multimodal; the rest are\n // text-only chat at the time of pinning.\n models: [\n {\n id: 'openai/gpt-4o-mini',\n capabilities: [\n 'chat',\n 'streaming',\n 'vision',\n 'multimodal',\n 'function_calling',\n 'json_mode',\n ],\n },\n {\n id: 'openai/gpt-4o',\n capabilities: [\n 'chat',\n 'streaming',\n 'vision',\n 'multimodal',\n 'function_calling',\n 'json_mode',\n ],\n },\n {\n id: 'anthropic/claude-3-haiku',\n capabilities: ['chat', 'streaming', 'function_calling'],\n },\n {\n id: 'anthropic/claude-3-5-sonnet',\n capabilities: [\n 'chat',\n 'streaming',\n 'vision',\n 'multimodal',\n 'function_calling',\n 'tool_use',\n 'reasoning',\n ],\n },\n {\n id: 'mistralai/mistral-large',\n capabilities: ['chat', 'streaming', 'function_calling', 'json_mode'],\n },\n {\n id: 'meta-llama/llama-3.1-70b-instruct',\n capabilities: ['chat', 'streaming', 'function_calling'],\n },\n ],\n reason: 'no-list-endpoint',\n lastReviewedAt: '2026-04-28',\n },\n pricingMode: 'none',\n enabledByDefault: true,\n priority: 10,\n docsUrl: 'https://docs.inworld.ai',\n notes:\n 'OAI-compat chat router + TTS/STT/voice-clone. Basic auth (key already base64-encoded in GCP). Discovery endpoints both return 404 (router/v1/models and /v1/models) — catalog supplies model IDs. Orphan-closure entry: the InworldAdapter class existed but was never wired until 2026-04-22.',\n lastReviewedAt: '2026-04-22',\n },\n {\n // Cloudflare Workers AI — OAI-compatible surface on CF's edge, with an\n // account-scoped URL that the hub can't template declaratively. The\n // dedicated adapter substitutes CLOUDFLARE_ACCOUNT_ID into the baseUrl\n // at construction time. Added 2026-04-22.\n providerId: 'cloudflare-workers-ai',\n displayName: 'Cloudflare Workers AI',\n providerFamily: 'cloudflare',\n aliases: ['cloudflare', 'workers-ai', 'cf-workers-ai'],\n integrationClass: 'oai-compat-pure',\n integrationMode: 'discovery+execution',\n // Placeholder — real URL is constructed by the adapter from CLOUDFLARE_ACCOUNT_ID.\n baseUrl: 'https://api.cloudflare.com/client/v4/accounts/{account_id}/ai/v1',\n authScheme: 'bearer',\n apiKeyEnvVar: 'CLOUDFLARE_API_TOKEN',\n apiKeyEnvVarOverrideReason:\n 'CLOUDFLARE_API_TOKEN is the canonical CF-wide env (Cloudflare SDK, wrangler, R2/Workers/DNS). CLOUDFLARE_WORKERS_AI_API_KEY would force users to double-set the same account-scoped token.',\n extraEnvVars: {\n CLOUDFLARE_ACCOUNT_ID:\n 'Cloudflare account ID — substituted into the Workers AI baseUrl path segment at adapter construction time. Required.',\n },\n supports: {\n chat: true,\n embeddings: true,\n streaming: true,\n tools: true,\n },\n pricingMode: 'none',\n enabledByDefault: true,\n priority: 20,\n docsUrl: 'https://developers.cloudflare.com/workers-ai/configuration/open-ai-compatibility/',\n notes:\n \"OAI-compatible surface at account-scoped URL. Model ids use @cf/vendor/name convention (e.g. @cf/meta/llama-3-8b-instruct). Workers AI has dedicated per-account neuron rate limits that do NOT share with the account's overall CF API quota — diagnose 429s via cf-ray + x-ratelimit-* response headers.\",\n adapterClass: 'CloudflareWorkersAIAdapter',\n lastReviewedAt: '2026-04-22',\n },\n {\n // HuggingFace Inference Providers — OAI-compatible router that fans out\n // to TogetherAI, Fireworks, Replicate, SambaNova, Cerebras, Nebius, etc.\n // on the user's behalf using HF_TOKEN. Uniquely valuable because HF billing\n // deducts from a single source even when inference is physically served\n // by third-party providers.\n providerId: 'huggingface',\n displayName: 'Hugging Face Inference',\n providerFamily: 'huggingface',\n aliases: ['hf', 'hf-inference', 'hf-inference-providers', 'huggingface-inference'],\n integrationClass: 'oai-compat-pure',\n integrationMode: 'discovery+execution',\n baseUrl: 'https://router.huggingface.co/v1',\n baseUrlEnvVar: 'HUGGINGFACE_BASE_URL',\n authScheme: 'bearer',\n apiKeyEnvVar: 'HF_TOKEN',\n apiKeyEnvVarOverrideReason:\n 'HF_TOKEN is the canonical env used by the HF CLI, huggingface_hub SDK, and every HF tutorial. HUGGINGFACE_API_KEY would double-set the same token across the hf-login cache and this app.',\n supports: {\n chat: true,\n embeddings: true,\n streaming: true,\n tools: true,\n },\n pricingMode: 'none',\n enabledByDefault: true,\n priority: 18,\n docsUrl: 'https://huggingface.co/docs/inference-providers/index',\n notes:\n 'Router that dispatches to TogetherAI/Fireworks/Replicate/etc. using a single HF_TOKEN. Single-tenant billing across heterogeneous backends. Uses dedicated adapter to inject `x-use-cache: false` for benchmark runs (hub default is a cache hit which ruins timing measurement).',\n adapterClass: 'HuggingFaceInferenceAdapter',\n lastReviewedAt: '2026-04-22',\n },\n\n // ──────────────────────────────────────────────────────────────────────────\n // LOTE C.1 — Enterprise hyperscaler gateways (Batch 6)\n //\n // Major-cloud-hosted OpenAI-compatible endpoints with non-trivial URL\n // composition. Each needs a dedicated adapter because the catalog's\n // declarative baseUrl field can't express per-tenant path substitution.\n // Pattern mirrors Cloudflare Workers AI (Batch 5) — factory-time resolution\n // of tenant-scoped variables into a concrete baseUrl; fail-soft sentinel URL\n // when config is missing so unrelated providers can still boot.\n // ──────────────────────────────────────────────────────────────────────────\n {\n // Azure OpenAI — deployment-scoped URL with api-version query string.\n // Most-deployed enterprise LLM gateway on the planet. Single adapter\n // instance per deployment — multi-deployment workspaces register N\n // instances via AZURE_OPENAI_DEPLOYMENTS env (factory handles expansion\n // as a future enhancement; MVP is single-deployment via AZURE_OPENAI_DEPLOYMENT).\n providerId: 'azure-openai',\n displayName: 'Azure OpenAI',\n providerFamily: 'azure-openai',\n aliases: ['azure', 'aoai', 'microsoft-azure-openai'],\n integrationClass: 'oai-compat-quirks',\n integrationMode: 'execution-only',\n // Placeholder — real URL is constructed by the adapter from\n // AZURE_OPENAI_RESOURCE_NAME + AZURE_OPENAI_DEPLOYMENT + AZURE_OPENAI_API_VERSION.\n baseUrl: 'https://{resource_name}.openai.azure.com/openai/deployments/{deployment}',\n authScheme: 'custom',\n apiKeyEnvVar: 'AZURE_OPENAI_API_KEY',\n extraEnvVars: {\n AZURE_OPENAI_RESOURCE_NAME:\n 'Azure resource subdomain (before `.openai.azure.com`). Required unless AZURE_OPENAI_ENDPOINT is set.',\n AZURE_OPENAI_DEPLOYMENT:\n 'Azure deployment alias (admin-chosen at deploy time). Required — Azure URLs embed the deployment as a path segment.',\n AZURE_OPENAI_API_VERSION:\n 'API version stamp (e.g. 2024-10-21). Required on every request as query string. Defaults to a known-GA version if unset.',\n AZURE_OPENAI_ENDPOINT:\n 'Full endpoint override for sovereign clouds (.openai.azure.us, .openai.azure.cn) or private-link FQDNs. Takes precedence over AZURE_OPENAI_RESOURCE_NAME.',\n },\n supports: {\n chat: true,\n embeddings: true,\n streaming: true,\n tools: true,\n jsonMode: true,\n vision: true, // GPT-4o deployments\n imageGeneration: true, // DALL-E 3 deployments\n textToSpeech: true,\n speechToText: true,\n },\n pricingMode: 'remote',\n enabledByDefault: true,\n priority: 35,\n // Azure deployments are admin-named aliases that wrap canonical OpenAI\n // models. We list the canonical OpenAI identities here — the adapter\n // resolves the runtime deployment alias via AZURE_OPENAI_DEPLOYMENT, but\n // the benchmark grid wants to know \"is gpt-4o available through Azure?\",\n // not \"is {arbitrary-alias} available\". Azure's convention uses `gpt-35`\n // (not `gpt-3.5`) in deployment URLs — keep both spellings here so\n // operators who mirror either convention resolve.\n // Phase 4d (2026-04-28): renamed staticModels → pinnedFallback with\n // reason='per-deployment' — there is no global Azure /models endpoint;\n // the listing is per-deployment and operator-scoped.\n pinnedFallback: {\n // Operator-declared (root-cause refactor 2026-04-28). Azure mirrors\n // the canonical OpenAI families — chat (gpt-*), reasoning (o1-*),\n // and embeddings (text-embedding-*). Capabilities follow OpenAI's\n // declared surface for each family at the model's API version.\n models: [\n {\n id: 'gpt-35-turbo',\n capabilities: ['chat', 'streaming', 'function_calling', 'json_mode'],\n },\n {\n id: 'gpt-35-turbo-16k',\n capabilities: ['chat', 'streaming', 'function_calling', 'json_mode'],\n },\n {\n id: 'gpt-4',\n capabilities: ['chat', 'streaming', 'function_calling', 'json_mode'],\n },\n {\n id: 'gpt-4-turbo',\n capabilities: [\n 'chat',\n 'streaming',\n 'vision',\n 'multimodal',\n 'function_calling',\n 'json_mode',\n ],\n },\n {\n id: 'gpt-4o',\n capabilities: [\n 'chat',\n 'streaming',\n 'vision',\n 'multimodal',\n 'function_calling',\n 'json_mode',\n ],\n },\n {\n id: 'gpt-4o-mini',\n capabilities: [\n 'chat',\n 'streaming',\n 'vision',\n 'multimodal',\n 'function_calling',\n 'json_mode',\n ],\n },\n {\n id: 'o1-mini',\n capabilities: ['chat', 'streaming', 'reasoning', 'thinking_mode'],\n },\n {\n id: 'o1-preview',\n capabilities: ['chat', 'streaming', 'reasoning', 'thinking_mode'],\n },\n {\n id: 'text-embedding-3-large',\n capabilities: ['embedding', 'embeddings'],\n },\n {\n id: 'text-embedding-3-small',\n capabilities: ['embedding', 'embeddings'],\n },\n {\n id: 'text-embedding-ada-002',\n capabilities: ['embedding', 'embeddings'],\n },\n ],\n reason: 'per-deployment',\n lastReviewedAt: '2026-04-28',\n },\n docsUrl: 'https://learn.microsoft.com/en-us/azure/ai-services/openai/reference',\n notes:\n 'Deployment-scoped URL: POST https://{resource}.openai.azure.com/openai/deployments/{deployment}/chat/completions?api-version={v}. Uses `api-key` header (not Authorization: Bearer) for subscription keys; Microsoft Entra ID bearer tokens work via Authorization header. One adapter instance per deployment — operators with multiple deployments register multiple instances.',\n adapterClass: 'AzureOpenAIAdapter',\n lastReviewedAt: '2026-04-22',\n },\n {\n // Google AI Studio (Gemini) — OAI-compatible shim endpoint. Distinct\n // from the native `google` provider (which uses @google/generative-ai\n // SDK and native /v1beta/models/{m}:generateContent surface). This\n // entry is for third-party routers that want vanilla OAI-compat chat.\n providerId: 'gemini-openai',\n displayName: 'Google AI Studio (Gemini OAI)',\n providerFamily: 'google',\n aliases: ['google-ai-studio-openai', 'gemini-oai', 'gemini-openai-compat'],\n integrationClass: 'oai-compat-pure',\n integrationMode: 'discovery+execution',\n baseUrl: 'https://generativelanguage.googleapis.com/v1beta/openai',\n baseUrlEnvVar: 'GEMINI_OPENAI_BASE_URL',\n authScheme: 'bearer',\n // Google accepts either GEMINI_API_KEY or GOOGLE_AI_STUDIO_API_KEY — the\n // env loader maps both. Catalog declares the canonical one.\n apiKeyEnvVar: 'GEMINI_API_KEY',\n apiKeyEnvVarOverrideReason:\n 'GEMINI_API_KEY is canonical in ai.google.dev docs, @google/generative-ai SDK, and Google AI Studio. The native `google` adapter shares it — GEMINI_OPENAI_API_KEY would fork one secret twice.',\n supports: {\n chat: true,\n embeddings: true,\n streaming: true,\n tools: true,\n jsonMode: true,\n vision: true,\n },\n pricingMode: 'remote',\n enabledByDefault: true,\n priority: 30,\n docsUrl: 'https://ai.google.dev/gemini-api/docs/openai',\n notes:\n \"Drop-in OpenAI-compat shim at /v1beta/openai. Same API key as the native `google` adapter (GEMINI_API_KEY). Use this for uniform wire-protocol routing via LiteLLM/OpenRouter-style aggregators; use the native GoogleAdapter for vision/video/grounded-search features that the OAI shim doesn't expose.\",\n adapterClass: 'GeminiOpenAIAdapter',\n lastReviewedAt: '2026-04-22',\n },\n {\n // GitHub Models — Microsoft's PAT-auth aggregator that surfaces OpenAI,\n // Meta, Mistral, Cohere models under a single GH-account-scoped endpoint.\n // Positioned as developer playground; aggressive per-PAT rate limits.\n providerId: 'github-models',\n displayName: 'GitHub Models',\n providerFamily: 'github-models',\n aliases: ['github', 'gh-models', 'github-marketplace-models'],\n integrationClass: 'oai-compat-pure',\n integrationMode: 'discovery+execution',\n baseUrl: 'https://models.github.ai/inference',\n baseUrlEnvVar: 'GITHUB_MODELS_BASE_URL',\n authScheme: 'bearer',\n apiKeyEnvVar: 'GITHUB_TOKEN',\n apiKeyEnvVarOverrideReason:\n 'GITHUB_TOKEN is canonical for every GitHub surface — gh CLI, Actions, REST/GraphQL, Octokit. GitHub Models accepts the same PAT; GITHUB_MODELS_API_KEY would force a second PAT per account.',\n supports: {\n chat: true,\n embeddings: true,\n streaming: true,\n tools: true,\n jsonMode: true,\n vision: true, // gpt-4o + Llama Vision deployments\n },\n pricingMode: 'none',\n enabledByDefault: true,\n priority: 10,\n docsUrl: 'https://docs.github.com/en/github-models',\n // Model catalog lives at /catalog/models on the top-level API host, NOT\n // nested under this baseUrl's /inference path — and paths.modelList only\n // accepts relative paths (ProviderEndpointPathsSchema's pathString regex\n // requires a leading \"/\"), so it can't express a different host either.\n // Discovery is wired as a dedicated hardcoded source (github-models-native\n // in central-model-discovery-service.ts) instead of the generic\n // catalog-bridge path.\n notes:\n 'GitHub PAT-auth aggregator (OpenAI / Meta / Mistral / Cohere). Free tier has aggressive per-PAT rate limits (50 req/day for cheap models) — 429s are CALLER quota, not provider health. Model ids follow `{publisher}/{name}` (e.g. openai/gpt-4o).',\n adapterClass: 'GitHubModelsAdapter',\n lastReviewedAt: '2026-04-22',\n },\n {\n // Databricks Model Serving — workspace-scoped, endpoint-scoped URL.\n // Like Azure: one adapter instance per serving endpoint. Multi-endpoint\n // workspaces register multiple instances.\n providerId: 'databricks',\n displayName: 'Databricks Model Serving',\n providerFamily: 'databricks',\n aliases: ['databricks-model-serving', 'databricks-foundation-models'],\n integrationClass: 'oai-compat-pure',\n integrationMode: 'execution-only',\n // Placeholder — real URL is constructed by the adapter from\n // DATABRICKS_HOST + DATABRICKS_SERVING_ENDPOINT.\n baseUrl: 'https://{workspace_host}/serving-endpoints/{endpoint}',\n authScheme: 'bearer',\n apiKeyEnvVar: 'DATABRICKS_TOKEN',\n apiKeyEnvVarOverrideReason:\n 'DATABRICKS_TOKEN is canonical for the Databricks CLI, databricks-sdk-py, Terraform provider, and every example. DATABRICKS_API_KEY would fork a secret the SDK already reads by name.',\n extraEnvVars: {\n DATABRICKS_HOST:\n 'Databricks workspace hostname (e.g. my-co.cloud.databricks.com, dbc-abc123.cloud.databricks.com). Required.',\n DATABRICKS_SERVING_ENDPOINT:\n 'Serving endpoint name (admin-chosen). Required — the endpoint IS the model for this adapter instance.',\n },\n supports: {\n chat: true,\n embeddings: true,\n streaming: true,\n tools: true,\n },\n pricingMode: 'none',\n enabledByDefault: true,\n priority: 20,\n // Databricks Foundation Model APIs are named canonically regardless of\n // workspace — the `databricks-*` prefix is stable across every\n // workspace-region and bills through the same pay-per-token SKU. Admins\n // can ALSO create custom serving endpoints (arbitrary aliases), but those\n // are workspace-local; the benchmark grid only meaningfully references\n // the canonical FM APIs. Expand as Databricks ships new foundation SKUs.\n // Phase 4d (2026-04-28): renamed staticModels → pinnedFallback with\n // reason='workspace-scoped' — every Databricks /serving-endpoints listing\n // is workspace-private.\n pinnedFallback: {\n models: [\n // BGE = BAAI's bge-large-en — pure embedding model.\n {\n id: 'databricks-bge-large-en',\n capabilities: ['embedding', 'embeddings'],\n },\n // DBRX is Databricks' MoE chat model with native tool-use.\n {\n id: 'databricks-dbrx-instruct',\n capabilities: ['chat', 'streaming', 'function_calling'],\n },\n // GTE = Alibaba's general text embedding.\n {\n id: 'databricks-gte-large-en',\n capabilities: ['embedding', 'embeddings'],\n },\n // Llama 3.1/3.3 instruct: native tool-use, streaming chat.\n {\n id: 'databricks-meta-llama-3-1-405b-instruct',\n capabilities: ['chat', 'streaming', 'function_calling'],\n },\n {\n id: 'databricks-meta-llama-3-1-70b-instruct',\n capabilities: ['chat', 'streaming', 'function_calling'],\n },\n {\n id: 'databricks-meta-llama-3-3-70b-instruct',\n capabilities: ['chat', 'streaming', 'function_calling'],\n },\n // Mixtral 8x7B instruct: chat + streaming + tools.\n {\n id: 'databricks-mixtral-8x7b-instruct',\n capabilities: ['chat', 'streaming', 'function_calling'],\n },\n // MPT predates native tool-use; chat + streaming only.\n {\n id: 'databricks-mpt-30b-instruct',\n capabilities: ['chat', 'streaming'],\n },\n {\n id: 'databricks-mpt-7b-instruct',\n capabilities: ['chat', 'streaming'],\n },\n ],\n reason: 'workspace-scoped',\n lastReviewedAt: '2026-04-28',\n },\n docsUrl: 'https://docs.databricks.com/en/machine-learning/model-serving/',\n notes:\n 'Workspace + endpoint-scoped URL: POST https://{workspace}.cloud.databricks.com/serving-endpoints/{endpoint}/chat/completions. Endpoint names are admin-chosen aliases (e.g. databricks-llama-3-70b-instruct, databricks-meta-llama-3-3-70b-instruct). One adapter instance per serving endpoint — for multi-endpoint workspaces, register multiple instances.',\n adapterClass: 'DatabricksAdapter',\n lastReviewedAt: '2026-04-22',\n },\n\n // ──────────────────────────────────────────────────────────────────────────\n // LOTE D — Self-hosted / local inference\n // ──────────────────────────────────────────────────────────────────────────\n {\n providerId: 'vllm',\n displayName: 'vLLM (self-hosted)',\n providerFamily: 'vllm',\n integrationClass: 'self-hosted-oai-compat',\n integrationMode: 'discovery+execution',\n baseUrl: 'http://localhost:8000/v1',\n baseUrlEnvVar: 'VLLM_BASE_URL',\n authScheme: 'bearer',\n apiKeyEnvVar: 'VLLM_API_KEY',\n apiKeyOptional: true, // vLLM doesn't require auth by default\n supports: {\n chat: true,\n embeddings: true,\n streaming: true,\n tools: true,\n },\n pricingMode: 'none',\n enabledByDefault: true,\n priority: 5,\n docsUrl: 'https://docs.vllm.ai/en/latest/serving/openai_compatible_server.html',\n notes:\n 'Self-hosted OpenAI-compatible server. Set VLLM_BASE_URL to enable. Uses dedicated VllmAdapter for apiKeyOptional handling + named identity.',\n adapterClass: 'VllmAdapter',\n lastReviewedAt: '2026-04-22',\n },\n {\n providerId: 'lm-studio',\n displayName: 'LM Studio',\n providerFamily: 'lm-studio',\n aliases: ['lmstudio', 'lm_studio'],\n integrationClass: 'self-hosted-oai-compat',\n integrationMode: 'discovery+execution',\n baseUrl: 'http://localhost:1234/v1',\n baseUrlEnvVar: 'LM_STUDIO_BASE_URL',\n authScheme: 'none',\n apiKeyEnvVar: 'LM_STUDIO_API_KEY',\n apiKeyOptional: true,\n supports: {\n chat: true,\n embeddings: true,\n streaming: true,\n },\n pricingMode: 'none',\n enabledByDefault: true,\n priority: 5,\n docsUrl: 'https://lmstudio.ai/docs/local-server',\n notes:\n 'Local OAI-compatible server bundled with LM Studio desktop. No auth by default. Uses LmStudioAdapter for apiKeyOptional + named identity.',\n adapterClass: 'LmStudioAdapter',\n lastReviewedAt: '2026-04-22',\n },\n {\n providerId: 'xinference',\n displayName: 'Xinference (Xorbits)',\n providerFamily: 'xinference',\n aliases: ['xorbits', 'xinf'],\n integrationClass: 'self-hosted-oai-compat',\n integrationMode: 'discovery+execution',\n baseUrl: 'http://localhost:9997/v1',\n baseUrlEnvVar: 'XINFERENCE_BASE_URL',\n authScheme: 'none',\n apiKeyEnvVar: 'XINFERENCE_API_KEY',\n apiKeyOptional: true,\n supports: {\n chat: true,\n embeddings: true,\n rerank: true,\n streaming: true,\n imageGeneration: true,\n speechToText: true,\n textToSpeech: true,\n },\n pricingMode: 'none',\n enabledByDefault: true,\n priority: 10,\n docsUrl: 'https://inference.readthedocs.io/en/latest/index.html',\n notes:\n 'Self-hosted multi-modality runtime. XinferenceAdapter exposes a Cohere-compatible /v1/rerank method on top of the hub OAI surface.',\n adapterClass: 'XinferenceAdapter',\n lastReviewedAt: '2026-04-22',\n },\n {\n providerId: 'triton',\n displayName: 'NVIDIA Triton Inference Server',\n providerFamily: 'triton',\n aliases: ['triton-inference-server', 'nvidia-triton'],\n integrationClass: 'self-hosted-native',\n integrationMode: 'discovery+execution',\n baseUrl: 'http://localhost:8000',\n baseUrlEnvVar: 'TRITON_BASE_URL',\n authScheme: 'none',\n apiKeyEnvVar: 'TRITON_API_KEY',\n apiKeyOptional: true,\n supports: {\n embeddings: true,\n },\n pricingMode: 'none',\n enabledByDefault: true,\n priority: 5,\n docsUrl:\n 'https://docs.nvidia.com/deeplearning/triton-inference-server/user-guide/docs/protocol/extension_generate.html',\n notes:\n 'Triton KServe v2 HTTP protocol — POST /v2/models/{model}/infer with INPUT/OUTPUT tensor arrays. Adapter converts OAI embedding request into Triton tensor shape and flattens response tensor back to float32[].',\n adapterClass: 'TritonAdapter',\n lastReviewedAt: '2026-04-22',\n },\n\n // ──────────────────────────────────────────────────────────────────────────\n // LOTE E — Migrated from provider-registry.ts switch (2026-04-21)\n //\n // These providers WERE hardcoded switch cases in provider-registry.ts\n // that all instantiated `OpenAICompatibleHubAdapter` with nothing but a\n // baseUrl + apiKey. Moving them here is a lossless migration — the same\n // adapter is built by `CatalogProviderPlugin.initialize()`, sourced from\n // the same env vars. The payoff: the switch shrinks from 41 → 21 cases,\n // and every provider added from now on is a data-row, not code.\n // ──────────────────────────────────────────────────────────────────────────\n\n // ─── E.1 — Cloud OAI-compatible hubs (single-provider endpoints) ─────────\n {\n providerId: 'nvidia',\n displayName: 'NVIDIA NIM',\n providerFamily: 'nvidia',\n // Lot B (2026-04-22) deleted `case 'nvidia-hub':` from\n // provider-registry.ts. The alias is re-attached here so historical\n // `config.providers[].name === 'nvidia-hub'` entries (or any external\n // caller that still says \"nvidia-hub\") still resolve to this single\n // catalog row. Same NVIDIA_API_KEY, same integrate.api.nvidia.com\n // endpoint — purely a rename/identity harmonization.\n aliases: ['nvidia-hub'],\n integrationClass: 'oai-compat-pure',\n integrationMode: 'discovery+execution',\n baseUrl: 'https://integrate.api.nvidia.com/v1',\n baseUrlEnvVar: 'NVIDIA_BASE_URL',\n authScheme: 'bearer',\n apiKeyEnvVar: 'NVIDIA_API_KEY',\n supports: {\n chat: true,\n embeddings: true,\n streaming: true,\n tools: true,\n jsonMode: true,\n vision: true,\n },\n pricingMode: 'none',\n enabledByDefault: true,\n priority: 40,\n docsUrl: 'https://docs.api.nvidia.com/nim/',\n notes: 'NVIDIA-hosted NIM microservices. Was switch case; migrated 2026-04-21.',\n lastReviewedAt: '2026-04-21',\n },\n {\n providerId: 'moonshot',\n displayName: 'Moonshot AI (Kimi)',\n providerFamily: 'moonshot',\n integrationClass: 'oai-compat-pure',\n integrationMode: 'discovery+execution',\n baseUrl: 'https://api.moonshot.ai/v1',\n baseUrlEnvVar: 'MOONSHOT_BASE_URL',\n authScheme: 'bearer',\n apiKeyEnvVar: 'MOONSHOT_API_KEY',\n supports: {\n chat: true,\n streaming: true,\n tools: true,\n jsonMode: true,\n vision: true,\n },\n pricingMode: 'remote',\n enabledByDefault: true,\n priority: 35,\n docsUrl: 'https://platform.moonshot.ai/docs/intro',\n notes: 'Kimi K2 / long-context family. Was switch case; migrated 2026-04-21.',\n lastReviewedAt: '2026-04-21',\n },\n {\n providerId: 'minimax',\n displayName: 'MiniMax',\n providerFamily: 'minimax',\n integrationClass: 'oai-compat-pure',\n integrationMode: 'discovery+execution',\n baseUrl: 'https://api.minimax.io/v1',\n baseUrlEnvVar: 'MINIMAX_BASE_URL',\n authScheme: 'bearer',\n apiKeyEnvVar: 'MINIMAX_API_KEY',\n supports: {\n chat: true,\n streaming: true,\n tools: true,\n vision: true,\n },\n pricingMode: 'remote',\n enabledByDefault: true,\n priority: 30,\n docsUrl: 'https://platform.minimaxi.com/document/ChatCompletion',\n notes: 'abab6 / Hailuo family. Was switch case; migrated 2026-04-21.',\n lastReviewedAt: '2026-04-21',\n },\n {\n providerId: 'friendli',\n displayName: 'Friendli',\n providerFamily: 'friendli',\n integrationClass: 'oai-compat-quirks',\n integrationMode: 'discovery+execution',\n baseUrl: 'https://api.friendli.ai/serverless/v1',\n baseUrlEnvVar: 'FRIENDLI_BASE_URL',\n authScheme: 'bearer',\n apiKeyEnvVar: 'FRIENDLI_API_KEY',\n // Team ID header is required for serverless billing attribution. The\n // default below mirrors `config/index.ts`; users override via\n // FRIENDLI_TEAM_ID at runtime — since `extraHeaders` is static JSON at\n // catalog build time, we keep the declared default here. Teams must\n // set their own FRIENDLI_TEAM_ID env if the default is stale.\n extraHeaders: {\n 'X-Friendli-Team': process.env.FRIENDLI_TEAM_ID || '',\n },\n supports: {\n chat: true,\n streaming: true,\n tools: true,\n jsonMode: true,\n },\n pricingMode: 'remote',\n enabledByDefault: true,\n priority: 30,\n docsUrl: 'https://docs.friendli.ai/',\n notes: 'Serverless endpoints for open-source LLMs. Was switch case; migrated 2026-04-21.',\n lastReviewedAt: '2026-04-21',\n },\n\n // ─── E.2 — Multi-provider aggregators / routers ──────────────────────────\n {\n providerId: 'aihubmix',\n displayName: 'AiHubMix',\n providerFamily: 'aihubmix',\n integrationClass: 'oai-compat-pure',\n integrationMode: 'discovery+execution',\n baseUrl: 'https://aihubmix.com/v1',\n baseUrlEnvVar: 'AIHUBMIX_BASE_URL',\n authScheme: 'bearer',\n apiKeyEnvVar: 'AIHUBMIX_API_KEY',\n paths: {\n // Live probe 2026-07-17: `/videos/generations` is 404 here, but POST\n // /videos EXISTS and validates (\"Missing required parameter: 'prompt'\",\n // Aihubmix_api_error) — a video route the catalog didn't know about.\n // Response/poll contract still to be proven by a first real generation.\n videoGenerate: '/videos',\n },\n supports: {\n chat: true,\n embeddings: true,\n streaming: true,\n tools: true,\n vision: true,\n imageGeneration: true,\n videoGeneration: true,\n },\n pricingMode: 'remote',\n enabledByDefault: true,\n priority: 25,\n docsUrl: 'https://aihubmix.com/docs',\n notes:\n 'Multi-upstream aggregator (OpenAI, Anthropic, Google, etc.). Was switch case; migrated 2026-04-21. Probe 2026-07-17: video route discovered at POST /videos (see paths).',\n lastReviewedAt: '2026-07-17',\n },\n {\n providerId: 'novita',\n displayName: 'Novita AI',\n providerFamily: 'novita',\n integrationClass: 'oai-compat-pure',\n integrationMode: 'discovery+execution',\n baseUrl: 'https://api.novita.ai/openai/v1',\n baseUrlEnvVar: 'NOVITA_BASE_URL',\n authScheme: 'bearer',\n apiKeyEnvVar: 'NOVITA_API_KEY',\n supports: {\n chat: true,\n embeddings: true,\n streaming: true,\n tools: true,\n imageGeneration: true,\n },\n pricingMode: 'remote',\n enabledByDefault: true,\n priority: 30,\n docsUrl: 'https://novita.ai/docs/',\n notes: 'GPU-as-a-service + LLM hub. Was switch case; migrated 2026-04-21.',\n lastReviewedAt: '2026-04-21',\n },\n {\n providerId: 'aiml',\n displayName: 'AI/ML API',\n providerFamily: 'aiml',\n integrationClass: 'oai-compat-quirks',\n integrationMode: 'discovery+execution',\n baseUrl: 'https://api.aimlapi.com/v1',\n baseUrlEnvVar: 'AIML_BASE_URL',\n authScheme: 'bearer',\n apiKeyEnvVar: 'AIML_API_KEY',\n paths: {\n // Model list lives on the bare host, not under /v1\n modelList: ['/models', '/v1/models'],\n },\n supports: {\n chat: true,\n streaming: true,\n tools: true,\n vision: true,\n imageGeneration: true,\n speechToText: true,\n textToSpeech: true,\n },\n pricingMode: 'remote',\n enabledByDefault: true,\n priority: 25,\n docsUrl: 'https://docs.aimlapi.com/',\n notes:\n 'Multi-modal aggregator. modelsBaseUrl quirk in legacy config: /models served from bare host. Migrated 2026-04-21.',\n lastReviewedAt: '2026-04-21',\n },\n {\n providerId: 'imagerouter',\n displayName: 'ImageRouter',\n providerFamily: 'imagerouter',\n integrationClass: 'oai-compat-quirks',\n integrationMode: 'discovery+execution',\n baseUrl: 'https://api.imagerouter.io',\n baseUrlEnvVar: 'IMAGEROUTER_BASE_URL',\n authScheme: 'bearer',\n apiKeyEnvVar: 'IMAGEROUTER_API_KEY',\n // Image + video paths are non-standard; routed through the OpenAI-shape\n // endpoints under /v1/openai/*. Catalog declares these so the hub adapter\n // targets the right URLs at runtime.\n paths: {\n imagesGenerate: '/v1/openai/images/generations',\n imagesEdit: '/v1/openai/images/edits',\n videoGenerate: '/v1/openai/videos/generations',\n modelList: ['/v1/models'],\n },\n supports: {\n chat: false,\n imageGeneration: true,\n imageEditing: true,\n videoGeneration: true,\n },\n pricingMode: 'remote',\n enabledByDefault: true,\n priority: 20,\n docsUrl: 'https://imagerouter.io/docs',\n notes:\n 'Image/video generation router. Quirks: paths differ from standard /v1/images/*. Was switch case; migrated 2026-04-21.',\n lastReviewedAt: '2026-04-21',\n },\n {\n providerId: 'orqai',\n displayName: 'ORQ.ai',\n providerFamily: 'orqai',\n integrationClass: 'oai-compat-pure',\n integrationMode: 'discovery+execution',\n baseUrl: 'https://api.orq.ai/v2/router',\n baseUrlEnvVar: 'ORQAI_BASE_URL',\n authScheme: 'bearer',\n apiKeyEnvVar: 'ORQAI_API_KEY',\n supports: {\n chat: true,\n streaming: true,\n tools: true,\n jsonMode: true,\n },\n pricingMode: 'remote',\n enabledByDefault: true,\n priority: 20,\n docsUrl: 'https://docs.orq.ai/',\n notes: 'LLM operations router. Was switch case; migrated 2026-04-21.',\n lastReviewedAt: '2026-04-21',\n },\n {\n providerId: 'edenai',\n displayName: 'Eden AI',\n providerFamily: 'edenai',\n integrationClass: 'oai-compat-pure',\n integrationMode: 'discovery+execution',\n baseUrl: 'https://api.edenai.run/v3/llm',\n baseUrlEnvVar: 'EDENAI_BASE_URL',\n authScheme: 'bearer',\n apiKeyEnvVar: 'EDENAI_API_KEY',\n supports: {\n chat: true,\n streaming: true,\n tools: true,\n },\n pricingMode: 'remote',\n enabledByDefault: true,\n priority: 20,\n docsUrl: 'https://docs.edenai.co/reference/llm_chat_create',\n notes: 'Multi-provider AI platform aggregator. Was switch case; migrated 2026-04-21.',\n lastReviewedAt: '2026-04-21',\n },\n {\n providerId: 'heliconeai',\n displayName: 'Helicone AI Gateway',\n providerFamily: 'heliconeai',\n integrationClass: 'gateway',\n integrationMode: 'discovery+execution',\n baseUrl: 'https://ai-gateway.helicone.ai/v1',\n baseUrlEnvVar: 'HELICONEAI_BASE_URL',\n authScheme: 'bearer',\n apiKeyEnvVar: 'HELICONEAI_API_KEY',\n supports: {\n chat: true,\n embeddings: true,\n streaming: true,\n tools: true,\n },\n pricingMode: 'remote',\n enabledByDefault: true,\n priority: 20,\n docsUrl: 'https://docs.helicone.ai/getting-started/integration-method/gateway',\n notes:\n 'Unified gateway proxying many upstreams. Was switch case; migrated 2026-04-21. D1 2026-04-24: live-validated — new ailin-heliconeai-api-key (sk-hel… 43B Virtual Key) replaces legacy ailin-heliconeai-key (11B \"PLACEHOLDER\"). Gateway routed gpt-4o-mini /chat 200 (1003B completion). Helicone injects its observability shim and forwards to the target vendor (OpenAI here); works with any target-model/Helicone-Target-Url configuration.',\n lastReviewedAt: '2026-04-24',\n },\n {\n providerId: 'cometapi',\n displayName: 'CometAPI',\n providerFamily: 'cometapi',\n integrationClass: 'oai-compat-pure',\n integrationMode: 'discovery+execution',\n baseUrl: 'https://api.cometapi.com/v1',\n baseUrlEnvVar: 'COMETAPI_BASE_URL',\n authScheme: 'bearer',\n apiKeyEnvVar: 'COMETAPI_API_KEY',\n paths: {\n // Live probe 2026-07-17: `/videos/generations` is 404 (\"Invalid URL\"),\n // but POST /videos EXISTS and validates (\"model name is required\",\n // comet_api_error). Response/poll contract still to be proven by a\n // first real generation.\n videoGenerate: '/videos',\n },\n supports: {\n chat: true,\n streaming: true,\n tools: true,\n videoGeneration: true,\n },\n pricingMode: 'remote',\n enabledByDefault: true,\n priority: 20,\n docsUrl: 'https://www.cometapi.com/docs',\n notes:\n 'Multi-model aggregator. Was switch case; migrated 2026-04-21. Probe 2026-07-17: video route discovered at POST /videos (see paths).',\n lastReviewedAt: '2026-07-17',\n },\n {\n providerId: 'nanogpt',\n displayName: 'Nano GPT',\n providerFamily: 'nanogpt',\n integrationClass: 'oai-compat-pure',\n integrationMode: 'discovery+execution',\n baseUrl: 'https://nano-gpt.com/api/v1',\n baseUrlEnvVar: 'NANOGPT_BASE_URL',\n authScheme: 'bearer',\n apiKeyEnvVar: 'NANOGPT_API_KEY',\n supports: {\n chat: true,\n streaming: true,\n },\n pricingMode: 'remote',\n enabledByDefault: true,\n priority: 15,\n docsUrl: 'https://nano-gpt.com/docs',\n notes: 'Was switch case; migrated 2026-04-21.',\n lastReviewedAt: '2026-04-21',\n },\n {\n providerId: 'requesty',\n displayName: 'Requesty',\n providerFamily: 'requesty',\n integrationClass: 'oai-compat-pure',\n integrationMode: 'discovery+execution',\n baseUrl: 'https://router.requesty.ai/v1',\n baseUrlEnvVar: 'REQUESTY_BASE_URL',\n authScheme: 'bearer',\n apiKeyEnvVar: 'REQUESTY_API_KEY',\n supports: {\n chat: true,\n streaming: true,\n tools: true,\n },\n pricingMode: 'remote',\n enabledByDefault: true,\n priority: 20,\n docsUrl: 'https://docs.requesty.ai/',\n notes: 'LLM router. Was switch case; migrated 2026-04-21.',\n lastReviewedAt: '2026-04-21',\n },\n {\n providerId: 'poe',\n displayName: 'Poe by Quora',\n providerFamily: 'poe',\n integrationClass: 'oai-compat-pure',\n integrationMode: 'discovery+execution',\n baseUrl: 'https://api.poe.com/v1',\n baseUrlEnvVar: 'POE_BASE_URL',\n authScheme: 'bearer',\n apiKeyEnvVar: 'POE_API_KEY',\n supports: {\n chat: true,\n streaming: true,\n },\n pricingMode: 'remote',\n enabledByDefault: true,\n priority: 15,\n docsUrl: 'https://creator.poe.com/docs/external-applications/openai-compatible-api',\n notes: 'Poe OpenAI-compatible endpoint. Was switch case; migrated 2026-04-21.',\n lastReviewedAt: '2026-04-21',\n },\n {\n providerId: 'routeway',\n displayName: 'Routeway',\n providerFamily: 'routeway',\n integrationClass: 'oai-compat-pure',\n integrationMode: 'discovery+execution',\n baseUrl: 'https://api.routeway.ai/v1',\n baseUrlEnvVar: 'ROUTEWAY_BASE_URL',\n authScheme: 'bearer',\n apiKeyEnvVar: 'ROUTEWAY_API_KEY',\n supports: {\n chat: true,\n streaming: true,\n tools: true,\n },\n pricingMode: 'remote',\n enabledByDefault: true,\n priority: 15,\n docsUrl: 'https://routeway.ai/docs',\n notes: 'Was switch case; migrated 2026-04-21.',\n lastReviewedAt: '2026-04-21',\n },\n\n // ─── E.3 — Local OAI-compatible sidecars ─────────────────────────────────\n //\n // These are migrated as `self-hosted-oai-compat`. They use a synthetic\n // apiKeyEnvVar for schema compliance but `apiKeyOptional: true` means the\n // adapter doesn't require it. The legacy config injected literal strings\n // like `apiKey: 'local'` — the catalog path uses whatever env value is\n // present (empty string if nothing set), which the hub adapter tolerates\n // for local servers that don't authenticate.\n //\n // enabledByDefault: true (per the universal \"habilitado e nunca censurado\"\n // policy). Loader still skips at boot when baseUrlEnvVar is unset — the\n // health check just fails-soft and the entry lands in the skipped bucket.\n {\n providerId: 'ollama',\n displayName: 'Ollama (Local LLM)',\n providerFamily: 'ollama',\n integrationClass: 'self-hosted-oai-compat',\n integrationMode: 'discovery+execution',\n baseUrl: 'http://localhost:11434/v1',\n // Legacy env var is OLLAMA_URL (not _BASE_URL). Honored here via\n // baseUrlEnvVar so existing deployments keep working.\n baseUrlEnvVar: 'OLLAMA_URL',\n authScheme: 'none',\n apiKeyEnvVar: 'OLLAMA_API_KEY',\n apiKeyOptional: true,\n supports: {\n chat: true,\n embeddings: true,\n streaming: true,\n tools: true,\n vision: true,\n },\n pricingMode: 'none',\n // Opt-in: loader only registers when OLLAMA_URL is set in env.\n enabledByDefault: true,\n priority: 10,\n docsUrl: 'https://github.com/ollama/ollama/blob/main/docs/openai.md',\n notes:\n 'Local Ollama server. Set OLLAMA_URL (e.g. http://localhost:11434/v1) to enable. Dedicated OllamaAdapter gives named identity for logs/metrics and a subclass seat for future native /api/* endpoints (tags, pull, generate).',\n adapterClass: 'OllamaAdapter',\n lastReviewedAt: '2026-04-22',\n },\n {\n providerId: 'local-llama',\n displayName: 'Local LLM (llama.cpp)',\n providerFamily: 'local-llama',\n integrationClass: 'self-hosted-oai-compat',\n integrationMode: 'discovery+execution',\n baseUrl: 'http://localhost:8080/v1',\n baseUrlEnvVar: 'LOCAL_LLAMA_URL',\n authScheme: 'none',\n apiKeyEnvVar: 'LOCAL_LLAMA_API_KEY',\n apiKeyOptional: true,\n supports: {\n chat: true,\n streaming: true,\n },\n pricingMode: 'none',\n enabledByDefault: true,\n priority: 5,\n docsUrl: 'https://github.com/ggerganov/llama.cpp/blob/master/examples/server/README.md',\n notes: 'llama.cpp server in OpenAI-compat mode. Set LOCAL_LLAMA_URL to enable.',\n lastReviewedAt: '2026-04-21',\n },\n {\n providerId: 'local-kobold',\n displayName: 'Local VLM (KoboldCpp)',\n providerFamily: 'local-kobold',\n integrationClass: 'self-hosted-oai-compat',\n integrationMode: 'discovery+execution',\n baseUrl: 'http://localhost:5001/v1',\n baseUrlEnvVar: 'LOCAL_KOBOLD_URL',\n authScheme: 'none',\n apiKeyEnvVar: 'LOCAL_KOBOLD_API_KEY',\n apiKeyOptional: true,\n supports: {\n chat: true,\n streaming: true,\n vision: true, // GGUF + mmproj path\n },\n pricingMode: 'none',\n enabledByDefault: true,\n priority: 5,\n docsUrl: 'https://github.com/LostRuins/koboldcpp',\n notes: 'KoboldCpp vision-capable local server. Set LOCAL_KOBOLD_URL to enable.',\n lastReviewedAt: '2026-04-21',\n },\n {\n providerId: 'local-embeddings',\n displayName: 'Local Embeddings (ONNX)',\n providerFamily: 'local-embeddings',\n integrationClass: 'self-hosted-oai-compat',\n integrationMode: 'discovery+execution',\n baseUrl: 'http://localhost:8081/v1',\n baseUrlEnvVar: 'LOCAL_EMBEDDINGS_URL',\n authScheme: 'none',\n apiKeyEnvVar: 'LOCAL_EMBEDDINGS_API_KEY',\n apiKeyOptional: true,\n supports: {\n embeddings: true,\n rerank: true,\n },\n pricingMode: 'none',\n enabledByDefault: true,\n priority: 5,\n docsUrl: 'https://onnxruntime.ai/',\n notes: 'ONNX Runtime embeddings + rerank server. Set LOCAL_EMBEDDINGS_URL to enable.',\n lastReviewedAt: '2026-04-21',\n },\n // ── Orphan closure (2026-04-23) — Lot B (Writer/Upstage/Reka) ─────────\n // Each of these three OpenAI-compatible providers had an adapter class on\n // disk (WriterAdapter, UpstageAdapter, RekaAIAdapter) and a working key\n // in GCP Secret Manager, but no catalog row and no factory registration —\n // i.e., the exact Inworld failure mode closed in 2026-04-22.\n //\n // Direct fetch probes executed this session against /v1/chat/completions\n // returned HTTP 200 for all three using the loaded keys. Post-closure\n // re-probe via the wired pipeline is scheduled as part of this turn; until\n // that completes, consolidation-matrix.ts pins them to a closure-transient\n // bucket. Mechanically they are full canonical providers from now on.\n {\n providerId: 'writer',\n displayName: 'Writer',\n providerFamily: 'writer',\n integrationClass: 'oai-compat-quirks',\n // execution-only because GET /v1/models returns `{models: [{id, name}]}`\n // (Writer's own shape), not OpenAI's `{data: [{id, object, ...}]}` —\n // the default hub discovery parser skips the body and discovers zero\n // models. Same mitigation as Inworld: hand-supply model IDs here.\n // Post-closure probe (2026-04-23) against /v1/chat/completions using\n // palmyra-x-004 returned HTTP 200 \"PONG\" — execution path verified.\n integrationMode: 'execution-only',\n baseUrl: 'https://api.writer.com/v1',\n authScheme: 'bearer',\n apiKeyEnvVar: 'WRITER_API_KEY',\n adapterClass: 'WriterAdapter',\n supports: {\n chat: true,\n streaming: true,\n tools: true,\n jsonMode: true,\n },\n // Sourced directly from GET /v1/models on 2026-04-23 with the canonical\n // key. This list reflects upstream AT probe time; treating it as\n // illustrative (not authoritative) — the router accepts any upstream\n // model ID Writer serves. Update when a discovery-parser enhancement\n // lands that handles the `{models: [...]}` shape.\n // Phase 4d (2026-04-28): renamed staticModels → pinnedFallback with\n // reason='proprietary-schema' — `/v1/models` returns Writer's own\n // `{models: [...]}` shape (not OpenAI `{data: [...]}`), which the default\n // OAI hub parser cannot consume; a dedicated `WriterModelFetcher` would\n // unblock discovery+execution but is deferred to a follow-up.\n pinnedFallback: {\n models: [\n // Palmyra X family — flagship general chat with native tools + JSON mode.\n {\n id: 'palmyra-x-004',\n capabilities: ['chat', 'streaming', 'function_calling', 'json_mode'],\n },\n {\n id: 'palmyra-x5',\n capabilities: ['chat', 'streaming', 'function_calling', 'json_mode'],\n },\n {\n id: 'palmyra-x4',\n capabilities: ['chat', 'streaming', 'function_calling', 'json_mode'],\n },\n {\n id: 'palmyra-x-003-instruct',\n capabilities: ['chat', 'streaming', 'function_calling', 'json_mode'],\n },\n // palmyra-vision IS multimodal upstream, but requires Writer-specific\n // content shape (see notes); declare vision so downstream callers can\n // route correctly even though the adapter currently rejects standard\n // OAI multimodal content arrays.\n {\n id: 'palmyra-vision',\n capabilities: ['chat', 'streaming', 'vision', 'multimodal'],\n },\n // Domain-specialised palmyras: chat-only (no documented tools/JSON).\n {\n id: 'palmyra-med',\n capabilities: ['chat', 'streaming'],\n },\n {\n id: 'palmyra-fin',\n capabilities: ['chat', 'streaming'],\n },\n {\n id: 'palmyra-creative',\n capabilities: ['chat', 'streaming'],\n },\n ],\n reason: 'proprietary-schema',\n lastReviewedAt: '2026-04-28',\n },\n pricingMode: 'none',\n enabledByDefault: true,\n priority: 10,\n docsUrl: 'https://dev.writer.com',\n notes:\n 'Enterprise Palmyra-family chat + generation. OAI-compat execution path (chat/completions); non-standard `{models: [...]}` discovery shape — catalog supplies model IDs until discovery-parser supports the shape. palmyra-vision is listed upstream but requires Writer-specific content shape (standard OAI multimodal content array returns 400 OpenrouterException); vision support deferred to a dedicated adapter extension. Orphan-closure entry 2026-04-23.',\n lastReviewedAt: '2026-04-23',\n },\n {\n providerId: 'upstage',\n displayName: 'Upstage',\n providerFamily: 'upstage',\n aliases: ['upstage-ai'],\n integrationClass: 'oai-compat-pure',\n integrationMode: 'discovery+execution',\n baseUrl: 'https://api.upstage.ai/v1',\n authScheme: 'bearer',\n apiKeyEnvVar: 'UPSTAGE_API_KEY',\n adapterClass: 'UpstageAdapter',\n // jsonMode capability confirmed via post-closure probe 2026-04-23\n // against solar-pro with response_format={type:'json_object'} — returned\n // valid {\"ok\": true}. Upstage's JSON-mode requires the prompt to\n // contain the literal substring \"json\" (identical to OpenAI's rule).\n // embeddings confirmed via /v1/embeddings probe on solar-embedding-1-\n // large-query: HTTP 200 with real float array.\n supports: {\n chat: true,\n streaming: true,\n embeddings: true,\n tools: true,\n jsonMode: true,\n },\n pricingMode: 'none',\n enabledByDefault: true,\n priority: 10,\n docsUrl: 'https://developers.upstage.ai',\n notes:\n 'Solar LLM family + document-AI embeddings. OAI-compat. Orphan-closure entry 2026-04-23: adapter class existed at providers/upstage/ but was never wired. Post-closure live probes (chat, embeddings, jsonMode) all 200.',\n lastReviewedAt: '2026-04-23',\n },\n {\n providerId: 'rekaai',\n displayName: 'Reka AI',\n providerFamily: 'reka',\n aliases: ['reka', 'reka-ai'],\n integrationClass: 'oai-compat-pure',\n integrationMode: 'discovery+execution',\n baseUrl: 'https://api.reka.ai/v1',\n authScheme: 'bearer',\n apiKeyEnvVar: 'REKA_API_KEY',\n apiKeyEnvVarOverrideReason:\n 'REKA_API_KEY is canonical in docs.reka.ai and their Python SDK. providerId `rekaai` disambiguates the adapter filename; env follows upstream to avoid double-setting the same secret.',\n adapterClass: 'RekaAIAdapter',\n supports: {\n chat: true,\n streaming: true,\n vision: true,\n tools: true,\n },\n pricingMode: 'none',\n enabledByDefault: true,\n priority: 10,\n docsUrl: 'https://docs.reka.ai',\n notes:\n 'Reka Core / Flash / Edge multimodal chat. OAI-compat surface. Orphan-closure entry 2026-04-23: adapter class existed at providers/rekaai/ but was never wired.',\n lastReviewedAt: '2026-04-23',\n },\n\n // ──────────────────────────────────────────────────────────────────────────\n // LOTE M — 2026-04-23 complementary lot (13 entries)\n //\n // Research basis: per-provider documentation audit on 2026-04-23 against\n // each vendor's official docs URL. All integration classes, auth schemes,\n // and base URLs derived from the authoritative docs (not inferred).\n //\n // Credential state for this lot: all 13 are in credentials-missing /\n // secret-absent at catalog-time (no GCP secret provisioned under any\n // known alias). Exception: `qianfan` is a promotion — 3 Baidu secrets\n // already exist (ailin-baidu-{key,secret,base-url}), but they back the\n // v1 legacy AK+SK OAuth flow, NOT the v2 bce-v3 bearer path this entry\n // canonicalizes; so qianfan enters bucket credentials-missing /\n // auth-incomplete until a QIANFAN_API_KEY (bce-v3 format) is provisioned.\n //\n // Not added this lot (documented in NON_CANONICAL_HISTORICAL_CLAIMS):\n // - liquid — no first-party production API; LFM only via OpenRouter\n // - modelrun — domain does not resolve (ECONNREFUSED); unverifiable\n // - ncompass — base_url unverified in public docs; requires operator\n // signup to discover the exact endpoint before canonicalizing\n // ──────────────────────────────────────────────────────────────────────────\n {\n providerId: 'arcee',\n displayName: 'Arcee AI',\n providerFamily: 'arcee',\n integrationClass: 'oai-compat-pure',\n integrationMode: 'discovery+execution',\n baseUrl: 'https://api.arcee.ai/api/v1',\n authScheme: 'bearer',\n apiKeyEnvVar: 'ARCEE_API_KEY',\n supports: {\n chat: true,\n streaming: true,\n reasoning: true,\n },\n pricingMode: 'remote',\n enabledByDefault: true,\n priority: 10,\n docsUrl: 'https://docs.arcee.ai',\n notes:\n 'Arcee SLM family (Trinity). OAI-compat chat, OpenRouter-style /models metadata. Reasoning at message.reasoning_content (streaming: delta.reasoning_content) — NOT message.reasoning as previously documented; unverified until 2026-07-29. Lot M 2026-04-23. D1 2026-04-24: 402 insufficient-credits. Live-verified 2026-07-29 after a $5 top-up: GET /v1/models 200 (trinity-mini renamed to trinity-large-thinking), POST /v1/chat/completions 200 (real content+usage), stream:true 200 (SSE, [DONE]).',\n lastReviewedAt: '2026-07-29',\n },\n {\n providerId: 'atlascloud',\n displayName: 'AtlasCloud',\n providerFamily: 'atlascloud',\n aliases: ['atlas-cloud'],\n integrationClass: 'oai-compat-pure',\n integrationMode: 'execution-only',\n baseUrl: 'https://api.atlascloud.ai/v1',\n authScheme: 'bearer',\n apiKeyEnvVar: 'ATLASCLOUD_API_KEY',\n // Phase 4d (2026-04-28): renamed staticModels → pinnedFallback with\n // reason='no-list-endpoint' (vendor docs document none).\n pinnedFallback: {\n models: [\n // DeepSeek-V3 chat — OAI-compat tools + streaming.\n {\n id: 'deepseek-v3',\n capabilities: ['chat', 'streaming', 'function_calling'],\n },\n // ByteDance Seedream-3.0 — image generation.\n {\n id: 'seedream-3.0',\n capabilities: ['image_generation'],\n },\n // Kling v2.0 — video generation.\n {\n id: 'kling-v2.0',\n capabilities: ['video_generation'],\n },\n ],\n reason: 'no-list-endpoint',\n lastReviewedAt: '2026-04-28',\n },\n supports: {\n chat: true,\n streaming: true,\n vision: true,\n imageGeneration: true,\n // 2026-07-17: videoGeneration REMOVED — live probe proved POST\n // /v1/videos/generations AND /v1/videos are both 404 here (\"404 page\n // not found\"), so the flag only fed the execution pool guaranteed\n // failures for the kling-v2.0 pinned model. Re-add together with a\n // paths.videoGenerate once the real Atlas video route is discovered\n // and live-proven.\n },\n pricingMode: 'none',\n enabledByDefault: true,\n priority: 10,\n docsUrl: 'https://www.atlascloud.ai/docs',\n notes:\n 'GPU cloud aggregator. Multi-surface (chat+image+video) under a single OAI-compat base URL. No public GET /models; static model IDs taken from docs. Lot M 2026-04-23. Probe 2026-07-17: no OAI-style video route (404 both candidates) — video de-advertised until a real route is proven.',\n lastReviewedAt: '2026-07-17',\n },\n {\n providerId: 'avian',\n displayName: 'Avian.io',\n providerFamily: 'avian',\n aliases: ['avian-io'],\n integrationClass: 'oai-compat-pure',\n integrationMode: 'execution-only',\n baseUrl: 'https://api.avian.io/v1',\n authScheme: 'bearer',\n apiKeyEnvVar: 'AVIAN_API_KEY',\n // Phase 4d (2026-04-28): renamed staticModels → pinnedFallback with\n // reason='no-list-endpoint' (no /models endpoint documented).\n pinnedFallback: {\n models: [\n // DeepSeek V3.2 — chat + streaming + tools (OAI-compat).\n {\n id: 'deepseek/deepseek-v3.2',\n capabilities: ['chat', 'streaming', 'function_calling'],\n },\n // Moonshot Kimi K2.6 — chat + streaming + tools + 200k context.\n {\n id: 'moonshotai/kimi-k2.6',\n capabilities: ['chat', 'streaming', 'function_calling', 'long_context'],\n },\n // Z.AI GLM-5.1 — chat + streaming + tools.\n {\n id: 'z-ai/glm-5.1',\n capabilities: ['chat', 'streaming', 'function_calling'],\n },\n ],\n reason: 'no-list-endpoint',\n lastReviewedAt: '2026-04-28',\n },\n supports: {\n chat: true,\n streaming: true,\n tools: true,\n },\n pricingMode: 'none',\n enabledByDefault: true,\n priority: 10,\n docsUrl: 'https://avian.io/docs',\n notes:\n 'OAI-compat inference aggregator (deepseek, moonshot, z-ai routes). Static model list per docs; no /models endpoint documented. Lot M 2026-04-23.',\n lastReviewedAt: '2026-04-23',\n },\n {\n // Baidu Qianfan (ERNIE platform) — promoted to canonical Lot M 2026-04-23.\n //\n // Pre-promotion state: 3 GCP secrets existed (ailin-baidu-{key,secret,base-url})\n // with load-secrets-into-env.ts mapping ERNIE_API_KEY / ERNIE_SECRET_KEY /\n // BAIDU_BASE_URL, but no catalog row and no adapter — classic\n // \"secret-descoberto-mas-não-canônico\".\n //\n // Qianfan documents TWO auth paths:\n // v1 (legacy): AK+SK → OAuth access_token → ?access_token= query param\n // against aip.baidubce.com/rpc/2.0/ai_custom/v1/wenxinworkshop/chat\n // v2 (current OAI-compat): bce-v3/... bearer key against\n // qianfan.baidubce.com/v2\n //\n // We canonicalize v2 (OAI-compat-quirks). v1 would require a dedicated\n // native adapter that does not exist in this repo; v2 unlocks\n // chat/stream/tools/embed/vision without new adapter code.\n //\n // The existing AK+SK secrets remain mapped (load-secrets-into-env.ts) for\n // any legacy caller that still wants the v1 path, but they are NOT\n // sufficient for this catalog entry's runtime. A QIANFAN_API_KEY\n // (bce-v3/... format) must be provisioned separately in GCP before\n // execution is possible — until then bucket = credentials-missing /\n // auth-incomplete.\n providerId: 'qianfan',\n displayName: 'Baidu Qianfan (ERNIE)',\n providerFamily: 'qianfan',\n aliases: ['baidu', 'ernie', 'baidu-qianfan'],\n integrationClass: 'oai-compat-quirks',\n integrationMode: 'execution-only',\n baseUrl: 'https://qianfan.baidubce.com/v2',\n baseUrlEnvVar: 'BAIDU_BASE_URL',\n authScheme: 'bearer',\n apiKeyEnvVar: 'QIANFAN_API_KEY',\n apiKeyEnvVarOverrideReason:\n 'QIANFAN_API_KEY matches the v2 bce-v3/... bearer format at qianfan.baidubce.com/v2. Legacy ERNIE_API_KEY / ERNIE_SECRET_KEY remain mapped for the v1 AK+SK OAuth flow, not this v2 runtime path.',\n // Phase 4d (2026-04-28): renamed staticModels → pinnedFallback with\n // reason='no-list-endpoint' — Qianfan v2 docs do not document a generic\n // model-list endpoint; ERNIE family IDs are vendor-published.\n pinnedFallback: {\n models: [\n // ERNIE 4.0 8k context — chat + streaming + tools (per Qianfan docs).\n {\n id: 'ernie-4.0-8k',\n capabilities: ['chat', 'streaming', 'function_calling'],\n },\n // ERNIE 3.5 8k context — chat + streaming (tools support partial in 3.5).\n {\n id: 'ernie-3.5-8k',\n capabilities: ['chat', 'streaming', 'function_calling'],\n },\n // ERNIE X1.1 — reasoning model (Baidu's deep-think SKU).\n {\n id: 'ernie-x1.1',\n capabilities: ['chat', 'streaming', 'reasoning', 'thinking_mode'],\n },\n ],\n reason: 'no-list-endpoint',\n lastReviewedAt: '2026-04-28',\n },\n supports: {\n chat: true,\n streaming: true,\n tools: true,\n embeddings: true,\n vision: true,\n imageGeneration: true,\n },\n pricingMode: 'none',\n enabledByDefault: true,\n priority: 5,\n docsUrl: 'https://cloud.baidu.com/doc/WENXINWORKSHOP/index.html',\n notes:\n 'PRC-region. Content-safety filters applied server-side. Two auth paths: v1 AK+SK→OAuth (legacy, not wired in this repo) and v2 bce-v3 bearer (our canonical). Sublote A probe 2026-04-23: v2 /chat/completions with empty bearer → HTTP 401 invalid_iam_token (expected); v1 /wenxinworkshop/chat/ernie-4.0-8k with empty access_token → HTTP 200 error_code=3 \"Unsupported openapi method\" (host reachable, method routing rejects empty auth). Both surfaces confirmed alive.',\n lastReviewedAt: '2026-04-23',\n },\n {\n providerId: 'gmi',\n displayName: 'GMICloud',\n providerFamily: 'gmi',\n aliases: ['gmicloud', 'gmi-cloud', 'gmi-serving'],\n integrationClass: 'oai-compat-pure',\n integrationMode: 'discovery+execution',\n baseUrl: 'https://api.gmi-serving.com/v1',\n authScheme: 'bearer',\n apiKeyEnvVar: 'GMI_API_KEY',\n supports: {\n chat: true,\n streaming: true,\n tools: true,\n vision: true,\n embeddings: true,\n imageGeneration: true,\n videoGeneration: true,\n },\n pricingMode: 'none',\n enabledByDefault: true,\n priority: 5,\n docsUrl: 'https://docs.gmicloud.ai',\n notes:\n 'Serverless + Dedicated GPU platform. Two-plane API: inference at api.gmi-serving.com, provisioning/IAM at console.gmicloud.ai. Video API distinct from LLM API. Sublote C1 2026-04-23: /v1/chat and /v1/embeddings return 404 \"No matching target server found for model X\" BEFORE auth validation — model routing is pre-auth; 404 with unknown model does NOT imply bad key. Lot M 2026-04-23.',\n lastReviewedAt: '2026-04-23',\n },\n {\n providerId: 'infermatic',\n displayName: 'Infermatic',\n providerFamily: 'infermatic',\n integrationClass: 'oai-compat-quirks',\n integrationMode: 'discovery+execution',\n baseUrl: 'https://api.totalgpt.ai/v1',\n authScheme: 'bearer',\n apiKeyEnvVar: 'INFERMATIC_API_KEY',\n supports: {\n chat: true,\n streaming: true,\n embeddings: true,\n },\n pricingMode: 'none',\n enabledByDefault: true,\n priority: 5,\n docsUrl: 'https://docs.infermatic.ai',\n notes:\n 'Zero-logs vLLM behind LiteLLM proxy. Roleplay catalog (Sao10K, TheDrummer, Magnum). Domain mismatch: docs on docs.infermatic.ai, API on api.totalgpt.ai. Extra sampling params (top_k, repetition_penalty); some models reject system prompts. Tools NOT documented. Lot M 2026-04-23. D1 2026-04-24: live-validated — ailin-infermatic-api-key (25B sk-12T…) is model-scoped LiteLLM Virtual Key; Qwen-Qwen3-30B-A3B chat 200 (476B). Key ACL pins model list.',\n lastReviewedAt: '2026-04-24',\n },\n {\n providerId: 'inflection',\n displayName: 'Inflection AI (Pi)',\n providerFamily: 'inflection',\n aliases: ['inflection-ai', 'pi', 'pi-inflection'],\n integrationClass: 'oai-compat-pure',\n // 2026-06-15: Inflection shipped a standard OpenAI-compatible API at\n // https://api.inflection.ai/v1 (chat/completions + embeddings, Bearer auth).\n // The old proprietary /external/api/inference path needed a custom adapter;\n // the OAI surface does not. Promoted catalog-only → execution-only — there is\n // no /v1/models listing (discovery is the non-standard /v1/discovery/configs),\n // so the pinnedFallback shortlist below is the inventory.\n integrationMode: 'execution-only',\n baseUrl: 'https://api.inflection.ai/v1',\n authScheme: 'bearer',\n apiKeyEnvVar: 'INFLECTION_API_KEY',\n pinnedFallback: {\n models: [\n // Inflection-3 Pi — emotionally-intelligent companion chat.\n {\n id: 'inflection_3_pi',\n capabilities: ['chat', 'streaming'],\n },\n // Inflection-3 Productivity — instruction-following + JSON-output tuned.\n {\n id: 'inflection_3_productivity',\n capabilities: ['chat', 'streaming', 'json_mode'],\n },\n // Pi-3.1 — beta agentic SKU.\n {\n id: 'Pi-3.1',\n capabilities: ['chat', 'streaming'],\n },\n ],\n reason: 'no-list-endpoint',\n lastReviewedAt: '2026-06-15',\n },\n supports: {\n chat: true,\n streaming: true,\n },\n pricingMode: 'none',\n enabledByDefault: true,\n priority: 5,\n docsUrl: 'https://developers.inflection.ai',\n notes:\n 'OpenAI-compatible API at https://api.inflection.ai/v1 (chat/completions + embeddings, Bearer auth) — no custom adapter needed (the old /external/api/inference proprietary path did). No /v1/models listing (discovery is /v1/discovery/configs, non-standard shape); pinnedFallback carries the documented chat SKUs: inflection_3_pi, inflection_3_productivity (on /v1), Pi-3.1 (legacy OAI-compat path). Promoted first-party-native/catalog-only → oai-compat-pure/execution-only 2026-06-15.',\n lastReviewedAt: '2026-04-23',\n },\n {\n providerId: 'mancer',\n displayName: 'Mancer',\n providerFamily: 'mancer',\n integrationClass: 'oai-compat-quirks',\n integrationMode: 'discovery+execution',\n baseUrl: 'https://neuro.mancer.tech/oai/v1',\n authScheme: 'bearer',\n apiKeyEnvVar: 'MANCER_API_KEY',\n supports: {\n chat: true,\n streaming: true,\n },\n pricingMode: 'none',\n enabledByDefault: true,\n contentPolicyClass: 'uncensored',\n priority: 5,\n docsUrl: 'https://docs.mancer.tech',\n notes:\n 'Roleplay/creative (\"No filters, No guidelines, No constraints\"). Tagged contentPolicyClass=uncensored — fully admitted per universal \"habilitado e nunca censurado\" policy (Phase 4b 2026-04-28); tag is informational, downstream surfaces may filter. Credit-based pricing (not USD/token). 9 models incl. MythoMax-13B, Goliath-120B, Magnum-72B-v4. SillyTavern primary client. Lot M 2026-04-23.',\n lastReviewedAt: '2026-04-28',\n },\n {\n providerId: 'phala',\n displayName: 'Phala (RedPill TEE)',\n providerFamily: 'phala',\n aliases: ['redpill', 'red-pill'],\n integrationClass: 'oai-compat-pure',\n integrationMode: 'discovery+execution',\n baseUrl: 'https://api.redpill.ai/v1',\n authScheme: 'bearer',\n apiKeyEnvVar: 'PHALA_API_KEY',\n apiKeyEnvVarOverrideReason:\n 'RedPill (api.redpill.ai) is the runtime face of Phala Network. We canonicalize env var to `phala` for providerId parity; `redpill-key` / `redpill-api-key` stay as GCP aliases.',\n supports: {\n chat: true,\n streaming: true,\n tools: true,\n jsonMode: true,\n vision: true,\n embeddings: true,\n },\n pricingMode: 'none',\n enabledByDefault: true,\n priority: 5,\n docsUrl: 'https://docs.phala.network',\n notes:\n 'TEE-attested LLM gateway (NVIDIA H100/H200 Confidential Computing). Every response carries cryptographic attestation verifiable at /phala-cloud/confidential-ai/verify. Aggregates upstream models (Anthropic, OpenAI, DeepSeek, Qwen) running inside enclaves. Lot M 2026-04-23.',\n lastReviewedAt: '2026-04-23',\n },\n {\n providerId: 'relace',\n displayName: 'Relace',\n providerFamily: 'relace',\n integrationClass: 'first-party-native',\n integrationMode: 'catalog-only',\n baseUrl: 'https://instantapply.endpoint.relace.run/v1',\n authScheme: 'bearer',\n apiKeyEnvVar: 'RELACE_API_KEY',\n // Phase 4d (2026-04-28): renamed staticModels → pinnedFallback with\n // reason='proprietary-schema'. Relace exposes specialty surfaces\n // (/v1/code/apply, /v1/code/rerank, /v1/embeddings) instead of a\n // generic /v1/models listing — proprietary schema, dedicated\n // RelaceAdapter required. Curated shortlist is the canonical inventory\n // until adapter lands.\n pinnedFallback: {\n models: [\n // Code-apply specialty: structured-edit application, NOT general chat.\n // 'code_edit' captures the surface; we also tag 'coding' for\n // capability-search discoverability (canonical enum value).\n {\n id: 'relace-apply-3',\n capabilities: ['code_edit', 'coding'],\n },\n // Code reranker — reranking + retrieval over a code corpus.\n {\n id: 'relace-code-reranker',\n capabilities: ['reranking', 'retrieval', 'coding'],\n },\n // Code-tuned embedding.\n {\n id: 'relace-embedding',\n capabilities: ['embedding', 'embeddings', 'coding'],\n },\n ],\n reason: 'proprietary-schema',\n lastReviewedAt: '2026-04-28',\n },\n supports: {\n // Specialty code-edit — standard chat surfaces intentionally NOT declared.\n // Proprietary surfaces (codeApply, codeRerank) are not in the generic\n // `supports` enum; documented in notes below until the schema grows.\n embeddings: true,\n },\n pricingMode: 'none',\n enabledByDefault: true,\n priority: 5,\n docsUrl: 'https://docs.relace.ai',\n notes:\n 'Specialty code-edit API — NOT a general-purpose chat provider. Proprietary /v1/code/apply merge endpoint (>10k tok/s). Also offers rerank, embed, retrieval. Requires dedicated RelaceAdapter (codeApply, codeRerank); integrationMode=catalog-only until adapter exists. Note 2026-07-29: morph is NOT a real precedent — morph uses standard /v1/chat/completions, no dedicated adapter, never catalog-only. Relace genuinely needs one: its endpoints are non-chat-shaped. Lot M 2026-04-23.',\n lastReviewedAt: '2026-04-23',\n },\n {\n providerId: 'siliconflow',\n displayName: 'SiliconFlow',\n providerFamily: 'siliconflow',\n aliases: ['silicon-flow'],\n integrationClass: 'oai-compat-quirks',\n integrationMode: 'discovery+execution',\n baseUrl: 'https://api.siliconflow.cn/v1',\n authScheme: 'bearer',\n apiKeyEnvVar: 'SILICONFLOW_API_KEY',\n supports: {\n chat: true,\n streaming: true,\n tools: true,\n vision: true,\n embeddings: true,\n rerank: true,\n imageGeneration: true,\n imageEditing: true,\n videoGeneration: true,\n speechToText: true,\n },\n pricingMode: 'none',\n enabledByDefault: true,\n priority: 5,\n docsUrl: 'https://docs.siliconflow.cn',\n notes:\n 'PRC-region primary (.cn); .com fronts same service. Full multi-surface: chat+image+video+audio+rerank. Supports enable_thinking + thinking_budget (128–32768). Returns x-siliconcloud-trace-id header. Lot M 2026-04-23. D1 2026-04-24: key provisioned (ailin-siliconflow-api-key, 51B \"sk-hhc…\") but /v1/chat returns 401 bare-JSON \"Api key is invalid\" (15B oai-compat-quirks shape). sk- prefix non-canonical; probable format mismatch. Sub-class: auth-incomplete.',\n lastReviewedAt: '2026-04-24',\n },\n {\n providerId: 'stepfun',\n displayName: 'StepFun',\n providerFamily: 'stepfun',\n aliases: ['step', 'step-ai'],\n integrationClass: 'oai-compat-pure',\n integrationMode: 'discovery+execution',\n baseUrl: 'https://api.stepfun.com/v1',\n authScheme: 'bearer',\n apiKeyEnvVar: 'STEPFUN_API_KEY',\n supports: {\n chat: true,\n streaming: true,\n tools: true,\n vision: true,\n embeddings: true,\n imageGeneration: true,\n imageEditing: true,\n speechToText: true,\n textToSpeech: true,\n videoGeneration: true,\n realtime: true,\n },\n pricingMode: 'none',\n enabledByDefault: true,\n priority: 5,\n docsUrl: 'https://platform.stepfun.com/docs',\n notes:\n 'OpenAI SDK v1.0+ drop-in compat. Dual-region: api.stepfun.com (PRC) / api.stepfun.ai (global). Realtime voice + cloning. 10-min timeout → 503. Lot M 2026-04-23. D1 2026-04-24: key provisioned (ailin-stepfun-api-key, 65B) but /v1/chat returns 401 OAI-shape {\"error\":{\"message\":\"Incorrect API key provided\",\"type\":\"invalid_api_key\"}}. Endpoint alive; specific key rejected. Operator must verify key-account binding. Sub-class: auth-incomplete.',\n lastReviewedAt: '2026-04-24',\n },\n {\n providerId: 'venice',\n displayName: 'Venice AI',\n providerFamily: 'venice',\n aliases: ['venice-ai'],\n integrationClass: 'oai-compat-quirks',\n integrationMode: 'discovery+execution',\n baseUrl: 'https://api.venice.ai/api/v1',\n authScheme: 'bearer',\n apiKeyEnvVar: 'VENICE_API_KEY',\n supports: {\n chat: true,\n streaming: true,\n tools: true,\n jsonMode: true,\n vision: true,\n embeddings: true,\n imageGeneration: true,\n imageEditing: true,\n speechToText: true,\n textToSpeech: true,\n videoGeneration: true,\n },\n pricingMode: 'none',\n enabledByDefault: true,\n contentPolicyClass: 'uncensored',\n priority: 5,\n docsUrl: 'https://docs.venice.ai',\n notes:\n 'Privacy-first, no retention. Tagged contentPolicyClass=uncensored (Phase 4b 2026-04-28; informational, downstream may filter). venice_parameters: enable_web_search, character_slug, E2EE, reasoning. Async /audio/queue + /video/queue. Double-path /api/v1. Safety headers x-venice-is-blurred / -contains-minor. Sublote A 2026-04-23: /models PUBLIC; /chat HTTP 402 (USDC Base L2 eip155:8453) alt to bearer — impl bearer-only. Lot M.',\n lastReviewedAt: '2026-04-28',\n },\n // ── Alibaba Cloud (Dashscope / Qwen) — closed 2026-05-06 ──────────────\n // Closes 154 orphan rows in DB that had `provider_id='alibaba'` but no\n // catalog entry. The Alibaba model fetcher\n // (services/model-fetchers/alibaba-model-fetcher.ts) was already\n // populating the DB via `discoverySource: 'alibaba-native'` against\n // Dashscope's OpenAI-compatible endpoint. The runtime adapter side was\n // missing — this row + the standard hub-extending OAI factory wires it\n // through. Dashscope-intl (Singapore) is the default region; operators\n // in mainland China should override DASHSCOPE_BASE_URL to the\n // dashscope.aliyuncs.com endpoint.\n {\n providerId: 'alibaba',\n displayName: 'Alibaba Cloud (Dashscope / Qwen)',\n providerFamily: 'alibaba',\n aliases: ['qwen', 'dashscope', 'alicloud', 'alibaba-cloud'],\n integrationClass: 'oai-compat-pure',\n integrationMode: 'discovery+execution',\n baseUrl: 'https://dashscope-intl.aliyuncs.com/compatible-mode/v1',\n baseUrlEnvVar: 'DASHSCOPE_BASE_URL',\n authScheme: 'bearer',\n apiKeyEnvVar: 'QWEN_API_KEY',\n apiKeyEnvVarOverrideReason:\n 'QWEN_API_KEY is the canonical env var documented in Dashscope upstream docs and SDK examples',\n supports: {\n chat: true,\n streaming: true,\n tools: true,\n jsonMode: true,\n embeddings: true,\n vision: true,\n },\n pricingMode: 'remote',\n enabledByDefault: true,\n priority: 25,\n docsUrl:\n 'https://help.aliyun.com/zh/dashscope/developer-reference/compatibility-of-openai-with-dashscope',\n notes:\n 'Qwen + DeepSeek hosted via Dashscope OAI-compat endpoint. Adds qwen-coder, qwen-vl, qwen-flash, qvq-max + DeepSeek-V3.x mirrors. The pre-existing alibaba-model-fetcher.ts handles discovery (wraps OpenAI client against compatible-mode/v1); this catalog row adds the runtime adapter via the standard hub bridge. Operator override DASHSCOPE_BASE_URL=https://dashscope.aliyuncs.com/compatible-mode/v1 for mainland China region.',\n lastReviewedAt: '2026-05-06',\n },\n // ── AWS Bedrock — closed 2026-05-06 ───────────────────────────────────\n // Closes 125 orphan rows in DB that had `provider_id='aws-bedrock'` but\n // no catalog entry (the legacy switch-case path in provider-registry.ts\n // is unreachable because no config.providers entry has `name:\n // 'aws-bedrock'`). Bedrock requires SigV4 signing — proprietary schema —\n // so integrationClass=first-party-native and a dedicated factory binding\n // (`AwsBedrockAdapter`) constructs the adapter with region + creds from\n // env. Auth methods, in priority order:\n // 1. AWS_ACCESS_KEY_ID + AWS_SECRET_ACCESS_KEY (+ AWS_SESSION_TOKEN)\n // 2. AWS_BEARER_TOKEN_BEDROCK (newer Bedrock-direct auth)\n // 3. Container/Lambda role via AWS SDK default credential chain\n // The factory passes ALL of these through; the adapter chooses based on\n // SDK precedence.\n {\n providerId: 'aws-bedrock',\n displayName: 'AWS Bedrock',\n providerFamily: 'aws-bedrock',\n aliases: ['bedrock', 'aws', 'amazon', 'bedrock-runtime', 'aws-bedrock-runtime'],\n integrationClass: 'first-party-native',\n integrationMode: 'execution-only',\n // Region is selected at request time inside the adapter; baseUrl here\n // is informational/placeholder. AWS SDK constructs the actual URL.\n baseUrl: 'https://bedrock-runtime.us-east-1.amazonaws.com',\n // hmac-sigv4: Bedrock auth is AWS Signature V4. The SDK signs each\n // request with credentials from the standard AWS credential chain\n // (env vars → shared config → instance role). Declaring the canonical\n // scheme here also satisfies Zod Rule 1's apiKeyEnvVar exemption for\n // SigV4 — AWS_ACCESS_KEY_ID is the SDK's convention and would force\n // users to double-set if we added an `AWS_BEDROCK_API_KEY` alternative.\n authScheme: 'hmac-sigv4',\n apiKeyEnvVar: 'AWS_ACCESS_KEY_ID',\n apiKeyOptional: true, // role-based auth path bypasses ACCESS_KEY_ID\n adapterClass: 'AwsBedrockAdapter',\n pinnedFallback: {\n // Bedrock model IDs are vendor-pinned identifiers (anthropic.claude-*,\n // amazon.nova-*, meta.llama3-*, mistral.*). The Bedrock /foundation-models\n // listing endpoint returns thousands of vendor variants — we pin a\n // curated roster of the most commonly-used canonical IDs. Operators\n // should expand this list as AWS publishes new SKUs.\n models: [\n {\n id: 'anthropic.claude-3-5-sonnet-20241022-v2:0',\n capabilities: ['chat', 'tool_use', 'vision', 'streaming'],\n },\n {\n id: 'anthropic.claude-3-5-haiku-20241022-v1:0',\n capabilities: ['chat', 'tool_use', 'streaming'],\n },\n {\n id: 'anthropic.claude-3-opus-20240229-v1:0',\n capabilities: ['chat', 'tool_use', 'vision', 'streaming'],\n },\n { id: 'amazon.nova-pro-v1:0', capabilities: ['chat', 'tool_use', 'vision', 'streaming'] },\n { id: 'amazon.nova-lite-v1:0', capabilities: ['chat', 'tool_use', 'vision', 'streaming'] },\n { id: 'amazon.nova-micro-v1:0', capabilities: ['chat', 'streaming'] },\n { id: 'meta.llama3-1-70b-instruct-v1:0', capabilities: ['chat', 'streaming'] },\n { id: 'meta.llama3-1-8b-instruct-v1:0', capabilities: ['chat', 'streaming'] },\n { id: 'mistral.mistral-large-2407-v1:0', capabilities: ['chat', 'tool_use', 'streaming'] },\n { id: 'cohere.command-r-plus-v1:0', capabilities: ['chat', 'tool_use', 'streaming'] },\n { id: 'cohere.embed-english-v3', capabilities: ['embeddings'] },\n { id: 'cohere.embed-multilingual-v3', capabilities: ['embeddings'] },\n { id: 'amazon.titan-embed-text-v2:0', capabilities: ['embeddings'] },\n ],\n reason: 'curated-shortlist',\n lastReviewedAt: '2026-05-06',\n },\n supports: {\n chat: true,\n streaming: true,\n tools: true,\n vision: true,\n },\n pricingMode: 'remote',\n enabledByDefault: true,\n priority: 40,\n docsUrl: 'https://docs.aws.amazon.com/bedrock/',\n notes:\n 'AWS Bedrock Converse API. SigV4 auth via AWS SDK default credential chain (AWS_ACCESS_KEY_ID/SECRET, AWS_BEARER_TOKEN_BEDROCK, container role, etc.). Region from AWS_BEDROCK_REGION or AWS_REGION (default us-east-1). Inference-profile ARNs supported via AWS_BEDROCK_INFERENCE_PROFILE_ARN for cross-region routing.',\n lastReviewedAt: '2026-05-06',\n },\n\n // ──────────────────────────────────────────────────────────────────────────\n // LOTE O (2026-07-10) — Apertis + Inception Labs onboarding.\n //\n // Both had GCP secrets provisioned by the operator (ailin-apertis-key,\n // ailin-inception-key) ahead of the catalog rows. No live probe was run\n // this session (gcloud ADC needed interactive reauth, unavailable\n // non-interactively) — see consolidation-matrix.ts 'no-live-validation'\n // bucket. Two sibling providers researched in the same pass — EmpirioLabs\n // AI and Concentrate AI — are intentionally NOT added yet: EmpirioLabs\n // pending a live smoke-test (its docs cite an unfamiliar domain/model\n // lineup), Concentrate AI pending its own bespoke discovery fetcher\n // (nested per-provider model shape, doesn't fit the generic hub fetcher).\n // ──────────────────────────────────────────────────────────────────────────\n {\n providerId: 'apertis',\n displayName: 'Apertis',\n providerFamily: 'apertis',\n // Gateway: re-serves 400+ models from 30+ upstream vendors (OpenAI,\n // Anthropic, Google, xAI, Qwen, DeepSeek, ...) behind one OpenAI-\n // compatible surface. `owned_by` on /v1/models attributes the real\n // upstream vendor per model.\n integrationClass: 'gateway',\n integrationMode: 'discovery+execution',\n baseUrl: 'https://api.apertis.ai/v1',\n paths: {\n embeddings: '/embeddings',\n // rerank REMOVED 2026-07-16: /v1/rerank returns 404 \"Invalid URL\" —\n // the route does not exist on the gateway even though reranker models\n // (BAAI/bge-reranker-v2-m3, Qwen3-Reranker-*) appear in ITS /v1/models\n // catalog. Advertised-but-unrouted; re-add only after a live 200.\n imagesGenerate: '/images/generations',\n // imagesEdit REMOVED 2026-07-16: the /v1/images/edits route parses the\n // multipart body but DROPS the `model` form field — every attempt\n // (model first, model last, different model ids) failed with\n // `The model '' does not exist`. Gateway-side defect; a standard\n // OpenAI SDK images.edit() call would fail identically.\n audioSpeech: '/audio/speech',\n audioTranscriptions: '/audio/transcriptions',\n },\n authScheme: 'bearer',\n apiKeyEnvVar: 'APERTIS_API_KEY',\n adapterClass: 'ApertisAdapter',\n originalProviderField: 'owned_by',\n supports: {\n chat: true,\n streaming: true, // live-probed 2026-07-16: token SSE + [DONE]\n embeddings: true, // live-probed 2026-07-16: text-embedding-3-small 200\n // rerank / imageEditing removed 2026-07-16 — see the paths comments:\n // both surfaces are broken on the provider side (unrouted endpoint /\n // dropped multipart field), not merely untested.\n imageGeneration: true, // route exists; upstream 429-saturated during the 2026-07-16 probe (dall-e-3), so functionally unconfirmed\n speechToText: true, // live-probed 2026-07-16: whisper-1 transcribed real audio\n textToSpeech: true, // route exists (tts-1 in catalog); upstream 429-saturated on all 3 probe attempts, functionally unconfirmed\n vision: true,\n },\n pricingMode: 'none', // hybrid subscription-multiplier + PAYG billing; doesn't fit the remote per-token extractor\n enabledByDefault: true,\n docsUrl: 'https://docs.apertis.ai/api/',\n notes:\n 'Multi-vendor gateway (400+ models, new-api-style). ApertisAdapter disables its native fallback_models so failures surface to our orchestrator. Probe 2026-07-16: SSE/embeddings/STT proven; imageGen+TTS routes exist, upstream 429-saturated (unconfirmed); rerank 404 + images/edits drops the model field — both removed from supports (see paths comments). dall-e-2 absent; use dall-e-3/gpt-image-*. Video async, out of scope. sk-sub- keys 403 on /audio.',\n lastReviewedAt: '2026-07-16',\n },\n {\n providerId: 'inception',\n displayName: 'Inception Labs (Mercury)',\n providerFamily: 'inception',\n // First-party inference (NOT a broker) — Mercury is a diffusion LLM\n // (dLLM), not autoregressive. OpenAI-compatible chat shape with real\n // quirks: restricted sampling-param surface and a `diffusing` streaming\n // mode with a non-standard SSE contract (see InceptionAdapter).\n integrationClass: 'oai-compat-quirks',\n integrationMode: 'discovery+execution',\n baseUrl: 'https://api.inceptionlabs.ai/v1',\n authScheme: 'bearer',\n apiKeyEnvVar: 'INCEPTION_API_KEY',\n adapterClass: 'InceptionAdapter',\n supports: {\n chat: true,\n streaming: true,\n tools: true,\n jsonMode: true,\n reasoning: true,\n },\n capabilityHints: [{ capability: 'diffusion_lm', rationale: 'docs-declared', confidence: 0.6 }],\n pricingMode: 'none',\n enabledByDefault: true,\n docsUrl: 'https://docs.inceptionlabs.ai/get-started/get-started',\n notes:\n 'dLLM (diffusion). NEVER send diffusing:true — confirmed live 2026-07-16: chunks carry diffusion_meta and the FULL rewritten text (naive delta concatenation duplicates output); InceptionAdapter drops the flag and logs. Normal SSE + tool-calling proven same day. temperature server-clamped [0.5,1.0]; adapter clamps client-side. Text-only. FIM/Next-Edit out of scope (non-chat payload).',\n lastReviewedAt: '2026-07-16',\n },\n\n // ──────────────────────────────────────────────────────────────────────────\n // LOTE P (2026-07-11) — EmpirioLabs AI onboarding.\n //\n // Held back in LOTE O pending live validation (docs cited an unfamiliar\n // domain/model lineup — Qwen3-Max, Seed 2.0 Pro, Kling O3, GLM-TTS —\n // that didn't match any known catalog). Operator re-authenticated gcloud\n // (device-code flow) and the real ailin-empiriolabs-key was probed live:\n // GET /v1/models 200 (134+ models incl. real Kling/DeepSeek/Zhipu/Qwen\n // entries) and POST /v1/chat/completions 200 with deepseek-v4-flash\n // (\"pong\", cost_usd tracked in the response). The domain and models are\n // real — validation gate satisfied, plain catalog-only entry (no quirks\n // requiring a dedicated adapter surfaced for the chat surface).\n // ──────────────────────────────────────────────────────────────────────────\n {\n providerId: 'empiriolabs',\n displayName: 'EmpirioLabs AI',\n providerFamily: 'empiriolabs',\n integrationClass: 'gateway',\n integrationMode: 'discovery+execution',\n baseUrl: 'https://api.empiriolabs.ai/v1',\n paths: {\n embeddings: '/embeddings',\n rerank: '/reranks',\n // Live probe 2026-07-17: POST /videos/generations resolves MODELS\n // (body {} → \"model_not_found\" naming a model, i.e. the route exists\n // and got past routing to model resolution). Production attempt on the\n // same day returned 402 insufficient_credits — the surface works; the\n // only blocker is account balance (operator funding decision).\n videoGenerate: '/videos/generations',\n },\n authScheme: 'bearer',\n apiKeyEnvVar: 'EMPIRIOLABS_API_KEY',\n // Confirmed via live probe: model entries carry `\"provider\":\"<vendor>\"`\n // (e.g. \"kling\", \"zhipu\", \"deepseek\"), not OpenAI's `owned_by`.\n originalProviderField: 'provider',\n supports: {\n chat: true,\n streaming: true,\n tools: true,\n jsonMode: true,\n vision: true,\n embeddings: true,\n rerank: true,\n videoGeneration: true,\n },\n pricingMode: 'none', // heterogeneous per-unit billing (token/request/image/second); per-response cost_usd is real but /models pricing shape doesn't fit the remote extractor\n enabledByDefault: true,\n docsUrl: 'https://docs.empiriolabs.ai/welcome',\n notes:\n 'Multi-vendor gateway (134+ models). Probe 2026-07-16: SSE, embeddings (text-embedding-v4), rerank (/reranks PLURAL, qwen3-rerank) and system-override all proven — our system message fully replaces the platform default. Its catalog category field is authoritative (embedding/reranker). CAVEAT: deepseek-v4-flash streams into delta.reasoning_content, content empty at low max_tokens — budget generously. Only /v1/chat/completions wired. Probe 2026-07-17: video route live (blocked only by 402 credits).',\n lastReviewedAt: '2026-07-17',\n },\n\n // ──────────────────────────────────────────────────────────────────────────\n // LOTE Q (2026-07-12) — Concentrate AI onboarding.\n //\n // Meta-aggregator: re-serves OpenAI/Anthropic/Bedrock/Vertex/Azure/Mistral/\n // Cohere/xAI/DeepSeek/MiniMax/z.ai/Moonshot/etc behind one surface (its own\n // `list-providers` enumerates 20 upstream slugs, incl. itself as\n // `concentrate`/`redact` — a PII-redaction feature, not a real inference\n // vendor). Per operator decision (2026-07-10): no gap-filler dedup engineering\n // — cataloged as a normal `gateway`, trusting the pool-builder's cost/quality\n // ranking to naturally deprioritize routes with proxy markup.\n //\n // Discovery is UNAUTHENTICATED — live-probed without any key 2026-07-12:\n // GET /v1/models/providers 200 (20 provider slugs, matches doc exactly) and\n // GET /v1/models/ 200 (`{object:\"list\",data:[{id,owned_by,...}]}` — same\n // shape the generic hub fetcher already expects, so NO custom fetcher is\n // needed despite the nested per-provider shape on the enrichment endpoints\n // (list-models-by-provider/get-model/get-provider-info) — those are richer\n // cross-reference endpoints we don't need for basic catalog population.\n // Execution (chat/completions) requires the provisioned key; gcloud ADC\n // expired again this session before it could be probed live — see\n // consolidation-matrix.ts `no-live-validation`.\n // ──────────────────────────────────────────────────────────────────────────\n {\n providerId: 'concentrate',\n displayName: 'Concentrate AI',\n providerFamily: 'concentrate',\n integrationClass: 'gateway',\n integrationMode: 'discovery+execution',\n baseUrl: 'https://api.concentrate.ai/v1',\n paths: {\n modelList: ['/models/'],\n chatCompletions: '/chat/completions/',\n responses: '/responses/',\n },\n authScheme: 'bearer',\n apiKeyEnvVar: 'CONCENTRATE_API_KEY',\n originalProviderField: 'owned_by',\n supports: {\n chat: true,\n responses: true,\n streaming: true,\n tools: true,\n },\n pricingMode: 'none', // pricing lives on the enrichment endpoints (get-model/get-provider-info), not on the flat /v1/models/ list the generic extractor reads\n enabledByDefault: true,\n docsUrl: 'https://concentrate.ai/docs/api-reference/introduction',\n notes:\n 'Meta-aggregator (20 upstream providers). Discovery unauthenticated; execution paths carry a TRAILING SLASH. Probe 2026-07-16: SSE + /responses/ proven (azure/gpt-4o-mini routing visible, cost breakdown in payload). 424 = ITS upstream vendor failed — classified retryable server_error by provider-error-classifier. Its concentrate/redact-v1 listing is a PII-redaction feature, not an inference vendor.',\n lastReviewedAt: '2026-07-16',\n },\n\n // ──────────────────────────────────────────────────────────────────────────\n // LOTE R (2026-07-13) — FastRouter onboarding.\n //\n // Meta-aggregator (\"control plane for routing across multiple LLM\n // providers\") — re-serves 20 upstream providers (OpenAI, Anthropic, Azure,\n // Bedrock, Vertex, Groq, Together, Fireworks, DeepInfra, Moonshot, MiniMax,\n // Perplexity, X-AI, Nebius, Baseten, FAL AI, Leonardo, Pollo, BytePlus,\n // itself). Per operator decision (2026-07-10, same as apertis/empiriolabs/\n // concentrate): no gap-filler dedup engineering — cataloged as a normal\n // `gateway`, trusting the pool-builder's cost/quality ranking.\n //\n // Discovery is UNAUTHENTICATED — live-probed without any key 2026-07-13:\n // GET /api/v1/providers 200 (21 provider_id/label pairs) and GET\n // /api/v1/models 200 (real data, `{data:[{id,creator,pricing,...}]}` —\n // same shape the generic hub fetcher expects, INCLUDING a `pricing` object\n // directly on the flat list — unlike apertis/empiriolabs/concentrate,\n // pricingMode can be `remote` here). Execution (chat/completions) requires\n // the provisioned key; gcloud ADC was mid-reauth at catalog-row time — see\n // consolidation-matrix.ts `no-live-validation`.\n // ──────────────────────────────────────────────────────────────────────────\n {\n providerId: 'fastrouter',\n displayName: 'FastRouter',\n providerFamily: 'fastrouter',\n integrationClass: 'gateway',\n integrationMode: 'discovery+execution',\n baseUrl: 'https://api.fastrouter.ai/api/v1',\n authScheme: 'bearer',\n apiKeyEnvVar: 'FASTROUTER_API_KEY',\n // Confirmed via live probe: model entries carry `\"creator\":\"<vendor>\"`\n // (e.g. \"Anthropic\", \"Google\", \"DeepSeek\"), not OpenAI's `owned_by`.\n originalProviderField: 'creator',\n paths: {\n // Live probe 2026-07-17: video is an ASYNC QUEUE at POST /videos\n // (`/videos/generations` has NEVER existed here — 404 Route not found).\n // Submit returns `{data:{taskId,status:\"processing\"}}` (FastRouter runs\n // its own upstream failover per fr_failover_history); poll GET\n // /videos/{taskId} until data.generations[]/fastrouter_assets.urls[]\n // appear. No cancel route (DELETE and /cancel both 404). CAUTION: the\n // submit endpoint accepts `{model}` WITHOUT prompt and starts a real\n // billable job — never probe it with a routable model id.\n videoGenerate: '/videos',\n videoPoll: '/videos/{taskId}',\n },\n supports: {\n chat: true,\n streaming: true,\n tools: true,\n embeddings: true,\n reasoning: true,\n videoGeneration: true,\n },\n pricingMode: 'remote',\n enabledByDefault: true,\n docsUrl: 'https://docs.fastrouter.ai/',\n notes:\n 'Meta-aggregator (20 upstream, BYOK). /api/v1 canonical (docs show both prefixes). Probe 2026-07-16: SSE proven (:FASTROUTER PROCESSING comment keep-alives + own final usage chunk with cost+provider before [DONE]); embeddings proven; :flex suffix proven (service_tier flex). Model ids provider/model may carry :price/:throughput/:flex — do not strip. fastrouter/auto not used (ci does its own selection). 402 = insufficient credits. Probe 2026-07-17: video async-queue surface mapped live (see paths).',\n lastReviewedAt: '2026-07-17',\n },\n\n // ──────────────────────────────────────────────────────────────────────────\n // LOTE S (2026-07-13) — Perplexity Agent API onboarding.\n //\n // Operator asked whether the EXISTING `perplexity` row (classic Sonar-only\n // /chat/completions) also served Anthropic/OpenAI/Google/xAI/Z.AI/Moonshot/\n // NVIDIA models. It doesn't — but a SEPARATE Perplexity product does: the\n // \"Agent API\" (`/v1/agent`, aliased `/v1/responses`), a genuinely different\n // wire protocol (Responses-style input/output, not messages/choices) under\n // the SAME Perplexity account/key. Modeled as its own catalog row rather\n // than extending `perplexity` — the two surfaces have disjoint model\n // namespaces (sonar-* vs vendor/model), disjoint wire shapes, and one\n // catalog row = one execution surface is the established convention here.\n //\n // Live-probed 2026-07-13 with the real ailin-perplexity-api-key (same\n // GCP secret as the classic `perplexity` row — no separate key was\n // provisioned; PROVIDER_SECRETS below points PERPLEXITY_AGENT_API_KEY at\n // the same secret names):\n // - GET /v1/models 200, AUTHENTICATED, real OpenAI-list shape\n // (`{data:[{id,object,owned_by,created}]}`) — 32 models across\n // anthropic/google/nvidia/openai/xai/perplexity(-owned: glm-5.2,\n // kimi-k2.7-code, sonar).\n // - POST /v1/agent 200 for: anthropic/claude-haiku-4-5, openai/gpt-5.4-mini\n // (NOT \"gpt-5-mini\" — that id doesn't exist, hangs), google/gemini-3.5-flash,\n // xai/grok-4.5, perplexity/glm-5.2 (z.ai). Also confirmed `input` accepts\n // a role+content array (same shape as our ChatMessage[]), not just a\n // flat string.\n // - perplexity/kimi-k2.7-code (Moonshot): accepted by model-id validation\n // (wrong ids reject fast with 400; this one doesn't) but every live call\n // hung with zero response bytes past 90s — left wired, NOT confirmed\n // working. Not in live-validation evidence below for that reason.\n // ──────────────────────────────────────────────────────────────────────────\n {\n providerId: 'perplexity-agent',\n displayName: 'Perplexity Agent API',\n providerFamily: 'perplexity-agent',\n integrationClass: 'gateway',\n integrationMode: 'discovery+execution',\n baseUrl: 'https://api.perplexity.ai/v1',\n authScheme: 'bearer',\n // Shares the same physical credential as the classic `perplexity` row\n // (one Perplexity account, two API surfaces) — deliberately a distinct\n // env var (not a typo/dup) so PROVIDER_SECRETS can source both from the\n // same GCP secret independently. Naturally matches the <PROVIDERID>_API_KEY\n // convention (perplexity-agent -> PERPLEXITY_AGENT_API_KEY); no Zod Rule 1\n // override needed.\n apiKeyEnvVar: 'PERPLEXITY_AGENT_API_KEY',\n adapterClass: 'PerplexityAgentAdapter',\n originalProviderField: 'owned_by',\n supports: {\n chat: true,\n streaming: true, // real SSE since 2026-07-16 — Responses-dialect parser in the adapter (shape captured live)\n tools: true, // live-probed 2026-07-16: flat Responses-style tools in, function_call items out (adapter converts both directions)\n reasoning: true,\n },\n pricingMode: 'none', // /v1/models discovery response carries no pricing object (unlike fastrouter); cost is only visible per-response (usage.cost) after a real call\n enabledByDefault: true,\n docsUrl: 'https://docs.perplexity.ai/docs/agent-api/models',\n notes:\n 'Second Perplexity surface (same account/key as `perplexity`): /v1/agent, Responses-style, NOT chat/completions. REAL token streaming since 2026-07-16 (response.output_text.delta / response.completed; NO [DONE]; gappy sequence_number) — unit-tested against the live-captured fixture. Tools proven live (flat shape; toolu_bdrk_ ids reveal Bedrock-served Anthropic). Moonshot kimi-k2.7-code works but hangs intermittently when cold — orchestrator timeout+fallback covers it.',\n lastReviewedAt: '2026-07-16',\n },\n\n // ──────────────────────────────────────────────────────────────────────────\n // LOTE T (2026-07-13) — Ailin gateway. Self-referential meta-provider. Lets\n // any deployment of this engine (this codebase, self-hosted by a third\n // party) reach the full ~100-provider / 70k+-model catalog through a single\n // AILIN_API_KEY instead of provisioning every upstream provider's own key\n // directly — see api.ailin.one's own docs/reference/endpoints/authentication.md,\n // which already documents AILIN_API_KEY as the `X-API-Key` credential\n // customers send inbound. This entry is the outbound mirror: THIS engine,\n // acting as a client of api.ailin.one. Inert for Ailin's own canonical\n // api.ailin.one deployment, since nothing there sets AILIN_API_KEY — the\n // loader's missing-api-key skip path (catalog-loader.ts) makes that a no-op,\n // not a self-call loop.\n //\n // discovery+execution, with a known heuristic gap: chat/embeddings/images/\n // audio were verified against api.ailin.one's own openapi-spec.yaml to be\n // bare OpenAI-compatible request/response shape at the generic hub's\n // default paths (no `paths` override needed). GET /v1/models was checked\n // too — its `data[]` items carry `id` and `originProvider` (both understood\n // by the generic fetcher: `id` always, `originProvider` via\n // `originalProviderField` below) but ALSO ailin-native fields the generic\n // fetcher does not understand (`operability`, `nonOperationalReasons`,\n // `fallbackChain`, `resolvedProvider`). Net effect: discovery will surface\n // every listed model, including ones ailin's own orchestration currently\n // marks `non_operational` — those may 4xx on execution until a dedicated\n // fetcher maps the richer shape (follow-up, not guessed at here). Given\n // the ~70k-model, constantly-shifting catalog, a hand-pinned fallback list\n // would be stale on arrival and violates this project's no-static-model\n // rule, so this entry accepts the heuristic gap over inventing one.\n // ──────────────────────────────────────────────────────────────────────────\n {\n providerId: 'ailin',\n displayName: 'Ailin (api.ailin.one)',\n providerFamily: 'ailin',\n integrationClass: 'gateway',\n integrationMode: 'discovery+execution',\n baseUrl: 'https://api.ailin.one/v1',\n authScheme: 'api-key-header',\n authHeaderName: 'X-API-Key',\n apiKeyEnvVar: 'AILIN_API_KEY',\n originalProviderField: 'originProvider',\n supports: {\n chat: true,\n embeddings: true,\n streaming: true,\n tools: true,\n jsonMode: true,\n vision: true,\n reasoning: true,\n imageGeneration: true,\n imageEditing: true,\n speechToText: true,\n textToSpeech: true,\n moderation: true,\n },\n pricingMode: 'none',\n enabledByDefault: true,\n priority: 0,\n notes:\n \"Meta-gateway to ~100 upstream providers / 70k+ models behind api.ailin.one's own orchestration. Discovery uses the generic hub fetcher against a richer native /v1/models shape it only partly understands (see header comment) — operability filtering is a known follow-up gap. Auth is X-API-Key; api.ailin.one also accepts bearerAuth JWT for user sessions, not used here (static server credential).\",\n lastReviewedAt: '2026-07-13',\n },\n // ──────────────────────────────────────────────────────────────────────────\n // Sakana AI (Fugu) — added 2026-07-29 in PR #234, promoted to fully\n // operational the same day once the operator activated pay-as-you-go\n // billing on the account (previously had no active subscription, so every\n // chat/completions call 429'd regardless of integration correctness).\n // - GET /v1/models -> 200, real 5-model array (fugu, fugu-ultra,\n // fugu-ultra-20260615, fugu-ultra-v1.0, fugu-ultra-v1.1) even though\n // NEITHER docs page (console.sakana.ai/get-started or /models)\n // documents this endpoint. Shape matches the generic\n // OpenAICompatibleHubModelFetcher (`data[].id`) — no dedicated\n // fetcher/pinnedFallback needed.\n // - POST /v1/chat/completions: after billing activation, a real\n // end-to-end re-verification was run directly against the live API\n // (2026-07-29, GCP secret `ailin-sakana-ai-key`) and confirmed chat,\n // streaming, tool calls, JSON mode, and vision ALL genuinely work —\n // real HTTP 200 responses with correct, sensible content, not merely\n // the absence of an error. Vision in particular is now evidence-\n // backed rather than doc-inferred: a multimodal request containing an\n // inline image correctly identified the image's content, and\n // prompt_tokens jumped sharply on that call versus text-only calls —\n // proof the image was actually processed, not silently dropped. That\n // DIRECTLY CONTRADICTS console.sakana.ai/models' own capability\n // table, which marks Vision unsupported (✗); the live probe is\n // trusted over the vendor's apparently stale/incorrect docs.\n // - Observed quirk, noted but not acted on: `usage.completion_tokens_\n // details.reasoning_tokens` showed up on a plain `fugu` call even\n // though nothing in the request asked for reasoning — worth watching,\n // not itself a reason to change anything (`supports.reasoning` was\n // already `true`).\n // - fugu-ultra appears to route requests through an internal\n // multi-agent orchestration layer (matches its docs' \"routes 1-3\n // agents\" description) — `usage.prompt_tokens_details.\n // orchestration_input_tokens` reflects that on fugu-ultra calls.\n // - fugu-cyber / fugu-cyber-v1.0 are excluded via modelDenylist: the\n // pricing docs say cyber \"requires access request approval\" and is\n // \"available only through pay-as-you-go billing\"; this key's\n // /v1/models response does not list either cyber model, consistent\n // with no cyber entitlement. Deliberately not advertising a model\n // most keys can't call.\n // - pricingMode is `none`, not `remote`: neither /v1/models nor\n // chat/completions responses expose pricing/context-window fields,\n // and this catalog's `static-file` PricingMode is reserved/\n // unimplemented. For reference only (marketing page, not machine-\n // read): fugu-ultra $5/$30 per 1M in/out tokens standard, $10/$45\n // above 272K context, $0.50/$1.00 cached; fugu-cyber $6/$36 standard,\n // $12/$54 above 272K, $0.60/$1.20 cached; base fugu has no per-token\n // price — subscription-only (Standard $20/mo, Pro $100/mo, Max\n // $200/mo).\n // - apiKeyEnvVar follows the `<PROVIDER_ID_UPPER>_API_KEY` convention\n // (SAKANA_AI_API_KEY, not the shorter SAKANA_API_KEY) per Rule 1 in\n // provider-catalog.schema.ts — Sakana has no well-known upstream SDK\n // env-var name to justify an override.\n // ──────────────────────────────────────────────────────────────────────────\n {\n providerId: 'sakana-ai',\n displayName: 'Sakana AI (Fugu)',\n providerFamily: 'sakana-ai',\n integrationClass: 'oai-compat-pure',\n integrationMode: 'discovery+execution',\n baseUrl: 'https://api.sakana.ai/v1',\n baseUrlEnvVar: 'SAKANA_AI_BASE_URL',\n authScheme: 'bearer',\n apiKeyEnvVar: 'SAKANA_AI_API_KEY',\n supports: {\n chat: true,\n streaming: true,\n tools: true,\n jsonMode: true,\n vision: true,\n reasoning: true,\n },\n pricingMode: 'none',\n modelDenylist: ['fugu-cyber', 'fugu-cyber-v1.0'],\n enabledByDefault: true,\n priority: 35,\n docsUrl: 'https://console.sakana.ai/get-started',\n notes:\n 'Fugu chat/tools/reasoning/vision models. Live-verified 2026-07-29: /v1/models works (5 real models). Billing activated same day; end-to-end re-verification confirmed chat, streaming, tools, JSON mode, and vision all genuinely work — real HTTP 200s with correct content. Vision is evidence-backed despite console.sakana.ai/models marking it unsupported. fugu-cyber excluded via modelDenylist (approval-gated). pricingMode=none: no pricing in API responses; see header comment for detail.',\n lastReviewedAt: '2026-07-29',\n },\n] as const satisfies readonly ProviderCatalogEntry[];\n"}, "9478f2ca54e894d213553c9fc0b8c1fd69d488bc": {"t": 1785846899.78658, "n": 160496, "h": "a7f9b98664b2dce8", "c": "// Copyright (C) 2026 Ailin One, Inc.\n//\n// This file is part of Collective Intelligence Engine (ci).\n// Licensed under the GNU Affero General Public License v3.0 or later.\n// See LICENSE in the repository root, or <https://www.gnu.org/licenses/>.\n//\n// SPDX-License-Identifier: AGPL-3.0-or-later\n// Source: https://github.com/ailinone/collective-intelligence\n\n/**\n * Provider Catalog — Data-Driven Entries\n *\n * Each entry registers ONE provider. Adding a new OpenAI-compatible provider\n * typically costs ~15 lines here and ZERO lines in `provider-registry.ts`.\n *\n * AUTHORING RULES (enforced by Zod schema):\n * - `providerId`: lowercase-kebab-case, unique.\n * - `apiKeyEnvVar`: must match `<PROVIDER_ID_UPPER>_API_KEY` unless the\n * authScheme is hmac-sigv4/iam-token/oauth2/custom.\n * - `baseUrl`: https only, unless self-hosted.\n * - `supports` reflects the provider's declared surface. Hints go to\n * `capabilityHints`. The PER-MODEL capability resolution is STILL done\n * by the HCRA merger with full provenance.\n *\n * WHAT THIS CATALOG IS NOT:\n * - It is NOT a per-model capability table.\n * - It is NOT an authoritative pricing source.\n * - It is NOT a replacement for the first-party adapters (OpenAI, Anthropic,\n * Google, etc.) — those remain as dedicated classes in provider-registry.ts.\n *\n * WHEN TO ADD HERE vs WRITE A NEW ADAPTER:\n * - Provider speaks OpenAI `/v1/chat/completions` and `/v1/models` → catalog entry only.\n * - Provider has quirks (citations, custom headers, non-standard list path) →\n * catalog entry with `integrationClass: 'oai-compat-quirks'` + overrides.\n * - Provider has a fundamentally different API shape (Bedrock Converse,\n * Anthropic Messages, Vertex Generate) → dedicated adapter class +\n * `adapterClass` field pointing to it.\n */\n\nimport type { ProviderCatalogEntry } from './provider-catalog.types';\n\n/**\n * Complete catalog. Order is the preferred registration order, which also\n * becomes the default execution priority tiebreaker (earlier = higher).\n */\nexport const PROVIDER_CATALOG: readonly ProviderCatalogEntry[] = [\n // ──────────────────────────────────────────────────────────────────────────\n // LOTE A — OpenAI-compatible pure providers\n // These speak the OpenAI protocol faithfully. Reuse the hub fetcher/adapter.\n // ──────────────────────────────────────────────────────────────────────────\n {\n providerId: 'groq',\n displayName: 'Groq',\n providerFamily: 'groq',\n integrationClass: 'oai-compat-quirks', // reasoning_format / reasoning_effort / service_tier — not pure\n integrationMode: 'discovery+execution',\n baseUrl: 'https://api.groq.com/openai/v1',\n authScheme: 'bearer',\n apiKeyEnvVar: 'GROQ_API_KEY',\n adapterClass: 'GroqAdapter',\n supports: {\n chat: true,\n streaming: true,\n tools: true,\n jsonMode: true,\n vision: true, // llama-3.2 vision family\n },\n capabilityHints: [\n { capability: 'low_latency', rationale: 'provider-class-default', confidence: 0.7 },\n ],\n pricingMode: 'none',\n enabledByDefault: true,\n priority: 50,\n docsUrl: 'https://console.groq.com/docs/api-reference',\n notes:\n 'Custom LPU hardware, sub-second latency for Llama/Mixtral/Gemma. Dedicated adapter injects reasoning_format/reasoning_effort/service_tier for OSS-reasoning models (gpt-oss, deepseek-r1, qwen-qwq, compound-beta).',\n lastReviewedAt: '2026-04-22',\n },\n {\n providerId: 'togetherai',\n displayName: 'Together AI',\n providerFamily: 'togetherai',\n integrationClass: 'oai-compat-pure',\n integrationMode: 'discovery+execution',\n baseUrl: 'https://api.together.xyz/v1',\n authScheme: 'bearer',\n apiKeyEnvVar: 'TOGETHERAI_API_KEY',\n // Live probe 2026-07-17: POST /videos/generations EXISTS and requires the\n // Together-specific body `{model, payload:{...}}` — the flat OAI shape is\n // rejected with \"validation failed for field 'payload': expected\n // required\". Video model ids validated as accepted at the field layer:\n // openai/sora-2, google/veo-3.0-fast, kwaivgI/kling-2.1-standard (their\n // GET /v1/models lists them with type \"video\"). Inner payload fields are\n // per-model; response/poll contract still to be proven by a first real\n // generation.\n videoRequestStyle: 'payload-wrap',\n supports: {\n chat: true,\n embeddings: true,\n streaming: true,\n tools: true,\n jsonMode: true,\n imageGeneration: true, // FLUX, SDXL hosted\n vision: true,\n videoGeneration: true,\n },\n pricingMode: 'remote',\n enabledByDefault: true,\n priority: 40,\n docsUrl: 'https://docs.together.ai/reference',\n notes:\n 'D1 2026-04-24: provisioned key (ailin-togetherai-api-key) returned 401 on /v1/chat — \"key_\" prefix non-canonical; operator to re-issue if chat 401 persists. Probe 2026-07-17: video surface live-validated to the field-validation layer with THIS key (payload-wrap body; see videoRequestStyle) — auth accepted there.',\n lastReviewedAt: '2026-07-17',\n originalProviderField: 'organization',\n },\n {\n providerId: 'fireworks-ai',\n displayName: 'Fireworks AI',\n providerFamily: 'fireworks-ai',\n aliases: ['fireworks', 'fireworks_ai'],\n integrationClass: 'oai-compat-pure',\n integrationMode: 'discovery+execution',\n baseUrl: 'https://api.fireworks.ai/inference/v1',\n authScheme: 'bearer',\n apiKeyEnvVar: 'FIREWORKS_AI_API_KEY',\n supports: {\n chat: true,\n embeddings: true,\n streaming: true,\n tools: true,\n jsonMode: true,\n imageGeneration: true,\n vision: true,\n },\n pricingMode: 'remote',\n enabledByDefault: true,\n priority: 40,\n docsUrl: 'https://docs.fireworks.ai/api-reference',\n lastReviewedAt: '2026-04-21',\n },\n {\n providerId: 'deepinfra',\n displayName: 'DeepInfra',\n providerFamily: 'deepinfra',\n integrationClass: 'oai-compat-pure',\n integrationMode: 'discovery+execution',\n baseUrl: 'https://api.deepinfra.com/v1/openai',\n authScheme: 'bearer',\n apiKeyEnvVar: 'DEEPINFRA_API_KEY',\n supports: {\n chat: true,\n embeddings: true,\n streaming: true,\n tools: true,\n imageGeneration: true,\n speechToText: true,\n textToSpeech: true,\n },\n pricingMode: 'remote',\n enabledByDefault: true,\n priority: 35,\n docsUrl: 'https://deepinfra.com/docs',\n lastReviewedAt: '2026-04-21',\n },\n {\n providerId: 'perplexity',\n displayName: 'Perplexity AI',\n providerFamily: 'perplexity',\n integrationClass: 'oai-compat-quirks',\n integrationMode: 'discovery+execution',\n baseUrl: 'https://api.perplexity.ai',\n authScheme: 'bearer',\n apiKeyEnvVar: 'PERPLEXITY_API_KEY',\n adapterClass: 'PerplexityAdapter',\n paths: {\n // Perplexity has no /models endpoint. The pinnedFallback below carries\n // the canonical Sonar family list (Phase 4d, 2026-04-28 — was staticModels).\n chatCompletions: '/chat/completions',\n },\n pinnedFallback: {\n // Operator-declared capabilities (root-cause refactor 2026-04-28).\n // All Sonar models are research-grounded chat with web_search; the\n // *-reasoning-* family adds extended reasoning. Declared here directly\n // so the catalog-bridge does NOT fall back to name-regex inference.\n models: [\n { id: 'sonar-small-online', capabilities: ['chat', 'streaming', 'web_search'] },\n { id: 'sonar-medium-online', capabilities: ['chat', 'streaming', 'web_search'] },\n { id: 'sonar-large-online', capabilities: ['chat', 'streaming', 'web_search'] },\n { id: 'sonar-pro', capabilities: ['chat', 'streaming', 'web_search'] },\n {\n id: 'sonar-reasoning',\n capabilities: ['chat', 'streaming', 'web_search', 'reasoning', 'thinking_mode'],\n },\n {\n id: 'sonar-reasoning-pro',\n capabilities: ['chat', 'streaming', 'web_search', 'reasoning', 'thinking_mode'],\n },\n ],\n reason: 'no-list-endpoint',\n lastReviewedAt: '2026-04-22',\n },\n supports: {\n chat: true,\n streaming: true,\n },\n capabilityHints: [\n { capability: 'web_search', rationale: 'provider-class-default', confidence: 0.85 },\n { capability: 'deep_research', rationale: 'docs-declared', confidence: 0.7 },\n ],\n pricingMode: 'none',\n enabledByDefault: true,\n priority: 30,\n docsUrl: 'https://docs.perplexity.ai/api-reference/chat-completions',\n notes:\n 'All models are web-search-grounded. Response includes citations[], related_questions[], images[]. Dedicated adapter preserves those extension fields on PerplexityChatResponse.',\n lastReviewedAt: '2026-04-22',\n },\n {\n providerId: 'cerebras',\n displayName: 'Cerebras',\n providerFamily: 'cerebras',\n integrationClass: 'oai-compat-quirks', // max_completion_tokens normalization\n integrationMode: 'discovery+execution',\n baseUrl: 'https://api.cerebras.ai/v1',\n authScheme: 'bearer',\n apiKeyEnvVar: 'CEREBRAS_API_KEY',\n adapterClass: 'CerebrasAdapter',\n supports: {\n chat: true,\n streaming: true,\n tools: true,\n jsonMode: true,\n },\n capabilityHints: [\n { capability: 'low_latency', rationale: 'provider-class-default', confidence: 0.75 },\n ],\n pricingMode: 'none',\n enabledByDefault: true,\n priority: 45,\n docsUrl: 'https://inference-docs.cerebras.ai/api-reference',\n notes:\n 'Wafer-scale inference; fastest Llama-70B in market. Dedicated adapter normalizes max_completion_tokens ↔ max_tokens per Cerebras docs.',\n lastReviewedAt: '2026-04-22',\n },\n {\n providerId: 'hyperbolic',\n displayName: 'Hyperbolic',\n providerFamily: 'hyperbolic',\n integrationClass: 'oai-compat-pure',\n integrationMode: 'discovery+execution',\n baseUrl: 'https://api.hyperbolic.xyz/v1',\n authScheme: 'bearer',\n apiKeyEnvVar: 'HYPERBOLIC_API_KEY',\n supports: {\n chat: true,\n embeddings: true,\n streaming: true,\n imageGeneration: true,\n },\n pricingMode: 'none',\n enabledByDefault: true,\n priority: 20,\n docsUrl: 'https://docs.hyperbolic.xyz/',\n notes:\n 'D1 2026-04-24: key provisioned (ailin-hyperbolic-api-key, 73B \"sk_liv…\") and auth accepted; /v1/chat returns 402 {\"detail\":\"Insufficient funds, please see https://docs.hyperbolic.xyz/docs/hyperbolic-pricing\"}. Classified upstream-suspended (not credentials-missing) because the credential itself is valid — only the account balance is zero. Operator top-up unblocks live-validation.',\n lastReviewedAt: '2026-04-24',\n },\n {\n providerId: 'nscale',\n displayName: 'Nscale (EU)',\n providerFamily: 'nscale',\n integrationClass: 'oai-compat-pure',\n integrationMode: 'discovery+execution',\n baseUrl: 'https://inference.api.nscale.com/v1',\n authScheme: 'bearer',\n apiKeyEnvVar: 'NSCALE_API_KEY',\n supports: {\n chat: true,\n streaming: true,\n tools: true,\n },\n capabilityHints: [\n { capability: 'eu_sovereign', rationale: 'provider-class-default', confidence: 0.85 },\n ],\n pricingMode: 'none',\n enabledByDefault: true,\n priority: 25,\n docsUrl: 'https://docs.nscale.com/docs/inference/chat',\n notes: 'EU data sovereignty; GDPR-compliant inference.',\n lastReviewedAt: '2026-04-21',\n },\n {\n providerId: 'anyscale',\n displayName: 'Anyscale Endpoints',\n providerFamily: 'anyscale',\n integrationClass: 'oai-compat-pure',\n integrationMode: 'discovery+execution',\n baseUrl: 'https://api.endpoints.anyscale.com/v1',\n authScheme: 'bearer',\n apiKeyEnvVar: 'ANYSCALE_API_KEY',\n supports: {\n chat: true,\n embeddings: true,\n streaming: true,\n },\n pricingMode: 'none',\n enabledByDefault: true, // Anyscale deprecated public endpoints mid-2024; secret provisioned but vendor-side suspended. Adapter still resolves for ANYSCALE_BASE_URL private deployments.\n priority: 10,\n notes:\n 'Deprecated for new signups. Entry retained for existing enterprise deployments via ANYSCALE_BASE_URL override. D1 2026-04-24: key provisioned (ailin-anyscale-api-key, 236B \"aph0_C…\") but api.endpoints.anyscale.com returns HTML shutdown notice \"Effective August 1, 2024 ... Multi-tenant access to LLM models has been removed.\" Permanent vendor-side shutdown; classified upstream-suspended. Unblock via private Hosted deployment with ANYSCALE_BASE_URL override.',\n baseUrlEnvVar: 'ANYSCALE_BASE_URL',\n lastReviewedAt: '2026-04-24',\n },\n {\n providerId: 'featherless-ai',\n displayName: 'Featherless AI',\n providerFamily: 'featherless-ai',\n aliases: ['featherless', 'featherless_ai'],\n integrationClass: 'oai-compat-pure',\n integrationMode: 'discovery+execution',\n baseUrl: 'https://api.featherless.ai/v1',\n authScheme: 'bearer',\n apiKeyEnvVar: 'FEATHERLESS_AI_API_KEY',\n adapterClass: 'FeatherlessAdapter',\n supports: {\n chat: true,\n streaming: true,\n },\n pricingMode: 'none',\n enabledByDefault: true,\n priority: 15,\n docsUrl: 'https://featherless.ai/docs/completions',\n notes:\n 'Large long-tail HF-model marketplace. Dedicated thin adapter for per-provider observability (logs/metrics scope to `provider: featherless-ai`). No model identifiers hardcoded — the catalog/discovery service is sole source of truth; the adapter is identity-only over the hub.',\n lastReviewedAt: '2026-04-22',\n },\n {\n providerId: 'nebius',\n displayName: 'Nebius AI Studio',\n providerFamily: 'nebius',\n integrationClass: 'oai-compat-pure',\n integrationMode: 'discovery+execution',\n baseUrl: 'https://api.studio.nebius.ai/v1',\n authScheme: 'bearer',\n apiKeyEnvVar: 'NEBIUS_API_KEY',\n supports: {\n chat: true,\n embeddings: true,\n streaming: true,\n tools: true,\n jsonMode: true,\n imageGeneration: true,\n vision: true,\n },\n pricingMode: 'remote',\n enabledByDefault: true,\n priority: 30,\n docsUrl: 'https://docs.nebius.com/studio/inference',\n lastReviewedAt: '2026-04-21',\n },\n {\n providerId: 'lambda-ai',\n displayName: 'Lambda AI',\n providerFamily: 'lambda-ai',\n aliases: ['lambda', 'lambdalabs'],\n integrationClass: 'oai-compat-pure',\n integrationMode: 'discovery+execution',\n baseUrl: 'https://api.lambdalabs.com/v1',\n authScheme: 'bearer',\n apiKeyEnvVar: 'LAMBDA_AI_API_KEY',\n supports: {\n chat: true,\n streaming: true,\n },\n pricingMode: 'none',\n enabledByDefault: true,\n priority: 20,\n docsUrl: 'https://docs.lambdalabs.com/public-cloud/lambda-inference-api/',\n lastReviewedAt: '2026-04-21',\n },\n {\n providerId: 'sambanova',\n displayName: 'SambaNova Cloud',\n providerFamily: 'sambanova',\n integrationClass: 'oai-compat-pure', // wire protocol is pure OAI; adapter only adds tier hints\n integrationMode: 'discovery+execution',\n baseUrl: 'https://api.sambanova.ai/v1',\n authScheme: 'bearer',\n apiKeyEnvVar: 'SAMBANOVA_API_KEY',\n adapterClass: 'SambanovaAdapter',\n supports: {\n chat: true,\n streaming: true,\n tools: true,\n },\n capabilityHints: [\n { capability: 'low_latency', rationale: 'provider-class-default', confidence: 0.7 },\n ],\n pricingMode: 'none',\n enabledByDefault: true,\n priority: 40,\n docsUrl: 'https://docs.sambanova.ai/cloud/docs/get-started/overview',\n notes:\n 'RDU hardware; fast Llama-405B inference. Dedicated adapter only labels observability and exposes FAST_TIER_MODELS hint.',\n lastReviewedAt: '2026-04-22',\n },\n {\n providerId: 'scaleway',\n displayName: 'Scaleway Generative APIs',\n providerFamily: 'scaleway',\n integrationClass: 'oai-compat-pure',\n integrationMode: 'discovery+execution',\n baseUrl: 'https://api.scaleway.ai/v1',\n authScheme: 'bearer',\n apiKeyEnvVar: 'SCALEWAY_API_KEY',\n supports: {\n chat: true,\n embeddings: true,\n streaming: true,\n tools: true,\n jsonMode: true,\n },\n capabilityHints: [\n { capability: 'eu_sovereign', rationale: 'provider-class-default', confidence: 0.85 },\n ],\n pricingMode: 'none',\n enabledByDefault: true,\n priority: 25,\n docsUrl: 'https://www.scaleway.com/en/docs/generative-apis/',\n lastReviewedAt: '2026-04-21',\n },\n {\n providerId: 'synthetic',\n displayName: 'Synthetic',\n providerFamily: 'synthetic',\n integrationClass: 'oai-compat-pure',\n integrationMode: 'discovery+execution',\n baseUrl: 'https://api.synthetic.new/v1',\n authScheme: 'bearer',\n apiKeyEnvVar: 'SYNTHETIC_API_KEY',\n supports: {\n chat: true,\n streaming: true,\n },\n pricingMode: 'none',\n enabledByDefault: true,\n priority: 10,\n docsUrl: 'https://synthetic.new/landing/home',\n notes: 'Smaller provider; smoke-test with real key before relying on routing.',\n lastReviewedAt: '2026-04-21',\n },\n {\n providerId: 'morph',\n displayName: 'Morph',\n providerFamily: 'morph',\n integrationClass: 'oai-compat-pure',\n integrationMode: 'discovery+execution',\n baseUrl: 'https://api.morphllm.com/v1',\n authScheme: 'bearer',\n apiKeyEnvVar: 'MORPH_API_KEY',\n supports: {\n chat: true,\n streaming: true,\n },\n pricingMode: 'none',\n enabledByDefault: true,\n priority: 10,\n docsUrl: 'https://docs.morphllm.com/',\n notes: 'Fast code-editing specialist. Verify baseUrl before enabling.',\n lastReviewedAt: '2026-04-21',\n },\n {\n providerId: 'chutes',\n displayName: 'Chutes',\n providerFamily: 'chutes',\n integrationClass: 'oai-compat-pure',\n integrationMode: 'discovery+execution',\n baseUrl: 'https://llm.chutes.ai/v1',\n authScheme: 'bearer',\n apiKeyEnvVar: 'CHUTES_API_KEY',\n supports: {\n chat: true,\n streaming: true,\n },\n pricingMode: 'none',\n enabledByDefault: true,\n priority: 15,\n docsUrl: 'https://chutes.ai/',\n notes:\n 'D1 2026-04-24: key provisioned (ailin-chutes-api-key) and auth accepted; /v1/chat returned 402 (account balance $0.0). Live-verified 2026-07-29 after operator top-up: GET /v1/models 200 (15 TEE models, this key is TEE-only by provisioning), POST /v1/chat/completions 200 for Qwen/Qwen3-32B-TEE and unsloth/Mistral-Nemo-Instruct-2407-TEE — zero 402s. No integration changes needed.',\n lastReviewedAt: '2026-07-29',\n },\n\n // ──────────────────────────────────────────────────────────────────────────\n // LOTE B — User-prioritized providers\n // ──────────────────────────────────────────────────────────────────────────\n {\n providerId: 'zai',\n displayName: 'Z.AI (Zhipu)',\n providerFamily: 'zai',\n aliases: ['zhipu', 'zhipuai', 'bigmodel'],\n integrationClass: 'oai-compat-pure',\n integrationMode: 'discovery+execution',\n baseUrl: 'https://open.bigmodel.cn/api/paas/v4',\n authScheme: 'bearer',\n apiKeyEnvVar: 'ZAI_API_KEY',\n supports: {\n chat: true,\n embeddings: true,\n streaming: true,\n tools: true,\n jsonMode: true,\n vision: true,\n imageGeneration: true,\n videoGeneration: true,\n },\n capabilityHints: [\n { capability: 'multilingual_chinese', rationale: 'provider-class-default', confidence: 0.85 },\n ],\n pricingMode: 'none',\n enabledByDefault: true,\n priority: 30,\n docsUrl: 'https://docs.z.ai/devpack/quick-start',\n notes: 'GLM-4 family; strong Chinese + multilingual. Video via CogVideoX.',\n lastReviewedAt: '2026-04-21',\n },\n {\n providerId: 'xiaomi-mimo',\n displayName: 'Xiaomi MiMo',\n providerFamily: 'xiaomi-mimo',\n aliases: ['xiaomi', 'mimo'],\n integrationClass: 'oai-compat-pure',\n // 2026-04-29: live probe of platform.xiaomimimo.com/v1/models returns the\n // platform's HTML homepage (no JSON listing endpoint exists). Switched\n // to execution-only with pinnedFallback so the catalog row materializes.\n // Model IDs sourced from the Vercel AI Gateway listing (xiaomi/mimo-v2-pro,\n // xiaomi/mimo-v2-flash) plus the platform's own docs (Omni multimodal).\n integrationMode: 'execution-only',\n baseUrl: 'https://platform.xiaomimimo.com/v1',\n authScheme: 'bearer',\n apiKeyEnvVar: 'XIAOMI_MIMO_API_KEY',\n supports: {\n chat: true,\n streaming: true,\n },\n pinnedFallback: {\n reason: 'no-list-endpoint',\n // Structured form with capabilities — same rationale as v0 above:\n // `mimo-*` ids don't match any built-in regex, so bare strings would\n // trip the `pinnedFallback-capability-coverage` invariant.\n //\n // Architectural note (2026-04-28): the model's Chinese-language\n // affinity is NOT in this strict capabilities array. It lives in\n // `capabilityHints` below (fuzzy soft hint, confidence 0.85) — the\n // strict capabilities surface answers \"can the model perform action\n // X?\" (boolean), while language strength is a fuzzy quality, not a\n // discrete capability surface. Other Chinese-trained providers\n // (qwen, glm, ernie, deepseek) follow the same convention.\n models: [\n { id: 'mimo-v2-pro', capabilities: ['chat', 'streaming', 'tool_use'] },\n { id: 'mimo-v2-flash', capabilities: ['chat', 'streaming'] },\n { id: 'mimo-omni', capabilities: ['chat', 'streaming', 'vision', 'multimodal'] },\n ],\n lastReviewedAt: '2026-04-29',\n },\n capabilityHints: [\n { capability: 'multilingual_chinese', rationale: 'provider-class-default', confidence: 0.85 },\n ],\n pricingMode: 'none',\n enabledByDefault: true,\n priority: 10,\n docsUrl: 'https://platform.xiaomimimo.com/#/docs/welcome',\n notes:\n 'New platform; no /v1/models listing — pinnedFallback covers the 3 published SKUs (MiMo-V2-Pro, V2-Flash, Omni). Cross-checked against Vercel AI Gateway listing 2026-04-29. Uses dedicated XiaomiMimoAdapter for named identity in metrics + circuit-breaker scoping.',\n adapterClass: 'XiaomiMimoAdapter',\n lastReviewedAt: '2026-04-29',\n },\n {\n providerId: 'v0',\n displayName: 'v0 (Vercel)',\n providerFamily: 'v0',\n integrationClass: 'oai-compat-pure',\n // 2026-04-29: live probe api.v0.dev/v1/models returns 404 (Not Found —\n // {\"error\":{\"type\":\"not_found_error\"}}). v0 has no models-listing endpoint;\n // their public docs (v0.app/docs/api/platform/overview) describe a chats\n // / projects / deployments shape, not a model selector. Falling back to\n // pinned list of advertised SKUs. Note: v0 is also exposed via Vercel AI\n // Gateway as `vercel/v0-...` — but the gateway listing today shows zero\n // vercel-owned entries, so the direct row is the canonical reach.\n integrationMode: 'execution-only',\n baseUrl: 'https://api.v0.dev/v1',\n authScheme: 'bearer',\n apiKeyEnvVar: 'V0_API_KEY',\n supports: {\n chat: true,\n streaming: true,\n tools: true,\n },\n pinnedFallback: {\n reason: 'no-list-endpoint',\n // Explicit capabilities (not bare strings) so the CI invariant\n // `pinnedFallback-capability-coverage` passes — `v0-*` ids don't match\n // any built-in name-regex, so bare strings would fail the coverage check.\n models: [\n { id: 'v0-1.5-md', capabilities: ['chat', 'streaming', 'tool_use', 'code_generation'] },\n { id: 'v0-1.5-sm', capabilities: ['chat', 'streaming', 'code_generation'] },\n { id: 'v0-1.0-md', capabilities: ['chat', 'streaming', 'code_generation'] },\n ],\n lastReviewedAt: '2026-04-29',\n },\n capabilityHints: [\n {\n capability: 'frontend_code_generation',\n rationale: 'provider-class-default',\n confidence: 0.8,\n },\n ],\n pricingMode: 'none',\n enabledByDefault: true,\n priority: 20,\n docsUrl: 'https://v0.app/docs/api/platform/overview',\n notes:\n 'Specialized in frontend/UI code generation. No /v1/models endpoint — pinnedFallback enumerates the published SKUs.',\n lastReviewedAt: '2026-04-29',\n },\n {\n providerId: 'vercel-ai-gateway',\n displayName: 'Vercel AI Gateway',\n providerFamily: 'vercel-ai-gateway',\n aliases: ['vercel_ai_gateway', 'vercel-gateway'],\n integrationClass: 'gateway',\n integrationMode: 'discovery+execution',\n baseUrl: 'https://ai-gateway.vercel.sh/v1',\n authScheme: 'bearer',\n apiKeyEnvVar: 'VERCEL_AI_GATEWAY_API_KEY',\n adapterClass: 'VercelAIGatewayAdapter',\n paths: {\n // Vercel implements the OpenAI surface verbatim — confirmed against\n // https://vercel.com/docs/ai-gateway/capabilities/image-generation/openai\n // (\"Image-only models use the OpenAI Images API (`/v1/images/generations`)\n // for specialized image creation\"). Multimodal LLMs that generate images\n // (Nano Banana, GPT-5 image variants) use `/v1/chat/completions` with\n // images returned in the response's `images` array. Embeddings follow\n // the OpenAI default. Topology: baseUrl already includes `/v1`, so\n // paths here are relative to that.\n //\n // VIDEO: deliberately omitted. The video docs (2026-04-29) only show\n // AI SDK's `experimental_generateVideo` — no published REST shape.\n // When Vercel publishes a REST endpoint, add `videoGenerate` here.\n chatCompletions: '/chat/completions',\n embeddings: '/embeddings',\n imagesGenerate: '/images/generations',\n },\n supports: {\n chat: true,\n embeddings: true,\n streaming: true,\n tools: true,\n jsonMode: true,\n vision: true,\n // 2026-04-29: align with Vercel's capability matrix. Gateway proxies\n // image (Recraft, BFL, Topaz routes), video (Runway, Topaz), reasoning\n // (o1/o3/Claude-thinking), and exposes web-search via the Responses\n // proxy when the upstream supports it. `rerank`/`moderation` follow\n // when the upstream model declares them via owned_by.\n imageGeneration: true,\n // 2026-07-17: videoGeneration REMOVED — it contradicted the VIDEO note\n // in `paths` above (\"deliberately omitted... no published REST shape\")\n // and made all 21 vercel video-tagged models enter the execution pool\n // just to 404 on /v1/videos/generations (live sweep re-confirmed: 404\n // on both /videos/generations and /videos). Re-add together with a\n // paths.videoGenerate when Vercel publishes a REST video endpoint.\n reasoning: true,\n rerank: true,\n moderation: true,\n },\n capabilityHints: [\n // `web_search` is not a `supports` flag (catalog enum stops at modality\n // flags); surface it via capabilityHint so the merger picks it up for\n // OpenAI/xAI/Perplexity routes that expose it through the gateway.\n // Rationale `docs-declared`: vercel.com/docs/ai-gateway/capabilities/web-search.\n { capability: 'web_search', rationale: 'docs-declared', confidence: 0.6 },\n ],\n originalProviderField: 'owned_by',\n pricingMode: 'remote',\n enabledByDefault: true,\n priority: 35,\n docsUrl: 'https://vercel.com/docs/ai-gateway',\n notes:\n 'Gateway routing to OpenAI/Anthropic/xAI/etc. Models namespaced as `provider/model`; adapter exposes parseModelId() + attributeFromDiscovery() so the capability merger attributes to the real owner. Image/video/reasoning/rerank/moderation surface; paths.imagesGenerate=/images/generations matches the OAI Images endpoint. videoGeneration=true reflects model surface, NOT REST availability — Vercel video is AI-SDK-only as of 2026-04-29 (no published REST).',\n lastReviewedAt: '2026-04-29',\n },\n {\n // providerId is `wandb` (not `wandb-inference`) so the convention\n // `WANDB_API_KEY` holds — matches W&B's published SDK/CLI env var name.\n // The longer form lives in aliases for inbound normalization.\n providerId: 'wandb',\n displayName: 'Weights & Biases Inference',\n providerFamily: 'wandb',\n aliases: ['wandb-inference', 'weave'],\n integrationClass: 'oai-compat-quirks',\n integrationMode: 'discovery+execution',\n baseUrl: 'https://api.inference.wandb.ai/v1',\n authScheme: 'bearer',\n apiKeyEnvVar: 'WANDB_API_KEY',\n adapterClass: 'WandbAdapter',\n extraEnvVars: {\n WANDB_PROJECT: 'W&B project slug required on requests (header wandb-project)',\n },\n supports: {\n chat: true,\n streaming: true,\n },\n pricingMode: 'none',\n enabledByDefault: true,\n priority: 15,\n docsUrl: 'https://docs.wandb.ai/weave/quickstart-inference',\n notes:\n 'OAI-compatible chat surface. Dedicated adapter injects the required `wandb-project` header from $WANDB_PROJECT at request time (env hot-swap safe).',\n lastReviewedAt: '2026-04-22',\n },\n {\n providerId: 'voyage',\n displayName: 'Voyage AI',\n providerFamily: 'voyage',\n aliases: ['voyage_ai', 'voyageai'],\n integrationClass: 'embeddings-only',\n // Voyage does NOT expose GET /v1/models (confirmed HTTP 404 on live probe\n // 2026-04-22; cross-checked against docs.voyageai.com — the public API\n // reference covers only /embeddings, /multimodalembeddings, and /rerank).\n // Model identifiers are therefore sourced from the catalog's own\n // enumeration (see MongoDB-hosted Voyage \"Models Overview\" for the\n // canonical family list). Execution-only reflects that truthfully.\n integrationMode: 'execution-only',\n baseUrl: 'https://api.voyageai.com/v1',\n authScheme: 'bearer',\n apiKeyEnvVar: 'VOYAGE_API_KEY',\n adapterClass: 'VoyageAdapter',\n paths: {\n embeddings: '/embeddings',\n rerank: '/rerank',\n },\n // Voyage exposes no /models endpoint (execution-only). The pinnedFallback\n // list below covers the stable families as of 2026-04 per blog.voyageai.com\n // and mongodb.com/docs/voyageai/models:\n // - Voyage-4 series: shared embedding space, interchangeable.\n // - voyage-3 / voyage-3-lite retained as \"previous generation\" for\n // existing indexes; not removed yet per vendor.\n // - Domain-specific: finance-2, law-2, code-2, code-3, multilingual-2.\n // - Multimodal: voyage-multimodal-3.\n // - Rerankers: rerank-2, rerank-2-lite, rerank-2.5, rerank-2.5-lite.\n // Expand when Voyage publishes a new generation; remove deprecated IDs\n // only after the vendor end-of-life window closes.\n // Phase 4d (2026-04-28): renamed staticModels → pinnedFallback with\n // reason='no-list-endpoint' (HTTP 404 confirmed by live probe 2026-04-22).\n pinnedFallback: {\n // Operator-declared (root-cause refactor 2026-04-28). Voyage's surface\n // splits cleanly: voyage-* are embedding models (multimodal-3 also\n // accepts image input); rerank-* are retrieval rerankers. Declared\n // here so the catalog-bridge does NOT regex-infer rerank-* as chat\n // (the regex table previously matched them via `rerank-` only after\n // the moderation/transcription rules were ordered correctly).\n models: [\n { id: 'voyage-4-large', capabilities: ['embedding', 'embeddings'] },\n { id: 'voyage-4', capabilities: ['embedding', 'embeddings'] },\n { id: 'voyage-4-lite', capabilities: ['embedding', 'embeddings'] },\n { id: 'voyage-4-nano', capabilities: ['embedding', 'embeddings'] },\n { id: 'voyage-3', capabilities: ['embedding', 'embeddings'] },\n { id: 'voyage-3-lite', capabilities: ['embedding', 'embeddings'] },\n { id: 'voyage-code-3', capabilities: ['embedding', 'embeddings'] },\n { id: 'voyage-code-2', capabilities: ['embedding', 'embeddings'] },\n { id: 'voyage-finance-2', capabilities: ['embedding', 'embeddings'] },\n { id: 'voyage-law-2', capabilities: ['embedding', 'embeddings'] },\n { id: 'voyage-multilingual-2', capabilities: ['embedding', 'embeddings'] },\n {\n id: 'voyage-multimodal-3',\n capabilities: ['embedding', 'embeddings', 'multimodal', 'vision'],\n },\n { id: 'rerank-2.5', capabilities: ['reranking', 'retrieval'] },\n { id: 'rerank-2.5-lite', capabilities: ['reranking', 'retrieval'] },\n { id: 'rerank-2', capabilities: ['reranking', 'retrieval'] },\n { id: 'rerank-2-lite', capabilities: ['reranking', 'retrieval'] },\n ],\n reason: 'no-list-endpoint',\n lastReviewedAt: '2026-04-28',\n },\n supports: {\n embeddings: true,\n rerank: true,\n },\n capabilityHints: [\n { capability: 'rerank', rationale: 'endpoint-declared', confidence: 0.9 },\n { capability: 'long_context_embedding', rationale: 'docs-declared', confidence: 0.8 },\n ],\n pricingMode: 'none',\n enabledByDefault: true,\n priority: 60,\n docsUrl: 'https://docs.voyageai.com/docs/introduction',\n notes:\n 'Top-tier embeddings + rerank. voyage-3, voyage-code-3, voyage-rerank-2. Voyage does NOT expose GET /v1/models (confirmed 404 via live probe 2026-04-22); discovery is skipped, model IDs are resolved from the catalog/DB. Dedicated adapter implements /v1/embeddings + /v1/rerank and explicitly rejects chat calls.',\n lastReviewedAt: '2026-04-22',\n },\n {\n providerId: 'volcano',\n displayName: 'Volcano Engine (Volcengine)',\n providerFamily: 'volcano',\n aliases: ['volcengine', 'ark', 'bytedance'],\n integrationClass: 'oai-compat-quirks', // model = endpoint id, no bulk /models route\n integrationMode: 'discovery+execution',\n baseUrl: 'https://ark.cn-beijing.volces.com/api/v3',\n authScheme: 'bearer',\n apiKeyEnvVar: 'VOLCANO_API_KEY',\n adapterClass: 'VolcanoAdapter',\n supports: {\n chat: true,\n embeddings: true,\n streaming: true,\n tools: true,\n vision: true,\n },\n capabilityHints: [\n { capability: 'multilingual_chinese', rationale: 'provider-class-default', confidence: 0.85 },\n ],\n pricingMode: 'none',\n enabledByDefault: true,\n priority: 30,\n docsUrl: 'https://www.volcengine.com/docs/82379/1099455',\n notes:\n 'ByteDance/Volcano ARK — Doubao models. Strong Chinese coverage. ARK does NOT expose a bulk /models route — dedicated adapter returns empty getModels() and validates the ep-<timestamp>-<random> endpoint-id format.',\n lastReviewedAt: '2026-04-22',\n },\n {\n providerId: 'watsonx',\n displayName: 'IBM watsonx.ai',\n providerFamily: 'watsonx',\n aliases: ['ibm-watsonx', 'ibm_watsonx'],\n integrationClass: 'first-party-native',\n // Promoted from catalog-only → discovery+execution now that WatsonxAdapter\n // implements the IAM token exchange (+cache), x-watsonx-project-id header,\n // and /ml/v1/text/chat + /ml/v1/text/embeddings routes with version pin.\n integrationMode: 'discovery+execution',\n baseUrl: 'https://us-south.ml.cloud.ibm.com',\n authScheme: 'iam-token',\n apiKeyEnvVar: 'WATSONX_APIKEY',\n baseUrlEnvVar: 'WATSONX_URL',\n extraEnvVars: {\n WATSONX_PROJECT_ID: 'watsonx.ai project ID (required on all generate calls)',\n WATSONX_URL: 'Region-specific host (overrides baseUrl) — us-south, eu-de, jp-tok...',\n WATSONX_ZENAPIKEY: 'Alternative: ZenAPIKey for Cloud Pak for Data',\n },\n supports: {\n chat: true,\n embeddings: true,\n streaming: true,\n },\n pricingMode: 'none',\n enabledByDefault: true,\n priority: 25,\n docsUrl: 'https://www.ibm.com/watsonx/developer/',\n notes:\n 'IAM token exchange against https://iam.cloud.ibm.com/identity/token (grant_type=urn:ibm:params:oauth:grant-type:apikey). Requires WATSONX_APIKEY + WATSONX_PROJECT_ID. API version pinned to 2024-05-31.',\n adapterClass: 'WatsonxAdapter',\n lastReviewedAt: '2026-04-22',\n },\n {\n providerId: 'snowflake',\n displayName: 'Snowflake Cortex',\n providerFamily: 'snowflake',\n integrationClass: 'first-party-native',\n // Promoted from catalog-only → discovery+execution: SnowflakeCortexAdapter\n // (registered in default-adapter-factories) implements JWT key-pair auth,\n // account-scoped baseUrl substitution, and getModels() discovery.\n integrationMode: 'discovery+execution',\n baseUrl: 'https://snowflake.example.snowflakecomputing.com',\n authScheme: 'custom',\n apiKeyEnvVar: 'SNOWFLAKE_PAT',\n extraEnvVars: {\n SNOWFLAKE_ACCOUNT: 'Account identifier (orgname-accountname)',\n SNOWFLAKE_USER: 'Username for key-pair auth',\n SNOWFLAKE_WAREHOUSE: 'Compute warehouse (optional)',\n SNOWFLAKE_BASE_URL: 'Override for baseUrl — https://<account>.snowflakecomputing.com',\n },\n baseUrlEnvVar: 'SNOWFLAKE_BASE_URL',\n supports: {\n chat: true,\n streaming: true,\n embeddings: true,\n },\n pricingMode: 'none',\n enabledByDefault: true,\n priority: 20,\n docsUrl: 'https://docs.snowflake.com/en/user-guide/snowflake-cortex/cortex-rest-api',\n notes:\n 'JWT key-pair auth via SnowflakeCortexAdapter. Requires SNOWFLAKE_PAT + SNOWFLAKE_USER + SNOWFLAKE_ACCOUNT set together (account-scoped baseUrl). Adapter wired 2026-06-15.',\n adapterClass: 'SnowflakeCortexAdapter',\n lastReviewedAt: '2026-04-21',\n },\n {\n providerId: 'sap',\n displayName: 'SAP Generative AI Hub',\n providerFamily: 'sap',\n aliases: ['sap-ai-core', 'sap-genai'],\n integrationClass: 'first-party-native',\n // Promoted from catalog-only → discovery+execution: SapAiCoreAdapter\n // (registered in default-adapter-factories) implements OAuth2 client_credentials\n // token exchange, AI-Resource-Group header injection, and getModels() discovery.\n integrationMode: 'discovery+execution',\n baseUrl: 'https://api.ai.prod.eu-central-1.aws.ml.hana.ondemand.com',\n authScheme: 'oauth2',\n apiKeyEnvVar: 'SAP_AI_CORE_CLIENT_ID',\n extraEnvVars: {\n SAP_AI_CORE_CLIENT_SECRET: 'OAuth2 client secret',\n SAP_AI_CORE_AUTH_URL: 'OAuth2 token endpoint',\n SAP_AI_CORE_RESOURCE_GROUP: 'Resource group (default: default)',\n SAP_AI_CORE_BASE_URL: 'Region-specific base URL',\n },\n baseUrlEnvVar: 'SAP_AI_CORE_BASE_URL',\n supports: {\n chat: true,\n streaming: true,\n embeddings: true,\n },\n pricingMode: 'none',\n enabledByDefault: true,\n priority: 15,\n docsUrl: 'https://help.sap.com/docs/sap-ai-core',\n notes:\n 'OAuth2 client_credentials via SapAiCoreAdapter. Requires SAP_AI_CORE_CLIENT_ID + SAP_AI_CORE_CLIENT_SECRET + SAP_AI_CORE_AUTH_URL set together (+ optional resource group). Adapter wired 2026-06-15.',\n adapterClass: 'SapAiCoreAdapter',\n lastReviewedAt: '2026-04-21',\n },\n\n // ──────────────────────────────────────────────────────────────────────────\n // LOTE C — Image/Video specialties\n // ──────────────────────────────────────────────────────────────────────────\n {\n providerId: 'recraft',\n displayName: 'Recraft',\n providerFamily: 'recraft',\n integrationClass: 'image-only',\n integrationMode: 'execution-only', // no /models endpoint\n baseUrl: 'https://external.api.recraft.ai/v1',\n authScheme: 'bearer',\n apiKeyEnvVar: 'RECRAFT_API_KEY',\n adapterClass: 'RecraftAdapter',\n paths: {\n imagesGenerate: '/images/generations',\n },\n pinnedFallback: {\n // Operator-declared (root-cause refactor 2026-04-28). Recraft v3/v2\n // are pure image-generation models — no /models endpoint exists, so\n // the catalog row IS the source of truth.\n models: [\n { id: 'recraftv3', capabilities: ['image_generation', 'image_editing'] },\n { id: 'recraftv2', capabilities: ['image_generation'] },\n ],\n reason: 'no-list-endpoint',\n lastReviewedAt: '2026-04-28',\n },\n supports: {\n imageGeneration: true,\n },\n capabilityHints: [\n { capability: 'vector_image_generation', rationale: 'docs-declared', confidence: 0.85 },\n { capability: 'brand_style_consistency', rationale: 'docs-declared', confidence: 0.75 },\n ],\n pricingMode: 'none',\n enabledByDefault: true,\n priority: 30,\n docsUrl: 'https://www.recraft.ai/docs/api-reference/getting-started',\n notes:\n 'Vector + raster image gen with style controls. Dedicated adapter validates model × style pairs before the wire (v3 has vector families + any, v2 is raster-only).',\n lastReviewedAt: '2026-04-22',\n },\n {\n providerId: 'runwayml',\n displayName: 'RunwayML',\n providerFamily: 'runwayml',\n integrationClass: 'video-only',\n integrationMode: 'execution-only',\n baseUrl: 'https://api.dev.runwayml.com',\n authScheme: 'bearer',\n apiKeyEnvVar: 'RUNWAYML_API_KEY',\n adapterClass: 'RunwayMLAdapter',\n extraHeaders: {\n 'X-Runway-Version': '2024-11-06',\n },\n paths: {\n videoGenerate: '/v1/image_to_video',\n },\n pinnedFallback: {\n // Operator-declared (root-cause refactor 2026-04-28). Runway gen3*\n // are video generation slots; act-one is video-to-video character\n // performance transfer. No /models endpoint, so these are authoritative.\n models: [\n {\n id: 'gen3a_turbo',\n capabilities: ['video_generation', 'image_to_video'],\n },\n {\n id: 'gen3_alpha',\n capabilities: ['video_generation', 'image_to_video'],\n },\n { id: 'act-one', capabilities: ['video_generation', 'video_to_video'] },\n ],\n reason: 'no-list-endpoint',\n lastReviewedAt: '2026-04-28',\n },\n supports: {\n videoGeneration: true,\n imageGeneration: true,\n },\n capabilityHints: [\n { capability: 'image_to_video', rationale: 'docs-declared', confidence: 0.9 },\n ],\n pricingMode: 'none',\n enabledByDefault: true,\n priority: 35,\n docsUrl: 'https://docs.dev.runwayml.com/',\n notes:\n 'Video-from-image + act-one motion transfer. Requires X-Runway-Version header. Async-job API: POST /v1/image_to_video → poll GET /v1/tasks/{id} until terminal status. Dedicated adapter owns the polling budget.',\n lastReviewedAt: '2026-04-22',\n },\n {\n providerId: 'topaz',\n displayName: 'Topaz Labs',\n providerFamily: 'topaz',\n integrationClass: 'image-only',\n // 2026-05-06: flipped catalog-only → execution-only. The TopazImageAdapter\n // exists at providers/topaz/topaz-adapter.ts AND is wired in the factory\n // registry (see default-adapter-factories.ts: registerAdapterFactory\n // 'TopazImageAdapter'). Topaz exposes no /models listing endpoint —\n // pinnedFallback below is the canonical inventory.\n integrationMode: 'execution-only',\n baseUrl: 'https://api.topazlabs.com/image/v1',\n authScheme: 'api-key-header',\n authHeaderName: 'X-API-Key',\n apiKeyEnvVar: 'TOPAZ_API_KEY',\n pinnedFallback: {\n // Topaz Image API exposes a fixed roster of image-enhance pipelines\n // (upscale, denoise, sharpen, recovery). The 'standard'/'high-fidelity'\n // pair is the canonical chat-routable surface; expand when the API\n // grows new model variants.\n models: [\n { id: 'standard', capabilities: ['image_upscale', 'image_editing', 'image_denoise'] },\n { id: 'high-fidelity', capabilities: ['image_upscale', 'image_editing'] },\n ],\n reason: 'no-list-endpoint',\n lastReviewedAt: '2026-05-06',\n },\n supports: {\n imageEditing: true,\n },\n capabilityHints: [\n { capability: 'image_upscale', rationale: 'docs-declared', confidence: 0.9 },\n { capability: 'image_denoise', rationale: 'docs-declared', confidence: 0.85 },\n ],\n pricingMode: 'none',\n enabledByDefault: true,\n priority: 20,\n docsUrl: 'https://www.topazlabs.com/api',\n notes: 'Image upscale/enhance only — async job API. Wired via TopazImageAdapter factory.',\n adapterClass: 'TopazImageAdapter',\n lastReviewedAt: '2026-05-06',\n },\n {\n // providerId is `bfl` (not `black-forest-labs`) so the convention\n // `BFL_API_KEY` holds — matches BFL's published env var name. The full\n // brand name stays in `displayName` + `aliases` for humans + inbound norm.\n providerId: 'bfl',\n displayName: 'Black Forest Labs (FLUX)',\n providerFamily: 'bfl',\n aliases: ['black-forest-labs', 'flux'],\n integrationClass: 'image-only',\n integrationMode: 'execution-only',\n baseUrl: 'https://api.bfl.ai/v1',\n authScheme: 'api-key-header',\n authHeaderName: 'x-key',\n apiKeyEnvVar: 'BFL_API_KEY',\n pinnedFallback: {\n // Operator-declared (root-cause refactor 2026-04-28). Black Forest Labs\n // FLUX is image-generation + edit. The pro-1.1-ultra variant adds\n // higher-fidelity output but the surface is identical.\n models: [\n { id: 'flux-pro-1.1', capabilities: ['image_generation', 'image_editing'] },\n { id: 'flux-pro', capabilities: ['image_generation', 'image_editing'] },\n { id: 'flux-dev', capabilities: ['image_generation'] },\n { id: 'flux-schnell', capabilities: ['image_generation'] },\n {\n id: 'flux-pro-1.1-ultra',\n capabilities: ['image_generation', 'image_editing'],\n },\n ],\n reason: 'no-list-endpoint',\n lastReviewedAt: '2026-04-28',\n },\n supports: {\n imageGeneration: true,\n imageEditing: true,\n },\n pricingMode: 'none',\n enabledByDefault: true,\n priority: 35,\n docsUrl: 'https://docs.bfl.ai/',\n notes:\n 'FLUX family. Uses x-key header (not Bearer). Async-job protocol — submit → poll → download.',\n adapterClass: 'BflAdapter',\n lastReviewedAt: '2026-04-29',\n },\n {\n // 302.AI — OAI-compatible aggregator at api.302.ai/v1. Migrated out of\n // the provider-registry.ts switch in 2026-04-22 (residue-closure phase).\n //\n // ── Why providerId is `ai302` and not `302ai` ───────────────────────────\n // Our providerId regex requires a leading alpha character\n // (`/^[a-z][a-z0-9]*.../`). The user-facing name \"302ai\" begins with a\n // digit, so it can't be a canonical providerId. We keep `302ai` as an\n // alias so historical `config.providers[].name === '302ai'` still resolves\n // and users don't need to migrate their configs.\n //\n // Live-probed 2026-04-22 via GCP secret `ailin-302-key`: upstream returned\n // HTTP 401 with `Insufficient account balance`. That means auth + routing\n // are correct (the token was accepted) — the account simply has zero credit.\n // Classification: \"integrated, live-auth-accepted, balance-exhausted\".\n providerId: 'ai302',\n displayName: '302.AI',\n providerFamily: 'ai302',\n aliases: ['302ai', '302-ai', '302'],\n integrationClass: 'oai-compat-pure',\n integrationMode: 'discovery+execution',\n baseUrl: 'https://api.302.ai/v1',\n authScheme: 'bearer',\n apiKeyEnvVar: 'AI302_API_KEY',\n supports: {\n chat: true,\n streaming: true,\n tools: true,\n embeddings: true,\n imageGeneration: true,\n },\n pricingMode: 'none',\n enabledByDefault: true,\n priority: 8,\n docsUrl: 'https://302.ai/',\n notes:\n 'OAI-compatible aggregator. Models-list endpoint works when account has balance. Historical user config `{name: \"302ai\"}` still resolves via the alias table.',\n lastReviewedAt: '2026-04-22',\n },\n {\n // Replicate — first-party-native predictions API. Model versions (not names)\n // drive execution. Sync predictions via `Prefer: wait` header. The adapter\n // existed for months before the catalog migration and is fully functional;\n // this row is the missing factory registration that lets it be resolved.\n providerId: 'replicate',\n displayName: 'Replicate',\n providerFamily: 'replicate',\n integrationClass: 'first-party-native',\n integrationMode: 'execution-only',\n baseUrl: 'https://api.replicate.com/v1',\n baseUrlEnvVar: 'REPLICATE_BASE_URL',\n authScheme: 'bearer',\n apiKeyEnvVar: 'REPLICATE_API_KEY',\n supports: {\n chat: true,\n streaming: true,\n imageGeneration: true,\n textToSpeech: true,\n speechToText: true,\n },\n pricingMode: 'none',\n enabledByDefault: true,\n priority: 15,\n // Replicate's /v1/models endpoint returns thousands of public predictions,\n // most of which are non-LLM or private to their authors. A wholesale\n // discovery import is worse than a curated canonical list, so this stays\n // execution-only with a deliberate shortlist. Expand when product wants\n // additional Replicate-hosted families on the benchmark grid.\n // Phase 4d (2026-04-28): renamed staticModels → pinnedFallback with\n // reason='curated-shortlist' — the dedicated reason for \"endpoint works\n // but signal-to-noise ratio favours a hand-picked list\".\n pinnedFallback: {\n // Operator-declared (root-cause refactor 2026-04-28). Replicate routes\n // by `owner/name` slug — capabilities are model-specific, NOT inferable\n // from the slug alone (claude-3.5-sonnet is chat+vision; whisper is\n // STT; flux is image; stable-diffusion is image). Declared here so the\n // catalog-bridge does not regress to regex inference, which would\n // mistype `openai/whisper` (whisper-prefixed) as STT-only and miss\n // claude's vision capability entirely.\n models: [\n {\n id: 'anthropic/claude-3.5-sonnet',\n capabilities: [\n 'chat',\n 'streaming',\n 'vision',\n 'multimodal',\n 'function_calling',\n 'tool_use',\n 'reasoning',\n ],\n },\n {\n id: 'black-forest-labs/flux-pro',\n capabilities: ['image_generation', 'image_editing'],\n },\n {\n id: 'black-forest-labs/flux-schnell',\n capabilities: ['image_generation'],\n },\n {\n id: 'meta/meta-llama-3-70b-instruct',\n capabilities: ['chat', 'streaming', 'function_calling'],\n },\n {\n id: 'meta/meta-llama-3-8b-instruct',\n capabilities: ['chat', 'streaming'],\n },\n {\n id: 'mistralai/mistral-7b-instruct-v0.2',\n capabilities: ['chat', 'streaming'],\n },\n {\n id: 'openai/gpt-4o-mini',\n capabilities: [\n 'chat',\n 'streaming',\n 'vision',\n 'multimodal',\n 'function_calling',\n 'json_mode',\n ],\n },\n {\n id: 'openai/whisper',\n capabilities: ['speech_to_text', 'transcription', 'audio'],\n },\n {\n id: 'stability-ai/stable-diffusion-3',\n capabilities: ['image_generation'],\n },\n ],\n reason: 'curated-shortlist',\n lastReviewedAt: '2026-04-28',\n },\n docsUrl: 'https://replicate.com/docs',\n notes:\n 'Predictions API (async-by-default, sync via Prefer: wait). Models are invoked as owner/name or owner/name:version. Adapter handles SSE streams for LLM models and downloads output URLs for image/audio models.',\n adapterClass: 'ReplicateAdapter',\n lastReviewedAt: '2026-04-22',\n },\n {\n // Bytez — multi-modality hub with a quirky OAI-compat surface.\n // apiKeyEnvVar wired to GCP secret ailin-bytez-key (2026-04-22).\n //\n // URL LAYOUT (documented, confirmed by docs.bytez.com/http-reference):\n // Chat (OAI-compat): POST https://api.bytez.com/models/v2/openai/v1/chat/completions\n // Models list (native shape, NOT OAI):\n // GET https://api.bytez.com/models/v2/list/models?task=chat\n // Response: { error, output: [{ modelId, task, meter, meterPrice, params, ramRequired }] }\n // Auth header format: `Authorization: <token>` (sample in docs omits\n // the `Bearer ` prefix; the hub adapter currently sends `Bearer <token>`,\n // which several OAI-compat gateways also accept — verify during the\n // first live probe).\n //\n // Classification consequences:\n // - `oai-compat-quirks` (not `-pure`) because the OAI path is nested\n // under `/models/v2/openai/v1` instead of the customary `/v1`.\n // - `discovery+execution` (Phase 4d, 2026-04-28): the dedicated\n // `BytezNativeModelFetcher` consumes `/models/v2/list/models`\n // (non-OAI shape `{error, output:[{modelId,task,...}]}`) and\n // transforms it into the discovery pipeline's `ProviderModel`\n // shape. This unlocks Bytez's full ~100k-model HuggingFace surface\n // instead of the 4-row hand-picked list that previously seeded\n // execution-only mode.\n // - Image/speech modalities ride on separate native endpoints (see\n // docs.bytez.com/http-reference/examples/open-source/*) and are\n // intentionally NOT in `supports` here — the OAI hub adapter can't\n // route them without modality-specific transforms. The native\n // fetcher still surfaces those model IDs for visibility, but\n // execution requires the modality-specific transforms to land.\n providerId: 'bytez',\n displayName: 'Bytez',\n providerFamily: 'bytez',\n integrationClass: 'oai-compat-quirks',\n integrationMode: 'discovery+execution',\n baseUrl: 'https://api.bytez.com/models/v2/openai/v1',\n baseUrlEnvVar: 'BYTEZ_BASE_URL',\n authScheme: 'bearer',\n apiKeyEnvVar: 'BYTEZ_API_KEY',\n supports: {\n chat: true,\n embeddings: true,\n streaming: true,\n },\n pricingMode: 'none',\n enabledByDefault: true,\n priority: 12,\n docsUrl: 'https://docs.bytez.com/http-reference/oaiCompliant/chatCompletions',\n notes:\n 'Multi-modality hub over HuggingFace + custom models. OAI-compat chat/embeddings live at /models/v2/openai/v1/... — NOT /v1/... (quirk). Native discovery GET /models/v2/list/models returns non-OAI shape {error, output:[{modelId,task,...}]}; consumed by BytezNativeModelFetcher (Phase 4d 2026-04-28 — was execution-only with 4 staticModels). Confirmed 2026-04-22 via docs + live probe (old /v1 baseUrl returned HTTP 404 on every path).',\n adapterClass: 'BytezAdapter',\n fetcherClass: 'BytezNativeModelFetcher',\n lastReviewedAt: '2026-04-28',\n },\n {\n // Inworld AI — router + TTS/STT specialist with an OAI-compat chat surface.\n // Orphan-closure entry (2026-04-22): InworldAdapter existed on disk but was\n // never registered in the catalog or provider-registry, so the class was\n // never instantiated. This row + the InworldAdapter factory registration\n // in default-adapter-factories.ts close the orphan.\n //\n // QUIRK 1 — Basic auth (not Bearer).\n // The INWORLD_API_KEY is already base64-encoded (132 chars in GCP). The\n // adapter passes `authScheme: 'Basic'` into its hub metadata so the hub's\n // buildRequestHeaders() prepends `Basic ` (not `Bearer `) at every HTTP\n // site. Catalog authScheme is 'custom' because the schema enum has no\n // discrete 'basic' value — the catalog surface is auth-agnostic and the\n // adapter encodes the concrete scheme.\n //\n // QUIRK 2 — discovery broken on both documented paths.\n // Live probe 2026-04-22:\n // GET https://api.inworld.ai/router/v1/models → HTTP 404\n // GET https://api.inworld.ai/v1/models → HTTP 404\n // Upstream docs at https://docs.inworld.ai don't publish a discovery\n // endpoint; the canonical router surface only exposes chat. So this\n // entry is `execution-only` with staticModels — the discovery merger\n // must not probe HTTP for this provider.\n //\n // QUIRK 3 — provider-prefixed model IDs.\n // Inworld's router exposes upstream model IDs verbatim with their\n // family prefix (openai/gpt-4o-mini, anthropic/claude-3-haiku, etc.),\n // which is why the catalog can enumerate a short list without caring\n // about Inworld's private inventory — the adapter forwards the id.\n //\n // QUIRK 4 — TTS/STT/voice-clone endpoints on the same adapter.\n // InworldAdapter overrides textToSpeech/speechToText + adds\n // cloneVoice() on top of the hub. Exposing those in `supports` is\n // deferred — the CapabilityHintSchema doesn't yet carve out voice-clone,\n // and speech tests against a metered endpoint burn quota. Until a\n // capability-level audit happens, `supports.chat` is the safe minimum.\n providerId: 'inworld',\n displayName: 'Inworld AI',\n providerFamily: 'inworld',\n aliases: ['inworld_ai'],\n integrationClass: 'oai-compat-quirks',\n integrationMode: 'execution-only',\n baseUrl: 'https://api.inworld.ai',\n baseUrlEnvVar: 'INWORLD_BASE_URL',\n authScheme: 'custom',\n apiKeyEnvVar: 'INWORLD_API_KEY',\n adapterClass: 'InworldAdapter',\n supports: {\n chat: true,\n streaming: true,\n },\n // Minimum viable pinned fallback — live-probed 2026-04-22 at\n // POST /v1/chat/completions { model: 'openai/gpt-4o-mini', ... }\n // returned HTTP 200 with a valid ChatCompletion (\"Pong!\"). Additional\n // families are listed by provider-prefix convention; the router accepts\n // any upstream model ID it has a route for, so this list is\n // intentionally illustrative rather than exhaustive.\n // Phase 4d (2026-04-28): renamed staticModels → pinnedFallback with\n // reason='no-list-endpoint' (router/v1/models AND /v1/models both 404).\n pinnedFallback: {\n // Operator-declared (root-cause refactor 2026-04-28). Inworld routes\n // upstream models verbatim; capabilities follow the upstream family.\n // GPT-4o + claude-3-5-sonnet carry vision/multimodal; the rest are\n // text-only chat at the time of pinning.\n models: [\n {\n id: 'openai/gpt-4o-mini',\n capabilities: [\n 'chat',\n 'streaming',\n 'vision',\n 'multimodal',\n 'function_calling',\n 'json_mode',\n ],\n },\n {\n id: 'openai/gpt-4o',\n capabilities: [\n 'chat',\n 'streaming',\n 'vision',\n 'multimodal',\n 'function_calling',\n 'json_mode',\n ],\n },\n {\n id: 'anthropic/claude-3-haiku',\n capabilities: ['chat', 'streaming', 'function_calling'],\n },\n {\n id: 'anthropic/claude-3-5-sonnet',\n capabilities: [\n 'chat',\n 'streaming',\n 'vision',\n 'multimodal',\n 'function_calling',\n 'tool_use',\n 'reasoning',\n ],\n },\n {\n id: 'mistralai/mistral-large',\n capabilities: ['chat', 'streaming', 'function_calling', 'json_mode'],\n },\n {\n id: 'meta-llama/llama-3.1-70b-instruct',\n capabilities: ['chat', 'streaming', 'function_calling'],\n },\n ],\n reason: 'no-list-endpoint',\n lastReviewedAt: '2026-04-28',\n },\n pricingMode: 'none',\n enabledByDefault: true,\n priority: 10,\n docsUrl: 'https://docs.inworld.ai',\n notes:\n 'OAI-compat chat router + TTS/STT/voice-clone. Basic auth (key already base64-encoded in GCP). Discovery endpoints both return 404 (router/v1/models and /v1/models) — catalog supplies model IDs. Orphan-closure entry: the InworldAdapter class existed but was never wired until 2026-04-22.',\n lastReviewedAt: '2026-04-22',\n },\n {\n // Cloudflare Workers AI — OAI-compatible surface on CF's edge, with an\n // account-scoped URL that the hub can't template declaratively. The\n // dedicated adapter substitutes CLOUDFLARE_ACCOUNT_ID into the baseUrl\n // at construction time. Added 2026-04-22.\n providerId: 'cloudflare-workers-ai',\n displayName: 'Cloudflare Workers AI',\n providerFamily: 'cloudflare',\n aliases: ['cloudflare', 'workers-ai', 'cf-workers-ai'],\n integrationClass: 'oai-compat-pure',\n integrationMode: 'discovery+execution',\n // Placeholder — real URL is constructed by the adapter from CLOUDFLARE_ACCOUNT_ID.\n baseUrl: 'https://api.cloudflare.com/client/v4/accounts/{account_id}/ai/v1',\n authScheme: 'bearer',\n apiKeyEnvVar: 'CLOUDFLARE_API_TOKEN',\n apiKeyEnvVarOverrideReason:\n 'CLOUDFLARE_API_TOKEN is the canonical CF-wide env (Cloudflare SDK, wrangler, R2/Workers/DNS). CLOUDFLARE_WORKERS_AI_API_KEY would force users to double-set the same account-scoped token.',\n extraEnvVars: {\n CLOUDFLARE_ACCOUNT_ID:\n 'Cloudflare account ID — substituted into the Workers AI baseUrl path segment at adapter construction time. Required.',\n },\n supports: {\n chat: true,\n embeddings: true,\n streaming: true,\n tools: true,\n },\n pricingMode: 'none',\n enabledByDefault: true,\n priority: 20,\n docsUrl: 'https://developers.cloudflare.com/workers-ai/configuration/open-ai-compatibility/',\n notes:\n \"OAI-compatible surface at account-scoped URL. Model ids use @cf/vendor/name convention (e.g. @cf/meta/llama-3-8b-instruct). Workers AI has dedicated per-account neuron rate limits that do NOT share with the account's overall CF API quota — diagnose 429s via cf-ray + x-ratelimit-* response headers.\",\n adapterClass: 'CloudflareWorkersAIAdapter',\n lastReviewedAt: '2026-04-22',\n },\n {\n // HuggingFace Inference Providers — OAI-compatible router that fans out\n // to TogetherAI, Fireworks, Replicate, SambaNova, Cerebras, Nebius, etc.\n // on the user's behalf using HF_TOKEN. Uniquely valuable because HF billing\n // deducts from a single source even when inference is physically served\n // by third-party providers.\n providerId: 'huggingface',\n displayName: 'Hugging Face Inference',\n providerFamily: 'huggingface',\n aliases: ['hf', 'hf-inference', 'hf-inference-providers', 'huggingface-inference'],\n integrationClass: 'oai-compat-pure',\n integrationMode: 'discovery+execution',\n baseUrl: 'https://router.huggingface.co/v1',\n baseUrlEnvVar: 'HUGGINGFACE_BASE_URL',\n authScheme: 'bearer',\n apiKeyEnvVar: 'HF_TOKEN',\n apiKeyEnvVarOverrideReason:\n 'HF_TOKEN is the canonical env used by the HF CLI, huggingface_hub SDK, and every HF tutorial. HUGGINGFACE_API_KEY would double-set the same token across the hf-login cache and this app.',\n supports: {\n chat: true,\n embeddings: true,\n streaming: true,\n tools: true,\n },\n pricingMode: 'none',\n enabledByDefault: true,\n priority: 18,\n docsUrl: 'https://huggingface.co/docs/inference-providers/index',\n notes:\n 'Router that dispatches to TogetherAI/Fireworks/Replicate/etc. using a single HF_TOKEN. Single-tenant billing across heterogeneous backends. Uses dedicated adapter to inject `x-use-cache: false` for benchmark runs (hub default is a cache hit which ruins timing measurement).',\n adapterClass: 'HuggingFaceInferenceAdapter',\n lastReviewedAt: '2026-04-22',\n },\n\n // ──────────────────────────────────────────────────────────────────────────\n // LOTE C.1 — Enterprise hyperscaler gateways (Batch 6)\n //\n // Major-cloud-hosted OpenAI-compatible endpoints with non-trivial URL\n // composition. Each needs a dedicated adapter because the catalog's\n // declarative baseUrl field can't express per-tenant path substitution.\n // Pattern mirrors Cloudflare Workers AI (Batch 5) — factory-time resolution\n // of tenant-scoped variables into a concrete baseUrl; fail-soft sentinel URL\n // when config is missing so unrelated providers can still boot.\n // ──────────────────────────────────────────────────────────────────────────\n {\n // Azure OpenAI — deployment-scoped URL with api-version query string.\n // Most-deployed enterprise LLM gateway on the planet. Single adapter\n // instance per deployment — multi-deployment workspaces register N\n // instances via AZURE_OPENAI_DEPLOYMENTS env (factory handles expansion\n // as a future enhancement; MVP is single-deployment via AZURE_OPENAI_DEPLOYMENT).\n providerId: 'azure-openai',\n displayName: 'Azure OpenAI',\n providerFamily: 'azure-openai',\n aliases: ['azure', 'aoai', 'microsoft-azure-openai'],\n integrationClass: 'oai-compat-quirks',\n integrationMode: 'execution-only',\n // Placeholder — real URL is constructed by the adapter from\n // AZURE_OPENAI_RESOURCE_NAME + AZURE_OPENAI_DEPLOYMENT + AZURE_OPENAI_API_VERSION.\n baseUrl: 'https://{resource_name}.openai.azure.com/openai/deployments/{deployment}',\n authScheme: 'custom',\n apiKeyEnvVar: 'AZURE_OPENAI_API_KEY',\n extraEnvVars: {\n AZURE_OPENAI_RESOURCE_NAME:\n 'Azure resource subdomain (before `.openai.azure.com`). Required unless AZURE_OPENAI_ENDPOINT is set.',\n AZURE_OPENAI_DEPLOYMENT:\n 'Azure deployment alias (admin-chosen at deploy time). Required — Azure URLs embed the deployment as a path segment.',\n AZURE_OPENAI_API_VERSION:\n 'API version stamp (e.g. 2024-10-21). Required on every request as query string. Defaults to a known-GA version if unset.',\n AZURE_OPENAI_ENDPOINT:\n 'Full endpoint override for sovereign clouds (.openai.azure.us, .openai.azure.cn) or private-link FQDNs. Takes precedence over AZURE_OPENAI_RESOURCE_NAME.',\n },\n supports: {\n chat: true,\n embeddings: true,\n streaming: true,\n tools: true,\n jsonMode: true,\n vision: true, // GPT-4o deployments\n imageGeneration: true, // DALL-E 3 deployments\n textToSpeech: true,\n speechToText: true,\n },\n pricingMode: 'remote',\n enabledByDefault: true,\n priority: 35,\n // Azure deployments are admin-named aliases that wrap canonical OpenAI\n // models. We list the canonical OpenAI identities here — the adapter\n // resolves the runtime deployment alias via AZURE_OPENAI_DEPLOYMENT, but\n // the benchmark grid wants to know \"is gpt-4o available through Azure?\",\n // not \"is {arbitrary-alias} available\". Azure's convention uses `gpt-35`\n // (not `gpt-3.5`) in deployment URLs — keep both spellings here so\n // operators who mirror either convention resolve.\n // Phase 4d (2026-04-28): renamed staticModels → pinnedFallback with\n // reason='per-deployment' — there is no global Azure /models endpoint;\n // the listing is per-deployment and operator-scoped.\n pinnedFallback: {\n // Operator-declared (root-cause refactor 2026-04-28). Azure mirrors\n // the canonical OpenAI families — chat (gpt-*), reasoning (o1-*),\n // and embeddings (text-embedding-*). Capabilities follow OpenAI's\n // declared surface for each family at the model's API version.\n models: [\n {\n id: 'gpt-35-turbo',\n capabilities: ['chat', 'streaming', 'function_calling', 'json_mode'],\n },\n {\n id: 'gpt-35-turbo-16k',\n capabilities: ['chat', 'streaming', 'function_calling', 'json_mode'],\n },\n {\n id: 'gpt-4',\n capabilities: ['chat', 'streaming', 'function_calling', 'json_mode'],\n },\n {\n id: 'gpt-4-turbo',\n capabilities: [\n 'chat',\n 'streaming',\n 'vision',\n 'multimodal',\n 'function_calling',\n 'json_mode',\n ],\n },\n {\n id: 'gpt-4o',\n capabilities: [\n 'chat',\n 'streaming',\n 'vision',\n 'multimodal',\n 'function_calling',\n 'json_mode',\n ],\n },\n {\n id: 'gpt-4o-mini',\n capabilities: [\n 'chat',\n 'streaming',\n 'vision',\n 'multimodal',\n 'function_calling',\n 'json_mode',\n ],\n },\n {\n id: 'o1-mini',\n capabilities: ['chat', 'streaming', 'reasoning', 'thinking_mode'],\n },\n {\n id: 'o1-preview',\n capabilities: ['chat', 'streaming', 'reasoning', 'thinking_mode'],\n },\n {\n id: 'text-embedding-3-large',\n capabilities: ['embedding', 'embeddings'],\n },\n {\n id: 'text-embedding-3-small',\n capabilities: ['embedding', 'embeddings'],\n },\n {\n id: 'text-embedding-ada-002',\n capabilities: ['embedding', 'embeddings'],\n },\n ],\n reason: 'per-deployment',\n lastReviewedAt: '2026-04-28',\n },\n docsUrl: 'https://learn.microsoft.com/en-us/azure/ai-services/openai/reference',\n notes:\n 'Deployment-scoped URL: POST https://{resource}.openai.azure.com/openai/deployments/{deployment}/chat/completions?api-version={v}. Uses `api-key` header (not Authorization: Bearer) for subscription keys; Microsoft Entra ID bearer tokens work via Authorization header. One adapter instance per deployment — operators with multiple deployments register multiple instances.',\n adapterClass: 'AzureOpenAIAdapter',\n lastReviewedAt: '2026-04-22',\n },\n {\n // Google AI Studio (Gemini) — OAI-compatible shim endpoint. Distinct\n // from the native `google` provider (which uses @google/generative-ai\n // SDK and native /v1beta/models/{m}:generateContent surface). This\n // entry is for third-party routers that want vanilla OAI-compat chat.\n providerId: 'gemini-openai',\n displayName: 'Google AI Studio (Gemini OAI)',\n providerFamily: 'google',\n aliases: ['google-ai-studio-openai', 'gemini-oai', 'gemini-openai-compat'],\n integrationClass: 'oai-compat-pure',\n integrationMode: 'discovery+execution',\n baseUrl: 'https://generativelanguage.googleapis.com/v1beta/openai',\n baseUrlEnvVar: 'GEMINI_OPENAI_BASE_URL',\n authScheme: 'bearer',\n // Google accepts either GEMINI_API_KEY or GOOGLE_AI_STUDIO_API_KEY — the\n // env loader maps both. Catalog declares the canonical one.\n apiKeyEnvVar: 'GEMINI_API_KEY',\n apiKeyEnvVarOverrideReason:\n 'GEMINI_API_KEY is canonical in ai.google.dev docs, @google/generative-ai SDK, and Google AI Studio. The native `google` adapter shares it — GEMINI_OPENAI_API_KEY would fork one secret twice.',\n supports: {\n chat: true,\n embeddings: true,\n streaming: true,\n tools: true,\n jsonMode: true,\n vision: true,\n },\n pricingMode: 'remote',\n enabledByDefault: true,\n priority: 30,\n docsUrl: 'https://ai.google.dev/gemini-api/docs/openai',\n notes:\n \"Drop-in OpenAI-compat shim at /v1beta/openai. Same API key as the native `google` adapter (GEMINI_API_KEY). Use this for uniform wire-protocol routing via LiteLLM/OpenRouter-style aggregators; use the native GoogleAdapter for vision/video/grounded-search features that the OAI shim doesn't expose.\",\n adapterClass: 'GeminiOpenAIAdapter',\n lastReviewedAt: '2026-04-22',\n },\n {\n // GitHub Models — Microsoft's PAT-auth aggregator that surfaces OpenAI,\n // Meta, Mistral, Cohere models under a single GH-account-scoped endpoint.\n // Positioned as developer playground; aggressive per-PAT rate limits.\n providerId: 'github-models',\n displayName: 'GitHub Models',\n providerFamily: 'github-models',\n aliases: ['github', 'gh-models', 'github-marketplace-models'],\n integrationClass: 'oai-compat-pure',\n integrationMode: 'discovery+execution',\n baseUrl: 'https://models.github.ai/inference',\n baseUrlEnvVar: 'GITHUB_MODELS_BASE_URL',\n authScheme: 'bearer',\n apiKeyEnvVar: 'GITHUB_TOKEN',\n apiKeyEnvVarOverrideReason:\n 'GITHUB_TOKEN is canonical for every GitHub surface — gh CLI, Actions, REST/GraphQL, Octokit. GitHub Models accepts the same PAT; GITHUB_MODELS_API_KEY would force a second PAT per account.',\n supports: {\n chat: true,\n embeddings: true,\n streaming: true,\n tools: true,\n jsonMode: true,\n vision: true, // gpt-4o + Llama Vision deployments\n },\n pricingMode: 'none',\n enabledByDefault: true,\n priority: 10,\n docsUrl: 'https://docs.github.com/en/github-models',\n // Model catalog lives at /catalog/models on the top-level API host, NOT\n // nested under this baseUrl's /inference path — and paths.modelList only\n // accepts relative paths (ProviderEndpointPathsSchema's pathString regex\n // requires a leading \"/\"), so it can't express a different host either.\n // Discovery is wired as a dedicated hardcoded source (github-models-native\n // in central-model-discovery-service.ts) instead of the generic\n // catalog-bridge path.\n notes:\n 'GitHub PAT-auth aggregator (OpenAI / Meta / Mistral / Cohere). Free tier has aggressive per-PAT rate limits (50 req/day for cheap models) — 429s are CALLER quota, not provider health. Model ids follow `{publisher}/{name}` (e.g. openai/gpt-4o).',\n adapterClass: 'GitHubModelsAdapter',\n lastReviewedAt: '2026-04-22',\n },\n {\n // Databricks Model Serving — workspace-scoped, endpoint-scoped URL.\n // Like Azure: one adapter instance per serving endpoint. Multi-endpoint\n // workspaces register multiple instances.\n providerId: 'databricks',\n displayName: 'Databricks Model Serving',\n providerFamily: 'databricks',\n aliases: ['databricks-model-serving', 'databricks-foundation-models'],\n integrationClass: 'oai-compat-pure',\n integrationMode: 'execution-only',\n // Placeholder — real URL is constructed by the adapter from\n // DATABRICKS_HOST + DATABRICKS_SERVING_ENDPOINT.\n baseUrl: 'https://{workspace_host}/serving-endpoints/{endpoint}',\n authScheme: 'bearer',\n apiKeyEnvVar: 'DATABRICKS_TOKEN',\n apiKeyEnvVarOverrideReason:\n 'DATABRICKS_TOKEN is canonical for the Databricks CLI, databricks-sdk-py, Terraform provider, and every example. DATABRICKS_API_KEY would fork a secret the SDK already reads by name.',\n extraEnvVars: {\n DATABRICKS_HOST:\n 'Databricks workspace hostname (e.g. my-co.cloud.databricks.com, dbc-abc123.cloud.databricks.com). Required.',\n DATABRICKS_SERVING_ENDPOINT:\n 'Serving endpoint name (admin-chosen). Required — the endpoint IS the model for this adapter instance.',\n },\n supports: {\n chat: true,\n embeddings: true,\n streaming: true,\n tools: true,\n },\n pricingMode: 'none',\n enabledByDefault: true,\n priority: 20,\n // Databricks Foundation Model APIs are named canonically regardless of\n // workspace — the `databricks-*` prefix is stable across every\n // workspace-region and bills through the same pay-per-token SKU. Admins\n // can ALSO create custom serving endpoints (arbitrary aliases), but those\n // are workspace-local; the benchmark grid only meaningfully references\n // the canonical FM APIs. Expand as Databricks ships new foundation SKUs.\n // Phase 4d (2026-04-28): renamed staticModels → pinnedFallback with\n // reason='workspace-scoped' — every Databricks /serving-endpoints listing\n // is workspace-private.\n pinnedFallback: {\n models: [\n // BGE = BAAI's bge-large-en — pure embedding model.\n {\n id: 'databricks-bge-large-en',\n capabilities: ['embedding', 'embeddings'],\n },\n // DBRX is Databricks' MoE chat model with native tool-use.\n {\n id: 'databricks-dbrx-instruct',\n capabilities: ['chat', 'streaming', 'function_calling'],\n },\n // GTE = Alibaba's general text embedding.\n {\n id: 'databricks-gte-large-en',\n capabilities: ['embedding', 'embeddings'],\n },\n // Llama 3.1/3.3 instruct: native tool-use, streaming chat.\n {\n id: 'databricks-meta-llama-3-1-405b-instruct',\n capabilities: ['chat', 'streaming', 'function_calling'],\n },\n {\n id: 'databricks-meta-llama-3-1-70b-instruct',\n capabilities: ['chat', 'streaming', 'function_calling'],\n },\n {\n id: 'databricks-meta-llama-3-3-70b-instruct',\n capabilities: ['chat', 'streaming', 'function_calling'],\n },\n // Mixtral 8x7B instruct: chat + streaming + tools.\n {\n id: 'databricks-mixtral-8x7b-instruct',\n capabilities: ['chat', 'streaming', 'function_calling'],\n },\n // MPT predates native tool-use; chat + streaming only.\n {\n id: 'databricks-mpt-30b-instruct',\n capabilities: ['chat', 'streaming'],\n },\n {\n id: 'databricks-mpt-7b-instruct',\n capabilities: ['chat', 'streaming'],\n },\n ],\n reason: 'workspace-scoped',\n lastReviewedAt: '2026-04-28',\n },\n docsUrl: 'https://docs.databricks.com/en/machine-learning/model-serving/',\n notes:\n 'Workspace + endpoint-scoped URL: POST https://{workspace}.cloud.databricks.com/serving-endpoints/{endpoint}/chat/completions. Endpoint names are admin-chosen aliases (e.g. databricks-llama-3-70b-instruct, databricks-meta-llama-3-3-70b-instruct). One adapter instance per serving endpoint — for multi-endpoint workspaces, register multiple instances.',\n adapterClass: 'DatabricksAdapter',\n lastReviewedAt: '2026-04-22',\n },\n\n // ──────────────────────────────────────────────────────────────────────────\n // LOTE D — Self-hosted / local inference\n // ──────────────────────────────────────────────────────────────────────────\n {\n providerId: 'vllm',\n displayName: 'vLLM (self-hosted)',\n providerFamily: 'vllm',\n integrationClass: 'self-hosted-oai-compat',\n integrationMode: 'discovery+execution',\n baseUrl: 'http://localhost:8000/v1',\n baseUrlEnvVar: 'VLLM_BASE_URL',\n authScheme: 'bearer',\n apiKeyEnvVar: 'VLLM_API_KEY',\n apiKeyOptional: true, // vLLM doesn't require auth by default\n supports: {\n chat: true,\n embeddings: true,\n streaming: true,\n tools: true,\n },\n pricingMode: 'none',\n enabledByDefault: true,\n priority: 5,\n docsUrl: 'https://docs.vllm.ai/en/latest/serving/openai_compatible_server.html',\n notes:\n 'Self-hosted OpenAI-compatible server. Set VLLM_BASE_URL to enable. Uses dedicated VllmAdapter for apiKeyOptional handling + named identity.',\n adapterClass: 'VllmAdapter',\n lastReviewedAt: '2026-04-22',\n },\n {\n providerId: 'lm-studio',\n displayName: 'LM Studio',\n providerFamily: 'lm-studio',\n aliases: ['lmstudio', 'lm_studio'],\n integrationClass: 'self-hosted-oai-compat',\n integrationMode: 'discovery+execution',\n baseUrl: 'http://localhost:1234/v1',\n baseUrlEnvVar: 'LM_STUDIO_BASE_URL',\n authScheme: 'none',\n apiKeyEnvVar: 'LM_STUDIO_API_KEY',\n apiKeyOptional: true,\n supports: {\n chat: true,\n embeddings: true,\n streaming: true,\n },\n pricingMode: 'none',\n enabledByDefault: true,\n priority: 5,\n docsUrl: 'https://lmstudio.ai/docs/local-server',\n notes:\n 'Local OAI-compatible server bundled with LM Studio desktop. No auth by default. Uses LmStudioAdapter for apiKeyOptional + named identity.',\n adapterClass: 'LmStudioAdapter',\n lastReviewedAt: '2026-04-22',\n },\n {\n providerId: 'xinference',\n displayName: 'Xinference (Xorbits)',\n providerFamily: 'xinference',\n aliases: ['xorbits', 'xinf'],\n integrationClass: 'self-hosted-oai-compat',\n integrationMode: 'discovery+execution',\n baseUrl: 'http://localhost:9997/v1',\n baseUrlEnvVar: 'XINFERENCE_BASE_URL',\n authScheme: 'none',\n apiKeyEnvVar: 'XINFERENCE_API_KEY',\n apiKeyOptional: true,\n supports: {\n chat: true,\n embeddings: true,\n rerank: true,\n streaming: true,\n imageGeneration: true,\n speechToText: true,\n textToSpeech: true,\n },\n pricingMode: 'none',\n enabledByDefault: true,\n priority: 10,\n docsUrl: 'https://inference.readthedocs.io/en/latest/index.html',\n notes:\n 'Self-hosted multi-modality runtime. XinferenceAdapter exposes a Cohere-compatible /v1/rerank method on top of the hub OAI surface.',\n adapterClass: 'XinferenceAdapter',\n lastReviewedAt: '2026-04-22',\n },\n {\n providerId: 'triton',\n displayName: 'NVIDIA Triton Inference Server',\n providerFamily: 'triton',\n aliases: ['triton-inference-server', 'nvidia-triton'],\n integrationClass: 'self-hosted-native',\n integrationMode: 'discovery+execution',\n baseUrl: 'http://localhost:8000',\n baseUrlEnvVar: 'TRITON_BASE_URL',\n authScheme: 'none',\n apiKeyEnvVar: 'TRITON_API_KEY',\n apiKeyOptional: true,\n supports: {\n embeddings: true,\n },\n pricingMode: 'none',\n enabledByDefault: true,\n priority: 5,\n docsUrl:\n 'https://docs.nvidia.com/deeplearning/triton-inference-server/user-guide/docs/protocol/extension_generate.html',\n notes:\n 'Triton KServe v2 HTTP protocol — POST /v2/models/{model}/infer with INPUT/OUTPUT tensor arrays. Adapter converts OAI embedding request into Triton tensor shape and flattens response tensor back to float32[].',\n adapterClass: 'TritonAdapter',\n lastReviewedAt: '2026-04-22',\n },\n\n // ──────────────────────────────────────────────────────────────────────────\n // LOTE E — Migrated from provider-registry.ts switch (2026-04-21)\n //\n // These providers WERE hardcoded switch cases in provider-registry.ts\n // that all instantiated `OpenAICompatibleHubAdapter` with nothing but a\n // baseUrl + apiKey. Moving them here is a lossless migration — the same\n // adapter is built by `CatalogProviderPlugin.initialize()`, sourced from\n // the same env vars. The payoff: the switch shrinks from 41 → 21 cases,\n // and every provider added from now on is a data-row, not code.\n // ──────────────────────────────────────────────────────────────────────────\n\n // ─── E.1 — Cloud OAI-compatible hubs (single-provider endpoints) ─────────\n {\n providerId: 'nvidia',\n displayName: 'NVIDIA NIM',\n providerFamily: 'nvidia',\n // Lot B (2026-04-22) deleted `case 'nvidia-hub':` from\n // provider-registry.ts. The alias is re-attached here so historical\n // `config.providers[].name === 'nvidia-hub'` entries (or any external\n // caller that still says \"nvidia-hub\") still resolve to this single\n // catalog row. Same NVIDIA_API_KEY, same integrate.api.nvidia.com\n // endpoint — purely a rename/identity harmonization.\n aliases: ['nvidia-hub'],\n integrationClass: 'oai-compat-pure',\n integrationMode: 'discovery+execution',\n baseUrl: 'https://integrate.api.nvidia.com/v1',\n baseUrlEnvVar: 'NVIDIA_BASE_URL',\n authScheme: 'bearer',\n apiKeyEnvVar: 'NVIDIA_API_KEY',\n supports: {\n chat: true,\n embeddings: true,\n streaming: true,\n tools: true,\n jsonMode: true,\n vision: true,\n },\n pricingMode: 'none',\n enabledByDefault: true,\n priority: 40,\n docsUrl: 'https://docs.api.nvidia.com/nim/',\n notes: 'NVIDIA-hosted NIM microservices. Was switch case; migrated 2026-04-21.',\n lastReviewedAt: '2026-04-21',\n },\n {\n providerId: 'moonshot',\n displayName: 'Moonshot AI (Kimi)',\n providerFamily: 'moonshot',\n integrationClass: 'oai-compat-pure',\n integrationMode: 'discovery+execution',\n baseUrl: 'https://api.moonshot.ai/v1',\n baseUrlEnvVar: 'MOONSHOT_BASE_URL',\n authScheme: 'bearer',\n apiKeyEnvVar: 'MOONSHOT_API_KEY',\n supports: {\n chat: true,\n streaming: true,\n tools: true,\n jsonMode: true,\n vision: true,\n },\n pricingMode: 'remote',\n enabledByDefault: true,\n priority: 35,\n docsUrl: 'https://platform.moonshot.ai/docs/intro',\n notes: 'Kimi K2 / long-context family. Was switch case; migrated 2026-04-21.',\n lastReviewedAt: '2026-04-21',\n },\n {\n providerId: 'minimax',\n displayName: 'MiniMax',\n providerFamily: 'minimax',\n integrationClass: 'oai-compat-pure',\n integrationMode: 'discovery+execution',\n baseUrl: 'https://api.minimax.io/v1',\n baseUrlEnvVar: 'MINIMAX_BASE_URL',\n authScheme: 'bearer',\n apiKeyEnvVar: 'MINIMAX_API_KEY',\n supports: {\n chat: true,\n streaming: true,\n tools: true,\n vision: true,\n },\n pricingMode: 'remote',\n enabledByDefault: true,\n priority: 30,\n docsUrl: 'https://platform.minimaxi.com/document/ChatCompletion',\n notes: 'abab6 / Hailuo family. Was switch case; migrated 2026-04-21.',\n lastReviewedAt: '2026-04-21',\n },\n {\n providerId: 'friendli',\n displayName: 'Friendli',\n providerFamily: 'friendli',\n integrationClass: 'oai-compat-quirks',\n integrationMode: 'discovery+execution',\n baseUrl: 'https://api.friendli.ai/serverless/v1',\n baseUrlEnvVar: 'FRIENDLI_BASE_URL',\n authScheme: 'bearer',\n apiKeyEnvVar: 'FRIENDLI_API_KEY',\n // Team ID header is required for serverless billing attribution. The\n // default below mirrors `config/index.ts`; users override via\n // FRIENDLI_TEAM_ID at runtime — since `extraHeaders` is static JSON at\n // catalog build time, we keep the declared default here. Teams must\n // set their own FRIENDLI_TEAM_ID env if the default is stale.\n extraHeaders: {\n 'X-Friendli-Team': process.env.FRIENDLI_TEAM_ID || '',\n },\n supports: {\n chat: true,\n streaming: true,\n tools: true,\n jsonMode: true,\n },\n pricingMode: 'remote',\n enabledByDefault: true,\n priority: 30,\n docsUrl: 'https://docs.friendli.ai/',\n notes: 'Serverless endpoints for open-source LLMs. Was switch case; migrated 2026-04-21.',\n lastReviewedAt: '2026-04-21',\n },\n\n // ─── E.2 — Multi-provider aggregators / routers ──────────────────────────\n {\n providerId: 'aihubmix',\n displayName: 'AiHubMix',\n providerFamily: 'aihubmix',\n integrationClass: 'oai-compat-pure',\n integrationMode: 'discovery+execution',\n baseUrl: 'https://aihubmix.com/v1',\n baseUrlEnvVar: 'AIHUBMIX_BASE_URL',\n authScheme: 'bearer',\n apiKeyEnvVar: 'AIHUBMIX_API_KEY',\n paths: {\n // Live probe 2026-07-17: `/videos/generations` is 404 here, but POST\n // /videos EXISTS and validates (\"Missing required parameter: 'prompt'\",\n // Aihubmix_api_error) — a video route the catalog didn't know about.\n // Response/poll contract still to be proven by a first real generation.\n videoGenerate: '/videos',\n },\n supports: {\n chat: true,\n embeddings: true,\n streaming: true,\n tools: true,\n vision: true,\n imageGeneration: true,\n videoGeneration: true,\n },\n pricingMode: 'remote',\n enabledByDefault: true,\n priority: 25,\n docsUrl: 'https://aihubmix.com/docs',\n notes:\n 'Multi-upstream aggregator (OpenAI, Anthropic, Google, etc.). Was switch case; migrated 2026-04-21. Probe 2026-07-17: video route discovered at POST /videos (see paths).',\n lastReviewedAt: '2026-07-17',\n },\n {\n providerId: 'novita',\n displayName: 'Novita AI',\n providerFamily: 'novita',\n integrationClass: 'oai-compat-pure',\n integrationMode: 'discovery+execution',\n baseUrl: 'https://api.novita.ai/openai/v1',\n baseUrlEnvVar: 'NOVITA_BASE_URL',\n authScheme: 'bearer',\n apiKeyEnvVar: 'NOVITA_API_KEY',\n supports: {\n chat: true,\n embeddings: true,\n streaming: true,\n tools: true,\n imageGeneration: true,\n },\n pricingMode: 'remote',\n enabledByDefault: true,\n priority: 30,\n docsUrl: 'https://novita.ai/docs/',\n notes: 'GPU-as-a-service + LLM hub. Was switch case; migrated 2026-04-21.',\n lastReviewedAt: '2026-04-21',\n },\n {\n providerId: 'aiml',\n displayName: 'AI/ML API',\n providerFamily: 'aiml',\n integrationClass: 'oai-compat-quirks',\n integrationMode: 'discovery+execution',\n baseUrl: 'https://api.aimlapi.com/v1',\n baseUrlEnvVar: 'AIML_BASE_URL',\n authScheme: 'bearer',\n apiKeyEnvVar: 'AIML_API_KEY',\n paths: {\n // Model list lives on the bare host, not under /v1\n modelList: ['/models', '/v1/models'],\n },\n supports: {\n chat: true,\n streaming: true,\n tools: true,\n vision: true,\n imageGeneration: true,\n speechToText: true,\n textToSpeech: true,\n },\n pricingMode: 'remote',\n enabledByDefault: true,\n priority: 25,\n docsUrl: 'https://docs.aimlapi.com/',\n notes:\n 'Multi-modal aggregator. modelsBaseUrl quirk in legacy config: /models served from bare host. Migrated 2026-04-21.',\n lastReviewedAt: '2026-04-21',\n },\n {\n providerId: 'imagerouter',\n displayName: 'ImageRouter',\n providerFamily: 'imagerouter',\n integrationClass: 'oai-compat-quirks',\n integrationMode: 'discovery+execution',\n baseUrl: 'https://api.imagerouter.io',\n baseUrlEnvVar: 'IMAGEROUTER_BASE_URL',\n authScheme: 'bearer',\n apiKeyEnvVar: 'IMAGEROUTER_API_KEY',\n // Image + video paths are non-standard; routed through the OpenAI-shape\n // endpoints under /v1/openai/*. Catalog declares these so the hub adapter\n // targets the right URLs at runtime.\n paths: {\n imagesGenerate: '/v1/openai/images/generations',\n imagesEdit: '/v1/openai/images/edits',\n videoGenerate: '/v1/openai/videos/generations',\n modelList: ['/v1/models'],\n },\n supports: {\n chat: false,\n imageGeneration: true,\n imageEditing: true,\n videoGeneration: true,\n },\n pricingMode: 'remote',\n enabledByDefault: true,\n priority: 20,\n docsUrl: 'https://imagerouter.io/docs',\n notes:\n 'Image/video generation router. Quirks: paths differ from standard /v1/images/*. Was switch case; migrated 2026-04-21.',\n lastReviewedAt: '2026-04-21',\n },\n {\n providerId: 'orqai',\n displayName: 'ORQ.ai',\n providerFamily: 'orqai',\n integrationClass: 'oai-compat-pure',\n integrationMode: 'discovery+execution',\n baseUrl: 'https://api.orq.ai/v2/router',\n baseUrlEnvVar: 'ORQAI_BASE_URL',\n authScheme: 'bearer',\n apiKeyEnvVar: 'ORQAI_API_KEY',\n supports: {\n chat: true,\n streaming: true,\n tools: true,\n jsonMode: true,\n },\n pricingMode: 'remote',\n enabledByDefault: true,\n priority: 20,\n docsUrl: 'https://docs.orq.ai/',\n notes: 'LLM operations router. Was switch case; migrated 2026-04-21.',\n lastReviewedAt: '2026-04-21',\n },\n {\n providerId: 'edenai',\n displayName: 'Eden AI',\n providerFamily: 'edenai',\n integrationClass: 'oai-compat-pure',\n integrationMode: 'discovery+execution',\n baseUrl: 'https://api.edenai.run/v3/llm',\n baseUrlEnvVar: 'EDENAI_BASE_URL',\n authScheme: 'bearer',\n apiKeyEnvVar: 'EDENAI_API_KEY',\n supports: {\n chat: true,\n streaming: true,\n tools: true,\n },\n pricingMode: 'remote',\n enabledByDefault: true,\n priority: 20,\n docsUrl: 'https://docs.edenai.co/reference/llm_chat_create',\n notes: 'Multi-provider AI platform aggregator. Was switch case; migrated 2026-04-21.',\n lastReviewedAt: '2026-04-21',\n },\n {\n providerId: 'heliconeai',\n displayName: 'Helicone AI Gateway',\n providerFamily: 'heliconeai',\n integrationClass: 'gateway',\n integrationMode: 'discovery+execution',\n baseUrl: 'https://ai-gateway.helicone.ai/v1',\n baseUrlEnvVar: 'HELICONEAI_BASE_URL',\n authScheme: 'bearer',\n apiKeyEnvVar: 'HELICONEAI_API_KEY',\n supports: {\n chat: true,\n embeddings: true,\n streaming: true,\n tools: true,\n },\n pricingMode: 'remote',\n enabledByDefault: true,\n priority: 20,\n docsUrl: 'https://docs.helicone.ai/getting-started/integration-method/gateway',\n notes:\n 'Unified gateway proxying many upstreams. Was switch case; migrated 2026-04-21. D1 2026-04-24: live-validated — new ailin-heliconeai-api-key (sk-hel… 43B Virtual Key) replaces legacy ailin-heliconeai-key (11B \"PLACEHOLDER\"). Gateway routed gpt-4o-mini /chat 200 (1003B completion). Helicone injects its observability shim and forwards to the target vendor (OpenAI here); works with any target-model/Helicone-Target-Url configuration.',\n lastReviewedAt: '2026-04-24',\n },\n {\n providerId: 'cometapi',\n displayName: 'CometAPI',\n providerFamily: 'cometapi',\n integrationClass: 'oai-compat-pure',\n integrationMode: 'discovery+execution',\n baseUrl: 'https://api.cometapi.com/v1',\n baseUrlEnvVar: 'COMETAPI_BASE_URL',\n authScheme: 'bearer',\n apiKeyEnvVar: 'COMETAPI_API_KEY',\n paths: {\n // Live probe 2026-07-17: `/videos/generations` is 404 (\"Invalid URL\"),\n // but POST /videos EXISTS and validates (\"model name is required\",\n // comet_api_error). Response/poll contract still to be proven by a\n // first real generation.\n videoGenerate: '/videos',\n },\n supports: {\n chat: true,\n streaming: true,\n tools: true,\n videoGeneration: true,\n },\n pricingMode: 'remote',\n enabledByDefault: true,\n priority: 20,\n docsUrl: 'https://www.cometapi.com/docs',\n notes:\n 'Multi-model aggregator. Was switch case; migrated 2026-04-21. Probe 2026-07-17: video route discovered at POST /videos (see paths).',\n lastReviewedAt: '2026-07-17',\n },\n {\n providerId: 'nanogpt',\n displayName: 'Nano GPT',\n providerFamily: 'nanogpt',\n integrationClass: 'oai-compat-pure',\n integrationMode: 'discovery+execution',\n baseUrl: 'https://nano-gpt.com/api/v1',\n baseUrlEnvVar: 'NANOGPT_BASE_URL',\n authScheme: 'bearer',\n apiKeyEnvVar: 'NANOGPT_API_KEY',\n supports: {\n chat: true,\n streaming: true,\n },\n pricingMode: 'remote',\n enabledByDefault: true,\n priority: 15,\n docsUrl: 'https://nano-gpt.com/docs',\n notes: 'Was switch case; migrated 2026-04-21.',\n lastReviewedAt: '2026-04-21',\n },\n {\n providerId: 'requesty',\n displayName: 'Requesty',\n providerFamily: 'requesty',\n integrationClass: 'oai-compat-pure',\n integrationMode: 'discovery+execution',\n baseUrl: 'https://router.requesty.ai/v1',\n baseUrlEnvVar: 'REQUESTY_BASE_URL',\n authScheme: 'bearer',\n apiKeyEnvVar: 'REQUESTY_API_KEY',\n supports: {\n chat: true,\n streaming: true,\n tools: true,\n },\n pricingMode: 'remote',\n enabledByDefault: true,\n priority: 20,\n docsUrl: 'https://docs.requesty.ai/',\n notes: 'LLM router. Was switch case; migrated 2026-04-21.',\n lastReviewedAt: '2026-04-21',\n },\n {\n providerId: 'poe',\n displayName: 'Poe by Quora',\n providerFamily: 'poe',\n integrationClass: 'oai-compat-pure',\n integrationMode: 'discovery+execution',\n baseUrl: 'https://api.poe.com/v1',\n baseUrlEnvVar: 'POE_BASE_URL',\n authScheme: 'bearer',\n apiKeyEnvVar: 'POE_API_KEY',\n supports: {\n chat: true,\n streaming: true,\n },\n pricingMode: 'remote',\n enabledByDefault: true,\n priority: 15,\n docsUrl: 'https://creator.poe.com/docs/external-applications/openai-compatible-api',\n notes: 'Poe OpenAI-compatible endpoint. Was switch case; migrated 2026-04-21.',\n lastReviewedAt: '2026-04-21',\n },\n {\n providerId: 'routeway',\n displayName: 'Routeway',\n providerFamily: 'routeway',\n integrationClass: 'oai-compat-pure',\n integrationMode: 'discovery+execution',\n baseUrl: 'https://api.routeway.ai/v1',\n baseUrlEnvVar: 'ROUTEWAY_BASE_URL',\n authScheme: 'bearer',\n apiKeyEnvVar: 'ROUTEWAY_API_KEY',\n supports: {\n chat: true,\n streaming: true,\n tools: true,\n },\n pricingMode: 'remote',\n enabledByDefault: true,\n priority: 15,\n docsUrl: 'https://routeway.ai/docs',\n notes: 'Was switch case; migrated 2026-04-21.',\n lastReviewedAt: '2026-04-21',\n },\n\n // ─── E.3 — Local OAI-compatible sidecars ─────────────────────────────────\n //\n // These are migrated as `self-hosted-oai-compat`. They use a synthetic\n // apiKeyEnvVar for schema compliance but `apiKeyOptional: true` means the\n // adapter doesn't require it. The legacy config injected literal strings\n // like `apiKey: 'local'` — the catalog path uses whatever env value is\n // present (empty string if nothing set), which the hub adapter tolerates\n // for local servers that don't authenticate.\n //\n // enabledByDefault: true (per the universal \"habilitado e nunca censurado\"\n // policy). Loader still skips at boot when baseUrlEnvVar is unset — the\n // health check just fails-soft and the entry lands in the skipped bucket.\n {\n providerId: 'ollama',\n displayName: 'Ollama (Local LLM)',\n providerFamily: 'ollama',\n integrationClass: 'self-hosted-oai-compat',\n integrationMode: 'discovery+execution',\n baseUrl: 'http://localhost:11434/v1',\n // Legacy env var is OLLAMA_URL (not _BASE_URL). Honored here via\n // baseUrlEnvVar so existing deployments keep working.\n baseUrlEnvVar: 'OLLAMA_URL',\n authScheme: 'none',\n apiKeyEnvVar: 'OLLAMA_API_KEY',\n apiKeyOptional: true,\n supports: {\n chat: true,\n embeddings: true,\n streaming: true,\n tools: true,\n vision: true,\n },\n pricingMode: 'none',\n // Opt-in: loader only registers when OLLAMA_URL is set in env.\n enabledByDefault: true,\n priority: 10,\n docsUrl: 'https://github.com/ollama/ollama/blob/main/docs/openai.md',\n notes:\n 'Local Ollama server. Set OLLAMA_URL (e.g. http://localhost:11434/v1) to enable. Dedicated OllamaAdapter gives named identity for logs/metrics and a subclass seat for future native /api/* endpoints (tags, pull, generate).',\n adapterClass: 'OllamaAdapter',\n lastReviewedAt: '2026-04-22',\n },\n {\n providerId: 'local-llama',\n displayName: 'Local LLM (llama.cpp)',\n providerFamily: 'local-llama',\n integrationClass: 'self-hosted-oai-compat',\n integrationMode: 'discovery+execution',\n baseUrl: 'http://localhost:8080/v1',\n baseUrlEnvVar: 'LOCAL_LLAMA_URL',\n authScheme: 'none',\n apiKeyEnvVar: 'LOCAL_LLAMA_API_KEY',\n apiKeyOptional: true,\n supports: {\n chat: true,\n streaming: true,\n },\n pricingMode: 'none',\n enabledByDefault: true,\n priority: 5,\n docsUrl: 'https://github.com/ggerganov/llama.cpp/blob/master/examples/server/README.md',\n notes: 'llama.cpp server in OpenAI-compat mode. Set LOCAL_LLAMA_URL to enable.',\n lastReviewedAt: '2026-04-21',\n },\n {\n providerId: 'local-kobold',\n displayName: 'Local VLM (KoboldCpp)',\n providerFamily: 'local-kobold',\n integrationClass: 'self-hosted-oai-compat',\n integrationMode: 'discovery+execution',\n baseUrl: 'http://localhost:5001/v1',\n baseUrlEnvVar: 'LOCAL_KOBOLD_URL',\n authScheme: 'none',\n apiKeyEnvVar: 'LOCAL_KOBOLD_API_KEY',\n apiKeyOptional: true,\n supports: {\n chat: true,\n streaming: true,\n vision: true, // GGUF + mmproj path\n },\n pricingMode: 'none',\n enabledByDefault: true,\n priority: 5,\n docsUrl: 'https://github.com/LostRuins/koboldcpp',\n notes: 'KoboldCpp vision-capable local server. Set LOCAL_KOBOLD_URL to enable.',\n lastReviewedAt: '2026-04-21',\n },\n {\n providerId: 'local-embeddings',\n displayName: 'Local Embeddings (ONNX)',\n providerFamily: 'local-embeddings',\n integrationClass: 'self-hosted-oai-compat',\n integrationMode: 'discovery+execution',\n baseUrl: 'http://localhost:8081/v1',\n baseUrlEnvVar: 'LOCAL_EMBEDDINGS_URL',\n authScheme: 'none',\n apiKeyEnvVar: 'LOCAL_EMBEDDINGS_API_KEY',\n apiKeyOptional: true,\n supports: {\n embeddings: true,\n rerank: true,\n },\n pricingMode: 'none',\n enabledByDefault: true,\n priority: 5,\n docsUrl: 'https://onnxruntime.ai/',\n notes: 'ONNX Runtime embeddings + rerank server. Set LOCAL_EMBEDDINGS_URL to enable.',\n lastReviewedAt: '2026-04-21',\n },\n // ── Orphan closure (2026-04-23) — Lot B (Writer/Upstage/Reka) ─────────\n // Each of these three OpenAI-compatible providers had an adapter class on\n // disk (WriterAdapter, UpstageAdapter, RekaAIAdapter) and a working key\n // in GCP Secret Manager, but no catalog row and no factory registration —\n // i.e., the exact Inworld failure mode closed in 2026-04-22.\n //\n // Direct fetch probes executed this session against /v1/chat/completions\n // returned HTTP 200 for all three using the loaded keys. Post-closure\n // re-probe via the wired pipeline is scheduled as part of this turn; until\n // that completes, consolidation-matrix.ts pins them to a closure-transient\n // bucket. Mechanically they are full canonical providers from now on.\n {\n providerId: 'writer',\n displayName: 'Writer',\n providerFamily: 'writer',\n integrationClass: 'oai-compat-quirks',\n // execution-only because GET /v1/models returns `{models: [{id, name}]}`\n // (Writer's own shape), not OpenAI's `{data: [{id, object, ...}]}` —\n // the default hub discovery parser skips the body and discovers zero\n // models. Same mitigation as Inworld: hand-supply model IDs here.\n // Post-closure probe (2026-04-23) against /v1/chat/completions using\n // palmyra-x-004 returned HTTP 200 \"PONG\" — execution path verified.\n integrationMode: 'execution-only',\n baseUrl: 'https://api.writer.com/v1',\n authScheme: 'bearer',\n apiKeyEnvVar: 'WRITER_API_KEY',\n adapterClass: 'WriterAdapter',\n supports: {\n chat: true,\n streaming: true,\n tools: true,\n jsonMode: true,\n },\n // Sourced directly from GET /v1/models on 2026-04-23 with the canonical\n // key. This list reflects upstream AT probe time; treating it as\n // illustrative (not authoritative) — the router accepts any upstream\n // model ID Writer serves. Update when a discovery-parser enhancement\n // lands that handles the `{models: [...]}` shape.\n // Phase 4d (2026-04-28): renamed staticModels → pinnedFallback with\n // reason='proprietary-schema' — `/v1/models` returns Writer's own\n // `{models: [...]}` shape (not OpenAI `{data: [...]}`), which the default\n // OAI hub parser cannot consume; a dedicated `WriterModelFetcher` would\n // unblock discovery+execution but is deferred to a follow-up.\n pinnedFallback: {\n models: [\n // Palmyra X family — flagship general chat with native tools + JSON mode.\n {\n id: 'palmyra-x-004',\n capabilities: ['chat', 'streaming', 'function_calling', 'json_mode'],\n },\n {\n id: 'palmyra-x5',\n capabilities: ['chat', 'streaming', 'function_calling', 'json_mode'],\n },\n {\n id: 'palmyra-x4',\n capabilities: ['chat', 'streaming', 'function_calling', 'json_mode'],\n },\n {\n id: 'palmyra-x-003-instruct',\n capabilities: ['chat', 'streaming', 'function_calling', 'json_mode'],\n },\n // palmyra-vision IS multimodal upstream, but requires Writer-specific\n // content shape (see notes); declare vision so downstream callers can\n // route correctly even though the adapter currently rejects standard\n // OAI multimodal content arrays.\n {\n id: 'palmyra-vision',\n capabilities: ['chat', 'streaming', 'vision', 'multimodal'],\n },\n // Domain-specialised palmyras: chat-only (no documented tools/JSON).\n {\n id: 'palmyra-med',\n capabilities: ['chat', 'streaming'],\n },\n {\n id: 'palmyra-fin',\n capabilities: ['chat', 'streaming'],\n },\n {\n id: 'palmyra-creative',\n capabilities: ['chat', 'streaming'],\n },\n ],\n reason: 'proprietary-schema',\n lastReviewedAt: '2026-04-28',\n },\n pricingMode: 'none',\n enabledByDefault: true,\n priority: 10,\n docsUrl: 'https://dev.writer.com',\n notes:\n 'Enterprise Palmyra-family chat + generation. OAI-compat execution path (chat/completions); non-standard `{models: [...]}` discovery shape — catalog supplies model IDs until discovery-parser supports the shape. palmyra-vision is listed upstream but requires Writer-specific content shape (standard OAI multimodal content array returns 400 OpenrouterException); vision support deferred to a dedicated adapter extension. Orphan-closure entry 2026-04-23.',\n lastReviewedAt: '2026-04-23',\n },\n {\n providerId: 'upstage',\n displayName: 'Upstage',\n providerFamily: 'upstage',\n aliases: ['upstage-ai'],\n integrationClass: 'oai-compat-pure',\n integrationMode: 'discovery+execution',\n baseUrl: 'https://api.upstage.ai/v1',\n authScheme: 'bearer',\n apiKeyEnvVar: 'UPSTAGE_API_KEY',\n adapterClass: 'UpstageAdapter',\n // jsonMode capability confirmed via post-closure probe 2026-04-23\n // against solar-pro with response_format={type:'json_object'} — returned\n // valid {\"ok\": true}. Upstage's JSON-mode requires the prompt to\n // contain the literal substring \"json\" (identical to OpenAI's rule).\n // embeddings confirmed via /v1/embeddings probe on solar-embedding-1-\n // large-query: HTTP 200 with real float array.\n supports: {\n chat: true,\n streaming: true,\n embeddings: true,\n tools: true,\n jsonMode: true,\n },\n pricingMode: 'none',\n enabledByDefault: true,\n priority: 10,\n docsUrl: 'https://developers.upstage.ai',\n notes:\n 'Solar LLM family + document-AI embeddings. OAI-compat. Orphan-closure entry 2026-04-23: adapter class existed at providers/upstage/ but was never wired. Post-closure live probes (chat, embeddings, jsonMode) all 200.',\n lastReviewedAt: '2026-04-23',\n },\n {\n providerId: 'rekaai',\n displayName: 'Reka AI',\n providerFamily: 'reka',\n aliases: ['reka', 'reka-ai'],\n integrationClass: 'oai-compat-pure',\n integrationMode: 'discovery+execution',\n baseUrl: 'https://api.reka.ai/v1',\n authScheme: 'bearer',\n apiKeyEnvVar: 'REKA_API_KEY',\n apiKeyEnvVarOverrideReason:\n 'REKA_API_KEY is canonical in docs.reka.ai and their Python SDK. providerId `rekaai` disambiguates the adapter filename; env follows upstream to avoid double-setting the same secret.',\n adapterClass: 'RekaAIAdapter',\n supports: {\n chat: true,\n streaming: true,\n vision: true,\n tools: true,\n },\n pricingMode: 'none',\n enabledByDefault: true,\n priority: 10,\n docsUrl: 'https://docs.reka.ai',\n notes:\n 'Reka Core / Flash / Edge multimodal chat. OAI-compat surface. Orphan-closure entry 2026-04-23: adapter class existed at providers/rekaai/ but was never wired.',\n lastReviewedAt: '2026-04-23',\n },\n\n // ──────────────────────────────────────────────────────────────────────────\n // LOTE M — 2026-04-23 complementary lot (13 entries)\n //\n // Research basis: per-provider documentation audit on 2026-04-23 against\n // each vendor's official docs URL. All integration classes, auth schemes,\n // and base URLs derived from the authoritative docs (not inferred).\n //\n // Credential state for this lot: all 13 are in credentials-missing /\n // secret-absent at catalog-time (no GCP secret provisioned under any\n // known alias). Exception: `qianfan` is a promotion — 3 Baidu secrets\n // already exist (ailin-baidu-{key,secret,base-url}), but they back the\n // v1 legacy AK+SK OAuth flow, NOT the v2 bce-v3 bearer path this entry\n // canonicalizes; so qianfan enters bucket credentials-missing /\n // auth-incomplete until a QIANFAN_API_KEY (bce-v3 format) is provisioned.\n //\n // Not added this lot (documented in NON_CANONICAL_HISTORICAL_CLAIMS):\n // - liquid — no first-party production API; LFM only via OpenRouter\n // - modelrun — domain does not resolve (ECONNREFUSED); unverifiable\n // - ncompass — base_url unverified in public docs; requires operator\n // signup to discover the exact endpoint before canonicalizing\n // ──────────────────────────────────────────────────────────────────────────\n {\n providerId: 'arcee',\n displayName: 'Arcee AI',\n providerFamily: 'arcee',\n integrationClass: 'oai-compat-pure',\n integrationMode: 'discovery+execution',\n baseUrl: 'https://api.arcee.ai/api/v1',\n authScheme: 'bearer',\n apiKeyEnvVar: 'ARCEE_API_KEY',\n supports: {\n chat: true,\n streaming: true,\n reasoning: true,\n },\n pricingMode: 'remote',\n enabledByDefault: true,\n priority: 10,\n docsUrl: 'https://docs.arcee.ai',\n notes:\n 'Arcee SLM family (Trinity). OAI-compat chat, OpenRouter-style /models metadata. Reasoning at message.reasoning_content (streaming: delta.reasoning_content) — NOT message.reasoning as previously documented; unverified until 2026-07-29. Lot M 2026-04-23. D1 2026-04-24: 402 insufficient-credits. Live-verified 2026-07-29 after a $5 top-up: GET /v1/models 200 (trinity-mini renamed to trinity-large-thinking), POST /v1/chat/completions 200 (real content+usage), stream:true 200 (SSE, [DONE]).',\n lastReviewedAt: '2026-07-29',\n },\n {\n providerId: 'atlascloud',\n displayName: 'AtlasCloud',\n providerFamily: 'atlascloud',\n aliases: ['atlas-cloud'],\n integrationClass: 'oai-compat-pure',\n integrationMode: 'execution-only',\n baseUrl: 'https://api.atlascloud.ai/v1',\n authScheme: 'bearer',\n apiKeyEnvVar: 'ATLASCLOUD_API_KEY',\n // Phase 4d (2026-04-28): renamed staticModels → pinnedFallback with\n // reason='no-list-endpoint' (vendor docs document none).\n pinnedFallback: {\n models: [\n // DeepSeek-V3 chat — OAI-compat tools + streaming.\n {\n id: 'deepseek-v3',\n capabilities: ['chat', 'streaming', 'function_calling'],\n },\n // ByteDance Seedream-3.0 — image generation.\n {\n id: 'seedream-3.0',\n capabilities: ['image_generation'],\n },\n // Kling v2.0 — video generation.\n {\n id: 'kling-v2.0',\n capabilities: ['video_generation'],\n },\n ],\n reason: 'no-list-endpoint',\n lastReviewedAt: '2026-04-28',\n },\n supports: {\n chat: true,\n streaming: true,\n vision: true,\n imageGeneration: true,\n // 2026-07-17: videoGeneration REMOVED — live probe proved POST\n // /v1/videos/generations AND /v1/videos are both 404 here (\"404 page\n // not found\"), so the flag only fed the execution pool guaranteed\n // failures for the kling-v2.0 pinned model. Re-add together with a\n // paths.videoGenerate once the real Atlas video route is discovered\n // and live-proven.\n },\n pricingMode: 'none',\n enabledByDefault: true,\n priority: 10,\n docsUrl: 'https://www.atlascloud.ai/docs',\n notes:\n 'GPU cloud aggregator. Multi-surface (chat+image+video) under a single OAI-compat base URL. No public GET /models; static model IDs taken from docs. Lot M 2026-04-23. Probe 2026-07-17: no OAI-style video route (404 both candidates) — video de-advertised until a real route is proven.',\n lastReviewedAt: '2026-07-17',\n },\n {\n providerId: 'avian',\n displayName: 'Avian.io',\n providerFamily: 'avian',\n aliases: ['avian-io'],\n integrationClass: 'oai-compat-pure',\n integrationMode: 'execution-only',\n baseUrl: 'https://api.avian.io/v1',\n authScheme: 'bearer',\n apiKeyEnvVar: 'AVIAN_API_KEY',\n // Phase 4d (2026-04-28): renamed staticModels → pinnedFallback with\n // reason='no-list-endpoint' (no /models endpoint documented).\n pinnedFallback: {\n models: [\n // DeepSeek V3.2 — chat + streaming + tools (OAI-compat).\n {\n id: 'deepseek/deepseek-v3.2',\n capabilities: ['chat', 'streaming', 'function_calling'],\n },\n // Moonshot Kimi K2.6 — chat + streaming + tools + 200k context.\n {\n id: 'moonshotai/kimi-k2.6',\n capabilities: ['chat', 'streaming', 'function_calling', 'long_context'],\n },\n // Z.AI GLM-5.1 — chat + streaming + tools.\n {\n id: 'z-ai/glm-5.1',\n capabilities: ['chat', 'streaming', 'function_calling'],\n },\n ],\n reason: 'no-list-endpoint',\n lastReviewedAt: '2026-04-28',\n },\n supports: {\n chat: true,\n streaming: true,\n tools: true,\n },\n pricingMode: 'none',\n enabledByDefault: true,\n priority: 10,\n docsUrl: 'https://avian.io/docs',\n notes:\n 'OAI-compat inference aggregator (deepseek, moonshot, z-ai routes). Static model list per docs; no /models endpoint documented. Lot M 2026-04-23.',\n lastReviewedAt: '2026-04-23',\n },\n {\n // Baidu Qianfan (ERNIE platform) — promoted to canonical Lot M 2026-04-23.\n //\n // Pre-promotion state: 3 GCP secrets existed (ailin-baidu-{key,secret,base-url})\n // with load-secrets-into-env.ts mapping ERNIE_API_KEY / ERNIE_SECRET_KEY /\n // BAIDU_BASE_URL, but no catalog row and no adapter — classic\n // \"secret-descoberto-mas-não-canônico\".\n //\n // Qianfan documents TWO auth paths:\n // v1 (legacy): AK+SK → OAuth access_token → ?access_token= query param\n // against aip.baidubce.com/rpc/2.0/ai_custom/v1/wenxinworkshop/chat\n // v2 (current OAI-compat): bce-v3/... bearer key against\n // qianfan.baidubce.com/v2\n //\n // We canonicalize v2 (OAI-compat-quirks). v1 would require a dedicated\n // native adapter that does not exist in this repo; v2 unlocks\n // chat/stream/tools/embed/vision without new adapter code.\n //\n // The existing AK+SK secrets remain mapped (load-secrets-into-env.ts) for\n // any legacy caller that still wants the v1 path, but they are NOT\n // sufficient for this catalog entry's runtime. A QIANFAN_API_KEY\n // (bce-v3/... format) must be provisioned separately in GCP before\n // execution is possible — until then bucket = credentials-missing /\n // auth-incomplete.\n providerId: 'qianfan',\n displayName: 'Baidu Qianfan (ERNIE)',\n providerFamily: 'qianfan',\n aliases: ['baidu', 'ernie', 'baidu-qianfan'],\n integrationClass: 'oai-compat-quirks',\n integrationMode: 'execution-only',\n baseUrl: 'https://qianfan.baidubce.com/v2',\n baseUrlEnvVar: 'BAIDU_BASE_URL',\n authScheme: 'bearer',\n apiKeyEnvVar: 'QIANFAN_API_KEY',\n apiKeyEnvVarOverrideReason:\n 'QIANFAN_API_KEY matches the v2 bce-v3/... bearer format at qianfan.baidubce.com/v2. Legacy ERNIE_API_KEY / ERNIE_SECRET_KEY remain mapped for the v1 AK+SK OAuth flow, not this v2 runtime path.',\n // Phase 4d (2026-04-28): renamed staticModels → pinnedFallback with\n // reason='no-list-endpoint' — Qianfan v2 docs do not document a generic\n // model-list endpoint; ERNIE family IDs are vendor-published.\n pinnedFallback: {\n models: [\n // ERNIE 4.0 8k context — chat + streaming + tools (per Qianfan docs).\n {\n id: 'ernie-4.0-8k',\n capabilities: ['chat', 'streaming', 'function_calling'],\n },\n // ERNIE 3.5 8k context — chat + streaming (tools support partial in 3.5).\n {\n id: 'ernie-3.5-8k',\n capabilities: ['chat', 'streaming', 'function_calling'],\n },\n // ERNIE X1.1 — reasoning model (Baidu's deep-think SKU).\n {\n id: 'ernie-x1.1',\n capabilities: ['chat', 'streaming', 'reasoning', 'thinking_mode'],\n },\n ],\n reason: 'no-list-endpoint',\n lastReviewedAt: '2026-04-28',\n },\n supports: {\n chat: true,\n streaming: true,\n tools: true,\n embeddings: true,\n vision: true,\n imageGeneration: true,\n },\n pricingMode: 'none',\n enabledByDefault: true,\n priority: 5,\n docsUrl: 'https://cloud.baidu.com/doc/WENXINWORKSHOP/index.html',\n notes:\n 'PRC-region. Content-safety filters applied server-side. Two auth paths: v1 AK+SK→OAuth (legacy, not wired in this repo) and v2 bce-v3 bearer (our canonical). Sublote A probe 2026-04-23: v2 /chat/completions with empty bearer → HTTP 401 invalid_iam_token (expected); v1 /wenxinworkshop/chat/ernie-4.0-8k with empty access_token → HTTP 200 error_code=3 \"Unsupported openapi method\" (host reachable, method routing rejects empty auth). Both surfaces confirmed alive.',\n lastReviewedAt: '2026-04-23',\n },\n {\n providerId: 'gmi',\n displayName: 'GMICloud',\n providerFamily: 'gmi',\n aliases: ['gmicloud', 'gmi-cloud', 'gmi-serving'],\n integrationClass: 'oai-compat-pure',\n integrationMode: 'discovery+execution',\n baseUrl: 'https://api.gmi-serving.com/v1',\n authScheme: 'bearer',\n apiKeyEnvVar: 'GMI_API_KEY',\n supports: {\n chat: true,\n streaming: true,\n tools: true,\n vision: true,\n embeddings: true,\n imageGeneration: true,\n videoGeneration: true,\n },\n pricingMode: 'none',\n enabledByDefault: true,\n priority: 5,\n docsUrl: 'https://docs.gmicloud.ai',\n notes:\n 'Serverless + Dedicated GPU platform. Two-plane API: inference at api.gmi-serving.com, provisioning/IAM at console.gmicloud.ai. Video API distinct from LLM API. Sublote C1 2026-04-23: /v1/chat and /v1/embeddings return 404 \"No matching target server found for model X\" BEFORE auth validation — model routing is pre-auth; 404 with unknown model does NOT imply bad key. Lot M 2026-04-23.',\n lastReviewedAt: '2026-04-23',\n },\n {\n providerId: 'infermatic',\n displayName: 'Infermatic',\n providerFamily: 'infermatic',\n integrationClass: 'oai-compat-quirks',\n integrationMode: 'discovery+execution',\n baseUrl: 'https://api.totalgpt.ai/v1',\n authScheme: 'bearer',\n apiKeyEnvVar: 'INFERMATIC_API_KEY',\n supports: {\n chat: true,\n streaming: true,\n embeddings: true,\n },\n pricingMode: 'none',\n enabledByDefault: true,\n priority: 5,\n docsUrl: 'https://docs.infermatic.ai',\n notes:\n 'Zero-logs vLLM behind LiteLLM proxy. Roleplay catalog (Sao10K, TheDrummer, Magnum). Domain mismatch: docs on docs.infermatic.ai, API on api.totalgpt.ai. Extra sampling params (top_k, repetition_penalty); some models reject system prompts. Tools NOT documented. Lot M 2026-04-23. D1 2026-04-24: live-validated — ailin-infermatic-api-key (25B sk-12T…) is model-scoped LiteLLM Virtual Key; Qwen-Qwen3-30B-A3B chat 200 (476B). Key ACL pins model list.',\n lastReviewedAt: '2026-04-24',\n },\n {\n providerId: 'inflection',\n displayName: 'Inflection AI (Pi)',\n providerFamily: 'inflection',\n aliases: ['inflection-ai', 'pi', 'pi-inflection'],\n integrationClass: 'oai-compat-pure',\n // 2026-06-15: Inflection shipped a standard OpenAI-compatible API at\n // https://api.inflection.ai/v1 (chat/completions + embeddings, Bearer auth).\n // The old proprietary /external/api/inference path needed a custom adapter;\n // the OAI surface does not. Promoted catalog-only → execution-only — there is\n // no /v1/models listing (discovery is the non-standard /v1/discovery/configs),\n // so the pinnedFallback shortlist below is the inventory.\n integrationMode: 'execution-only',\n baseUrl: 'https://api.inflection.ai/v1',\n authScheme: 'bearer',\n apiKeyEnvVar: 'INFLECTION_API_KEY',\n pinnedFallback: {\n models: [\n // Inflection-3 Pi — emotionally-intelligent companion chat.\n {\n id: 'inflection_3_pi',\n capabilities: ['chat', 'streaming'],\n },\n // Inflection-3 Productivity — instruction-following + JSON-output tuned.\n {\n id: 'inflection_3_productivity',\n capabilities: ['chat', 'streaming', 'json_mode'],\n },\n // Pi-3.1 — beta agentic SKU.\n {\n id: 'Pi-3.1',\n capabilities: ['chat', 'streaming'],\n },\n ],\n reason: 'no-list-endpoint',\n lastReviewedAt: '2026-06-15',\n },\n supports: {\n chat: true,\n streaming: true,\n },\n pricingMode: 'none',\n enabledByDefault: true,\n priority: 5,\n docsUrl: 'https://developers.inflection.ai',\n notes:\n 'OpenAI-compatible API at https://api.inflection.ai/v1 (chat/completions + embeddings, Bearer auth) — no custom adapter needed (the old /external/api/inference proprietary path did). No /v1/models listing (discovery is /v1/discovery/configs, non-standard shape); pinnedFallback carries the documented chat SKUs: inflection_3_pi, inflection_3_productivity (on /v1), Pi-3.1 (legacy OAI-compat path). Promoted first-party-native/catalog-only → oai-compat-pure/execution-only 2026-06-15.',\n lastReviewedAt: '2026-04-23',\n },\n {\n providerId: 'mancer',\n displayName: 'Mancer',\n providerFamily: 'mancer',\n integrationClass: 'oai-compat-quirks',\n integrationMode: 'discovery+execution',\n baseUrl: 'https://neuro.mancer.tech/oai/v1',\n authScheme: 'bearer',\n apiKeyEnvVar: 'MANCER_API_KEY',\n supports: {\n chat: true,\n streaming: true,\n },\n pricingMode: 'none',\n enabledByDefault: true,\n contentPolicyClass: 'uncensored',\n priority: 5,\n docsUrl: 'https://docs.mancer.tech',\n notes:\n 'Roleplay/creative (\"No filters, No guidelines, No constraints\"). Tagged contentPolicyClass=uncensored — fully admitted per universal \"habilitado e nunca censurado\" policy (Phase 4b 2026-04-28); tag is informational, downstream surfaces may filter. Credit-based pricing (not USD/token). 9 models incl. MythoMax-13B, Goliath-120B, Magnum-72B-v4. SillyTavern primary client. Lot M 2026-04-23.',\n lastReviewedAt: '2026-04-28',\n },\n {\n providerId: 'phala',\n displayName: 'Phala (RedPill TEE)',\n providerFamily: 'phala',\n aliases: ['redpill', 'red-pill'],\n integrationClass: 'oai-compat-pure',\n integrationMode: 'discovery+execution',\n baseUrl: 'https://api.redpill.ai/v1',\n authScheme: 'bearer',\n apiKeyEnvVar: 'PHALA_API_KEY',\n apiKeyEnvVarOverrideReason:\n 'RedPill (api.redpill.ai) is the runtime face of Phala Network. We canonicalize env var to `phala` for providerId parity; `redpill-key` / `redpill-api-key` stay as GCP aliases.',\n supports: {\n chat: true,\n streaming: true,\n tools: true,\n jsonMode: true,\n vision: true,\n embeddings: true,\n },\n pricingMode: 'none',\n enabledByDefault: true,\n priority: 5,\n docsUrl: 'https://docs.phala.network',\n notes:\n 'TEE-attested LLM gateway (NVIDIA H100/H200 Confidential Computing). Every response carries cryptographic attestation verifiable at /phala-cloud/confidential-ai/verify. Aggregates upstream models (Anthropic, OpenAI, DeepSeek, Qwen) running inside enclaves. Lot M 2026-04-23.',\n lastReviewedAt: '2026-04-23',\n },\n {\n providerId: 'relace',\n displayName: 'Relace',\n providerFamily: 'relace',\n integrationClass: 'first-party-native',\n integrationMode: 'catalog-only',\n baseUrl: 'https://instantapply.endpoint.relace.run/v1',\n authScheme: 'bearer',\n apiKeyEnvVar: 'RELACE_API_KEY',\n // Phase 4d (2026-04-28): renamed staticModels → pinnedFallback with\n // reason='proprietary-schema'. Relace exposes specialty surfaces\n // (/v1/code/apply, /v1/code/rerank, /v1/embeddings) instead of a\n // generic /v1/models listing — proprietary schema, dedicated\n // RelaceAdapter required. Curated shortlist is the canonical inventory\n // until adapter lands.\n pinnedFallback: {\n models: [\n // Code-apply specialty: structured-edit application, NOT general chat.\n // 'code_edit' captures the surface; we also tag 'coding' for\n // capability-search discoverability (canonical enum value).\n {\n id: 'relace-apply-3',\n capabilities: ['code_edit', 'coding'],\n },\n // Code reranker — reranking + retrieval over a code corpus.\n {\n id: 'relace-code-reranker',\n capabilities: ['reranking', 'retrieval', 'coding'],\n },\n // Code-tuned embedding.\n {\n id: 'relace-embedding',\n capabilities: ['embedding', 'embeddings', 'coding'],\n },\n ],\n reason: 'proprietary-schema',\n lastReviewedAt: '2026-04-28',\n },\n supports: {\n // Specialty code-edit — standard chat surfaces intentionally NOT declared.\n // Proprietary surfaces (codeApply, codeRerank) are not in the generic\n // `supports` enum; documented in notes below until the schema grows.\n embeddings: true,\n },\n pricingMode: 'none',\n enabledByDefault: true,\n priority: 5,\n docsUrl: 'https://docs.relace.ai',\n notes:\n 'Specialty code-edit API — NOT a general-purpose chat provider. Proprietary /v1/code/apply merge endpoint (>10k tok/s). Also offers rerank, embed, retrieval. Requires dedicated RelaceAdapter (codeApply, codeRerank); integrationMode=catalog-only until adapter exists. Note 2026-07-29: morph is NOT a real precedent — morph uses standard /v1/chat/completions, no dedicated adapter, never catalog-only. Relace genuinely needs one: its endpoints are non-chat-shaped. Lot M 2026-04-23.',\n lastReviewedAt: '2026-04-23',\n },\n {\n providerId: 'siliconflow',\n displayName: 'SiliconFlow',\n providerFamily: 'siliconflow',\n aliases: ['silicon-flow'],\n integrationClass: 'oai-compat-quirks',\n integrationMode: 'discovery+execution',\n baseUrl: 'https://api.siliconflow.cn/v1',\n authScheme: 'bearer',\n apiKeyEnvVar: 'SILICONFLOW_API_KEY',\n supports: {\n chat: true,\n streaming: true,\n tools: true,\n vision: true,\n embeddings: true,\n rerank: true,\n imageGeneration: true,\n imageEditing: true,\n videoGeneration: true,\n speechToText: true,\n },\n pricingMode: 'none',\n enabledByDefault: true,\n priority: 5,\n docsUrl: 'https://docs.siliconflow.cn',\n notes:\n 'PRC-region primary (.cn); .com fronts same service. Full multi-surface: chat+image+video+audio+rerank. Supports enable_thinking + thinking_budget (128–32768). Returns x-siliconcloud-trace-id header. Lot M 2026-04-23. D1 2026-04-24: key provisioned (ailin-siliconflow-api-key, 51B \"sk-hhc…\") but /v1/chat returns 401 bare-JSON \"Api key is invalid\" (15B oai-compat-quirks shape). sk- prefix non-canonical; probable format mismatch. Sub-class: auth-incomplete.',\n lastReviewedAt: '2026-04-24',\n },\n {\n providerId: 'stepfun',\n displayName: 'StepFun',\n providerFamily: 'stepfun',\n aliases: ['step', 'step-ai'],\n integrationClass: 'oai-compat-pure',\n integrationMode: 'discovery+execution',\n baseUrl: 'https://api.stepfun.com/v1',\n authScheme: 'bearer',\n apiKeyEnvVar: 'STEPFUN_API_KEY',\n supports: {\n chat: true,\n streaming: true,\n tools: true,\n vision: true,\n embeddings: true,\n imageGeneration: true,\n imageEditing: true,\n speechToText: true,\n textToSpeech: true,\n videoGeneration: true,\n realtime: true,\n },\n pricingMode: 'none',\n enabledByDefault: true,\n priority: 5,\n docsUrl: 'https://platform.stepfun.com/docs',\n notes:\n 'OpenAI SDK v1.0+ drop-in compat. Dual-region: api.stepfun.com (PRC) / api.stepfun.ai (global). Realtime voice + cloning. 10-min timeout → 503. Lot M 2026-04-23. D1 2026-04-24: key provisioned (ailin-stepfun-api-key, 65B) but /v1/chat returns 401 OAI-shape {\"error\":{\"message\":\"Incorrect API key provided\",\"type\":\"invalid_api_key\"}}. Endpoint alive; specific key rejected. Operator must verify key-account binding. Sub-class: auth-incomplete.',\n lastReviewedAt: '2026-04-24',\n },\n {\n providerId: 'venice',\n displayName: 'Venice AI',\n providerFamily: 'venice',\n aliases: ['venice-ai'],\n integrationClass: 'oai-compat-quirks',\n integrationMode: 'discovery+execution',\n baseUrl: 'https://api.venice.ai/api/v1',\n authScheme: 'bearer',\n apiKeyEnvVar: 'VENICE_API_KEY',\n supports: {\n chat: true,\n streaming: true,\n tools: true,\n jsonMode: true,\n vision: true,\n embeddings: true,\n imageGeneration: true,\n imageEditing: true,\n speechToText: true,\n textToSpeech: true,\n videoGeneration: true,\n },\n pricingMode: 'none',\n enabledByDefault: true,\n contentPolicyClass: 'uncensored',\n priority: 5,\n docsUrl: 'https://docs.venice.ai',\n notes:\n 'Privacy-first, no retention. Tagged contentPolicyClass=uncensored (Phase 4b 2026-04-28; informational, downstream may filter). venice_parameters: enable_web_search, character_slug, E2EE, reasoning. Async /audio/queue + /video/queue. Double-path /api/v1. Safety headers x-venice-is-blurred / -contains-minor. Sublote A 2026-04-23: /models PUBLIC; /chat HTTP 402 (USDC Base L2 eip155:8453) alt to bearer — impl bearer-only. Lot M.',\n lastReviewedAt: '2026-04-28',\n },\n // ── Alibaba Cloud (Dashscope / Qwen) — closed 2026-05-06 ──────────────\n // Closes 154 orphan rows in DB that had `provider_id='alibaba'` but no\n // catalog entry. The Alibaba model fetcher\n // (services/model-fetchers/alibaba-model-fetcher.ts) was already\n // populating the DB via `discoverySource: 'alibaba-native'` against\n // Dashscope's OpenAI-compatible endpoint. The runtime adapter side was\n // missing — this row + the standard hub-extending OAI factory wires it\n // through. Dashscope-intl (Singapore) is the default region; operators\n // in mainland China should override DASHSCOPE_BASE_URL to the\n // dashscope.aliyuncs.com endpoint.\n {\n providerId: 'alibaba',\n displayName: 'Alibaba Cloud (Dashscope / Qwen)',\n providerFamily: 'alibaba',\n aliases: ['qwen', 'dashscope', 'alicloud', 'alibaba-cloud'],\n integrationClass: 'oai-compat-pure',\n integrationMode: 'discovery+execution',\n baseUrl: 'https://dashscope-intl.aliyuncs.com/compatible-mode/v1',\n baseUrlEnvVar: 'DASHSCOPE_BASE_URL',\n authScheme: 'bearer',\n apiKeyEnvVar: 'QWEN_API_KEY',\n apiKeyEnvVarOverrideReason:\n 'QWEN_API_KEY is the canonical env var documented in Dashscope upstream docs and SDK examples',\n supports: {\n chat: true,\n streaming: true,\n tools: true,\n jsonMode: true,\n embeddings: true,\n vision: true,\n },\n pricingMode: 'remote',\n enabledByDefault: true,\n priority: 25,\n docsUrl:\n 'https://help.aliyun.com/zh/dashscope/developer-reference/compatibility-of-openai-with-dashscope',\n notes:\n 'Qwen + DeepSeek hosted via Dashscope OAI-compat endpoint. Adds qwen-coder, qwen-vl, qwen-flash, qvq-max + DeepSeek-V3.x mirrors. The pre-existing alibaba-model-fetcher.ts handles discovery (wraps OpenAI client against compatible-mode/v1); this catalog row adds the runtime adapter via the standard hub bridge. Operator override DASHSCOPE_BASE_URL=https://dashscope.aliyuncs.com/compatible-mode/v1 for mainland China region.',\n lastReviewedAt: '2026-05-06',\n },\n // ── AWS Bedrock — closed 2026-05-06 ───────────────────────────────────\n // Closes 125 orphan rows in DB that had `provider_id='aws-bedrock'` but\n // no catalog entry (the legacy switch-case path in provider-registry.ts\n // is unreachable because no config.providers entry has `name:\n // 'aws-bedrock'`). Bedrock requires SigV4 signing — proprietary schema —\n // so integrationClass=first-party-native and a dedicated factory binding\n // (`AwsBedrockAdapter`) constructs the adapter with region + creds from\n // env. Auth methods, in priority order:\n // 1. AWS_ACCESS_KEY_ID + AWS_SECRET_ACCESS_KEY (+ AWS_SESSION_TOKEN)\n // 2. AWS_BEARER_TOKEN_BEDROCK (newer Bedrock-direct auth)\n // 3. Container/Lambda role via AWS SDK default credential chain\n // The factory passes ALL of these through; the adapter chooses based on\n // SDK precedence.\n {\n providerId: 'aws-bedrock',\n displayName: 'AWS Bedrock',\n providerFamily: 'aws-bedrock',\n aliases: ['bedrock', 'aws', 'amazon', 'bedrock-runtime', 'aws-bedrock-runtime'],\n integrationClass: 'first-party-native',\n integrationMode: 'execution-only',\n // Region is selected at request time inside the adapter; baseUrl here\n // is informational/placeholder. AWS SDK constructs the actual URL.\n baseUrl: 'https://bedrock-runtime.us-east-1.amazonaws.com',\n // hmac-sigv4: Bedrock auth is AWS Signature V4. The SDK signs each\n // request with credentials from the standard AWS credential chain\n // (env vars → shared config → instance role). Declaring the canonical\n // scheme here also satisfies Zod Rule 1's apiKeyEnvVar exemption for\n // SigV4 — AWS_ACCESS_KEY_ID is the SDK's convention and would force\n // users to double-set if we added an `AWS_BEDROCK_API_KEY` alternative.\n authScheme: 'hmac-sigv4',\n apiKeyEnvVar: 'AWS_ACCESS_KEY_ID',\n apiKeyOptional: true, // role-based auth path bypasses ACCESS_KEY_ID\n adapterClass: 'AwsBedrockAdapter',\n pinnedFallback: {\n // Bedrock model IDs are vendor-pinned identifiers (anthropic.claude-*,\n // amazon.nova-*, meta.llama3-*, mistral.*). The Bedrock /foundation-models\n // listing endpoint returns thousands of vendor variants — we pin a\n // curated roster of the most commonly-used canonical IDs. Operators\n // should expand this list as AWS publishes new SKUs.\n models: [\n {\n id: 'anthropic.claude-3-5-sonnet-20241022-v2:0',\n capabilities: ['chat', 'tool_use', 'vision', 'streaming'],\n },\n {\n id: 'anthropic.claude-3-5-haiku-20241022-v1:0',\n capabilities: ['chat', 'tool_use', 'streaming'],\n },\n {\n id: 'anthropic.claude-3-opus-20240229-v1:0',\n capabilities: ['chat', 'tool_use', 'vision', 'streaming'],\n },\n { id: 'amazon.nova-pro-v1:0', capabilities: ['chat', 'tool_use', 'vision', 'streaming'] },\n { id: 'amazon.nova-lite-v1:0', capabilities: ['chat', 'tool_use', 'vision', 'streaming'] },\n { id: 'amazon.nova-micro-v1:0', capabilities: ['chat', 'streaming'] },\n { id: 'meta.llama3-1-70b-instruct-v1:0', capabilities: ['chat', 'streaming'] },\n { id: 'meta.llama3-1-8b-instruct-v1:0', capabilities: ['chat', 'streaming'] },\n { id: 'mistral.mistral-large-2407-v1:0', capabilities: ['chat', 'tool_use', 'streaming'] },\n { id: 'cohere.command-r-plus-v1:0', capabilities: ['chat', 'tool_use', 'streaming'] },\n { id: 'cohere.embed-english-v3', capabilities: ['embeddings'] },\n { id: 'cohere.embed-multilingual-v3', capabilities: ['embeddings'] },\n { id: 'amazon.titan-embed-text-v2:0', capabilities: ['embeddings'] },\n ],\n reason: 'curated-shortlist',\n lastReviewedAt: '2026-05-06',\n },\n supports: {\n chat: true,\n streaming: true,\n tools: true,\n vision: true,\n },\n pricingMode: 'remote',\n enabledByDefault: true,\n priority: 40,\n docsUrl: 'https://docs.aws.amazon.com/bedrock/',\n notes:\n 'AWS Bedrock Converse API. SigV4 auth via AWS SDK default credential chain (AWS_ACCESS_KEY_ID/SECRET, AWS_BEARER_TOKEN_BEDROCK, container role, etc.). Region from AWS_BEDROCK_REGION or AWS_REGION (default us-east-1). Inference-profile ARNs supported via AWS_BEDROCK_INFERENCE_PROFILE_ARN for cross-region routing.',\n lastReviewedAt: '2026-05-06',\n },\n\n // ──────────────────────────────────────────────────────────────────────────\n // LOTE O (2026-07-10) — Apertis + Inception Labs onboarding.\n //\n // Both had GCP secrets provisioned by the operator (ailin-apertis-key,\n // ailin-inception-key) ahead of the catalog rows. No live probe was run\n // this session (gcloud ADC needed interactive reauth, unavailable\n // non-interactively) — see consolidation-matrix.ts 'no-live-validation'\n // bucket. Two sibling providers researched in the same pass — EmpirioLabs\n // AI and Concentrate AI — are intentionally NOT added yet: EmpirioLabs\n // pending a live smoke-test (its docs cite an unfamiliar domain/model\n // lineup), Concentrate AI pending its own bespoke discovery fetcher\n // (nested per-provider model shape, doesn't fit the generic hub fetcher).\n // ──────────────────────────────────────────────────────────────────────────\n {\n providerId: 'apertis',\n displayName: 'Apertis',\n providerFamily: 'apertis',\n // Gateway: re-serves 400+ models from 30+ upstream vendors (OpenAI,\n // Anthropic, Google, xAI, Qwen, DeepSeek, ...) behind one OpenAI-\n // compatible surface. `owned_by` on /v1/models attributes the real\n // upstream vendor per model.\n integrationClass: 'gateway',\n integrationMode: 'discovery+execution',\n baseUrl: 'https://api.apertis.ai/v1',\n paths: {\n embeddings: '/embeddings',\n // rerank REMOVED 2026-07-16: /v1/rerank returns 404 \"Invalid URL\" —\n // the route does not exist on the gateway even though reranker models\n // (BAAI/bge-reranker-v2-m3, Qwen3-Reranker-*) appear in ITS /v1/models\n // catalog. Advertised-but-unrouted; re-add only after a live 200.\n imagesGenerate: '/images/generations',\n // imagesEdit REMOVED 2026-07-16: the /v1/images/edits route parses the\n // multipart body but DROPS the `model` form field — every attempt\n // (model first, model last, different model ids) failed with\n // `The model '' does not exist`. Gateway-side defect; a standard\n // OpenAI SDK images.edit() call would fail identically.\n audioSpeech: '/audio/speech',\n audioTranscriptions: '/audio/transcriptions',\n },\n authScheme: 'bearer',\n apiKeyEnvVar: 'APERTIS_API_KEY',\n adapterClass: 'ApertisAdapter',\n originalProviderField: 'owned_by',\n supports: {\n chat: true,\n streaming: true, // live-probed 2026-07-16: token SSE + [DONE]\n embeddings: true, // live-probed 2026-07-16: text-embedding-3-small 200\n // rerank / imageEditing removed 2026-07-16 — see the paths comments:\n // both surfaces are broken on the provider side (unrouted endpoint /\n // dropped multipart field), not merely untested.\n imageGeneration: true, // route exists; upstream 429-saturated during the 2026-07-16 probe (dall-e-3), so functionally unconfirmed\n speechToText: true, // live-probed 2026-07-16: whisper-1 transcribed real audio\n textToSpeech: true, // route exists (tts-1 in catalog); upstream 429-saturated on all 3 probe attempts, functionally unconfirmed\n vision: true,\n },\n pricingMode: 'none', // hybrid subscription-multiplier + PAYG billing; doesn't fit the remote per-token extractor\n enabledByDefault: true,\n docsUrl: 'https://docs.apertis.ai/api/',\n notes:\n 'Multi-vendor gateway (400+ models, new-api-style). ApertisAdapter disables its native fallback_models so failures surface to our orchestrator. Probe 2026-07-16: SSE/embeddings/STT proven; imageGen+TTS routes exist, upstream 429-saturated (unconfirmed); rerank 404 + images/edits drops the model field — both removed from supports (see paths comments). dall-e-2 absent; use dall-e-3/gpt-image-*. Video async, out of scope. sk-sub- keys 403 on /audio.',\n lastReviewedAt: '2026-07-16',\n },\n {\n providerId: 'inception',\n displayName: 'Inception Labs (Mercury)',\n providerFamily: 'inception',\n // First-party inference (NOT a broker) — Mercury is a diffusion LLM\n // (dLLM), not autoregressive. OpenAI-compatible chat shape with real\n // quirks: restricted sampling-param surface and a `diffusing` streaming\n // mode with a non-standard SSE contract (see InceptionAdapter).\n integrationClass: 'oai-compat-quirks',\n integrationMode: 'discovery+execution',\n baseUrl: 'https://api.inceptionlabs.ai/v1',\n authScheme: 'bearer',\n apiKeyEnvVar: 'INCEPTION_API_KEY',\n adapterClass: 'InceptionAdapter',\n supports: {\n chat: true,\n streaming: true,\n tools: true,\n jsonMode: true,\n reasoning: true,\n },\n capabilityHints: [{ capability: 'diffusion_lm', rationale: 'docs-declared', confidence: 0.6 }],\n pricingMode: 'none',\n enabledByDefault: true,\n docsUrl: 'https://docs.inceptionlabs.ai/get-started/get-started',\n notes:\n 'dLLM (diffusion). NEVER send diffusing:true — confirmed live 2026-07-16: chunks carry diffusion_meta and the FULL rewritten text (naive delta concatenation duplicates output); InceptionAdapter drops the flag and logs. Normal SSE + tool-calling proven same day. temperature server-clamped [0.5,1.0]; adapter clamps client-side. Text-only. FIM/Next-Edit out of scope (non-chat payload).',\n lastReviewedAt: '2026-07-16',\n },\n\n // ──────────────────────────────────────────────────────────────────────────\n // LOTE P (2026-07-11) — EmpirioLabs AI onboarding.\n //\n // Held back in LOTE O pending live validation (docs cited an unfamiliar\n // domain/model lineup — Qwen3-Max, Seed 2.0 Pro, Kling O3, GLM-TTS —\n // that didn't match any known catalog). Operator re-authenticated gcloud\n // (device-code flow) and the real ailin-empiriolabs-key was probed live:\n // GET /v1/models 200 (134+ models incl. real Kling/DeepSeek/Zhipu/Qwen\n // entries) and POST /v1/chat/completions 200 with deepseek-v4-flash\n // (\"pong\", cost_usd tracked in the response). The domain and models are\n // real — validation gate satisfied, plain catalog-only entry (no quirks\n // requiring a dedicated adapter surfaced for the chat surface).\n // ──────────────────────────────────────────────────────────────────────────\n {\n providerId: 'empiriolabs',\n displayName: 'EmpirioLabs AI',\n providerFamily: 'empiriolabs',\n integrationClass: 'gateway',\n integrationMode: 'discovery+execution',\n baseUrl: 'https://api.empiriolabs.ai/v1',\n paths: {\n embeddings: '/embeddings',\n rerank: '/reranks',\n // Live probe 2026-07-17: POST /videos/generations resolves MODELS\n // (body {} → \"model_not_found\" naming a model, i.e. the route exists\n // and got past routing to model resolution). Production attempt on the\n // same day returned 402 insufficient_credits — the surface works; the\n // only blocker is account balance (operator funding decision).\n videoGenerate: '/videos/generations',\n },\n authScheme: 'bearer',\n apiKeyEnvVar: 'EMPIRIOLABS_API_KEY',\n // Confirmed via live probe: model entries carry `\"provider\":\"<vendor>\"`\n // (e.g. \"kling\", \"zhipu\", \"deepseek\"), not OpenAI's `owned_by`.\n originalProviderField: 'provider',\n supports: {\n chat: true,\n streaming: true,\n tools: true,\n jsonMode: true,\n vision: true,\n embeddings: true,\n rerank: true,\n videoGeneration: true,\n },\n pricingMode: 'none', // heterogeneous per-unit billing (token/request/image/second); per-response cost_usd is real but /models pricing shape doesn't fit the remote extractor\n enabledByDefault: true,\n docsUrl: 'https://docs.empiriolabs.ai/welcome',\n notes:\n 'Multi-vendor gateway (134+ models). Probe 2026-07-16: SSE, embeddings (text-embedding-v4), rerank (/reranks PLURAL, qwen3-rerank) and system-override all proven — our system message fully replaces the platform default. Its catalog category field is authoritative (embedding/reranker). CAVEAT: deepseek-v4-flash streams into delta.reasoning_content, content empty at low max_tokens — budget generously. Only /v1/chat/completions wired. Probe 2026-07-17: video route live (blocked only by 402 credits).',\n lastReviewedAt: '2026-07-17',\n },\n\n // ──────────────────────────────────────────────────────────────────────────\n // LOTE Q (2026-07-12) — Concentrate AI onboarding.\n //\n // Meta-aggregator: re-serves OpenAI/Anthropic/Bedrock/Vertex/Azure/Mistral/\n // Cohere/xAI/DeepSeek/MiniMax/z.ai/Moonshot/etc behind one surface (its own\n // `list-providers` enumerates 20 upstream slugs, incl. itself as\n // `concentrate`/`redact` — a PII-redaction feature, not a real inference\n // vendor). Per operator decision (2026-07-10): no gap-filler dedup engineering\n // — cataloged as a normal `gateway`, trusting the pool-builder's cost/quality\n // ranking to naturally deprioritize routes with proxy markup.\n //\n // Discovery is UNAUTHENTICATED — live-probed without any key 2026-07-12:\n // GET /v1/models/providers 200 (20 provider slugs, matches doc exactly) and\n // GET /v1/models/ 200 (`{object:\"list\",data:[{id,owned_by,...}]}` — same\n // shape the generic hub fetcher already expects, so NO custom fetcher is\n // needed despite the nested per-provider shape on the enrichment endpoints\n // (list-models-by-provider/get-model/get-provider-info) — those are richer\n // cross-reference endpoints we don't need for basic catalog population.\n // Execution (chat/completions) requires the provisioned key; gcloud ADC\n // expired again this session before it could be probed live — see\n // consolidation-matrix.ts `no-live-validation`.\n // ──────────────────────────────────────────────────────────────────────────\n {\n providerId: 'concentrate',\n displayName: 'Concentrate AI',\n providerFamily: 'concentrate',\n integrationClass: 'gateway',\n integrationMode: 'discovery+execution',\n baseUrl: 'https://api.concentrate.ai/v1',\n paths: {\n modelList: ['/models/'],\n chatCompletions: '/chat/completions/',\n responses: '/responses/',\n },\n authScheme: 'bearer',\n apiKeyEnvVar: 'CONCENTRATE_API_KEY',\n originalProviderField: 'owned_by',\n supports: {\n chat: true,\n responses: true,\n streaming: true,\n tools: true,\n },\n pricingMode: 'none', // pricing lives on the enrichment endpoints (get-model/get-provider-info), not on the flat /v1/models/ list the generic extractor reads\n enabledByDefault: true,\n docsUrl: 'https://concentrate.ai/docs/api-reference/introduction',\n notes:\n 'Meta-aggregator (20 upstream providers). Discovery unauthenticated; execution paths carry a TRAILING SLASH. Probe 2026-07-16: SSE + /responses/ proven (azure/gpt-4o-mini routing visible, cost breakdown in payload). 424 = ITS upstream vendor failed — classified retryable server_error by provider-error-classifier. Its concentrate/redact-v1 listing is a PII-redaction feature, not an inference vendor.',\n lastReviewedAt: '2026-07-16',\n },\n\n // ──────────────────────────────────────────────────────────────────────────\n // LOTE R (2026-07-13) — FastRouter onboarding.\n //\n // Meta-aggregator (\"control plane for routing across multiple LLM\n // providers\") — re-serves 20 upstream providers (OpenAI, Anthropic, Azure,\n // Bedrock, Vertex, Groq, Together, Fireworks, DeepInfra, Moonshot, MiniMax,\n // Perplexity, X-AI, Nebius, Baseten, FAL AI, Leonardo, Pollo, BytePlus,\n // itself). Per operator decision (2026-07-10, same as apertis/empiriolabs/\n // concentrate): no gap-filler dedup engineering — cataloged as a normal\n // `gateway`, trusting the pool-builder's cost/quality ranking.\n //\n // Discovery is UNAUTHENTICATED — live-probed without any key 2026-07-13:\n // GET /api/v1/providers 200 (21 provider_id/label pairs) and GET\n // /api/v1/models 200 (real data, `{data:[{id,creator,pricing,...}]}` —\n // same shape the generic hub fetcher expects, INCLUDING a `pricing` object\n // directly on the flat list — unlike apertis/empiriolabs/concentrate,\n // pricingMode can be `remote` here). Execution (chat/completions) requires\n // the provisioned key; gcloud ADC was mid-reauth at catalog-row time — see\n // consolidation-matrix.ts `no-live-validation`.\n // ──────────────────────────────────────────────────────────────────────────\n {\n providerId: 'fastrouter',\n displayName: 'FastRouter',\n providerFamily: 'fastrouter',\n integrationClass: 'gateway',\n integrationMode: 'discovery+execution',\n baseUrl: 'https://api.fastrouter.ai/api/v1',\n authScheme: 'bearer',\n apiKeyEnvVar: 'FASTROUTER_API_KEY',\n // Confirmed via live probe: model entries carry `\"creator\":\"<vendor>\"`\n // (e.g. \"Anthropic\", \"Google\", \"DeepSeek\"), not OpenAI's `owned_by`.\n originalProviderField: 'creator',\n paths: {\n // Live probe 2026-07-17: video is an ASYNC QUEUE at POST /videos\n // (`/videos/generations` has NEVER existed here — 404 Route not found).\n // Submit returns `{data:{taskId,status:\"processing\"}}` (FastRouter runs\n // its own upstream failover per fr_failover_history); poll GET\n // /videos/{taskId} until data.generations[]/fastrouter_assets.urls[]\n // appear. No cancel route (DELETE and /cancel both 404). CAUTION: the\n // submit endpoint accepts `{model}` WITHOUT prompt and starts a real\n // billable job — never probe it with a routable model id.\n videoGenerate: '/videos',\n videoPoll: '/videos/{taskId}',\n },\n supports: {\n chat: true,\n streaming: true,\n tools: true,\n embeddings: true,\n reasoning: true,\n videoGeneration: true,\n },\n pricingMode: 'remote',\n enabledByDefault: true,\n docsUrl: 'https://docs.fastrouter.ai/',\n notes:\n 'Meta-aggregator (20 upstream, BYOK). /api/v1 canonical (docs show both prefixes). Probe 2026-07-16: SSE proven (:FASTROUTER PROCESSING comment keep-alives + own final usage chunk with cost+provider before [DONE]); embeddings proven; :flex suffix proven (service_tier flex). Model ids provider/model may carry :price/:throughput/:flex — do not strip. fastrouter/auto not used (ci does its own selection). 402 = insufficient credits. Probe 2026-07-17: video async-queue surface mapped live (see paths).',\n lastReviewedAt: '2026-07-17',\n },\n\n // ──────────────────────────────────────────────────────────────────────────\n // LOTE S (2026-07-13) — Perplexity Agent API onboarding.\n //\n // Operator asked whether the EXISTING `perplexity` row (classic Sonar-only\n // /chat/completions) also served Anthropic/OpenAI/Google/xAI/Z.AI/Moonshot/\n // NVIDIA models. It doesn't — but a SEPARATE Perplexity product does: the\n // \"Agent API\" (`/v1/agent`, aliased `/v1/responses`), a genuinely different\n // wire protocol (Responses-style input/output, not messages/choices) under\n // the SAME Perplexity account/key. Modeled as its own catalog row rather\n // than extending `perplexity` — the two surfaces have disjoint model\n // namespaces (sonar-* vs vendor/model), disjoint wire shapes, and one\n // catalog row = one execution surface is the established convention here.\n //\n // Live-probed 2026-07-13 with the real ailin-perplexity-api-key (same\n // GCP secret as the classic `perplexity` row — no separate key was\n // provisioned; PROVIDER_SECRETS below points PERPLEXITY_AGENT_API_KEY at\n // the same secret names):\n // - GET /v1/models 200, AUTHENTICATED, real OpenAI-list shape\n // (`{data:[{id,object,owned_by,created}]}`) — 32 models across\n // anthropic/google/nvidia/openai/xai/perplexity(-owned: glm-5.2,\n // kimi-k2.7-code, sonar).\n // - POST /v1/agent 200 for: anthropic/claude-haiku-4-5, openai/gpt-5.4-mini\n // (NOT \"gpt-5-mini\" — that id doesn't exist, hangs), google/gemini-3.5-flash,\n // xai/grok-4.5, perplexity/glm-5.2 (z.ai). Also confirmed `input` accepts\n // a role+content array (same shape as our ChatMessage[]), not just a\n // flat string.\n // - perplexity/kimi-k2.7-code (Moonshot): accepted by model-id validation\n // (wrong ids reject fast with 400; this one doesn't) but every live call\n // hung with zero response bytes past 90s — left wired, NOT confirmed\n // working. Not in live-validation evidence below for that reason.\n // ──────────────────────────────────────────────────────────────────────────\n {\n providerId: 'perplexity-agent',\n displayName: 'Perplexity Agent API',\n providerFamily: 'perplexity-agent',\n integrationClass: 'gateway',\n integrationMode: 'discovery+execution',\n baseUrl: 'https://api.perplexity.ai/v1',\n authScheme: 'bearer',\n // Shares the same physical credential as the classic `perplexity` row\n // (one Perplexity account, two API surfaces) — deliberately a distinct\n // env var (not a typo/dup) so PROVIDER_SECRETS can source both from the\n // same GCP secret independently. Naturally matches the <PROVIDERID>_API_KEY\n // convention (perplexity-agent -> PERPLEXITY_AGENT_API_KEY); no Zod Rule 1\n // override needed.\n apiKeyEnvVar: 'PERPLEXITY_AGENT_API_KEY',\n adapterClass: 'PerplexityAgentAdapter',\n originalProviderField: 'owned_by',\n supports: {\n chat: true,\n streaming: true, // real SSE since 2026-07-16 — Responses-dialect parser in the adapter (shape captured live)\n tools: true, // live-probed 2026-07-16: flat Responses-style tools in, function_call items out (adapter converts both directions)\n reasoning: true,\n },\n pricingMode: 'none', // /v1/models discovery response carries no pricing object (unlike fastrouter); cost is only visible per-response (usage.cost) after a real call\n enabledByDefault: true,\n docsUrl: 'https://docs.perplexity.ai/docs/agent-api/models',\n notes:\n 'Second Perplexity surface (same account/key as `perplexity`): /v1/agent, Responses-style, NOT chat/completions. REAL token streaming since 2026-07-16 (response.output_text.delta / response.completed; NO [DONE]; gappy sequence_number) — unit-tested against the live-captured fixture. Tools proven live (flat shape; toolu_bdrk_ ids reveal Bedrock-served Anthropic). Moonshot kimi-k2.7-code works but hangs intermittently when cold — orchestrator timeout+fallback covers it.',\n lastReviewedAt: '2026-07-16',\n },\n\n // ──────────────────────────────────────────────────────────────────────────\n // LOTE T (2026-07-13) — Ailin gateway. Self-referential meta-provider. Lets\n // any deployment of this engine (this codebase, self-hosted by a third\n // party) reach the full ~100-provider / 70k+-model catalog through a single\n // AILIN_API_KEY instead of provisioning every upstream provider's own key\n // directly — see api.ailin.one's own docs/reference/endpoints/authentication.md,\n // which already documents AILIN_API_KEY as the `X-API-Key` credential\n // customers send inbound. This entry is the outbound mirror: THIS engine,\n // acting as a client of api.ailin.one. Inert for Ailin's own canonical\n // api.ailin.one deployment, since nothing there sets AILIN_API_KEY — the\n // loader's missing-api-key skip path (catalog-loader.ts) makes that a no-op,\n // not a self-call loop.\n //\n // discovery+execution, with a known heuristic gap: chat/embeddings/images/\n // audio were verified against api.ailin.one's own openapi-spec.yaml to be\n // bare OpenAI-compatible request/response shape at the generic hub's\n // default paths (no `paths` override needed). GET /v1/models was checked\n // too — its `data[]` items carry `id` and `originProvider` (both understood\n // by the generic fetcher: `id` always, `originProvider` via\n // `originalProviderField` below) but ALSO ailin-native fields the generic\n // fetcher does not understand (`operability`, `nonOperationalReasons`,\n // `fallbackChain`, `resolvedProvider`). Net effect: discovery will surface\n // every listed model, including ones ailin's own orchestration currently\n // marks `non_operational` — those may 4xx on execution until a dedicated\n // fetcher maps the richer shape (follow-up, not guessed at here). Given\n // the ~70k-model, constantly-shifting catalog, a hand-pinned fallback list\n // would be stale on arrival and violates this project's no-static-model\n // rule, so this entry accepts the heuristic gap over inventing one.\n // ──────────────────────────────────────────────────────────────────────────\n {\n providerId: 'ailin',\n displayName: 'Ailin (api.ailin.one)',\n providerFamily: 'ailin',\n integrationClass: 'gateway',\n integrationMode: 'discovery+execution',\n baseUrl: 'https://api.ailin.one/v1',\n authScheme: 'api-key-header',\n authHeaderName: 'X-API-Key',\n apiKeyEnvVar: 'AILIN_API_KEY',\n originalProviderField: 'originProvider',\n supports: {\n chat: true,\n embeddings: true,\n streaming: true,\n tools: true,\n jsonMode: true,\n vision: true,\n reasoning: true,\n imageGeneration: true,\n imageEditing: true,\n speechToText: true,\n textToSpeech: true,\n moderation: true,\n },\n pricingMode: 'none',\n enabledByDefault: true,\n priority: 0,\n notes:\n \"Meta-gateway to ~100 upstream providers / 70k+ models behind api.ailin.one's own orchestration. Discovery uses the generic hub fetcher against a richer native /v1/models shape it only partly understands (see header comment) — operability filtering is a known follow-up gap. Auth is X-API-Key; api.ailin.one also accepts bearerAuth JWT for user sessions, not used here (static server credential).\",\n lastReviewedAt: '2026-07-13',\n },\n // ──────────────────────────────────────────────────────────────────────────\n // Sakana AI (Fugu) — added 2026-07-29 in PR #234, promoted to fully\n // operational the same day once the operator activated pay-as-you-go\n // billing on the account (previously had no active subscription, so every\n // chat/completions call 429'd regardless of integration correctness).\n // - GET /v1/models -> 200, real 5-model array (fugu, fugu-ultra,\n // fugu-ultra-20260615, fugu-ultra-v1.0, fugu-ultra-v1.1) even though\n // NEITHER docs page (console.sakana.ai/get-started or /models)\n // documents this endpoint. Shape matches the generic\n // OpenAICompatibleHubModelFetcher (`data[].id`) — no dedicated\n // fetcher/pinnedFallback needed.\n // - POST /v1/chat/completions: after billing activation, a real\n // end-to-end re-verification was run directly against the live API\n // (2026-07-29, GCP secret `ailin-sakana-ai-key`) and confirmed chat,\n // streaming, tool calls, JSON mode, and vision ALL genuinely work —\n // real HTTP 200 responses with correct, sensible content, not merely\n // the absence of an error. Vision in particular is now evidence-\n // backed rather than doc-inferred: a multimodal request containing an\n // inline image correctly identified the image's content, and\n // prompt_tokens jumped sharply on that call versus text-only calls —\n // proof the image was actually processed, not silently dropped. That\n // DIRECTLY CONTRADICTS console.sakana.ai/models' own capability\n // table, which marks Vision unsupported (✗); the live probe is\n // trusted over the vendor's apparently stale/incorrect docs.\n // - Observed quirk, noted but not acted on: `usage.completion_tokens_\n // details.reasoning_tokens` showed up on a plain `fugu` call even\n // though nothing in the request asked for reasoning — worth watching,\n // not itself a reason to change anything (`supports.reasoning` was\n // already `true`).\n // - fugu-ultra appears to route requests through an internal\n // multi-agent orchestration layer (matches its docs' \"routes 1-3\n // agents\" description) — `usage.prompt_tokens_details.\n // orchestration_input_tokens` reflects that on fugu-ultra calls.\n // - fugu-cyber / fugu-cyber-v1.0 are excluded via modelDenylist: the\n // pricing docs say cyber \"requires access request approval\" and is\n // \"available only through pay-as-you-go billing\"; this key's\n // /v1/models response does not list either cyber model, consistent\n // with no cyber entitlement. Deliberately not advertising a model\n // most keys can't call.\n // - pricingMode is `none`, not `remote`: neither /v1/models nor\n // chat/completions responses expose pricing/context-window fields,\n // and this catalog's `static-file` PricingMode is reserved/\n // unimplemented. For reference only (marketing page, not machine-\n // read): fugu-ultra $5/$30 per 1M in/out tokens standard, $10/$45\n // above 272K context, $0.50/$1.00 cached; fugu-cyber $6/$36 standard,\n // $12/$54 above 272K, $0.60/$1.20 cached; base fugu has no per-token\n // price — subscription-only (Standard $20/mo, Pro $100/mo, Max\n // $200/mo).\n // - apiKeyEnvVar follows the `<PROVIDER_ID_UPPER>_API_KEY` convention\n // (SAKANA_AI_API_KEY, not the shorter SAKANA_API_KEY) per Rule 1 in\n // provider-catalog.schema.ts — Sakana has no well-known upstream SDK\n // env-var name to justify an override.\n // ──────────────────────────────────────────────────────────────────────────\n {\n providerId: 'sakana-ai',\n displayName: 'Sakana AI (Fugu)',\n providerFamily: 'sakana-ai',\n integrationClass: 'oai-compat-pure',\n integrationMode: 'discovery+execution',\n baseUrl: 'https://api.sakana.ai/v1',\n baseUrlEnvVar: 'SAKANA_AI_BASE_URL',\n authScheme: 'bearer',\n apiKeyEnvVar: 'SAKANA_AI_API_KEY',\n supports: {\n chat: true,\n streaming: true,\n tools: true,\n jsonMode: true,\n vision: true,\n reasoning: true,\n },\n pricingMode: 'none',\n modelDenylist: ['fugu-cyber', 'fugu-cyber-v1.0'],\n enabledByDefault: true,\n priority: 35,\n docsUrl: 'https://console.sakana.ai/get-started',\n notes:\n 'Fugu chat/tools/reasoning/vision models. Live-verified 2026-07-29: /v1/models works (5 real models). Billing activated same day; end-to-end re-verification confirmed chat, streaming, tools, JSON mode, and vision all genuinely work — real HTTP 200s with correct content. Vision is evidence-backed despite console.sakana.ai/models marking it unsupported. fugu-cyber excluded via modelDenylist (approval-gated). pricingMode=none: no pricing in API responses; see header comment for detail.',\n lastReviewedAt: '2026-07-29',\n },\n] as const satisfies readonly ProviderCatalogEntry[];\n"}}, "api/src/providers/catalog/consolidation-matrix.ts": {"cb3d697c21c8852d6f51ec5cd9aa1e4a02d90328": {"t": 1785674631.2881508, "n": 108185, "h": "a936418c93ce3313", "c": "// Copyright (C) 2026 Ailin One, Inc.\n//\n// This file is part of Collective Intelligence Engine (ci).\n// Licensed under the GNU Affero General Public License v3.0 or later.\n// See LICENSE in the repository root, or <https://www.gnu.org/licenses/>.\n//\n// SPDX-License-Identifier: AGPL-3.0-or-later\n// Source: https://github.com/ailinone/collective-intelligence\n\n/**\n * Canonical provider consolidation matrix (SOTA — saneamento final).\n *\n * ## Purpose\n *\n * This module is the **single authoritative classification** of the canonical\n * 103-provider set (81 catalog + 22 switch, post LOTE M 2026-04-23) into\n * mutually-exclusive operational buckets. It exists to make the\n * SOTA-CONSOLIDAÇÃO-FINAL decision **machine-readable**: narrative reports in\n * markdown can drift; code with a passing test cannot.\n *\n * Canonical-count history (each transition has a supersession entry in\n * `NON_CANONICAL_HISTORICAL_CLAIMS` below):\n * - 87 = pre Lot B (catalog=65 + switch=22)\n * - 90 = post Lot B, pre LOTE M (catalog=68 + switch=22)\n * - 103 = post LOTE M 2026-04-23 (catalog=81 + switch=22)\n *\n * ## Decision record (frozen 2026-04-23)\n *\n * **Live-validation rule = Option A (accept this-session probes).**\n *\n * The 41 HTTP probes emitted during the consolidation session of 2026-04-23\n * were executed against the then-current HEAD working-tree catalog using\n * credentials freshly loaded from GCP Secret Manager. They produced real\n * HTTP responses (2xx, or 4xx with auth accepted + tier-gated). The\n * catalog has not changed since probe time, so the probes remain valid\n * evidence of live integration for every canonical provider they touched.\n *\n * Any prior claim that \"0 probes were executed this session\" is marked\n * explicitly below as `nonCanonicalHistoricalClaims` — historical text\n * from earlier pass(es), superseded by this decision.\n *\n * ## Bucket semantics (10 buckets, mutually exclusive)\n *\n * live-validation — probe emitted this session, response indicates\n * working integration (2xx, OR 4xx that proves auth\n * accepted such as tier-gate / insufficient-balance\n * on a reachable endpoint)\n * no-live-validation — adapter + catalog/switch present, no probe this\n * session (credential available but probe skipped,\n * or scope-limited credential)\n * partial — integration incomplete at HEAD (e.g. adapter\n * exists, catalog missing, or vice versa)\n * credentials-missing — canonical provider, no key available this session\n * vendor-side-failure — probe returned 5xx/403 from vendor-side config\n * (NOT an integration bug)\n * upstream-suspended — auth accepted but vendor reports credit/quota\n * exhausted\n * defunct-unreachable — credential invalid this session (stale/rotated)\n * catalog-only-inventory — catalog row with integrationMode: 'catalog-only'\n * (intentionally non-executing metadata inventory)\n * switch-only-legitimate — provider-registry.ts switch case that is NOT\n * OAI-compatible and cannot be migrated to catalog\n * without an adapterClass framework extension\n * not-eligible — secret and/or adapter exists in-tree but is NOT\n * in the canonical 103 (orphan); tracked separately\n * so operational counters never include them\n *\n * ## Invariants (enforced by consolidation-matrix.test.ts)\n *\n * I1 — every canonical providerId (catalog ∪ switch) appears in exactly\n * ONE bucket\n * I2 — no providerId appears in more than one bucket\n * I3 — buckets are exactly the 10 above\n * I4 — nonCanonicalProviders (orphans) are disjoint from canonical\n * I5 — nonCanonicalHistoricalClaims cannot contaminate the matrix\n * (they live in a separate, audit-only field)\n *\n * When a provider's operational state changes, update ONE bucket and the\n * test will catch any accidental double-classification.\n */\n\n/**\n * The ten mutually-exclusive classification buckets.\n * Order is NOT significant — the bucket is identity; no hierarchy implied.\n */\nexport const CONSOLIDATION_BUCKETS = [\n 'live-validation',\n 'no-live-validation',\n 'partial',\n 'credentials-missing',\n 'vendor-side-failure',\n 'upstream-suspended',\n 'defunct-unreachable',\n 'catalog-only-inventory',\n 'switch-only-legitimate',\n 'not-eligible',\n] as const;\n\nexport type ConsolidationBucket = (typeof CONSOLIDATION_BUCKETS)[number];\n\n/**\n * Per-bucket assignment of every canonical providerId.\n *\n * Each providerId MUST appear in exactly one bucket's array.\n * Adding a new canonical provider requires adding it to exactly one bucket.\n */\nexport const CONSOLIDATION_MATRIX: Record<ConsolidationBucket, readonly string[]> = {\n // ── 38 canonical providers with real HTTP evidence ───────────────────\n // Pre-update: 35 (24 catalog + 11 switch). Post-update 2026-04-23 final\n // pass: 38 (26 catalog + 12 switch). Deltas applied in THIS final pass:\n //\n // + moonshot → was 'defunct-unreachable' from an earlier late-pass.\n // Root cause of the prior 401 was probe-target drift:\n // the probe hit `api.moonshot.cn` (the PRC endpoint),\n // but the canonical MoonshotAdapter uses\n // `api.moonshot.ai/v1` (confirmed: DEFAULT_BASE_URL\n // in moonshot-adapter.ts). This final pass probed\n // `api.moonshot.ai/v1/models` with the same GCP\n // secret `ailin-moonshot-key` → HTTP 200, 14 models.\n // The integration was never broken; the previous\n // verdict was a probe-target bug. Classification\n // restored.\n //\n // + aws-bedrock → was 'credentials-missing'. GCP has\n // `ailin-aws-bearer-token` (AWS added Bearer-token\n // auth for read endpoints in 2025, alongside SigV4).\n // Probe `GET bedrock.us-east-1.amazonaws.com/foundation-models`\n // with Bearer → HTTP 200, 163KB body. This proves\n // credentials + reachability; SigV4 compatibility for\n // invocation endpoints is a separate surface and is\n // covered by the switch-case adapter which uses the\n // @aws-sdk/signature-v4 path at execution time.\n //\n // + gemini-openai → was 'vendor-side-failure' (google-key 403\n // CONSUMER_SUSPENDED). Root cause was the\n // GEMINI_API_KEY fallback chain picking `google-key`\n // (suspended) before any alternative. The fallback\n // chain in load-secrets-into-env.ts was extended\n // this pass to include `vertex-key` at the end —\n // a generic Google Cloud API key that is NOT\n // suspended and IS scoped for the generativelanguage\n // API. Probe with vertex-key on\n // generativelanguage.googleapis.com/v1beta/models\n // → HTTP 200, 50 Gemini models. gemini-openai is\n // now reachable; vertex-ai (which uses SA OAuth\n // against aiplatform.googleapis.com, not an API\n // key) remains independently in no-live-validation.\n //\n // Pre-previous late-pass carry-forward (kept for traceability):\n // + edenai, openrouter, cartesia, deepgram, elevenlabs (all +200s)\n //\n // Deltas from prior lates (also carried forward):\n // Lot B: writer, upstage, rekaai promoted from orphan→canonical→live.\n 'live-validation': [\n // Catalog (24 — +moonshot restored, +gemini-openai promoted this final pass;\n // bytez demoted to partial; heliconeai demoted to credentials-missing)\n 'aihubmix',\n 'aiml',\n // bytez REMOVED this pass — demoted to `partial`. The adapter extends\n // OpenAICompatibleHubAdapter (treats bytez as OAI-shape at /v1/models),\n // but the real Bytez API is at /models/v2/<hfmodel> with a native\n // request shape. The key itself works (HTTP 403 \"upgrade plan\" with\n // a legitimate tier-gate message proves auth), but the adapter wire\n // shape does not match. Reclassified as partial until the adapter is\n // rewritten against the native surface.\n 'cerebras',\n 'cometapi',\n 'edenai', // re-probed with alternate secret — 330 models\n 'featherless-ai',\n 'friendli',\n 'gemini-openai', // 2026-04-23 final pass — vertex-key added to GEMINI_API_KEY fallback chain → generativelanguage.googleapis.com 200, 50 Gemini models (prior state: 403 suspended google-key)\n // heliconeai REMOVED this pass — demoted to credentials-missing.\n // Both ailin-heliconeai-key AND ailin-heliconeai-api-key (the two known\n // GCP secret names for Helicone's AI gateway) are the literal 12-byte\n // string \"PLACEHOLDER\". The /models 200 from earlier probes was on a\n // public no-auth endpoint (Helicone's models registry is browsable);\n // /chat with \"PLACEHOLDER\" Bearer returned 401 as expected. This is a\n // placeholder-credential case, not a live integration.\n 'imagerouter',\n 'inworld',\n 'moonshot', // restored 2026-04-23 final pass — /v1/models on api.moonshot.ai → 200, 14 models (prior 401 was a probe-target bug against api.moonshot.cn)\n 'nanogpt',\n 'novita',\n 'nvidia',\n 'orqai',\n 'poe',\n 'rekaai',\n 'replicate',\n 'requesty',\n 'routeway',\n 'upstage',\n 'voyage',\n 'wandb',\n 'writer',\n // Switch (13 — +aws-bedrock and +vertex-ai both promoted this phase-final\n // pass; vertex-ai on generativelanguage.googleapis.com/v1beta/models)\n 'openai',\n 'anthropic',\n 'deepseek',\n 'mistral',\n 'xai',\n 'cohere',\n 'jina',\n 'openrouter',\n 'cartesia', // specialty audio, switch-only — /voices → 200, 751 voices\n 'deepgram', // specialty audio, switch-only — /v1/models → 200 (auth: Token not Bearer)\n 'elevenlabs', // specialty audio, switch-only — /v1/voices → 200 (not /v1/models)\n 'aws-bedrock', // 2026-04-23 final pass — Bearer-token auth on /foundation-models → 200, 163KB (AWS added Bearer support for read endpoints in 2025)\n 'vertex-ai', // 2026-04-23 phase-final — ailin-vertex-key (distinct from suspended google-api-key) → generativelanguage.googleapis.com/v1beta/models 200, 50 Gemini models. The prior \"ADC scope-limited\" diagnosis was wrong — the GCP secret itself is a usable API key.\n // ── Sublote D1 (2026-04-24) promotions — 10 providers, real chat proofs ──\n // Operator provisioned 17 secrets in GCP; each promoted provider below\n // was verified with GET /v1/models (200) AND POST /v1/chat/completions\n // (200 body with real completion) against the canonical adapter baseUrl.\n // Evidence in /tmp/subd1/bodies/<provider>.{models,chat,rechat}.body.\n 'groq', // llama-3.1-8b-instant chat 200 (592B)\n 'alibaba', // 2026-06: catalog entry added (oai-compat-pure, dashscope) closing 154 orphan DB rows — see providers.catalog.ts; was missing from the matrix (I1 invariant)\n 'deepinfra', // Meta-Llama-3.1-8B-Instruct chat 200 (455B); 65KB models catalog\n 'huggingface', // Llama-3.1-8B-Instruct chat 200 (668B) via router.huggingface.co\n 'cloudflare-workers-ai', // @cf/meta/llama-3-8b-instruct chat 200 (360B); /models 405 is expected (CF uses /ai/models/search, not /ai/v1/models)\n 'github-models', // openai/gpt-4o-mini chat 200 (1252B); models at /catalog/models not /v1/models\n 'perplexity', // sonar chat 200 (2992B); /models returns 404 by design — discovery is not exposed\n 'fireworks-ai', // accounts/fireworks/models/glm-5p1 chat 200 (447B) — first-pick llama-v3p1-8b was 404 (model-id deprecated), re-probed with real id from /models\n 'sambanova', // Meta-Llama-3.3-70B-Instruct chat 200 (910B) — first-pick 3.1 was 410 Gone (deprecated), re-probed with 3.3\n 'infermatic', // Qwen-Qwen3-30B-A3B chat 200 (476B) — LiteLLM Virtual Key with sk- prefix confirmed; key has model-scoped ACL (pinned in catalog note)\n 'heliconeai', // gpt-4o-mini chat 200 (1003B) — new ailin-heliconeai-api-key (sk-hel… 43B) replaces legacy ailin-heliconeai-key (literal PLACEHOLDER). Gateway routes to OpenAI with real completion.\n // LOTE O (2026-07-11) — apertis, inception. Promoted same-day from\n // `no-live-validation` (catalog landed 2026-07-10 with no probe; gcloud\n // ADC needed interactive reauth). Operator completed `gcloud auth login\n // --no-launch-browser` device-code flow; both keys re-fetched from GCP\n // and probed live:\n // apertis — GET /v1/models 200 (400+ models); POST /v1/chat/completions\n // with gpt-4o-mini 200 (\"Ping!\", 3 completion tokens). First probe\n // against gemini-2.0-flash-lite-001 returned 503 upstream_error —\n // a genuine transient upstream-vendor flake (auth was accepted,\n // not an integration bug); gpt-4o-mini retry on the same key/\n // endpoint confirmed the integration itself is sound.\n // inception — GET /v1/models 200 (mercury-2, mercury-edit-2); POST\n // /v1/chat/completions with mercury-2 + reasoning_effort:'instant'\n // 200 (\"pong\", finish_reason:'stop'). First probe with default\n // reasoning_effort (medium) + max_tokens:10 hit finish_reason:\n // 'length' with 0 completion_tokens (all 10 tokens consumed by\n // reasoning) — not a failure, just an under-provisioned test\n // budget for a reasoning-capable model.\n 'apertis',\n 'inception',\n // LOTE P (2026-07-11) — empiriolabs. Held back in LOTE O pending\n // validation (unfamiliar model lineup in docs); operator's real\n // ailin-empiriolabs-key probed live same day as the apertis/inception\n // promotion: GET /v1/models 200 (134+ real models — Kling, DeepSeek,\n // Zhipu, Qwen, confirmed genuine, not fabricated), POST\n // /v1/chat/completions 200 with deepseek-v4-flash (\"pong\", cost_usd\n // tracked). Went straight to live-validation — no no-live-validation\n // stopover needed since the probe happened before the catalog row\n // was ever added.\n 'empiriolabs',\n // LOTE Q (2026-07-12) — concentrate. PROMOTED same-day from\n // `no-live-validation` once the operator re-authenticated gcloud a\n // second time: POST /v1/chat/completions/ with gpt-4o-mini 200\n // (\"Pong\", 3 completion tokens). Response shows `model:\n // \"azure/gpt-4o-mini\"` — live confirmation that Concentrate really is\n // routing through an upstream vendor (Azure) under the hood, not\n // serving inference itself. `cost` breakdown present in the payload.\n 'concentrate',\n // LOTE S (2026-07-13) — perplexity-agent. Went straight to\n // live-validation (no no-live-validation stopover) — full execution\n // evidence gathered in the same session the catalog row was written:\n // GET /v1/models 200 (32 models); POST /v1/agent 200 for 5 of 6\n // requested third-party vendors — anthropic/claude-haiku-4-5,\n // openai/gpt-5.4-mini, google/gemini-3.5-flash, xai/grok-4.5,\n // perplexity/glm-5.2 (z.ai) — plus nvidia/nemotron-3-super-120b-a12b\n // (6/6 confirmed). perplexity/kimi-k2.7-code (Moonshot) is accepted by\n // model-id validation but hung with 0 response bytes on every attempt\n // (45-90s timeouts) — NOT counted as confirmed; only the 6 verified\n // vendors back this classification, not all 7 originally asked about.\n 'perplexity-agent',\n // LOTE R (2026-07-13/15) — fastrouter. PROMOTED from `no-live-validation`\n // once the operator re-authenticated gcloud a third time: POST\n // /api/v1/chat/completions with openai/gpt-5.4-mini 200\n // (`choices[0].message.content:\"pong\"`, `usage.provider:\"openai\"`,\n // real `usage.cost`). Live confirmation FastRouter genuinely routes\n // through the named upstream vendor, same pattern already seen for\n // apertis/concentrate.\n 'fastrouter',\n // LOTE U+1 (2026-07-29) — sakana-ai. Catalog row landed same-day (PR\n // #234) in 'upstream-suspended': GET /v1/models was already 200 (real\n // 5-model list) but the account had no active subscription, so every\n // POST /v1/chat/completions 429'd usage_limit_reached. The operator\n // activated pay-as-you-go billing on the account later the same day\n // and re-ran a full end-to-end verification directly against the live\n // API with the same GCP secret (ailin-sakana-ai-key): chat (fugu, plain\n // text), streaming (SSE chunks terminating in [DONE], content\n // assembled correctly), tool calls (correct function + arguments\n // extracted for a weather-lookup prompt), JSON mode\n // (response_format:{type:'json_object'} returned valid JSON), and\n // vision (a multimodal request with an inline image correctly\n // identified its content, with prompt_tokens jumping sharply versus\n // text-only calls — proof the image was actually processed) all\n // returned real HTTP 200 responses with correct, sensible content.\n // Promoted from upstream-suspended to live-validation; no other field\n // changed (auth, endpoint, and integration shape were already correct\n // at PR #234 merge — the only blocker was account-level billing).\n 'sakana-ai',\n // LOTE V (2026-07-29) — arcee, chutes. Both landed in upstream-suspended\n // on 2026-04-24 (Sublote D1) on real 402 \"insufficient credits/quota\"\n // responses. The operator topped up both accounts with a small balance\n // and a full live re-verification was run directly against each API:\n // arcee — GET /api/v1/models 200 (6 live models: trinity-large-\n // thinking, zai-org/glm-5.2, deepseek-ai/deepseek-v4-pro,\n // minimaxai/minimax-m3, moonshotai/kimi-k2.7-code,\n // moonshotai/kimi-k2.6; note the originally-tested 'trinity-mini'\n // is gone from discovery — the Trinity family was reorganized to\n // trinity-large-thinking). POST /v1/chat/completions with\n // trinity-large-thinking 200 (real content + usage, the prior 402\n // is gone) and stream:true 200 (proper SSE, delta.reasoning_content\n // chunks, terminating [DONE]). Reasoning content is exposed at\n // `message.reasoning_content` (streaming: `delta.reasoning_content`)\n // — NOT `message.reasoning` as the pre-existing catalog notes\n // claimed; that path was never actually live-verified until now.\n // chutes — GET /v1/models 200 (15 confidential-compute \"-TEE\" models;\n // this key's account is TEE-only by provisioning, not a plain-vs-\n // TEE choice). POST /v1/chat/completions 200 for BOTH\n // Qwen/Qwen3-32B-TEE (the originally-tested model, still valid) and\n // unsloth/Mistral-Nemo-Instruct-2407-TEE (a second model, clean\n // \"PONG\" completion) — zero 402s, zero balance errors on any call.\n // No integration/adapter changes were needed: baseUrl, bearer auth,\n // and the generic OAI-compat bridge all already matched.\n // Promoted from upstream-suspended to live-validation for both; no\n // adapter or catalog-shape changes required — the only blocker in\n // both cases was account-level billing, now resolved.\n 'arcee',\n 'chutes',\n ],\n\n // ── 1 canonical provider with adapter+catalog/switch but no probe ────\n // Reduced from 3 → 1 in the 2026-04-23 late pass:\n // - openrouter promoted to live-validation (probe-200)\n // - gemini-openai demoted to vendor-side-failure (api_key suspended)\n // Remaining: vertex-ai uses a deployment-bound auth flow (SA token\n // + region + project + /models path). The ADC present in this session\n // is scope-limited to Secret Manager; it cannot mint Vertex tokens\n // directly. Promotion requires ADC with roles/aiplatform.user.\n 'no-live-validation': [\n // Empty post phase-final: vertex-ai was promoted to live-validation\n // (ailin-vertex-key 200 on generativelanguage.googleapis.com/v1beta/models).\n // The prior rationale \"ADC scope-limit requires roles/aiplatform.user\"\n // was a misdiagnosis — the GCP secret is a usable API key, not an SA.\n //\n // LOTE O (2026-07-10): apertis, inception landed here with no probe\n // (gcloud ADC needed interactive reauth). PROMOTED to `live-validation`\n // 2026-07-11 once the operator completed device-code reauth and a real\n // probe confirmed both — see the `live-validation` bucket entry above\n // for full evidence. Bucket empty again.\n //\n // LOTE Q (2026-07-12): concentrate landed here with discovery proven\n // (unauthenticated) but execution unprobed (gcloud ADC expired before\n // the key could be fetched). PROMOTED to `live-validation` same day\n // once the operator re-authenticated — see the `live-validation`\n // bucket entry above for full evidence. Bucket empty again.\n //\n // LOTE R (2026-07-13): fastrouter landed here with discovery proven\n // (unauthenticated) but execution unprobed (gcloud ADC expired before\n // the key could be fetched). PROMOTED to `live-validation` 2026-07-15\n // once the operator re-authenticated — see the `live-validation`\n // bucket entry above for full evidence. Bucket empty again.\n //\n // LOTE T (2026-07-13): ailin — self-referential meta-gateway (this\n // engine, as a client of api.ailin.one). No AILIN_API_KEY provisioned\n // this session, so unlike O/Q there is no promotion to report yet.\n // Wiring verified contract-only against api.ailin.one's own\n // openapi-spec.yaml (chat/embeddings/images/audio confirmed OpenAI-\n // compatible shape at the generic hub's default paths); GET /v1/models\n // confirmed to return a richer native shape the generic fetcher only\n // partly understands. See the catalog entry's own notes for the\n // discovery-shape follow-up. Promote once a real key is provisioned\n // and a live probe confirms chat/completions.\n 'ailin',\n ],\n\n // ── 6 partial ────────────────────────────────────────────────────────\n // `partial` captures the SHAPE \"at least one surface is live-proven,\n // at least one surface is gated\". Current instances split into two\n // structural sub-shapes:\n //\n // (A) Adapter wire-shape mismatch — key is valid, but the shipped\n // adapter targets the wrong surface.\n //\n // bytez — BytezAdapter extends OpenAICompatibleHubAdapter (expects\n // /v1/models OAI-shape), but the real Bytez API lives at\n // /models/v2/<hfmodel> with a native request shape. Key is valid\n // (HTTP 403 \"upgrade plan\" = tier-gate proves auth). Partial until\n // the adapter is rewritten against the native surface or until\n // Bytez exposes an OAI-compat surface.\n //\n // (B) Asymmetric auth — /models public, /chat credential-gated.\n // Proven via no-auth probe against the canonical baseUrl.\n //\n // venice (Sublote A 2026-04-23) — /api/v1/models HTTP 200 PUBLICLY\n // (auth header actively ignored: invalid Bearer → same 200 body;\n // 72 models). /api/v1/chat/completions → HTTP 402 x402 payment-\n // required body (USDC on Base eip155:8453) without Bearer, HTTP\n // 401 \"Authentication failed\" with invalid Bearer. Discovery\n // live-proven; execution credential-gated.\n //\n // atlascloud (Sublote B 2026-04-23) — api.atlascloud.ai/v1/models\n // HTTP 200 PUBLICLY (auth ignored, 107 models, custom OAI-adjacent\n // wrapper {code:200,msg:\"succeed\",data:[...]}). /chat gated.\n //\n // avian (Sublote B 2026-04-23) — api.avian.io/v1/models HTTP 200\n // PUBLICLY (auth ignored, 6 models, pure OAI shape). /chat gated.\n //\n // phala (Sublote B 2026-04-23) — api.redpill.ai/v1/models HTTP 200\n // PUBLICLY (auth ignored, 76 models including Anthropic Claude\n // Sonnet 4.5 via the Phala TEE confidential-compute routing).\n // /chat gated. Note: phala is a CONFIDENTIAL-COMPUTE router on\n // Phala Network SGX TEEs; the public /models surface exposes the\n // set of models available via the TEE routing layer.\n //\n // mancer (Sublote B 2026-04-23) — neuro.mancer.tech/oai/v1/models\n // HTTP 200 PUBLICLY (9 LLaMA-based fiction/RP models). Asymmetric\n // detail: unlike venice/atlascloud/avian/phala, mancer VALIDATES\n // the Authorization header when present (invalid Bearer → 401)\n // but serves discovery when NO header is sent. Effectively the\n // same operational signal: a client can enumerate models without\n // a credential. /chat gated.\n //\n // Promotion path out of `partial` for all (B)-class providers: a\n // 200 on /chat/completions (or the vendor's execution surface) with\n // a real provisioned credential → live-validation.\n partial: [\n 'bytez', // switch — wire-shape mismatch (OAI-compat adapter vs. vendor's /models/v2 native shape); tier-gated 403 on real path with valid key\n 'venice', // catalog — asymmetric auth: /models 200 public (auth ignored), /chat 401/402 gated. Sublote A 2026-04-23\n 'atlascloud', // catalog — /models 200 public (auth ignored, 107 models, custom {code,msg,data} wrapper). Sublote B 2026-04-23\n 'avian', // catalog — /models 200 public (auth ignored, 6 models, pure OAI shape). Sublote B 2026-04-23\n 'mancer', // catalog — /models 200 public when no header sent (9 models); VALIDATES invalid Bearer → 401 (asymmetric). Sublote B 2026-04-23\n 'phala', // catalog — /models 200 public (auth ignored, 76 models via Phala SGX TEE confidential-compute routing on redpill.ai). Sublote B 2026-04-23\n ],\n\n // ── 54 canonical providers with no credential reachable this session ─\n // Breakdown: 47 catalog (39 hosted + 8 self-hosted) + 7 switch\n // (1 hosted + 5 self-hosted-local-* + 1 generic self-hosted adapter).\n // Pre-LOTE-M: 46 (39 catalog + 7 switch). LOTE M added 13 catalog rows;\n // Sublote A (2026-04-23) promoted venice → partial and Sublote B\n // (2026-04-23) promoted atlascloud, avian, mancer, phala → partial on\n // proven /models public HTTP 200 probes. Net LOTE M credentials-missing\n // contribution: +8 (13 added − 5 promoted to partial).\n //\n // 2026-04-23 LOTE M (complement lot) initial deltas — 13 providers\n // added; 8 remain in credentials-missing, 5 in partial post-Sublote-A+B:\n //\n // + arcee, atlascloud, avian, gmi, infermatic, inflection, mancer,\n // phala, relace, siliconflow, stepfun\n // → secret-absent. Catalog rows added this lot; no GCP secret\n // under any known alias. Operator must provision\n // <PROVIDER>_API_KEY to unlock live-validation.\n //\n // + qianfan (Baidu ERNIE promotion)\n // → auth-incomplete. GCP holds ailin-baidu-{key,secret,base-url}\n // (legacy v1 AK+SK OAuth material) but those secrets are the\n // 12-byte string \"PLACEHOLDER\" AND the canonical v2 runtime\n // path uses a DIFFERENT key format (bce-v3/... bearer). A\n // real QIANFAN_API_KEY must be provisioned alongside real\n // baidu-{key,secret}. Analogous to aws-sagemaker's\n // classification reasoning.\n //\n // + venice — landed in credentials-missing at LOTE M close, then\n // MOVED to `partial` on 2026-04-23 Sublote A after /api/v1/models\n // was proven HTTP 200 public (no auth required, 72 models).\n // Only 1 of 2 canonical surfaces is gated; discovery is live.\n //\n // + atlascloud, avian, mancer, phala — all landed in credentials-\n // missing at LOTE M close, then MOVED to `partial` on 2026-04-23\n // Sublote B after /models/v1/models was proven HTTP 200 public\n // (107 + 6 + 9 + 76 models respectively). Each provider has one\n // canonical surface live-proven (discovery) and one gated\n // (execution). See the `partial` bucket comment and the Sublote B\n // superseded-claim #7 in NON_CANONICAL_HISTORICAL_CLAIMS for the\n // probe evidence (16 probes × 12 providers = 192 probe records).\n //\n // Previous (pre-LOTE-M) total 46 → now 54 (+8 net after Sublote A+B).\n //\n // 2026-04-23 SUBLOTE A probe-evidence (operational verification of\n // the 4 Grupo-A providers — qianfan, venice, siliconflow, stepfun.\n // ONE bucket move occurred: venice → partial on proven live discovery.\n // The other three confirmed endpoint/base-URL correctness against\n // live HEAD but remain gated by missing credentials):\n //\n // · qianfan — v2 endpoint https://qianfan.baidubce.com/v2 ALIVE;\n // 401 invalid_iam_token on invalid bearer (BCE-shape error:\n // {error:{code,message,type},id:'as-...'}). Base URL + authScheme\n // confirmed. GCP legacy v1 secrets ailin-baidu-{key,secret,\n // base-url} re-read this session: all three still literal\n // \"PLACEHOLDER\". No ailin-qianfan-* or ailin-baidu-qianfan-*\n // secret exists. Stays credentials-missing / auth-incomplete.\n // · venice — /api/v1/models returns HTTP 200 PUBLICLY (no auth\n // header required; with an INVALID Bearer it also returns 200,\n // confirming auth is ignored on /models) with 72 text models\n // in OAI-conformant shape ({data:[{id,object,owned_by,...}]});\n // /api/v1/chat/completions returns HTTP 402 with x402 crypto-\n // payment protocol body (USDC on Base eip155:8453,\n // 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913) when no Bearer\n // header is sent, HTTP 401 {\"error\":\"Authentication failed\"}\n // when an invalid Bearer is sent. Asymmetric auth: one canonical\n // surface is live-proven, one is gated. PROMOTED credentials-\n // missing → partial (see `partial` bucket above for rationale).\n // No ailin-venice-* secret under any alias in GCP.\n // · siliconflow — api.siliconflow.cn/v1 ALIVE; 401 with body as\n // a bare JSON string \"Invalid token\" (justifies integrationClass\n // 'oai-compat-quirks' — non-object error body). PRC-only endpoint\n // confirmed: no .com equivalent probed successfully. No ailin-\n // siliconflow-* secret. Stays credentials-missing / secret-absent.\n // · stepfun — BOTH api.stepfun.com (canonical) and api.stepfun.ai\n // (CDN alt; Alibaba Cloud cookies + x-router-id header observed)\n // return identical OAI-compat error shape {\"error\":{\"message\":\n // \"Incorrect API key provided\",\"type\":\"invalid_api_key\"}} on\n // invalid bearer. .com preserved as baseUrl (.ai remains an\n // acceptable alt via notes). No ailin-stepfun-* secret. Stays\n // credentials-missing / secret-absent.\n //\n // Net matrix delta from Sublote A: ONE bucket move (venice →\n // partial). 3/4 remain credentials-missing; 1/4 promoted on proven\n // public discovery. Sublote A's primary value is OPERATIONAL proof-\n // of-reachability — full live-validation of the execution surfaces\n // requires secret provisioning (deferred to next sublote).\n //\n // Sub-classification (applied to the credentials-missing bucket only):\n // secret-absent — no GCP secret of any known name or alias\n // placeholder — secret exists but value == \"PLACEHOLDER\"\n // endpoint-absent — key exists, no public base URL defined\n // self-hosted-unreachable — expected to run locally; no reachable\n // endpoint from the probe environment\n // auth-incomplete — multi-piece auth where one component is absent\n // operator-action-required — vendor requires out-of-band onboarding\n // (AWS IAM role, GCP SA, enterprise SSO)\n // The sub-classification is documented in\n // CREDENTIALS_MISSING_SUBCLASS below (not a matrix column — keeps the\n // matrix structurally simple; the sub-class is a refinement layer).\n 'credentials-missing': [\n // ── Post Sublote D1 (2026-04-24) ────────────────────────────────────\n // Operator provisioned 17 GCP secrets on 2026-04-24. Of the 14 providers\n // that left this bucket:\n // 10 → live-validation (real chat-200 with provisioned credential)\n // groq, deepinfra, huggingface, cloudflare-workers-ai,\n // github-models, perplexity, fireworks-ai, sambanova,\n // infermatic, heliconeai\n // 4 → upstream-suspended (auth accepted, credit/service gated)\n // anyscale (permanent vendor-side shutdown Aug 2024),\n // arcee/chutes/hyperbolic (402 \"insufficient funds\" with\n // structurally-valid credential — same shape as ai302/palabraai)\n // 3 STAY here but move sub-class secret-absent → auth-incomplete\n // (togetherai, siliconflow, stepfun — keys exist in vault,\n // upstream rejects them with OAI-shape \"invalid api key\";\n // probable format mismatch, operator must issue keys in the\n // vendor's canonical prefix. See CREDENTIALS_MISSING_SUBCLASS).\n // 1 EXPECTED but NOT provisioned: gmi (operator's 2026-04-24\n // announcement listed gmi, but GCP Secret Manager re-scan\n // found no ailin-gmi-* or ailin-gmicloud-* under any alias —\n // vault state disagrees with operator claim. gmi stays\n // secret-absent until the actual secret lands.)\n // Net bucket delta: 54 → 40 (−14). No new adds.\n // Catalog — hosted (20 remaining: 17 pre-LOTE-M kept + 3 LOTE M kept)\n 'bfl',\n 'databricks',\n 'lambda-ai',\n 'minimax',\n 'morph',\n 'nebius',\n 'nscale',\n 'recraft',\n 'runwayml',\n 'scaleway',\n 'synthetic',\n 'togetherai', // D1 2026-04-24 — key provisioned (ailin-togetherai-api-key, key_CY… 25B) but /v1/chat returns OpenAI-shape 401 \"Invalid API key\". key_CY prefix is non-canonical for Together AI (normally tok_/hex). Probable format mismatch; operator must re-issue. Sub-class: auth-incomplete.\n 'v0',\n 'vercel-ai-gateway',\n 'volcano',\n 'watsonx',\n 'xiaomi-mimo',\n 'zai',\n // LOTE M 2026-04-23 survivors post-D1 (3 remain of original 7)\n 'gmi', // D1 2026-04-24 — operator claimed provisioned but GCP scan 2026-04-24 found NO matching secret; vault state disagrees. Stays secret-absent until secret actually lands.\n 'inflection',\n 'relace',\n 'siliconflow', // D1 2026-04-24 — key provisioned (ailin-siliconflow-api-key, sk-hhc… 51B) but /v1/chat returns bare JSON \"Api key is invalid\" (oai-compat-quirks shape). Sub-class: auth-incomplete.\n 'stepfun', // D1 2026-04-24 — key provisioned (ailin-stepfun-api-key, 65B) but /v1/chat returns OAI-shape 401 \"Incorrect API key provided\". Sub-class: auth-incomplete.\n // 1 auth-incomplete carried from LOTE M close: qianfan has legacy v1\n // baidu-{key,secret} secrets in GCP but all are \"PLACEHOLDER\"; the v2\n // canonical runtime path also requires a bce-v3/... QIANFAN_API_KEY\n // that is not provisioned.\n 'qianfan',\n // Catalog — self-hosted (8). Localhost endpoints are unreachable from\n // the probe environment; no key is needed but reachability is absent.\n 'ollama',\n 'local-llama',\n 'local-kobold',\n 'local-embeddings',\n 'vllm',\n 'lm-studio',\n 'xinference',\n 'triton',\n // Switch — hosted (1 — AWS pair reduced to just aws-sagemaker after\n // aws-bedrock was promoted this pass)\n 'aws-sagemaker',\n // Catalog hosted: azure-openai (was in vendor-side-failure — re-probed\n // this phase-final confirms all three secrets (api-key, endpoint,\n // deployment) are the literal 12-byte string \"PLACEHOLDER\". This is a\n // secret-never-provisioned case, not a vendor-side failure.)\n 'azure-openai',\n // Switch — self-hosted local-* (5)\n 'local-ocr',\n 'local-docling',\n 'local-nllb',\n 'local-cosyvoice',\n 'local-piper',\n // Switch — generic self-hosted adapter (1)\n 'self-hosted',\n ],\n\n // ── 2 vendor-side / config failures (not integration bugs) ───────────\n // 2026-04-23 final pass deltas:\n // − gemini-openai (promoted to live-validation once GEMINI_API_KEY\n // fallback chain was extended with `vertex-key`, which is NOT\n // suspended and IS valid against generativelanguage.googleapis.com)\n //\n // Remaining:\n // · google — 403 CONSUMER_SUSPENDED on the google-key specifically.\n // The switch case adapter reads GOOGLE_API_KEY (different env\n // var than GEMINI_API_KEY), and the loader does NOT currently\n // fall back to vertex-key for GOOGLE_API_KEY. Fixing this is a\n // separate deliberate decision: `google` is the native\n // GoogleGenerativeAI adapter, and routing traffic through\n // gemini-openai (OAI-compat) may be preferred anyway. Kept as\n // vendor-side-failure pending that decision.\n // · azure-openai — endpoint secret literal \"PLACEHOLDER\" (never\n // provisioned). Unchanged this pass.\n 'vendor-side-failure': [\n 'google', // switch — 403 CONSUMER_SUSPENDED on GOOGLE_API_KEY (google-key); separate from gemini-openai which now uses vertex-key fallback\n // azure-openai moved to credentials-missing (placeholder secrets)\n ],\n\n // ── 3 upstream suspended (auth valid, credit exhausted / service gated) ─\n // jina classified as 'live-validation' because /v1/models returned\n // 200; the /v1/embeddings 403 \"Insufficient balance\" is a per-surface\n // state, not a whole-provider classification.\n //\n // 2026-07-29: −sakana-ai, promoted to live-validation. It landed here\n // on 2026-07-29 (LOTE U, same day as catalog onboarding in PR #234)\n // because the account had no active subscription and every\n // chat/completions call 429'd usage_limit_reached. Later the same day\n // the operator activated pay-as-you-go billing and re-verified\n // end-to-end directly against the live API: chat, streaming, tool\n // calls, JSON mode, and vision all returned real HTTP 200 responses\n // with correct content. See the 'live-validation' bucket entry below\n // for the full writeup.\n //\n // 2026-07-29: −arcee, −chutes, promoted to live-validation (LOTE V).\n // Both landed here 2026-04-24 (Sublote D1) on real 402 zero-balance\n // responses. The operator topped up both accounts with a small\n // balance and a fresh live re-verification confirmed genuine, error-\n // free chat completions (and streaming, for arcee) on both. See the\n // 'live-validation' bucket entry below for the full per-provider\n // writeup.\n //\n // 2026-04-23 final pass: +palabraai. The ClientId/ClientSecret header\n // pair (GCP secrets ailin-palabraai-id + ailin-palabraai-key) was\n // accepted by api.palabra.ai/session-storage/session; the server\n // rejected the action with an explicit \"Insufficient balance\"\n // (error_code 100050, documented in their public error code table).\n // Authentication is complete, balance is zero → upstream-suspended,\n // NOT credentials-missing.\n //\n // 2026-04-24 Sublote D1: +4 (anyscale, arcee, chutes, hyperbolic).\n // Three distinct sub-shapes land in this bucket this pass:\n //\n // (a) Permanent vendor-side shutdown — anyscale: the Anyscale\n // Endpoints multi-tenant LLM API was discontinued 2024-08-01\n // (\"available exclusively through the fully Hosted Anyscale\n // Platform\"). Probe of api.endpoints.anyscale.com returns the\n // HTML shutdown notice. Auth cannot be tested because the\n // endpoint itself is gone. Classified upstream-suspended\n // because the vendor has permanently ceased serving public\n // credentials; defunct-unreachable is reserved for credential-\n // state rotations, not service-level shutdowns.\n //\n // (b) Credit-exhausted 402 — arcee/chutes/hyperbolic: each provider\n // accepted the provisioned API key (parsed Authorization,\n // routed to the correct account) but returned an explicit\n // zero-balance error:\n // arcee — {\"detail\":\"Insufficient credits. Required: 0.000037, Available: 0.000000\"}\n // chutes — {\"detail\":{\"message\":\"Quota exceeded and account balance is $0.0, please pay with fiat or send tao to...\"}}\n // hyperbolic — {\"detail\":\"Insufficient funds, please see https://docs.hyperbolic.xyz/docs/hyperbolic-pricing\"}\n // Identical operational shape to ai302 and palabraai:\n // authentication complete, balance zero, execution blocked.\n // Promotion path: operator tops up credit → re-probe → live.\n 'upstream-suspended': [\n 'ai302', // 401 on /v1/models — prior session recorded \"Insufficient account balance\"; endpoint alive, tier-gated\n 'palabraai', // switch — /session-storage/session POST returned 403 \"Insufficient balance\" (code 100050). Auth accepted.\n 'anyscale', // D1 2026-04-24 — endpoint returns HTML shutdown notice \"Effective August 1, 2024 Anyscale Endpoints API is available exclusively through the fully Hosted Anyscale Platform. Multi-tenant access to LLM models has been removed.\" ailin-anyscale-api-key provisioned (236B, aph0_C…) but cannot be exercised.\n 'hyperbolic', // D1 2026-04-24 — /v1/chat returned 402 {\"detail\":\"Insufficient funds, please see https://docs.hyperbolic.xyz/docs/hyperbolic-pricing\"}. Auth accepted.\n ],\n\n // ── 0 credentials in cache invalidated ───────────────────────────────\n // 2026-04-23 final pass: moonshot restored to live-validation. The\n // previous \"defunct\" verdict came from probing api.moonshot.cn, but\n // the canonical adapter uses api.moonshot.ai/v1 (verified in\n // moonshot-adapter.ts::DEFAULT_BASE_URL). Re-probe against .ai → 200,\n // 14 models. Bucket now empty.\n 'defunct-unreachable': [],\n\n // ── 3 catalog-only inventory (intentional non-execution) ─────────────\n 'catalog-only-inventory': ['sap', 'snowflake', 'topaz'],\n\n // ── 0 additional switch-only-legitimate ──────────────────────────────\n // NOTE: first-party natives (openai, anthropic, …) and specialty\n // audio (deepgram, cartesia, elevenlabs, palabraai) are switch-only\n // BY LEGITIMATE design, but their current operational state is what\n // determines their bucket. An openai that answered 200 this session\n // is 'live-validation', not 'switch-only-legitimate'. This bucket\n // exists for switch cases that have no other operational evidence\n // AND are legitimate switch-only (not migration candidates). Today\n // all such cases are covered by the buckets above.\n 'switch-only-legitimate': [],\n\n // ── 0 canonical providers classified as 'not-eligible' ───────────────\n // By definition canonical = catalog ∪ switch; all 103 members of\n // that union are eligible. The 'not-eligible' field below lists\n // orphans that are NOT in the canonical 103.\n 'not-eligible': [],\n};\n\n/**\n * Sub-classification of the `credentials-missing` bucket — refines the\n * operator's next action for each provider. This is NOT a matrix column\n * (the matrix stays structurally simple with 10 buckets); it is a\n * refinement layer that the CLI tooling, admin dashboard, and backlog\n * reports consume to decide *what kind* of work is needed to unblock.\n *\n * ## Sub-class semantics\n *\n * - `secret-absent` — no GCP secret under any known name/alias\n * - `placeholder` — secret exists but value === \"PLACEHOLDER\"\n * - `endpoint-absent` — key exists but no public base URL\n * - `self-hosted-unreachable` — expected local runtime; no endpoint reachable\n * - `auth-incomplete` — multi-piece auth where one component is absent\n * - `operator-action-required` — vendor requires out-of-band onboarding\n * (AWS IAM role, GCP SA, enterprise SSO)\n *\n * ## Invariant\n *\n * Every provider in CONSOLIDATION_MATRIX['credentials-missing'] MUST\n * appear in exactly one sub-class here. The total of all sub-classes\n * equals the credentials-missing bucket size.\n */\nexport const CREDENTIALS_MISSING_SUBCLASS: Record<string, readonly string[]> = {\n // Catalog hosted — no GCP secret under any known alias\n // pre-LOTE-M count = 29\n // LOTE M 2026-04-23 initial additions = 12 (arcee, atlascloud, avian,\n // gmi, infermatic, inflection, mancer, phala, relace, siliconflow,\n // stepfun, venice)\n // Sublote A 2026-04-23 refinement = −1 (venice → partial).\n // Sublote B 2026-04-23 refinement = −4 (atlascloud, avian, mancer,\n // phala all promoted to `partial` after HTTP 200 public /models\n // probe against canonical baseUrl).\n // Sublote D1 2026-04-24 refinement = −16:\n // −13 out of credentials-missing entirely (9 secret-absent →\n // live-validation, 4 secret-absent → upstream-suspended).\n // −3 same-bucket move (secret-absent → auth-incomplete for\n // togetherai/siliconflow/stepfun — keys provisioned but\n // upstream rejects the format).\n // Post-D1 secret-absent total = 36 − 16 = 20\n // (Of those 20: 17 pre-LOTE-M survivors + 3 LOTE-M survivors.\n // gmi is notable — operator announced provisioning on 2026-04-24\n // but GCP re-scan found no matching secret, so gmi stays here\n // until the actual secret lands.)\n 'secret-absent': [\n // Pre-LOTE-M survivors (17 of original 29; 12 promoted by D1)\n 'bfl',\n 'databricks',\n 'lambda-ai',\n 'minimax',\n 'morph',\n 'nebius',\n 'nscale',\n 'recraft',\n 'runwayml',\n 'scaleway',\n 'synthetic',\n 'v0',\n 'vercel-ai-gateway',\n 'volcano',\n 'watsonx',\n 'xiaomi-mimo',\n 'zai',\n // LOTE M 2026-04-23 survivors (3 of original 7 — infermatic promoted\n // to live by D1; siliconflow/stepfun moved to auth-incomplete by D1)\n 'gmi', // operator announced 2026-04-24 provisioning but GCP scan found no ailin-gmi-* or ailin-gmicloud-* under any alias — vault state disagrees with announcement.\n 'inflection',\n 'relace',\n ],\n // Secret exists but value === literal \"PLACEHOLDER\" (1)\n // pre-D1 count = 2 (heliconeai + azure-openai)\n // D1 2026-04-24 refinement = −1 (heliconeai → live-validation:\n // operator provisioned ailin-heliconeai-api-key (sk-hel… 43B, real\n // Helicone Proxy Virtual Key) which SUPERSEDES the legacy\n // ailin-heliconeai-key (11B literal \"PLACEHOLDER\" that caused the\n // prior mis-classification). Gateway routes to OpenAI with a real\n // HTTP 200 completion body.)\n placeholder: [\n 'azure-openai', // ailin-azure-openai-api-key + -endpoint + -deployment all \"PLACEHOLDER\"\n ],\n // Local runtime expected; no reachable endpoint from probe environment (13)\n 'self-hosted-unreachable': [\n 'ollama',\n 'local-llama',\n 'local-kobold',\n 'local-embeddings',\n 'vllm',\n 'lm-studio',\n 'xinference',\n 'triton',\n 'local-ocr',\n 'local-docling',\n 'local-nllb',\n 'local-cosyvoice',\n 'local-piper',\n 'self-hosted', // generic self-hosted adapter\n ],\n // Multi-piece auth where one component is absent (2 post-LOTE-M)\n //\n // aws-sagemaker: requires classic SigV4 secret access key (40-char base64),\n // but GCP only holds ABSK-prefixed Bedrock API bearer tokens. The\n // ailin-aws-key-id (AKIA... classic access key id) has no matching\n // ailin-aws-secret-access-key pair in the vault.\n //\n // qianfan (LOTE M 2026-04-23): Baidu Qianfan exposes two auth paths:\n // v1 legacy — AK+SK → OAuth access_token → query-param (backed by\n // GCP secrets ailin-baidu-{key,secret,base-url}, BUT all three are\n // literal \"PLACEHOLDER\" strings at HEAD so they cannot drive\n // the v1 OAuth flow either).\n // v2 canonical — bce-v3/... bearer key against qianfan.baidubce.com/v2\n // (no ailin-qianfan-* secret exists in GCP; a new QIANFAN_API_KEY\n // must be provisioned in the bce-v3 format — this is distinct\n // from the legacy baidu-key even if both are eventually populated).\n // Both paths are currently un-executable: v1 has placeholder secrets,\n // v2 has no secret at all. Promotion to live-validation requires\n // EITHER (a) real baidu-key + baidu-secret for the legacy adapter\n // that this repo does not ship, OR (b) a new QIANFAN_API_KEY for the\n // v2 OAI-compat catalog row that this lot added.\n //\n // togetherai (Sublote D1 2026-04-24): ailin-togetherai-api-key exists\n // in GCP (25B, prefix \"key_CY…\") but POST /v1/chat/completions returns\n // HTTP 401 {\"error\":{\"message\":\"Invalid API key provided. You can find\n // your API key at https://api.together.ai/settings/api-keys.\"...}}.\n // The \"key_\" prefix is non-canonical for Together AI (whose keys are\n // typically tok_ or raw 64-char hex). Probable format mismatch or a\n // stale/revoked token; operator must re-issue from the Together\n // dashboard. Auth-incomplete because the VAULT component exists but\n // the credential's current state prevents upstream validation.\n //\n // siliconflow (Sublote D1 2026-04-24): ailin-siliconflow-api-key exists\n // (51B, prefix \"sk-hhc…\") but POST /v1/chat/completions returns HTTP\n // 401 with body as a bare JSON string \"Api key is invalid\" (15 bytes,\n // oai-compat-quirks shape). The sk- prefix looks OpenAI-style rather\n // than SiliconFlow's standard format; probable format mismatch or\n // account-scope issue. Operator must re-issue.\n //\n // stepfun (Sublote D1 2026-04-24): ailin-stepfun-api-key exists (65B)\n // but POST /v1/chat/completions returns HTTP 401 with OAI-shape body\n // {\"error\":{\"message\":\"Incorrect API key provided\",\"type\":\"invalid_api_key\"}}.\n // Endpoint confirmed alive (verbatim OpenAI error shape). Operator\n // must verify the key is correctly associated with the StepFun\n // account and has the required access tier.\n 'auth-incomplete': [\n 'aws-sagemaker',\n 'qianfan',\n 'togetherai', // D1 2026-04-24 — key exists, upstream rejects (\"Invalid API key\")\n 'siliconflow', // D1 2026-04-24 — key exists, upstream rejects (\"Api key is invalid\")\n 'stepfun', // D1 2026-04-24 — key exists, upstream rejects (\"Incorrect API key provided\")\n ],\n} as const;\n\n/**\n * Discovery-compliance taxonomy — orthogonal to the operational matrix above.\n *\n * ## Why this exists (2026-04-27 — SOTA dynamic-discovery escalation)\n *\n * The operational `CONSOLIDATION_MATRIX` answers \"is this provider reachable\n * right now?\" — a credentialing/runtime question. It does NOT answer the\n * separate question: \"does this provider's listing in /v1/models reflect a\n * machine-readable, official source-of-truth, or is it shaped by hand-curated\n * catalog rows we baked into the binary?\"\n *\n * That second question is the SOTA dynamic-discovery contract. As of\n * 2026-04-27 the operator escalated the policy: ANY provider whose runtime\n * inventory in `/v1/models` is sourced from `staticModels`, the deprecated\n * static model-catalog stub (config/model-catalog.ts), frontier-candidate\n * seed arrays, hardcoded ID arrays in heuristics, or any in-repo seed list\n * is **non-compliant** with the dynamic-discovery contract — regardless of\n * whether the provider itself happens to also expose a real /models endpoint\n * that we just have not wired up yet.\n * (The symbol names are deliberately not written out here: the no-static-\n * model-catalog-fallback guard test scans every production source for them.)\n *\n * ## Revocation of prior completion buckets (binding 2026-04-27)\n *\n * The following INFORMAL labels — used in earlier audit reports as\n * \"acceptable completion states\" — are HEREBY REVOKED:\n *\n * - `static-catalog-required` — no longer a valid completion state\n * - `catalog-inferred` — no longer a valid completion state\n * - `execution-only` w/ inventory — `integrationMode: 'execution-only'`\n * remains a valid mode, but combining\n * it with `staticModels:` to fabricate\n * inventory is now non-compliant\n * - `no-discovery accepted` — no provider can reach a \"discovery\n * accepted\" completion state if its\n * /v1/models inventory is hardcoded\n *\n * The `catalog-only-inventory` operational bucket above (sap, snowflake,\n * topaz) DOES remain a structurally valid CLASSIFICATION — those providers\n * are intentionally non-executing — but the absence of a machine-readable\n * model surface means each of them ALSO appears in\n * `non-compliant-no-machine-readable-discovery` or\n * `not-applicable-non-model-surface` here, depending on whether the\n * provider's surface is in the LLM ontology at all.\n *\n * ## Bucket semantics (9 buckets, mutually exclusive over the canonical set)\n *\n * compliant-dynamic-discovery\n * The provider exposes a machine-readable /models endpoint (or a\n * close equivalent — HuggingFace's catalog API, GitHub Models\n * /catalog/models, Cloudflare's /ai/models/search, OpenAI /v1/models,\n * etc.) AND a registered fetcher in\n * `central-model-discovery-service.ts` materialises that into our DB\n * at runtime. The /v1/models response for these providers reflects\n * truly upstream-discovered IDs.\n *\n * compliant-deployment-discovery\n * The provider's \"models\" are deployment-scoped — Vertex AI,\n * AWS Bedrock, AWS SageMaker — and discovery happens via a\n * deployment-listing API at runtime. There is no static catalog of\n * model IDs; the runtime materialises only the deployments the\n * operator has provisioned in their cloud account.\n *\n * compliant-machine-readable-official-catalog\n * The provider does not publish a per-account /models endpoint, but\n * ships an OFFICIAL machine-readable catalog (a versioned JSON file\n * under their own domain, OpenAPI metadata in their docs, etc.) that\n * we can fetch at runtime. Currently EMPTY; reserved for future\n * promotions when a vendor publishes such a catalog without a\n * /models endpoint of its own.\n *\n * pinnedFallback-by-design\n * The provider has `pinnedFallback.reason === 'no-list-endpoint'` —\n * the vendor's API genuinely doesn't expose a model-listing surface\n * (perplexity's docs say so explicitly; v0/xiaomi-mimo probes return\n * 404/HTML respectively; image-gen specialty providers like recraft\n * and bfl have execution-only surfaces by design). The catalog row's\n * pinnedFallback IS the source of truth and is treated as such by\n * the runtime. CLASSIFIED AS COMPLIANT because there is nothing\n * \"non-compliant\" about a vendor choosing not to publish /models —\n * the operator-curated list is the inventory contract, not a\n * placeholder. Distinct from `non-compliant-hardcoded-inventory`\n * (which tracks debt: a parser we owe but haven't written) and\n * `not-applicable-non-model-surface` (which tracks providers with\n * no model ontology at all, like cartesia/elevenlabs voices).\n *\n * non-compliant-hardcoded-inventory\n * The provider has `pinnedFallback.reason === 'proprietary-schema'`\n * (or legacy `staticModels`) — the vendor DOES expose a discovery\n * surface but it's non-OAI-shaped and our adapter hasn't been\n * written yet. The pinnedFallback list is an interim crutch.\n * Promotion path: write the parser, drop the crutch, move to\n * `compliant-dynamic-discovery`. This bucket is real engineering\n * debt that should shrink over time.\n *\n * non-compliant-no-machine-readable-discovery\n * No `staticModels`, no upstream surface, no inventory. The provider\n * is canonical (operator wants it tracked) but has nothing to list\n * in /v1/models. Promotion path: vendor publishes a discovery API.\n *\n * non-compliant-runtime-not-materialized\n * The provider DOES expose a machine-readable surface (we have probe\n * evidence or vendor docs prove its existence) but our runtime is\n * currently NOT calling it — usually because the response shape needs\n * a custom transform that has not been written yet. The `staticModels`\n * array, if any, is an interim crutch. Promotion path: write the\n * transform, drop the static array, register the fetcher.\n *\n * not-applicable-non-model-surface\n * The provider does not have \"models\" in the LLM sense — its API\n * surface is voices, agents, scenes, image-enhancement filters, or\n * other artefacts that don't map to the chat/completion ontology.\n * Specialty audio (cartesia/deepgram/elevenlabs/palabraai) and\n * image-enhancement (topaz) are the canonical examples. These\n * providers are NOT non-compliant; they are operating in a different\n * category entirely.\n *\n * self-hosted-runtime-dependent\n * The provider is operator-deployed (ollama, vLLM, lm-studio, generic\n * self-hosted, every local-* adapter). Discovery is a function of\n * what the operator has loaded into THEIR runtime. This is a\n * compliance-irrelevant case, not a non-compliance — discovery for\n * these providers is genuinely runtime-bound and should not pretend\n * to be otherwise.\n *\n * ## Invariants (enforced by discovery-compliance-registry.test.ts)\n *\n * J1 — every canonical providerId (catalog ∪ switch) appears in exactly\n * ONE compliance bucket.\n * J2 — every providerId in `non-compliant-runtime-not-materialized`\n * MUST also be a provider with `staticModels` in\n * `providers.catalog.ts` (the static array is the smoking gun\n * for this bucket; otherwise it would be `compliant-dynamic-\n * discovery` or `non-compliant-no-machine-readable-discovery`).\n * J3 — every providerId with `staticModels` or `pinnedFallback.models`\n * in `providers.catalog.ts` MUST appear in one of THREE\n * static-inventory buckets:\n * • `pinnedFallback-by-design` (compliant — vendor has no\n * upstream surface, the curated list IS the contract),\n * • `non-compliant-hardcoded-inventory` (debt — vendor has a\n * non-OAI surface, our parser is queued), or\n * • `non-compliant-runtime-not-materialized` (debt — real\n * machine-readable surface exists, fetcher pending).\n * No quiet exemptions: every static array is bucketed.\n * J4 — buckets are exactly the 9 above.\n * J5 — the registry's union equals the canonical set\n * |catalog ∪ switch| = 103 at HEAD.\n *\n * ## How to USE this registry\n *\n * - The /v1/models route consumes this to populate the `inventoryClass`\n * provenance field on each model row in the response.\n * - The CI guard `discovery-compliance-registry.test.ts` enforces\n * J1–J5.\n * - When a non-compliant provider is migrated (a fetcher lands, a\n * deployment-listing call is wired, etc.), update both the catalog\n * row (drop `staticModels`) AND this registry in the same commit.\n */\nexport const DISCOVERY_COMPLIANCE_BUCKETS = [\n 'compliant-dynamic-discovery',\n 'compliant-deployment-discovery',\n 'compliant-machine-readable-official-catalog',\n // Phase 6 Fix 7 (2026-04-30): pinnedFallback-by-design split.\n // Providers with `pinnedFallback.reason === 'no-list-endpoint'` are\n // promoted out of `non-compliant-hardcoded-inventory` to reflect\n // that the operator-curated list IS the inventory contract — the\n // vendor having no /models is a vendor choice, not engineering debt.\n 'pinnedFallback-by-design',\n 'non-compliant-hardcoded-inventory',\n 'non-compliant-no-machine-readable-discovery',\n 'non-compliant-runtime-not-materialized',\n 'not-applicable-non-model-surface',\n 'self-hosted-runtime-dependent',\n] as const;\n\nexport type DiscoveryComplianceClass = (typeof DISCOVERY_COMPLIANCE_BUCKETS)[number];\n\n/**\n * Per-bucket compliance assignment of every canonical providerId.\n *\n * The classification is independent of the operational `CONSOLIDATION_MATRIX`:\n * an upstream-suspended provider that DOES expose /v1/models on its docs\n * (e.g. arcee, chutes, hyperbolic) is still `compliant-dynamic-discovery`\n * here — because the question this registry answers is \"is the inventory\n * source-of-truth dynamic?\", not \"is the provider reachable?\".\n *\n * Distribution at HEAD (2026-04-30, post Phase 6 Fix 7 split):\n * compliant-dynamic-discovery = 62\n * compliant-deployment-discovery = 3\n * compliant-machine-readable-official-catalog = 0\n * pinnedFallback-by-design = 7 (Phase 6 Fix 7: perplexity, recraft,\n * runwayml, bfl, inworld, v0, xiaomi-mimo —\n * all `reason: 'no-list-endpoint'`)\n * non-compliant-hardcoded-inventory = 2 (inflection, relace — both\n * `reason: 'proprietary-schema'`,\n * i.e. real parser-debt)\n * non-compliant-no-machine-readable-discovery = 2\n * non-compliant-runtime-not-materialized = 8\n * not-applicable-non-model-surface = 5\n * self-hosted-runtime-dependent = 14\n * TOTAL = 103 ✓\n *\n * Phase 4d (2026-04-28): bytez was promoted from\n * `non-compliant-runtime-not-materialized` to `compliant-dynamic-discovery`.\n * The dedicated `BytezNativeModelFetcher` consumes the vendor's\n * `/models/v2/list/models` endpoint (non-OAI shape) and feeds the\n * central discovery service via the `fetcherClass` catalog field. The\n * row's `integrationMode` flipped `execution-only` → `discovery+execution`\n * accordingly, and the deprecated `staticModels` block was removed.\n *\n * Registry-sync 2026-04-28 (Phase 4d closure): `v0` and `xiaomi-mimo`\n * were demoted from `compliant-dynamic-discovery` to\n * `non-compliant-hardcoded-inventory` to match the catalog's\n * `pinnedFallback` declaration. Live probes (catalog `notes`):\n * - api.v0.dev/v1/models → HTTP 404 not_found_error\n * - platform.xiaomimimo.com/v1/models → HTML homepage (no JSON listing)\n * Both rows ship `pinnedFallback.models` as the inventory of record;\n * `compliant-dynamic-discovery` was a stale assumption from initial\n * catalog drafting. Surfaced by test J3 (catalog static-inventory must\n * map to a non-compliant bucket).\n */\nexport const DISCOVERY_COMPLIANCE_REGISTRY: Record<DiscoveryComplianceClass, readonly string[]> = {\n // ── 62 — Real /models endpoint + registered fetcher ──────────────────\n // For each of these, central-model-discovery-service.ts has a\n // DiscoverySource that calls the upstream /models surface, parses the\n // response, and writes canonical Model rows to the DB. The /v1/models\n // response for these providers reflects runtime-materialised inventory.\n 'compliant-dynamic-discovery': [\n // Switch natives (8 — first-party APIs with /v1/models or equivalent)\n 'openai',\n 'anthropic',\n 'google',\n 'deepseek',\n 'mistral',\n 'xai',\n 'cohere',\n 'jina',\n // Switch routers (1)\n 'openrouter',\n // Catalog hub OAI-compat with materialiser wired into the central\n // discovery service (20 — pre-D1 set, all live-validated at HEAD)\n 'aihubmix',\n 'aiml',\n 'cerebras',\n 'cometapi',\n 'edenai',\n 'featherless-ai',\n 'friendli',\n 'gemini-openai',\n 'heliconeai',\n 'imagerouter',\n 'moonshot',\n 'nanogpt',\n 'novita',\n 'nvidia',\n 'orqai',\n 'poe',\n 'requesty',\n 'routeway',\n 'upstage',\n 'wandb',\n // 2026-06: alibaba catalog entry added (oai-compat-pure /models discovery\n // on dashscope) — registered here to keep J1 partition complete.\n 'alibaba',\n // Sublote D1 promotions (8 — credential provisioned 2026-04-24)\n 'groq',\n 'deepinfra',\n 'huggingface',\n 'cloudflare-workers-ai',\n 'github-models',\n 'fireworks-ai',\n 'sambanova',\n 'infermatic',\n // Operationally upstream-suspended but discovery-shape compliant (5)\n // (auth accepted, balance/credit gated; their /models surface is real)\n 'ai302',\n 'arcee',\n 'chutes',\n 'hyperbolic',\n 'anyscale',\n // Credentials-missing but the catalog row points at a real /models\n // surface — these will materialise when a credential lands (18)\n 'lambda-ai',\n 'minimax',\n 'morph',\n 'nebius',\n 'nscale',\n 'scaleway',\n 'synthetic',\n // v0 and xiaomi-mimo demoted 2026-04-28 to `non-compliant-hardcoded-inventory`\n // — live probes prove no /v1/models surface exists; pinnedFallback in\n // catalog is the inventory of record. See registry-sync note above.\n 'vercel-ai-gateway',\n 'volcano',\n 'watsonx',\n 'zai',\n 'gmi',\n 'rekaai',\n 'siliconflow',\n 'stepfun',\n 'togetherai',\n // Sublote A/B confirmed /models 200 public against canonical baseUrl (3)\n 'venice',\n 'mancer',\n 'phala',\n // Phase 4d 2026-04-28 — promoted from non-compliant-runtime-not-materialized\n // BytezNativeModelFetcher (api/src/services/model-fetchers/bytez-native-model-fetcher.ts)\n // wires the vendor's /models/v2/list/models non-OAI shape into the\n // central discovery service via the catalog `fetcherClass` field;\n // execution path flipped `execution-only` → `discovery+execution`.\n 'bytez',\n // LOTE O (2026-07-10) — apertis, inception. Both confirmed via doc\n // research to expose a real `GET /v1/models` (OpenAI-shaped) surface\n // consumed by the generic OpenAICompatibleHubModelFetcher; no probe\n // ran this session (see consolidation-matrix `no-live-validation`),\n // but the discovery SHAPE is compliant by design, independent of\n // whether a probe has confirmed it live yet.\n 'apertis',\n 'inception',\n // LOTE P (2026-07-11) — empiriolabs. Real GET /v1/models confirmed\n // live (134+ models, OpenAI-list shape), consumed by the generic hub\n // fetcher same as apertis/inception.\n 'empiriolabs',\n // LOTE Q (2026-07-12) — concentrate. UNAUTHENTICATED /v1/models/\n // confirmed live 2026-07-12 (`{object:\"list\",data:[{id,owned_by,...}]}`\n // — same shape the generic hub fetcher expects). The provider's own\n // richer per-provider enrichment endpoints are NOT consumed here.\n 'concentrate',\n // LOTE R (2026-07-13) — fastrouter. UNAUTHENTICATED /api/v1/models\n // confirmed live 2026-07-13 (`{data:[{id,creator,pricing,...}]}` —\n // same shape the generic hub fetcher expects, plus a real `pricing`\n // object directly on the flat list unlike apertis/empiriolabs/\n // concentrate).\n 'fastrouter',\n // LOTE S (2026-07-13) — perplexity-agent. AUTHENTICATED GET /v1/models\n // confirmed live 2026-07-13 — real OpenAI-list shape\n // (`{data:[{id,object,owned_by,created}]}`), 32 real models across 6\n // vendor families. Unlike the classic `perplexity` row (no /models\n // endpoint at all, pinnedFallback by design), the Agent API surface has\n // genuine dynamic discovery.\n 'perplexity-agent',\n // LOTE T (2026-07-13) — ailin. Compliant by shape, same reasoning as\n // apertis/inception above (no live probe this session, but the\n // discovery SHAPE is what this bucket classifies). GET /v1/models\n // returns id + a provider-attribution field the generic fetcher\n // successfully extracts (via `originalProviderField: 'originProvider'`\n // in the catalog entry) — the two things `compliant-dynamic-discovery`\n // actually requires. The response ALSO carries ailin-native enrichment\n // fields (operability, fallbackChain, resolvedProvider) the generic\n // fetcher ignores rather than mis-parses; that's a metadata-richness\n // gap, not a shape-compliance failure — J2's static-inventory\n // requirement (which `non-compliant-runtime-not-materialized` would\n // impose) does not apply here because there IS no static inventory:\n // this is a 70k-model, constantly-shifting catalog, and pinning a\n // fallback list would misrepresent it (see catalog entry notes).\n 'ailin',\n // LOTE U (2026-07-29) — sakana-ai. AUTHENTICATED GET /v1/models\n // confirmed live 2026-07-29 with a real key from GCP secret\n // `ailin-sakana-ai-key`: HTTP 200, real OpenAI-list shape\n // (`{object:\"list\",data:[{id,object,created,owned_by}]}`), 5 real\n // models — the exact shape the generic OpenAICompatibleHubModelFetcher\n // expects, no dedicated fetcher needed. Note: chat/completions itself\n // could not be live-confirmed end-to-end on this key (429\n // usage_limit_reached — no active subscription/billing on the account)\n // but that's an execution-time/billing concern, not a discovery-shape\n // one; the /v1/models surface itself is genuinely live and compliant.\n 'sakana-ai',\n ],\n // ── 3 — Deployment-bound discovery ───────────────────────────────────\n // Discovery via a deployment-listing API; runtime materialises only\n // what the operator has provisioned in their cloud account.\n 'compliant-deployment-discovery': [\n 'vertex-ai', // /publishers/{publisher}/models — deployment-scoped\n 'aws-bedrock', // /foundation-models — Bearer or SigV4\n 'aws-sagemaker', // /endpoints — operator-provisioned\n ],\n // ── 0 — Reserved for future official machine-readable catalogs ───────\n 'compliant-machine-readable-official-catalog': [\n // EMPTY at HEAD. Fill when a vendor publishes a versioned JSON\n // catalog under their own domain (no /models endpoint, no per-account\n // state) that we can fetch at runtime. None of the canonical 103\n // currently fit.\n ],\n // ── 7 — pinnedFallback is the inventory contract, by vendor design ───\n // The vendor's API genuinely doesn't expose a /models endpoint and\n // never will (catalog `pinnedFallback.reason === 'no-list-endpoint'`).\n // The operator-curated `pinnedFallback.models` list IS the source of\n // truth — there is nothing to \"fix\" upstream. Classified as compliant\n // because forcing these into a \"non-compliant-*\" bucket misleads\n // operators into chasing phantom upstream surfaces.\n //\n // Distinct from `non-compliant-hardcoded-inventory` (vendor DOES expose\n // discovery, parser unwritten — engineering debt) and from\n // `not-applicable-non-model-surface` (vendor has no model ontology at\n // all — voices, filters, etc.).\n //\n // Phase 6 Fix 7 (2026-04-30): split out from\n // `non-compliant-hardcoded-inventory` once the dossier flagged that\n // /v1/models was reporting 22 model rows under \"non-compliant\" when\n // they were operator-curated by-design pins.\n 'pinnedFallback-by-design': [\n 'perplexity', // pinnedFallback.reason: 'no-list-endpoint' — Sonar family,\n // Perplexity's docs explicitly state no /models endpoint.\n 'recraft', // pinnedFallback.reason: 'no-list-endpoint' — image-only\n // (recraftv3, recraftv2); execution-only integrationMode.\n 'runwayml', // pinnedFallback.reason: 'no-list-endpoint' — video-only\n // (gen3*); execution-only integrationMode.\n 'bfl', // pinnedFallback.reason: 'no-list-endpoint' — Black Forest Labs\n // Flux image generation; no /models surface published.\n 'inworld', // pinnedFallback.reason: 'no-list-endpoint' — discovery\n // endpoints both return 404 per catalog notes.\n 'v0', // pinnedFallback.reason: 'no-list-endpoint' — api.v0.dev/v1/models\n // returns HTTP 404 not_found_error; public docs describe\n // chats/projects/deployments shape, not a model selector.\n 'xiaomi-mimo', // pinnedFallback.reason: 'no-list-endpoint' —\n // platform.xiaomimimo.com/v1/models returns the\n // platform's HTML homepage (no JSON listing).\n 'topaz', // pinnedFallback.reason: 'no-list-endpoint' — fixed roster of\n // image-enhance pipelines (standard/high-fidelity); moved here\n // from not-applicable-non-model-surface when the runnable-gap\n // pass gave it a curated pinned inventory (2026-06-11, J3).\n ],\n // ── 2 — pinnedFallback as interim crutch, parser pending ─────────────\n // Real engineering debt — vendor DOES expose a discovery surface but\n // it's `pinnedFallback.reason === 'proprietary-schema'`: the parser\n // has not been written yet. Promotion path: write the parser, drop\n // the static array, move to `compliant-dynamic-discovery`. This\n // bucket should shrink over time.\n 'non-compliant-hardcoded-inventory': [\n 'inflection', // pinnedFallback.reason: 'proprietary-schema' — Sublote B\n // confirmed 404 on candidate OAI paths; vendor's discovery\n // surface, when available, is non-OAI shape.\n 'relace', // pinnedFallback.reason: 'proprietary-schema' — Sublote B\n // observed uniform 401 across 8 discovery paths; the catalog\n // carries pinnedFallback (relace-apply-3, relace-code-reranker,\n // relace-embedding) as the only inventory source until the\n // adapter+secret combination is wired.\n ],\n // ── 2 — No staticModels, no upstream surface, nothing to materialise ─\n // The provider is canonical (operator wants it tracked) but currently\n // has nothing to list in /v1/models. Promotion path: vendor publishes\n // a discovery API.\n 'non-compliant-no-machine-readable-discovery': [\n 'sap', // SAP AI Core — workspace-local deployment IDs, no model-listing\n 'snowflake', // Cortex — function-based API, no model-listing endpoint\n ],\n // ── 9 — staticModels but real upstream surface exists ────────────────\n // These have `staticModels: [...]` in providers.catalog.ts AND a real\n // machine-readable surface that we know about. Each is a queued\n // engineering task — wire the surface, drop the static array, move to\n // `compliant-dynamic-discovery`.\n 'non-compliant-runtime-not-materialized': [\n 'replicate', // /v1/models exists; operator opted not to import 'thousands' wholesale\n // Phase 4d 2026-04-28 — bytez promoted to `compliant-dynamic-discovery`\n // via BytezNativeModelFetcher; entry removed from this bucket.\n 'voyage', // /v1/models exists upstream, fetcher not wired\n 'azure-openai', // /openai/deployments exists, deployment-listing not wired\n 'databricks', // /api/2.0/serving-endpoints exists, fetcher not wired\n 'writer', // /models {models:[...]} non-OAI shape, parser pending\n 'atlascloud', // Sublote B: /v1/models 200 public (107 models), wrapper transform pending\n 'avian', // Sublote B: /v1/models 200 public (6 models), wiring pending\n 'qianfan', // /v2/models per BCE OAI-compat docs; secret + adapter pending\n ],\n // ── 5 — Specialty surfaces, not LLM-model-shaped ─────────────────────\n // These providers are NOT non-compliant; they operate in a different\n // category entirely. Their public surface is voices/scenes/filters,\n // not a chat/completion ontology.\n 'not-applicable-non-model-surface': [\n 'cartesia', // /voices, not /models — TTS specialty\n 'deepgram', // /v1/models lists STT engines, not LLM ontology\n 'elevenlabs', // /v1/voices, not /v1/models — TTS specialty\n 'palabraai', // session-based real-time translation, no model ontology\n // 'topaz' moved to pinnedFallback-by-design (2026-06-11): the\n // runnable-gap pass gave it a curated pinnedFallback inventory\n // (reason: 'no-list-endpoint'), which is exactly that bucket's\n // semantics (J3).\n ],\n // ── 14 — Self-hosted runtime-dependent ───────────────────────────────\n // Discovery is a function of operator runtime. NOT non-compliant —\n // discovery for these providers is genuinely runtime-bound by design.\n 'self-hosted-runtime-dependent': [\n // Catalog self-hosted (8)\n 'ollama',\n 'local-llama',\n 'local-kobold',\n 'local-embeddings',\n 'vllm',\n 'lm-studio',\n 'xinference',\n 'triton',\n // Switch self-hosted (6)\n 'self-hosted',\n 'local-ocr',\n 'local-docling',\n 'local-nllb',\n 'local-cosyvoice',\n 'local-piper',\n ],\n} as const;\n\n/**\n * Convenience predicate: is the providerId classified as compliant\n * with the SOTA dynamic-discovery contract (any of the three compliant\n * buckets)? Self-hosted is treated as compliant-by-category since its\n * inventory genuinely depends on operator runtime.\n */\nexport function isDiscoveryCompliant(providerId: string): boolean {\n const compliantBuckets: readonly DiscoveryComplianceClass[] = [\n 'compliant-dynamic-discovery',\n 'compliant-deployment-discovery',\n 'compliant-machine-readable-official-catalog',\n // Phase 6 Fix 7 (2026-04-30): pinnedFallback-by-design is compliant —\n // the operator-curated list IS the inventory contract when the vendor\n // genuinely doesn't expose /models. See bucket docstring.\n 'pinnedFallback-by-design',\n 'self-hosted-runtime-dependent',\n 'not-applicable-non-model-surface',\n ];\n for (const bucket of compliantBuckets) {\n if (DISCOVERY_COMPLIANCE_REGISTRY[bucket].includes(providerId)) return true;\n }\n return false;\n}\n\n/**\n * Reverse lookup: given a providerId, return its compliance class\n * (or undefined if the providerId is not in the canonical set).\n */\nexport function getDiscoveryComplianceClass(\n providerId: string\n): DiscoveryComplianceClass | undefined {\n for (const bucket of DISCOVERY_COMPLIANCE_BUCKETS) {\n if (DISCOVERY_COMPLIANCE_REGISTRY[bucket].includes(providerId)) return bucket;\n }\n return undefined;\n}\n\n/**\n * Orphan inventory — in-tree secrets and/or adapter classes that are NOT\n * in the canonical set. These are tracked so operational counters never\n * accidentally include them. They are NOT part of the consolidation matrix\n * proper (the test asserts disjointness).\n *\n * ## Triage decision (2026-04-23 — Lot B closure applied + backlog cleared):\n *\n * - `writer`, `upstage`, `rekaai`: CLOSED as of 2026-04-23 via Lot B\n * (catalog rows, factory registrations, secret mappings added). They\n * are no longer orphans — they are canonical providers in the\n * `live-validation` bucket above.\n *\n * - `ai21`, `llmstats`, `merge`: VERIFIED-CLEAN as of 2026-04-23. Grep\n * across `src/config/load-secrets-into-env.ts` returned no matches\n * for any of these — previous removal lots already cleared them.\n * The only residual reference is `ai21` in `NATIVE_PROVIDERS`\n * (src/core/selection/provider-kind.ts): that set is a permissive\n * routing-bias classifier where extra entries have no runtime cost\n * (the classifier never receives a non-canonical providerId from\n * code paths that only dispatch canonical providers). Accepted as\n * structural exception; documented here rather than surgically\n * removed to avoid re-breaking a settled decision.\n */\nexport const NON_CANONICAL_PROVIDERS = {\n // Empty as of 2026-04-23 — Lot B closed writer/upstage/rekaai. If a\n // future orphan needs transient tracking (adapter on disk, no catalog\n // row, no factory), it goes here until the closure lot is shipped.\n pending_closure: [] as const,\n // Empty as of 2026-04-23 — backlog verified clean via grep. See\n // triage note above for the full reasoning.\n pending_removal: [] as const,\n} as const;\n\n/**\n * Historical claims from prior reports that are superseded by this\n * consolidation. Keeping them here (not in the matrix) makes the\n * supersession explicit and audit-traceable, without letting them\n * contaminate operational counts.\n *\n * The invariant test asserts none of these strings appear in any\n * bucket of CONSOLIDATION_MATRIX.\n */\nexport const NON_CANONICAL_HISTORICAL_CLAIMS = [\n {\n claim: '0 live probes this session',\n superseded_at: '2026-04-23',\n reason:\n '41 HTTP probes were executed in the 2026-04-23 consolidation session; 28 canonical providers classified as live-validation at first-pass, 31 after Lot B closure of writer/upstage/rekaai.',\n },\n {\n claim: '29 providers live-validated',\n superseded_at: '2026-04-23',\n reason:\n 'The 29 figure belonged to a prior probe session. Pre-Lot-B this consolidation recorded 28 canonical live-validated providers; post-Lot-B (writer/upstage/rekaai promoted from orphan to canonical in the same turn) the figure is 31.',\n },\n {\n claim: '32 providers live-validated',\n superseded_at: '2026-04-23',\n reason:\n 'The 32 figure counted writer/upstage/rekaai/ai21 as canonical, but at the time only ai21 remained non-canonical. As of Lot B closure in this session, writer/upstage/rekaai ARE canonical (with probe-pre-closure-200 evidence); ai21 remains non-canonical pending removal. The correct canonical count for that probe set is now 31.',\n },\n {\n claim: 'canonical count = 87',\n superseded_at: '2026-04-23',\n reason:\n 'At first-pass of this session the audit recorded catalog=65 + switch=22 = 87. Lot B closure (writer/upstage/rekaai orphan-to-canonical) brings catalog=68 + switch=22 = 90. The invariant test does NOT pin the number; it pins structural identity (matrix total equals canonical union size).',\n },\n {\n claim: 'canonical count = 90',\n superseded_at: '2026-04-23',\n reason:\n 'Post Lot B and pre LOTE M the audit recorded catalog=68 + switch=22 = 90. LOTE M 2026-04-23 complement lot (arcee, atlascloud, avian, gmi, infermatic, inflection, mancer, phala, qianfan, relace, siliconflow, stepfun, venice — 13 net adds: 12 hosted OAI-compat + 1 catalog-only) brings catalog=81 + switch=22 = 103. The invariant test still pins structural identity, not the number; this claim exists so a grep for `= 90` against the matrix surfaces as explicitly superseded rather than looking authoritative.',\n },\n {\n claim: '31 providers live-validated (post Lot B, pre late-pass)',\n superseded_at: '2026-04-23',\n reason:\n 'Late-pass probes on 2026-04-23 promoted 5 providers to live-validation (edenai re-probed with correct secret → 200/330 models; openrouter /api/v1/models → 200/349 models; cartesia /voices → 200/751 voices; deepgram /v1/models → 200 using Token auth scheme; elevenlabs /v1/voices → 200) and demoted 1 (moonshot /v1/models → 401, previously 200). Net +4 → 35 live-validated providers (24 catalog + 11 switch).',\n },\n {\n claim: 'gemini-openai is no-live-validation',\n superseded_at: '2026-04-23',\n reason:\n 'gemini-openai shares the google api_key state. A direct probe against generativelanguage.googleapis.com returned 403 CONSUMER_SUSPENDED — the key was suspended server-side, not scope-limited. gemini-openai therefore shares the google vendor-side-failure classification rather than sitting in no-live-validation.',\n },\n {\n claim: 'google 403 is an SA scope limit',\n superseded_at: '2026-04-23',\n reason:\n 'Probe against generativelanguage.googleapis.com this session returned an explicit \"Consumer \\'api_key:...\\' has been suspended\" message (403 CONSUMER_SUSPENDED), not a scope-denial. The failure is vendor-side key state, not a scope mismatch. Bucket is unchanged (still vendor-side-failure), only the reason text is corrected.',\n },\n {\n claim: 'edenai credential is rotated server-side (defunct)',\n superseded_at: '2026-04-23',\n reason:\n 'The prior probe pass used secret ailin-edenai-api-key and got 401, concluding \"rotated\". This pass probed /v3/llm/models with ailin-edenai-key (alternate GCP secret in the same project) and got HTTP 200 with 330 models. edenai is live-validated; the defunct verdict was a secret-selection bug in the prior pass.',\n },\n {\n claim: 'moonshot live-validated (from earlier session)',\n superseded_at: '2026-04-23',\n reason:\n 'Re-probe of api.moonshot.cn/v1/models with the same secret (ailin-moonshot-key) returned 401 this pass, where the previous session recorded 200. Whether the key was rotated or the public API tier changed is undetermined; moonshot is reclassified defunct-unreachable until next probe lot establishes the cause.',\n },\n {\n claim: 'moonshot is defunct-unreachable (from the pre-final late-pass)',\n superseded_at: '2026-04-23',\n reason:\n 'The \"defunct\" verdict came from probing api.moonshot.cn (PRC-region host). The canonical MoonshotAdapter uses api.moonshot.ai/v1 (see DEFAULT_BASE_URL in moonshot-adapter.ts). This final pass re-probed the ACTUAL adapter target api.moonshot.ai/v1/models with the same GCP secret ailin-moonshot-key → HTTP 200, 14 models. The integration was never broken; the earlier 401 was a probe-target bug, not a credential rotation. moonshot is restored to live-validation.',\n },\n {\n claim: 'aws-bedrock is credentials-missing (requires SigV4, cannot probe)',\n superseded_at: '2026-04-23',\n reason:\n 'AWS added Bearer-token authentication for Bedrock read endpoints in 2025, alongside the existing SigV4 flow. GCP secret ailin-aws-bearer-token was probed against GET bedrock.us-east-1.amazonaws.com/foundation-models → HTTP 200, 163KB body, modelSummaries array populated. Credentials ARE available and the read path works without SigV4 signing. Invocation endpoints still use SigV4 via @aws-sdk/signature-v4, which is a separate surface; the bucket classification is based on reachable credential evidence, and that evidence now exists. aws-bedrock → live-validation.',\n },\n {\n claim: 'palabraai is credentials-missing',\n superseded_at: '2026-04-23',\n reason:\n 'GCP secrets ailin-palabraai-id (ClientId) and ailin-palabraai-key (ClientSecret) exist. Probe of POST api.palabra.ai/session-storage/session with both headers returned HTTP 403 with body {\"detail\":\"Insufficient balance\",\"error_code\":100050} — the Palabra public error-code table documents 100050 as post-authentication credit exhaustion. This is upstream-suspended (auth accepted, tier-gated), NOT credentials-missing.',\n },\n {\n claim: 'complement lot (2026-04-23) — 21 brand names to integrate',\n superseded_at: '2026-04-23',\n reason:\n 'Mapping table produced for Arcee/AtlasCloud/Avian/BaiduQianfan/BFL/Chutes/GMICloud/Infermatic/Inflection/Liquid/Mancer/MiniMax/ModelRun/nCompass/Phala/Reka/Relace/SiliconFlow/StepFun/Upstage/Venice. Outcome: 5 already-canonical (bfl, chutes, minimax, rekaai, upstage); 1 orphan-with-material (baidu-qianfan — has BaiduModelFetcher + secret mapping ailin-baidu-{key,secret,base-url}, but all 3 secrets are literal \"PLACEHOLDER\" and the fetcher uses deprecated ERNIE OAuth2 shape; canonical promotion deferred to a dedicated lot that (a) adds baidu-qianfan catalog row with qianfan.baidubce.com/v2 OAI-compat base URL, (b) replaces the ERNIE OAuth2 fetcher with Bearer auth, (c) requires real secret provisioning); 15 absent (arcee, atlascloud, avian, gmicloud, infermatic, inflection, liquid, mancer, modelrun, ncompass, phala, relace, siliconflow, stepfun, venice — no material in-tree, no secrets provisioned). Per the directive exigência final (\"Não quero provider adicionado só porque ganhou row no catálogo\"), catalog row additions were NOT committed in this lot; they await operator secret provisioning to ensure each row ships with live-validation evidence. Proposed row shapes captured in the lot report (section 14 \"Próximo lote objetivo\").',\n },\n {\n claim: 'LOTE M deferral — await operator secret provisioning before adding rows',\n superseded_at: '2026-04-23',\n reason:\n 'Operator decision 2026-04-23 (message #2 of complement-lot session): \"#1 Escopo completo #2 A #3 Prossiga com a melhor recomendação para cada\" authorized catalog-row additions without waiting for secret provisioning. The deferral rationale (every row should ship with live-validation evidence) was superseded by the operator-level decision to land the rows now AND track each one explicitly in credentials-missing/secret-absent (or auth-incomplete for qianfan). This makes the \"missing work\" visible in the matrix — operators see 12 rows waiting for <PROVIDER>_API_KEY provisioning — rather than invisible in a narrative-only backlog. 13 rows added this lot (arcee, atlascloud, avian, gmi, infermatic, inflection, mancer, phala, qianfan, relace, siliconflow, stepfun, venice); the 3 NOT_ELIGIBLE providers (liquid, modelrun, ncompass) are recorded in separate historical claims below.',\n },\n {\n claim: 'liquid (Liquid AI) eligible for canonical catalog inclusion',\n superseded_at: '2026-04-23',\n reason:\n 'Liquid AI (LFM family) has no first-party production API as of 2026-04-23. LFM models (LFM-40B, LFM-3B, LFM-1B) are distributed through OpenRouter and other aggregators; liquid.ai offers a research/demo surface but not an OAI-compat or native production endpoint that an adapter can target. A `liquid` canonical providerId would duplicate the routing a user already gets via openrouter → liquid/lfm-*. Reclassification requires Liquid AI to publish a first-party API with a stable base URL. Status: NOT_ELIGIBLE until that upstream change.',\n },\n {\n claim: 'modelrun eligible for canonical catalog inclusion',\n superseded_at: '2026-04-23',\n reason:\n 'ModelRun (modelrun.xyz and known alternative domains) returned ECONNREFUSED on all probed endpoints during the 2026-04-23 complement-lot evaluation. No indexed public service fronted by the expected base URL. No adapter material exists in-tree. Cannot be canonicalized because the base URL itself is unverifiable. Status: NOT_ELIGIBLE until operator identifies the correct hostname (if the brand still operates) or confirms the service is defunct.',\n },\n {\n claim: 'ncompass eligible for canonical catalog inclusion',\n superseded_at: '2026-04-23',\n reason:\n 'nCompass Technologies (ncompass.tech) base URL for its production inference API is not published in public docs accessible from the probe environment. The landing pages reference a private signup flow; the actual api.* or inference.* subdomain is gated behind operator registration. Cannot add a catalog row without a verifiable baseUrl — Zod validation requires it, and a wrong URL creates silent 404-loops. Status: NOT_ELIGIBLE until operator signs up, discovers the base URL, and provisions NCOMPASS_API_KEY.',\n },\n {\n claim: 'venice is credentials-missing / secret-absent (from LOTE M close)',\n superseded_at: '2026-04-23',\n reason:\n 'Sublote A 2026-04-23 probed venice directly. /api/v1/models returned HTTP 200 PUBLICLY (no auth header required; even with an invalid Bearer the same 200 response body was returned — auth is IGNORED on /models), carrying 72 text models in OAI-conformant shape ({data:[{id,object,owned_by,...}]}). /api/v1/chat/completions returned HTTP 402 with a full x402 crypto-payment-protocol body (network eip155:8453 Base, USDC token 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913) when no Bearer was sent, and HTTP 401 {\"error\":\"Authentication failed\"} when an invalid Bearer was sent. Discovery surface is live-proven against the canonical adapter target; execution surface is credential-gated. Under the Sublote A rule \"se houver probe real bem-sucedida, não rebaixar\", venice was promoted from credentials-missing → partial. GCP still has no ailin-venice-* secret under any alias, so live-validation of the chat surface remains blocked. This claim is documented here (rather than silently updating the earlier claim) to make the Sublote A bucket move visible to future auditors grepping for venice.',\n },\n {\n claim: 'Sublote A (2026-04-23) produced zero bucket moves',\n superseded_at: '2026-04-23',\n reason:\n 'First-pass Sublote A narrative incorrectly concluded \"4/4 stay in credentials-missing; zero moves\". The rule \"se houver probe real bem-sucedida, não rebaixar\" is satisfied by venice /api/v1/models returning HTTP 200 against the canonical adapter base URL. Revised outcome: ONE bucket move (venice credentials-missing → partial). The other three (qianfan, siliconflow, stepfun) stay credentials-missing because their probes returned only error responses against the canonical surface — endpoint liveness is confirmed but no canonical surface is live-proven. Concrete impact: credentials-missing 59→58, partial 1→2.',\n },\n {\n claim: 'Sublote A second-pass verification found new promotion opportunities',\n superseded_at: '2026-04-23',\n reason:\n 'Sublote A re-run 2026-04-23 with deeper probes. (1) GCP Secret Manager re-scanned WITHOUT the ailin- prefix constraint across all 82 secrets — zero matches for qianfan/venice/siliconflow/stepfun under any alias (qianfan-*, baidu-qianfan-*, silicon-flow-*, step-*, venice-ai-*). Only ailin-baidu-{key,secret,base-url} exist, all three still literal \"PLACEHOLDER\" (11 bytes each). (2) Baidu v1 AK+SK OAuth token endpoint https://aip.baidubce.com/oauth/2.0/token probed with client_id=PLACEHOLDER&client_secret=PLACEHOLDER → HTTP 401 \"invalid_client / unknown client id\". This is STRONGER evidence than the previous \"material is placeholder\" framing: Baidu\\'s OAuth provider does not even recognize the client_id — the string is unregistered upstream, not merely format-wrong. qianfan classification is unchanged but the auth-incomplete subclass is now supported by upstream server-side rejection, not just local placeholder heuristic. (3) Venice /api/v1/models re-probed WITH an invalid Bearer header → HTTP 200, identical 78617-byte body to the no-auth request. Confirms the Authorization header is actively IGNORED on discovery — venice discovery is truly unauthenticated public, not \"optional auth accepted\". (4) siliconflow and stepfun probed on common unauth paths /health, /ping, /, /v1/health, /v1/ping → all HTTP 404 on both .cn/.com (siliconflow) and .com/.ai (stepfun). No public discovery surface exists; discovery IS credential-gated. This forecloses the Venice-style \"promote on public /models\" path for these two — they will move only when real secrets are provisioned. Net bucket delta this second pass: ZERO (matrix state is already consistent post first-pass Sublote A). Evidence strengthening only. .env.production, .env.test, .env.example, .env.example.clean all scanned — zero mentions of baidu/qianfan/venice/siliconflow/stepfun/ernie/step aliases.',\n },\n {\n claim: 'LOTE M remainder (12 providers) is a uniform credentials-missing block',\n superseded_at: '2026-04-23',\n reason:\n 'Sublote B 2026-04-23 applied the same probe method that promoted venice (Sublote A) to the 12 LOTE M providers still in credentials-missing/secret-absent (11 providers + 1 auth-incomplete qianfan). Method: for each provider, probe canonical adapter baseUrl across 8 paths ({/models, /v1/models, /health, /ping, /status, /info, /v1/info, /}) × 2 auth modes (no Authorization header vs invalid Bearer) = 16 probes × 12 providers = 192 probes total. Result: 4 PROMOTIONS from credentials-missing → partial on confirmed HTTP 200 public /models, 7 CONFIRMED-blocked (discovery credential-gated, no public surface), 1 catalog-only inventory (no OAI discovery surface at all, POST-only execution endpoint). Evidence per provider: (a) PROMOTED — atlascloud: api.atlascloud.ai/v1/models HTTP 200 59503 bytes (107 models, custom {code:200,msg:\"succeed\",data:[...]} wrapper; other 7 paths asymmetric: noauth 401 vs badauth 404, revealing server differentiates absent-header vs invalid-header). avian: api.avian.io/v1/models HTTP 200 1782 bytes (6 models, pure OAI shape; other 7 paths uniform 404). mancer: neuro.mancer.tech/oai/v1/models HTTP 200 2379 bytes no-header (9 LLaMA fiction/RP models) but HTTP 401 80 bytes with invalid Bearer — asymmetric VALIDATION where server accepts enumeration when no credential is CLAIMED but rejects claimed-invalid credentials. phala: api.redpill.ai/v1/models HTTP 200 72119 bytes (76 models via Phala SGX TEE confidential-compute routing, same body with and without invalid Bearer; other paths uniform 400 \"Internal server error\"). (b) CONFIRMED-BLOCKED — arcee: /models 401 both modes (other paths 404). gmi: /models 401 both modes (other paths 405 Method Not Allowed — endpoints exist but POST-only, not queryable unauth). infermatic: /models 401 both modes (other paths 404). relace: ALL 8 paths return 401 uniformly (catch-all auth middleware — no public discovery). siliconflow: /models 401 with 15-byte body (bare JSON string \"Invalid token\" confirming oai-compat-quirks class; other paths 404). stepfun: /models 401 with 75-byte OAI-shape error {\"error\":{\"message\":\"Incorrect API key provided\",\"type\":\"invalid_api_key\"}} (other paths 404, 0-byte bodies). qianfan (v2 surface qianfan.baidubce.com/v2): /models 401 with 120-byte BCE-shape error (other paths 404). (c) CATALOG-ONLY CONFIRMED — inflection: /models 404 on inference.ai.inflection.ai across ALL 8 paths (root / returns 307 redirect). The correct execution surface is /external/api/inference (POST-only, no discovery sidecar); inflection is a catalog-only-inventory-candidate operationally but retains secret-absent subclass because INFLECTION_API_KEY is still the expected unblock vector. Net bucket delta: partial 2→6 (+4), credentials-missing 58→54 (−4). Secret-absent subclass 40→36 (−4: atlascloud/avian/mancer/phala removed; 7 LOTE M rows remain: arcee/gmi/infermatic/inflection/relace/siliconflow/stepfun). No other buckets touched. Invariants I1–I6 hold: |catalog ∪ switch| unchanged at 103, every canonical id still in exactly one bucket, credentials-missing subclass partition preserved. The probe evidence is replicable from /tmp/subb/{provider}.out (12 files × 16 probes each) and model payloads /tmp/subb/payloads/{atlascloud,avian,mancer,phala}.json.',\n },\n {\n claim:\n 'LOTE M remainder (6 providers in Sublote C1 scope) can be unblocked by provisioning alone',\n superseded_at: '2026-04-23',\n reason:\n 'Sublote C1 2026-04-23 attempted credential resolution + per-surface live probes for arcee/gmi/infermatic/siliconflow/stepfun/qianfan. (1) GCP Secret Manager exhaustively re-scanned: 82 secrets × 27 alias patterns (arcee, conductor, gmi, gmicloud, gmi-cloud, gmi-serving, gmiserv, infermatic, totalgpt, total-gpt, siliconflow, silicon-flow, siliconcloud, silicon-cloud, stepfun, step, step-ai, stepai, qianfan, baidu, ernie, baidu-qianfan, wenxin, wenxinworkshop, bce, ai21, api-ernie) — zero matches except the known 3 ailin-baidu-{key,secret,base-url}, all confirmed this session as literal \"PLACEHOLDER\" (11 bytes). Environment variables checked at runtime: zero set. .env/.env.production/.env.test on disk: zero mentions. (2) Per-surface live probes executed with invalid bearer against canonical adapter baseUrls. Key doc-vs-impl discoveries: (a) INFERMATIC — api.totalgpt.ai badauth response echoes \\'LiteLLM Virtual Key expected. Received=INVALID_TEST_KEY_12345, expected to start with sk-\\'. Confirmed: infermatic is a LiteLLM proxy in front of the vLLM backend, not direct vLLM. API keys must be sk-prefixed LiteLLM Virtual Keys (generated via LiteLLM Admin UI or /key/generate). Notes updated. (b) GMI — api.gmi-serving.com/v1/chat and /v1/embeddings return HTTP 404 \"No matching target server found for model X\" BEFORE auth validation (pre-auth routing layer). This means a 404 on gmi chat does NOT imply bad credentials — it implies the model ID is not registered. Notes updated. (c) QIANFAN v1 — aip.baidubce.com/oauth/2.0/token with client_id=PLACEHOLDER returned 401 invalid_client/unknown client id (confirms upstream does not recognize), but the v1 chat endpoint aip.baidubce.com/rpc/2.0/ai_custom/v1/wenxinworkshop/chat/ernie-4.0-8k with access_token=PLACEHOLDER returned HTTP 200 with body-encoded {error_code:3,\"Unsupported openapi method\"} — Baidu\\'s API uses 200+error_code pattern, and the v1 endpoint IS reachable and would route a real token if provisioned. (d) SILICONFLOW — 6 surfaces (models.cn, models.com, chat, embed, rerank, audio) all uniform 401 bare-JSON-string \"Invalid token\" (15 bytes) — confirms integrationClass oai-compat-quirks across every surface. Both .cn and .com hosts equivalent. (e) STEPFUN — 5 surfaces (models.com, models.ai, chat, embed, audio/speech) all uniform 401 OAI-shape {\"error\":{\"message\":\"Incorrect API key provided\",\"type\":\"invalid_api_key\"}} (75 bytes) — confirms integrationClass oai-compat-pure. Both .com and .ai hosts equivalent. (f) ARCEE — /api/v1/models and /api/v1/chat both 401 with Arcee-specific {\"detail\":\"Missing or invalid Authorization header. Expected: Bearer <api_key>\"} (80 bytes noauth) / {\"detail\":\"Invalid or expired API key\"} (39 bytes badauth). Root / returns 200 \"OK\" (health check, not discovery). Net bucket delta: ZERO. All 6 providers remain in same bucket as pre-Sublote-C1 because no credential became available during this session. However, Sublote C1 produced: (i) 2 catalog notes updates (infermatic LiteLLM proxy, gmi pre-auth routing), (ii) 1 new historical claim (this entry), (iii) definitive proof that unblocking all 6 is PURE credential-provisioning work (no adapter changes, no endpoint changes, no structural refactoring required). Probe evidence in /tmp/subc1/clean.out and /tmp/subc1/bodies/*.body.',\n },\n {\n claim: 'LOTE M remainder mostly blocked by credential provisioning (pre-Sublote-D1)',\n superseded_at: '2026-04-24',\n reason:\n 'Sublote D1 2026-04-24 — operator provisioned 17 GCP secrets (ailin-{groq,togetherai,fireworks-ai,deepinfra,perplexity,huggingface,cloudflare-workers-ai,cloudflare-workers-ai-id,github-models,sambanova,hyperbolic,arcee,infermatic,siliconflow,stepfun,heliconeai-api,chutes,anyscale}-*-key(s)). Live probes (GET /v1/models + POST /v1/chat/completions with minimal body {\"model\":\"X\",\"messages\":[{\"role\":\"user\",\"content\":\"hi\"}],\"max_tokens\":5}) executed against canonical adapter baseUrls with the provisioned credentials. Result summary: (1) TEN promotions to live-validation (real HTTP 200 with completion body on /v1/chat): groq (llama-3.1-8b-instant 592B), deepinfra (Meta-Llama-3.1-8B-Instruct 455B), huggingface (Llama-3.1-8B-Instruct 668B via router.huggingface.co), cloudflare-workers-ai (@cf/meta/llama-3-8b-instruct 360B; /models 405 is expected per CF docs — their discovery uses /ai/models/search), github-models (openai/gpt-4o-mini 1252B; models at /catalog/models not /v1/models), perplexity (sonar 2992B; /models 404 by design), fireworks-ai (accounts/fireworks/models/glm-5p1 447B — re-probed after first-pick llama-v3p1-8b returned 404 deprecated), sambanova (Meta-Llama-3.3-70B-Instruct 910B — re-probed after first-pick 3.1 returned 410 Gone), infermatic (Qwen-Qwen3-30B-A3B 476B — confirmed LiteLLM Virtual Key with sk- prefix; key has a model-scoped ACL), heliconeai (gpt-4o-mini 1003B — new ailin-heliconeai-api-key sk-hel… 43B SUPERSEDES the legacy ailin-heliconeai-key \"PLACEHOLDER\" 11B that caused the prior mis-classification in CREDENTIALS_MISSING_SUBCLASS.placeholder). (2) FOUR promotions to upstream-suspended: anyscale (endpoint returns HTML shutdown notice \"Effective August 1, 2024 Anyscale Endpoints API is available exclusively through the fully Hosted Anyscale Platform; Multi-tenant access to LLM models has been removed.\" — the key ailin-anyscale-api-key 236B exists but cannot be exercised), arcee (trinity-mini → 402 {\"detail\":\"Insufficient credits. Required: 0.000037, Available: 0.000000\"}), chutes (Qwen/Qwen3-32B-TEE → 402 {\"detail\":{\"message\":\"Quota exceeded and account balance is $0.0, please pay with fiat or send tao...\"}}), hyperbolic (→ 402 {\"detail\":\"Insufficient funds\"}). All four have valid credentials (auth-accepted) but gated execution — operationally identical to ai302/palabraai. (3) THREE stay in credentials-missing but move sub-class secret-absent → auth-incomplete: togetherai (key_CY… 25B; /v1/chat returns OpenAI-shape 401 \"Invalid API key provided. You can find your API key at https://api.together.ai/settings/api-keys\" — the \"key_\" prefix is non-canonical for Together AI, probable format mismatch), siliconflow (sk-hhc… 51B; /v1/chat returns bare JSON \"Api key is invalid\" 15B oai-compat-quirks shape — probable format mismatch, sk-prefix looks OpenAI-style rather than SiliconFlow native), stepfun (65B; /v1/chat returns OAI-shape {\"error\":{\"message\":\"Incorrect API key provided\",\"type\":\"invalid_api_key\"}} 75B — verbatim OpenAI error shape, operator must verify the key maps to their StepFun account). (4) ONE operator-claim-vs-vault mismatch: gmi — the operator announcement listed gmi among the 16 provisioned but GCP Secret Manager scan 2026-04-24 against all known aliases (ailin-gmi-*, ailin-gmicloud-*, ailin-gmi-cloud-*, ailin-gmi-serving-*, ailin-gmiserv-*) found ZERO matches. gmi stays secret-absent until the actual secret lands in the vault. Net matrix deltas: live-validation 37→47 (+10), credentials-missing 54→40 (−14), upstream-suspended 2→6 (+4). Sub-class deltas: secret-absent 36→20 (−16: 9 to live-validation [all 10 promotees except heliconeai which was in placeholder sub-class], 4 to upstream-suspended, 3 to auth-incomplete), placeholder 2→1 (heliconeai promoted — legacy secret superseded by new api-key form), auth-incomplete 2→5 (+togetherai,siliconflow,stepfun). All invariants I1–I6 hold post-D1: 103 canonical total unchanged, every providerId in exactly one bucket, sub-class partition of credentials-missing covers all 40 entries. Probe evidence: /tmp/subd1/probe.out + /tmp/subd1/reprobe.out + /tmp/subd1/bodies/*.{models,chat,rechat}.body (17 providers × 2–3 surfaces each = ~50 probe records).',\n },\n] as const;\n\n/**\n * Sum of all canonical providers across the matrix. Convenience function —\n * the test asserts this equals |catalog ∪ switch| (structural identity, no\n * pinned number). At 2026-04-23 post-LOTE-M: catalog=81 + switch=22 = 103.\n *\n * Bucket distribution post-Sublote-D1 (2026-04-24):\n * live-validation = 47 (34 catalog + 13 switch) — +10 this sublote\n * no-live-validation = 0\n * partial = 6 (bytez — wire-shape mismatch;\n * venice — asymmetric auth (Sublote A);\n * atlascloud, avian, mancer, phala —\n * /models 200 public (Sublote B).)\n * — unchanged by Sublote D1.\n * credentials-missing = 40 (33 catalog + 7 switch/self-hosted) — −14\n * net this sublote (10 to live, 4 to\n * upstream-suspended). Sub-class partition:\n * secret-absent = 20\n * placeholder = 1\n * self-hosted-unreachable = 14\n * auth-incomplete = 5\n * total = 40 ✓\n * vendor-side-failure = 1 (google — GOOGLE_API_KEY still suspended)\n * upstream-suspended = 6 (ai302, palabraai, anyscale, arcee,\n * chutes, hyperbolic — +4 this sublote)\n * defunct-unreachable = 0\n * catalog-only-inventory = 3 (sap, snowflake, topaz)\n * switch-only-legitimate = 0\n * not-eligible = 0\n * TOTAL = 103 (matches |catalog ∪ switch|)\n *\n * Historical progression (each transition has a supersession entry):\n * Pre-LOTE-M : live=37 partial=1 creds=46 vendor=1 susp=2 cat-only=3 → 90\n * LOTE M close : live=37 partial=1 creds=59 vendor=1 susp=2 cat-only=3 → 103\n * Sublote A : live=37 partial=2 creds=58 vendor=1 susp=2 cat-only=3 → 103 (venice → partial)\n * Sublote B : live=37 partial=6 creds=54 vendor=1 susp=2 cat-only=3 → 103 (+atlascloud,avian,mancer,phala → partial)\n * Sublote C1 : live=37 partial=6 creds=54 vendor=1 susp=2 cat-only=3 → 103 (no moves — probe evidence strengthening only)\n * Sublote D1 : live=47 partial=6 creds=40 vendor=1 susp=6 cat-only=3 → 103 (+10 live, −14 creds, +4 upstream-suspended)\n */\nexport function totalCanonicalInMatrix(): number {\n return CONSOLIDATION_BUCKETS.reduce(\n (sum, bucket) => sum + CONSOLIDATION_MATRIX[bucket].length,\n 0\n );\n}\n"}, "9478f2ca54e894d213553c9fc0b8c1fd69d488bc": {"t": 1785846911.0136948, "n": 108185, "h": "a936418c93ce3313", "c": "// Copyright (C) 2026 Ailin One, Inc.\n//\n// This file is part of Collective Intelligence Engine (ci).\n// Licensed under the GNU Affero General Public License v3.0 or later.\n// See LICENSE in the repository root, or <https://www.gnu.org/licenses/>.\n//\n// SPDX-License-Identifier: AGPL-3.0-or-later\n// Source: https://github.com/ailinone/collective-intelligence\n\n/**\n * Canonical provider consolidation matrix (SOTA — saneamento final).\n *\n * ## Purpose\n *\n * This module is the **single authoritative classification** of the canonical\n * 103-provider set (81 catalog + 22 switch, post LOTE M 2026-04-23) into\n * mutually-exclusive operational buckets. It exists to make the\n * SOTA-CONSOLIDAÇÃO-FINAL decision **machine-readable**: narrative reports in\n * markdown can drift; code with a passing test cannot.\n *\n * Canonical-count history (each transition has a supersession entry in\n * `NON_CANONICAL_HISTORICAL_CLAIMS` below):\n * - 87 = pre Lot B (catalog=65 + switch=22)\n * - 90 = post Lot B, pre LOTE M (catalog=68 + switch=22)\n * - 103 = post LOTE M 2026-04-23 (catalog=81 + switch=22)\n *\n * ## Decision record (frozen 2026-04-23)\n *\n * **Live-validation rule = Option A (accept this-session probes).**\n *\n * The 41 HTTP probes emitted during the consolidation session of 2026-04-23\n * were executed against the then-current HEAD working-tree catalog using\n * credentials freshly loaded from GCP Secret Manager. They produced real\n * HTTP responses (2xx, or 4xx with auth accepted + tier-gated). The\n * catalog has not changed since probe time, so the probes remain valid\n * evidence of live integration for every canonical provider they touched.\n *\n * Any prior claim that \"0 probes were executed this session\" is marked\n * explicitly below as `nonCanonicalHistoricalClaims` — historical text\n * from earlier pass(es), superseded by this decision.\n *\n * ## Bucket semantics (10 buckets, mutually exclusive)\n *\n * live-validation — probe emitted this session, response indicates\n * working integration (2xx, OR 4xx that proves auth\n * accepted such as tier-gate / insufficient-balance\n * on a reachable endpoint)\n * no-live-validation — adapter + catalog/switch present, no probe this\n * session (credential available but probe skipped,\n * or scope-limited credential)\n * partial — integration incomplete at HEAD (e.g. adapter\n * exists, catalog missing, or vice versa)\n * credentials-missing — canonical provider, no key available this session\n * vendor-side-failure — probe returned 5xx/403 from vendor-side config\n * (NOT an integration bug)\n * upstream-suspended — auth accepted but vendor reports credit/quota\n * exhausted\n * defunct-unreachable — credential invalid this session (stale/rotated)\n * catalog-only-inventory — catalog row with integrationMode: 'catalog-only'\n * (intentionally non-executing metadata inventory)\n * switch-only-legitimate — provider-registry.ts switch case that is NOT\n * OAI-compatible and cannot be migrated to catalog\n * without an adapterClass framework extension\n * not-eligible — secret and/or adapter exists in-tree but is NOT\n * in the canonical 103 (orphan); tracked separately\n * so operational counters never include them\n *\n * ## Invariants (enforced by consolidation-matrix.test.ts)\n *\n * I1 — every canonical providerId (catalog ∪ switch) appears in exactly\n * ONE bucket\n * I2 — no providerId appears in more than one bucket\n * I3 — buckets are exactly the 10 above\n * I4 — nonCanonicalProviders (orphans) are disjoint from canonical\n * I5 — nonCanonicalHistoricalClaims cannot contaminate the matrix\n * (they live in a separate, audit-only field)\n *\n * When a provider's operational state changes, update ONE bucket and the\n * test will catch any accidental double-classification.\n */\n\n/**\n * The ten mutually-exclusive classification buckets.\n * Order is NOT significant — the bucket is identity; no hierarchy implied.\n */\nexport const CONSOLIDATION_BUCKETS = [\n 'live-validation',\n 'no-live-validation',\n 'partial',\n 'credentials-missing',\n 'vendor-side-failure',\n 'upstream-suspended',\n 'defunct-unreachable',\n 'catalog-only-inventory',\n 'switch-only-legitimate',\n 'not-eligible',\n] as const;\n\nexport type ConsolidationBucket = (typeof CONSOLIDATION_BUCKETS)[number];\n\n/**\n * Per-bucket assignment of every canonical providerId.\n *\n * Each providerId MUST appear in exactly one bucket's array.\n * Adding a new canonical provider requires adding it to exactly one bucket.\n */\nexport const CONSOLIDATION_MATRIX: Record<ConsolidationBucket, readonly string[]> = {\n // ── 38 canonical providers with real HTTP evidence ───────────────────\n // Pre-update: 35 (24 catalog + 11 switch). Post-update 2026-04-23 final\n // pass: 38 (26 catalog + 12 switch). Deltas applied in THIS final pass:\n //\n // + moonshot → was 'defunct-unreachable' from an earlier late-pass.\n // Root cause of the prior 401 was probe-target drift:\n // the probe hit `api.moonshot.cn` (the PRC endpoint),\n // but the canonical MoonshotAdapter uses\n // `api.moonshot.ai/v1` (confirmed: DEFAULT_BASE_URL\n // in moonshot-adapter.ts). This final pass probed\n // `api.moonshot.ai/v1/models` with the same GCP\n // secret `ailin-moonshot-key` → HTTP 200, 14 models.\n // The integration was never broken; the previous\n // verdict was a probe-target bug. Classification\n // restored.\n //\n // + aws-bedrock → was 'credentials-missing'. GCP has\n // `ailin-aws-bearer-token` (AWS added Bearer-token\n // auth for read endpoints in 2025, alongside SigV4).\n // Probe `GET bedrock.us-east-1.amazonaws.com/foundation-models`\n // with Bearer → HTTP 200, 163KB body. This proves\n // credentials + reachability; SigV4 compatibility for\n // invocation endpoints is a separate surface and is\n // covered by the switch-case adapter which uses the\n // @aws-sdk/signature-v4 path at execution time.\n //\n // + gemini-openai → was 'vendor-side-failure' (google-key 403\n // CONSUMER_SUSPENDED). Root cause was the\n // GEMINI_API_KEY fallback chain picking `google-key`\n // (suspended) before any alternative. The fallback\n // chain in load-secrets-into-env.ts was extended\n // this pass to include `vertex-key` at the end —\n // a generic Google Cloud API key that is NOT\n // suspended and IS scoped for the generativelanguage\n // API. Probe with vertex-key on\n // generativelanguage.googleapis.com/v1beta/models\n // → HTTP 200, 50 Gemini models. gemini-openai is\n // now reachable; vertex-ai (which uses SA OAuth\n // against aiplatform.googleapis.com, not an API\n // key) remains independently in no-live-validation.\n //\n // Pre-previous late-pass carry-forward (kept for traceability):\n // + edenai, openrouter, cartesia, deepgram, elevenlabs (all +200s)\n //\n // Deltas from prior lates (also carried forward):\n // Lot B: writer, upstage, rekaai promoted from orphan→canonical→live.\n 'live-validation': [\n // Catalog (24 — +moonshot restored, +gemini-openai promoted this final pass;\n // bytez demoted to partial; heliconeai demoted to credentials-missing)\n 'aihubmix',\n 'aiml',\n // bytez REMOVED this pass — demoted to `partial`. The adapter extends\n // OpenAICompatibleHubAdapter (treats bytez as OAI-shape at /v1/models),\n // but the real Bytez API is at /models/v2/<hfmodel> with a native\n // request shape. The key itself works (HTTP 403 \"upgrade plan\" with\n // a legitimate tier-gate message proves auth), but the adapter wire\n // shape does not match. Reclassified as partial until the adapter is\n // rewritten against the native surface.\n 'cerebras',\n 'cometapi',\n 'edenai', // re-probed with alternate secret — 330 models\n 'featherless-ai',\n 'friendli',\n 'gemini-openai', // 2026-04-23 final pass — vertex-key added to GEMINI_API_KEY fallback chain → generativelanguage.googleapis.com 200, 50 Gemini models (prior state: 403 suspended google-key)\n // heliconeai REMOVED this pass — demoted to credentials-missing.\n // Both ailin-heliconeai-key AND ailin-heliconeai-api-key (the two known\n // GCP secret names for Helicone's AI gateway) are the literal 12-byte\n // string \"PLACEHOLDER\". The /models 200 from earlier probes was on a\n // public no-auth endpoint (Helicone's models registry is browsable);\n // /chat with \"PLACEHOLDER\" Bearer returned 401 as expected. This is a\n // placeholder-credential case, not a live integration.\n 'imagerouter',\n 'inworld',\n 'moonshot', // restored 2026-04-23 final pass — /v1/models on api.moonshot.ai → 200, 14 models (prior 401 was a probe-target bug against api.moonshot.cn)\n 'nanogpt',\n 'novita',\n 'nvidia',\n 'orqai',\n 'poe',\n 'rekaai',\n 'replicate',\n 'requesty',\n 'routeway',\n 'upstage',\n 'voyage',\n 'wandb',\n 'writer',\n // Switch (13 — +aws-bedrock and +vertex-ai both promoted this phase-final\n // pass; vertex-ai on generativelanguage.googleapis.com/v1beta/models)\n 'openai',\n 'anthropic',\n 'deepseek',\n 'mistral',\n 'xai',\n 'cohere',\n 'jina',\n 'openrouter',\n 'cartesia', // specialty audio, switch-only — /voices → 200, 751 voices\n 'deepgram', // specialty audio, switch-only — /v1/models → 200 (auth: Token not Bearer)\n 'elevenlabs', // specialty audio, switch-only — /v1/voices → 200 (not /v1/models)\n 'aws-bedrock', // 2026-04-23 final pass — Bearer-token auth on /foundation-models → 200, 163KB (AWS added Bearer support for read endpoints in 2025)\n 'vertex-ai', // 2026-04-23 phase-final — ailin-vertex-key (distinct from suspended google-api-key) → generativelanguage.googleapis.com/v1beta/models 200, 50 Gemini models. The prior \"ADC scope-limited\" diagnosis was wrong — the GCP secret itself is a usable API key.\n // ── Sublote D1 (2026-04-24) promotions — 10 providers, real chat proofs ──\n // Operator provisioned 17 secrets in GCP; each promoted provider below\n // was verified with GET /v1/models (200) AND POST /v1/chat/completions\n // (200 body with real completion) against the canonical adapter baseUrl.\n // Evidence in /tmp/subd1/bodies/<provider>.{models,chat,rechat}.body.\n 'groq', // llama-3.1-8b-instant chat 200 (592B)\n 'alibaba', // 2026-06: catalog entry added (oai-compat-pure, dashscope) closing 154 orphan DB rows — see providers.catalog.ts; was missing from the matrix (I1 invariant)\n 'deepinfra', // Meta-Llama-3.1-8B-Instruct chat 200 (455B); 65KB models catalog\n 'huggingface', // Llama-3.1-8B-Instruct chat 200 (668B) via router.huggingface.co\n 'cloudflare-workers-ai', // @cf/meta/llama-3-8b-instruct chat 200 (360B); /models 405 is expected (CF uses /ai/models/search, not /ai/v1/models)\n 'github-models', // openai/gpt-4o-mini chat 200 (1252B); models at /catalog/models not /v1/models\n 'perplexity', // sonar chat 200 (2992B); /models returns 404 by design — discovery is not exposed\n 'fireworks-ai', // accounts/fireworks/models/glm-5p1 chat 200 (447B) — first-pick llama-v3p1-8b was 404 (model-id deprecated), re-probed with real id from /models\n 'sambanova', // Meta-Llama-3.3-70B-Instruct chat 200 (910B) — first-pick 3.1 was 410 Gone (deprecated), re-probed with 3.3\n 'infermatic', // Qwen-Qwen3-30B-A3B chat 200 (476B) — LiteLLM Virtual Key with sk- prefix confirmed; key has model-scoped ACL (pinned in catalog note)\n 'heliconeai', // gpt-4o-mini chat 200 (1003B) — new ailin-heliconeai-api-key (sk-hel… 43B) replaces legacy ailin-heliconeai-key (literal PLACEHOLDER). Gateway routes to OpenAI with real completion.\n // LOTE O (2026-07-11) — apertis, inception. Promoted same-day from\n // `no-live-validation` (catalog landed 2026-07-10 with no probe; gcloud\n // ADC needed interactive reauth). Operator completed `gcloud auth login\n // --no-launch-browser` device-code flow; both keys re-fetched from GCP\n // and probed live:\n // apertis — GET /v1/models 200 (400+ models); POST /v1/chat/completions\n // with gpt-4o-mini 200 (\"Ping!\", 3 completion tokens). First probe\n // against gemini-2.0-flash-lite-001 returned 503 upstream_error —\n // a genuine transient upstream-vendor flake (auth was accepted,\n // not an integration bug); gpt-4o-mini retry on the same key/\n // endpoint confirmed the integration itself is sound.\n // inception — GET /v1/models 200 (mercury-2, mercury-edit-2); POST\n // /v1/chat/completions with mercury-2 + reasoning_effort:'instant'\n // 200 (\"pong\", finish_reason:'stop'). First probe with default\n // reasoning_effort (medium) + max_tokens:10 hit finish_reason:\n // 'length' with 0 completion_tokens (all 10 tokens consumed by\n // reasoning) — not a failure, just an under-provisioned test\n // budget for a reasoning-capable model.\n 'apertis',\n 'inception',\n // LOTE P (2026-07-11) — empiriolabs. Held back in LOTE O pending\n // validation (unfamiliar model lineup in docs); operator's real\n // ailin-empiriolabs-key probed live same day as the apertis/inception\n // promotion: GET /v1/models 200 (134+ real models — Kling, DeepSeek,\n // Zhipu, Qwen, confirmed genuine, not fabricated), POST\n // /v1/chat/completions 200 with deepseek-v4-flash (\"pong\", cost_usd\n // tracked). Went straight to live-validation — no no-live-validation\n // stopover needed since the probe happened before the catalog row\n // was ever added.\n 'empiriolabs',\n // LOTE Q (2026-07-12) — concentrate. PROMOTED same-day from\n // `no-live-validation` once the operator re-authenticated gcloud a\n // second time: POST /v1/chat/completions/ with gpt-4o-mini 200\n // (\"Pong\", 3 completion tokens). Response shows `model:\n // \"azure/gpt-4o-mini\"` — live confirmation that Concentrate really is\n // routing through an upstream vendor (Azure) under the hood, not\n // serving inference itself. `cost` breakdown present in the payload.\n 'concentrate',\n // LOTE S (2026-07-13) — perplexity-agent. Went straight to\n // live-validation (no no-live-validation stopover) — full execution\n // evidence gathered in the same session the catalog row was written:\n // GET /v1/models 200 (32 models); POST /v1/agent 200 for 5 of 6\n // requested third-party vendors — anthropic/claude-haiku-4-5,\n // openai/gpt-5.4-mini, google/gemini-3.5-flash, xai/grok-4.5,\n // perplexity/glm-5.2 (z.ai) — plus nvidia/nemotron-3-super-120b-a12b\n // (6/6 confirmed). perplexity/kimi-k2.7-code (Moonshot) is accepted by\n // model-id validation but hung with 0 response bytes on every attempt\n // (45-90s timeouts) — NOT counted as confirmed; only the 6 verified\n // vendors back this classification, not all 7 originally asked about.\n 'perplexity-agent',\n // LOTE R (2026-07-13/15) — fastrouter. PROMOTED from `no-live-validation`\n // once the operator re-authenticated gcloud a third time: POST\n // /api/v1/chat/completions with openai/gpt-5.4-mini 200\n // (`choices[0].message.content:\"pong\"`, `usage.provider:\"openai\"`,\n // real `usage.cost`). Live confirmation FastRouter genuinely routes\n // through the named upstream vendor, same pattern already seen for\n // apertis/concentrate.\n 'fastrouter',\n // LOTE U+1 (2026-07-29) — sakana-ai. Catalog row landed same-day (PR\n // #234) in 'upstream-suspended': GET /v1/models was already 200 (real\n // 5-model list) but the account had no active subscription, so every\n // POST /v1/chat/completions 429'd usage_limit_reached. The operator\n // activated pay-as-you-go billing on the account later the same day\n // and re-ran a full end-to-end verification directly against the live\n // API with the same GCP secret (ailin-sakana-ai-key): chat (fugu, plain\n // text), streaming (SSE chunks terminating in [DONE], content\n // assembled correctly), tool calls (correct function + arguments\n // extracted for a weather-lookup prompt), JSON mode\n // (response_format:{type:'json_object'} returned valid JSON), and\n // vision (a multimodal request with an inline image correctly\n // identified its content, with prompt_tokens jumping sharply versus\n // text-only calls — proof the image was actually processed) all\n // returned real HTTP 200 responses with correct, sensible content.\n // Promoted from upstream-suspended to live-validation; no other field\n // changed (auth, endpoint, and integration shape were already correct\n // at PR #234 merge — the only blocker was account-level billing).\n 'sakana-ai',\n // LOTE V (2026-07-29) — arcee, chutes. Both landed in upstream-suspended\n // on 2026-04-24 (Sublote D1) on real 402 \"insufficient credits/quota\"\n // responses. The operator topped up both accounts with a small balance\n // and a full live re-verification was run directly against each API:\n // arcee — GET /api/v1/models 200 (6 live models: trinity-large-\n // thinking, zai-org/glm-5.2, deepseek-ai/deepseek-v4-pro,\n // minimaxai/minimax-m3, moonshotai/kimi-k2.7-code,\n // moonshotai/kimi-k2.6; note the originally-tested 'trinity-mini'\n // is gone from discovery — the Trinity family was reorganized to\n // trinity-large-thinking). POST /v1/chat/completions with\n // trinity-large-thinking 200 (real content + usage, the prior 402\n // is gone) and stream:true 200 (proper SSE, delta.reasoning_content\n // chunks, terminating [DONE]). Reasoning content is exposed at\n // `message.reasoning_content` (streaming: `delta.reasoning_content`)\n // — NOT `message.reasoning` as the pre-existing catalog notes\n // claimed; that path was never actually live-verified until now.\n // chutes — GET /v1/models 200 (15 confidential-compute \"-TEE\" models;\n // this key's account is TEE-only by provisioning, not a plain-vs-\n // TEE choice). POST /v1/chat/completions 200 for BOTH\n // Qwen/Qwen3-32B-TEE (the originally-tested model, still valid) and\n // unsloth/Mistral-Nemo-Instruct-2407-TEE (a second model, clean\n // \"PONG\" completion) — zero 402s, zero balance errors on any call.\n // No integration/adapter changes were needed: baseUrl, bearer auth,\n // and the generic OAI-compat bridge all already matched.\n // Promoted from upstream-suspended to live-validation for both; no\n // adapter or catalog-shape changes required — the only blocker in\n // both cases was account-level billing, now resolved.\n 'arcee',\n 'chutes',\n ],\n\n // ── 1 canonical provider with adapter+catalog/switch but no probe ────\n // Reduced from 3 → 1 in the 2026-04-23 late pass:\n // - openrouter promoted to live-validation (probe-200)\n // - gemini-openai demoted to vendor-side-failure (api_key suspended)\n // Remaining: vertex-ai uses a deployment-bound auth flow (SA token\n // + region + project + /models path). The ADC present in this session\n // is scope-limited to Secret Manager; it cannot mint Vertex tokens\n // directly. Promotion requires ADC with roles/aiplatform.user.\n 'no-live-validation': [\n // Empty post phase-final: vertex-ai was promoted to live-validation\n // (ailin-vertex-key 200 on generativelanguage.googleapis.com/v1beta/models).\n // The prior rationale \"ADC scope-limit requires roles/aiplatform.user\"\n // was a misdiagnosis — the GCP secret is a usable API key, not an SA.\n //\n // LOTE O (2026-07-10): apertis, inception landed here with no probe\n // (gcloud ADC needed interactive reauth). PROMOTED to `live-validation`\n // 2026-07-11 once the operator completed device-code reauth and a real\n // probe confirmed both — see the `live-validation` bucket entry above\n // for full evidence. Bucket empty again.\n //\n // LOTE Q (2026-07-12): concentrate landed here with discovery proven\n // (unauthenticated) but execution unprobed (gcloud ADC expired before\n // the key could be fetched). PROMOTED to `live-validation` same day\n // once the operator re-authenticated — see the `live-validation`\n // bucket entry above for full evidence. Bucket empty again.\n //\n // LOTE R (2026-07-13): fastrouter landed here with discovery proven\n // (unauthenticated) but execution unprobed (gcloud ADC expired before\n // the key could be fetched). PROMOTED to `live-validation` 2026-07-15\n // once the operator re-authenticated — see the `live-validation`\n // bucket entry above for full evidence. Bucket empty again.\n //\n // LOTE T (2026-07-13): ailin — self-referential meta-gateway (this\n // engine, as a client of api.ailin.one). No AILIN_API_KEY provisioned\n // this session, so unlike O/Q there is no promotion to report yet.\n // Wiring verified contract-only against api.ailin.one's own\n // openapi-spec.yaml (chat/embeddings/images/audio confirmed OpenAI-\n // compatible shape at the generic hub's default paths); GET /v1/models\n // confirmed to return a richer native shape the generic fetcher only\n // partly understands. See the catalog entry's own notes for the\n // discovery-shape follow-up. Promote once a real key is provisioned\n // and a live probe confirms chat/completions.\n 'ailin',\n ],\n\n // ── 6 partial ────────────────────────────────────────────────────────\n // `partial` captures the SHAPE \"at least one surface is live-proven,\n // at least one surface is gated\". Current instances split into two\n // structural sub-shapes:\n //\n // (A) Adapter wire-shape mismatch — key is valid, but the shipped\n // adapter targets the wrong surface.\n //\n // bytez — BytezAdapter extends OpenAICompatibleHubAdapter (expects\n // /v1/models OAI-shape), but the real Bytez API lives at\n // /models/v2/<hfmodel> with a native request shape. Key is valid\n // (HTTP 403 \"upgrade plan\" = tier-gate proves auth). Partial until\n // the adapter is rewritten against the native surface or until\n // Bytez exposes an OAI-compat surface.\n //\n // (B) Asymmetric auth — /models public, /chat credential-gated.\n // Proven via no-auth probe against the canonical baseUrl.\n //\n // venice (Sublote A 2026-04-23) — /api/v1/models HTTP 200 PUBLICLY\n // (auth header actively ignored: invalid Bearer → same 200 body;\n // 72 models). /api/v1/chat/completions → HTTP 402 x402 payment-\n // required body (USDC on Base eip155:8453) without Bearer, HTTP\n // 401 \"Authentication failed\" with invalid Bearer. Discovery\n // live-proven; execution credential-gated.\n //\n // atlascloud (Sublote B 2026-04-23) — api.atlascloud.ai/v1/models\n // HTTP 200 PUBLICLY (auth ignored, 107 models, custom OAI-adjacent\n // wrapper {code:200,msg:\"succeed\",data:[...]}). /chat gated.\n //\n // avian (Sublote B 2026-04-23) — api.avian.io/v1/models HTTP 200\n // PUBLICLY (auth ignored, 6 models, pure OAI shape). /chat gated.\n //\n // phala (Sublote B 2026-04-23) — api.redpill.ai/v1/models HTTP 200\n // PUBLICLY (auth ignored, 76 models including Anthropic Claude\n // Sonnet 4.5 via the Phala TEE confidential-compute routing).\n // /chat gated. Note: phala is a CONFIDENTIAL-COMPUTE router on\n // Phala Network SGX TEEs; the public /models surface exposes the\n // set of models available via the TEE routing layer.\n //\n // mancer (Sublote B 2026-04-23) — neuro.mancer.tech/oai/v1/models\n // HTTP 200 PUBLICLY (9 LLaMA-based fiction/RP models). Asymmetric\n // detail: unlike venice/atlascloud/avian/phala, mancer VALIDATES\n // the Authorization header when present (invalid Bearer → 401)\n // but serves discovery when NO header is sent. Effectively the\n // same operational signal: a client can enumerate models without\n // a credential. /chat gated.\n //\n // Promotion path out of `partial` for all (B)-class providers: a\n // 200 on /chat/completions (or the vendor's execution surface) with\n // a real provisioned credential → live-validation.\n partial: [\n 'bytez', // switch — wire-shape mismatch (OAI-compat adapter vs. vendor's /models/v2 native shape); tier-gated 403 on real path with valid key\n 'venice', // catalog — asymmetric auth: /models 200 public (auth ignored), /chat 401/402 gated. Sublote A 2026-04-23\n 'atlascloud', // catalog — /models 200 public (auth ignored, 107 models, custom {code,msg,data} wrapper). Sublote B 2026-04-23\n 'avian', // catalog — /models 200 public (auth ignored, 6 models, pure OAI shape). Sublote B 2026-04-23\n 'mancer', // catalog — /models 200 public when no header sent (9 models); VALIDATES invalid Bearer → 401 (asymmetric). Sublote B 2026-04-23\n 'phala', // catalog — /models 200 public (auth ignored, 76 models via Phala SGX TEE confidential-compute routing on redpill.ai). Sublote B 2026-04-23\n ],\n\n // ── 54 canonical providers with no credential reachable this session ─\n // Breakdown: 47 catalog (39 hosted + 8 self-hosted) + 7 switch\n // (1 hosted + 5 self-hosted-local-* + 1 generic self-hosted adapter).\n // Pre-LOTE-M: 46 (39 catalog + 7 switch). LOTE M added 13 catalog rows;\n // Sublote A (2026-04-23) promoted venice → partial and Sublote B\n // (2026-04-23) promoted atlascloud, avian, mancer, phala → partial on\n // proven /models public HTTP 200 probes. Net LOTE M credentials-missing\n // contribution: +8 (13 added − 5 promoted to partial).\n //\n // 2026-04-23 LOTE M (complement lot) initial deltas — 13 providers\n // added; 8 remain in credentials-missing, 5 in partial post-Sublote-A+B:\n //\n // + arcee, atlascloud, avian, gmi, infermatic, inflection, mancer,\n // phala, relace, siliconflow, stepfun\n // → secret-absent. Catalog rows added this lot; no GCP secret\n // under any known alias. Operator must provision\n // <PROVIDER>_API_KEY to unlock live-validation.\n //\n // + qianfan (Baidu ERNIE promotion)\n // → auth-incomplete. GCP holds ailin-baidu-{key,secret,base-url}\n // (legacy v1 AK+SK OAuth material) but those secrets are the\n // 12-byte string \"PLACEHOLDER\" AND the canonical v2 runtime\n // path uses a DIFFERENT key format (bce-v3/... bearer). A\n // real QIANFAN_API_KEY must be provisioned alongside real\n // baidu-{key,secret}. Analogous to aws-sagemaker's\n // classification reasoning.\n //\n // + venice — landed in credentials-missing at LOTE M close, then\n // MOVED to `partial` on 2026-04-23 Sublote A after /api/v1/models\n // was proven HTTP 200 public (no auth required, 72 models).\n // Only 1 of 2 canonical surfaces is gated; discovery is live.\n //\n // + atlascloud, avian, mancer, phala — all landed in credentials-\n // missing at LOTE M close, then MOVED to `partial` on 2026-04-23\n // Sublote B after /models/v1/models was proven HTTP 200 public\n // (107 + 6 + 9 + 76 models respectively). Each provider has one\n // canonical surface live-proven (discovery) and one gated\n // (execution). See the `partial` bucket comment and the Sublote B\n // superseded-claim #7 in NON_CANONICAL_HISTORICAL_CLAIMS for the\n // probe evidence (16 probes × 12 providers = 192 probe records).\n //\n // Previous (pre-LOTE-M) total 46 → now 54 (+8 net after Sublote A+B).\n //\n // 2026-04-23 SUBLOTE A probe-evidence (operational verification of\n // the 4 Grupo-A providers — qianfan, venice, siliconflow, stepfun.\n // ONE bucket move occurred: venice → partial on proven live discovery.\n // The other three confirmed endpoint/base-URL correctness against\n // live HEAD but remain gated by missing credentials):\n //\n // · qianfan — v2 endpoint https://qianfan.baidubce.com/v2 ALIVE;\n // 401 invalid_iam_token on invalid bearer (BCE-shape error:\n // {error:{code,message,type},id:'as-...'}). Base URL + authScheme\n // confirmed. GCP legacy v1 secrets ailin-baidu-{key,secret,\n // base-url} re-read this session: all three still literal\n // \"PLACEHOLDER\". No ailin-qianfan-* or ailin-baidu-qianfan-*\n // secret exists. Stays credentials-missing / auth-incomplete.\n // · venice — /api/v1/models returns HTTP 200 PUBLICLY (no auth\n // header required; with an INVALID Bearer it also returns 200,\n // confirming auth is ignored on /models) with 72 text models\n // in OAI-conformant shape ({data:[{id,object,owned_by,...}]});\n // /api/v1/chat/completions returns HTTP 402 with x402 crypto-\n // payment protocol body (USDC on Base eip155:8453,\n // 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913) when no Bearer\n // header is sent, HTTP 401 {\"error\":\"Authentication failed\"}\n // when an invalid Bearer is sent. Asymmetric auth: one canonical\n // surface is live-proven, one is gated. PROMOTED credentials-\n // missing → partial (see `partial` bucket above for rationale).\n // No ailin-venice-* secret under any alias in GCP.\n // · siliconflow — api.siliconflow.cn/v1 ALIVE; 401 with body as\n // a bare JSON string \"Invalid token\" (justifies integrationClass\n // 'oai-compat-quirks' — non-object error body). PRC-only endpoint\n // confirmed: no .com equivalent probed successfully. No ailin-\n // siliconflow-* secret. Stays credentials-missing / secret-absent.\n // · stepfun — BOTH api.stepfun.com (canonical) and api.stepfun.ai\n // (CDN alt; Alibaba Cloud cookies + x-router-id header observed)\n // return identical OAI-compat error shape {\"error\":{\"message\":\n // \"Incorrect API key provided\",\"type\":\"invalid_api_key\"}} on\n // invalid bearer. .com preserved as baseUrl (.ai remains an\n // acceptable alt via notes). No ailin-stepfun-* secret. Stays\n // credentials-missing / secret-absent.\n //\n // Net matrix delta from Sublote A: ONE bucket move (venice →\n // partial). 3/4 remain credentials-missing; 1/4 promoted on proven\n // public discovery. Sublote A's primary value is OPERATIONAL proof-\n // of-reachability — full live-validation of the execution surfaces\n // requires secret provisioning (deferred to next sublote).\n //\n // Sub-classification (applied to the credentials-missing bucket only):\n // secret-absent — no GCP secret of any known name or alias\n // placeholder — secret exists but value == \"PLACEHOLDER\"\n // endpoint-absent — key exists, no public base URL defined\n // self-hosted-unreachable — expected to run locally; no reachable\n // endpoint from the probe environment\n // auth-incomplete — multi-piece auth where one component is absent\n // operator-action-required — vendor requires out-of-band onboarding\n // (AWS IAM role, GCP SA, enterprise SSO)\n // The sub-classification is documented in\n // CREDENTIALS_MISSING_SUBCLASS below (not a matrix column — keeps the\n // matrix structurally simple; the sub-class is a refinement layer).\n 'credentials-missing': [\n // ── Post Sublote D1 (2026-04-24) ────────────────────────────────────\n // Operator provisioned 17 GCP secrets on 2026-04-24. Of the 14 providers\n // that left this bucket:\n // 10 → live-validation (real chat-200 with provisioned credential)\n // groq, deepinfra, huggingface, cloudflare-workers-ai,\n // github-models, perplexity, fireworks-ai, sambanova,\n // infermatic, heliconeai\n // 4 → upstream-suspended (auth accepted, credit/service gated)\n // anyscale (permanent vendor-side shutdown Aug 2024),\n // arcee/chutes/hyperbolic (402 \"insufficient funds\" with\n // structurally-valid credential — same shape as ai302/palabraai)\n // 3 STAY here but move sub-class secret-absent → auth-incomplete\n // (togetherai, siliconflow, stepfun — keys exist in vault,\n // upstream rejects them with OAI-shape \"invalid api key\";\n // probable format mismatch, operator must issue keys in the\n // vendor's canonical prefix. See CREDENTIALS_MISSING_SUBCLASS).\n // 1 EXPECTED but NOT provisioned: gmi (operator's 2026-04-24\n // announcement listed gmi, but GCP Secret Manager re-scan\n // found no ailin-gmi-* or ailin-gmicloud-* under any alias —\n // vault state disagrees with operator claim. gmi stays\n // secret-absent until the actual secret lands.)\n // Net bucket delta: 54 → 40 (−14). No new adds.\n // Catalog — hosted (20 remaining: 17 pre-LOTE-M kept + 3 LOTE M kept)\n 'bfl',\n 'databricks',\n 'lambda-ai',\n 'minimax',\n 'morph',\n 'nebius',\n 'nscale',\n 'recraft',\n 'runwayml',\n 'scaleway',\n 'synthetic',\n 'togetherai', // D1 2026-04-24 — key provisioned (ailin-togetherai-api-key, key_CY… 25B) but /v1/chat returns OpenAI-shape 401 \"Invalid API key\". key_CY prefix is non-canonical for Together AI (normally tok_/hex). Probable format mismatch; operator must re-issue. Sub-class: auth-incomplete.\n 'v0',\n 'vercel-ai-gateway',\n 'volcano',\n 'watsonx',\n 'xiaomi-mimo',\n 'zai',\n // LOTE M 2026-04-23 survivors post-D1 (3 remain of original 7)\n 'gmi', // D1 2026-04-24 — operator claimed provisioned but GCP scan 2026-04-24 found NO matching secret; vault state disagrees. Stays secret-absent until secret actually lands.\n 'inflection',\n 'relace',\n 'siliconflow', // D1 2026-04-24 — key provisioned (ailin-siliconflow-api-key, sk-hhc… 51B) but /v1/chat returns bare JSON \"Api key is invalid\" (oai-compat-quirks shape). Sub-class: auth-incomplete.\n 'stepfun', // D1 2026-04-24 — key provisioned (ailin-stepfun-api-key, 65B) but /v1/chat returns OAI-shape 401 \"Incorrect API key provided\". Sub-class: auth-incomplete.\n // 1 auth-incomplete carried from LOTE M close: qianfan has legacy v1\n // baidu-{key,secret} secrets in GCP but all are \"PLACEHOLDER\"; the v2\n // canonical runtime path also requires a bce-v3/... QIANFAN_API_KEY\n // that is not provisioned.\n 'qianfan',\n // Catalog — self-hosted (8). Localhost endpoints are unreachable from\n // the probe environment; no key is needed but reachability is absent.\n 'ollama',\n 'local-llama',\n 'local-kobold',\n 'local-embeddings',\n 'vllm',\n 'lm-studio',\n 'xinference',\n 'triton',\n // Switch — hosted (1 — AWS pair reduced to just aws-sagemaker after\n // aws-bedrock was promoted this pass)\n 'aws-sagemaker',\n // Catalog hosted: azure-openai (was in vendor-side-failure — re-probed\n // this phase-final confirms all three secrets (api-key, endpoint,\n // deployment) are the literal 12-byte string \"PLACEHOLDER\". This is a\n // secret-never-provisioned case, not a vendor-side failure.)\n 'azure-openai',\n // Switch — self-hosted local-* (5)\n 'local-ocr',\n 'local-docling',\n 'local-nllb',\n 'local-cosyvoice',\n 'local-piper',\n // Switch — generic self-hosted adapter (1)\n 'self-hosted',\n ],\n\n // ── 2 vendor-side / config failures (not integration bugs) ───────────\n // 2026-04-23 final pass deltas:\n // − gemini-openai (promoted to live-validation once GEMINI_API_KEY\n // fallback chain was extended with `vertex-key`, which is NOT\n // suspended and IS valid against generativelanguage.googleapis.com)\n //\n // Remaining:\n // · google — 403 CONSUMER_SUSPENDED on the google-key specifically.\n // The switch case adapter reads GOOGLE_API_KEY (different env\n // var than GEMINI_API_KEY), and the loader does NOT currently\n // fall back to vertex-key for GOOGLE_API_KEY. Fixing this is a\n // separate deliberate decision: `google` is the native\n // GoogleGenerativeAI adapter, and routing traffic through\n // gemini-openai (OAI-compat) may be preferred anyway. Kept as\n // vendor-side-failure pending that decision.\n // · azure-openai — endpoint secret literal \"PLACEHOLDER\" (never\n // provisioned). Unchanged this pass.\n 'vendor-side-failure': [\n 'google', // switch — 403 CONSUMER_SUSPENDED on GOOGLE_API_KEY (google-key); separate from gemini-openai which now uses vertex-key fallback\n // azure-openai moved to credentials-missing (placeholder secrets)\n ],\n\n // ── 3 upstream suspended (auth valid, credit exhausted / service gated) ─\n // jina classified as 'live-validation' because /v1/models returned\n // 200; the /v1/embeddings 403 \"Insufficient balance\" is a per-surface\n // state, not a whole-provider classification.\n //\n // 2026-07-29: −sakana-ai, promoted to live-validation. It landed here\n // on 2026-07-29 (LOTE U, same day as catalog onboarding in PR #234)\n // because the account had no active subscription and every\n // chat/completions call 429'd usage_limit_reached. Later the same day\n // the operator activated pay-as-you-go billing and re-verified\n // end-to-end directly against the live API: chat, streaming, tool\n // calls, JSON mode, and vision all returned real HTTP 200 responses\n // with correct content. See the 'live-validation' bucket entry below\n // for the full writeup.\n //\n // 2026-07-29: −arcee, −chutes, promoted to live-validation (LOTE V).\n // Both landed here 2026-04-24 (Sublote D1) on real 402 zero-balance\n // responses. The operator topped up both accounts with a small\n // balance and a fresh live re-verification confirmed genuine, error-\n // free chat completions (and streaming, for arcee) on both. See the\n // 'live-validation' bucket entry below for the full per-provider\n // writeup.\n //\n // 2026-04-23 final pass: +palabraai. The ClientId/ClientSecret header\n // pair (GCP secrets ailin-palabraai-id + ailin-palabraai-key) was\n // accepted by api.palabra.ai/session-storage/session; the server\n // rejected the action with an explicit \"Insufficient balance\"\n // (error_code 100050, documented in their public error code table).\n // Authentication is complete, balance is zero → upstream-suspended,\n // NOT credentials-missing.\n //\n // 2026-04-24 Sublote D1: +4 (anyscale, arcee, chutes, hyperbolic).\n // Three distinct sub-shapes land in this bucket this pass:\n //\n // (a) Permanent vendor-side shutdown — anyscale: the Anyscale\n // Endpoints multi-tenant LLM API was discontinued 2024-08-01\n // (\"available exclusively through the fully Hosted Anyscale\n // Platform\"). Probe of api.endpoints.anyscale.com returns the\n // HTML shutdown notice. Auth cannot be tested because the\n // endpoint itself is gone. Classified upstream-suspended\n // because the vendor has permanently ceased serving public\n // credentials; defunct-unreachable is reserved for credential-\n // state rotations, not service-level shutdowns.\n //\n // (b) Credit-exhausted 402 — arcee/chutes/hyperbolic: each provider\n // accepted the provisioned API key (parsed Authorization,\n // routed to the correct account) but returned an explicit\n // zero-balance error:\n // arcee — {\"detail\":\"Insufficient credits. Required: 0.000037, Available: 0.000000\"}\n // chutes — {\"detail\":{\"message\":\"Quota exceeded and account balance is $0.0, please pay with fiat or send tao to...\"}}\n // hyperbolic — {\"detail\":\"Insufficient funds, please see https://docs.hyperbolic.xyz/docs/hyperbolic-pricing\"}\n // Identical operational shape to ai302 and palabraai:\n // authentication complete, balance zero, execution blocked.\n // Promotion path: operator tops up credit → re-probe → live.\n 'upstream-suspended': [\n 'ai302', // 401 on /v1/models — prior session recorded \"Insufficient account balance\"; endpoint alive, tier-gated\n 'palabraai', // switch — /session-storage/session POST returned 403 \"Insufficient balance\" (code 100050). Auth accepted.\n 'anyscale', // D1 2026-04-24 — endpoint returns HTML shutdown notice \"Effective August 1, 2024 Anyscale Endpoints API is available exclusively through the fully Hosted Anyscale Platform. Multi-tenant access to LLM models has been removed.\" ailin-anyscale-api-key provisioned (236B, aph0_C…) but cannot be exercised.\n 'hyperbolic', // D1 2026-04-24 — /v1/chat returned 402 {\"detail\":\"Insufficient funds, please see https://docs.hyperbolic.xyz/docs/hyperbolic-pricing\"}. Auth accepted.\n ],\n\n // ── 0 credentials in cache invalidated ───────────────────────────────\n // 2026-04-23 final pass: moonshot restored to live-validation. The\n // previous \"defunct\" verdict came from probing api.moonshot.cn, but\n // the canonical adapter uses api.moonshot.ai/v1 (verified in\n // moonshot-adapter.ts::DEFAULT_BASE_URL). Re-probe against .ai → 200,\n // 14 models. Bucket now empty.\n 'defunct-unreachable': [],\n\n // ── 3 catalog-only inventory (intentional non-execution) ─────────────\n 'catalog-only-inventory': ['sap', 'snowflake', 'topaz'],\n\n // ── 0 additional switch-only-legitimate ──────────────────────────────\n // NOTE: first-party natives (openai, anthropic, …) and specialty\n // audio (deepgram, cartesia, elevenlabs, palabraai) are switch-only\n // BY LEGITIMATE design, but their current operational state is what\n // determines their bucket. An openai that answered 200 this session\n // is 'live-validation', not 'switch-only-legitimate'. This bucket\n // exists for switch cases that have no other operational evidence\n // AND are legitimate switch-only (not migration candidates). Today\n // all such cases are covered by the buckets above.\n 'switch-only-legitimate': [],\n\n // ── 0 canonical providers classified as 'not-eligible' ───────────────\n // By definition canonical = catalog ∪ switch; all 103 members of\n // that union are eligible. The 'not-eligible' field below lists\n // orphans that are NOT in the canonical 103.\n 'not-eligible': [],\n};\n\n/**\n * Sub-classification of the `credentials-missing` bucket — refines the\n * operator's next action for each provider. This is NOT a matrix column\n * (the matrix stays structurally simple with 10 buckets); it is a\n * refinement layer that the CLI tooling, admin dashboard, and backlog\n * reports consume to decide *what kind* of work is needed to unblock.\n *\n * ## Sub-class semantics\n *\n * - `secret-absent` — no GCP secret under any known name/alias\n * - `placeholder` — secret exists but value === \"PLACEHOLDER\"\n * - `endpoint-absent` — key exists but no public base URL\n * - `self-hosted-unreachable` — expected local runtime; no endpoint reachable\n * - `auth-incomplete` — multi-piece auth where one component is absent\n * - `operator-action-required` — vendor requires out-of-band onboarding\n * (AWS IAM role, GCP SA, enterprise SSO)\n *\n * ## Invariant\n *\n * Every provider in CONSOLIDATION_MATRIX['credentials-missing'] MUST\n * appear in exactly one sub-class here. The total of all sub-classes\n * equals the credentials-missing bucket size.\n */\nexport const CREDENTIALS_MISSING_SUBCLASS: Record<string, readonly string[]> = {\n // Catalog hosted — no GCP secret under any known alias\n // pre-LOTE-M count = 29\n // LOTE M 2026-04-23 initial additions = 12 (arcee, atlascloud, avian,\n // gmi, infermatic, inflection, mancer, phala, relace, siliconflow,\n // stepfun, venice)\n // Sublote A 2026-04-23 refinement = −1 (venice → partial).\n // Sublote B 2026-04-23 refinement = −4 (atlascloud, avian, mancer,\n // phala all promoted to `partial` after HTTP 200 public /models\n // probe against canonical baseUrl).\n // Sublote D1 2026-04-24 refinement = −16:\n // −13 out of credentials-missing entirely (9 secret-absent →\n // live-validation, 4 secret-absent → upstream-suspended).\n // −3 same-bucket move (secret-absent → auth-incomplete for\n // togetherai/siliconflow/stepfun — keys provisioned but\n // upstream rejects the format).\n // Post-D1 secret-absent total = 36 − 16 = 20\n // (Of those 20: 17 pre-LOTE-M survivors + 3 LOTE-M survivors.\n // gmi is notable — operator announced provisioning on 2026-04-24\n // but GCP re-scan found no matching secret, so gmi stays here\n // until the actual secret lands.)\n 'secret-absent': [\n // Pre-LOTE-M survivors (17 of original 29; 12 promoted by D1)\n 'bfl',\n 'databricks',\n 'lambda-ai',\n 'minimax',\n 'morph',\n 'nebius',\n 'nscale',\n 'recraft',\n 'runwayml',\n 'scaleway',\n 'synthetic',\n 'v0',\n 'vercel-ai-gateway',\n 'volcano',\n 'watsonx',\n 'xiaomi-mimo',\n 'zai',\n // LOTE M 2026-04-23 survivors (3 of original 7 — infermatic promoted\n // to live by D1; siliconflow/stepfun moved to auth-incomplete by D1)\n 'gmi', // operator announced 2026-04-24 provisioning but GCP scan found no ailin-gmi-* or ailin-gmicloud-* under any alias — vault state disagrees with announcement.\n 'inflection',\n 'relace',\n ],\n // Secret exists but value === literal \"PLACEHOLDER\" (1)\n // pre-D1 count = 2 (heliconeai + azure-openai)\n // D1 2026-04-24 refinement = −1 (heliconeai → live-validation:\n // operator provisioned ailin-heliconeai-api-key (sk-hel… 43B, real\n // Helicone Proxy Virtual Key) which SUPERSEDES the legacy\n // ailin-heliconeai-key (11B literal \"PLACEHOLDER\" that caused the\n // prior mis-classification). Gateway routes to OpenAI with a real\n // HTTP 200 completion body.)\n placeholder: [\n 'azure-openai', // ailin-azure-openai-api-key + -endpoint + -deployment all \"PLACEHOLDER\"\n ],\n // Local runtime expected; no reachable endpoint from probe environment (13)\n 'self-hosted-unreachable': [\n 'ollama',\n 'local-llama',\n 'local-kobold',\n 'local-embeddings',\n 'vllm',\n 'lm-studio',\n 'xinference',\n 'triton',\n 'local-ocr',\n 'local-docling',\n 'local-nllb',\n 'local-cosyvoice',\n 'local-piper',\n 'self-hosted', // generic self-hosted adapter\n ],\n // Multi-piece auth where one component is absent (2 post-LOTE-M)\n //\n // aws-sagemaker: requires classic SigV4 secret access key (40-char base64),\n // but GCP only holds ABSK-prefixed Bedrock API bearer tokens. The\n // ailin-aws-key-id (AKIA... classic access key id) has no matching\n // ailin-aws-secret-access-key pair in the vault.\n //\n // qianfan (LOTE M 2026-04-23): Baidu Qianfan exposes two auth paths:\n // v1 legacy — AK+SK → OAuth access_token → query-param (backed by\n // GCP secrets ailin-baidu-{key,secret,base-url}, BUT all three are\n // literal \"PLACEHOLDER\" strings at HEAD so they cannot drive\n // the v1 OAuth flow either).\n // v2 canonical — bce-v3/... bearer key against qianfan.baidubce.com/v2\n // (no ailin-qianfan-* secret exists in GCP; a new QIANFAN_API_KEY\n // must be provisioned in the bce-v3 format — this is distinct\n // from the legacy baidu-key even if both are eventually populated).\n // Both paths are currently un-executable: v1 has placeholder secrets,\n // v2 has no secret at all. Promotion to live-validation requires\n // EITHER (a) real baidu-key + baidu-secret for the legacy adapter\n // that this repo does not ship, OR (b) a new QIANFAN_API_KEY for the\n // v2 OAI-compat catalog row that this lot added.\n //\n // togetherai (Sublote D1 2026-04-24): ailin-togetherai-api-key exists\n // in GCP (25B, prefix \"key_CY…\") but POST /v1/chat/completions returns\n // HTTP 401 {\"error\":{\"message\":\"Invalid API key provided. You can find\n // your API key at https://api.together.ai/settings/api-keys.\"...}}.\n // The \"key_\" prefix is non-canonical for Together AI (whose keys are\n // typically tok_ or raw 64-char hex). Probable format mismatch or a\n // stale/revoked token; operator must re-issue from the Together\n // dashboard. Auth-incomplete because the VAULT component exists but\n // the credential's current state prevents upstream validation.\n //\n // siliconflow (Sublote D1 2026-04-24): ailin-siliconflow-api-key exists\n // (51B, prefix \"sk-hhc…\") but POST /v1/chat/completions returns HTTP\n // 401 with body as a bare JSON string \"Api key is invalid\" (15 bytes,\n // oai-compat-quirks shape). The sk- prefix looks OpenAI-style rather\n // than SiliconFlow's standard format; probable format mismatch or\n // account-scope issue. Operator must re-issue.\n //\n // stepfun (Sublote D1 2026-04-24): ailin-stepfun-api-key exists (65B)\n // but POST /v1/chat/completions returns HTTP 401 with OAI-shape body\n // {\"error\":{\"message\":\"Incorrect API key provided\",\"type\":\"invalid_api_key\"}}.\n // Endpoint confirmed alive (verbatim OpenAI error shape). Operator\n // must verify the key is correctly associated with the StepFun\n // account and has the required access tier.\n 'auth-incomplete': [\n 'aws-sagemaker',\n 'qianfan',\n 'togetherai', // D1 2026-04-24 — key exists, upstream rejects (\"Invalid API key\")\n 'siliconflow', // D1 2026-04-24 — key exists, upstream rejects (\"Api key is invalid\")\n 'stepfun', // D1 2026-04-24 — key exists, upstream rejects (\"Incorrect API key provided\")\n ],\n} as const;\n\n/**\n * Discovery-compliance taxonomy — orthogonal to the operational matrix above.\n *\n * ## Why this exists (2026-04-27 — SOTA dynamic-discovery escalation)\n *\n * The operational `CONSOLIDATION_MATRIX` answers \"is this provider reachable\n * right now?\" — a credentialing/runtime question. It does NOT answer the\n * separate question: \"does this provider's listing in /v1/models reflect a\n * machine-readable, official source-of-truth, or is it shaped by hand-curated\n * catalog rows we baked into the binary?\"\n *\n * That second question is the SOTA dynamic-discovery contract. As of\n * 2026-04-27 the operator escalated the policy: ANY provider whose runtime\n * inventory in `/v1/models` is sourced from `staticModels`, the deprecated\n * static model-catalog stub (config/model-catalog.ts), frontier-candidate\n * seed arrays, hardcoded ID arrays in heuristics, or any in-repo seed list\n * is **non-compliant** with the dynamic-discovery contract — regardless of\n * whether the provider itself happens to also expose a real /models endpoint\n * that we just have not wired up yet.\n * (The symbol names are deliberately not written out here: the no-static-\n * model-catalog-fallback guard test scans every production source for them.)\n *\n * ## Revocation of prior completion buckets (binding 2026-04-27)\n *\n * The following INFORMAL labels — used in earlier audit reports as\n * \"acceptable completion states\" — are HEREBY REVOKED:\n *\n * - `static-catalog-required` — no longer a valid completion state\n * - `catalog-inferred` — no longer a valid completion state\n * - `execution-only` w/ inventory — `integrationMode: 'execution-only'`\n * remains a valid mode, but combining\n * it with `staticModels:` to fabricate\n * inventory is now non-compliant\n * - `no-discovery accepted` — no provider can reach a \"discovery\n * accepted\" completion state if its\n * /v1/models inventory is hardcoded\n *\n * The `catalog-only-inventory` operational bucket above (sap, snowflake,\n * topaz) DOES remain a structurally valid CLASSIFICATION — those providers\n * are intentionally non-executing — but the absence of a machine-readable\n * model surface means each of them ALSO appears in\n * `non-compliant-no-machine-readable-discovery` or\n * `not-applicable-non-model-surface` here, depending on whether the\n * provider's surface is in the LLM ontology at all.\n *\n * ## Bucket semantics (9 buckets, mutually exclusive over the canonical set)\n *\n * compliant-dynamic-discovery\n * The provider exposes a machine-readable /models endpoint (or a\n * close equivalent — HuggingFace's catalog API, GitHub Models\n * /catalog/models, Cloudflare's /ai/models/search, OpenAI /v1/models,\n * etc.) AND a registered fetcher in\n * `central-model-discovery-service.ts` materialises that into our DB\n * at runtime. The /v1/models response for these providers reflects\n * truly upstream-discovered IDs.\n *\n * compliant-deployment-discovery\n * The provider's \"models\" are deployment-scoped — Vertex AI,\n * AWS Bedrock, AWS SageMaker — and discovery happens via a\n * deployment-listing API at runtime. There is no static catalog of\n * model IDs; the runtime materialises only the deployments the\n * operator has provisioned in their cloud account.\n *\n * compliant-machine-readable-official-catalog\n * The provider does not publish a per-account /models endpoint, but\n * ships an OFFICIAL machine-readable catalog (a versioned JSON file\n * under their own domain, OpenAPI metadata in their docs, etc.) that\n * we can fetch at runtime. Currently EMPTY; reserved for future\n * promotions when a vendor publishes such a catalog without a\n * /models endpoint of its own.\n *\n * pinnedFallback-by-design\n * The provider has `pinnedFallback.reason === 'no-list-endpoint'` —\n * the vendor's API genuinely doesn't expose a model-listing surface\n * (perplexity's docs say so explicitly; v0/xiaomi-mimo probes return\n * 404/HTML respectively; image-gen specialty providers like recraft\n * and bfl have execution-only surfaces by design). The catalog row's\n * pinnedFallback IS the source of truth and is treated as such by\n * the runtime. CLASSIFIED AS COMPLIANT because there is nothing\n * \"non-compliant\" about a vendor choosing not to publish /models —\n * the operator-curated list is the inventory contract, not a\n * placeholder. Distinct from `non-compliant-hardcoded-inventory`\n * (which tracks debt: a parser we owe but haven't written) and\n * `not-applicable-non-model-surface` (which tracks providers with\n * no model ontology at all, like cartesia/elevenlabs voices).\n *\n * non-compliant-hardcoded-inventory\n * The provider has `pinnedFallback.reason === 'proprietary-schema'`\n * (or legacy `staticModels`) — the vendor DOES expose a discovery\n * surface but it's non-OAI-shaped and our adapter hasn't been\n * written yet. The pinnedFallback list is an interim crutch.\n * Promotion path: write the parser, drop the crutch, move to\n * `compliant-dynamic-discovery`. This bucket is real engineering\n * debt that should shrink over time.\n *\n * non-compliant-no-machine-readable-discovery\n * No `staticModels`, no upstream surface, no inventory. The provider\n * is canonical (operator wants it tracked) but has nothing to list\n * in /v1/models. Promotion path: vendor publishes a discovery API.\n *\n * non-compliant-runtime-not-materialized\n * The provider DOES expose a machine-readable surface (we have probe\n * evidence or vendor docs prove its existence) but our runtime is\n * currently NOT calling it — usually because the response shape needs\n * a custom transform that has not been written yet. The `staticModels`\n * array, if any, is an interim crutch. Promotion path: write the\n * transform, drop the static array, register the fetcher.\n *\n * not-applicable-non-model-surface\n * The provider does not have \"models\" in the LLM sense — its API\n * surface is voices, agents, scenes, image-enhancement filters, or\n * other artefacts that don't map to the chat/completion ontology.\n * Specialty audio (cartesia/deepgram/elevenlabs/palabraai) and\n * image-enhancement (topaz) are the canonical examples. These\n * providers are NOT non-compliant; they are operating in a different\n * category entirely.\n *\n * self-hosted-runtime-dependent\n * The provider is operator-deployed (ollama, vLLM, lm-studio, generic\n * self-hosted, every local-* adapter). Discovery is a function of\n * what the operator has loaded into THEIR runtime. This is a\n * compliance-irrelevant case, not a non-compliance — discovery for\n * these providers is genuinely runtime-bound and should not pretend\n * to be otherwise.\n *\n * ## Invariants (enforced by discovery-compliance-registry.test.ts)\n *\n * J1 — every canonical providerId (catalog ∪ switch) appears in exactly\n * ONE compliance bucket.\n * J2 — every providerId in `non-compliant-runtime-not-materialized`\n * MUST also be a provider with `staticModels` in\n * `providers.catalog.ts` (the static array is the smoking gun\n * for this bucket; otherwise it would be `compliant-dynamic-\n * discovery` or `non-compliant-no-machine-readable-discovery`).\n * J3 — every providerId with `staticModels` or `pinnedFallback.models`\n * in `providers.catalog.ts` MUST appear in one of THREE\n * static-inventory buckets:\n * • `pinnedFallback-by-design` (compliant — vendor has no\n * upstream surface, the curated list IS the contract),\n * • `non-compliant-hardcoded-inventory` (debt — vendor has a\n * non-OAI surface, our parser is queued), or\n * • `non-compliant-runtime-not-materialized` (debt — real\n * machine-readable surface exists, fetcher pending).\n * No quiet exemptions: every static array is bucketed.\n * J4 — buckets are exactly the 9 above.\n * J5 — the registry's union equals the canonical set\n * |catalog ∪ switch| = 103 at HEAD.\n *\n * ## How to USE this registry\n *\n * - The /v1/models route consumes this to populate the `inventoryClass`\n * provenance field on each model row in the response.\n * - The CI guard `discovery-compliance-registry.test.ts` enforces\n * J1–J5.\n * - When a non-compliant provider is migrated (a fetcher lands, a\n * deployment-listing call is wired, etc.), update both the catalog\n * row (drop `staticModels`) AND this registry in the same commit.\n */\nexport const DISCOVERY_COMPLIANCE_BUCKETS = [\n 'compliant-dynamic-discovery',\n 'compliant-deployment-discovery',\n 'compliant-machine-readable-official-catalog',\n // Phase 6 Fix 7 (2026-04-30): pinnedFallback-by-design split.\n // Providers with `pinnedFallback.reason === 'no-list-endpoint'` are\n // promoted out of `non-compliant-hardcoded-inventory` to reflect\n // that the operator-curated list IS the inventory contract — the\n // vendor having no /models is a vendor choice, not engineering debt.\n 'pinnedFallback-by-design',\n 'non-compliant-hardcoded-inventory',\n 'non-compliant-no-machine-readable-discovery',\n 'non-compliant-runtime-not-materialized',\n 'not-applicable-non-model-surface',\n 'self-hosted-runtime-dependent',\n] as const;\n\nexport type DiscoveryComplianceClass = (typeof DISCOVERY_COMPLIANCE_BUCKETS)[number];\n\n/**\n * Per-bucket compliance assignment of every canonical providerId.\n *\n * The classification is independent of the operational `CONSOLIDATION_MATRIX`:\n * an upstream-suspended provider that DOES expose /v1/models on its docs\n * (e.g. arcee, chutes, hyperbolic) is still `compliant-dynamic-discovery`\n * here — because the question this registry answers is \"is the inventory\n * source-of-truth dynamic?\", not \"is the provider reachable?\".\n *\n * Distribution at HEAD (2026-04-30, post Phase 6 Fix 7 split):\n * compliant-dynamic-discovery = 62\n * compliant-deployment-discovery = 3\n * compliant-machine-readable-official-catalog = 0\n * pinnedFallback-by-design = 7 (Phase 6 Fix 7: perplexity, recraft,\n * runwayml, bfl, inworld, v0, xiaomi-mimo —\n * all `reason: 'no-list-endpoint'`)\n * non-compliant-hardcoded-inventory = 2 (inflection, relace — both\n * `reason: 'proprietary-schema'`,\n * i.e. real parser-debt)\n * non-compliant-no-machine-readable-discovery = 2\n * non-compliant-runtime-not-materialized = 8\n * not-applicable-non-model-surface = 5\n * self-hosted-runtime-dependent = 14\n * TOTAL = 103 ✓\n *\n * Phase 4d (2026-04-28): bytez was promoted from\n * `non-compliant-runtime-not-materialized` to `compliant-dynamic-discovery`.\n * The dedicated `BytezNativeModelFetcher` consumes the vendor's\n * `/models/v2/list/models` endpoint (non-OAI shape) and feeds the\n * central discovery service via the `fetcherClass` catalog field. The\n * row's `integrationMode` flipped `execution-only` → `discovery+execution`\n * accordingly, and the deprecated `staticModels` block was removed.\n *\n * Registry-sync 2026-04-28 (Phase 4d closure): `v0` and `xiaomi-mimo`\n * were demoted from `compliant-dynamic-discovery` to\n * `non-compliant-hardcoded-inventory` to match the catalog's\n * `pinnedFallback` declaration. Live probes (catalog `notes`):\n * - api.v0.dev/v1/models → HTTP 404 not_found_error\n * - platform.xiaomimimo.com/v1/models → HTML homepage (no JSON listing)\n * Both rows ship `pinnedFallback.models` as the inventory of record;\n * `compliant-dynamic-discovery` was a stale assumption from initial\n * catalog drafting. Surfaced by test J3 (catalog static-inventory must\n * map to a non-compliant bucket).\n */\nexport const DISCOVERY_COMPLIANCE_REGISTRY: Record<DiscoveryComplianceClass, readonly string[]> = {\n // ── 62 — Real /models endpoint + registered fetcher ──────────────────\n // For each of these, central-model-discovery-service.ts has a\n // DiscoverySource that calls the upstream /models surface, parses the\n // response, and writes canonical Model rows to the DB. The /v1/models\n // response for these providers reflects runtime-materialised inventory.\n 'compliant-dynamic-discovery': [\n // Switch natives (8 — first-party APIs with /v1/models or equivalent)\n 'openai',\n 'anthropic',\n 'google',\n 'deepseek',\n 'mistral',\n 'xai',\n 'cohere',\n 'jina',\n // Switch routers (1)\n 'openrouter',\n // Catalog hub OAI-compat with materialiser wired into the central\n // discovery service (20 — pre-D1 set, all live-validated at HEAD)\n 'aihubmix',\n 'aiml',\n 'cerebras',\n 'cometapi',\n 'edenai',\n 'featherless-ai',\n 'friendli',\n 'gemini-openai',\n 'heliconeai',\n 'imagerouter',\n 'moonshot',\n 'nanogpt',\n 'novita',\n 'nvidia',\n 'orqai',\n 'poe',\n 'requesty',\n 'routeway',\n 'upstage',\n 'wandb',\n // 2026-06: alibaba catalog entry added (oai-compat-pure /models discovery\n // on dashscope) — registered here to keep J1 partition complete.\n 'alibaba',\n // Sublote D1 promotions (8 — credential provisioned 2026-04-24)\n 'groq',\n 'deepinfra',\n 'huggingface',\n 'cloudflare-workers-ai',\n 'github-models',\n 'fireworks-ai',\n 'sambanova',\n 'infermatic',\n // Operationally upstream-suspended but discovery-shape compliant (5)\n // (auth accepted, balance/credit gated; their /models surface is real)\n 'ai302',\n 'arcee',\n 'chutes',\n 'hyperbolic',\n 'anyscale',\n // Credentials-missing but the catalog row points at a real /models\n // surface — these will materialise when a credential lands (18)\n 'lambda-ai',\n 'minimax',\n 'morph',\n 'nebius',\n 'nscale',\n 'scaleway',\n 'synthetic',\n // v0 and xiaomi-mimo demoted 2026-04-28 to `non-compliant-hardcoded-inventory`\n // — live probes prove no /v1/models surface exists; pinnedFallback in\n // catalog is the inventory of record. See registry-sync note above.\n 'vercel-ai-gateway',\n 'volcano',\n 'watsonx',\n 'zai',\n 'gmi',\n 'rekaai',\n 'siliconflow',\n 'stepfun',\n 'togetherai',\n // Sublote A/B confirmed /models 200 public against canonical baseUrl (3)\n 'venice',\n 'mancer',\n 'phala',\n // Phase 4d 2026-04-28 — promoted from non-compliant-runtime-not-materialized\n // BytezNativeModelFetcher (api/src/services/model-fetchers/bytez-native-model-fetcher.ts)\n // wires the vendor's /models/v2/list/models non-OAI shape into the\n // central discovery service via the catalog `fetcherClass` field;\n // execution path flipped `execution-only` → `discovery+execution`.\n 'bytez',\n // LOTE O (2026-07-10) — apertis, inception. Both confirmed via doc\n // research to expose a real `GET /v1/models` (OpenAI-shaped) surface\n // consumed by the generic OpenAICompatibleHubModelFetcher; no probe\n // ran this session (see consolidation-matrix `no-live-validation`),\n // but the discovery SHAPE is compliant by design, independent of\n // whether a probe has confirmed it live yet.\n 'apertis',\n 'inception',\n // LOTE P (2026-07-11) — empiriolabs. Real GET /v1/models confirmed\n // live (134+ models, OpenAI-list shape), consumed by the generic hub\n // fetcher same as apertis/inception.\n 'empiriolabs',\n // LOTE Q (2026-07-12) — concentrate. UNAUTHENTICATED /v1/models/\n // confirmed live 2026-07-12 (`{object:\"list\",data:[{id,owned_by,...}]}`\n // — same shape the generic hub fetcher expects). The provider's own\n // richer per-provider enrichment endpoints are NOT consumed here.\n 'concentrate',\n // LOTE R (2026-07-13) — fastrouter. UNAUTHENTICATED /api/v1/models\n // confirmed live 2026-07-13 (`{data:[{id,creator,pricing,...}]}` —\n // same shape the generic hub fetcher expects, plus a real `pricing`\n // object directly on the flat list unlike apertis/empiriolabs/\n // concentrate).\n 'fastrouter',\n // LOTE S (2026-07-13) — perplexity-agent. AUTHENTICATED GET /v1/models\n // confirmed live 2026-07-13 — real OpenAI-list shape\n // (`{data:[{id,object,owned_by,created}]}`), 32 real models across 6\n // vendor families. Unlike the classic `perplexity` row (no /models\n // endpoint at all, pinnedFallback by design), the Agent API surface has\n // genuine dynamic discovery.\n 'perplexity-agent',\n // LOTE T (2026-07-13) — ailin. Compliant by shape, same reasoning as\n // apertis/inception above (no live probe this session, but the\n // discovery SHAPE is what this bucket classifies). GET /v1/models\n // returns id + a provider-attribution field the generic fetcher\n // successfully extracts (via `originalProviderField: 'originProvider'`\n // in the catalog entry) — the two things `compliant-dynamic-discovery`\n // actually requires. The response ALSO carries ailin-native enrichment\n // fields (operability, fallbackChain, resolvedProvider) the generic\n // fetcher ignores rather than mis-parses; that's a metadata-richness\n // gap, not a shape-compliance failure — J2's static-inventory\n // requirement (which `non-compliant-runtime-not-materialized` would\n // impose) does not apply here because there IS no static inventory:\n // this is a 70k-model, constantly-shifting catalog, and pinning a\n // fallback list would misrepresent it (see catalog entry notes).\n 'ailin',\n // LOTE U (2026-07-29) — sakana-ai. AUTHENTICATED GET /v1/models\n // confirmed live 2026-07-29 with a real key from GCP secret\n // `ailin-sakana-ai-key`: HTTP 200, real OpenAI-list shape\n // (`{object:\"list\",data:[{id,object,created,owned_by}]}`), 5 real\n // models — the exact shape the generic OpenAICompatibleHubModelFetcher\n // expects, no dedicated fetcher needed. Note: chat/completions itself\n // could not be live-confirmed end-to-end on this key (429\n // usage_limit_reached — no active subscription/billing on the account)\n // but that's an execution-time/billing concern, not a discovery-shape\n // one; the /v1/models surface itself is genuinely live and compliant.\n 'sakana-ai',\n ],\n // ── 3 — Deployment-bound discovery ───────────────────────────────────\n // Discovery via a deployment-listing API; runtime materialises only\n // what the operator has provisioned in their cloud account.\n 'compliant-deployment-discovery': [\n 'vertex-ai', // /publishers/{publisher}/models — deployment-scoped\n 'aws-bedrock', // /foundation-models — Bearer or SigV4\n 'aws-sagemaker', // /endpoints — operator-provisioned\n ],\n // ── 0 — Reserved for future official machine-readable catalogs ───────\n 'compliant-machine-readable-official-catalog': [\n // EMPTY at HEAD. Fill when a vendor publishes a versioned JSON\n // catalog under their own domain (no /models endpoint, no per-account\n // state) that we can fetch at runtime. None of the canonical 103\n // currently fit.\n ],\n // ── 7 — pinnedFallback is the inventory contract, by vendor design ───\n // The vendor's API genuinely doesn't expose a /models endpoint and\n // never will (catalog `pinnedFallback.reason === 'no-list-endpoint'`).\n // The operator-curated `pinnedFallback.models` list IS the source of\n // truth — there is nothing to \"fix\" upstream. Classified as compliant\n // because forcing these into a \"non-compliant-*\" bucket misleads\n // operators into chasing phantom upstream surfaces.\n //\n // Distinct from `non-compliant-hardcoded-inventory` (vendor DOES expose\n // discovery, parser unwritten — engineering debt) and from\n // `not-applicable-non-model-surface` (vendor has no model ontology at\n // all — voices, filters, etc.).\n //\n // Phase 6 Fix 7 (2026-04-30): split out from\n // `non-compliant-hardcoded-inventory` once the dossier flagged that\n // /v1/models was reporting 22 model rows under \"non-compliant\" when\n // they were operator-curated by-design pins.\n 'pinnedFallback-by-design': [\n 'perplexity', // pinnedFallback.reason: 'no-list-endpoint' — Sonar family,\n // Perplexity's docs explicitly state no /models endpoint.\n 'recraft', // pinnedFallback.reason: 'no-list-endpoint' — image-only\n // (recraftv3, recraftv2); execution-only integrationMode.\n 'runwayml', // pinnedFallback.reason: 'no-list-endpoint' — video-only\n // (gen3*); execution-only integrationMode.\n 'bfl', // pinnedFallback.reason: 'no-list-endpoint' — Black Forest Labs\n // Flux image generation; no /models surface published.\n 'inworld', // pinnedFallback.reason: 'no-list-endpoint' — discovery\n // endpoints both return 404 per catalog notes.\n 'v0', // pinnedFallback.reason: 'no-list-endpoint' — api.v0.dev/v1/models\n // returns HTTP 404 not_found_error; public docs describe\n // chats/projects/deployments shape, not a model selector.\n 'xiaomi-mimo', // pinnedFallback.reason: 'no-list-endpoint' —\n // platform.xiaomimimo.com/v1/models returns the\n // platform's HTML homepage (no JSON listing).\n 'topaz', // pinnedFallback.reason: 'no-list-endpoint' — fixed roster of\n // image-enhance pipelines (standard/high-fidelity); moved here\n // from not-applicable-non-model-surface when the runnable-gap\n // pass gave it a curated pinned inventory (2026-06-11, J3).\n ],\n // ── 2 — pinnedFallback as interim crutch, parser pending ─────────────\n // Real engineering debt — vendor DOES expose a discovery surface but\n // it's `pinnedFallback.reason === 'proprietary-schema'`: the parser\n // has not been written yet. Promotion path: write the parser, drop\n // the static array, move to `compliant-dynamic-discovery`. This\n // bucket should shrink over time.\n 'non-compliant-hardcoded-inventory': [\n 'inflection', // pinnedFallback.reason: 'proprietary-schema' — Sublote B\n // confirmed 404 on candidate OAI paths; vendor's discovery\n // surface, when available, is non-OAI shape.\n 'relace', // pinnedFallback.reason: 'proprietary-schema' — Sublote B\n // observed uniform 401 across 8 discovery paths; the catalog\n // carries pinnedFallback (relace-apply-3, relace-code-reranker,\n // relace-embedding) as the only inventory source until the\n // adapter+secret combination is wired.\n ],\n // ── 2 — No staticModels, no upstream surface, nothing to materialise ─\n // The provider is canonical (operator wants it tracked) but currently\n // has nothing to list in /v1/models. Promotion path: vendor publishes\n // a discovery API.\n 'non-compliant-no-machine-readable-discovery': [\n 'sap', // SAP AI Core — workspace-local deployment IDs, no model-listing\n 'snowflake', // Cortex — function-based API, no model-listing endpoint\n ],\n // ── 9 — staticModels but real upstream surface exists ────────────────\n // These have `staticModels: [...]` in providers.catalog.ts AND a real\n // machine-readable surface that we know about. Each is a queued\n // engineering task — wire the surface, drop the static array, move to\n // `compliant-dynamic-discovery`.\n 'non-compliant-runtime-not-materialized': [\n 'replicate', // /v1/models exists; operator opted not to import 'thousands' wholesale\n // Phase 4d 2026-04-28 — bytez promoted to `compliant-dynamic-discovery`\n // via BytezNativeModelFetcher; entry removed from this bucket.\n 'voyage', // /v1/models exists upstream, fetcher not wired\n 'azure-openai', // /openai/deployments exists, deployment-listing not wired\n 'databricks', // /api/2.0/serving-endpoints exists, fetcher not wired\n 'writer', // /models {models:[...]} non-OAI shape, parser pending\n 'atlascloud', // Sublote B: /v1/models 200 public (107 models), wrapper transform pending\n 'avian', // Sublote B: /v1/models 200 public (6 models), wiring pending\n 'qianfan', // /v2/models per BCE OAI-compat docs; secret + adapter pending\n ],\n // ── 5 — Specialty surfaces, not LLM-model-shaped ─────────────────────\n // These providers are NOT non-compliant; they operate in a different\n // category entirely. Their public surface is voices/scenes/filters,\n // not a chat/completion ontology.\n 'not-applicable-non-model-surface': [\n 'cartesia', // /voices, not /models — TTS specialty\n 'deepgram', // /v1/models lists STT engines, not LLM ontology\n 'elevenlabs', // /v1/voices, not /v1/models — TTS specialty\n 'palabraai', // session-based real-time translation, no model ontology\n // 'topaz' moved to pinnedFallback-by-design (2026-06-11): the\n // runnable-gap pass gave it a curated pinnedFallback inventory\n // (reason: 'no-list-endpoint'), which is exactly that bucket's\n // semantics (J3).\n ],\n // ── 14 — Self-hosted runtime-dependent ───────────────────────────────\n // Discovery is a function of operator runtime. NOT non-compliant —\n // discovery for these providers is genuinely runtime-bound by design.\n 'self-hosted-runtime-dependent': [\n // Catalog self-hosted (8)\n 'ollama',\n 'local-llama',\n 'local-kobold',\n 'local-embeddings',\n 'vllm',\n 'lm-studio',\n 'xinference',\n 'triton',\n // Switch self-hosted (6)\n 'self-hosted',\n 'local-ocr',\n 'local-docling',\n 'local-nllb',\n 'local-cosyvoice',\n 'local-piper',\n ],\n} as const;\n\n/**\n * Convenience predicate: is the providerId classified as compliant\n * with the SOTA dynamic-discovery contract (any of the three compliant\n * buckets)? Self-hosted is treated as compliant-by-category since its\n * inventory genuinely depends on operator runtime.\n */\nexport function isDiscoveryCompliant(providerId: string): boolean {\n const compliantBuckets: readonly DiscoveryComplianceClass[] = [\n 'compliant-dynamic-discovery',\n 'compliant-deployment-discovery',\n 'compliant-machine-readable-official-catalog',\n // Phase 6 Fix 7 (2026-04-30): pinnedFallback-by-design is compliant —\n // the operator-curated list IS the inventory contract when the vendor\n // genuinely doesn't expose /models. See bucket docstring.\n 'pinnedFallback-by-design',\n 'self-hosted-runtime-dependent',\n 'not-applicable-non-model-surface',\n ];\n for (const bucket of compliantBuckets) {\n if (DISCOVERY_COMPLIANCE_REGISTRY[bucket].includes(providerId)) return true;\n }\n return false;\n}\n\n/**\n * Reverse lookup: given a providerId, return its compliance class\n * (or undefined if the providerId is not in the canonical set).\n */\nexport function getDiscoveryComplianceClass(\n providerId: string\n): DiscoveryComplianceClass | undefined {\n for (const bucket of DISCOVERY_COMPLIANCE_BUCKETS) {\n if (DISCOVERY_COMPLIANCE_REGISTRY[bucket].includes(providerId)) return bucket;\n }\n return undefined;\n}\n\n/**\n * Orphan inventory — in-tree secrets and/or adapter classes that are NOT\n * in the canonical set. These are tracked so operational counters never\n * accidentally include them. They are NOT part of the consolidation matrix\n * proper (the test asserts disjointness).\n *\n * ## Triage decision (2026-04-23 — Lot B closure applied + backlog cleared):\n *\n * - `writer`, `upstage`, `rekaai`: CLOSED as of 2026-04-23 via Lot B\n * (catalog rows, factory registrations, secret mappings added). They\n * are no longer orphans — they are canonical providers in the\n * `live-validation` bucket above.\n *\n * - `ai21`, `llmstats`, `merge`: VERIFIED-CLEAN as of 2026-04-23. Grep\n * across `src/config/load-secrets-into-env.ts` returned no matches\n * for any of these — previous removal lots already cleared them.\n * The only residual reference is `ai21` in `NATIVE_PROVIDERS`\n * (src/core/selection/provider-kind.ts): that set is a permissive\n * routing-bias classifier where extra entries have no runtime cost\n * (the classifier never receives a non-canonical providerId from\n * code paths that only dispatch canonical providers). Accepted as\n * structural exception; documented here rather than surgically\n * removed to avoid re-breaking a settled decision.\n */\nexport const NON_CANONICAL_PROVIDERS = {\n // Empty as of 2026-04-23 — Lot B closed writer/upstage/rekaai. If a\n // future orphan needs transient tracking (adapter on disk, no catalog\n // row, no factory), it goes here until the closure lot is shipped.\n pending_closure: [] as const,\n // Empty as of 2026-04-23 — backlog verified clean via grep. See\n // triage note above for the full reasoning.\n pending_removal: [] as const,\n} as const;\n\n/**\n * Historical claims from prior reports that are superseded by this\n * consolidation. Keeping them here (not in the matrix) makes the\n * supersession explicit and audit-traceable, without letting them\n * contaminate operational counts.\n *\n * The invariant test asserts none of these strings appear in any\n * bucket of CONSOLIDATION_MATRIX.\n */\nexport const NON_CANONICAL_HISTORICAL_CLAIMS = [\n {\n claim: '0 live probes this session',\n superseded_at: '2026-04-23',\n reason:\n '41 HTTP probes were executed in the 2026-04-23 consolidation session; 28 canonical providers classified as live-validation at first-pass, 31 after Lot B closure of writer/upstage/rekaai.',\n },\n {\n claim: '29 providers live-validated',\n superseded_at: '2026-04-23',\n reason:\n 'The 29 figure belonged to a prior probe session. Pre-Lot-B this consolidation recorded 28 canonical live-validated providers; post-Lot-B (writer/upstage/rekaai promoted from orphan to canonical in the same turn) the figure is 31.',\n },\n {\n claim: '32 providers live-validated',\n superseded_at: '2026-04-23',\n reason:\n 'The 32 figure counted writer/upstage/rekaai/ai21 as canonical, but at the time only ai21 remained non-canonical. As of Lot B closure in this session, writer/upstage/rekaai ARE canonical (with probe-pre-closure-200 evidence); ai21 remains non-canonical pending removal. The correct canonical count for that probe set is now 31.',\n },\n {\n claim: 'canonical count = 87',\n superseded_at: '2026-04-23',\n reason:\n 'At first-pass of this session the audit recorded catalog=65 + switch=22 = 87. Lot B closure (writer/upstage/rekaai orphan-to-canonical) brings catalog=68 + switch=22 = 90. The invariant test does NOT pin the number; it pins structural identity (matrix total equals canonical union size).',\n },\n {\n claim: 'canonical count = 90',\n superseded_at: '2026-04-23',\n reason:\n 'Post Lot B and pre LOTE M the audit recorded catalog=68 + switch=22 = 90. LOTE M 2026-04-23 complement lot (arcee, atlascloud, avian, gmi, infermatic, inflection, mancer, phala, qianfan, relace, siliconflow, stepfun, venice — 13 net adds: 12 hosted OAI-compat + 1 catalog-only) brings catalog=81 + switch=22 = 103. The invariant test still pins structural identity, not the number; this claim exists so a grep for `= 90` against the matrix surfaces as explicitly superseded rather than looking authoritative.',\n },\n {\n claim: '31 providers live-validated (post Lot B, pre late-pass)',\n superseded_at: '2026-04-23',\n reason:\n 'Late-pass probes on 2026-04-23 promoted 5 providers to live-validation (edenai re-probed with correct secret → 200/330 models; openrouter /api/v1/models → 200/349 models; cartesia /voices → 200/751 voices; deepgram /v1/models → 200 using Token auth scheme; elevenlabs /v1/voices → 200) and demoted 1 (moonshot /v1/models → 401, previously 200). Net +4 → 35 live-validated providers (24 catalog + 11 switch).',\n },\n {\n claim: 'gemini-openai is no-live-validation',\n superseded_at: '2026-04-23',\n reason:\n 'gemini-openai shares the google api_key state. A direct probe against generativelanguage.googleapis.com returned 403 CONSUMER_SUSPENDED — the key was suspended server-side, not scope-limited. gemini-openai therefore shares the google vendor-side-failure classification rather than sitting in no-live-validation.',\n },\n {\n claim: 'google 403 is an SA scope limit',\n superseded_at: '2026-04-23',\n reason:\n 'Probe against generativelanguage.googleapis.com this session returned an explicit \"Consumer \\'api_key:...\\' has been suspended\" message (403 CONSUMER_SUSPENDED), not a scope-denial. The failure is vendor-side key state, not a scope mismatch. Bucket is unchanged (still vendor-side-failure), only the reason text is corrected.',\n },\n {\n claim: 'edenai credential is rotated server-side (defunct)',\n superseded_at: '2026-04-23',\n reason:\n 'The prior probe pass used secret ailin-edenai-api-key and got 401, concluding \"rotated\". This pass probed /v3/llm/models with ailin-edenai-key (alternate GCP secret in the same project) and got HTTP 200 with 330 models. edenai is live-validated; the defunct verdict was a secret-selection bug in the prior pass.',\n },\n {\n claim: 'moonshot live-validated (from earlier session)',\n superseded_at: '2026-04-23',\n reason:\n 'Re-probe of api.moonshot.cn/v1/models with the same secret (ailin-moonshot-key) returned 401 this pass, where the previous session recorded 200. Whether the key was rotated or the public API tier changed is undetermined; moonshot is reclassified defunct-unreachable until next probe lot establishes the cause.',\n },\n {\n claim: 'moonshot is defunct-unreachable (from the pre-final late-pass)',\n superseded_at: '2026-04-23',\n reason:\n 'The \"defunct\" verdict came from probing api.moonshot.cn (PRC-region host). The canonical MoonshotAdapter uses api.moonshot.ai/v1 (see DEFAULT_BASE_URL in moonshot-adapter.ts). This final pass re-probed the ACTUAL adapter target api.moonshot.ai/v1/models with the same GCP secret ailin-moonshot-key → HTTP 200, 14 models. The integration was never broken; the earlier 401 was a probe-target bug, not a credential rotation. moonshot is restored to live-validation.',\n },\n {\n claim: 'aws-bedrock is credentials-missing (requires SigV4, cannot probe)',\n superseded_at: '2026-04-23',\n reason:\n 'AWS added Bearer-token authentication for Bedrock read endpoints in 2025, alongside the existing SigV4 flow. GCP secret ailin-aws-bearer-token was probed against GET bedrock.us-east-1.amazonaws.com/foundation-models → HTTP 200, 163KB body, modelSummaries array populated. Credentials ARE available and the read path works without SigV4 signing. Invocation endpoints still use SigV4 via @aws-sdk/signature-v4, which is a separate surface; the bucket classification is based on reachable credential evidence, and that evidence now exists. aws-bedrock → live-validation.',\n },\n {\n claim: 'palabraai is credentials-missing',\n superseded_at: '2026-04-23',\n reason:\n 'GCP secrets ailin-palabraai-id (ClientId) and ailin-palabraai-key (ClientSecret) exist. Probe of POST api.palabra.ai/session-storage/session with both headers returned HTTP 403 with body {\"detail\":\"Insufficient balance\",\"error_code\":100050} — the Palabra public error-code table documents 100050 as post-authentication credit exhaustion. This is upstream-suspended (auth accepted, tier-gated), NOT credentials-missing.',\n },\n {\n claim: 'complement lot (2026-04-23) — 21 brand names to integrate',\n superseded_at: '2026-04-23',\n reason:\n 'Mapping table produced for Arcee/AtlasCloud/Avian/BaiduQianfan/BFL/Chutes/GMICloud/Infermatic/Inflection/Liquid/Mancer/MiniMax/ModelRun/nCompass/Phala/Reka/Relace/SiliconFlow/StepFun/Upstage/Venice. Outcome: 5 already-canonical (bfl, chutes, minimax, rekaai, upstage); 1 orphan-with-material (baidu-qianfan — has BaiduModelFetcher + secret mapping ailin-baidu-{key,secret,base-url}, but all 3 secrets are literal \"PLACEHOLDER\" and the fetcher uses deprecated ERNIE OAuth2 shape; canonical promotion deferred to a dedicated lot that (a) adds baidu-qianfan catalog row with qianfan.baidubce.com/v2 OAI-compat base URL, (b) replaces the ERNIE OAuth2 fetcher with Bearer auth, (c) requires real secret provisioning); 15 absent (arcee, atlascloud, avian, gmicloud, infermatic, inflection, liquid, mancer, modelrun, ncompass, phala, relace, siliconflow, stepfun, venice — no material in-tree, no secrets provisioned). Per the directive exigência final (\"Não quero provider adicionado só porque ganhou row no catálogo\"), catalog row additions were NOT committed in this lot; they await operator secret provisioning to ensure each row ships with live-validation evidence. Proposed row shapes captured in the lot report (section 14 \"Próximo lote objetivo\").',\n },\n {\n claim: 'LOTE M deferral — await operator secret provisioning before adding rows',\n superseded_at: '2026-04-23',\n reason:\n 'Operator decision 2026-04-23 (message #2 of complement-lot session): \"#1 Escopo completo #2 A #3 Prossiga com a melhor recomendação para cada\" authorized catalog-row additions without waiting for secret provisioning. The deferral rationale (every row should ship with live-validation evidence) was superseded by the operator-level decision to land the rows now AND track each one explicitly in credentials-missing/secret-absent (or auth-incomplete for qianfan). This makes the \"missing work\" visible in the matrix — operators see 12 rows waiting for <PROVIDER>_API_KEY provisioning — rather than invisible in a narrative-only backlog. 13 rows added this lot (arcee, atlascloud, avian, gmi, infermatic, inflection, mancer, phala, qianfan, relace, siliconflow, stepfun, venice); the 3 NOT_ELIGIBLE providers (liquid, modelrun, ncompass) are recorded in separate historical claims below.',\n },\n {\n claim: 'liquid (Liquid AI) eligible for canonical catalog inclusion',\n superseded_at: '2026-04-23',\n reason:\n 'Liquid AI (LFM family) has no first-party production API as of 2026-04-23. LFM models (LFM-40B, LFM-3B, LFM-1B) are distributed through OpenRouter and other aggregators; liquid.ai offers a research/demo surface but not an OAI-compat or native production endpoint that an adapter can target. A `liquid` canonical providerId would duplicate the routing a user already gets via openrouter → liquid/lfm-*. Reclassification requires Liquid AI to publish a first-party API with a stable base URL. Status: NOT_ELIGIBLE until that upstream change.',\n },\n {\n claim: 'modelrun eligible for canonical catalog inclusion',\n superseded_at: '2026-04-23',\n reason:\n 'ModelRun (modelrun.xyz and known alternative domains) returned ECONNREFUSED on all probed endpoints during the 2026-04-23 complement-lot evaluation. No indexed public service fronted by the expected base URL. No adapter material exists in-tree. Cannot be canonicalized because the base URL itself is unverifiable. Status: NOT_ELIGIBLE until operator identifies the correct hostname (if the brand still operates) or confirms the service is defunct.',\n },\n {\n claim: 'ncompass eligible for canonical catalog inclusion',\n superseded_at: '2026-04-23',\n reason:\n 'nCompass Technologies (ncompass.tech) base URL for its production inference API is not published in public docs accessible from the probe environment. The landing pages reference a private signup flow; the actual api.* or inference.* subdomain is gated behind operator registration. Cannot add a catalog row without a verifiable baseUrl — Zod validation requires it, and a wrong URL creates silent 404-loops. Status: NOT_ELIGIBLE until operator signs up, discovers the base URL, and provisions NCOMPASS_API_KEY.',\n },\n {\n claim: 'venice is credentials-missing / secret-absent (from LOTE M close)',\n superseded_at: '2026-04-23',\n reason:\n 'Sublote A 2026-04-23 probed venice directly. /api/v1/models returned HTTP 200 PUBLICLY (no auth header required; even with an invalid Bearer the same 200 response body was returned — auth is IGNORED on /models), carrying 72 text models in OAI-conformant shape ({data:[{id,object,owned_by,...}]}). /api/v1/chat/completions returned HTTP 402 with a full x402 crypto-payment-protocol body (network eip155:8453 Base, USDC token 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913) when no Bearer was sent, and HTTP 401 {\"error\":\"Authentication failed\"} when an invalid Bearer was sent. Discovery surface is live-proven against the canonical adapter target; execution surface is credential-gated. Under the Sublote A rule \"se houver probe real bem-sucedida, não rebaixar\", venice was promoted from credentials-missing → partial. GCP still has no ailin-venice-* secret under any alias, so live-validation of the chat surface remains blocked. This claim is documented here (rather than silently updating the earlier claim) to make the Sublote A bucket move visible to future auditors grepping for venice.',\n },\n {\n claim: 'Sublote A (2026-04-23) produced zero bucket moves',\n superseded_at: '2026-04-23',\n reason:\n 'First-pass Sublote A narrative incorrectly concluded \"4/4 stay in credentials-missing; zero moves\". The rule \"se houver probe real bem-sucedida, não rebaixar\" is satisfied by venice /api/v1/models returning HTTP 200 against the canonical adapter base URL. Revised outcome: ONE bucket move (venice credentials-missing → partial). The other three (qianfan, siliconflow, stepfun) stay credentials-missing because their probes returned only error responses against the canonical surface — endpoint liveness is confirmed but no canonical surface is live-proven. Concrete impact: credentials-missing 59→58, partial 1→2.',\n },\n {\n claim: 'Sublote A second-pass verification found new promotion opportunities',\n superseded_at: '2026-04-23',\n reason:\n 'Sublote A re-run 2026-04-23 with deeper probes. (1) GCP Secret Manager re-scanned WITHOUT the ailin- prefix constraint across all 82 secrets — zero matches for qianfan/venice/siliconflow/stepfun under any alias (qianfan-*, baidu-qianfan-*, silicon-flow-*, step-*, venice-ai-*). Only ailin-baidu-{key,secret,base-url} exist, all three still literal \"PLACEHOLDER\" (11 bytes each). (2) Baidu v1 AK+SK OAuth token endpoint https://aip.baidubce.com/oauth/2.0/token probed with client_id=PLACEHOLDER&client_secret=PLACEHOLDER → HTTP 401 \"invalid_client / unknown client id\". This is STRONGER evidence than the previous \"material is placeholder\" framing: Baidu\\'s OAuth provider does not even recognize the client_id — the string is unregistered upstream, not merely format-wrong. qianfan classification is unchanged but the auth-incomplete subclass is now supported by upstream server-side rejection, not just local placeholder heuristic. (3) Venice /api/v1/models re-probed WITH an invalid Bearer header → HTTP 200, identical 78617-byte body to the no-auth request. Confirms the Authorization header is actively IGNORED on discovery — venice discovery is truly unauthenticated public, not \"optional auth accepted\". (4) siliconflow and stepfun probed on common unauth paths /health, /ping, /, /v1/health, /v1/ping → all HTTP 404 on both .cn/.com (siliconflow) and .com/.ai (stepfun). No public discovery surface exists; discovery IS credential-gated. This forecloses the Venice-style \"promote on public /models\" path for these two — they will move only when real secrets are provisioned. Net bucket delta this second pass: ZERO (matrix state is already consistent post first-pass Sublote A). Evidence strengthening only. .env.production, .env.test, .env.example, .env.example.clean all scanned — zero mentions of baidu/qianfan/venice/siliconflow/stepfun/ernie/step aliases.',\n },\n {\n claim: 'LOTE M remainder (12 providers) is a uniform credentials-missing block',\n superseded_at: '2026-04-23',\n reason:\n 'Sublote B 2026-04-23 applied the same probe method that promoted venice (Sublote A) to the 12 LOTE M providers still in credentials-missing/secret-absent (11 providers + 1 auth-incomplete qianfan). Method: for each provider, probe canonical adapter baseUrl across 8 paths ({/models, /v1/models, /health, /ping, /status, /info, /v1/info, /}) × 2 auth modes (no Authorization header vs invalid Bearer) = 16 probes × 12 providers = 192 probes total. Result: 4 PROMOTIONS from credentials-missing → partial on confirmed HTTP 200 public /models, 7 CONFIRMED-blocked (discovery credential-gated, no public surface), 1 catalog-only inventory (no OAI discovery surface at all, POST-only execution endpoint). Evidence per provider: (a) PROMOTED — atlascloud: api.atlascloud.ai/v1/models HTTP 200 59503 bytes (107 models, custom {code:200,msg:\"succeed\",data:[...]} wrapper; other 7 paths asymmetric: noauth 401 vs badauth 404, revealing server differentiates absent-header vs invalid-header). avian: api.avian.io/v1/models HTTP 200 1782 bytes (6 models, pure OAI shape; other 7 paths uniform 404). mancer: neuro.mancer.tech/oai/v1/models HTTP 200 2379 bytes no-header (9 LLaMA fiction/RP models) but HTTP 401 80 bytes with invalid Bearer — asymmetric VALIDATION where server accepts enumeration when no credential is CLAIMED but rejects claimed-invalid credentials. phala: api.redpill.ai/v1/models HTTP 200 72119 bytes (76 models via Phala SGX TEE confidential-compute routing, same body with and without invalid Bearer; other paths uniform 400 \"Internal server error\"). (b) CONFIRMED-BLOCKED — arcee: /models 401 both modes (other paths 404). gmi: /models 401 both modes (other paths 405 Method Not Allowed — endpoints exist but POST-only, not queryable unauth). infermatic: /models 401 both modes (other paths 404). relace: ALL 8 paths return 401 uniformly (catch-all auth middleware — no public discovery). siliconflow: /models 401 with 15-byte body (bare JSON string \"Invalid token\" confirming oai-compat-quirks class; other paths 404). stepfun: /models 401 with 75-byte OAI-shape error {\"error\":{\"message\":\"Incorrect API key provided\",\"type\":\"invalid_api_key\"}} (other paths 404, 0-byte bodies). qianfan (v2 surface qianfan.baidubce.com/v2): /models 401 with 120-byte BCE-shape error (other paths 404). (c) CATALOG-ONLY CONFIRMED — inflection: /models 404 on inference.ai.inflection.ai across ALL 8 paths (root / returns 307 redirect). The correct execution surface is /external/api/inference (POST-only, no discovery sidecar); inflection is a catalog-only-inventory-candidate operationally but retains secret-absent subclass because INFLECTION_API_KEY is still the expected unblock vector. Net bucket delta: partial 2→6 (+4), credentials-missing 58→54 (−4). Secret-absent subclass 40→36 (−4: atlascloud/avian/mancer/phala removed; 7 LOTE M rows remain: arcee/gmi/infermatic/inflection/relace/siliconflow/stepfun). No other buckets touched. Invariants I1–I6 hold: |catalog ∪ switch| unchanged at 103, every canonical id still in exactly one bucket, credentials-missing subclass partition preserved. The probe evidence is replicable from /tmp/subb/{provider}.out (12 files × 16 probes each) and model payloads /tmp/subb/payloads/{atlascloud,avian,mancer,phala}.json.',\n },\n {\n claim:\n 'LOTE M remainder (6 providers in Sublote C1 scope) can be unblocked by provisioning alone',\n superseded_at: '2026-04-23',\n reason:\n 'Sublote C1 2026-04-23 attempted credential resolution + per-surface live probes for arcee/gmi/infermatic/siliconflow/stepfun/qianfan. (1) GCP Secret Manager exhaustively re-scanned: 82 secrets × 27 alias patterns (arcee, conductor, gmi, gmicloud, gmi-cloud, gmi-serving, gmiserv, infermatic, totalgpt, total-gpt, siliconflow, silicon-flow, siliconcloud, silicon-cloud, stepfun, step, step-ai, stepai, qianfan, baidu, ernie, baidu-qianfan, wenxin, wenxinworkshop, bce, ai21, api-ernie) — zero matches except the known 3 ailin-baidu-{key,secret,base-url}, all confirmed this session as literal \"PLACEHOLDER\" (11 bytes). Environment variables checked at runtime: zero set. .env/.env.production/.env.test on disk: zero mentions. (2) Per-surface live probes executed with invalid bearer against canonical adapter baseUrls. Key doc-vs-impl discoveries: (a) INFERMATIC — api.totalgpt.ai badauth response echoes \\'LiteLLM Virtual Key expected. Received=INVALID_TEST_KEY_12345, expected to start with sk-\\'. Confirmed: infermatic is a LiteLLM proxy in front of the vLLM backend, not direct vLLM. API keys must be sk-prefixed LiteLLM Virtual Keys (generated via LiteLLM Admin UI or /key/generate). Notes updated. (b) GMI — api.gmi-serving.com/v1/chat and /v1/embeddings return HTTP 404 \"No matching target server found for model X\" BEFORE auth validation (pre-auth routing layer). This means a 404 on gmi chat does NOT imply bad credentials — it implies the model ID is not registered. Notes updated. (c) QIANFAN v1 — aip.baidubce.com/oauth/2.0/token with client_id=PLACEHOLDER returned 401 invalid_client/unknown client id (confirms upstream does not recognize), but the v1 chat endpoint aip.baidubce.com/rpc/2.0/ai_custom/v1/wenxinworkshop/chat/ernie-4.0-8k with access_token=PLACEHOLDER returned HTTP 200 with body-encoded {error_code:3,\"Unsupported openapi method\"} — Baidu\\'s API uses 200+error_code pattern, and the v1 endpoint IS reachable and would route a real token if provisioned. (d) SILICONFLOW — 6 surfaces (models.cn, models.com, chat, embed, rerank, audio) all uniform 401 bare-JSON-string \"Invalid token\" (15 bytes) — confirms integrationClass oai-compat-quirks across every surface. Both .cn and .com hosts equivalent. (e) STEPFUN — 5 surfaces (models.com, models.ai, chat, embed, audio/speech) all uniform 401 OAI-shape {\"error\":{\"message\":\"Incorrect API key provided\",\"type\":\"invalid_api_key\"}} (75 bytes) — confirms integrationClass oai-compat-pure. Both .com and .ai hosts equivalent. (f) ARCEE — /api/v1/models and /api/v1/chat both 401 with Arcee-specific {\"detail\":\"Missing or invalid Authorization header. Expected: Bearer <api_key>\"} (80 bytes noauth) / {\"detail\":\"Invalid or expired API key\"} (39 bytes badauth). Root / returns 200 \"OK\" (health check, not discovery). Net bucket delta: ZERO. All 6 providers remain in same bucket as pre-Sublote-C1 because no credential became available during this session. However, Sublote C1 produced: (i) 2 catalog notes updates (infermatic LiteLLM proxy, gmi pre-auth routing), (ii) 1 new historical claim (this entry), (iii) definitive proof that unblocking all 6 is PURE credential-provisioning work (no adapter changes, no endpoint changes, no structural refactoring required). Probe evidence in /tmp/subc1/clean.out and /tmp/subc1/bodies/*.body.',\n },\n {\n claim: 'LOTE M remainder mostly blocked by credential provisioning (pre-Sublote-D1)',\n superseded_at: '2026-04-24',\n reason:\n 'Sublote D1 2026-04-24 — operator provisioned 17 GCP secrets (ailin-{groq,togetherai,fireworks-ai,deepinfra,perplexity,huggingface,cloudflare-workers-ai,cloudflare-workers-ai-id,github-models,sambanova,hyperbolic,arcee,infermatic,siliconflow,stepfun,heliconeai-api,chutes,anyscale}-*-key(s)). Live probes (GET /v1/models + POST /v1/chat/completions with minimal body {\"model\":\"X\",\"messages\":[{\"role\":\"user\",\"content\":\"hi\"}],\"max_tokens\":5}) executed against canonical adapter baseUrls with the provisioned credentials. Result summary: (1) TEN promotions to live-validation (real HTTP 200 with completion body on /v1/chat): groq (llama-3.1-8b-instant 592B), deepinfra (Meta-Llama-3.1-8B-Instruct 455B), huggingface (Llama-3.1-8B-Instruct 668B via router.huggingface.co), cloudflare-workers-ai (@cf/meta/llama-3-8b-instruct 360B; /models 405 is expected per CF docs — their discovery uses /ai/models/search), github-models (openai/gpt-4o-mini 1252B; models at /catalog/models not /v1/models), perplexity (sonar 2992B; /models 404 by design), fireworks-ai (accounts/fireworks/models/glm-5p1 447B — re-probed after first-pick llama-v3p1-8b returned 404 deprecated), sambanova (Meta-Llama-3.3-70B-Instruct 910B — re-probed after first-pick 3.1 returned 410 Gone), infermatic (Qwen-Qwen3-30B-A3B 476B — confirmed LiteLLM Virtual Key with sk- prefix; key has a model-scoped ACL), heliconeai (gpt-4o-mini 1003B — new ailin-heliconeai-api-key sk-hel… 43B SUPERSEDES the legacy ailin-heliconeai-key \"PLACEHOLDER\" 11B that caused the prior mis-classification in CREDENTIALS_MISSING_SUBCLASS.placeholder). (2) FOUR promotions to upstream-suspended: anyscale (endpoint returns HTML shutdown notice \"Effective August 1, 2024 Anyscale Endpoints API is available exclusively through the fully Hosted Anyscale Platform; Multi-tenant access to LLM models has been removed.\" — the key ailin-anyscale-api-key 236B exists but cannot be exercised), arcee (trinity-mini → 402 {\"detail\":\"Insufficient credits. Required: 0.000037, Available: 0.000000\"}), chutes (Qwen/Qwen3-32B-TEE → 402 {\"detail\":{\"message\":\"Quota exceeded and account balance is $0.0, please pay with fiat or send tao...\"}}), hyperbolic (→ 402 {\"detail\":\"Insufficient funds\"}). All four have valid credentials (auth-accepted) but gated execution — operationally identical to ai302/palabraai. (3) THREE stay in credentials-missing but move sub-class secret-absent → auth-incomplete: togetherai (key_CY… 25B; /v1/chat returns OpenAI-shape 401 \"Invalid API key provided. You can find your API key at https://api.together.ai/settings/api-keys\" — the \"key_\" prefix is non-canonical for Together AI, probable format mismatch), siliconflow (sk-hhc… 51B; /v1/chat returns bare JSON \"Api key is invalid\" 15B oai-compat-quirks shape — probable format mismatch, sk-prefix looks OpenAI-style rather than SiliconFlow native), stepfun (65B; /v1/chat returns OAI-shape {\"error\":{\"message\":\"Incorrect API key provided\",\"type\":\"invalid_api_key\"}} 75B — verbatim OpenAI error shape, operator must verify the key maps to their StepFun account). (4) ONE operator-claim-vs-vault mismatch: gmi — the operator announcement listed gmi among the 16 provisioned but GCP Secret Manager scan 2026-04-24 against all known aliases (ailin-gmi-*, ailin-gmicloud-*, ailin-gmi-cloud-*, ailin-gmi-serving-*, ailin-gmiserv-*) found ZERO matches. gmi stays secret-absent until the actual secret lands in the vault. Net matrix deltas: live-validation 37→47 (+10), credentials-missing 54→40 (−14), upstream-suspended 2→6 (+4). Sub-class deltas: secret-absent 36→20 (−16: 9 to live-validation [all 10 promotees except heliconeai which was in placeholder sub-class], 4 to upstream-suspended, 3 to auth-incomplete), placeholder 2→1 (heliconeai promoted — legacy secret superseded by new api-key form), auth-incomplete 2→5 (+togetherai,siliconflow,stepfun). All invariants I1–I6 hold post-D1: 103 canonical total unchanged, every providerId in exactly one bucket, sub-class partition of credentials-missing covers all 40 entries. Probe evidence: /tmp/subd1/probe.out + /tmp/subd1/reprobe.out + /tmp/subd1/bodies/*.{models,chat,rechat}.body (17 providers × 2–3 surfaces each = ~50 probe records).',\n },\n] as const;\n\n/**\n * Sum of all canonical providers across the matrix. Convenience function —\n * the test asserts this equals |catalog ∪ switch| (structural identity, no\n * pinned number). At 2026-04-23 post-LOTE-M: catalog=81 + switch=22 = 103.\n *\n * Bucket distribution post-Sublote-D1 (2026-04-24):\n * live-validation = 47 (34 catalog + 13 switch) — +10 this sublote\n * no-live-validation = 0\n * partial = 6 (bytez — wire-shape mismatch;\n * venice — asymmetric auth (Sublote A);\n * atlascloud, avian, mancer, phala —\n * /models 200 public (Sublote B).)\n * — unchanged by Sublote D1.\n * credentials-missing = 40 (33 catalog + 7 switch/self-hosted) — −14\n * net this sublote (10 to live, 4 to\n * upstream-suspended). Sub-class partition:\n * secret-absent = 20\n * placeholder = 1\n * self-hosted-unreachable = 14\n * auth-incomplete = 5\n * total = 40 ✓\n * vendor-side-failure = 1 (google — GOOGLE_API_KEY still suspended)\n * upstream-suspended = 6 (ai302, palabraai, anyscale, arcee,\n * chutes, hyperbolic — +4 this sublote)\n * defunct-unreachable = 0\n * catalog-only-inventory = 3 (sap, snowflake, topaz)\n * switch-only-legitimate = 0\n * not-eligible = 0\n * TOTAL = 103 (matches |catalog ∪ switch|)\n *\n * Historical progression (each transition has a supersession entry):\n * Pre-LOTE-M : live=37 partial=1 creds=46 vendor=1 susp=2 cat-only=3 → 90\n * LOTE M close : live=37 partial=1 creds=59 vendor=1 susp=2 cat-only=3 → 103\n * Sublote A : live=37 partial=2 creds=58 vendor=1 susp=2 cat-only=3 → 103 (venice → partial)\n * Sublote B : live=37 partial=6 creds=54 vendor=1 susp=2 cat-only=3 → 103 (+atlascloud,avian,mancer,phala → partial)\n * Sublote C1 : live=37 partial=6 creds=54 vendor=1 susp=2 cat-only=3 → 103 (no moves — probe evidence strengthening only)\n * Sublote D1 : live=47 partial=6 creds=40 vendor=1 susp=6 cat-only=3 → 103 (+10 live, −14 creds, +4 upstream-suspended)\n */\nexport function totalCanonicalInMatrix(): number {\n return CONSOLIDATION_BUCKETS.reduce(\n (sum, bucket) => sum + CONSOLIDATION_MATRIX[bucket].length,\n 0\n );\n}\n"}}, "reports/experiments/AILIN-COLLECTIVE-FRONTIER-BENCHMARK-2026-07.md": {"6ff5594f823d436d8790cbaff441cc3b2e82b54a": {"t": 1785747191.3609042, "n": 14066, "h": "eb780f42963c8b08", "c": "<!--\nCopyright (C) 2026 Ailin One, Inc.\n\nThis file is part of Collective Intelligence Engine (ci).\nLicensed under the GNU Affero General Public License v3.0 or later.\nSee LICENSE in the repository root, or <https://www.gnu.org/licenses/>.\n\nSPDX-License-Identifier: AGPL-3.0-or-later\nSource: https://github.com/ailinone/collective-intelligence\n-->\n\n# Ailin Collective-vs-Frontier Benchmark — July 2026\n\n**Question under test:** can a collective of cheap, diverse models beat the\ncurrent frontier single models — and at what cost?\n\n**Campaign:** 3 runs, 2026-07-05 (single day, single code vintage, same\npinned judge): `9590ff41` (round 1, 4 flagships × 3 collectives),\n`f7b76768` (H-A top-up, collectives only), `569ce880` (round 2, 8 singles\nincl. Claude Fable 5 × 3 collectives). **1,278 executions persisted,\n~$143 total spend.** Every number below is reproducible from the committed\nCSV artifacts via the committed scripts (see §10).\n\n---\n\n## 1. TL;DR — the verdict, split by axis\n\n| Claim | Verdict | Evidence |\n|---|---|---|\n| Collective **with objective verifier** beats every frontier single on verifiable tasks (H-A, quality) | **VALIDATED** | Checker pass-rate 97% (37/38) vs flagships' 68–82% (§3) |\n| Collectives **without** the verifier beat frontier singles on verifiable tasks | **NOT validated** | blind-debate 77% / expert-panel 81% — inside the flagship range (§3) |\n| Collective beats frontier singles on open-ended tasks (H-C) | **NOT validated — parity** | Judge-metric deltas within ±0.06 A/A noise; loses on creative/refactoring, wins/ties on reasoning (§4) |\n| Collective is **cheaper per token** | **INVALIDATED** (as recorded) | Collectives $99–181/Mtok vs singles $0.14–1/Mtok recorded (§5) — but see the verifier short-circuit: $2.98/Mtok when it fires |\n| Collective is faster | **INVALIDATED** | p50 latency 15–49s vs 8–29s for singles (§6) |\n\n**One-sentence public claim that survives every audit in this campaign:**\n*a ~$0.002–0.26/query ensemble of sub-frontier open-weight models, when\narmed with a deterministic answer-verifier, produced objectively correct\nanswers more reliably (97%) than any of GPT-5.5-pro, Claude Opus 4.8,\nClaude Fable 5, Gemini 3.1 Pro or Grok 4.3 (65–82%) on the verifiable\nsubset of this suite — while costing 2–3 orders of magnitude more per\ntoken than the recorded single-model prices and running 2–4× slower.*\n\n## 2. Setup\n\n- **Single arms (pinned per run, election logged):** round 1:\n `gpt-5.5-pro-2026-04-23`, `claude-opus-4-8`, `gemini-3.1-pro`,\n `grok-4.3`. Round 2 adds: `claude-fable-5`, `gpt-5.5-pro` (A/A pair),\n `google/gemini-3.1-pro` (A/A pair), and — disclosed mis-election — the\n community fork `King3Djbl/mythos-9b-unhinged` (matched the `mythos`\n pattern; NOT a flagship; kept, analyzed separately). GPT-5.6, Gemini 3.5\n and Claude Mythos 5 were not present in the live catalog.\n- **Collective arms:** `consensus` (best-of-N answer-verifier armed on the\n verifiable subset), `blind-debate`, `expert-panel`. Voter pools are\n cheap open-weight/small models (kimi-k2, Qwen3.5-397B/8B, gpt-oss-120b,\n Llama-3.1-8B, ministral/codestral/mistral, GLM-4.6V…) — **no flagship\n ever appears as a voter** (verified from per-execution internals).\n- **Tasks:** 38 (stratified sample of the 126-task suite ∪ all 10\n verifiable tasks 116–125 with machine-checkable answers). Types:\n reasoning 10, strategy-specific 7, code-gen 4, creative 3, debugging 2,\n refactoring 2, adversarial 2, factual-QA 2, analysis/documentation/stt/\n video/leader/compositor 1 each.\n- **Judge:** pinned `deepseek-v4-pro` (DeepSeek family — non-competitor;\n serving id observed `DeepSeek-V4-Flash`). Its bias against terse\n verified answers is documented (§8) — hence the checker is the primary\n metric on verifiable tasks.\n- **Metrics:** (a) objective checker pass-rate (deterministic\n `answer_check`, judge-free) on tasks 116–125; (b) judge quality 0–1 on\n the full suite, **routing-faithful rows only** (single-arm rows served\n by a fallback model are excluded — fidelity was 65–82%).\n\n## 3. Leaderboard A — OBJECTIVE correctness (verifiable tasks, all 3 runs pooled)\n\n| Arm | correct | wrong | degraded† | contaminated† | **pass-rate** |\n|---|---|---|---|---|---|\n| **consensus + verifier** | **37** | **1** | 0 | 0 | **97%** (n=38) |\n| claude-fable-5 * | 15 | 0 | 5 | 0 | 100% (n=15) |\n| mythos-9b fork * | 15 | 0 | 5 | 0 | 100% (n=15) |\n| google/gemini-3.1-pro * | 14 | 1 | 5 | 0 | 93% (n=15) |\n| gpt-5.5-pro * | 14 | 3 | 3 | 0 | 82% (n=17) |\n| claude-opus-4-8 | 23 | 5 | 9 | 3 | 82% (n=28) |\n| expert-panel (no verifier) | 39 | 9 | 0 | 0 | 81% (n=48) |\n| gemini-3.1-pro | 24 | 6 | 9 | 1 | 80% (n=30) |\n| blind-debate (no verifier) | 27 | 8 | 0 | 0 | 77% (n=35) |\n| gpt-5.5-pro-2026-04-23 | 22 | 10 | 7 | 1 | 69% (n=32) |\n| grok-4.3 | 25 | 12 | 1 | 2 | 68% (n=37) |\n\n\\* round-2-only arms (smaller n). † `[DEGRADED]` no-response placeholders\nand self-review prompt-contamination rows are harness failures, counted\nseparately, never as model errors (§8).\n\nConsensus' single miss is a truncated generation (no `FINAL:` emitted) —\nacross 3 runs the verifier **never selected an objectively wrong answer**\n(53/55 verified-selection telemetry, 0 checker failures). Flagship errors\nare concrete and listed verbatim in the script output: grok answering\n`0.984` where `98.41%` was required, emitting `FINAL:` with empty payload,\nanswering the wrong prompt; gemini answering `99.999995%`; models\nanswering profit-*increase* (2500) where the new *total* (44500) was asked.\n\n## 4. Leaderboard B — judge metric, and WHERE the collective wins/loses\n\nFull-suite judge scores (faithful rows): round 1 — consensus 0.664 >\nbest single 0.615; round 2 — blind-debate 0.621 > best single 0.608. In\nboth rounds the best collective arm tops the table, but **which** strategy\nleads varies; treat as an arm-class result, not a strategy result.\n\n**Per-task-type map (round 2, best collective vs best faithful single):**\n\n| Task type | W/L/T | Reading |\n|---|---|---|\n| reasoning | **3W/0L/7T** | collective never loses; wins concentrate here |\n| adversarial | 1W/0L/1T | collective robust |\n| factual-QA | 0W/0L/2T | ceiling for both |\n| code-generation | 1W/2L/1T | mixed |\n| debugging | 0W/1L/1T | mixed-negative |\n| creative | **0W/3L/0T** | single models win open prose |\n| refactoring | 0W/2L/0T | single models win |\n| documentation / analysis / stt | 0W/3L | single models win |\n| by complexity | low 4W/4L/3T · medium 1W/4L/5T · high 0W/3L/5T | collective edge shrinks as open-endedness grows |\n\nThe full per-task table (38 rows: task, type, winning arm, scores, delta)\nis emitted by `c3-publication-tables.py §B`. Caveat: this map uses the\njudge metric, whose pro-prose bias (§8) systematically favors singles on\nopen tasks and *understates* the collective on verifiable ones.\n\n## 5. Cost — per Mtok, per execution, per success (AS RECORDED)\n\n| Arm | $/Mtok | $/execution | notes |\n|---|---|---|---|\n| singles (flagships) | **$0.14–1.02** | $0.0002–0.0018 | recorded catalog prices FAIL a list-price sanity check (frontier models at ~$0.2/Mtok is implausible) — cost axis is reported, not adjudicated |\n| grok-4.3 | $0.47–0.56 | $0.0007–0.0010 | most plausible frontier pricing |\n| consensus | $118–181 | $0.26–0.44 | **$2.98/Mtok, $0.0023/exec in the top-up** where the verifier fired 20/20 and short-circuited synthesis (~100× cheaper than its own unverified path) |\n| blind-debate | $141–160 | $0.39–0.60 | most expensive arm |\n| expert-panel | $99–131 | $0.23–0.29 | cheapest full collective |\n\n**Cost verdict:** at recorded prices the collective is 2–3 orders of\nmagnitude more expensive per token. The one mechanism that bends this —\ndramatically — is the verifier short-circuit. The \"higher quality AND\nlower cost\" thesis is **not** validated on this evidence; \"higher\nobjective reliability at higher cost, with a cost-collapsing verifier\npath\" is.\n\n## 6. Latency (p50 / p90, seconds)\n\n| Arm | r1 | r2 |\n|---|---|---|\n| singles | 15–29 / 28–54 | 8–29 / 25–58 |\n| expert-panel | 20.5 / 80.3 | 14.8 / 52.8 |\n| consensus | 33.4 / 66.4 | 35.3 / 81.0 |\n| blind-debate | 49.1 / 64.4 | 31.8 / 72.1 |\n\nCollectives run 1.5–4× slower at p50. Expert-panel is the fastest\ncollective; consensus pays for its verification/synthesis loop.\n\n## 7. Model sets inside the strategies (from per-execution internals)\n\n- **Core voter pool (round 2, 205 collective executions):**\n kimi-k2-0905-preview (180), Qwen3.5-397B-A17B (173), gpt-oss-120b\n (135 across two routes), Qwen3-8B (63), Llama-3.1-8B (47),\n deepseek-v4-pro (40), codestral (38), ministral-14b (36),\n mistral-large (27), GLM-4.6V, qwen3-max-preview…\n- **Winning vs losing compositions:** the same core pool appears in both\n (kimi/Qwen3.5 in 68 high-quality and 93 low-quality executions) — the\n *voters* don't separate winners from losers. The **decider** does:\n Qwen3-8B (18 high / 11 low) and mistral-large (8/2) skew winning;\n gpt-oss-120b (11/22) and Llama-3.1-8B (4/13) skew losing. Actionable:\n decider selection is a lever the harness can learn.\n- **Aggregation observed:** synthesis 36, `verified_individual` 15\n (checker override), agreement 2, best-individual fallback 2.\n- Per-execution role/cost/latency decomposition is in the internals\n artifact; intermediate voter texts are not persisted (disclosed).\n\n## 8. Validity: audits applied and defects disclosed\n\n1. **Routing fidelity:** only 65–82% of single rows were served by the\n pinned model; all single-arm numbers use faithful rows only.\n2. **DEGRADED-as-success:** 55 round-2 + 44 round-1 no-response rows\n recorded `success=true` (0 tokens) — excluded; defect filed.\n3. **Prompt contamination:** 7 round-1 single rows contain self-review\n JSON instead of task answers — classified separately; defect filed.\n4. **Judge bias, documented with receipts:** the pinned judge scored\n checker-verified correct answers 0.02–0.03 (e.g. task 119\n `FINAL: 299792458` → judge 0.02). Judge rankings measure style;\n checker rankings measure correctness. Both are reported.\n5. **A/A noise floor:** same-model alias pairs differ by 0.025–0.060\n judge points — deltas below that are not interpretable.\n6. **Skips visible:** 23 arm-budget skips in round 2 (consensus 11,\n blind-debate 11, expert-panel 1), counted in `progress.skipReasons`.\n7. **Cost metadata:** flagship $/Mtok as recorded fails list-price sanity;\n the cost axis is presented as recorded, with this caveat, and no cost\n claim is made in the thesis verdict beyond it.\n8. **Mis-election disclosed:** the mythos-9b community fork ran as a\n \"single\" arm; its strong showing (judge 0.54–0.61; checker 15/15) is\n reported but excluded from flagship claims — and is itself evidence of\n both judge style-bias and the answerability of FINAL-format tasks.\n\n## 9. Hardness of the benchmark\n\n- Frontier bar is real (actual current flagships, election logged,\n absences documented).\n- Objective ground truth on the H-A subset (numeric/regex/string checks,\n machine-verifiable, re-scored judge-free post-hoc).\n- No ceiling: hard tail where every arm fails (tasks 21/68/76 mean <0.30);\n flagships objectively erred on trivial-looking tasks at 18–32% rates.\n- Strict non-competitor judge + documented failure mode, rather than a\n friendly judge.\n- Same 38 tasks for every arm — composition confounding impossible.\n- Every table regenerable from committed raw artifacts by two scripts.\n\n## 10. Reproduce\n\n```\ncd reports/experiments\npython3 c3-frontier-audit-9590ff41.py # round-1 audit (+ --pooled)\npython3 c3-frontier2-objective-ha-569ce880.py # round-2 excerpt scoring\npython3 c3-campaign-objective-ha-final.py # DEFINITIVE objective H-A\npython3 c3-publication-tables.py # cost/latency/win-loss/voters\n```\n\nArtifacts: `c3-frontier-executions-9590ff41.csv`,\n`c3-frontier-topup-executions-f7b76768.csv`,\n`c3-frontier2-executions-569ce880.csv`,\n`c3-frontier2-hardness-{tasks,outputs,internals}-569ce880.csv`,\n`c3-frontier{,-topup,2}-answer-tails-*.csv`. Full narrative:\n`c3-frontier-report-9590ff41.md` (+2 addenda),\n`c3-frontier2-hardness-dossier-569ce880.md` (§1–9-FINAL).\n\n## Addendum A (2026-07-06): H-B instantiation attempt — routing-fidelity failure, quality mechanism replicated\n\nA mini-run (`c3-hb-mixed-minirun`, experiment `36183d17`) attempted to\ninstantiate pre-registered **H-B** (own/self-hosted model + cheap external\nmodels as a mixed collective) using Ollama (`qwen3:8b`, `llama3.2:3b`) on\nthe project's CPU VPS. Full report:\n`c3-hb-minirun-report-36183d17.md`; raw artifacts\n`c3-hb-executions-36183d17.csv`, `c3-hb-answer-tails-36183d17.csv`.\n\n- **H-B remains NOT INSTANTIATED.** The routing-fidelity audit found\n **0 of 283** executions served by the local models: the model-equivalence\n layer substituted external same-family providers for the own arms\n (`qwen3:8b` → `Qwen/Qwen2.5-Coder-32B`, `Llama-3.3-70B`,\n `DeepSeek-V4-Flash`, …). Same defect class as the round-1 single-arm\n silent fallback; for H-B it is disqualifying, so no claim about own\n infrastructure is made in either direction. A provider-pinned execution\n path for own arms is filed as the blocking defect for H-B v2.\n- **The quality mechanism replicated a third time** on the as-served\n cheap-external arms (objective checker, verifiable tasks): dynamic\n consensus **11/11 (100%)**, mixed forced-pool consensus **10/11 (91%)**\n vs singles **60–70%** — consistent with the campaign's consensus 97% vs\n flagships 68–82%.\n- Infrastructure that DID land and verify: Ollama service on the swarm,\n discovery of ollama models into the runtime inventory (after the\n `OLLAMA_URL` `/v1`-suffix normalization fix, PR #68), own-model\n resolvers/config/gates (PR #67), and skip accounting (454 skips fully\n attributed when the $20 cap was exhausted at $25.12 — the dynamic arm's\n router-chosen strategies consumed >$18 on 7 executions, a further\n instance of the pricing-metadata caveat now shown to also *starve arms\n via the budget governor*, not just misreport cost).\n"}, "70e542f201c7de16aef8b6f54ad3a419a4943015": {"t": 1785754620.9221663, "n": 14066, "h": "eb780f42963c8b08", "c": "<!--\nCopyright (C) 2026 Ailin One, Inc.\n\nThis file is part of Collective Intelligence Engine (ci).\nLicensed under the GNU Affero General Public License v3.0 or later.\nSee LICENSE in the repository root, or <https://www.gnu.org/licenses/>.\n\nSPDX-License-Identifier: AGPL-3.0-or-later\nSource: https://github.com/ailinone/collective-intelligence\n-->\n\n# Ailin Collective-vs-Frontier Benchmark — July 2026\n\n**Question under test:** can a collective of cheap, diverse models beat the\ncurrent frontier single models — and at what cost?\n\n**Campaign:** 3 runs, 2026-07-05 (single day, single code vintage, same\npinned judge): `9590ff41` (round 1, 4 flagships × 3 collectives),\n`f7b76768` (H-A top-up, collectives only), `569ce880` (round 2, 8 singles\nincl. Claude Fable 5 × 3 collectives). **1,278 executions persisted,\n~$143 total spend.** Every number below is reproducible from the committed\nCSV artifacts via the committed scripts (see §10).\n\n---\n\n## 1. TL;DR — the verdict, split by axis\n\n| Claim | Verdict | Evidence |\n|---|---|---|\n| Collective **with objective verifier** beats every frontier single on verifiable tasks (H-A, quality) | **VALIDATED** | Checker pass-rate 97% (37/38) vs flagships' 68–82% (§3) |\n| Collectives **without** the verifier beat frontier singles on verifiable tasks | **NOT validated** | blind-debate 77% / expert-panel 81% — inside the flagship range (§3) |\n| Collective beats frontier singles on open-ended tasks (H-C) | **NOT validated — parity** | Judge-metric deltas within ±0.06 A/A noise; loses on creative/refactoring, wins/ties on reasoning (§4) |\n| Collective is **cheaper per token** | **INVALIDATED** (as recorded) | Collectives $99–181/Mtok vs singles $0.14–1/Mtok recorded (§5) — but see the verifier short-circuit: $2.98/Mtok when it fires |\n| Collective is faster | **INVALIDATED** | p50 latency 15–49s vs 8–29s for singles (§6) |\n\n**One-sentence public claim that survives every audit in this campaign:**\n*a ~$0.002–0.26/query ensemble of sub-frontier open-weight models, when\narmed with a deterministic answer-verifier, produced objectively correct\nanswers more reliably (97%) than any of GPT-5.5-pro, Claude Opus 4.8,\nClaude Fable 5, Gemini 3.1 Pro or Grok 4.3 (65–82%) on the verifiable\nsubset of this suite — while costing 2–3 orders of magnitude more per\ntoken than the recorded single-model prices and running 2–4× slower.*\n\n## 2. Setup\n\n- **Single arms (pinned per run, election logged):** round 1:\n `gpt-5.5-pro-2026-04-23`, `claude-opus-4-8`, `gemini-3.1-pro`,\n `grok-4.3`. Round 2 adds: `claude-fable-5`, `gpt-5.5-pro` (A/A pair),\n `google/gemini-3.1-pro` (A/A pair), and — disclosed mis-election — the\n community fork `King3Djbl/mythos-9b-unhinged` (matched the `mythos`\n pattern; NOT a flagship; kept, analyzed separately). GPT-5.6, Gemini 3.5\n and Claude Mythos 5 were not present in the live catalog.\n- **Collective arms:** `consensus` (best-of-N answer-verifier armed on the\n verifiable subset), `blind-debate`, `expert-panel`. Voter pools are\n cheap open-weight/small models (kimi-k2, Qwen3.5-397B/8B, gpt-oss-120b,\n Llama-3.1-8B, ministral/codestral/mistral, GLM-4.6V…) — **no flagship\n ever appears as a voter** (verified from per-execution internals).\n- **Tasks:** 38 (stratified sample of the 126-task suite ∪ all 10\n verifiable tasks 116–125 with machine-checkable answers). Types:\n reasoning 10, strategy-specific 7, code-gen 4, creative 3, debugging 2,\n refactoring 2, adversarial 2, factual-QA 2, analysis/documentation/stt/\n video/leader/compositor 1 each.\n- **Judge:** pinned `deepseek-v4-pro` (DeepSeek family — non-competitor;\n serving id observed `DeepSeek-V4-Flash`). Its bias against terse\n verified answers is documented (§8) — hence the checker is the primary\n metric on verifiable tasks.\n- **Metrics:** (a) objective checker pass-rate (deterministic\n `answer_check`, judge-free) on tasks 116–125; (b) judge quality 0–1 on\n the full suite, **routing-faithful rows only** (single-arm rows served\n by a fallback model are excluded — fidelity was 65–82%).\n\n## 3. Leaderboard A — OBJECTIVE correctness (verifiable tasks, all 3 runs pooled)\n\n| Arm | correct | wrong | degraded† | contaminated† | **pass-rate** |\n|---|---|---|---|---|---|\n| **consensus + verifier** | **37** | **1** | 0 | 0 | **97%** (n=38) |\n| claude-fable-5 * | 15 | 0 | 5 | 0 | 100% (n=15) |\n| mythos-9b fork * | 15 | 0 | 5 | 0 | 100% (n=15) |\n| google/gemini-3.1-pro * | 14 | 1 | 5 | 0 | 93% (n=15) |\n| gpt-5.5-pro * | 14 | 3 | 3 | 0 | 82% (n=17) |\n| claude-opus-4-8 | 23 | 5 | 9 | 3 | 82% (n=28) |\n| expert-panel (no verifier) | 39 | 9 | 0 | 0 | 81% (n=48) |\n| gemini-3.1-pro | 24 | 6 | 9 | 1 | 80% (n=30) |\n| blind-debate (no verifier) | 27 | 8 | 0 | 0 | 77% (n=35) |\n| gpt-5.5-pro-2026-04-23 | 22 | 10 | 7 | 1 | 69% (n=32) |\n| grok-4.3 | 25 | 12 | 1 | 2 | 68% (n=37) |\n\n\\* round-2-only arms (smaller n). † `[DEGRADED]` no-response placeholders\nand self-review prompt-contamination rows are harness failures, counted\nseparately, never as model errors (§8).\n\nConsensus' single miss is a truncated generation (no `FINAL:` emitted) —\nacross 3 runs the verifier **never selected an objectively wrong answer**\n(53/55 verified-selection telemetry, 0 checker failures). Flagship errors\nare concrete and listed verbatim in the script output: grok answering\n`0.984` where `98.41%` was required, emitting `FINAL:` with empty payload,\nanswering the wrong prompt; gemini answering `99.999995%`; models\nanswering profit-*increase* (2500) where the new *total* (44500) was asked.\n\n## 4. Leaderboard B — judge metric, and WHERE the collective wins/loses\n\nFull-suite judge scores (faithful rows): round 1 — consensus 0.664 >\nbest single 0.615; round 2 — blind-debate 0.621 > best single 0.608. In\nboth rounds the best collective arm tops the table, but **which** strategy\nleads varies; treat as an arm-class result, not a strategy result.\n\n**Per-task-type map (round 2, best collective vs best faithful single):**\n\n| Task type | W/L/T | Reading |\n|---|---|---|\n| reasoning | **3W/0L/7T** | collective never loses; wins concentrate here |\n| adversarial | 1W/0L/1T | collective robust |\n| factual-QA | 0W/0L/2T | ceiling for both |\n| code-generation | 1W/2L/1T | mixed |\n| debugging | 0W/1L/1T | mixed-negative |\n| creative | **0W/3L/0T** | single models win open prose |\n| refactoring | 0W/2L/0T | single models win |\n| documentation / analysis / stt | 0W/3L | single models win |\n| by complexity | low 4W/4L/3T · medium 1W/4L/5T · high 0W/3L/5T | collective edge shrinks as open-endedness grows |\n\nThe full per-task table (38 rows: task, type, winning arm, scores, delta)\nis emitted by `c3-publication-tables.py §B`. Caveat: this map uses the\njudge metric, whose pro-prose bias (§8) systematically favors singles on\nopen tasks and *understates* the collective on verifiable ones.\n\n## 5. Cost — per Mtok, per execution, per success (AS RECORDED)\n\n| Arm | $/Mtok | $/execution | notes |\n|---|---|---|---|\n| singles (flagships) | **$0.14–1.02** | $0.0002–0.0018 | recorded catalog prices FAIL a list-price sanity check (frontier models at ~$0.2/Mtok is implausible) — cost axis is reported, not adjudicated |\n| grok-4.3 | $0.47–0.56 | $0.0007–0.0010 | most plausible frontier pricing |\n| consensus | $118–181 | $0.26–0.44 | **$2.98/Mtok, $0.0023/exec in the top-up** where the verifier fired 20/20 and short-circuited synthesis (~100× cheaper than its own unverified path) |\n| blind-debate | $141–160 | $0.39–0.60 | most expensive arm |\n| expert-panel | $99–131 | $0.23–0.29 | cheapest full collective |\n\n**Cost verdict:** at recorded prices the collective is 2–3 orders of\nmagnitude more expensive per token. The one mechanism that bends this —\ndramatically — is the verifier short-circuit. The \"higher quality AND\nlower cost\" thesis is **not** validated on this evidence; \"higher\nobjective reliability at higher cost, with a cost-collapsing verifier\npath\" is.\n\n## 6. Latency (p50 / p90, seconds)\n\n| Arm | r1 | r2 |\n|---|---|---|\n| singles | 15–29 / 28–54 | 8–29 / 25–58 |\n| expert-panel | 20.5 / 80.3 | 14.8 / 52.8 |\n| consensus | 33.4 / 66.4 | 35.3 / 81.0 |\n| blind-debate | 49.1 / 64.4 | 31.8 / 72.1 |\n\nCollectives run 1.5–4× slower at p50. Expert-panel is the fastest\ncollective; consensus pays for its verification/synthesis loop.\n\n## 7. Model sets inside the strategies (from per-execution internals)\n\n- **Core voter pool (round 2, 205 collective executions):**\n kimi-k2-0905-preview (180), Qwen3.5-397B-A17B (173), gpt-oss-120b\n (135 across two routes), Qwen3-8B (63), Llama-3.1-8B (47),\n deepseek-v4-pro (40), codestral (38), ministral-14b (36),\n mistral-large (27), GLM-4.6V, qwen3-max-preview…\n- **Winning vs losing compositions:** the same core pool appears in both\n (kimi/Qwen3.5 in 68 high-quality and 93 low-quality executions) — the\n *voters* don't separate winners from losers. The **decider** does:\n Qwen3-8B (18 high / 11 low) and mistral-large (8/2) skew winning;\n gpt-oss-120b (11/22) and Llama-3.1-8B (4/13) skew losing. Actionable:\n decider selection is a lever the harness can learn.\n- **Aggregation observed:** synthesis 36, `verified_individual` 15\n (checker override), agreement 2, best-individual fallback 2.\n- Per-execution role/cost/latency decomposition is in the internals\n artifact; intermediate voter texts are not persisted (disclosed).\n\n## 8. Validity: audits applied and defects disclosed\n\n1. **Routing fidelity:** only 65–82% of single rows were served by the\n pinned model; all single-arm numbers use faithful rows only.\n2. **DEGRADED-as-success:** 55 round-2 + 44 round-1 no-response rows\n recorded `success=true` (0 tokens) — excluded; defect filed.\n3. **Prompt contamination:** 7 round-1 single rows contain self-review\n JSON instead of task answers — classified separately; defect filed.\n4. **Judge bias, documented with receipts:** the pinned judge scored\n checker-verified correct answers 0.02–0.03 (e.g. task 119\n `FINAL: 299792458` → judge 0.02). Judge rankings measure style;\n checker rankings measure correctness. Both are reported.\n5. **A/A noise floor:** same-model alias pairs differ by 0.025–0.060\n judge points — deltas below that are not interpretable.\n6. **Skips visible:** 23 arm-budget skips in round 2 (consensus 11,\n blind-debate 11, expert-panel 1), counted in `progress.skipReasons`.\n7. **Cost metadata:** flagship $/Mtok as recorded fails list-price sanity;\n the cost axis is presented as recorded, with this caveat, and no cost\n claim is made in the thesis verdict beyond it.\n8. **Mis-election disclosed:** the mythos-9b community fork ran as a\n \"single\" arm; its strong showing (judge 0.54–0.61; checker 15/15) is\n reported but excluded from flagship claims — and is itself evidence of\n both judge style-bias and the answerability of FINAL-format tasks.\n\n## 9. Hardness of the benchmark\n\n- Frontier bar is real (actual current flagships, election logged,\n absences documented).\n- Objective ground truth on the H-A subset (numeric/regex/string checks,\n machine-verifiable, re-scored judge-free post-hoc).\n- No ceiling: hard tail where every arm fails (tasks 21/68/76 mean <0.30);\n flagships objectively erred on trivial-looking tasks at 18–32% rates.\n- Strict non-competitor judge + documented failure mode, rather than a\n friendly judge.\n- Same 38 tasks for every arm — composition confounding impossible.\n- Every table regenerable from committed raw artifacts by two scripts.\n\n## 10. Reproduce\n\n```\ncd reports/experiments\npython3 c3-frontier-audit-9590ff41.py # round-1 audit (+ --pooled)\npython3 c3-frontier2-objective-ha-569ce880.py # round-2 excerpt scoring\npython3 c3-campaign-objective-ha-final.py # DEFINITIVE objective H-A\npython3 c3-publication-tables.py # cost/latency/win-loss/voters\n```\n\nArtifacts: `c3-frontier-executions-9590ff41.csv`,\n`c3-frontier-topup-executions-f7b76768.csv`,\n`c3-frontier2-executions-569ce880.csv`,\n`c3-frontier2-hardness-{tasks,outputs,internals}-569ce880.csv`,\n`c3-frontier{,-topup,2}-answer-tails-*.csv`. Full narrative:\n`c3-frontier-report-9590ff41.md` (+2 addenda),\n`c3-frontier2-hardness-dossier-569ce880.md` (§1–9-FINAL).\n\n## Addendum A (2026-07-06): H-B instantiation attempt — routing-fidelity failure, quality mechanism replicated\n\nA mini-run (`c3-hb-mixed-minirun`, experiment `36183d17`) attempted to\ninstantiate pre-registered **H-B** (own/self-hosted model + cheap external\nmodels as a mixed collective) using Ollama (`qwen3:8b`, `llama3.2:3b`) on\nthe project's CPU VPS. Full report:\n`c3-hb-minirun-report-36183d17.md`; raw artifacts\n`c3-hb-executions-36183d17.csv`, `c3-hb-answer-tails-36183d17.csv`.\n\n- **H-B remains NOT INSTANTIATED.** The routing-fidelity audit found\n **0 of 283** executions served by the local models: the model-equivalence\n layer substituted external same-family providers for the own arms\n (`qwen3:8b` → `Qwen/Qwen2.5-Coder-32B`, `Llama-3.3-70B`,\n `DeepSeek-V4-Flash`, …). Same defect class as the round-1 single-arm\n silent fallback; for H-B it is disqualifying, so no claim about own\n infrastructure is made in either direction. A provider-pinned execution\n path for own arms is filed as the blocking defect for H-B v2.\n- **The quality mechanism replicated a third time** on the as-served\n cheap-external arms (objective checker, verifiable tasks): dynamic\n consensus **11/11 (100%)**, mixed forced-pool consensus **10/11 (91%)**\n vs singles **60–70%** — consistent with the campaign's consensus 97% vs\n flagships 68–82%.\n- Infrastructure that DID land and verify: Ollama service on the swarm,\n discovery of ollama models into the runtime inventory (after the\n `OLLAMA_URL` `/v1`-suffix normalization fix, PR #68), own-model\n resolvers/config/gates (PR #67), and skip accounting (454 skips fully\n attributed when the $20 cap was exhausted at $25.12 — the dynamic arm's\n router-chosen strategies consumed >$18 on 7 executions, a further\n instance of the pricing-metadata caveat now shown to also *starve arms\n via the budget governor*, not just misreport cost).\n"}}}}