Files
OmniRoute/src/shared/constants/providers.ts
Diego Rodrigues de Sa e Souza abd4df63dc fix(sse): surface Qwen/Alibaba personal Token Plan quota in dashboard and preflight (#10290)
* fix(sse): surface Qwen/Alibaba personal Token Plan quota in dashboard and preflight

The personal Token Plan (5-hour / 7-day sliding windows) has no official
OpenAPI and the inference API key cannot read it. Add a cookie-authenticated
fetcher for the console gateway shared by home.qwencloud.com and the Model
Studio console (contract captured live from a logged-in session):

- open-sse/services/qwenTokenPlanQuotaFetcher.ts: POST /data/api.json
  (IntlBroadScopeAspnGateway / sfm_bailian) for usage + quota-config +
  subscription; sec_token resolved best-effort from the dashboard HTML;
  per-window parse (fields are omitted while a window is Temporarily
  Removed); 60s usage cache, 1h tier cache.
- usage/qwen-token-plan.ts leaf + registration in the usage dispatcher,
  USAGE_FETCHER_PROVIDERS, USAGE_SUPPORTED_PROVIDERS,
  PROVIDER_LIMITS_APIKEY_PROVIDERS and bespoke preflight/monitor windows.
- Also adds bailian-coding-plan to USAGE_SUPPORTED_PROVIDERS /
  PROVIDER_LIMITS_APIKEY_PROVIDERS: the coding-plan fetcher existed but the
  dashboard filtered those connections out (UI gap).

Refs #9603 (Problema 1 — quota missing; the 429 recovery half is a
follow-up).

* docs(env): document Qwen Token Plan quota env vars + regen omni-settings skill

QWEN_CLOUD_COOKIE, QWEN_CLOUD_SEC_TOKEN, QWEN_TOKEN_PLAN_HOST and
QWEN_TOKEN_PLAN_DASHBOARD_URL added to .env.example and
docs/reference/ENVIRONMENT.md (check:env-doc-sync), with the generated
omni-settings skill refreshed (check:agent-skills-sync).

Refs #9603

* revert: keep hand-tuned omni-settings thinking-budget section

The agent-skills-sync drift predates this PR (hand improvement from #10169
not yet synced into the generator source) — it fails on every open PR and
belongs to a base-reds fix, not this branch. Regenerating here would erase
the intentional content.

* feat(dashboard): add the Qwen/Model Studio console cookie field to the connection modal

The Token Plan quota fetcher is cookie-authenticated (the inference API key
cannot read the console gateway), but no modal field existed to paste that
cookie — so the quota was unconfigurable from the dashboard and the fetcher
could only ever return its 'needs a cookie' message.

Adds the field for qwen-cloud-token-plan and bailian-coding-plan alongside the
existing ollama-cloud / alibaba console-cookie inputs (same password-input,
blank-keeps-stored semantics), pre-fills it when editing a connection, and
extends the providerSpecificData string/length validation to the two new keys.

Tests: tests/unit/qwen-token-plan-cookie-field.test.ts (RED before, GREEN
after) covers persistence + trimming, the blank-input no-overwrite rule and
schema acceptance/rejection.

Refs #9603

* docs(dashboard): correct the Qwen console cookie instructions

The placeholder claimed the cookie looks like 'token=...'; the qwencloud
portal actually issues 'login_qwencloud_ticket=...' alongside cna/cnaui/aui
(mirroring login_aliyunid_ticket on the Alibaba console), so the hint pointed
at the wrong value.

Replaces the guesswork with the verified retrieval steps in all three places
an operator can hit — the modal field hint, the fetcher's 'needs a cookie'
message and .env.example/ENVIRONMENT.md: log in to home.qwencloud.com >
Billing > Subscription, F12 > Network, reload, filter by api.json, click a
request to cs-data.qwencloud.com and copy the WHOLE Cookie request header.
Also documents that the value must go on one line (it contains '=' and ';')
and that it dies with the browser session.

Refs #9603

* fix(dashboard): tolerate partial form objects in the qwen cookie branch

Adding bailian-coding-plan to QWEN_TOKEN_PLAN_PROVIDERS routed callers that
previously matched NO branch in assignQuotaScrapingProviderData into the new
one, which assumed the two new fields are always present. Older callers build
a partial form object, so buildAddProviderSpecificData threw:

  TypeError: Cannot read properties of undefined (reading 'trim')
  (tests/unit/dashboard/agentrouter-connection-modal-fields.test.ts)

