{"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;\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;\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 = {\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
\nView all available models\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
\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"}}}}