{"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 .\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._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._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();\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 = {\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 {\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 {\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 .\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._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._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();\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 = {\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 {\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 {\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 .\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._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._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();\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 = {\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 {\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 {\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 .\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._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._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();\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 = {\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 {\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 {\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 .\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._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._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();\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 = {\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 {\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 {\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 .\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._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._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();\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 = {\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 {\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 {\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 .\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._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._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();\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 = {\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 {\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 {\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 .\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._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._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();\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 = {\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 {\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 {\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"}, "bfc0be00dcf64493a39936a5c9db274fd1302e52": {"t": 1786068279.1985734, "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 .\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._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._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();\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 = {\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 {\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 {\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"}}, "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 .\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 `_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-- 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://.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: ` (sample in docs omits\n // the `Bearer ` prefix; the hub adapter currently sends `Bearer `,\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\":\"\"`\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\":\"\"`\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 _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 `_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 .\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 `_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-- 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://.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: ` (sample in docs omits\n // the `Bearer ` prefix; the hub adapter currently sends `Bearer `,\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\":\"\"`\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\":\"\"`\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 _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 `_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 .\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 = {\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/ 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/.{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/ 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 // _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 = {\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 = {\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 _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 \"} (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 .\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 = {\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/ 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/.{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/ 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 // _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 = {\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 = {\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 _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 \"} (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": "\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": "\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"}, "b748363822c6e602b7a1e3d6c098e8ea9eefcf3b": {"t": 1786068096.6234336, "n": 14066, "h": "eb780f42963c8b08", "c": "\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"}, "bfc0be00dcf64493a39936a5c9db274fd1302e52": {"t": 1786068570.0296888, "n": 14066, "h": "eb780f42963c8b08", "c": "\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"}}}}