Reads the new fields with optional chaining and adds a regression test that
calls the helper with those keys deleted for both providers.

Refs #9603

* refactor(dashboard): move quota-scraping form logic into a UI-free module

tests/unit/qwen-token-plan-cookie-field.test.ts imported QuotaScrapingFields
directly, which pulls `@/shared/components` and, through that barrel,
untranspiled ESM (@lobehub/icons). The node:test runner cannot parse it and
the whole test file died in CI with:

  SyntaxError: Unexpected token 'export'
    at @lobehub/icons/es/Ai21/components/Mono.js

(It passed locally, so only the CI shard surfaced it.)

Extracts the pure pieces — QWEN_TOKEN_PLAN_PROVIDERS, QuotaScrapingFieldValues,
EMPTY_QUOTA_SCRAPING_FIELDS and assignQuotaScrapingProviderData — into
quotaScrapingFieldValues.ts. The component imports them and re-exports the
public names, so every existing importer keeps its current path. The unit test
now targets the UI-free module.

Refs #9603

* fix(providers): point bailian-coding-plan at the Token Plan endpoint and its console

Two independent defects kept this provider unusable with a valid Alibaba
Token Plan key (verified live 2026-08-14 with the owner's key and cookie):

1. Wrong inference host. The catalog entry is named "Alibaba Token Plan",
   links to token-plan-overview and its hint asks for a Token Plan key, but
   the registry pointed at coding-intl.dashscope.aliyuncs.com — the Coding
   Plan host, which rejects Token Plan keys with 401 invalid_api_key. The
   documented Anthropic base URL for Token Plan is
   token-plan.ap-southeast-1.maas.aliyuncs.com/apps/anthropic
   (https://www.alibabacloud.com/help/en/model-studio/more-tools). Against
   the new host the same key returns 200 for all six registry models and a
   real completion; auth stays on x-api-key.

2. Wrong console identity for quota. The personal Token Plan is sold through
   two consoles sharing one backend, and the gateway validates the session
   against the console declared in the request: an Alibaba console cookie
   (login_aliyunid_ticket) sent with the QwenCloud identity is refused with
   BailianGateway.Login.NotLogined. resolveConsoleSite() now picks host,
   cornerstoneParam.consoleSite/domain and Origin/Referer from the cookie's
   login ticket, falling back to the provider. With that switch the same
   cookie returns usage/subscription/quota-config.

Also routes bailian-coding-plan quota through the Token Plan fetcher (the
Coding Plan call returns "Bad Request" for these accounts), keeping the old
fetcher as the fallback for real Coding Plan keys, and labels the plan by
console ("Alibaba Token Plan (Pro)" vs "Qwen …").

Live validation: inference 200 (qwen3.7-plus answered "FUNCIONA"); quota
12,934/40,000 credits, 67.7% remaining, resets 2026-08-20.

Refs #9603

---------

Co-authored-by: Xiangzhe <bakryun0718@proton.me>
2026-08-14 15:40:33 -03:00

536 lines
15 KiB
TypeScript

// Re-export service kinds from leaf module (avoids circular dep with providerSchema)
export type { ServiceKind } from "./serviceKinds";
export { SERVICE_KIND_VALUES } from "./serviceKinds";
export type RiskNoticeVariant = "oauth" | "webCookie" | "deprecated" | "embedded-service";
export interface ProviderRiskNoticeFields {
subscriptionRisk?: boolean;
riskNoticeVariant?: RiskNoticeVariant;
isEmbeddedService?: boolean;
}
import { NOAUTH_PROVIDERS } from "./providers/noauth";
export { supportsNoAuthProviderProxy } from "./providers/noauth";
import { OAUTH_PROVIDERS } from "./providers/oauth";
import { WEB_COOKIE_PROVIDERS, resolveWebProviderHost } from "./providers/web-cookie";
export { resolveWebProviderHost };
export type { WebProviderHostLink } from "./providers/web-cookie";
import { APIKEY_PROVIDERS } from "./providers/apikey";
import { LOCAL_PROVIDERS } from "./providers/local";
import { SEARCH_PROVIDERS } from "./providers/search";
import { AUDIO_ONLY_PROVIDERS } from "./providers/audio";
import { UPSTREAM_PROXY_PROVIDERS } from "./providers/upstream-proxy";
import { CLOUD_AGENT_PROVIDERS } from "./providers/cloud-agent";
import { SYSTEM_PROVIDERS } from "./providers/system";
export const FREE_PROVIDERS = {};
// No-auth Providers
export const FREE_APIKEY_PROVIDER_IDS = new Set([
"qoder",
"mimocode",
"opencode",
"dahl",
// auggie is a fully local, credential-less CLI passthrough (auth handled by
// `auggie login` outside OmniRoute). Admitted here purely so POST /api/providers
// accepts an optional connection row for display/priority/testStatus tracking —
// no apiKey is ever required or sent upstream.
"auggie",
]);
export function supportsApiKeyOnFreeProvider(providerId: unknown): boolean {
return typeof providerId === "string" && FREE_APIKEY_PROVIDER_IDS.has(providerId);
}
// OAuth-primary providers that also accept a direct API key. Keep these out of
// FREE_APIKEY_PROVIDER_IDS so the dashboard's primary action remains OAuth.
const DUAL_AUTH_PROVIDER_IDS = new Set(["clinepass", "codebuddy-cn"]);
export function supportsDualAuthProvider(providerId: unknown): boolean {
return typeof providerId === "string" && DUAL_AUTH_PROVIDER_IDS.has(providerId);
}
// Web / Cookie Providers
// API Key Providers
// Sub-categories within APIKEY_PROVIDERS (used by dashboard and catalog views).
export const IMAGE_ONLY_PROVIDER_IDS = new Set([
"nanobanana",
"fal-ai",
"stability-ai",
"black-forest-labs",
"recraft",
"topaz",
"segmind",
"freepik",
"deepai",
]);
export const AGGREGATOR_PROVIDER_IDS = new Set([
"openrouter",
"synthetic",
"kilo-gateway",
"aimlapi",
"novita",
"piapi",
"getgoapi",
"laozhang",
"vercel-ai-gateway",
"unorouter",
"agentrouter",
"thebai",
"fenayai",
"empower",
"poe",
"chutes",
"hackclub",
"freetheai",
"g4f-groq",
"g4f-gemini",
"g4f-pollinations",
"g4f-ollama",
"g4f-nvidia",
"naga-ac",
"chatanywhere",
"zylo-api",
"fastrouter",
"anyapi",
"electronhub",
"llmgateway",
"llm-kiwi",
"literouter",
"mnn-ai",
"meganova-ai",
"mixlayer",
"speka",
"tokenreply",
"yolo-auto",
"dxnt",
"cloudcode-one",
"ofoxai",
"zerolimitai",
"helyxai",
"auriko",
"poixe-ai",
"naga-ai",
"chat-oripe",
"freeinference",
"free-ai",
"void-ai",
"helixmind",
]);;
export const ENTERPRISE_CLOUD_PROVIDER_IDS = new Set([
"azure-openai",
"azure-ai",
"bedrock",
"watsonx",
"oci",
"sap",
"vertex",
"vertex-partner",
"databricks",
"datarobot",
"clarifai",
"snowflake",
"heroku",
"modal",
]);
export const VIDEO_PROVIDER_IDS = new Set([
"agnes",
"runwayml",
"veoaifree-web",
"pollinations",
"minimax",
"together",
"replicate",
"haiper",
"leonardo",
"segmind",
"novita",
]);
// IDE Providers: editors with built-in AI subscription (separate section in UI).
// These providers live in OAUTH_PROVIDERS but render under "IDE Providers"
// instead of "OAuth Providers" to avoid visual duplication.
export const IDE_PROVIDER_IDS = new Set(["cursor", "zed", "trae", "raycast"]);
export const EMBEDDING_RERANK_PROVIDER_IDS = new Set(["voyage-ai", "jina-ai"]);
// Local / Self-Hosted Providers
// Search Providers
// Audio Only Providers
export const OPENAI_COMPATIBLE_PREFIX = "openai-compatible-";
export const ANTHROPIC_COMPATIBLE_PREFIX = "anthropic-compatible-";
export const CLAUDE_CODE_COMPATIBLE_PREFIX = "anthropic-compatible-cc-";
export function isOpenAICompatibleProvider(providerId: unknown): providerId is string {
return typeof providerId === "string" && providerId.startsWith(OPENAI_COMPATIBLE_PREFIX);
}
export function isAnthropicCompatibleProvider(providerId: unknown): providerId is string {
return typeof providerId === "string" && providerId.startsWith(ANTHROPIC_COMPATIBLE_PREFIX);
}
export function isClaudeCodeCompatibleProvider(providerId: unknown): providerId is string {
return typeof providerId === "string" && providerId.startsWith(CLAUDE_CODE_COMPATIBLE_PREFIX);
}
export function isLocalProvider(providerId: unknown): boolean {
return (
typeof providerId === "string" &&
Object.prototype.hasOwnProperty.call(LOCAL_PROVIDERS, providerId)
);
}
export const SELF_HOSTED_CHAT_PROVIDER_IDS = new Set([
"ollama-local",
"lm-studio",
"vllm",
"lemonade",
"llamafile",
"llama-cpp",
"triton",
"docker-model-runner",
"xinference",
"oobabooga",
]);
export function isSelfHostedChatProvider(providerId: unknown): boolean {
return typeof providerId === "string" && SELF_HOSTED_CHAT_PROVIDER_IDS.has(providerId);
}
// Providers with heterogeneous/no-key auth that don't fit the NOAUTH_PROVIDERS
// registry (e.g. free-tier gateways where a key is accepted but not required).
// Kept as a Set (not an || chain) to keep providerAllowsOptionalApiKey's
// cyclomatic complexity flat as this list grows — see g4f.space (#6650).
const EXPLICIT_OPTIONAL_APIKEY_PROVIDER_IDS = new Set([
"searxng-search",
"firecrawl",
"pollinations",
"copilot-web",
"hackclub",
"g4f-groq",
"g4f-gemini",
"g4f-pollinations",
"g4f-ollama",
"g4f-nvidia",
"huggingchat",
"gitlawb",
"gitlawb-gmi",
"naga-ac",
]);
export function providerAllowsOptionalApiKey(providerId: unknown): boolean {
return (
// ponytail: any noAuth provider auto-qualifies — no per-provider maintenance
(typeof providerId === "string" && providerId in NOAUTH_PROVIDERS) ||
(typeof providerId === "string" && EXPLICIT_OPTIONAL_APIKEY_PROVIDER_IDS.has(providerId)) ||
isLocalProvider(providerId) ||
isSelfHostedChatProvider(providerId) ||
isOpenAICompatibleProvider(providerId) ||
isAnthropicCompatibleProvider(providerId)
);
}
/**
* Providers explicitly excluded from bulk API key add — auth is heterogeneous,
* OAuth-based, multi-field, or requires manual setup per connection.
*/
const BULK_API_KEY_EXCLUDED = new Set([
"vertex",
"vertex-partner",
"ollama-local",
"grok-web",
"perplexity-web",
"blackbox-web",
"muse-spark-web",
"deepseek-web",
"inner-ai",
"qoder",
"google-pse-search",
"command-code",
"azure",
]);
export function supportsBulkApiKey(providerId: unknown): boolean {
if (typeof providerId !== "string" || !providerId) return false;
if (BULK_API_KEY_EXCLUDED.has(providerId)) return false;
if (isLocalProvider(providerId)) return false;
if (isSelfHostedChatProvider(providerId)) return false;
if (isClaudeCodeCompatibleProvider(providerId)) return false;
return true;
}
// ── System Providers (virtual, not user-connectable) ──────────────────────────
const _PROVIDER_SECTIONS = [
NOAUTH_PROVIDERS,
OAUTH_PROVIDERS,
APIKEY_PROVIDERS,
WEB_COOKIE_PROVIDERS,
LOCAL_PROVIDERS,
SEARCH_PROVIDERS,
AUDIO_ONLY_PROVIDERS,
UPSTREAM_PROXY_PROVIDERS,
CLOUD_AGENT_PROVIDERS,
SYSTEM_PROVIDERS,
] as const;
let _aiProviders: Record<string, any> | null = null;
function getOrCreateAiProviders(): Record<string, any> {
if (!_aiProviders) {
_aiProviders = {};
for (const section of _PROVIDER_SECTIONS) {
Object.assign(_aiProviders, section);
}
}
return _aiProviders;
}
let _ALIAS_TO_ID: Record<string, string> | null = null;
function getOrCreateAliasToId(): Record<string, string> {
if (!_ALIAS_TO_ID) {
_ALIAS_TO_ID = {};
for (const section of _PROVIDER_SECTIONS) {
for (const p of Object.values(section)) {
if ((p as any).alias) _ALIAS_TO_ID[(p as any).alias] = (p as any).id;
}
}
}
return _ALIAS_TO_ID;
}
let _ID_TO_ALIAS: Record<string, string> | null = null;
function getOrCreateIdToAlias(): Record<string, string> {
if (!_ID_TO_ALIAS) {
_ID_TO_ALIAS = {};
for (const section of _PROVIDER_SECTIONS) {
for (const p of Object.values(section)) {
_ID_TO_ALIAS[(p as any).id] = (p as any).alias || (p as any).id;
}
}
}
return _ID_TO_ALIAS;
}
export function getProviderById(id: string) {
return (
(NOAUTH_PROVIDERS as Record<string, any>)[id] ??
(OAUTH_PROVIDERS as Record<string, any>)[id] ??
(APIKEY_PROVIDERS as Record<string, any>)[id] ??
(WEB_COOKIE_PROVIDERS as Record<string, any>)[id] ??
(LOCAL_PROVIDERS as Record<string, any>)[id] ??
(SEARCH_PROVIDERS as Record<string, any>)[id] ??
(AUDIO_ONLY_PROVIDERS as Record<string, any>)[id] ??
(UPSTREAM_PROXY_PROVIDERS as Record<string, any>)[id] ??
(CLOUD_AGENT_PROVIDERS as Record<string, any>)[id] ??
(SYSTEM_PROVIDERS as Record<string, any>)[id] ??
undefined
);
}
export const AI_PROVIDERS = new Proxy({} as Record<string, any>, {
get(_, key) {
if (key === "then") return undefined;
return typeof key === "string" ? getOrCreateAiProviders()[key] : undefined;
},
ownKeys() {
return Reflect.ownKeys(getOrCreateAiProviders());
},
has(_, key) {
return key in getOrCreateAiProviders();
},
getOwnPropertyDescriptor(_, key) {
const obj = getOrCreateAiProviders();
if (typeof key === "string" && key in obj) {
return { configurable: true, enumerable: true, value: obj[key] };
}
return undefined;
},
});
export type AiProviderId =
| keyof typeof NOAUTH_PROVIDERS
| keyof typeof OAUTH_PROVIDERS
| keyof typeof APIKEY_PROVIDERS
| keyof typeof WEB_COOKIE_PROVIDERS
| keyof typeof LOCAL_PROVIDERS
| keyof typeof SEARCH_PROVIDERS
| keyof typeof AUDIO_ONLY_PROVIDERS
| keyof typeof UPSTREAM_PROXY_PROVIDERS
| keyof typeof CLOUD_AGENT_PROVIDERS
| keyof typeof SYSTEM_PROVIDERS;
export type AiProviderDefinition =
| (typeof NOAUTH_PROVIDERS)[keyof typeof NOAUTH_PROVIDERS]
| (typeof OAUTH_PROVIDERS)[keyof typeof OAUTH_PROVIDERS]
| (typeof APIKEY_PROVIDERS)[keyof typeof APIKEY_PROVIDERS]
| (typeof WEB_COOKIE_PROVIDERS)[keyof typeof WEB_COOKIE_PROVIDERS]
| (typeof LOCAL_PROVIDERS)[keyof typeof LOCAL_PROVIDERS]
| (typeof SEARCH_PROVIDERS)[keyof typeof SEARCH_PROVIDERS]
| (typeof AUDIO_ONLY_PROVIDERS)[keyof typeof AUDIO_ONLY_PROVIDERS]
| (typeof UPSTREAM_PROXY_PROVIDERS)[keyof typeof UPSTREAM_PROXY_PROVIDERS]
| (typeof CLOUD_AGENT_PROVIDERS)[keyof typeof CLOUD_AGENT_PROVIDERS]
| (typeof SYSTEM_PROVIDERS)[keyof typeof SYSTEM_PROVIDERS];
// Auth methods
export const AUTH_METHODS = {
oauth: { id: "oauth", name: "OAuth", icon: "lock" },
apikey: { id: "apikey", name: "API Key", icon: "key" },
};
export function getProviderByAlias(alias: string): AiProviderDefinition | null {
for (const section of _PROVIDER_SECTIONS) {
for (const provider of Object.values(section)) {
if (provider.alias === alias || provider.id === alias) {
return provider as AiProviderDefinition;
}
}
}
return null;
}
// Helper: Get provider ID from alias
export function resolveProviderId(aliasOrId: string): string {
const provider = getProviderByAlias(aliasOrId);
return provider?.id || aliasOrId;
}
export function getProviderAlias(providerId: string): string {
const provider = getProviderById(providerId);
return provider?.alias || providerId;
}
export const ALIAS_TO_ID = new Proxy({} as Record<string, string>, {
get(_, key) {
return typeof key === "string" ? getOrCreateAliasToId()[key] : undefined;
},
ownKeys() {
return Reflect.ownKeys(getOrCreateAliasToId());
},
has(_, key) {
return key in getOrCreateAliasToId();
},
getOwnPropertyDescriptor(_, key) {
const obj = getOrCreateAliasToId();
if (typeof key === "string" && key in obj) {
return { configurable: true, enumerable: true, value: obj[key] };
}
return undefined;
},
});
export const ID_TO_ALIAS = new Proxy({} as Record<string, string>, {
get(_, key) {
return typeof key === "string" ? getOrCreateIdToAlias()[key] : undefined;
},
ownKeys() {
return Reflect.ownKeys(getOrCreateIdToAlias());
},
has(_, key) {
return key in getOrCreateIdToAlias();
},
getOwnPropertyDescriptor(_, key) {
const obj = getOrCreateIdToAlias();
if (typeof key === "string" && key in obj) {
return { configurable: true, enumerable: true, value: obj[key] };
}
return undefined;
},
});
// Providers that support usage/quota API
export const USAGE_SUPPORTED_PROVIDERS = [
"antigravity",
"agy",
"kiro",
"amazon-q",
"github",
"codex",
"claude",
"cursor",
"qoder",
"kimi-coding",
"kimi-coding-apikey",
"glm",
"glm-cn",
"zai",
"glmt",
"opencode-go",
"ollama-cloud",
"minimax",
"minimax-cn",
"crof",
"nanogpt",
"deepseek",
"xiaomi-mimo",
"xiaomi-mimo-token-plan",
"vertex",
"vertex-partner",
"codebuddy-cn",
// PromptQL playground credits (getCreditSummary → USD micros)
"promptql",
"pql",
// Adobe Firefly web (cookie/JWT as apikey) — GET firefly.adobe.io/v1/credits/balance
"adobe-firefly",
"firefly",
"hyperagent",
"ha",
// xAI OAuth (Grok) weekly quota (id + public alias, same pattern as ha/agy)
"xai-oauth",
"xao",
// Grok Build subscription, billing credits, and auto top-up status
"grok-cli",
// Firecrawl team credits (GET /v2/team/credit-usage)
"firecrawl",
// Command Code credits + 5h/weekly rolling windows
"command-code",
"conol-web",
"cnl",
// Alibaba Coding Plan triple-window quota (#9603 UI gap — fetcher existed, list entry missing)
"bailian-coding-plan",
// Qwen Cloud / Model Studio personal Token Plan (cookie-authenticated console gateway)
"qwen-cloud-token-plan",
];
// ── Zod validation at module load (Phase 7.2) ──
// Re-export the extracted data catalogs so external importers of providers.ts are unchanged.
export {
NOAUTH_PROVIDERS,
OAUTH_PROVIDERS,
WEB_COOKIE_PROVIDERS,
APIKEY_PROVIDERS,
LOCAL_PROVIDERS,
SEARCH_PROVIDERS,
AUDIO_ONLY_PROVIDERS,
UPSTREAM_PROXY_PROVIDERS,
CLOUD_AGENT_PROVIDERS,
SYSTEM_PROVIDERS,
};
import { validateProviders } from "../validation/providerSchema";
validateProviders(NOAUTH_PROVIDERS, "NOAUTH_PROVIDERS");
validateProviders(OAUTH_PROVIDERS, "OAUTH_PROVIDERS");
validateProviders(APIKEY_PROVIDERS, "APIKEY_PROVIDERS");
validateProviders(WEB_COOKIE_PROVIDERS, "WEB_COOKIE_PROVIDERS");
validateProviders(LOCAL_PROVIDERS, "LOCAL_PROVIDERS");
validateProviders(SEARCH_PROVIDERS, "SEARCH_PROVIDERS");
validateProviders(AUDIO_ONLY_PROVIDERS, "AUDIO_ONLY_PROVIDERS");
validateProviders(UPSTREAM_PROXY_PROVIDERS, "UPSTREAM_PROXY_PROVIDERS");
validateProviders(CLOUD_AGENT_PROVIDERS, "CLOUD_AGENT_PROVIDERS");