#!/usr/bin/env node // Generates docs/reference/PROVIDER_REFERENCE.md from src/shared/constants/providers.ts. // Run: node --import tsx scripts/docs/gen-provider-reference.ts import fs from "node:fs"; import path from "node:path"; import { fileURLToPath } from "node:url"; import { FREE_PROVIDERS, OAUTH_PROVIDERS, WEB_COOKIE_PROVIDERS, APIKEY_PROVIDERS, LOCAL_PROVIDERS, SEARCH_PROVIDERS, AUDIO_ONLY_PROVIDERS, UPSTREAM_PROXY_PROVIDERS, CLOUD_AGENT_PROVIDERS, SYSTEM_PROVIDERS, IMAGE_ONLY_PROVIDER_IDS, AGGREGATOR_PROVIDER_IDS, ENTERPRISE_CLOUD_PROVIDER_IDS, VIDEO_PROVIDER_IDS, EMBEDDING_RERANK_PROVIDER_IDS, SELF_HOSTED_CHAT_PROVIDER_IDS, } from "../../src/shared/constants/providers.ts"; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const ROOT = path.resolve(__dirname, "..", ".."); const OUT_FILE = path.join(ROOT, "docs", "reference", "PROVIDER_REFERENCE.md"); type ProviderRecord = { id: string; alias?: string | undefined; name: string; icon?: string; color?: string; textIcon?: string; website?: string; authHint?: string; freeNote?: string; hasFree?: boolean; deprecated?: boolean; deprecationReason?: string; [k: string]: unknown; }; function asRecords(map: Record): ProviderRecord[] { return Object.values(map).map((p) => ({ ...p })); } function escapeCell(value: string | undefined): string { if (!value) return "—"; // Escape backslash first so the subsequent escapes don't double-escape it. return value.replace(/\\/g, "\\\\").replace(/\|/g, "\\|").replace(/\n/g, " "); } function row(p: ProviderRecord, category: string): string { const alias = p.alias ? `\`${p.alias}\`` : "—"; const hint = p.deprecated ? `⚠️ **DEPRECATED.** ${escapeCell(p.deprecationReason)}` : escapeCell(p.authHint || p.freeNote); const link = p.website ? `[link](${p.website})` : "—"; return `| \`${p.id}\` | ${alias} | ${escapeCell(p.name)} | ${category} | ${link} | ${hint} |`; } function categoryTags(id: string): string[] { const tags: string[] = []; if (IMAGE_ONLY_PROVIDER_IDS.has(id)) tags.push("image"); if (VIDEO_PROVIDER_IDS.has(id)) tags.push("video"); if (AGGREGATOR_PROVIDER_IDS.has(id)) tags.push("aggregator"); if (ENTERPRISE_CLOUD_PROVIDER_IDS.has(id)) tags.push("enterprise"); if (EMBEDDING_RERANK_PROVIDER_IDS.has(id)) tags.push("embed/rerank"); if (SELF_HOSTED_CHAT_PROVIDER_IDS.has(id)) tags.push("self-hosted"); return tags; } function sortById(rows: ProviderRecord[]): ProviderRecord[] { return [...rows].sort((a, b) => a.id.localeCompare(b.id)); } function buildSection(title: string, rows: ProviderRecord[], category: string): string { if (rows.length === 0) return ""; const lines: string[] = []; lines.push(`## ${title} (${rows.length})\n`); lines.push("| ID | Alias | Name | Tags | Website | Notes |"); lines.push("|----|-------|------|------|---------|-------|"); for (const p of sortById(rows)) { const tags = [category, ...categoryTags(p.id)].join(", "); lines.push(row(p, tags)); } lines.push(""); return lines.join("\n"); } function buildHeader(total: number): string { const date = new Date().toISOString().slice(0, 10); const pkg = JSON.parse(fs.readFileSync(path.join(ROOT, "package.json"), "utf8")) as { version?: string; }; return [ "---", 'title: "Provider Reference"', `version: ${pkg.version || "unknown"}`, `lastUpdated: ${date}`, "---", "", "# Provider Reference", "", `> **Auto-generated** from \`src/shared/constants/providers.ts\` — do not edit by hand.`, `> Regenerate with: \`npm run gen:provider-reference\``, `> **Last generated:** ${date}`, "", `Total providers: **${total}**. See category breakdown below.`, "", "## Categories", "", "- **Free** — free tier with API key (configured via dashboard)", "- **OAuth** — sign-in flow handled by OmniRoute, no API key needed", "- **Web cookie** — wraps the provider's web app via cookie auth", "- **API key** — paid provider configured via API key (free credits may apply)", "- **Local** — runs on the user's machine (Ollama, LM Studio, vLLM, etc.)", "- **Search** — web search providers", "- **Audio** — audio-only providers (TTS/STT)", "- **Upstream proxy** — providers that proxy to other providers", "- **Cloud agent** — long-running coding agents (Codex Cloud, Devin, Jules)", "- **System** — OmniRoute-internal providers (loopback, etc.)", "", "Additional tags: `image`, `video`, `aggregator`, `enterprise`, `embed/rerank`, `self-hosted`.", "", "Use the dashboard at `/dashboard/providers` to enable, configure, and test each provider.", "", "---", "", ].join("\n"); } function main() { const free = asRecords(FREE_PROVIDERS); const oauth = asRecords(OAUTH_PROVIDERS); const webCookie = asRecords(WEB_COOKIE_PROVIDERS); const apiKey = asRecords(APIKEY_PROVIDERS); const local = asRecords(LOCAL_PROVIDERS); const search = asRecords(SEARCH_PROVIDERS); const audio = asRecords(AUDIO_ONLY_PROVIDERS); const upstreamProxy = asRecords(UPSTREAM_PROXY_PROVIDERS); const cloudAgent = asRecords(CLOUD_AGENT_PROVIDERS); const system = asRecords(SYSTEM_PROVIDERS); const allIds = new Set([ ...free.map((p) => p.id), ...oauth.map((p) => p.id), ...webCookie.map((p) => p.id), ...apiKey.map((p) => p.id), ...local.map((p) => p.id), ...search.map((p) => p.id), ...audio.map((p) => p.id), ...upstreamProxy.map((p) => p.id), ...cloudAgent.map((p) => p.id), ...system.map((p) => p.id), ]); const sections = [ buildSection("Free Tier (OAuth-first or no-key)", free, "Free"), buildSection("OAuth Providers", oauth, "OAuth"), buildSection("Web Cookie Providers", webCookie, "Web cookie"), buildSection("API Key Providers (paid / paid-with-free-credits)", apiKey, "API key"), buildSection("Local Providers", local, "Local"), buildSection("Search Providers", search, "Search"), buildSection("Audio-only Providers", audio, "Audio"), buildSection("Upstream Proxy Providers", upstreamProxy, "Upstream proxy"), buildSection("Cloud Agent Providers", cloudAgent, "Cloud agent"), buildSection("System Providers", system, "System"), ]; const footer = [ "## Sources of truth", "", "- Catalog: [`src/shared/constants/providers.ts`](../../src/shared/constants/providers.ts)", "- Registry (per-model details): [`open-sse/config/providerRegistry.ts`](../../open-sse/config/providerRegistry.ts)", "- Executors: [`open-sse/executors/`](../../open-sse/executors/) (31 files)", "- Translators: [`open-sse/translator/`](../../open-sse/translator/)", "", "## See Also", "", "- [FREE_TIERS.md](./FREE_TIERS.md) — curated free-tier guide", "- [USER_GUIDE.md](../guides/USER_GUIDE.md) — provider setup walkthrough", "- [ARCHITECTURE.md](../architecture/ARCHITECTURE.md) — overall architecture", "", ].join("\n"); const content = buildHeader(allIds.size) + sections.join("\n") + "\n" + footer; fs.writeFileSync(OUT_FILE, content); console.log(`✓ Wrote ${OUT_FILE}`); console.log(` Providers: ${allIds.size} unique IDs`); console.log( ` Sections: free=${free.length}, oauth=${oauth.length}, web=${webCookie.length}, ` + `apikey=${apiKey.length}, local=${local.length}, search=${search.length}, ` + `audio=${audio.length}, proxy=${upstreamProxy.length}, cloud=${cloudAgent.length}, system=${system.length}` ); } main();