Files
hack/tools/scripts/llm-key-hunter/results/_cache/content/andersonfirmino_better-omniroute.json
T
OpenCode 68a9071cf1 feat: reconcile local master with origin (exa/hot-platform hunters, leaks ledger, latest results)
Local branch had diverged from origin/master (sibling commits on the same base). Rewrote local history linearly on top of origin/master, folding in all local content: exa/hot-platform discovery hunters, leaks ledger, nightly sweep orchestrator, updated .gitignore and skill docs, plus the latest hunt output and vault state. Remote-only files (vault keys, channel scripts) were restored rather than dropped, so the resulting tree is a full union of both sides.
2026-08-07 12:53:47 +08:00

1 line
46 KiB
JSON
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{"repo": "andersonfirmino/better-omniroute", "files": {"src/app/api/providers/[id]/models/route.ts": {"d396039de54871bcd9fb31c3cd6e71c5c06a18b1": {"t": 1786068363.1249974, "n": 14231, "h": "c17fd936d4cd85e2", "c": "import { NextResponse } from \"next/server\";\nimport { getProviderConnectionById } from \"@/models\";\nimport {\n isOpenAICompatibleProvider,\n isAnthropicCompatibleProvider,\n} from \"@/shared/constants/providers\";\n\ntype JsonRecord = Record<string, unknown>;\n\nfunction asRecord(value: unknown): JsonRecord {\n return value && typeof value === \"object\" && !Array.isArray(value) ? (value as JsonRecord) : {};\n}\n\nfunction getProviderBaseUrl(providerSpecificData: unknown): string | null {\n const data = asRecord(providerSpecificData);\n const baseUrl = data.baseUrl;\n return typeof baseUrl === \"string\" && baseUrl.trim().length > 0 ? baseUrl : null;\n}\n\ntype ProviderModelsConfigEntry = {\n url: string;\n method: \"GET\" | \"POST\";\n headers: Record<string, string>;\n authHeader?: string;\n authPrefix?: string;\n authQuery?: string;\n body?: unknown;\n parseResponse: (data: any) => any;\n};\n\n// Providers that return hardcoded models (no remote /models API)\nconst STATIC_MODEL_PROVIDERS = {\n deepgram: () => [\n { id: \"nova-3\", name: \"Nova 3 (Transcription)\" },\n { id: \"nova-2\", name: \"Nova 2 (Transcription)\" },\n { id: \"whisper-large\", name: \"Whisper Large (Transcription)\" },\n { id: \"aura-asteria-en\", name: \"Aura Asteria EN (TTS)\" },\n { id: \"aura-luna-en\", name: \"Aura Luna EN (TTS)\" },\n { id: \"aura-stella-en\", name: \"Aura Stella EN (TTS)\" },\n ],\n assemblyai: () => [\n { id: \"universal-3-pro\", name: \"Universal 3 Pro (Transcription)\" },\n { id: \"universal-2\", name: \"Universal 2 (Transcription)\" },\n ],\n nanobanana: () => [\n { id: \"nanobanana-flash\", name: \"NanoBanana Flash (Gemini 2.5 Flash)\" },\n { id: \"nanobanana-pro\", name: \"NanoBanana Pro (Gemini 3 Pro)\" },\n ],\n perplexity: () => [\n { id: \"sonar\", name: \"Sonar (Fast Search)\" },\n { id: \"sonar-pro\", name: \"Sonar Pro (Advanced Search)\" },\n { id: \"sonar-reasoning\", name: \"Sonar Reasoning (CoT + Search)\" },\n { id: \"sonar-reasoning-pro\", name: \"Sonar Reasoning Pro (Advanced CoT + Search)\" },\n { id: \"sonar-deep-research\", name: \"Sonar Deep Research (Expert Analysis)\" },\n ],\n};\n\n// Provider models endpoints configuration\nconst PROVIDER_MODELS_CONFIG: Record<string, ProviderModelsConfigEntry> = {\n claude: {\n url: \"https://api.anthropic.com/v1/models\",\n method: \"GET\",\n headers: {\n \"Anthropic-Version\": \"2023-06-01\",\n \"Content-Type\": \"application/json\",\n },\n authHeader: \"x-api-key\",\n parseResponse: (data) => data.data || [],\n },\n gemini: {\n url: \"https://generativelanguage.googleapis.com/v1beta/models\",\n method: \"GET\",\n headers: { \"Content-Type\": \"application/json\" },\n authQuery: \"key\", // Use query param for API key\n parseResponse: (data) =>\n (data.models || []).map((m) => ({\n ...m,\n id: (m.name || m.id || \"\").replace(/^models\\//, \"\"),\n name: m.displayName || (m.name || \"\").replace(/^models\\//, \"\"),\n })),\n },\n \"gemini-cli\": {\n url: \"https://generativelanguage.googleapis.com/v1beta/models\",\n method: \"GET\",\n headers: { \"Content-Type\": \"application/json\" },\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n parseResponse: (data) =>\n (data.models || []).map((m) => ({\n ...m,\n id: (m.name || m.id || \"\").replace(/^models\\//, \"\"),\n name: m.displayName || (m.name || \"\").replace(/^models\\//, \"\"),\n })),\n },\n qwen: {\n url: \"https://portal.qwen.ai/v1/models\",\n method: \"GET\",\n headers: { \"Content-Type\": \"application/json\" },\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n parseResponse: (data) => data.data || [],\n },\n antigravity: {\n url: \"https://daily-cloudcode-pa.sandbox.googleapis.com/v1internal:models\",\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n body: {},\n parseResponse: (data) => data.models || [],\n },\n openai: {\n url: \"https://api.openai.com/v1/models\",\n method: \"GET\",\n headers: { \"Content-Type\": \"application/json\" },\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n parseResponse: (data) => data.data || [],\n },\n openrouter: {\n url: \"https://openrouter.ai/api/v1/models\",\n method: \"GET\",\n headers: { \"Content-Type\": \"application/json\" },\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n parseResponse: (data) => data.data || [],\n },\n kimi: {\n url: \"https://api.moonshot.ai/v1/models\",\n method: \"GET\",\n headers: { \"Content-Type\": \"application/json\" },\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n parseResponse: (data) => data.data || [],\n },\n \"kimi-coding\": {\n url: \"https://api.kimi.com/coding/v1/models\",\n method: \"GET\",\n headers: { \"Content-Type\": \"application/json\" },\n authHeader: \"x-api-key\",\n parseResponse: (data) => data.data || data.models || [],\n },\n anthropic: {\n url: \"https://api.anthropic.com/v1/models\",\n method: \"GET\",\n headers: {\n \"Anthropic-Version\": \"2023-06-01\",\n \"Content-Type\": \"application/json\",\n },\n authHeader: \"x-api-key\",\n parseResponse: (data) => data.data || [],\n },\n deepseek: {\n url: \"https://api.deepseek.com/v1/models\",\n method: \"GET\",\n headers: { \"Content-Type\": \"application/json\" },\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n parseResponse: (data) => data.data || data.models || [],\n },\n groq: {\n url: \"https://api.groq.com/openai/v1/models\",\n method: \"GET\",\n headers: { \"Content-Type\": \"application/json\" },\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n parseResponse: (data) => data.data || data.models || [],\n },\n blackbox: {\n url: \"https://api.blackbox.ai/v1/models\",\n method: \"GET\",\n headers: { \"Content-Type\": \"application/json\" },\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n parseResponse: (data) => data.data || data.models || [],\n },\n xai: {\n url: \"https://api.x.ai/v1/models\",\n method: \"GET\",\n headers: { \"Content-Type\": \"application/json\" },\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n parseResponse: (data) => data.data || data.models || [],\n },\n mistral: {\n url: \"https://api.mistral.ai/v1/models\",\n method: \"GET\",\n headers: { \"Content-Type\": \"application/json\" },\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n parseResponse: (data) => data.data || data.models || [],\n },\n\n together: {\n url: \"https://api.together.xyz/v1/models\",\n method: \"GET\",\n headers: { \"Content-Type\": \"application/json\" },\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n parseResponse: (data) => data.data || data.models || [],\n },\n fireworks: {\n url: \"https://api.fireworks.ai/inference/v1/models\",\n method: \"GET\",\n headers: { \"Content-Type\": \"application/json\" },\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n parseResponse: (data) => data.data || data.models || [],\n },\n cerebras: {\n url: \"https://api.cerebras.ai/v1/models\",\n method: \"GET\",\n headers: { \"Content-Type\": \"application/json\" },\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n parseResponse: (data) => data.data || data.models || [],\n },\n cohere: {\n url: \"https://api.cohere.com/v2/models\",\n method: \"GET\",\n headers: { \"Content-Type\": \"application/json\" },\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n parseResponse: (data) => data.data || data.models || [],\n },\n nvidia: {\n url: \"https://integrate.api.nvidia.com/v1/models\",\n method: \"GET\",\n headers: { \"Content-Type\": \"application/json\" },\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n parseResponse: (data) => data.data || data.models || [],\n },\n nebius: {\n url: \"https://api.tokenfactory.nebius.com/v1/models\",\n method: \"GET\",\n headers: { \"Content-Type\": \"application/json\" },\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n parseResponse: (data) => data.data || data.models || [],\n },\n kilocode: {\n url: \"https://api.kilo.ai/api/openrouter/models\",\n method: \"GET\",\n headers: { \"Content-Type\": \"application/json\" },\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n parseResponse: (data) => data.data || data.models || [],\n },\n \"ollama-cloud\": {\n url: \"https://api.ollama.com/v1/models\",\n method: \"GET\",\n headers: { \"Content-Type\": \"application/json\" },\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n parseResponse: (data) => data.models || data.data || [],\n },\n};\n\n/**\n * GET /api/providers/[id]/models - Get models list from provider\n */\nexport async function GET(request, { params }) {\n try {\n const { id } = await params;\n const connection = await getProviderConnectionById(id);\n\n if (!connection) {\n return NextResponse.json({ error: \"Connection not found\" }, { status: 404 });\n }\n\n const provider =\n typeof connection.provider === \"string\" && connection.provider.trim().length > 0\n ? connection.provider\n : null;\n if (!provider) {\n return NextResponse.json({ error: \"Invalid connection provider\" }, { status: 400 });\n }\n\n const connectionId = typeof connection.id === \"string\" ? connection.id : id;\n const apiKey = typeof connection.apiKey === \"string\" ? connection.apiKey : \"\";\n const accessToken = typeof connection.accessToken === \"string\" ? connection.accessToken : \"\";\n\n if (isOpenAICompatibleProvider(provider)) {\n const baseUrl = getProviderBaseUrl(connection.providerSpecificData);\n if (!baseUrl) {\n return NextResponse.json(\n { error: \"No base URL configured for OpenAI compatible provider\" },\n { status: 400 }\n );\n }\n\n let modelsUrl = baseUrl.replace(/\\/$/, \"\");\n if (modelsUrl.endsWith(\"/chat/completions\")) {\n modelsUrl = modelsUrl.slice(0, -17) + \"/models\";\n } else if (modelsUrl.endsWith(\"/completions\")) {\n modelsUrl = modelsUrl.slice(0, -12) + \"/models\";\n } else {\n modelsUrl = `${modelsUrl}/models`;\n }\n\n const response = await fetch(modelsUrl, {\n method: \"GET\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${apiKey}`,\n },\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n console.log(`Error fetching models from ${provider}:`, errorText);\n return NextResponse.json(\n { error: `Failed to fetch models: ${response.status}` },\n { status: response.status }\n );\n }\n\n const data = await response.json();\n const models = data.data || data.models || [];\n\n return NextResponse.json({\n provider,\n connectionId,\n models,\n });\n }\n\n if (isAnthropicCompatibleProvider(provider)) {\n let baseUrl = getProviderBaseUrl(connection.providerSpecificData);\n if (!baseUrl) {\n return NextResponse.json(\n { error: \"No base URL configured for Anthropic compatible provider\" },\n { status: 400 }\n );\n }\n\n baseUrl = baseUrl.replace(/\\/$/, \"\");\n if (baseUrl.endsWith(\"/messages\")) {\n baseUrl = baseUrl.slice(0, -9);\n }\n\n const url = `${baseUrl}/models`;\n const response = await fetch(url, {\n method: \"GET\",\n headers: {\n \"Content-Type\": \"application/json\",\n \"x-api-key\": apiKey,\n \"anthropic-version\": \"2023-06-01\",\n Authorization: `Bearer ${apiKey}`,\n },\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n console.log(`Error fetching models from ${provider}:`, errorText);\n return NextResponse.json(\n { error: `Failed to fetch models: ${response.status}` },\n { status: response.status }\n );\n }\n\n const data = await response.json();\n const models = data.data || data.models || [];\n\n return NextResponse.json({\n provider,\n connectionId,\n models,\n });\n }\n\n // Static model providers (no remote /models API)\n const staticModelsFn =\n provider in STATIC_MODEL_PROVIDERS\n ? STATIC_MODEL_PROVIDERS[provider as keyof typeof STATIC_MODEL_PROVIDERS]\n : undefined;\n if (staticModelsFn) {\n return NextResponse.json({\n provider,\n connectionId,\n models: staticModelsFn(),\n });\n }\n\n const config =\n provider in PROVIDER_MODELS_CONFIG\n ? PROVIDER_MODELS_CONFIG[provider as keyof typeof PROVIDER_MODELS_CONFIG]\n : undefined;\n if (!config) {\n return NextResponse.json(\n { error: `Provider ${provider} does not support models listing` },\n { status: 400 }\n );\n }\n\n // Get auth token\n const token = accessToken || apiKey;\n if (!token) {\n return NextResponse.json(\n {\n error:\n \"No API key configured for this provider. Please add an API key in the provider settings.\",\n },\n { status: 400 }\n );\n }\n\n // Build request URL\n let url = config.url;\n if (config.authQuery) {\n url += `?${config.authQuery}=${token}`;\n }\n\n // Build headers\n const headers = { ...config.headers };\n if (config.authHeader && !config.authQuery) {\n headers[config.authHeader] = (config.authPrefix || \"\") + token;\n }\n\n // Make request\n const fetchOptions: any = {\n method: config.method,\n headers,\n };\n\n if (config.body && config.method === \"POST\") {\n fetchOptions.body = JSON.stringify(config.body);\n }\n\n const response = await fetch(url, fetchOptions);\n\n if (!response.ok) {\n const errorText = await response.text();\n console.log(`Error fetching models from ${provider}:`, errorText);\n return NextResponse.json(\n { error: `Failed to fetch models: ${response.status}` },\n { status: response.status }\n );\n }\n\n const data = await response.json();\n const models = config.parseResponse(data);\n\n return NextResponse.json({\n provider,\n connectionId,\n models,\n });\n } catch (error) {\n console.log(\"Error fetching provider models:\", error);\n return NextResponse.json({ error: \"Failed to fetch models\" }, { status: 500 });\n }\n}\n"}}, "docs/i18n/id/USER_GUIDE.md": {"d396039de54871bcd9fb31c3cd6e71c5c06a18b1": {"t": 1786068464.0050218, "n": 30337, "h": "20f69cad45e70558", "c": "🌐 **Languages:** 🇺🇸 [English](../../README.md) · 🇧🇷 [pt-BR](../pt-BR/USER_GUIDE.md) · 🇪🇸 [es](../es/USER_GUIDE.md) · 🇫🇷 [fr](../fr/USER_GUIDE.md) · 🇩🇪 [de](../de/USER_GUIDE.md) · 🇮🇹 [it](../it/USER_GUIDE.md) · 🇷🇺 [ru](../ru/USER_GUIDE.md) · 🇨🇳 [zh-CN](../zh-CN/USER_GUIDE.md) · 🇯🇵 [ja](../ja/USER_GUIDE.md) · 🇰🇷 [ko](../ko/USER_GUIDE.md) · 🇸🇦 [ar](../ar/USER_GUIDE.md) · 🇮🇳 [in](../in/USER_GUIDE.md) · 🇹🇭 [th](../th/USER_GUIDE.md) · 🇻🇳 [vi](../vi/USER_GUIDE.md) · 🇮🇩 [id](../id/USER_GUIDE.md) · 🇲🇾 [ms](../ms/USER_GUIDE.md) · 🇳🇱 [nl](../nl/USER_GUIDE.md) · 🇵🇱 [pl](../pl/USER_GUIDE.md) · 🇸🇪 [sv](../sv/USER_GUIDE.md) · 🇳🇴 [no](../no/USER_GUIDE.md) · 🇩🇰 [da](../da/USER_GUIDE.md) · 🇫🇮 [fi](../fi/USER_GUIDE.md) · 🇵🇹 [pt](../pt/USER_GUIDE.md) · 🇷🇴 [ro](../ro/USER_GUIDE.md) · 🇭🇺 [hu](../hu/USER_GUIDE.md) · 🇧🇬 [bg](../bg/USER_GUIDE.md) · 🇸🇰 [sk](../sk/USER_GUIDE.md) · 🇺🇦 [uk-UA](../uk-UA/USER_GUIDE.md) · 🇮🇱 [he](../he/USER_GUIDE.md) · 🇵🇭 [phi](../phi/USER_GUIDE.md)\n\n---\n\n# User Guide\n\n🌐 **Languages:** 🇺🇸 [English](USER_GUIDE.md) | 🇧🇷 [Português (Brasil)](i18n/pt-BR/USER_GUIDE.md) | 🇪🇸 [Español](i18n/es/USER_GUIDE.md) | 🇫🇷 [Français](i18n/fr/USER_GUIDE.md) | 🇮🇹 [Italiano](i18n/it/USER_GUIDE.md) | 🇷🇺 [Русский](i18n/ru/USER_GUIDE.md) | 🇨🇳 [中文 (简体)](i18n/zh-CN/USER_GUIDE.md) | 🇩🇪 [Deutsch](i18n/de/USER_GUIDE.md) | 🇮🇳 [हिन्दी](i18n/in/USER_GUIDE.md) | 🇹🇭 [ไทย](i18n/th/USER_GUIDE.md) | 🇺🇦 [Українська](i18n/uk-UA/USER_GUIDE.md) | 🇸🇦 [العربية](i18n/ar/USER_GUIDE.md) | 🇯🇵 [日本語](i18n/ja/USER_GUIDE.md) | 🇻🇳 [Tiếng Việt](i18n/vi/USER_GUIDE.md) | 🇧🇬 [Български](i18n/bg/USER_GUIDE.md) | 🇩🇰 [Dansk](i18n/da/USER_GUIDE.md) | 🇫🇮 [Suomi](i18n/fi/USER_GUIDE.md) | 🇮🇱 [עברית](i18n/he/USER_GUIDE.md) | 🇭🇺 [Magyar](i18n/hu/USER_GUIDE.md) | 🇮🇩 [Bahasa Indonesia](i18n/id/USER_GUIDE.md) | 🇰🇷 [한국어](i18n/ko/USER_GUIDE.md) | 🇲🇾 [Bahasa Melayu](i18n/ms/USER_GUIDE.md) | 🇳🇱 [Nederlands](i18n/nl/USER_GUIDE.md) | 🇳🇴 [Norsk](i18n/no/USER_GUIDE.md) | 🇵🇹 [Português (Portugal)](i18n/pt/USER_GUIDE.md) | 🇷🇴 [Română](i18n/ro/USER_GUIDE.md) | 🇵🇱 [Polski](i18n/pl/USER_GUIDE.md) | 🇸🇰 [Slovenčina](i18n/sk/USER_GUIDE.md) | 🇸🇪 [Svenska](i18n/sv/USER_GUIDE.md) | 🇵🇭 [Filipino](i18n/phi/USER_GUIDE.md)\n\nComplete guide for configuring providers, creating combos, integrating CLI tools, and deploying OmniRoute.\n\n---\n\n## Table of Contents\n\n- [Pricing at a Glance](#-pricing-at-a-glance)\n- [Use Cases](#-use-cases)\n- [Provider Setup](#-provider-setup)\n- [CLI Integration](#-cli-integration)\n- [Deployment](#-deployment)\n- [Available Models](#-available-models)\n- [Advanced Features](#-advanced-features)\n\n---\n\n## 💰 Pricing at a Glance\n\n| Tier | Provider | Cost | Quota Reset | Best For |\n| ------------------- | ----------------- | ----------- | ---------------- | -------------------- |\n| **💳 SUBSCRIPTION** | Claude Code (Pro) | $20/mo | 5h + weekly | Already subscribed |\n| | Codex (Plus/Pro) | $20-200/mo | 5h + weekly | OpenAI users |\n| | Gemini CLI | **FREE** | 180K/mo + 1K/day | Everyone! |\n| | GitHub Copilot | $10-19/mo | Monthly | GitHub users |\n| **🔑 API KEY** | DeepSeek | Pay per use | None | Cheap reasoning |\n| | Groq | Pay per use | None | Ultra-fast inference |\n| | xAI (Grok) | Pay per use | None | Grok 4 reasoning |\n| | Mistral | Pay per use | None | EU-hosted models |\n| | Perplexity | Pay per use | None | Search-augmented |\n| | Together AI | Pay per use | None | Open-source models |\n| | Fireworks AI | Pay per use | None | Fast FLUX images |\n| | Cerebras | Pay per use | None | Wafer-scale speed |\n| | Cohere | Pay per use | None | Command R+ RAG |\n| | NVIDIA NIM | Pay per use | None | Enterprise models |\n| **💰 CHEAP** | GLM-4.7 | $0.6/1M | Daily 10AM | Budget backup |\n| | MiniMax M2.1 | $0.2/1M | 5-hour rolling | Cheapest option |\n| | Kimi K2 | $9/mo flat | 10M tokens/mo | Predictable cost |\n| **🆓 FREE** | iFlow | $0 | Unlimited | 8 models free |\n| | Qwen | $0 | Unlimited | 3 models free |\n| | Kiro | $0 | Unlimited | Claude free |\n\n**💡 Pro Tip:** Start with Gemini CLI (180K free/month) + iFlow (unlimited free) combo = $0 cost!\n\n---\n\n## 🎯 Use Cases\n\n### Case 1: \"I have Claude Pro subscription\"\n\n**Problem:** Quota expires unused, rate limits during heavy coding\n\n```\nCombo: \"maximize-claude\"\n 1. cc/claude-opus-4-6 (use subscription fully)\n 2. glm/glm-4.7 (cheap backup when quota out)\n 3. if/kimi-k2-thinking (free emergency fallback)\n\nMonthly cost: $20 (subscription) + ~$5 (backup) = $25 total\nvs. $20 + hitting limits = frustration\n```\n\n### Case 2: \"I want zero cost\"\n\n**Problem:** Can't afford subscriptions, need reliable AI coding\n\n```\nCombo: \"free-forever\"\n 1. gc/gemini-3-flash (180K free/month)\n 2. if/kimi-k2-thinking (unlimited free)\n 3. qw/qwen3-coder-plus (unlimited free)\n\nMonthly cost: $0\nQuality: Production-ready models\n```\n\n### Case 3: \"I need 24/7 coding, no interruptions\"\n\n**Problem:** Deadlines, can't afford downtime\n\n```\nCombo: \"always-on\"\n 1. cc/claude-opus-4-6 (best quality)\n 2. cx/gpt-5.2-codex (second subscription)\n 3. glm/glm-4.7 (cheap, resets daily)\n 4. minimax/MiniMax-M2.1 (cheapest, 5h reset)\n 5. if/kimi-k2-thinking (free unlimited)\n\nResult: 5 layers of fallback = zero downtime\nMonthly cost: $20-200 (subscriptions) + $10-20 (backup)\n```\n\n### Case 4: \"I want FREE AI in OpenClaw\"\n\n**Problem:** Need AI assistant in messaging apps, completely free\n\n```\nCombo: \"openclaw-free\"\n 1. if/glm-4.7 (unlimited free)\n 2. if/minimax-m2.1 (unlimited free)\n 3. if/kimi-k2-thinking (unlimited free)\n\nMonthly cost: $0\nAccess via: WhatsApp, Telegram, Slack, Discord, iMessage, Signal...\n```\n\n---\n\n## 📖 Provider Setup\n\n### 🔐 Subscription Providers\n\n#### Claude Code (Pro/Max)\n\n```bash\nDashboard → Providers → Connect Claude Code\n→ OAuth login → Auto token refresh\n→ 5-hour + weekly quota tracking\n\nModels:\n cc/claude-opus-4-6\n cc/claude-sonnet-4-5-20250929\n cc/claude-haiku-4-5-20251001\n```\n\n**Pro Tip:** Use Opus for complex tasks, Sonnet for speed. OmniRoute tracks quota per model!\n\n#### OpenAI Codex (Plus/Pro)\n\n```bash\nDashboard → Providers → Connect Codex\n→ OAuth login (port 1455)\n→ 5-hour + weekly reset\n\nModels:\n cx/gpt-5.2-codex\n cx/gpt-5.1-codex-max\n```\n\n#### Gemini CLI (FREE 180K/month!)\n\n```bash\nDashboard → Providers → Connect Gemini CLI\n→ Google OAuth\n→ 180K completions/month + 1K/day\n\nModels:\n gc/gemini-3-flash-preview\n gc/gemini-2.5-pro\n```\n\n**Best Value:** Huge free tier! Use this before paid tiers.\n\n#### GitHub Copilot\n\n```bash\nDashboard → Providers → Connect GitHub\n→ OAuth via GitHub\n→ Monthly reset (1st of month)\n\nModels:\n gh/gpt-5\n gh/claude-4.5-sonnet\n gh/gemini-3-pro\n```\n\n### 💰 Cheap Providers\n\n#### GLM-4.7 (Daily reset, $0.6/1M)\n\n1. Sign up: [Zhipu AI](https://open.bigmodel.cn/)\n2. Get API key from Coding Plan\n3. Dashboard → Add API Key: Provider: `glm`, API Key: `your-key`\n\n**Use:** `glm/glm-4.7` — **Pro Tip:** Coding Plan offers 3× quota at 1/7 cost! Reset daily 10:00 AM.\n\n#### MiniMax M2.1 (5h reset, $0.20/1M)\n\n1. Sign up: [MiniMax](https://www.minimax.io/)\n2. Get API key → Dashboard → Add API Key\n\n**Use:** `minimax/MiniMax-M2.1` — **Pro Tip:** Cheapest option for long context (1M tokens)!\n\n#### Kimi K2 ($9/month flat)\n\n1. Subscribe: [Moonshot AI](https://platform.moonshot.ai/)\n2. Get API key → Dashboard → Add API Key\n\n**Use:** `kimi/kimi-latest` — **Pro Tip:** Fixed $9/month for 10M tokens = $0.90/1M effective cost!\n\n### 🆓 FREE Providers\n\n#### iFlow (8 FREE models)\n\n```bash\nDashboard → Connect iFlow → OAuth login → Unlimited usage\n\nModels: if/kimi-k2-thinking, if/qwen3-coder-plus, if/glm-4.7, if/minimax-m2, if/deepseek-r1\n```\n\n#### Qwen (3 FREE models)\n\n```bash\nDashboard → Connect Qwen → Device code auth → Unlimited usage\n\nModels: qw/qwen3-coder-plus, qw/qwen3-coder-flash\n```\n\n#### Kiro (Claude FREE)\n\n```bash\nDashboard → Connect Kiro → AWS Builder ID or Google/GitHub → Unlimited\n\nModels: kr/claude-sonnet-4.5, kr/claude-haiku-4.5\n```\n\n---\n\n## 🎨 Combos\n\n### Example 1: Maximize Subscription → Cheap Backup\n\n```\nDashboard → Combos → Create New\n\nName: premium-coding\nModels:\n 1. cc/claude-opus-4-6 (Subscription primary)\n 2. glm/glm-4.7 (Cheap backup, $0.6/1M)\n 3. minimax/MiniMax-M2.1 (Cheapest fallback, $0.20/1M)\n\nUse in CLI: premium-coding\n```\n\n### Example 2: Free-Only (Zero Cost)\n\n```\nName: free-combo\nModels:\n 1. gc/gemini-3-flash-preview (180K free/month)\n 2. if/kimi-k2-thinking (unlimited)\n 3. qw/qwen3-coder-plus (unlimited)\n\nCost: $0 forever!\n```\n\n---\n\n## 🔧 CLI Integration\n\n### Cursor IDE\n\n```\nSettings → Models → Advanced:\n OpenAI API Base URL: http://localhost:20128/v1\n OpenAI API Key: [from omniroute dashboard]\n Model: cc/claude-opus-4-6\n```\n\n### Claude Code\n\nEdit `~/.claude/config.json`:\n\n```json\n{\n \"anthropic_api_base\": \"http://localhost:20128/v1\",\n \"anthropic_api_key\": \"your-omniroute-api-key\"\n}\n```\n\n### Codex CLI\n\n```bash\nexport OPENAI_BASE_URL=\"http://localhost:20128\"\nexport OPENAI_API_KEY=\"your-omniroute-api-key\"\ncodex \"your prompt\"\n```\n\n### OpenClaw\n\nEdit `~/.openclaw/openclaw.json`:\n\n```json\n{\n \"agents\": {\n \"defaults\": {\n \"model\": { \"primary\": \"omniroute/if/glm-4.7\" }\n }\n },\n \"models\": {\n \"providers\": {\n \"omniroute\": {\n \"baseUrl\": \"http://localhost:20128/v1\",\n \"apiKey\": \"your-omniroute-api-key\",\n \"api\": \"openai-completions\",\n \"models\": [{ \"id\": \"if/glm-4.7\", \"name\": \"glm-4.7\" }]\n }\n }\n }\n}\n```\n\n**Or use Dashboard:** CLI Tools → OpenClaw → Auto-config\n\n### Cline / Continue / RooCode\n\n```\nProvider: OpenAI Compatible\nBase URL: http://localhost:20128/v1\nAPI Key: [from dashboard]\nModel: cc/claude-opus-4-6\n```\n\n---\n\n## 🚀 Deployment\n\n### Global npm install (Recommended)\n\n```bash\nnpm install -g omniroute\n\n# Create config directory\nmkdir -p ~/.omniroute\n\n# Create .env file (see .env.example)\ncp .env.example ~/.omniroute/.env\n\n# Start server\nomniroute\n# Or with custom port:\nomniroute --port 3000\n```\n\nThe CLI automatically loads `.env` from `~/.omniroute/.env` or `./.env`.\n\n### VPS Deployment\n\n```bash\ngit clone https://github.com/diegosouzapw/OmniRoute.git\ncd OmniRoute && npm install && npm run build\n\nexport JWT_SECRET=\"your-secure-secret-change-this\"\nexport INITIAL_PASSWORD=\"your-password\"\nexport DATA_DIR=\"/var/lib/omniroute\"\nexport PORT=\"20128\"\nexport HOSTNAME=\"0.0.0.0\"\nexport NODE_ENV=\"production\"\nexport NEXT_PUBLIC_BASE_URL=\"http://localhost:20128\"\nexport API_KEY_SECRET=\"endpoint-proxy-api-key-secret\"\n\nnpm run start\n# Or: pm2 start npm --name omniroute -- start\n```\n\n### PM2 Deployment (Low Memory)\n\nFor servers with limited RAM, use the memory limit option:\n\n```bash\n# With 512MB limit (default)\npm2 start npm --name omniroute -- start\n\n# Or with custom memory limit\nOMNIROUTE_MEMORY_MB=512 pm2 start npm --name omniroute -- start\n\n# Or using ecosystem.config.js\npm2 start ecosystem.config.js\n```\n\nCreate `ecosystem.config.js`:\n\n```javascript\nmodule.exports = {\n apps: [\n {\n name: \"omniroute\",\n script: \"npm\",\n args: \"start\",\n env: {\n NODE_ENV: \"production\",\n OMNIROUTE_MEMORY_MB: \"512\",\n JWT_SECRET: \"your-secret\",\n INITIAL_PASSWORD: \"your-password\",\n },\n node_args: \"--max-old-space-size=512\",\n max_memory_restart: \"300M\",\n },\n ],\n};\n```\n\n### Docker\n\n```bash\n# Build image (default = runner-cli with codex/claude/droid preinstalled)\ndocker build -t omniroute:cli .\n\n# Portable mode (recommended)\ndocker run -d --name omniroute -p 20128:20128 --env-file ./.env -v omniroute-data:/app/data omniroute:cli\n```\n\nFor host-integrated mode with CLI binaries, see the Docker section in the main docs.\n\n### Environment Variables\n\n| Variable | Default | Description |\n| ------------------------- | ------------------------------------ | ------------------------------------------------------- |\n| `JWT_SECRET` | `omniroute-default-secret-change-me` | JWT signing secret (**change in production**) |\n| `INITIAL_PASSWORD` | `123456` | First login password |\n| `DATA_DIR` | `~/.omniroute` | Data directory (db, usage, logs) |\n| `PORT` | framework default | Service port (`20128` in examples) |\n| `HOSTNAME` | framework default | Bind host (Docker defaults to `0.0.0.0`) |\n| `NODE_ENV` | runtime default | Set `production` for deploy |\n| `BASE_URL` | `http://localhost:20128` | Server-side internal base URL |\n| `CLOUD_URL` | `https://omniroute.dev` | Cloud sync endpoint base URL |\n| `API_KEY_SECRET` | `endpoint-proxy-api-key-secret` | HMAC secret for generated API keys |\n| `REQUIRE_API_KEY` | `false` | Enforce Bearer API key on `/v1/*` |\n| `ENABLE_REQUEST_LOGS` | `false` | Enables request/response logs |\n| `AUTH_COOKIE_SECURE` | `false` | Force `Secure` auth cookie (behind HTTPS reverse proxy) |\n| `OMNIROUTE_MEMORY_MB` | `512` | Node.js heap limit in MB |\n| `PROMPT_CACHE_MAX_SIZE` | `50` | Max prompt cache entries |\n| `SEMANTIC_CACHE_MAX_SIZE` | `100` | Max semantic cache entries |\n\nFor the full environment variable reference, see the [README](../README.md).\n\n---\n\n## 📊 Available Models\n\n<details>\n<summary><b>View all available models</b></summary>\n\n**Claude Code (`cc/`)** — Pro/Max: `cc/claude-opus-4-6`, `cc/claude-sonnet-4-5-20250929`, `cc/claude-haiku-4-5-20251001`\n\n**Codex (`cx/`)** — Plus/Pro: `cx/gpt-5.2-codex`, `cx/gpt-5.1-codex-max`\n\n**Gemini CLI (`gc/`)** — FREE: `gc/gemini-3-flash-preview`, `gc/gemini-2.5-pro`\n\n**GitHub Copilot (`gh/`)**: `gh/gpt-5`, `gh/claude-4.5-sonnet`\n\n**GLM (`glm/`)** — $0.6/1M: `glm/glm-4.7`\n\n**MiniMax (`minimax/`)** — $0.2/1M: `minimax/MiniMax-M2.1`\n\n**iFlow (`if/`)** — FREE: `if/kimi-k2-thinking`, `if/qwen3-coder-plus`, `if/deepseek-r1`\n\n**Qwen (`qw/`)** — FREE: `qw/qwen3-coder-plus`, `qw/qwen3-coder-flash`\n\n**Kiro (`kr/`)** — FREE: `kr/claude-sonnet-4.5`, `kr/claude-haiku-4.5`\n\n**DeepSeek (`ds/`)**: `ds/deepseek-chat`, `ds/deepseek-reasoner`\n\n**Groq (`groq/`)**: `groq/llama-3.3-70b-versatile`, `groq/llama-4-maverick-17b-128e-instruct`\n\n**xAI (`xai/`)**: `xai/grok-4`, `xai/grok-4-0709-fast-reasoning`, `xai/grok-code-mini`\n\n**Mistral (`mistral/`)**: `mistral/mistral-large-2501`, `mistral/codestral-2501`\n\n**Perplexity (`pplx/`)**: `pplx/sonar-pro`, `pplx/sonar`\n\n**Together AI (`together/`)**: `together/meta-llama/Llama-3.3-70B-Instruct-Turbo`\n\n**Fireworks AI (`fireworks/`)**: `fireworks/accounts/fireworks/models/deepseek-v3p1`\n\n**Cerebras (`cerebras/`)**: `cerebras/llama-3.3-70b`\n\n**Cohere (`cohere/`)**: `cohere/command-r-plus-08-2024`\n\n**NVIDIA NIM (`nvidia/`)**: `nvidia/nvidia/llama-3.3-70b-instruct`\n\n</details>\n\n---\n\n## 🧩 Advanced Features\n\n### Custom Models\n\nAdd any model ID to any provider without waiting for an app update:\n\n```bash\n# Via API\ncurl -X POST http://localhost:20128/api/provider-models \\\n -H \"Content-Type: application/json\" \\\n -d '{\"provider\": \"openai\", \"modelId\": \"gpt-4.5-preview\", \"modelName\": \"GPT-4.5 Preview\"}'\n\n# List: curl http://localhost:20128/api/provider-models?provider=openai\n# Remove: curl -X DELETE \"http://localhost:20128/api/provider-models?provider=openai&model=gpt-4.5-preview\"\n```\n\nOr use Dashboard: **Providers → [Provider] → Custom Models**.\n\n### Dedicated Provider Routes\n\nRoute requests directly to a specific provider with model validation:\n\n```bash\nPOST http://localhost:20128/v1/providers/openai/chat/completions\nPOST http://localhost:20128/v1/providers/openai/embeddings\nPOST http://localhost:20128/v1/providers/fireworks/images/generations\n```\n\nThe provider prefix is auto-added if missing. Mismatched models return `400`.\n\n### Network Proxy Configuration\n\n```bash\n# Set global proxy\ncurl -X PUT http://localhost:20128/api/settings/proxy \\\n -d '{\"global\": {\"type\":\"http\",\"host\":\"proxy.example.com\",\"port\":\"8080\"}}'\n\n# Per-provider proxy\ncurl -X PUT http://localhost:20128/api/settings/proxy \\\n -d '{\"providers\": {\"openai\": {\"type\":\"socks5\",\"host\":\"proxy.example.com\",\"port\":\"1080\"}}}'\n\n# Test proxy\ncurl -X POST http://localhost:20128/api/settings/proxy/test \\\n -d '{\"proxy\":{\"type\":\"socks5\",\"host\":\"proxy.example.com\",\"port\":\"1080\"}}'\n```\n\n**Precedence:** Key-specific → Combo-specific → Provider-specific → Global → Environment.\n\n### Model Catalog API\n\n```bash\ncurl http://localhost:20128/api/models/catalog\n```\n\nReturns models grouped by provider with types (`chat`, `embedding`, `image`).\n\n### Cloud Sync\n\n- Sync providers, combos, and settings across devices\n- Automatic background sync with timeout + fail-fast\n- Prefer server-side `BASE_URL`/`CLOUD_URL` in production\n\n### LLM Gateway Intelligence (Phase 9)\n\n- **Semantic Cache** — Auto-caches non-streaming, temperature=0 responses (bypass with `X-OmniRoute-No-Cache: true`)\n- **Request Idempotency** — Deduplicates requests within 5s via `Idempotency-Key` or `X-Request-Id` header\n- **Progress Tracking** — Opt-in SSE `event: progress` events via `X-OmniRoute-Progress: true` header\n\n---\n\n### Translator Playground\n\nAccess via **Dashboard → Translator**. Debug and visualize how OmniRoute translates API requests between providers.\n\n| Mode | Purpose |\n| ---------------- | -------------------------------------------------------------------------------------- |\n| **Playground** | Select source/target formats, paste a request, and see the translated output instantly |\n| **Chat Tester** | Send live chat messages through the proxy and inspect the full request/response cycle |\n| **Test Bench** | Run batch tests across multiple format combinations to verify translation correctness |\n| **Live Monitor** | Watch real-time translations as requests flow through the proxy |\n\n**Use cases:**\n\n- Debug why a specific client/provider combination fails\n- Verify that thinking tags, tool calls, and system prompts translate correctly\n- Compare format differences between OpenAI, Claude, Gemini, and Responses API formats\n\n---\n\n### Routing Strategies\n\nConfigure via **Dashboard → Settings → Routing**.\n\n| Strategy | Description |\n| ------------------------------ | ------------------------------------------------------------------------------------------------ |\n| **Fill First** | Uses accounts in priority order — primary account handles all requests until unavailable |\n| **Round Robin** | Cycles through all accounts with a configurable sticky limit (default: 3 calls per account) |\n| **P2C (Power of Two Choices)** | Picks 2 random accounts and routes to the healthier one — balances load with awareness of health |\n| **Random** | Randomly selects an account for each request using Fisher-Yates shuffle |\n| **Least Used** | Routes to the account with the oldest `lastUsedAt` timestamp, distributing traffic evenly |\n| **Cost Optimized** | Routes to the account with the lowest priority value, optimizing for lowest-cost providers |\n\n#### Wildcard Model Aliases\n\nCreate wildcard patterns to remap model names:\n\n```\nPattern: claude-sonnet-* → Target: cc/claude-sonnet-4-5-20250929\nPattern: gpt-* → Target: gh/gpt-5.1-codex\n```\n\nWildcards support `*` (any characters) and `?` (single character).\n\n#### Fallback Chains\n\nDefine global fallback chains that apply across all requests:\n\n```\nChain: production-fallback\n 1. cc/claude-opus-4-6\n 2. gh/gpt-5.1-codex\n 3. glm/glm-4.7\n```\n\n---\n\n### Resilience & Circuit Breakers\n\nConfigure via **Dashboard → Settings → Resilience**.\n\nOmniRoute implements provider-level resilience with four components:\n\n1. **Provider Profiles** — Per-provider configuration for:\n - Failure threshold (how many failures before opening)\n - Cooldown duration\n - Rate limit detection sensitivity\n - Exponential backoff parameters\n\n2. **Editable Rate Limits** — System-level defaults configurable in the dashboard:\n - **Requests Per Minute (RPM)** — Maximum requests per minute per account\n - **Min Time Between Requests** — Minimum gap in milliseconds between requests\n - **Max Concurrent Requests** — Maximum simultaneous requests per account\n - Click **Edit** to modify, then **Save** or **Cancel**. Values persist via the resilience API.\n\n3. **Circuit Breaker** — Tracks failures per provider and automatically opens the circuit when a threshold is reached:\n - **CLOSED** (Healthy) — Requests flow normally\n - **OPEN** — Provider is temporarily blocked after repeated failures\n - **HALF_OPEN** — Testing if provider has recovered\n\n4. **Policies & Locked Identifiers** — Shows circuit breaker status and locked identifiers with force-unlock capability.\n\n5. **Rate Limit Auto-Detection** — Monitors `429` and `Retry-After` headers to proactively avoid hitting provider rate limits.\n\n**Pro Tip:** Use **Reset All** button to clear all circuit breakers and cooldowns when a provider recovers from an outage.\n\n---\n\n### Database Export / Import\n\nManage database backups in **Dashboard → Settings → System & Storage**.\n\n| Action | Description |\n| ------------------------ | ------------------------------------------------------------------------------------------------------------------------------ |\n| **Export Database** | Downloads the current SQLite database as a `.sqlite` file |\n| **Export All (.tar.gz)** | Downloads a full backup archive including: database, settings, combos, provider connections (no credentials), API key metadata |\n| **Import Database** | Upload a `.sqlite` file to replace the current database. A pre-import backup is automatically created |\n\n```bash\n# API: Export database\ncurl -o backup.sqlite http://localhost:20128/api/db-backups/export\n\n# API: Export all (full archive)\ncurl -o backup.tar.gz http://localhost:20128/api/db-backups/exportAll\n\n# API: Import database\ncurl -X POST http://localhost:20128/api/db-backups/import \\\n -F \"file=@backup.sqlite\"\n```\n\n**Import Validation:** The imported file is validated for integrity (SQLite pragma check), required tables (`provider_connections`, `provider_nodes`, `combos`, `api_keys`), and size (max 100MB).\n\n**Use Cases:**\n\n- Migrate OmniRoute between machines\n- Create external backups for disaster recovery\n- Share configurations between team members (export all → share archive)\n\n---\n\n### Settings Dashboard\n\nThe settings page is organized into 5 tabs for easy navigation:\n\n| Tab | Contents |\n| -------------- | ---------------------------------------------------------------------------------------------- |\n| **Security** | Login/Password settings, IP Access Control, API auth for `/models`, and Provider Blocking |\n| **Routing** | Global routing strategy (6 options), wildcard model aliases, fallback chains, combo defaults |\n| **Resilience** | Provider profiles, editable rate limits, circuit breaker status, policies & locked identifiers |\n| **AI** | Thinking budget configuration, global system prompt injection, prompt cache stats |\n| **Advanced** | Global proxy configuration (HTTP/SOCKS5) |\n\n---\n\n### Costs & Budget Management\n\nAccess via **Dashboard → Costs**.\n\n| Tab | Purpose |\n| ----------- | ---------------------------------------------------------------------------------------- |\n| **Budget** | Set spending limits per API key with daily/weekly/monthly budgets and real-time tracking |\n| **Pricing** | View and edit model pricing entries — cost per 1K input/output tokens per provider |\n\n```bash\n# API: Set a budget\ncurl -X POST http://localhost:20128/api/usage/budget \\\n -H \"Content-Type: application/json\" \\\n -d '{\"keyId\": \"key-123\", \"limit\": 50.00, \"period\": \"monthly\"}'\n\n# API: Get current budget status\ncurl http://localhost:20128/api/usage/budget\n```\n\n**Cost Tracking:** Every request logs token usage and calculates cost using the pricing table. View breakdowns in **Dashboard → Usage** by provider, model, and API key.\n\n---\n\n### Audio Transcription\n\nOmniRoute supports audio transcription via the OpenAI-compatible endpoint:\n\n```bash\nPOST /v1/audio/transcriptions\nAuthorization: Bearer your-api-key\nContent-Type: multipart/form-data\n\n# Example with curl\ncurl -X POST http://localhost:20128/v1/audio/transcriptions \\\n -H \"Authorization: Bearer your-api-key\" \\\n -F \"file=@audio.mp3\" \\\n -F \"model=deepgram/nova-3\"\n```\n\nAvailable providers: **Deepgram** (`deepgram/`), **AssemblyAI** (`assemblyai/`).\n\nSupported audio formats: `mp3`, `wav`, `m4a`, `flac`, `ogg`, `webm`.\n\n---\n\n### Combo Balancing Strategies\n\nConfigure per-combo balancing in **Dashboard → Combos → Create/Edit → Strategy**.\n\n| Strategy | Description |\n| ------------------ | ------------------------------------------------------------------------ |\n| **Round-Robin** | Rotates through models sequentially |\n| **Priority** | Always tries the first model; falls back only on error |\n| **Random** | Picks a random model from the combo for each request |\n| **Weighted** | Routes proportionally based on assigned weights per model |\n| **Least-Used** | Routes to the model with the fewest recent requests (uses combo metrics) |\n| **Cost-Optimized** | Routes to the cheapest available model (uses pricing table) |\n\nGlobal combo defaults can be set in **Dashboard → Settings → Routing → Combo Defaults**.\n\n---\n\n### Health Dashboard\n\nAccess via **Dashboard → Health**. Real-time system health overview with 6 cards:\n\n| Card | What It Shows |\n| --------------------- | ----------------------------------------------------------- |\n| **System Status** | Uptime, version, memory usage, data directory |\n| **Provider Health** | Per-provider circuit breaker state (Closed/Open/Half-Open) |\n| **Rate Limits** | Active rate limit cooldowns per account with remaining time |\n| **Active Lockouts** | Providers temporarily blocked by the lockout policy |\n| **Signature Cache** | Deduplication cache stats (active keys, hit rate) |\n| **Latency Telemetry** | p50/p95/p99 latency aggregation per provider |\n\n**Pro Tip:** The Health page auto-refreshes every 10 seconds. Use the circuit breaker card to identify which providers are experiencing issues.\n\n---\n\n## 🖥️ Desktop Application (Electron)\n\nOmniRoute is available as a native desktop application for Windows, macOS, and Linux.\n\n### Installation\n\n```bash\n# From the electron directory:\ncd electron\nnpm install\n\n# Development mode (connect to running Next.js dev server):\nnpm run dev\n\n# Production mode (uses standalone build):\nnpm start\n```\n\n### Building Installers\n\n```bash\ncd electron\nnpm run build # Current platform\nnpm run build:win # Windows (.exe NSIS)\nnpm run build:mac # macOS (.dmg universal)\nnpm run build:linux # Linux (.AppImage)\n```\n\nOutput → `electron/dist-electron/`\n\n### Key Features\n\n| Feature | Description |\n| --------------------------- | ---------------------------------------------------- |\n| **Server Readiness** | Polls server before showing window (no blank screen) |\n| **System Tray** | Minimize to tray, change port, quit from tray menu |\n| **Port Management** | Change server port from tray (auto-restarts server) |\n| **Content Security Policy** | Restrictive CSP via session headers |\n| **Single Instance** | Only one app instance can run at a time |\n| **Offline Mode** | Bundled Next.js server works without internet |\n\n### Environment Variables\n\n| Variable | Default | Description |\n| --------------------- | ------- | -------------------------------- |\n| `OMNIROUTE_PORT` | `20128` | Server port |\n| `OMNIROUTE_MEMORY_MB` | `512` | Node.js heap limit (64–16384 MB) |\n\n📖 Full documentation: [`electron/README.md`](../electron/README.md)\n"}}}}