diff --git a/.codegraph/codegraph.db b/.codegraph/codegraph.db new file mode 100644 index 0000000000..f90d4d501a Binary files /dev/null and b/.codegraph/codegraph.db differ diff --git a/.env.example b/.env.example index 8356626136..5315949804 100644 --- a/.env.example +++ b/.env.example @@ -77,6 +77,16 @@ PORT=20128 # API_HOST=0.0.0.0 # DASHBOARD_PORT=20128 +# Port for the real-time WebSocket live monitoring server. +# Used by: src/server/ws/liveServer.ts, src/app/api/v1/ws/route.ts +# Default: 20129 +# LIVE_WS_PORT=20129 + +# Disable the real-time WebSocket server. +# Used by: src/server/ws/liveServer.ts, scripts/start-ws-server.mjs +# Default: false | Set to 1 or true to disable. +# OMNIROUTE_DISABLE_LIVE_WS=false + # Use Turbopack in local dev. Next 16.2.4 can fail to compile next/font/google # through the custom dev runner without this on Windows. OMNIROUTE_USE_TURBOPACK=1 @@ -85,6 +95,21 @@ OMNIROUTE_USE_TURBOPACK=1 # Used by: src/lib/db/core.ts, src/lib/db/healthCheck.ts. Set to 1 to skip. # OMNIROUTE_SKIP_DB_HEALTHCHECK=1 +# Interval (ms) for the background credential health check scheduler. +# Default: 300000 (5 minutes). Minimum: 10000 (10 seconds). +# Used by: open-sse/config/constants.ts, src/lib/credentialHealth/scheduler.ts +# CREDENTIAL_HEALTH_CHECK_INTERVAL=300000 + +# TTL (ms) for cached credential health status. +# Default: 300000 (5 minutes). +# Used by: open-sse/config/constants.ts, src/lib/credentialHealth/cache.ts +# CREDENTIAL_HEALTH_CACHE_TTL=300000 + +# Set to 1 or true to disable background periodic testing of provider connections. +# Default: false +# Used by: src/lib/credentialHealth/scheduler.ts +# OMNIROUTE_DISABLE_CREDENTIAL_HEALTH_CHECK=false + # Docker production port mappings (docker-compose.prod.yml only). # These set the HOST-side published ports. Container ports use PORT/API_PORT. # PROD_DASHBOARD_PORT=20130 @@ -419,6 +444,12 @@ PROVIDER_LIMITS_SYNC_INTERVAL_MINUTES=70 #OMNIROUTE_SPEND_FLUSH_INTERVAL_MS=60000 #OMNIROUTE_SPEND_MAX_BUFFER_SIZE=1000 +# Batch request processor retry and backoff settings. +# Used by: open-sse/services/batchProcessor.ts. Defaults shown. +#BATCH_RETRY_DURATION_MS=86400000 +#BATCH_BACKOFF_BASE_MS=5000 +#BATCH_BACKOFF_MAX_MS=3600000 + # Config hot-reload polling interval (ms). Default: 5000. # Used by: src/lib/config/hotReload.ts. Lower than 1000ms is rejected. #OMNIROUTE_CONFIG_HOT_RELOAD_MS=5000 diff --git a/.github/workflows/npm-publish.yml b/.github/workflows/npm-publish.yml index 3f38d74570..059d467d8b 100644 --- a/.github/workflows/npm-publish.yml +++ b/.github/workflows/npm-publish.yml @@ -131,3 +131,42 @@ jobs: echo "✅ Action finished for GitHub Packages" env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + publish-opencode-plugin: + runs-on: ubuntu-latest + environment: NPM_TOKEN + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Setup Node.js + uses: actions/setup-node@v6 + with: + node-version: ${{ env.NPM_PUBLISH_NODE_VERSION }} + registry-url: https://registry.npmjs.org + + - name: Install plugin dependencies + working-directory: "@omniroute/opencode-plugin" + run: npm install --no-audit --no-fund + + - name: Build plugin + working-directory: "@omniroute/opencode-plugin" + run: npm run clean && npm run build + + - name: Test plugin + working-directory: "@omniroute/opencode-plugin" + run: npm test + + - name: Publish @omniroute/opencode-plugin to npm + working-directory: "@omniroute/opencode-plugin" + run: | + PKG_VERSION=$(node -p "require('./package.json').version") + PKG_NAME=$(node -p "require('./package.json').name") + if npm view "${PKG_NAME}@${PKG_VERSION}" version --silent 2>/dev/null | grep -q "^${PKG_VERSION}$"; then + echo "⚠️ ${PKG_NAME}@${PKG_VERSION} is already published on npm — skipping." + exit 0 + fi + npm publish --access public --ignore-scripts + echo "✅ Published ${PKG_NAME}@${PKG_VERSION}" + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/.gitignore b/.gitignore index 93920b485b..9c412cd077 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,11 @@ omnirouteCloud/ omnirouteSite/ +# Memory Bank and Cursor rules (local-only AI agent context) +memory-bank/ +.cursor/rules/core.mdc +.cursor/rules/memory-bank.mdc + # Claude Code local state — runtime files only; shared commands at .claude/commands/ are tracked .claude/scheduled_tasks.lock .claude/scheduled_tasks/ @@ -166,6 +171,13 @@ scripts/i18n/_pending-keys.json # These contain proprietary multi-phase logic and should not be committed .agents/workflows/implement-features-ag.md .agents/workflows/port-upstream-features-ag.md +.agents/workflows/port-upstream-issues-ag.md .agents/skills/implement-features/ .claude/commands/implement-features-cc.md +.claude/commands/port-upstream-features-cc.md +.claude/commands/port-upstream-issues-cc.md .claude/worktrees/ +.codegraph/ + +# Fumadocs generated source +.source/ diff --git a/.source/browser.ts b/.source/browser.ts new file mode 100644 index 0000000000..1ef2b1b1ed --- /dev/null +++ b/.source/browser.ts @@ -0,0 +1,12 @@ +// @ts-nocheck +import { browser } from 'fumadocs-mdx/runtime/browser'; +import type * as Config from '../source.config'; + +const create = browser(); +const browserCollections = { + docs: create.doc("docs", {"architecture/ARCHITECTURE.md": () => import("../docs/architecture/ARCHITECTURE.md?collection=docs"), "architecture/AUTHZ_GUIDE.md": () => import("../docs/architecture/AUTHZ_GUIDE.md?collection=docs"), "architecture/CODEBASE_DOCUMENTATION.md": () => import("../docs/architecture/CODEBASE_DOCUMENTATION.md?collection=docs"), "architecture/REPOSITORY_MAP.md": () => import("../docs/architecture/REPOSITORY_MAP.md?collection=docs"), "architecture/RESILIENCE_GUIDE.md": () => import("../docs/architecture/RESILIENCE_GUIDE.md?collection=docs"), "compression/COMPRESSION_ENGINES.md": () => import("../docs/compression/COMPRESSION_ENGINES.md?collection=docs"), "compression/COMPRESSION_GUIDE.md": () => import("../docs/compression/COMPRESSION_GUIDE.md?collection=docs"), "compression/COMPRESSION_LANGUAGE_PACKS.md": () => import("../docs/compression/COMPRESSION_LANGUAGE_PACKS.md?collection=docs"), "compression/COMPRESSION_RULES_FORMAT.md": () => import("../docs/compression/COMPRESSION_RULES_FORMAT.md?collection=docs"), "compression/RTK_COMPRESSION.md": () => import("../docs/compression/RTK_COMPRESSION.md?collection=docs"), "frameworks/A2A-SERVER.md": () => import("../docs/frameworks/A2A-SERVER.md?collection=docs"), "frameworks/AGENT_PROTOCOLS_GUIDE.md": () => import("../docs/frameworks/AGENT_PROTOCOLS_GUIDE.md?collection=docs"), "frameworks/CLOUD_AGENT.md": () => import("../docs/frameworks/CLOUD_AGENT.md?collection=docs"), "frameworks/EVALS.md": () => import("../docs/frameworks/EVALS.md?collection=docs"), "frameworks/GAMIFICATION.md": () => import("../docs/frameworks/GAMIFICATION.md?collection=docs"), "frameworks/MCP-SERVER.md": () => import("../docs/frameworks/MCP-SERVER.md?collection=docs"), "frameworks/MEMORY.md": () => import("../docs/frameworks/MEMORY.md?collection=docs"), "frameworks/OPENCODE.md": () => import("../docs/frameworks/OPENCODE.md?collection=docs"), "frameworks/SKILLS.md": () => import("../docs/frameworks/SKILLS.md?collection=docs"), "frameworks/WEBHOOKS.md": () => import("../docs/frameworks/WEBHOOKS.md?collection=docs"), "guides/DOCKER_GUIDE.md": () => import("../docs/guides/DOCKER_GUIDE.md?collection=docs"), "guides/ELECTRON_GUIDE.md": () => import("../docs/guides/ELECTRON_GUIDE.md?collection=docs"), "guides/FEATURES.md": () => import("../docs/guides/FEATURES.md?collection=docs"), "guides/I18N.md": () => import("../docs/guides/I18N.md?collection=docs"), "guides/KIRO_SETUP.md": () => import("../docs/guides/KIRO_SETUP.md?collection=docs"), "guides/PWA_GUIDE.md": () => import("../docs/guides/PWA_GUIDE.md?collection=docs"), "guides/SETUP_GUIDE.md": () => import("../docs/guides/SETUP_GUIDE.md?collection=docs"), "guides/TERMUX_GUIDE.md": () => import("../docs/guides/TERMUX_GUIDE.md?collection=docs"), "guides/TROUBLESHOOTING.md": () => import("../docs/guides/TROUBLESHOOTING.md?collection=docs"), "guides/UNINSTALL.md": () => import("../docs/guides/UNINSTALL.md?collection=docs"), "guides/USER_GUIDE.md": () => import("../docs/guides/USER_GUIDE.md?collection=docs"), "ops/COVERAGE_PLAN.md": () => import("../docs/ops/COVERAGE_PLAN.md?collection=docs"), "ops/E2E_DASHBOARD_SHAKEDOWN_v3.8.0.md": () => import("../docs/ops/E2E_DASHBOARD_SHAKEDOWN_v3.8.0.md?collection=docs"), "ops/FLY_IO_DEPLOYMENT_GUIDE.md": () => import("../docs/ops/FLY_IO_DEPLOYMENT_GUIDE.md?collection=docs"), "ops/PROXY_GUIDE.md": () => import("../docs/ops/PROXY_GUIDE.md?collection=docs"), "ops/RELEASE_CHECKLIST.md": () => import("../docs/ops/RELEASE_CHECKLIST.md?collection=docs"), "ops/SQLITE_RUNTIME.md": () => import("../docs/ops/SQLITE_RUNTIME.md?collection=docs"), "ops/TUNNELS_GUIDE.md": () => import("../docs/ops/TUNNELS_GUIDE.md?collection=docs"), "ops/VM_DEPLOYMENT_GUIDE.md": () => import("../docs/ops/VM_DEPLOYMENT_GUIDE.md?collection=docs"), "reference/API_REFERENCE.md": () => import("../docs/reference/API_REFERENCE.md?collection=docs"), "reference/CLI-TOOLS.md": () => import("../docs/reference/CLI-TOOLS.md?collection=docs"), "reference/ENVIRONMENT.md": () => import("../docs/reference/ENVIRONMENT.md?collection=docs"), "reference/FREE_TIERS.md": () => import("../docs/reference/FREE_TIERS.md?collection=docs"), "reference/PROVIDER_REFERENCE.md": () => import("../docs/reference/PROVIDER_REFERENCE.md?collection=docs"), "routing/AUTO-COMBO.md": () => import("../docs/routing/AUTO-COMBO.md?collection=docs"), "routing/REASONING_REPLAY.md": () => import("../docs/routing/REASONING_REPLAY.md?collection=docs"), "security/CLI_TOKEN.md": () => import("../docs/security/CLI_TOKEN.md?collection=docs"), "security/CLI_TOKEN_AUTH.md": () => import("../docs/security/CLI_TOKEN_AUTH.md?collection=docs"), "security/COMPLIANCE.md": () => import("../docs/security/COMPLIANCE.md?collection=docs"), "security/ERROR_SANITIZATION.md": () => import("../docs/security/ERROR_SANITIZATION.md?collection=docs"), "security/GUARDRAILS.md": () => import("../docs/security/GUARDRAILS.md?collection=docs"), "security/PUBLIC_CREDS.md": () => import("../docs/security/PUBLIC_CREDS.md?collection=docs"), "security/ROUTE_GUARD_TIERS.md": () => import("../docs/security/ROUTE_GUARD_TIERS.md?collection=docs"), "security/STEALTH_GUIDE.md": () => import("../docs/security/STEALTH_GUIDE.md?collection=docs"), }), +}; +export default browserCollections; \ No newline at end of file diff --git a/.source/dynamic.ts b/.source/dynamic.ts new file mode 100644 index 0000000000..7dd9c10a61 --- /dev/null +++ b/.source/dynamic.ts @@ -0,0 +1,8 @@ +// @ts-nocheck +import { dynamic } from 'fumadocs-mdx/runtime/dynamic'; +import * as Config from '../source.config'; + +const create = await dynamic(Config, {"configPath":"source.config.ts","environment":"next","outDir":".source"}, {"doc":{"passthroughs":["extractedReferences"]}}); \ No newline at end of file diff --git a/.source/server.ts b/.source/server.ts new file mode 100644 index 0000000000..c4a1689684 --- /dev/null +++ b/.source/server.ts @@ -0,0 +1,74 @@ +// @ts-nocheck +import { default as __fd_glob_63 } from "../docs/security/meta.json?collection=docs" +import { default as __fd_glob_62 } from "../docs/routing/meta.json?collection=docs" +import { default as __fd_glob_61 } from "../docs/reference/openapi.yaml?collection=docs" +import { default as __fd_glob_60 } from "../docs/reference/meta.json?collection=docs" +import { default as __fd_glob_59 } from "../docs/ops/meta.json?collection=docs" +import { default as __fd_glob_58 } from "../docs/guides/meta.json?collection=docs" +import { default as __fd_glob_57 } from "../docs/frameworks/meta.json?collection=docs" +import { default as __fd_glob_56 } from "../docs/compression/meta.json?collection=docs" +import { default as __fd_glob_55 } from "../docs/architecture/meta.json?collection=docs" +import { default as __fd_glob_54 } from "../docs/meta.json?collection=docs" +import * as __fd_glob_53 from "../docs/security/STEALTH_GUIDE.md?collection=docs" +import * as __fd_glob_52 from "../docs/security/ROUTE_GUARD_TIERS.md?collection=docs" +import * as __fd_glob_51 from "../docs/security/PUBLIC_CREDS.md?collection=docs" +import * as __fd_glob_50 from "../docs/security/GUARDRAILS.md?collection=docs" +import * as __fd_glob_49 from "../docs/security/ERROR_SANITIZATION.md?collection=docs" +import * as __fd_glob_48 from "../docs/security/COMPLIANCE.md?collection=docs" +import * as __fd_glob_47 from "../docs/security/CLI_TOKEN_AUTH.md?collection=docs" +import * as __fd_glob_46 from "../docs/security/CLI_TOKEN.md?collection=docs" +import * as __fd_glob_45 from "../docs/routing/REASONING_REPLAY.md?collection=docs" +import * as __fd_glob_44 from "../docs/routing/AUTO-COMBO.md?collection=docs" +import * as __fd_glob_43 from "../docs/reference/PROVIDER_REFERENCE.md?collection=docs" +import * as __fd_glob_42 from "../docs/reference/FREE_TIERS.md?collection=docs" +import * as __fd_glob_41 from "../docs/reference/ENVIRONMENT.md?collection=docs" +import * as __fd_glob_40 from "../docs/reference/CLI-TOOLS.md?collection=docs" +import * as __fd_glob_39 from "../docs/reference/API_REFERENCE.md?collection=docs" +import * as __fd_glob_38 from "../docs/ops/VM_DEPLOYMENT_GUIDE.md?collection=docs" +import * as __fd_glob_37 from "../docs/ops/TUNNELS_GUIDE.md?collection=docs" +import * as __fd_glob_36 from "../docs/ops/SQLITE_RUNTIME.md?collection=docs" +import * as __fd_glob_35 from "../docs/ops/RELEASE_CHECKLIST.md?collection=docs" +import * as __fd_glob_34 from "../docs/ops/PROXY_GUIDE.md?collection=docs" +import * as __fd_glob_33 from "../docs/ops/FLY_IO_DEPLOYMENT_GUIDE.md?collection=docs" +import * as __fd_glob_32 from "../docs/ops/E2E_DASHBOARD_SHAKEDOWN_v3.8.0.md?collection=docs" +import * as __fd_glob_31 from "../docs/ops/COVERAGE_PLAN.md?collection=docs" +import * as __fd_glob_30 from "../docs/guides/USER_GUIDE.md?collection=docs" +import * as __fd_glob_29 from "../docs/guides/UNINSTALL.md?collection=docs" +import * as __fd_glob_28 from "../docs/guides/TROUBLESHOOTING.md?collection=docs" +import * as __fd_glob_27 from "../docs/guides/TERMUX_GUIDE.md?collection=docs" +import * as __fd_glob_26 from "../docs/guides/SETUP_GUIDE.md?collection=docs" +import * as __fd_glob_25 from "../docs/guides/PWA_GUIDE.md?collection=docs" +import * as __fd_glob_24 from "../docs/guides/KIRO_SETUP.md?collection=docs" +import * as __fd_glob_23 from "../docs/guides/I18N.md?collection=docs" +import * as __fd_glob_22 from "../docs/guides/FEATURES.md?collection=docs" +import * as __fd_glob_21 from "../docs/guides/ELECTRON_GUIDE.md?collection=docs" +import * as __fd_glob_20 from "../docs/guides/DOCKER_GUIDE.md?collection=docs" +import * as __fd_glob_19 from "../docs/frameworks/WEBHOOKS.md?collection=docs" +import * as __fd_glob_18 from "../docs/frameworks/SKILLS.md?collection=docs" +import * as __fd_glob_17 from "../docs/frameworks/OPENCODE.md?collection=docs" +import * as __fd_glob_16 from "../docs/frameworks/MEMORY.md?collection=docs" +import * as __fd_glob_15 from "../docs/frameworks/MCP-SERVER.md?collection=docs" +import * as __fd_glob_14 from "../docs/frameworks/GAMIFICATION.md?collection=docs" +import * as __fd_glob_13 from "../docs/frameworks/EVALS.md?collection=docs" +import * as __fd_glob_12 from "../docs/frameworks/CLOUD_AGENT.md?collection=docs" +import * as __fd_glob_11 from "../docs/frameworks/AGENT_PROTOCOLS_GUIDE.md?collection=docs" +import * as __fd_glob_10 from "../docs/frameworks/A2A-SERVER.md?collection=docs" +import * as __fd_glob_9 from "../docs/compression/RTK_COMPRESSION.md?collection=docs" +import * as __fd_glob_8 from "../docs/compression/COMPRESSION_RULES_FORMAT.md?collection=docs" +import * as __fd_glob_7 from "../docs/compression/COMPRESSION_LANGUAGE_PACKS.md?collection=docs" +import * as __fd_glob_6 from "../docs/compression/COMPRESSION_GUIDE.md?collection=docs" +import * as __fd_glob_5 from "../docs/compression/COMPRESSION_ENGINES.md?collection=docs" +import * as __fd_glob_4 from "../docs/architecture/RESILIENCE_GUIDE.md?collection=docs" +import * as __fd_glob_3 from "../docs/architecture/REPOSITORY_MAP.md?collection=docs" +import * as __fd_glob_2 from "../docs/architecture/CODEBASE_DOCUMENTATION.md?collection=docs" +import * as __fd_glob_1 from "../docs/architecture/AUTHZ_GUIDE.md?collection=docs" +import * as __fd_glob_0 from "../docs/architecture/ARCHITECTURE.md?collection=docs" +import { server } from 'fumadocs-mdx/runtime/server'; +import type * as Config from '../source.config'; + +const create = server({"doc":{"passthroughs":["extractedReferences"]}}); + +export const docs = await create.docs("docs", "docs", {"meta.json": __fd_glob_54, "architecture/meta.json": __fd_glob_55, "compression/meta.json": __fd_glob_56, "frameworks/meta.json": __fd_glob_57, "guides/meta.json": __fd_glob_58, "ops/meta.json": __fd_glob_59, "reference/meta.json": __fd_glob_60, "reference/openapi.yaml": __fd_glob_61, "routing/meta.json": __fd_glob_62, "security/meta.json": __fd_glob_63, }, {"architecture/ARCHITECTURE.md": __fd_glob_0, "architecture/AUTHZ_GUIDE.md": __fd_glob_1, "architecture/CODEBASE_DOCUMENTATION.md": __fd_glob_2, "architecture/REPOSITORY_MAP.md": __fd_glob_3, "architecture/RESILIENCE_GUIDE.md": __fd_glob_4, "compression/COMPRESSION_ENGINES.md": __fd_glob_5, "compression/COMPRESSION_GUIDE.md": __fd_glob_6, "compression/COMPRESSION_LANGUAGE_PACKS.md": __fd_glob_7, "compression/COMPRESSION_RULES_FORMAT.md": __fd_glob_8, "compression/RTK_COMPRESSION.md": __fd_glob_9, "frameworks/A2A-SERVER.md": __fd_glob_10, "frameworks/AGENT_PROTOCOLS_GUIDE.md": __fd_glob_11, "frameworks/CLOUD_AGENT.md": __fd_glob_12, "frameworks/EVALS.md": __fd_glob_13, "frameworks/GAMIFICATION.md": __fd_glob_14, "frameworks/MCP-SERVER.md": __fd_glob_15, "frameworks/MEMORY.md": __fd_glob_16, "frameworks/OPENCODE.md": __fd_glob_17, "frameworks/SKILLS.md": __fd_glob_18, "frameworks/WEBHOOKS.md": __fd_glob_19, "guides/DOCKER_GUIDE.md": __fd_glob_20, "guides/ELECTRON_GUIDE.md": __fd_glob_21, "guides/FEATURES.md": __fd_glob_22, "guides/I18N.md": __fd_glob_23, "guides/KIRO_SETUP.md": __fd_glob_24, "guides/PWA_GUIDE.md": __fd_glob_25, "guides/SETUP_GUIDE.md": __fd_glob_26, "guides/TERMUX_GUIDE.md": __fd_glob_27, "guides/TROUBLESHOOTING.md": __fd_glob_28, "guides/UNINSTALL.md": __fd_glob_29, "guides/USER_GUIDE.md": __fd_glob_30, "ops/COVERAGE_PLAN.md": __fd_glob_31, "ops/E2E_DASHBOARD_SHAKEDOWN_v3.8.0.md": __fd_glob_32, "ops/FLY_IO_DEPLOYMENT_GUIDE.md": __fd_glob_33, "ops/PROXY_GUIDE.md": __fd_glob_34, "ops/RELEASE_CHECKLIST.md": __fd_glob_35, "ops/SQLITE_RUNTIME.md": __fd_glob_36, "ops/TUNNELS_GUIDE.md": __fd_glob_37, "ops/VM_DEPLOYMENT_GUIDE.md": __fd_glob_38, "reference/API_REFERENCE.md": __fd_glob_39, "reference/CLI-TOOLS.md": __fd_glob_40, "reference/ENVIRONMENT.md": __fd_glob_41, "reference/FREE_TIERS.md": __fd_glob_42, "reference/PROVIDER_REFERENCE.md": __fd_glob_43, "routing/AUTO-COMBO.md": __fd_glob_44, "routing/REASONING_REPLAY.md": __fd_glob_45, "security/CLI_TOKEN.md": __fd_glob_46, "security/CLI_TOKEN_AUTH.md": __fd_glob_47, "security/COMPLIANCE.md": __fd_glob_48, "security/ERROR_SANITIZATION.md": __fd_glob_49, "security/GUARDRAILS.md": __fd_glob_50, "security/PUBLIC_CREDS.md": __fd_glob_51, "security/ROUTE_GUARD_TIERS.md": __fd_glob_52, "security/STEALTH_GUIDE.md": __fd_glob_53, }); \ No newline at end of file diff --git a/.source/source.config.mjs b/.source/source.config.mjs new file mode 100644 index 0000000000..1b0644fe1e --- /dev/null +++ b/.source/source.config.mjs @@ -0,0 +1,22 @@ +// source.config.ts +import { defineDocs, defineConfig } from "fumadocs-mdx/config"; +var docs = defineDocs({ + dir: "docs", + docs: { + files: [ + "./architecture/**/*.md", + "./guides/**/*.md", + "./reference/**/*.md", + "./frameworks/**/*.md", + "./routing/**/*.md", + "./security/**/*.md", + "./compression/**/*.md", + "./ops/**/*.md" + ] + } +}); +var source_config_default = defineConfig(); +export { + source_config_default as default, + docs +}; diff --git a/@omniroute/opencode-plugin/tests/features.test.ts b/@omniroute/opencode-plugin/tests/features.test.ts index fa6a6d78b7..d245ee9aca 100644 --- a/@omniroute/opencode-plugin/tests/features.test.ts +++ b/@omniroute/opencode-plugin/tests/features.test.ts @@ -929,9 +929,24 @@ test("canonicalDedupSet: multi-provider — drops all canonical twins where alia test("buildAliasIndex: indexes one entry per alias (first-wins on duplicates)", () => { const map = makeEnrichmentMap([ - { key: "cohere/command-a", providerAlias: "cohere", providerCanonical: "cohere", providerDisplayName: "Cohere" }, - { key: "cohere/embed-v4", providerAlias: "cohere", providerCanonical: "cohere", providerDisplayName: "Cohere" }, - { key: "cc/claude-opus-4-7", providerAlias: "cc", providerCanonical: "claude", providerDisplayName: "Claude" }, + { + key: "cohere/command-a", + providerAlias: "cohere", + providerCanonical: "cohere", + providerDisplayName: "Cohere", + }, + { + key: "cohere/embed-v4", + providerAlias: "cohere", + providerCanonical: "cohere", + providerDisplayName: "Cohere", + }, + { + key: "cc/claude-opus-4-7", + providerAlias: "cc", + providerCanonical: "claude", + providerDisplayName: "Claude", + }, ]); const idx = buildAliasIndex(map); assert.equal(idx.size, 2); @@ -942,16 +957,19 @@ test("buildAliasIndex: indexes one entry per alias (first-wins on duplicates)", test("buildAliasIndex: upgrades to first entry with non-empty providerDisplayName", () => { const map = makeEnrichmentMap([ { key: "cohere/a", providerAlias: "cohere", providerCanonical: "cohere" }, // no displayName - { key: "cohere/b", providerAlias: "cohere", providerCanonical: "cohere", providerDisplayName: "Cohere" }, + { + key: "cohere/b", + providerAlias: "cohere", + providerCanonical: "cohere", + providerDisplayName: "Cohere", + }, ]); const idx = buildAliasIndex(map); assert.equal(idx.get("cohere")?.providerDisplayName, "Cohere"); }); test("buildAliasIndex: skips entries with no providerAlias", () => { - const map = makeEnrichmentMap([ - { key: "orphan", providerCanonical: "something" }, - ]); + const map = makeEnrichmentMap([{ key: "orphan", providerCanonical: "something" }]); assert.equal(buildAliasIndex(map).size, 0); }); @@ -970,7 +988,13 @@ test("resolveProviderTagEntry: no direct, alias matches → synthesised entry fr // cohere class: direct lookup misses (model not in curated 10) but // alias=cohere maps to the cohere slot in /api/pricing/models. const map = makeEnrichmentMap([ - { key: "cohere/command-a", providerAlias: "cohere", providerCanonical: "cohere", providerDisplayName: "Cohere", name: "Command A" }, + { + key: "cohere/command-a", + providerAlias: "cohere", + providerCanonical: "cohere", + providerDisplayName: "Cohere", + name: "Command A", + }, ]); const idx = buildAliasIndex(map); const out = resolveProviderTagEntry("cohere/rerank-multilingual-v3.0", undefined, idx); @@ -986,7 +1010,12 @@ test("resolveProviderTagEntry: canonical prefix → alias fallback (pollinations // /api/pricing/models keys it under alias `pol`. canonicalToAlias map // bridges the gap. const map = makeEnrichmentMap([ - { key: "pol/openai-large", providerAlias: "pol", providerCanonical: "pollinations", providerDisplayName: "Pollinations" }, + { + key: "pol/openai-large", + providerAlias: "pol", + providerCanonical: "pollinations", + providerDisplayName: "Pollinations", + }, ]); const idx = buildAliasIndex(map); const c2a = buildCanonicalToAliasMap(map); @@ -1004,7 +1033,12 @@ test("resolveProviderTagEntry: no prefix and no direct → returns direct (undef test("resolveProviderTagEntry: prefix unknown to alias index → returns direct (undefined)", () => { const map = makeEnrichmentMap([ - { key: "cc/x", providerAlias: "cc", providerCanonical: "claude", providerDisplayName: "Claude" }, + { + key: "cc/x", + providerAlias: "cc", + providerCanonical: "claude", + providerDisplayName: "Claude", + }, ]); const idx = buildAliasIndex(map); const out = resolveProviderTagEntry("unknownprovider/some-model", undefined, idx); @@ -1017,7 +1051,12 @@ test("resolveProviderTagEntry: direct present but empty alias+display → still // via alias index. const direct = { name: "Some Model" }; const map = makeEnrichmentMap([ - { key: "cohere/x", providerAlias: "cohere", providerCanonical: "cohere", providerDisplayName: "Cohere" }, + { + key: "cohere/x", + providerAlias: "cohere", + providerCanonical: "cohere", + providerDisplayName: "Cohere", + }, ]); const idx = buildAliasIndex(map); const out = resolveProviderTagEntry("cohere/rerank-v4.0", direct, idx); diff --git a/CHANGELOG.md b/CHANGELOG.md index 4a37ea2210..bad181bbb7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,70 @@ ## [Unreleased] +### ✨ New Features + +### 🔧 Bug Fixes + +--- + +## [3.8.3] — 2026-05-24 + +### ✨ New Features + +- **feat(combos):** universal context handoff for cross-model conversation continuity — structured XML summary system (``) that preserves conversation continuity and handles state transfer when combo routing switches models. ([#2653](https://github.com/diegosouzapw/OmniRoute/pull/2653) — thanks @herjarsa) +- **feat(docs):** migrate `/docs` to Fumadocs MDX with nested routes — replaces the custom docs engine with Fumadocs, adding `[...slug]` catch-all routing, search API at `/docs/api/search`, `source.config.ts` content configuration, and `meta.json` navigation files across 8 doc sections (`architecture/`, `compression/`, `frameworks/`, `guides/`, `ops/`, `reference/`, `routing/`, `security/`). Includes 50+ URL redirects for backward compatibility via `next.config.mjs`. ([#2614](https://github.com/diegosouzapw/OmniRoute/pull/2614) — thanks @ovehbe) +- **feat(dashboard):** add search and filters to `/dashboard/api-manager` — filter bar with search by name/key, active-only toggle (persisted to localStorage), status filter (active/disabled/banned/expired), type filter (standard/manage/restricted), filter count badges, and empty state with "Clear Filters" button. ([#2628](https://github.com/diegosouzapw/OmniRoute/pull/2628) / [#2641](https://github.com/diegosouzapw/OmniRoute/pull/2641) — thanks @diegosouzapw) +- **feat(dashboard):** free-tier grouping with symbolic link in `/dashboard/providers` — groups and displays all free-tier providers from all categories dynamically using `hasFree: true` properties without removing them from their native lists. Displays category dot and amber dot with localizable tooltips, dedupes search results by provider ID, and corrects free tier count statistics. ([#2632](https://github.com/diegosouzapw/OmniRoute/pull/2632) — thanks @diegosouzapw) +- **feat(dashboard):** risk notice modal for sensitive providers — show a soft, informative warning modal when connecting to session-based or OAuth providers (like Claude, Cursor, Copilot) for the first time. Adds `subscriptionRisk` properties to 20 providers, localizable templates, and stores acknowledgment in localStorage. ([#2633](https://github.com/diegosouzapw/OmniRoute/pull/2633) / [#2638](https://github.com/diegosouzapw/OmniRoute/pull/2638) — thanks @diegosouzapw) +- **feat(dashboard):** refactor free-tier provider dashboard layout — cleans up visual clutter, reorganizes categories, hides redundant banners, and integrates free-tier categories nicely into the primary provider interface. ([#2640](https://github.com/diegosouzapw/OmniRoute/pull/2640) — thanks @diegosouzapw) +- **feat(dashboard):** mini-playground inline (Phase 4) — integrated interactive mini-playground capabilities to provider details pages, including support for specialized example cards (Embedding, Image, LLM Chat, Music, STT, TTS, Video, Web Fetch, Web Search), unified API key loading hooks, model listing hooks, and curl command builder. ([#2648](https://github.com/diegosouzapw/OmniRoute/pull/2648) — thanks @diegosouzapw) +- **feat(webfetch):** category support with dedicated media providers page and executors for Firecrawl, Jina Reader, and Tavily. ([#2645](https://github.com/diegosouzapw/OmniRoute/pull/2645) — thanks @diegosouzapw) +- **feat(adapta):** integrate Adapta Org (`adapta-web`) provider with automatic Clerk authentication refresh and custom onboarding tutorial modal. ([#2643](https://github.com/diegosouzapw/OmniRoute/pull/2643) — thanks @df4p) +- **feat(i18n):** complete translations for Simplified Chinese — translates 1220 missing keys bringing UI coverage to 98.8% with 0 placeholders. ([#2655](https://github.com/diegosouzapw/OmniRoute/pull/2655) — thanks @L-aros) +- **feat(dashboard):** add Cmd+K / Ctrl+K command palette for sidebar navigation and a slideover panel for LLM provider testing UI. ([#2656](https://github.com/diegosouzapw/OmniRoute/pull/2656) — thanks @mrmm) +- **feat(i18n):** finish Simplified Chinese (zh-CN) UI coverage with 377 translated entries. ([#2659](https://github.com/diegosouzapw/OmniRoute/pull/2659) — thanks @L-aros) +- **feat(dashboard):** chat-first test slide-over layout — consolidates header controls (Model/Key selects, Clear button) into a unified toolbar, maximizes vertical conversation space, integrates live tailing of provider logs in Logs tab, and locks composer focus for keyboard-only convenience. ([#2660](https://github.com/diegosouzapw/OmniRoute/pull/2660) — thanks @mrmm) +- **feat(cli):** desktop updates, autostart, and headless CLI modes — integrates native auto-updater checks, login autostart (Linux .desktop, macOS/Windows login items), and a background headless server CLI daemon mode (`--headless` or `OMNIROUTE_HEADLESS=true`) into the Electron app wrapper. ([#2662](https://github.com/diegosouzapw/OmniRoute/pull/2662) — thanks @benzntech) +- **feat(quota):** card-grid layout and provider group headers under quota management — replaces monolithic table with a beautiful 4-column card grid in limits. ([#2667](https://github.com/diegosouzapw/OmniRoute/pull/2667) — thanks @Gi99lin) +- **feat(dashboard):** real-time WebSocket live monitoring daemon — runs a Node.js WebSocket daemon sidecar on port `20129` to emit real-time events for request starts/completes/fails, combo attempts, and credential status in the dashboard logs. ([#2668](https://github.com/diegosouzapw/OmniRoute/pull/2668) — thanks @herjarsa) +- **feat(copilot):** AI assistant with CodeGraph + CLI + knowledge base — integrates a dashboard assistant with CodeGraph knowledge base access and CLI capabilities for app exploration. ([#2669](https://github.com/diegosouzapw/OmniRoute/pull/2669) — thanks @ovehbe / @herjarsa) +- **feat(pipeline):** pre-request middleware hooks — pipeline executing custom JS hooks before routing/combo logic to mutate headers/body or short-circuit requests. ([#2670](https://github.com/diegosouzapw/OmniRoute/pull/2670) — thanks @herjarsa) +- **feat(resilience):** credential health check + adaptive circuit breaker v2 — background connection health check scheduler with progressive circuit breaker adding DEGRADED state and HALF-OPEN recovery validation to avoid latency spikes. ([#2671](https://github.com/diegosouzapw/OmniRoute/pull/2671) — thanks @herjarsa) +- **feat(playground):** combo routing visual simulator — interactive route simulation page at `/dashboard/combos/playground` to showcase cascade hops, latency, and cost estimates. ([#2672](https://github.com/diegosouzapw/OmniRoute/pull/2672) — thanks @herjarsa) +- **feat(auth):** API key groups with model-level permissions — group definitions with model-level wildcards/denies where API keys inherit group-scoped restrictions. ([#2673](https://github.com/diegosouzapw/OmniRoute/pull/2673) — thanks @herjarsa) +- **feat(pwa):** enhanced manifest + push notification support — polishes offline shortcuts, screenshots, display metadata, and push service workers. ([#2674](https://github.com/diegosouzapw/OmniRoute/pull/2674) — thanks @herjarsa) +- **feat(proxy):** serverless relay proxy endpoints with rate limiting — public relay proxy endpoints with cost caps and rate limits, CRUD API, and dashboard usage tracking. ([#2675](https://github.com/diegosouzapw/OmniRoute/pull/2675) — thanks @herjarsa) + +### 🔧 Bug Fixes + +- **fix(settings):** Require Login modal Cancel button text and dismissal — modal now renders localized cancel label via the `common` namespace and closes correctly without modifying settings when cancelled. ([#2649](https://github.com/diegosouzapw/OmniRoute/pull/2649) — thanks @Chewji9875) +- **fix(deepseek-web):** re-apply SSE parser, prompt format, and error handling fixes — handles all 3 DeepSeek SSE stream formats (initial fragments, APPEND operations, bare string tokens), uses non-greedy regex for markdown image stripping, simplifies prompt to single-turn, checks `json.code` before token extraction, and uses `accessToken` fallback for session cache eviction on auth errors. ([#2616](https://github.com/diegosouzapw/OmniRoute/pull/2616) — thanks @ovehbe) +- **fix(deepseek-web):** SSE thinking/search routing and session lifecycle — properly routes thinking vs content fragments based on `thinking_enabled` flag, handles search results with citation indices, appends search result footnotes, refactors `transformSSE()` and `collectSSEContent()` with shared helpers. ([#2624](https://github.com/diegosouzapw/OmniRoute/pull/2624) — thanks @ovehbe) +- **fix(codex):** use allowlist to strip non-Responses-API fields in non-passthrough path — strips residual Chat Completions fields (`stream_options`, `service_tier`, `store`, `metadata`) from the request body when routing through the non-passthrough (translation) code path, preventing GPT-5.5 from receiving invalid parameters. ([#2615](https://github.com/diegosouzapw/OmniRoute/pull/2615) — thanks @diegosouzapw) +- **fix(catalog):** skip static PROVIDER_MODELS when synced models exist — prevents stale/duplicate model entries in `/v1/models` for auto-synced providers. ([#2625](https://github.com/diegosouzapw/OmniRoute/pull/2625) — thanks @herjarsa) +- **fix(qoder):** Cosy auth fallback for PAT tokens + vision support for qwen3-vl-plus — when a PAT token gets 401, falls back to Cosy auth against `api1.qoder.sh`; adds `supportsVision: true` to qwen3-vl-plus. ([#2629](https://github.com/diegosouzapw/OmniRoute/pull/2629) — thanks @herjarsa) +- **fix(cli):** register tsx loader and add opencode config subcommand — registers `tsx/esm` at CLI startup so dynamic `.ts` imports resolve; adds `omniroute config opencode` convenience alias. ([#2631](https://github.com/diegosouzapw/OmniRoute/pull/2631) — thanks @amogus22877769) +- **fix(claude):** improve Pi and OpenCode compatibility — adds Pi Coding Agent anchors to system transform removal, stores `_toolNameMap` as non-enumerable, strips `context_management` when thinking is disabled. ([#2621](https://github.com/diegosouzapw/OmniRoute/pull/2621) — thanks @unitythemaker) +- **fix(passthrough):** restore semantic passthrough system-role-only extraction — reverts full `normalizeClaudeUpstreamMessages()` to lighter `extractSystemRoleMessages()` in CC semantic passthrough paths, preventing document/tool chain corruption. ([#2620](https://github.com/diegosouzapw/OmniRoute/pull/2620) — thanks @Tentoxa) +- **fix(kiro):** stabilize conversationId across prompt compression — captures pre-compression body and uses the original first user message as seed for UUID v5, keeping Kiro's AWS conversation context stable. ([#2630](https://github.com/diegosouzapw/OmniRoute/pull/2630) — thanks @HALDRO) +- **fix(t3-chat-web):** close implementation gaps for t3.chat TanStack Start, tracking of stream_options, and retry configurations — parses TSS Turbo Stream Serialization from `_serverFn/*`, tracks request `combo_strategy` via database migration `062_usage_history_combo_strategy.sql`, and makes batch retry backoffs custom-configurable via environment variables. ([#2634](https://github.com/diegosouzapw/OmniRoute/pull/2634) — thanks @oyi77) +- **fix(reasoning):** extend empty `reasoning_content` injection to prevent tool call loops in Kimi K2 and replay models — injects empty `reasoning_content` field to Kimi models during tool-calling sequences to bypass loop issues. ([#2639](https://github.com/diegosouzapw/OmniRoute/pull/2639) — thanks @herjarsa) +- **fix(cli):** Linux autostart via systemd user service on headless VPS — adds auto-generating systemd user service unit for headless setups on Linux, updating tray configs and system variables allowlist (`LOGNAME` and `XDG_CURRENT_DESKTOP`). ([#2635](https://github.com/diegosouzapw/OmniRoute/pull/2635) — thanks @janeza2) +- **fix(combo):** preserve `` tag in SSE stream output for combos when using `context_cache_protection` to ensure correct context pinning round-trips. ([#2646](https://github.com/diegosouzapw/OmniRoute/pull/2646) — thanks @herjarsa) +- **fix(rtk):** prevent false positives in RTK compression by skipping content-based filter matching for non-shell tool results (e.g. read_file, grep_search). ([#2642](https://github.com/diegosouzapw/OmniRoute/pull/2642) — thanks @HALDRO) +- **fix(translator):** enable Claude extended thinking for Copilot Responses-API requests — handles reasoning budget and translations for Copilot. ([#2647](https://github.com/diegosouzapw/OmniRoute/pull/2647) — thanks @ivan-mezentsev) +- **fix(tests):** remove duplicate assertion in schema coercion & fix(cli): ignore system vars in env check. (thanks @diegosouzapw) +- **fix(combo):** resolve pending request leaks on unresponsive combo targets — implements a default 60-second per-target timeout during combo routing loops to abort hanging upstream requests and release capacity limits. ([#2663](https://github.com/diegosouzapw/OmniRoute/pull/2663) — thanks @Chewji9875) +- **fix(proxy):** save custom dashboard proxies directly in SQLite registry — writes new provider/account/global/combo custom proxies directly to the modern `proxy_registry` database and assigns them via `proxy_assignments` instead of creating duplicate configurations. ([#2661](https://github.com/diegosouzapw/OmniRoute/pull/2661) — thanks @terence71-glitch) +- **fix(settings):** expand effortLevel enum to support xhigh and max reasoning efforts — adds `xhigh` and `max` levels to the updateThinkingBudgetSchema to resolve validation failures that silently discarded top-effort request payloads. ([#2666](https://github.com/diegosouzapw/OmniRoute/pull/2666) — thanks @mrmm) +- **fix(codex):** Codex OAuth refresh token reuse race condition under parallel requests. ([#2667](https://github.com/diegosouzapw/OmniRoute/pull/2667) — thanks @diegosouzapw) + +### 📝 Maintenance + +- **chore(config):** ignore additional agent workflow command files (`.agents/commands/`). (thanks @diegosouzapw) +- **chore(config):** ignore `memory-bank` and Cursor agent rules from tracking. (thanks @ovehbe) +- **chore(ci):** publish @omniroute/opencode-plugin to npm — adds a parallel build, test, and publish job to the npm release workflow for automated package deployment. ([#2666](https://github.com/diegosouzapw/OmniRoute/pull/2666) — thanks @mrmm) + --- ## [3.8.2] — 2026-05-22 diff --git a/bin/cli/commands/autostart.mjs b/bin/cli/commands/autostart.mjs index de77f2d701..066cbc5a37 100644 --- a/bin/cli/commands/autostart.mjs +++ b/bin/cli/commands/autostart.mjs @@ -33,7 +33,7 @@ export function registerAutostart(program) { .description(t("autostart.status") || "Show autostart status") .action(async (opts, c) => { const globalOpts = c.optsWithGlobals(); - const { isAutostartEnabled } = await import("../tray/autostart.mjs"); - emit({ enabled: isAutostartEnabled() }, globalOpts); + const { getAutostartStatus } = await import("../tray/autostart.mjs"); + emit(getAutostartStatus(), globalOpts); }); } diff --git a/bin/cli/commands/config.mjs b/bin/cli/commands/config.mjs index bdea980ac6..da40cb9f3b 100644 --- a/bin/cli/commands/config.mjs +++ b/bin/cli/commands/config.mjs @@ -268,27 +268,52 @@ export function registerConfig(program) { config .command("set ") .description("Write config for a tool") - .option("--base-url ", "OmniRoute API base URL", "http://localhost:20128/v1") - .option("--api-key ", "API key for the tool") .option("--model ", "Model identifier (where applicable)") .option("--non-interactive", "Do not prompt for confirmation") .option("--yes", "Skip confirmation prompt") .action(async (tool, opts, cmd) => { const globalOpts = cmd.parent.optsWithGlobals(); - const exitCode = await runConfigSetCommand(tool, { ...opts, output: globalOpts.output }); + const exitCode = await runConfigSetCommand(tool, { + ...opts, + apiKey: opts.apiKey || globalOpts.apiKey || process.env.OMNIROUTE_API_KEY, + baseUrl: opts.baseUrl || globalOpts.baseUrl || process.env.OMNIROUTE_BASE_URL, + output: globalOpts.output, + }); if (exitCode !== 0) process.exit(exitCode); }); config .command("validate ") .description("Validate config format without writing") - .option("--base-url ", "OmniRoute API base URL", "http://localhost:20128/v1") - .option("--api-key ", "API key for the tool") .option("--model ", "Model identifier (where applicable)") .option("--json", "Output as JSON") .action(async (tool, opts, cmd) => { const globalOpts = cmd.parent.optsWithGlobals(); - const exitCode = await runConfigValidateCommand(tool, { ...opts, output: globalOpts.output }); + const exitCode = await runConfigValidateCommand(tool, { + ...opts, + apiKey: opts.apiKey || globalOpts.apiKey || process.env.OMNIROUTE_API_KEY, + baseUrl: opts.baseUrl || globalOpts.baseUrl || process.env.OMNIROUTE_BASE_URL, + output: globalOpts.output, + }); + if (exitCode !== 0) process.exit(exitCode); + }); + + // Convenience alias: `config opencode` → `config set opencode` + // Matches the documented CLI usage in docs/frameworks/OPENCODE.md. + config + .command("opencode") + .description("Generate OpenCode config (alias for 'config set opencode')") + .option("--model ", "Model identifier") + .option("--non-interactive", "Do not prompt for confirmation") + .option("--yes", "Skip confirmation prompt") + .action(async (opts, cmd) => { + const globalOpts = cmd.parent.optsWithGlobals(); + const exitCode = await runConfigSetCommand("opencode", { + ...opts, + apiKey: opts.apiKey || globalOpts.apiKey || process.env.OMNIROUTE_API_KEY, + baseUrl: opts.baseUrl || globalOpts.baseUrl || process.env.OMNIROUTE_BASE_URL, + output: globalOpts.output, + }); if (exitCode !== 0) process.exit(exitCode); }); diff --git a/bin/cli/locales/en.json b/bin/cli/locales/en.json index 9434b9e8d8..18e58b4c2a 100644 --- a/bin/cli/locales/en.json +++ b/bin/cli/locales/en.json @@ -1225,9 +1225,9 @@ "quit": "Quit OmniRoute via tray" }, "autostart": { - "description": "Manage OmniRoute autostart at login", - "enable": "Enable autostart at login", - "disable": "Disable autostart at login", + "description": "Manage OmniRoute autostart at boot (Linux: systemd user service)", + "enable": "Enable autostart at boot", + "disable": "Disable autostart at boot", "status": "Show autostart status" }, "runtime": { diff --git a/bin/cli/tray/autostart.mjs b/bin/cli/tray/autostart.mjs index 8ecb59eb7d..acba8090f1 100644 --- a/bin/cli/tray/autostart.mjs +++ b/bin/cli/tray/autostart.mjs @@ -1,4 +1,4 @@ -import { existsSync, writeFileSync, unlinkSync, mkdirSync } from "node:fs"; +import { existsSync, writeFileSync, unlinkSync, mkdirSync, realpathSync } from "node:fs"; import { execSync } from "node:child_process"; import { homedir } from "node:os"; import { join, dirname } from "node:path"; @@ -6,11 +6,172 @@ import { fileURLToPath } from "node:url"; const APP_LABEL = "com.omniroute.autostart"; const WIN_REG_VALUE = "OmniRoute"; +const LINUX_SERVICE_NAME = "omniroute.service"; +const LINUX_DESKTOP_NAME = "omniroute.desktop"; function resolveCliPath() { - return ( - process.argv[1] || join(dirname(fileURLToPath(import.meta.url)), "..", "..", "omniroute.mjs") - ); + const candidates = []; + if (process.argv[1]) candidates.push(process.argv[1]); + try { + const which = execSync("command -v omniroute 2>/dev/null", { encoding: "utf8" }).trim(); + if (which) candidates.push(which); + } catch { + // command -v unavailable + } + candidates.push(join(dirname(fileURLToPath(import.meta.url)), "..", "..", "omniroute.mjs")); + + for (const candidate of candidates) { + if (!candidate) continue; + try { + const resolved = realpathSync(candidate); + if (resolved.endsWith("omniroute.mjs") && existsSync(resolved)) return resolved; + } catch { + // try next candidate + } + } + + const fallback = candidates[candidates.length - 1]; + return existsSync(fallback) ? fallback : null; +} + +function quoteExecArg(value) { + if (!/[ \t"'\\]/.test(value)) return value; + return `"${value.replace(/(["\\])/g, "\\$1")}"`; +} + +function buildServeExecLine(cliPath, { tray = false } = {}) { + const parts = [quoteExecArg(process.execPath), quoteExecArg(cliPath), "serve", "--no-open"]; + if (tray) parts.push("--tray"); + return parts.join(" "); +} + +function isGraphicalLinuxSession() { + if (process.env.DISPLAY || process.env.WAYLAND_DISPLAY) return true; + if (process.env.XDG_CURRENT_DESKTOP) return true; + return false; +} + +function linuxSystemdUnitPath() { + return join(homedir(), ".config", "systemd", "user", LINUX_SERVICE_NAME); +} + +function linuxDesktopPath() { + return join(homedir(), ".config", "autostart", LINUX_DESKTOP_NAME); +} + +function runUserSystemctl(args, { ignoreFailure = true } = {}) { + try { + execSync(`systemctl --user ${args}`, { stdio: "ignore" }); + return true; + } catch { + return ignoreFailure ? false : false; + } +} + +function isSystemdUserAvailable() { + try { + execSync("systemctl --user --version", { stdio: "ignore" }); + return true; + } catch { + return false; + } +} + +function isSystemdServiceEnabled() { + if (!existsSync(linuxSystemdUnitPath())) return false; + try { + execSync(`systemctl --user is-enabled ${LINUX_SERVICE_NAME}`, { stdio: "ignore" }); + return true; + } catch { + return false; + } +} + +function tryEnableLinger() { + try { + const user = + process.env.USER || + process.env.LOGNAME || + execSync("whoami", { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim(); + if (!user) return false; + execSync(`loginctl enable-linger ${JSON.stringify(user)}`, { stdio: "ignore" }); + return true; + } catch { + return false; + } +} + +function writeLinuxSystemdUnit(cliPath) { + const unitDir = dirname(linuxSystemdUnitPath()); + mkdirSync(unitDir, { recursive: true }); + const envFile = join(homedir(), ".omniroute", ".env"); + const lines = [ + "[Unit]", + "Description=OmniRoute AI proxy router", + "After=network-online.target", + "Wants=network-online.target", + "", + "[Service]", + "Type=simple", + `ExecStart=${buildServeExecLine(cliPath, { tray: false })}`, + "Restart=on-failure", + "RestartSec=5", + ]; + if (existsSync(envFile)) lines.push(`EnvironmentFile=-${envFile}`); + lines.push("", "[Install]", "WantedBy=default.target", ""); + writeFileSync(linuxSystemdUnitPath(), `${lines.join("\n")}\n`, { mode: 0o644 }); +} + +function writeLinuxDesktopEntry(cliPath) { + const dir = dirname(linuxDesktopPath()); + mkdirSync(dir, { recursive: true }); + const desktop = + [ + "[Desktop Entry]", + "Type=Application", + "Name=OmniRoute", + "Comment=AI proxy router with auto fallback", + `Exec=${buildServeExecLine(cliPath, { tray: true })}`, + "Terminal=false", + "Hidden=false", + "X-GNOME-Autostart-enabled=true", + ].join("\n") + "\n"; + writeFileSync(linuxDesktopPath(), desktop, { mode: 0o644 }); +} + +export function getAutostartStatus() { + if (process.platform === "linux") { + const systemdUnit = linuxSystemdUnitPath(); + const desktopFile = linuxDesktopPath(); + const systemdEnabled = isSystemdServiceEnabled(); + const desktopEnabled = existsSync(desktopFile); + const enabled = systemdEnabled || desktopEnabled; + let mechanism = null; + if (systemdEnabled) mechanism = "systemd-user"; + else if (desktopEnabled) mechanism = "xdg-desktop"; + return { + enabled, + mechanism, + systemdUnit: existsSync(systemdUnit) ? systemdUnit : null, + desktopFile: desktopEnabled ? desktopFile : null, + linger: tryReadLingerEnabled(), + }; + } + return { enabled: isAutostartEnabled(), mechanism: null }; +} + +function tryReadLingerEnabled() { + try { + const user = process.env.USER || process.env.LOGNAME; + if (!user) return null; + const out = execSync(`loginctl show-user ${JSON.stringify(user)} -p Linger`, { + encoding: "utf8", + stdio: ["ignore", "pipe", "ignore"], + }); + return out.includes("Linger=yes"); + } catch { + return null; + } } export function enable() { @@ -39,6 +200,7 @@ function enableMac() { mkdirSync(plistDir, { recursive: true }); const plistPath = join(plistDir, `${APP_LABEL}.plist`); const cliPath = resolveCliPath(); + if (!cliPath) return false; const plist = ` @@ -77,6 +239,7 @@ function isEnabledMac() { function enableWin() { const cliPath = resolveCliPath(); + if (!cliPath) return false; const value = `"${process.execPath}" "${cliPath}" serve --tray --no-open`; try { execSync( @@ -114,34 +277,46 @@ function isEnabledWin() { } function enableLinux() { - const dir = join(homedir(), ".config", "autostart"); - mkdirSync(dir, { recursive: true }); const cliPath = resolveCliPath(); - const desktop = - [ - "[Desktop Entry]", - "Type=Application", - "Name=OmniRoute", - "Comment=AI proxy router with auto fallback", - `Exec=${process.execPath} ${cliPath} serve --tray --no-open`, - "Terminal=false", - "Hidden=false", - "X-GNOME-Autostart-enabled=true", - ].join("\n") + "\n"; - writeFileSync(join(dir, "omniroute.desktop"), desktop, { mode: 0o644 }); - return true; + if (!cliPath) return false; + + if (!isSystemdUserAvailable() && !isGraphicalLinuxSession()) { + return false; + } + + let ok = false; + + if (isSystemdUserAvailable()) { + writeLinuxSystemdUnit(cliPath); + runUserSystemctl("daemon-reload"); + ok = runUserSystemctl(`enable ${LINUX_SERVICE_NAME}`) || existsSync(linuxSystemdUnitPath()); + runUserSystemctl(`start ${LINUX_SERVICE_NAME}`); + tryEnableLinger(); + } + + if (isGraphicalLinuxSession()) { + writeLinuxDesktopEntry(cliPath); + ok = true; + } + + return ok || isEnabledLinux(); } function disableLinux() { - const desktopPath = join(homedir(), ".config", "autostart", "omniroute.desktop"); - try { - unlinkSync(desktopPath); - return true; - } catch { - return false; + if (isSystemdUserAvailable()) { + runUserSystemctl(`disable --now ${LINUX_SERVICE_NAME}`); + runUserSystemctl("daemon-reload"); } + try { + unlinkSync(linuxSystemdUnitPath()); + } catch {} + try { + unlinkSync(linuxDesktopPath()); + } catch {} + return !isEnabledLinux(); } function isEnabledLinux() { - return existsSync(join(homedir(), ".config", "autostart", "omniroute.desktop")); + if (isSystemdServiceEnabled()) return true; + return existsSync(linuxDesktopPath()); } diff --git a/bin/cli/tray/autostart.ts b/bin/cli/tray/autostart.ts index 512749433f..712d69c81b 100644 --- a/bin/cli/tray/autostart.ts +++ b/bin/cli/tray/autostart.ts @@ -1,164 +1,18 @@ -import { existsSync, writeFileSync, mkdirSync, unlinkSync } from "node:fs"; -import { join, dirname, basename, resolve } from "node:path"; -import { fileURLToPath } from "node:url"; -import { homedir, platform } from "node:os"; -import { execSync } from "node:child_process"; - -const __dirname = dirname(fileURLToPath(import.meta.url)); -const APP_NAME = "omniroute"; -const APP_LABEL = "com.omniroute.autostart"; - -function resolveCliPath(): string | null { - if ( - process.argv[1] && - basename(process.argv[1]) === "omniroute.mjs" && - existsSync(process.argv[1]) - ) { - return resolve(process.argv[1]); - } - // autostart.ts lives at bin/cli/tray/ → walk up to bin/omniroute.mjs - const guess = resolve(__dirname, "..", "..", "omniroute.mjs"); - return existsSync(guess) ? guess : null; -} +/** + * Tray autostart — delegates to autostart.mjs (single implementation). + */ +import { enable, disable, isAutostartEnabled, getAutostartStatus } from "./autostart.mjs"; export async function enableAutoStart(): Promise { - const cliPath = resolveCliPath(); - if (!cliPath) return false; - switch (platform()) { - case "darwin": - return enableMacOS(cliPath); - case "linux": - return enableLinux(cliPath); - case "win32": - return enableWindows(cliPath); - default: - return false; - } + return enable(); } export async function disableAutoStart(): Promise { - switch (platform()) { - case "darwin": - return disableMacOS(); - case "linux": - return disableLinux(); - case "win32": - return disableWindows(); - default: - return false; - } + return disable(); } export async function isAutoStartEnabled(): Promise { - switch (platform()) { - case "darwin": - return existsSync(macPlistPath()); - case "linux": - return existsSync(linuxDesktopPath()); - case "win32": - return windowsRegistryHasKey(); - default: - return false; - } + return isAutostartEnabled(); } -function macPlistPath(): string { - return join(homedir(), "Library", "LaunchAgents", `${APP_LABEL}.plist`); -} - -function enableMacOS(cliPath: string): boolean { - const plist = ` - - - Label${APP_LABEL} - ProgramArguments/usr/bin/envnode${cliPath}--tray - RunAtLoad - KeepAlive -`; - const path = macPlistPath(); - mkdirSync(dirname(path), { recursive: true }); - writeFileSync(path, plist, "utf-8"); - try { - execSync(`launchctl load -w "${path}"`); - } catch { - // non-fatal — file was written, launchctl may fail in CI - } - return true; -} - -function disableMacOS(): boolean { - const path = macPlistPath(); - if (!existsSync(path)) return true; - try { - execSync(`launchctl unload "${path}"`); - } catch { - // non-fatal - } - try { - unlinkSync(path); - } catch { - // non-fatal - } - return true; -} - -function linuxDesktopPath(): string { - return join(homedir(), ".config", "autostart", `${APP_NAME}.desktop`); -} - -function enableLinux(cliPath: string): boolean { - const desktop = `[Desktop Entry] -Type=Application -Name=OmniRoute -Exec=node "${cliPath}" --tray -X-GNOME-Autostart-enabled=true -NoDisplay=false -`; - const path = linuxDesktopPath(); - mkdirSync(dirname(path), { recursive: true }); - writeFileSync(path, desktop, "utf-8"); - return true; -} - -function disableLinux(): boolean { - const path = linuxDesktopPath(); - if (existsSync(path)) unlinkSync(path); - return true; -} - -function enableWindows(cliPath: string): boolean { - try { - const cmd = `node "${cliPath}" --tray`; - execSync( - `reg add "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run" /v ${APP_NAME} /t REG_SZ /d "${cmd}" /f`, - { windowsHide: true } - ); - return true; - } catch { - return false; - } -} - -function disableWindows(): boolean { - try { - execSync( - `reg delete "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run" /v ${APP_NAME} /f`, - { windowsHide: true } - ); - } catch { - // already absent — treat as success - } - return true; -} - -function windowsRegistryHasKey(): boolean { - try { - const out = execSync( - `reg query "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run" /v ${APP_NAME}`, - { windowsHide: true, stdio: ["ignore", "pipe", "ignore"] } - ).toString(); - return out.includes(APP_NAME); - } catch { - return false; - } -} +export { getAutostartStatus }; diff --git a/bin/omniroute.mjs b/bin/omniroute.mjs index a7a13a02ac..fc17e09672 100755 --- a/bin/omniroute.mjs +++ b/bin/omniroute.mjs @@ -16,6 +16,11 @@ import { fileURLToPath, pathToFileURL } from "node:url"; import { homedir, platform } from "node:os"; import updateNotifier from "update-notifier"; +// Register tsx so dynamic imports of .ts source files (referenced as .js per +// TypeScript conventions) resolve correctly. The build never emits .js for +// src/lib/cli-helper/, so tsx handles the .ts → .js resolution at runtime. +await import("tsx/esm"); + const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); const ROOT = join(__dirname, ".."); diff --git a/docs/architecture/meta.json b/docs/architecture/meta.json new file mode 100644 index 0000000000..11bda7a2b9 --- /dev/null +++ b/docs/architecture/meta.json @@ -0,0 +1,10 @@ +{ + "title": "Architecture", + "pages": [ + "ARCHITECTURE", + "AUTHZ_GUIDE", + "CODEBASE_DOCUMENTATION", + "REPOSITORY_MAP", + "RESILIENCE_GUIDE" + ] +} diff --git a/docs/compression/meta.json b/docs/compression/meta.json new file mode 100644 index 0000000000..bfc16e0493 --- /dev/null +++ b/docs/compression/meta.json @@ -0,0 +1,10 @@ +{ + "title": "Compression", + "pages": [ + "COMPRESSION_GUIDE", + "COMPRESSION_ENGINES", + "RTK_COMPRESSION", + "COMPRESSION_LANGUAGE_PACKS", + "COMPRESSION_RULES_FORMAT" + ] +} diff --git a/docs/frameworks/meta.json b/docs/frameworks/meta.json new file mode 100644 index 0000000000..faad60d4b0 --- /dev/null +++ b/docs/frameworks/meta.json @@ -0,0 +1,15 @@ +{ + "title": "Frameworks", + "pages": [ + "MCP-SERVER", + "A2A-SERVER", + "AGENT_PROTOCOLS_GUIDE", + "CLOUD_AGENT", + "EVALS", + "GAMIFICATION", + "MEMORY", + "OPENCODE", + "SKILLS", + "WEBHOOKS" + ] +} diff --git a/docs/guides/KIRO_SETUP.md b/docs/guides/KIRO_SETUP.md index 9466a89ff8..737ff8c2e7 100644 --- a/docs/guides/KIRO_SETUP.md +++ b/docs/guides/KIRO_SETUP.md @@ -1,3 +1,7 @@ +--- +title: "Kiro Setup Guide" +--- + # Kiro Setup Guide This guide covers adding Kiro (AWS-hosted AI coding assistant) accounts to OmniRoute, diff --git a/docs/guides/meta.json b/docs/guides/meta.json new file mode 100644 index 0000000000..67a106dfe6 --- /dev/null +++ b/docs/guides/meta.json @@ -0,0 +1,16 @@ +{ + "title": "Guides", + "pages": [ + "SETUP_GUIDE", + "USER_GUIDE", + "DOCKER_GUIDE", + "ELECTRON_GUIDE", + "FEATURES", + "I18N", + "KIRO_SETUP", + "PWA_GUIDE", + "TERMUX_GUIDE", + "TROUBLESHOOTING", + "UNINSTALL" + ] +} diff --git a/docs/i18n/ar/CHANGELOG.md b/docs/i18n/ar/CHANGELOG.md index 16998b51cd..b294dfa2f7 100644 --- a/docs/i18n/ar/CHANGELOG.md +++ b/docs/i18n/ar/CHANGELOG.md @@ -6,12 +6,91 @@ ## [Unreleased] +### ✨ New Features + +### 🔧 Bug Fixes + +--- + +## [3.8.3] — 2026-05-24 + +### ✨ New Features + +- **feat(combos):** universal context handoff for cross-model conversation continuity — structured XML summary system (``) that preserves conversation continuity and handles state transfer when combo routing switches models. ([#2653](https://github.com/diegosouzapw/OmniRoute/pull/2653) — thanks @herjarsa) +- **feat(docs):** migrate `/docs` to Fumadocs MDX with nested routes — replaces the custom docs engine with Fumadocs, adding `[...slug]` catch-all routing, search API at `/docs/api/search`, `source.config.ts` content configuration, and `meta.json` navigation files across 8 doc sections (`architecture/`, `compression/`, `frameworks/`, `guides/`, `ops/`, `reference/`, `routing/`, `security/`). Includes 50+ URL redirects for backward compatibility via `next.config.mjs`. ([#2614](https://github.com/diegosouzapw/OmniRoute/pull/2614) — thanks @ovehbe) +- **feat(dashboard):** add search and filters to `/dashboard/api-manager` — filter bar with search by name/key, active-only toggle (persisted to localStorage), status filter (active/disabled/banned/expired), type filter (standard/manage/restricted), filter count badges, and empty state with "Clear Filters" button. ([#2628](https://github.com/diegosouzapw/OmniRoute/pull/2628) / [#2641](https://github.com/diegosouzapw/OmniRoute/pull/2641) — thanks @diegosouzapw) +- **feat(dashboard):** free-tier grouping with symbolic link in `/dashboard/providers` — groups and displays all free-tier providers from all categories dynamically using `hasFree: true` properties without removing them from their native lists. Displays category dot and amber dot with localizable tooltips, dedupes search results by provider ID, and corrects free tier count statistics. ([#2632](https://github.com/diegosouzapw/OmniRoute/pull/2632) — thanks @diegosouzapw) +- **feat(dashboard):** risk notice modal for sensitive providers — show a soft, informative warning modal when connecting to session-based or OAuth providers (like Claude, Cursor, Copilot) for the first time. Adds `subscriptionRisk` properties to 20 providers, localizable templates, and stores acknowledgment in localStorage. ([#2633](https://github.com/diegosouzapw/OmniRoute/pull/2633) / [#2638](https://github.com/diegosouzapw/OmniRoute/pull/2638) — thanks @diegosouzapw) +- **feat(dashboard):** refactor free-tier provider dashboard layout — cleans up visual clutter, reorganizes categories, hides redundant banners, and integrates free-tier categories nicely into the primary provider interface. ([#2640](https://github.com/diegosouzapw/OmniRoute/pull/2640) — thanks @diegosouzapw) +- **feat(dashboard):** mini-playground inline (Phase 4) — integrated interactive mini-playground capabilities to provider details pages, including support for specialized example cards (Embedding, Image, LLM Chat, Music, STT, TTS, Video, Web Fetch, Web Search), unified API key loading hooks, model listing hooks, and curl command builder. ([#2648](https://github.com/diegosouzapw/OmniRoute/pull/2648) — thanks @diegosouzapw) +- **feat(webfetch):** category support with dedicated media providers page and executors for Firecrawl, Jina Reader, and Tavily. ([#2645](https://github.com/diegosouzapw/OmniRoute/pull/2645) — thanks @diegosouzapw) +- **feat(adapta):** integrate Adapta Org (`adapta-web`) provider with automatic Clerk authentication refresh and custom onboarding tutorial modal. ([#2643](https://github.com/diegosouzapw/OmniRoute/pull/2643) — thanks @df4p) +- **feat(i18n):** complete translations for Simplified Chinese — translates 1220 missing keys bringing UI coverage to 98.8% with 0 placeholders. ([#2655](https://github.com/diegosouzapw/OmniRoute/pull/2655) — thanks @L-aros) + +### 🔧 Bug Fixes + +- **fix(settings):** Require Login modal Cancel button text and dismissal — modal now renders localized cancel label via the `common` namespace and closes correctly without modifying settings when cancelled. ([#2649](https://github.com/diegosouzapw/OmniRoute/pull/2649) — thanks @Chewji9875) +- **fix(deepseek-web):** re-apply SSE parser, prompt format, and error handling fixes — handles all 3 DeepSeek SSE stream formats (initial fragments, APPEND operations, bare string tokens), uses non-greedy regex for markdown image stripping, simplifies prompt to single-turn, checks `json.code` before token extraction, and uses `accessToken` fallback for session cache eviction on auth errors. ([#2616](https://github.com/diegosouzapw/OmniRoute/pull/2616) — thanks @ovehbe) +- **fix(deepseek-web):** SSE thinking/search routing and session lifecycle — properly routes thinking vs content fragments based on `thinking_enabled` flag, handles search results with citation indices, appends search result footnotes, refactors `transformSSE()` and `collectSSEContent()` with shared helpers. ([#2624](https://github.com/diegosouzapw/OmniRoute/pull/2624) — thanks @ovehbe) +- **fix(codex):** use allowlist to strip non-Responses-API fields in non-passthrough path — strips residual Chat Completions fields (`stream_options`, `service_tier`, `store`, `metadata`) from the request body when routing through the non-passthrough (translation) code path, preventing GPT-5.5 from receiving invalid parameters. ([#2615](https://github.com/diegosouzapw/OmniRoute/pull/2615) — thanks @diegosouzapw) +- **fix(catalog):** skip static PROVIDER_MODELS when synced models exist — prevents stale/duplicate model entries in `/v1/models` for auto-synced providers. ([#2625](https://github.com/diegosouzapw/OmniRoute/pull/2625) — thanks @herjarsa) +- **fix(qoder):** Cosy auth fallback for PAT tokens + vision support for qwen3-vl-plus — when a PAT token gets 401, falls back to Cosy auth against `api1.qoder.sh`; adds `supportsVision: true` to qwen3-vl-plus. ([#2629](https://github.com/diegosouzapw/OmniRoute/pull/2629) — thanks @herjarsa) +- **fix(cli):** register tsx loader and add opencode config subcommand — registers `tsx/esm` at CLI startup so dynamic `.ts` imports resolve; adds `omniroute config opencode` convenience alias. ([#2631](https://github.com/diegosouzapw/OmniRoute/pull/2631) — thanks @amogus22877769) +- **fix(claude):** improve Pi and OpenCode compatibility — adds Pi Coding Agent anchors to system transform removal, stores `_toolNameMap` as non-enumerable, strips `context_management` when thinking is disabled. ([#2621](https://github.com/diegosouzapw/OmniRoute/pull/2621) — thanks @unitythemaker) +- **fix(passthrough):** restore semantic passthrough system-role-only extraction — reverts full `normalizeClaudeUpstreamMessages()` to lighter `extractSystemRoleMessages()` in CC semantic passthrough paths, preventing document/tool chain corruption. ([#2620](https://github.com/diegosouzapw/OmniRoute/pull/2620) — thanks @Tentoxa) +- **fix(kiro):** stabilize conversationId across prompt compression — captures pre-compression body and uses the original first user message as seed for UUID v5, keeping Kiro's AWS conversation context stable. ([#2630](https://github.com/diegosouzapw/OmniRoute/pull/2630) — thanks @HALDRO) +- **fix(t3-chat-web):** close implementation gaps for t3.chat TanStack Start, tracking of stream_options, and retry configurations — parses TSS Turbo Stream Serialization from `_serverFn/*`, tracks request `combo_strategy` via database migration `062_usage_history_combo_strategy.sql`, and makes batch retry backoffs custom-configurable via environment variables. ([#2634](https://github.com/diegosouzapw/OmniRoute/pull/2634) — thanks @oyi77) +- **fix(reasoning):** extend empty `reasoning_content` injection to prevent tool call loops in Kimi K2 and replay models — injects empty `reasoning_content` field to Kimi models during tool-calling sequences to bypass loop issues. ([#2639](https://github.com/diegosouzapw/OmniRoute/pull/2639) — thanks @herjarsa) +- **fix(cli):** Linux autostart via systemd user service on headless VPS — adds auto-generating systemd user service unit for headless setups on Linux, updating tray configs and system variables allowlist (`LOGNAME` and `XDG_CURRENT_DESKTOP`). ([#2635](https://github.com/diegosouzapw/OmniRoute/pull/2635) — thanks @janeza2) +- **fix(combo):** preserve `` tag in SSE stream output for combos when using `context_cache_protection` to ensure correct context pinning round-trips. ([#2646](https://github.com/diegosouzapw/OmniRoute/pull/2646) — thanks @herjarsa) +- **fix(rtk):** prevent false positives in RTK compression by skipping content-based filter matching for non-shell tool results (e.g. read_file, grep_search). ([#2642](https://github.com/diegosouzapw/OmniRoute/pull/2642) — thanks @HALDRO) +- **fix(translator):** enable Claude extended thinking for Copilot Responses-API requests — handles reasoning budget and translations for Copilot. ([#2647](https://github.com/diegosouzapw/OmniRoute/pull/2647) — thanks @ivan-mezentsev) +- **fix(tests):** remove duplicate assertion in schema coercion & fix(cli): ignore system vars in env check. (thanks @diegosouzapw) + +### 📝 Maintenance + +- **chore(config):** ignore additional agent workflow command files (`.agents/commands/`). (thanks @diegosouzapw) +- **chore(config):** ignore `memory-bank` and Cursor agent rules from tracking. (thanks @ovehbe) + +--- + +## [3.8.2] — 2026-05-22 + +### ✨ New Features + +- **feat(@omniroute/opencode-plugin):** upstream-provider suffix in model display name — appends provider label to enriched names (e.g. `Claude Opus 4.7 · Claude` vs `Claude Opus 4.7 · Kiro`) so the OC TUI model picker can differentiate same-id models routed through different upstream connections. Default-on, opt-out via `features.providerTag: false`. ([#2602](https://github.com/diegosouzapw/OmniRoute/pull/2602) — thanks @mrmm) +- **feat(@omniroute/opencode-plugin):** provider-tag becomes a prefix + traffic-light compression emoji — provider label now prepends (`Claude - Claude Opus 4.7`) for better TUI column grouping, with smart abbreviation for long labels (`GitHub Models` → `GHM`). Compression pipelines render intensity as emoji (🟢🟡🟠🔴). ([#2604](https://github.com/diegosouzapw/OmniRoute/pull/2604) — thanks @mrmm) +- **feat(providers):** add 7 free-tier providers (Wave 1) — Arcee AI, InclusionAI, Krutrim, Liquid AI, MonsterAPI, Nomic, and Poolside now available as new API-key providers with provider icons, model specs, and full routing support. ([#2479](https://github.com/diegosouzapw/OmniRoute/pull/2479) — thanks @oyi77) +- **feat(providers):** add Astraflow provider support with global + China endpoints — new provider with dual-region base URLs for global and mainland China access. ([#2486](https://github.com/diegosouzapw/OmniRoute/pull/2486) — thanks @ucloudnb666) +- **feat(providers):** add `claude-web` provider — cookie-based Claude Web chat access without OAuth. ([#2476](https://github.com/diegosouzapw/OmniRoute/pull/2476) — thanks @oyi77) +- **feat(providers):** add 14 free-tier providers (Wave 1b) — 360AI, Baichuan, Baidu, ByteDance/Doubao, IDEO, Kuaishou/Kling, Kunlun/Skywork, SenseTime/SenseNova, Stepfun, Tencent HunYuan, Zhipu GLM, Replicate, RunPod, and Modal with provider icons, model specs, and routing support. ([#2488](https://github.com/diegosouzapw/OmniRoute/pull/2488) — thanks @oyi77) +- **feat(hermes):** add rich multi-role Hermes Agent CLI support — 7 configurable roles (default, delegation, vision, compression, web_extract, skills_hub, approval), per-role model selection with YAML config generation, dashboard card with preview, and home widget integration. ([#2526](https://github.com/diegosouzapw/OmniRoute/pull/2526) — thanks @apoapostolov) +- **feat(cloud-agents):** cloud agents UX overhaul — tabs (tasks/agents/settings), status filters, Material icons, duration formatting, cloud agent credentials and health API endpoints, memory stats endpoint. ([#2516](https://github.com/diegosouzapw/OmniRoute/pull/2516) — thanks @oyi77) +- **feat(authz):** manage-scope API keys may reach `/api/mcp/*` from non-loopback — Route Guard Tiers system (LOCAL_ONLY / ALWAYS_PROTECTED / MANAGEMENT), narrow carve-out for remote MCP access gated by `manage` scope; `/api/cli-tools/runtime/*` stays strict-loopback. Includes dashboard AuthzSection, inventory API, and comprehensive docs. ([#2473](https://github.com/diegosouzapw/OmniRoute/pull/2473) — thanks @mrmm) +- **feat(home):** home page customization for experienced users — pin Provider Quota to home, toggle Quick Start and Provider Topology visibility via Appearance settings. ([#2531](https://github.com/diegosouzapw/OmniRoute/pull/2531) — thanks @apoapostolov) +- **feat(home):** automatic refresh of Provider Quota — configurable interval (60s–600s) with toggle in Appearance settings; auto-refreshes pinned quota on the home page. ([#2532](https://github.com/diegosouzapw/OmniRoute/pull/2532) — thanks @apoapostolov) +- **feat(@omniroute/opencode-plugin):** OmniRoute OpenCode plugin — live models fetched from OmniRoute API, combo-aware model listing, Gemini request sanitization, multi-instance support, auth flow integration, and 10 test files. ([#2529](https://github.com/diegosouzapw/OmniRoute/pull/2529) — thanks @mrmm) +- **feat(executors):** forward OpenCode client headers to upstream providers — OpenCode-specific headers are now forwarded through the executor pipeline for improved compatibility. ([#2538](https://github.com/diegosouzapw/OmniRoute/pull/2538) — thanks @kang-heewon) +- **feat(fireworks):** add new models with `modelIdPrefix` support — generic registry field that stores short model IDs and prepends the full path prefix before upstream API calls. Adds 6 new Fireworks models, `modelsUrl` for dynamic sync, and Qwen3 reranker. ([#2560](https://github.com/diegosouzapw/OmniRoute/pull/2560) — thanks @HALDRO) +- **feat(@omniroute/opencode-plugin):** readable + filterable + offline-resilient model picker — `usableOnly` filter (only show providers with healthy connections), `diskCache` for offline hydration, `Combo:` prefix labeling, and compression metadata tags in combo display names. ([#2572](https://github.com/diegosouzapw/OmniRoute/pull/2572) — thanks @mrmm) +- **feat(smart-pipeline):** multi-stage pipeline for auto combo routing — rule-based + intent-classifier + domain-specific stages with configurable pipeline router, accuracy benchmarks, and comprehensive tests. ([#2551](https://github.com/diegosouzapw/OmniRoute/pull/2551) — thanks @oyi77) +- **feat(ops):** skip DB health check on startup via `OMNIROUTE_SKIP_DB_HEALTHCHECK=1` — replaces slow `integrity_check` (7+ min on large WAL) with `quick_check`, and adds env var to skip entirely. ([#2554](https://github.com/diegosouzapw/OmniRoute/pull/2554) — thanks @soyelmismo) +- **refactor(dashboard):** Provider Quota grouped layout with vertical rail — restructures the page to a 2-column per-provider layout (left rail with icon/name/status, right content with dynamic per-provider columns), new `providerColumns.ts` / `ProviderGroup.tsx` / `AccountRow.tsx` components, env chip-filter row, bulk-refresh per group, and inline expanded panels. ([#2528](https://github.com/diegosouzapw/OmniRoute/pull/2528) — thanks @Gi99lin) +- **feat(providers):** add 26 free-tier providers missing from registry — Novita, Avian, Chutes, Kluster, Targon, Nineteen, Celery, Ditto, Atoma, and more. ([#2590](https://github.com/diegosouzapw/OmniRoute/pull/2590) — thanks @oyi77) +- **feat(providers):** add api-airforce free provider with 55 models. ([#2587](https://github.com/diegosouzapw/OmniRoute/pull/2587) — thanks @oyi77) +- **feat(dashboard):** configurable sidebar — presets, drag-and-drop ordering, smart-grouping, and new Settings → Sidebar page. ([#2581](https://github.com/diegosouzapw/OmniRoute/pull/2581) — thanks @Gi99lin) + ### 🔧 Bug Fixes - **fix(validation):** stop appending a second `/models` when the Gemini base URL already ends in `/models` — Google AI Studio connections using the default base URL were validating against `.../v1beta/models/models` and failing with `404` for every connection. ([#2545](https://github.com/diegosouzapw/OmniRoute/issues/2545)) - **fix(cloudflare-ai):** flatten OpenAI content-part arrays to plain strings for the Workers AI (`cf/`) executor — Workers AI's `/ai/v1/chat/completions` rejects `content: [{type:"text",...}]` with HTTP 400, so requests with array content now have their text parts joined into a string. ([#2539](https://github.com/diegosouzapw/OmniRoute/issues/2539)) - **fix(i18n):** replace leftover Portuguese strings in the English source with English on the Quota dashboards — the quota-share Beta notice (`betaConfigSaved*`) and the Provider Quota row's `Edit cutoffs` / `Refresh now` fallbacks were showing Portuguese. ([#2540](https://github.com/diegosouzapw/OmniRoute/issues/2540)) +- **fix(proxy):** honor the legacy per-provider/global proxy config in `resolveProxyForProvider` — the Claude OAuth token exchange and token refresh only consulted the new proxy registry, so a proxy configured the legacy way (`/api/settings/proxy?level=provider`) was ignored and the exchange went out directly from the host, tripping Anthropic's IP `rate_limit_error` on VPS deployments. It now falls back to the legacy config, mirroring `resolveProxyForConnection`. ([#2456](https://github.com/diegosouzapw/OmniRoute/issues/2456)) +- **fix(antigravity):** auto-discover a missing Cloud Code `projectId` via `loadCodeAssist` before failing — a freshly re-added Antigravity account whose stored `projectId` was empty (OAuth-time discovery returned nothing) now recovers the project on the first request instead of returning `422 Missing Google projectId`, mirroring the `gemini-cli` bootstrap. ([#2334](https://github.com/diegosouzapw/OmniRoute/issues/2334), [#2541](https://github.com/diegosouzapw/OmniRoute/issues/2541)) +- **fix(stream):** keep the `/v1/responses` SSE connection warm for strict clients — emit an early keepalive while the upstream produces its first token and lower the heartbeat cadence to 4s, so Codex CLI's `reqwest` client (≈5s idle-read timeout) no longer drops the stream "before completion" on slow/reasoning models. `curl` was unaffected because it has no idle timeout. ([#2544](https://github.com/diegosouzapw/OmniRoute/issues/2544)) +- **fix(electron):** wait longer for the server on first launch and reload once it responds — long post-upgrade DB migrations could exceed the 30s readiness probe, leaving the desktop app stuck on the "Server starting" screen even though the backend was healthy. The probe now targets the auth-exempt health endpoint with a generous timeout and reloads the window once the server comes up. ([#2460](https://github.com/diegosouzapw/OmniRoute/issues/2460)) + - **fix(cli):** mark `bin/omniroute.mjs` as executable (mode 755) so the globally-installed CLI runs directly without a manual `chmod +x`. ([#2469](https://github.com/diegosouzapw/OmniRoute/issues/2469) — thanks @disonjer) - **fix(settings):** restore the Global System Prompt into the in-memory config on server startup and after JSON/SQLite import — it was only loaded by the PUT endpoint, so the toggle/prompt silently reverted to defaults after any restart or import. ([#2470](https://github.com/diegosouzapw/OmniRoute/issues/2470) — thanks @disonjer) - **fix(settings):** append the Global System Prompt **after** existing system content instead of prepending it, so provider/agent instructions (Kiro, OpenCode, Hermes, …) injected into the system message no longer override the user's global prompt via recency bias. ([#2468](https://github.com/diegosouzapw/OmniRoute/issues/2468) — thanks @disonjer) @@ -19,30 +98,10 @@ - **fix(antigravity):** resolve the Cloud Code `projectId` from `providerSpecificData` as a fallback (and preserve it across token refresh) so the Gemini `/v1beta` streaming path stops returning a spurious `422 Missing Google projectId` for connections that store the project there. ([#2480](https://github.com/diegosouzapw/OmniRoute/issues/2480)) - **fix(api):** `GET /v1beta/models` now lists only models whose provider has an active/validated connection, matching the OpenAI-format `/v1/models` behavior, instead of returning the entire catalog. ([#2483](https://github.com/diegosouzapw/OmniRoute/issues/2483)) -- **fix(translator):** inject `omniroute_web_search` in the Responses-API flat tool shape (`{ type, name }`) when the target provider speaks the Responses API — previously it was always emitted in the Chat Completions nested shape (`{ type, function: { name } }`), and on the Responses→Responses passthrough path nothing flattened it, so Codex/relay upstreams rejected the request with `[400]: Missing required parameter: 'tools[0].name'.` ([#2390](https://github.com/diegosouzapw/OmniRoute/issues/2390)) -- **fix(kiro):** serialize non-string `role:"tool"` message content before sending to CodeWhisperer — structured/array tool output was collapsing to `content:[{ text: "" }]`, which Kiro rejects with `400 Improperly formed request`. Now reuses the shared `serializeToolResultContent`. ([#2446](https://github.com/diegosouzapw/OmniRoute/issues/2446)) -- **fix(claude):** gate heavy-agent beta headers (`context-1m-2025-08-07`, `effort-2025-11-24`, `advanced-tool-use-2025-11-20`) on Opus/Sonnet only and stop deleting Haiku's `thinking` config — Haiku with OAuth was rejecting `context-1m` with `[400]: This authentication style is incompatible with the long context beta header`. Also sanitizes historical `thinking` block signatures in Claude OAuth passthrough, fixing `[400]: Invalid signature in thinking block` on mid-session model switches. ([#2454](https://github.com/diegosouzapw/OmniRoute/issues/2454) — thanks @havockdev) -- **fix(perplexity-web):** route requests through a Firefox-148 TLS-impersonating client so Perplexity's Cloudflare edge stops rejecting VPS/datacenter IPs with a 403 challenge — mirrors the existing `chatgpt-web` approach. Validation/execution now distinguish a Cloudflare block from an invalid session cookie. New env vars `OMNIROUTE_PPLX_TLS_TIMEOUT_MS` / `OMNIROUTE_PPLX_TLS_GRACE_MS`. ([#2459](https://github.com/diegosouzapw/OmniRoute/issues/2459) — thanks @havockdev) -- **fix(validation):** guard `apiKey`/`modelsUrl` against non-string values before calling `.startsWith()` / `.trim()` in the provider connection-test path — a corrupted or mis-typed credential could throw `TypeError: ... is not a function` mid-validation instead of returning a clean result. ([#2463](https://github.com/diegosouzapw/OmniRoute/issues/2463)) - -## [3.8.2] — 2026-05-21 - -### ✨ New Features - -- **feat(providers):** add 7 free-tier providers (Wave 1) — Arcee AI, InclusionAI, Krutrim, Liquid AI, MonsterAPI, Nomic, and Poolside now available as new API-key providers with provider icons, model specs, and full routing support. ([#2479](https://github.com/diegosouzapw/OmniRoute/pull/2479) — thanks @oyi77) -- **feat(providers):** add Astraflow provider support with global + China endpoints — new provider with dual-region base URLs for global and mainland China access. ([#2486](https://github.com/diegosouzapw/OmniRoute/pull/2486) — thanks @ucloudnb666) -- **feat(providers):** add `claude-web` provider — cookie-based Claude Web chat access without OAuth. ([#2476](https://github.com/diegosouzapw/OmniRoute/pull/2476) — thanks @oyi77) -- **feat(providers):** add 14 free-tier providers (Wave 1b) — 360AI, Baichuan, Baidu, ByteDance/Doubao, IDEO, Kuaishou/Kling, Kunlun/Skywork, SenseTime/SenseNova, Stepfun, Tencent HunYuan, Zhipu GLM, Replicate, RunPod, and Modal with provider icons, model specs, and routing support. ([#2488](https://github.com/diegosouzapw/OmniRoute/pull/2488) — thanks @oyi77) -- **feat(hermes):** add rich multi-role Hermes Agent CLI support — 7 configurable roles (default, delegation, vision, compression, web_extract, skills_hub, approval), per-role model selection with YAML config generation, dashboard card with preview, and home widget integration. ([#2526](https://github.com/diegosouzapw/OmniRoute/pull/2526) — thanks @apoapostolov) -- **feat(cloud-agents):** cloud agents UX overhaul — tabs (tasks/agents/settings), status filters, Material icons, duration formatting, cloud agent credentials and health API endpoints, memory stats endpoint. ([#2516](https://github.com/diegosouzapw/OmniRoute/pull/2516) — thanks @oyi77) -- **feat(authz):** manage-scope API keys may reach `/api/mcp/*` from non-loopback — Route Guard Tiers system (LOCAL_ONLY / ALWAYS_PROTECTED / MANAGEMENT), narrow carve-out for remote MCP access gated by `manage` scope; `/api/cli-tools/runtime/*` stays strict-loopback. Includes dashboard AuthzSection, inventory API, and comprehensive docs. ([#2473](https://github.com/diegosouzapw/OmniRoute/pull/2473) — thanks @mrmm) - -### 🔧 Bug Fixes - -- **fix(cli):** persist `STORAGE_ENCRYPTION_KEY` into `DATA_DIR` (not only `~/.omniroute`) and refuse to auto-generate a fresh key when a `storage.sqlite` already exists — silently regenerating it locked users out of their encrypted database. Mirrors the server `bootstrapEnv` guard. (reported by Daniel Nach; original key persistence by @Chewji9875 — follow-up to [#1622](https://github.com/diegosouzapw/OmniRoute/issues/1622)) -- **fix(gemini):** preserve and re-attach the `thoughtSignature` on Gemini thinking-model tool calls so the cached signature is found on the follow-up turn — fixes `[400]: Function call is missing a thought_signature`. ([#2504](https://github.com/diegosouzapw/OmniRoute/issues/2504)) -- **fix(translator):** accept PDFs sent as `input_file` on the Gemini path and as `document` on the Responses/Codex path — content parts normalized across `input_file`/`file`/`document`. ([#2515](https://github.com/diegosouzapw/OmniRoute/issues/2515)) -- **fix(stream):** count `thinking` arrays and `reasoning_details` as useful stream output — a reasoning-only response was misclassified as "Stream ended before producing useful content" (spurious 502). ([#2520](https://github.com/diegosouzapw/OmniRoute/issues/2520)) +- **fix(cli):** persist `STORAGE_ENCRYPTION_KEY` into `DATA_DIR` (not only `~/.omniroute`) and refuse to auto-generate a fresh key when a `storage.sqlite` already exists — a new key cannot decrypt previously-encrypted credentials, so silently regenerating it locked users out of their database. The CLI now mirrors the server `bootstrapEnv` guard. (reported by Daniel Nach; original key persistence by @Chewji9875 — follow-up to [#1622](https://github.com/diegosouzapw/OmniRoute/issues/1622)) +- **fix(gemini):** preserve and re-attach the `thoughtSignature` on Gemini thinking-model tool calls — thread the signature namespace through the `FORMATS.GEMINI` and `FORMATS.GEMINI_CLI` request translators so the cached signature (keyed by connection + tool-call id) is found on the follow-up turn. Fixes `[400]: Function call is missing a thought_signature in functionCall parts` on agentic Gemini tool use. ([#2504](https://github.com/diegosouzapw/OmniRoute/issues/2504)) +- **fix(translator):** accept PDFs sent in the Responses-API `input_file` shape on the Gemini path, and the Gemini-style `document` shape on the Responses/Codex path — content parts are now normalized across `input_file` / `file` / `document` so a PDF reaches the model regardless of which field name the client used. ([#2515](https://github.com/diegosouzapw/OmniRoute/issues/2515)) +- **fix(stream):** count `thinking` arrays and `reasoning_details` as useful stream output — a reasoning-only response (e.g. Mistral/StepFun with a low `max_tokens`) was misclassified as "Stream ended before producing useful content" and turned into a spurious 502; it is now recognized as valid output. ([#2520](https://github.com/diegosouzapw/OmniRoute/issues/2520)) - **fix(claude):** extract system/developer role messages in Claude Code semantic passthrough paths — moves `role:"system"` / `role:"developer"` messages from the `messages[]` array to the top-level `system` parameter before sending to Anthropic, which rejects them inside messages. Fixes memory injection context being silently dropped. ([#2497](https://github.com/diegosouzapw/OmniRoute/pull/2497) — thanks @unitythemaker) - **fix(vision-bridge):** auto-route non-standard provider models through OmniRoute self-loop — vision-bridge now detects when a model doesn't natively support vision and automatically re-routes the image through OmniRoute's own endpoint for format translation. ([#2487](https://github.com/diegosouzapw/OmniRoute/pull/2487) — thanks @herjarsa) - **fix(mitm):** add IPv6 DNS redirect, modular antigravity target, improved logging — MITM DNS handler now correctly redirects IPv6 (AAAA) queries alongside IPv4, adds a dedicated `antigravity.ts` target module, and enhances DNS/TLS logging for debugging. ([#2514](https://github.com/diegosouzapw/OmniRoute/pull/2514) — thanks @herjarsa) @@ -70,69 +129,520 @@ - **fix(providers):** resolve `opencode/` → `opencode-zen` slug mismatch + add 40+ new models — `opencode` is now a proper alias for `opencode-zen` in executor, model resolver, and provider registry; adds GPT 5.x, Claude 4.x, Gemini 3.x, Grok, Kimi, and other models with tests. ([#2517](https://github.com/diegosouzapw/OmniRoute/pull/2517) — thanks @herjarsa) - **fix(antigravity):** fail over stalled Antigravity sessions — new `ANTIGRAVITY_PRE_RESPONSE_TIMEOUT_CODE` shared constant for pre-response timeout detection, automatic failover to next account when session stalls before headers arrive. Node.js engine range relaxed to `>=20.20.2`. ([#2464](https://github.com/diegosouzapw/OmniRoute/pull/2464) — thanks @dhaern) - **fix(deepseek-web):** fix SSE parser, prompt format, and error handling — handles all 3 DeepSeek SSE stream formats (initial fragments, APPEND operations, bare string tokens), simplifies prompt to single-turn to prevent chat marker leakage, and checks `json.code` before token extraction. ([#2502](https://github.com/diegosouzapw/OmniRoute/pull/2502) — thanks @ovehbe) +- **fix(codex):** accept `auth.json` without `auth_mode` field on import — Codex CLI no longer writes `auth_mode`; import now accepts both formats as long as required tokens are present. Semantic cache read now requires explicit `temperature: 0`. ([#2536](https://github.com/diegosouzapw/OmniRoute/pull/2536) — thanks @janeza2) +- **fix(freetheai):** add `/chat/completions` to baseUrl to resolve 404 errors. ([#2557](https://github.com/diegosouzapw/OmniRoute/pull/2557) — thanks @lordavadon2) +- **fix(qoder):** route PAT tokens to Qoder native API instead of DashScope — detects `pt-` prefixed tokens and routes to `api.qoder.com` with proper User-Agent header. ([#2559](https://github.com/diegosouzapw/OmniRoute/pull/2559) — thanks @herjarsa) +- **fix(perf):** cache compiled RegExp in RTK compression hot path — eliminates thousands of redundant `new RegExp()` instantiations per second. ([#2553](https://github.com/diegosouzapw/OmniRoute/pull/2553) — thanks @soyelmismo) +- **fix(reasoning-cache):** auto-start periodic cleanup on module load — the `server-init.ts` job was never imported (dead code), causing the `reasoning_cache` table to grow indefinitely. Now runs 30-min cleanup cycles automatically. ([#2552](https://github.com/diegosouzapw/OmniRoute/pull/2552) — thanks @soyelmismo) +- **fix(claude):** omit `context-1m` beta for Sonnet — restrict to Opus-only to avoid long-context credit gate errors. Add `afk-mode-2026-01-31`, replace `redact-thinking` with `thinking-token-count-2026-05-13`. ([#2568](https://github.com/diegosouzapw/OmniRoute/pull/2568) — thanks @unitythemaker) +- **fix(codex):** relax `auth_mode` check in frontend import preview — accept `undefined`/`null`/`"chatgpt"` instead of requiring `"chatgpt"` strictly, matching the backend fix in #2536. ([#2567](https://github.com/diegosouzapw/OmniRoute/pull/2567) — thanks @janeza2) +- **fix(kimi):** declare vision capability for Kimi K2.6 in all 4 layers — `providerRegistry`, `modelSpecs`, `catalog.ts` keyword list, and Playground `VISION_MODELS`; previously the model silently rejected image uploads. ([#2573](https://github.com/diegosouzapw/OmniRoute/pull/2573) — thanks @herjarsa) +- **fix(dashboard):** paginate request-log viewer beyond 300 rows — `getCallLogs` now accepts `offset` with parameterized SQL (eliminates string-interpolated `LIMIT`); `RequestLoggerV2` grows its window via "Load more" + IntersectionObserver infinite scroll, resetting on filter change. ([#2576](https://github.com/diegosouzapw/OmniRoute/pull/2576)) +- **fix(cli):** use `/api/monitoring/health` for server readiness check — `waitForServer()` was polling the auth-protected `/api/health` (401), causing `omniroute serve` to hang indefinitely. ([#2578](https://github.com/diegosouzapw/OmniRoute/pull/2578) — thanks @amogus22877769) +- **fix(combo):** detect invalid model errors via structured error codes + regex fallback — when a combo target rejects a model (e.g. free account vs Pro), the router now recognizes `model_not_found` / `deployment_not_found` codes and 6 regex patterns, and falls through to the next target instead of stopping the loop. ([#2534](https://github.com/diegosouzapw/OmniRoute/pull/2534) — thanks @HALDRO) +- **fix(security):** post-review hardening batch — `spawnSync` arg-array replaces `execSync` string-template (command injection), CSP `unsafe-eval` gated on `!app.isPackaged`, `requireManagementAuth` guard on budget/bulk and resilience/reset endpoints, error messages sanitized in gemini-web/claude-web/copilot-web/oauth/agents catch blocks, circuit breaker persists `lastFailureKind`, and combo resets `exhaustedProviders` per set-retry iteration. ([#2435](https://github.com/diegosouzapw/OmniRoute/pull/2435)) +- **fix(@omniroute/opencode-plugin):** honor `geminiSanitization` and `fetchInterceptor` feature flags — both were applied unconditionally; now each fetch layer is gated by its flag (default ON), and disabling both falls back to plain SDK fetch. ([#2546](https://github.com/diegosouzapw/OmniRoute/pull/2546)) +- **fix(#2575):** check DB feature flag override in `arePrivateProviderUrlsAllowed()` — supports runtime toggle without restart. ([#2595](https://github.com/diegosouzapw/OmniRoute/pull/2595) — thanks @herjarsa) +- **fix(mimo):** add `supportsVision` flag to MiMo-V2.5, V2.5-Pro, and V2-Omni — previously image uploads were silently rejected. ([#2592](https://github.com/diegosouzapw/OmniRoute/pull/2592) — thanks @herjarsa) +- **fix(ops):** propagate `OMNIROUTE_SKIP_DB_HEALTHCHECK` env var to periodic DB health check scheduler — companion fix to #2554. ([#2591](https://github.com/diegosouzapw/OmniRoute/pull/2591) — thanks @soyelmismo) +- **fix(github):** remove incorrect `openai-responses` targetFormat from GitHub Copilot's Haiku/Sonnet models. ([#2583](https://github.com/diegosouzapw/OmniRoute/pull/2583) — thanks @oyi77) +- **fix(copilot):** stabilize responses configuration — removes 865 lines of unstable config, simplifies handler. ([#2579](https://github.com/diegosouzapw/OmniRoute/pull/2579) — thanks @ivan-mezentsev) +- **fix(#2544):** add SSE heartbeat keepalive to Responses API transform stream — prevents Codex CLI 0.130.0 from disconnecting during long thinking/reasoning phases. ([#2599](https://github.com/diegosouzapw/OmniRoute/pull/2599) — thanks @herjarsa) +- **fix(memory):** extract system role messages in semantic passthrough path to prevent 400 on memory injection — system messages were being passed as-is to providers that reject mixed roles. ([#2474](https://github.com/diegosouzapw/OmniRoute/pull/2474) — thanks @Tentoxa) +- **fix(@omniroute/opencode-provider):** include `limit.context` in model entries for OpenCode context window detection — previously OpenCode couldn't determine model context size. ([#2482](https://github.com/diegosouzapw/OmniRoute/pull/2482) — thanks @herjarsa) +- **fix(mimo):** add `supportsVision` flag to Kimi K2.6 in providerRegistry + comprehensive vision tests for MiMo V2.5/V2.5-Pro/V2-Omni. ([#2600](https://github.com/diegosouzapw/OmniRoute/pull/2600) — thanks @herjarsa) +- **fix(proxy):** prefer scoped proxies over registry global fallback — legacy provider-specific proxy was being shadowed by a registry-global fallback across both storage backends. Resolution now follows strict specificity: account → provider → combo → global. ([#2606](https://github.com/diegosouzapw/OmniRoute/pull/2606) — thanks @terence71-glitch) +- **fix(@omniroute/opencode-plugin):** canonical-twin dedup + alias-fallback enrichment — `/v1/models` returned the same model under both alias (`cc/claude-opus-4-7`) and canonical (`claude/claude-opus-4-7`) names; now drops ~75 canonical duplicates and rescues ~88 raw-id rows with proper provider prefix via alias-index fallback. Also emits `cost`, `release_date`, `modalities` fields in static catalog and raises provider label threshold to 12 chars (preserves `AssemblyAI`, `Antigravity` verbatim). ([#2607](https://github.com/diegosouzapw/OmniRoute/pull/2607) — thanks @mrmm) +- **fix(registry):** populate empty models arrays for HuggingFace (6 models) and HackClub (3 models) + fix Snowflake placeholder baseUrl to `{account}` template pattern. ([#2611](https://github.com/diegosouzapw/OmniRoute/pull/2611) — thanks @oyi77) ### 🌐 Internationalization - **i18n(zh-CN):** translate 830 missing UI strings — replaces all `__MISSING__:` placeholders with proper Chinese translations. ([#2523](https://github.com/diegosouzapw/OmniRoute/pull/2523) — thanks @InkshadeWoods) - **i18n(dashboard):** add missing dashboard keys and fix EN fallbacks — hundreds of hardcoded English strings across cache, caveman, costs, skills, memory, and evals pages replaced with `t()` calls. ([#2500](https://github.com/diegosouzapw/OmniRoute/pull/2500) — thanks @Gi99lin) +- **i18n(pt-BR):** complete and fix Brazilian Portuguese translation — comprehensive overhaul of pt-BR locale with ~3000 lines of quality translations, filling all missing keys and correcting existing entries. ([#2543](https://github.com/diegosouzapw/OmniRoute/pull/2543) — thanks @alltomatos) +- **i18n(ru):** comprehensive Russian translation update — ~2000 lines of corrected and filled translations. ([#2550](https://github.com/diegosouzapw/OmniRoute/pull/2550) — thanks @AgentAlexAI) +- **i18n(all):** comprehensive localization and UI refactoring — 42 locale files synchronized with missing keys, cloud-agents page i18n rewrite, and consistent `t()` usage across 21 dashboard components. ([#2580](https://github.com/diegosouzapw/OmniRoute/pull/2580) — thanks @alltomatos) +- **i18n(all):** translate freeTier provider strings across 41 locales — replaces `__MISSING__:Free Tier Providers` placeholders with proper translations in both `common` and `providers` namespaces. ([#2609](https://github.com/diegosouzapw/OmniRoute/pull/2609) — thanks @leninejunior) +- **i18n(pt-BR):** eliminate all 1270 remaining `__MISSING__` markers — completes pt-BR translation across 41 namespaces to true 100% coverage. ([#2610](https://github.com/diegosouzapw/OmniRoute/pull/2610) — thanks @leninejunior) ### 📝 Maintenance - **chore:** remove Akamai VPS deploy from release workflow and skills. +- **chore(deps):** bump `actions/setup-node` from v4 to v6 + `randomBytes` security fix for cloud agent task IDs. ([#2589](https://github.com/diegosouzapw/OmniRoute/pull/2589)) +- **chore(deps):** bump `actions/upload-artifact` from v4 to v7. ([#2588](https://github.com/diegosouzapw/OmniRoute/pull/2588)) - **chore:** ignore `.claude/worktrees` from git tracking. +- **chore(ci):** auto-lock release branch on version publish — new CI workflow applies `lock_branch` protection when a GitHub Release is published. ([#2542](https://github.com/diegosouzapw/OmniRoute/pull/2542)) +- **docs:** redesign README — marketing-first layout with accurate provider counts. ([#2490](https://github.com/diegosouzapw/OmniRoute/pull/2490)) --- -## [3.8.1] — 2026-05-20 +## [3.8.1] — 2026-05-21 + +### ✨ New Features + +- **feat(settings):** Feature Flags Settings Page (Card Grid + DB overrides) — fully implements the feature flags UI dashboard using Variant A (Card Grid) with Glassmorphism, complete with global `GET/PUT/DELETE` API routes, Zod validation, debounced search, category filters, and full 30+ locale i18n support. Resolves priority hierarchy to DB > ENV > Defaults. ([#2457](https://github.com/diegosouzapw/OmniRoute/pull/2457)) +- **feat(db):** multi-driver SQLite abstraction layer — new `SqliteAdapter` interface with 3 concrete adapters (`betterSqliteAdapter`, `nodeSqliteAdapter`, `sqljsAdapter`) and a `driverFactory` that cascades `better-sqlite3` → `node:sqlite` → `sql.js (WASM)`. Enables OmniRoute to run on any JavaScript runtime (Node.js, Bun, Deno, Cloudflare Workers) without native binary dependencies. `better-sqlite3` moved to `optionalDependencies`. ([#2447](https://github.com/diegosouzapw/OmniRoute/pull/2447)) +- **feat(settings):** Claude Fast Mode toggle in Settings › AI — opt-in toggle that forwards `X-CPA-Force-Fast-Mode` header so a paired CLIProxyAPI build can reach Anthropic Fast Mode (`speed:"fast"`). Model-gated to Opus models matching Anthropic's binary KT() check. ([#2449](https://github.com/diegosouzapw/OmniRoute/pull/2449) — thanks @NomenAK) +- **feat(settings):** Codex Fast Tier — tier dropdown (`default`/`priority`/`flex`) + per-model gate preventing 400 errors from OpenAI when the tier toggle was on for non-Fast-eligible models. ([#2451](https://github.com/diegosouzapw/OmniRoute/pull/2451) — thanks @NomenAK) +- **feat:** align Antigravity 2.0.1 support — updated client profile, upstream headers, and model aliases. ([#2443](https://github.com/diegosouzapw/OmniRoute/pull/2443) — thanks @dhaern) +- **feat:** enhance `extractBearer` to support `x-api-key` for Anthropic API style auth. ([#2436](https://github.com/diegosouzapw/OmniRoute/pull/2436) — thanks @thedtvn) +- **feat(memory):** wire `createMemory` to `upsertSemanticMemoryPoint` (Qdrant). ([#2439](https://github.com/diegosouzapw/OmniRoute/pull/2439) — thanks @NomenAK) ### 🔧 Bug Fixes & Refactors +- **fix(deepseek-web):** rewrite auth to userToken Bearer + WASM PoW solver. ([#2452](https://github.com/diegosouzapw/OmniRoute/pull/2452) — thanks @ovehbe) +- **chore:** update node dependencies and runtime support. ([#2453](https://github.com/diegosouzapw/OmniRoute/pull/2453) — thanks @backryun) +- **fix(translator):** fix 3 Kiro `tool_result` defects causing 400 on follow-up turns — missing `tool_use_id` mapping, orphan result blocks, and conversation ID collision on assistant-first turns. ([#2447](https://github.com/diegosouzapw/OmniRoute/pull/2447)) - **fix(translator):** treat `developer` role as system in OpenAI → Claude translation — `openAIToClaude` now extracts `developer`-role messages into `systemParts` (same as `system`) and filters them from the non-system message list, preventing identity context injected via the Responses API `developer` role from silently becoming an assistant turn when routing to a Claude-format provider. ([#2407](https://github.com/diegosouzapw/OmniRoute/issues/2407)) - **fix(antigravity):** deduplicate `removeHeaderCaseInsensitive` — export canonical implementation from `antigravityClientProfile.ts` and remove the local copy in `antigravity.ts`; export `AntigravityCredentialsLike` type for cross-module use. (#2433 — thanks @Gi99lin) +- **refactor(docs):** enhance frontmatter handling in DocPage — gray-matter Date object parsing bug fix. ([#2448](https://github.com/diegosouzapw/OmniRoute/pull/2448) — thanks @ovehbe) +- **fix(jules):** Jules API parity and cloud-agent provider registration. ([#2438](https://github.com/diegosouzapw/OmniRoute/pull/2438)) +- **fix(i18n):** harden diff key extraction tag sanitization in `extract-keys-from-diff.mjs`. +- **chore(i18n):** refresh fr/es/de locales + add missing `settings.update` key. ([#2437](https://github.com/diegosouzapw/OmniRoute/pull/2437)) +- **fix(dashboard):** allow bracketed combo names — align dashboard combo-name validator regex with the shared/server schema updated in PR #2354; names like `Claude [1m]` are now accepted in the create/edit form. ([#2458](https://github.com/diegosouzapw/OmniRoute/pull/2458) — thanks @congvc-dev) +- **docs(agentrouter):** recommend native provider as the simple path — guide now prefers the built-in AgentRouter provider instead of manual OpenAI-compatible configuration. ([#2429](https://github.com/diegosouzapw/OmniRoute/pull/2429) — thanks @leninejunior) +- **feat(settings):** surface Codex Fast Tier toggle in Settings › AI — companion UI toggle for the Codex Fast Tier feature. ([#2440](https://github.com/diegosouzapw/OmniRoute/pull/2440) — thanks @NomenAK) ### 🔒 Security Fixes -- **fix(security):** replace execSync string-template with spawnSync arg-array in plugin.mjs — eliminates shell command injection. -- **fix(security):** gate Electron CSP unsafe-eval on !app.isPackaged — was leaking unsafe-eval into production builds. -- **fix(api):** add requireManagementAuth to /api/usage/budget/bulk and /api/resilience/reset. -- **fix(security):** route catch-block error messages through sanitizeErrorMessage() in executors and API routes. -- **fix(codex):** refreshCredentials returns null on token refresh failure. -- **fix(tokenRefresh):** safe unknown-error access in catch block. -- **fix(combo):** reset exhaustedProviders set at start of each set-retry iteration. -- **fix(circuitBreaker):** persist and restore lastFailureKind via options JSON column. +- **fix(security):** replace `execSync` string-template with `spawnSync` arg-array in `plugin.mjs` — eliminates shell command injection via malicious plugin names. +- **fix(security):** gate Electron CSP `unsafe-eval` on `!app.isPackaged` instead of URL substring match — was leaking `unsafe-eval` into production builds; merged duplicate `connect-src` directives. +- **fix(api):** add `requireManagementAuth` to `/api/usage/budget/bulk` and `/api/resilience/reset` — both endpoints exposed spend data and circuit-breaker controls without auth. +- **fix(security):** route catch-block error messages through `sanitizeErrorMessage()` in `gemini-web`, `claude-web`, `copilot-web` executors, `oauth` route, and cloud-agent task routes — prevents stack traces and internal paths leaking into HTTP responses. +- **fix(codex):** `refreshCredentials` returns `null` (not error-object) on token refresh failure — prevents base executor from spreading `{error}` onto active credentials. +- **fix(tokenRefresh):** safe `unknown`-error access in `catch` block (`error instanceof Error ? error.message : String(error)`). +- **fix(combo):** reset `exhaustedProviders` set at start of each set-retry iteration — providers excluded in a failing pass now get a second chance on retry. +- **fix(circuitBreaker):** persist and restore `lastFailureKind` via the `options` JSON column — kind-based cooldown overrides (`cooldownByKind`) now survive server restarts. --- ## [3.8.0] — 2026-05-06 -### ✨ New Features +### 🚀 Post-release hotfixes e contribuições (2026-05-06 → 2026-05-20) +#### 2026-05-20 + +- **feat(batch):** implement 10 feature requests harvested from issues — T3 Chat Web executor (cookie-based), per-request exhausted-provider tracking (#1731) to skip quota-drained providers mid-combo, Zed Docker detection, API key rotator health dashboard, Kiro multi-account isolation, context-window model filtering, cost blending in combos, combo config tests, provider validation branches, and postinstall support scripts. ([#2414](https://github.com/diegosouzapw/OmniRoute/pull/2414)) +- **feat(combos):** add `falloverBeforeRetry` strategy — combo routing now falls over to the next target before retrying the same model, eliminating the tail-latency spike from exhausting all per-model retries on a failing endpoint. Also wraps the retry loop in a `setTry` outer loop for per-target retry coordination. ([#2417](https://github.com/diegosouzapw/OmniRoute/pull/2417) — thanks @hartmark) +- **fix(gamification):** resolve 6 implementation gaps — missing `SELECT` in `checkActionCountBadges` SQL (was silently skipping 8 badges), federation leaderboard auth enforcement, pagination `offset` parameter no longer silently discarded, admin anomaly view now computes real z-scores, `addXp` correctly calculates initial level from XP amount, and barrel `index.ts` for clean module exports. 72-test suite covering all fixes. ([#2421](https://github.com/diegosouzapw/OmniRoute/pull/2421) — thanks @oyi77) +- **docs:** add AgentRouter provider setup guide — step-by-step instructions for connecting OmniRoute to AgentRouter.org's Claude-compatible relay endpoint, covering API key configuration and wire-image headers. ([#2422](https://github.com/diegosouzapw/OmniRoute/pull/2422) — thanks @leninejunior) +- **fix(claude):** drop orphan `tool_result` blocks left behind when `fixToolAdjacency` strips a dangling `tool_use` — resolves HTTP 400 "unexpected tool_use_id in tool_result blocks" from the Anthropic API on truncated histories. `fixToolPairs` now re-runs after every `fixToolAdjacency` pass across all three call sites (`contextManager.ts`, `base.ts`, `claudeCodeCompatible.ts`). (discussion [#2410](https://github.com/diegosouzapw/OmniRoute/discussions/2410)) +- **fix(playground):** guard against `null`/non-string model IDs in Playground dropdowns — `typeof m?.id !== "string"` check prevents a silent crash in the provider discovery loop and `filteredModels` computation that was leaving all Playground dropdowns empty when `/v1/models` returned entries with `id: null`; adds deduplication via `Set` to eliminate duplicate React key warnings. +- **fix(mitm):** point MITM runtime manager re-export to the compiled `.js` entrypoint — fixes module resolution after build when the `.ts` source is no longer present. +- **fix(storage):** persist `STORAGE_ENCRYPTION_KEY` across upgrades (closes #1622) — ensures that SQLite encryption keys are preserved during version upgrades. ([#2428](https://github.com/diegosouzapw/OmniRoute/pull/2428) — thanks @Chewji9875) +- **fix(auth):** auto-reset credential `apiKeyHealth` status on successful connection test. ([#2427](https://github.com/diegosouzapw/OmniRoute/pull/2427) — thanks @clousky2020) +- **fix(mitm):** drop `.js` extension on `manager.runtime` re-export to fix webpack packaging issue. ([#2425](https://github.com/diegosouzapw/OmniRoute/pull/2425) — thanks @NomenAK) +- **fix(image):** support Antigravity image generation and add Gemini 3.5 Flash support. ([#2423](https://github.com/diegosouzapw/OmniRoute/pull/2423) — thanks @backryun) + +#### 2026-05-19 + +- **chore(i18n):** comprehensive dashboard i18n coverage — 6 rounds of parallel refactoring replacing hardcoded English/Portuguese text with `t()` calls across 57+ dashboard pages; 420+ new keys added to `en.json` covering `settings`, `playground`, `analytics`, `apiManager`, `providers`, `skills`, `memory`, `agents`, and 15 other namespaces (coverage: ~88%, up from ~20%). +- **fix(offline):** avoid SSR/CSR hydration mismatch on the offline status page — switches from a `useState` lazy initializer (which accessed `navigator.onLine` on the server) to `useSyncExternalStore` with a distinct `false` server snapshot, eliminating the React hydration warning. +- **fix(cli-tools):** guard `modelId` type before calling `.indexOf()` — prevents a `TypeError` when a model entry without a string `id` reaches the comparison logic in CLI Tools. +- **fix(providers):** add missing `isLocalProvider` import and update changelog. +- **fix(resilience):** add API Key health tracking with automatic rotation and UI toast alerts. ([#2412](https://github.com/diegosouzapw/OmniRoute/pull/2412) — thanks @clousky2020) +- **feat(providers):** support Gemini API keys for Gemini CLI executor. ([#2408](https://github.com/diegosouzapw/OmniRoute/pull/2408) — thanks @benzntech) +- **feat(gamification):** implement Gamification & Leaderboard System with non-blocking event-driven updates. ([#2405](https://github.com/diegosouzapw/OmniRoute/pull/2405) — thanks @oyi77) +- **fix(providers):** Kilo Code provider no longer blocks on a missing local `kilocode` CLI binary — the provider uses OAuth device flow + direct HTTPS to `api.kilo.ai` and never required the CLI at runtime; the connection test was hard-failing with "Local CLI runtime is not installed" even when the OAuth token was valid. CLI Tools integration (`/api/cli-tools/kilo-settings`) keeps its own runtime check. ([#2404](https://github.com/diegosouzapw/OmniRoute/issues/2404) — thanks @Flexible78) +- **fix(db):** `bun add -g omniroute` (and other runtimes that skip postinstall) no longer surfaces a generic 500 — `isNativeSqliteLoadError` now also detects "Could not locate the bindings file" / `MODULE_NOT_FOUND`, so the user gets the friendly rebuild guide instead. ([#2358](https://github.com/diegosouzapw/OmniRoute/issues/2358) — thanks @yamansin) +- **fix(kiro):** enable Google OAuth login option in the Kiro auth modal — surfaces the Google SSO button alongside the existing identity providers. ([#2392](https://github.com/diegosouzapw/OmniRoute/pull/2392) — thanks @congvc-dev) +- **fix(security):** drop hashing layer in `sessionPoolKey` after switching to a non-cryptographic key derivation strategy that clears CodeQL alert #247. ([#2396](https://github.com/diegosouzapw/OmniRoute/pull/2396)) +- **feat(providers):** Gemini Web cookie-based provider — proxies google.com chat through a session cookie, allowing free Gemini access without API keys. ([#2380](https://github.com/diegosouzapw/OmniRoute/pull/2380) — thanks @oyi77) +- **model:** add Composer 2.5 to the Cursor provider catalog. ([#2381](https://github.com/diegosouzapw/OmniRoute/pull/2381) — thanks @backryun) +- **fix:** `tool_use` without adjacent `tool_result` causes Claude 400 — adjacency guard now also applies inside `compressContext`. ([#2383](https://github.com/diegosouzapw/OmniRoute/pull/2383) — thanks @oyi77) +- **build(deps):** bump `electron` from 42.0.1 to 42.1.0 in `/electron`. ([#2397](https://github.com/diegosouzapw/OmniRoute/pull/2397)) +- **build(deps):** production group bumps — 4 updates. ([#2398](https://github.com/diegosouzapw/OmniRoute/pull/2398)) +- **build(deps):** development group bumps — 4 updates. ([#2399](https://github.com/diegosouzapw/OmniRoute/pull/2399)) +- **chore:** sync `release/v3.8.0` with `main` (CodeQL hotfixes + Dependabot bumps) via merge commit. + +#### 2026-05-18 + +- **fix(security):** resolve CodeQL alerts #243/#244/#245 — incomplete URL substring sanitization and weak crypto signal hardening. ([#2391](https://github.com/diegosouzapw/OmniRoute/pull/2391)) +- **fix(security):** switch `sessionPoolKey` derivation to HMAC-SHA256 to clear CodeQL alert #246 (insecure hash for sensitive data). ([#2394](https://github.com/diegosouzapw/OmniRoute/pull/2394)) +- **docs(readme):** restore the 9router acknowledgment that was inadvertently dropped during the v3.8.0 README rework. ([#2393](https://github.com/diegosouzapw/OmniRoute/pull/2393)) +- **refactor(dashboard):** comprehensive nav, providers, endpoint, runtime, quota, pricing, budget redesign + quota sharing preview (sidebar restructure → 12 collapsible sections, 22 new routes). ([#2384](https://github.com/diegosouzapw/OmniRoute/pull/2384)) +- **fix(dashboard):** PR #2384 follow-up review fixes — Runtime quota i18n, Budget projection logic, QuotaShare i18n externalization, Provider Limits semantic markup, bulk endpoint usage. ([#2389](https://github.com/diegosouzapw/OmniRoute/pull/2389)) +- **feat(content):** add Haiper, Leonardo, Ideogram, Suno, and Udio as content/media providers. ([#2377](https://github.com/diegosouzapw/OmniRoute/pull/2377) — thanks @oyi77) +- **feat(@omniroute/opencode-provider):** expand config helpers, add MCP entry, live model fetch, and combo builder. ([#2375](https://github.com/diegosouzapw/OmniRoute/pull/2375) — thanks @mrmm) +- **fix(claude-oauth):** enable system-transforms pipeline for the native Claude executor (closes 400 billing-gate). ([#2370](https://github.com/diegosouzapw/OmniRoute/pull/2370) — thanks @thepigdestroyer) +- **feat(content):** extend providers with video, audio, TTS, music capabilities — Pollinations, MiniMax, Together, Replicate across Audio TTS and Transcription registries. ([#2369](https://github.com/diegosouzapw/OmniRoute/pull/2369) — thanks @oyi77) +- **feat(providers):** add Veo AI Free as a web-wrapper provider for generating video, image, and TTS without an API key. ([#2366](https://github.com/diegosouzapw/OmniRoute/pull/2366) — thanks @oyi77) +- **feat(providers):** add Replicate as a free provider for OpenAI-compatible inference with community models. ([#2364](https://github.com/diegosouzapw/OmniRoute/pull/2364) — thanks @oyi77) +- **fix(claude):** avoided redundant deep cloning of Claude Code messages during semantic passthrough preparation, improving memory/CPU efficiency for large histories. ([#2362](https://github.com/diegosouzapw/OmniRoute/pull/2362) — thanks @terence71-glitch) +- **fix(providers):** register `llm7` in the executor registry and route Cohere via OpenAI-compatible layer. ([#2361](https://github.com/diegosouzapw/OmniRoute/pull/2361), [#2360](https://github.com/diegosouzapw/OmniRoute/pull/2360)) +- **fix(rate-limiter):** Redis is now opt-in — when `REDIS_URL` is unset, the rate limiter falls back to the in-memory store instead of spamming `ECONNREFUSED`. ([#2357](https://github.com/diegosouzapw/OmniRoute/pull/2357)) +- **fix(streaming):** emit protocol-aware stream errors — `createDisconnectAwareStream()` now emits native Responses API or Claude API SSE error blocks based on the client protocol. ([#2355](https://github.com/diegosouzapw/OmniRoute/pull/2355) — thanks @dhaern) +- **fix(combos):** allow bracketed combo names (e.g. `Claude [1m]`) by updating validation schemas. ([#2354](https://github.com/diegosouzapw/OmniRoute/pull/2354) — thanks @congvc-dev) +- **fix(claude-code):** semantic passthrough — preserve Claude Code `messages[]` structure for native Claude OAuth and relay routes. ([#2351](https://github.com/diegosouzapw/OmniRoute/pull/2351) — thanks @terence71-glitch) +- **fix(usage):** extract flat `cached_tokens` and `reasoning_tokens` from OpenAI-compatible usage objects. ([#2350](https://github.com/diegosouzapw/OmniRoute/pull/2350) — thanks @TF0rd) +- **fix(translator):** DeepSeek tool-call response lookup reads cached reasoning before falling back to empty string. ([#2349](https://github.com/diegosouzapw/OmniRoute/pull/2349) — thanks @herjarsa) +- **fix(ui/tooltip):** render in portal + clamp to viewport so tooltips aren't clipped in modal dialogs. ([#2352](https://github.com/diegosouzapw/OmniRoute/pull/2352) — thanks @slider23) +- **fix(auto-routing):** replace bare `getSettings()` with `getCachedSettings` to stop 500 on `auto/*` requests. ([#2346](https://github.com/diegosouzapw/OmniRoute/pull/2346)) +- **fix(docker):** ship Dashboard Docs markdown in the container image. ([#2348](https://github.com/diegosouzapw/OmniRoute/pull/2348)) +- **fix(combo/validator):** treat upstream responses carrying a non-empty `reasoning_content` as valid output. ([#2341](https://github.com/diegosouzapw/OmniRoute/pull/2341)) +- **fix(account-fallback):** classify Anthropic `Usage Limit Reached` as `QUOTA_EXHAUSTED` with a 1h cooldown. ([#2321](https://github.com/diegosouzapw/OmniRoute/pull/2321)) +- **feat(providers):** add GitHub Models as a free provider — GPT-5, o-series, DeepSeek-R1, Llama 4, Grok 3. ([#2344](https://github.com/diegosouzapw/OmniRoute/pull/2344) — thanks @oyi77) +- **feat(providers):** add Hackclub AI as a free provider — 30+ models, no credit card required. ([#2339](https://github.com/diegosouzapw/OmniRoute/pull/2339) — thanks @oyi77) +- **feat(providers):** add Microsoft Copilot Web executor — WebSocket-based provider. ([#2340](https://github.com/diegosouzapw/OmniRoute/pull/2340) — thanks @oyi77) +- **feat(routing):** LKGP stores last known good account `connectionId` alongside provider. ([#2338](https://github.com/diegosouzapw/OmniRoute/pull/2338) — thanks @oyi77) +- **feat(dashboard):** add Claude Code auth import/export UI + i18n (three-PR series: libs, API routes, dashboard UI). +- **feat(dashboard):** add Gemini CLI auth import/export UI + i18n (three-PR series: libs, API routes, dashboard UI). +- **fix(routing):** implement embedding combos, local provider validation bypass, and resolve migration collisions. +- **fix(build):** import Monaco ESM API to fix webpack `nls.messages-loader` error. +- **fix(ui):** v3.8.0 polish — connections border, sticky tabs, EN translations, save toasts, auto-combo catalog. ([#2305](https://github.com/diegosouzapw/OmniRoute/pull/2305) — thanks @mrmm) +- **fix(auth+build):** Bearer manage scope on management routes + lazy-load deepseek PoW solver. ([#2308](https://github.com/diegosouzapw/OmniRoute/pull/2308) — thanks @mrmm) +- **fix(claude):** guard orphan tool_use/tool_result pairs before upstream send. ([#2312](https://github.com/diegosouzapw/OmniRoute/pull/2312) — thanks @mrmm) +- **fix(ui):** remove count from batch removal button. ([#2309](https://github.com/diegosouzapw/OmniRoute/pull/2309) — thanks @hartmark) +- **fix:** remove implicit API key request caps — removes default 1K/5K/20K rate caps. ([#2289](https://github.com/diegosouzapw/OmniRoute/pull/2289) — thanks @josephvoxone) +- **fix(sse):** strip stale `Content-Encoding`, `Content-Length`, and `Transfer-Encoding` headers on non-streaming forward. ([#2264](https://github.com/diegosouzapw/OmniRoute/pull/2264) — thanks @gleber) +- **chore(providers):** refresh provider model metadata and ordering. ([#2318](https://github.com/diegosouzapw/OmniRoute/pull/2318) — thanks @backryun) +- **chore(providers):** consolidate Alibaba provider entries. ([#2319](https://github.com/diegosouzapw/OmniRoute/pull/2319) — thanks @backryun) +- **fix(streaming):** harden stream readiness detection. ([#2317](https://github.com/diegosouzapw/OmniRoute/pull/2317) — thanks @dhaern) +- **fix(v1/messages):** default to non-streaming when `stream` field is absent for Anthropic format. ([#2326](https://github.com/diegosouzapw/OmniRoute/pull/2326) — thanks @thepigdestroyer) +- **fix(claude):** `fitThinkingToMaxTokens` caps thinking budget to model's output ceiling. ([#2327](https://github.com/diegosouzapw/OmniRoute/pull/2327) — thanks @thepigdestroyer) +- **fix(codex):** Codex reasoning priority resolves `modelEffort` before `explicitReasoning`. ([#2335](https://github.com/diegosouzapw/OmniRoute/pull/2335) — thanks @terence71-glitch) +- **fix(providers):** providers page no longer deadlocks when no providers are configured. ([#2329](https://github.com/diegosouzapw/OmniRoute/pull/2329) — thanks @slider23) +- **chore(providers):** update HuggingFace to use the new `/v1/` router endpoint. ([#2322](https://github.com/diegosouzapw/OmniRoute/pull/2322) — thanks @backryun) +- **fix(security):** resolve CodeQL ReDoS + URL sanitization alerts. +- **fix(auth):** stop retrying unrecoverable token refresh failures and include connection id in token health check credentials. +- **fix(auth):** return synthetic credentials for noAuth free providers and show no-auth card in dashboard instead of OAuth modal. +- **fix(endpoint):** replace nested ` + )} + {electronUpdateStatus.status === "downloading" && ( +
+ + progress_activity + + + {electronUpdateStatus.percent || 0}% + +
+ )} + {electronUpdateStatus.status === "downloaded" && ( + + )} + {(electronUpdateStatus.status === "error" || + electronUpdateStatus.status === "idle" || + electronUpdateStatus.status === "not-available") && ( + + )} + + ) : ( + + )} - + + {/* Direct download fallback links shown if in Electron and auto-updater has failed, is idle, or has completed check */} + {isElectron && + (electronUpdateStatus.status === "error" || + electronUpdateStatus.status === "idle" || + electronUpdateStatus.status === "available" || + electronUpdateStatus.status === "not-available") && ( +
+

+ Or download the respective installer format directly: +

+
+ + +
+
+ )} {/* News Notification Banner */} @@ -774,75 +955,77 @@ export default function HomePageClient({ machineId }: HomePageClientProps) { -
    -
  1. -
    - key -
    -
    - {t("step1Title")} -

    - {t.rich("step1Desc", { - endpoint: (chunks) => ( - - {chunks} - - ), - })} -

    -
    -
  2. -
  3. -
    - dns -
    -
    - {t("step2Title")} -

    - {t.rich("step2Desc", { - providers: (chunks) => ( - - {chunks} - - ), - })} -

    -
    -
  4. -
  5. -
    - link -
    -
    - {t("step3Title")} -

    {t("step3Desc", { url: currentEndpoint })}

    -
    -
  6. -
  7. -
    - analytics -
    -
    - {t("step4Title")} -

    - {t.rich("step4Desc", { - logs: (chunks) => ( - - {chunks} - - ), - analytics: (chunks) => ( - - {chunks} - - ), - })} -

    -
    -
  8. -
- - +
    +
  1. +
    + key +
    +
    + {t("step1Title")} +

    + {t.rich("step1Desc", { + endpoint: (chunks) => ( + + {chunks} + + ), + })} +

    +
    +
  2. +
  3. +
    + dns +
    +
    + {t("step2Title")} +

    + {t.rich("step2Desc", { + providers: (chunks) => ( + + {chunks} + + ), + })} +

    +
    +
  4. +
  5. +
    + link +
    +
    + {t("step3Title")} +

    + {t("step3Desc", { url: currentEndpoint })} +

    +
    +
  6. +
  7. +
    + analytics +
    +
    + {t("step4Title")} +

    + {t.rich("step4Desc", { + logs: (chunks) => ( + + {chunks} + + ), + analytics: (chunks) => ( + + {chunks} + + ), + })} +

    +
    +
  8. +
+ + )} {/* Provider Topology (controlled by Appearance setting, default on) */} @@ -903,6 +1086,7 @@ function ProviderOverviewCard({ item.errors > 0 ? "text-red-500" : item.connected > 0 ? "text-green-500" : "text-text-muted"; const authTypeConfig = { + "no-auth": { color: "bg-stone-500", label: "No Auth" }, free: { color: "bg-green-500", label: tc("free") }, oauth: { color: "bg-blue-500", label: t("oauthLabel") }, apikey: { color: "bg-amber-500", label: t("apiKeyLabel") }, diff --git a/src/app/(dashboard)/dashboard/TierCoverageWidget.tsx b/src/app/(dashboard)/dashboard/TierCoverageWidget.tsx index 6ce971b063..d356c6a578 100644 --- a/src/app/(dashboard)/dashboard/TierCoverageWidget.tsx +++ b/src/app/(dashboard)/dashboard/TierCoverageWidget.tsx @@ -3,16 +3,16 @@ import { useEffect, useState } from "react"; import { useTranslations } from "next-intl"; import Link from "next/link"; -import { FREE_PROVIDERS, OAUTH_PROVIDERS } from "@/shared/constants/providers"; +import { NOAUTH_PROVIDERS, OAUTH_PROVIDERS } from "@/shared/constants/providers"; type TierCount = { configured: number; active: number }; type Coverage = { tier1: TierCount; tier2: TierCount; tier3: TierCount }; -const FREE_IDS = new Set(Object.keys(FREE_PROVIDERS)); +const NOAUTH_IDS = new Set(Object.keys(NOAUTH_PROVIDERS)); const OAUTH_IDS = new Set(Object.keys(OAUTH_PROVIDERS)); function classifyConnection(providerId: string): "tier1" | "tier2" | "tier3" { - if (FREE_IDS.has(providerId)) return "tier3"; + if (NOAUTH_IDS.has(providerId)) return "tier3"; if (OAUTH_IDS.has(providerId)) return "tier1"; return "tier2"; } diff --git a/src/app/(dashboard)/dashboard/api-manager/ApiManagerPageClient.tsx b/src/app/(dashboard)/dashboard/api-manager/ApiManagerPageClient.tsx index 887dbe6240..6fdabf7ed0 100644 --- a/src/app/(dashboard)/dashboard/api-manager/ApiManagerPageClient.tsx +++ b/src/app/(dashboard)/dashboard/api-manager/ApiManagerPageClient.tsx @@ -5,6 +5,16 @@ import { Card, Button, Input, Modal, CardSkeleton } from "@/shared/components"; import { useCopyToClipboard } from "@/shared/hooks/useCopyToClipboard"; import { useTranslations } from "next-intl"; import { getProviderDisplayName } from "@/lib/display/names"; +import ApiKeyFilterBar from "./components/ApiKeyFilterBar"; +import { + isKeyActive, + isExpired, + isRestricted as isKeyRestricted, + classifyKeyStatus, + computeApiKeyCounts, +} from "./apiManagerPageUtils"; +import type { KeyStatus, KeyType } from "./apiManagerPageUtils"; +import { readActiveOnlyPreference, writeActiveOnlyPreference } from "./apiManagerPageStorage"; // Constants for validation const MAX_KEY_NAME_LENGTH = 200; @@ -121,6 +131,11 @@ export default function ApiManagerPageClient() { const [sessionCounts, setSessionCounts] = useState>({}); const [allowKeyReveal, setAllowKeyReveal] = useState(false); + const [searchQuery, setSearchQuery] = useState(""); + const [activeOnly, setActiveOnly] = useState(false); + const [statusFilter, setStatusFilter] = useState(null); + const [typeFilter, setTypeFilter] = useState(null); + const { copied, copy } = useCopyToClipboard(); useEffect(() => { @@ -129,6 +144,14 @@ export default function ApiManagerPageClient() { fetchConnections(); }, []); + useEffect(() => { + setActiveOnly(readActiveOnlyPreference()); + }, []); + + useEffect(() => { + writeActiveOnlyPreference(activeOnly); + }, [activeOnly]); + const fetchModels = async () => { try { const res = await fetch("/v1/models"); @@ -239,6 +262,49 @@ export default function ApiManagerPageClient() { const clearPageError = useCallback(() => setPageError(null), []); + const keyCounts = useMemo(() => computeApiKeyCounts(keys), [keys]); + + const filteredKeys = useMemo(() => { + let list = keys; + + // 1. activeOnly toggle (shortcut for the most common case) + if (activeOnly) { + list = list.filter(isKeyActive); + } + + // 2. status chip filter + if (statusFilter === "active") list = list.filter(isKeyActive); + else if (statusFilter === "disabled") list = list.filter((k) => k.isActive === false); + else if (statusFilter === "banned") list = list.filter((k) => k.isBanned === true); + else if (statusFilter === "expired") list = list.filter(isExpired); + + // 3. type chip filter + if (typeFilter === "manage") list = list.filter((k) => k.scopes?.includes("manage")); + else if (typeFilter === "restricted") list = list.filter(isKeyRestricted); + else if (typeFilter === "standard") + list = list.filter((k) => !k.scopes?.includes("manage") && !isKeyRestricted(k)); + + // 4. search query (case-insensitive substring on name and key) + if (searchQuery.trim()) { + const q = searchQuery.toLowerCase(); + list = list.filter( + (k) => k.name.toLowerCase().includes(q) || k.key.toLowerCase().includes(q) + ); + } + + return list; + }, [keys, activeOnly, statusFilter, typeFilter, searchQuery]); + + const isFiltered = + activeOnly || statusFilter !== null || typeFilter !== null || searchQuery.trim() !== ""; + + const handleClearFilters = () => { + setSearchQuery(""); + setActiveOnly(false); + setStatusFilter(null); + setTypeFilter(null); + }; + const handleCreateKey = async () => { // Validate raw input first, then sanitize const validation = validateKeyName(newKeyName, t); @@ -555,6 +621,21 @@ export default function ApiManagerPageClient() { )} + {/* Filter Bar — shown when there are keys */} + {keys.length > 0 && ( + + )} + {/* Keys List Card */}
@@ -563,7 +644,19 @@ export default function ApiManagerPageClient() { vpn_key
-

{t("registeredKeys")}

+

+ {t("registeredKeys")} + {isFiltered && ( + + ({t("shownOf", { shown: filteredKeys.length, total: keys.length })}) + + )} + {!isFiltered && ( + + ({keys.length}) + + )} +

{keys.length}{" "} {keys.length === 1 @@ -605,6 +698,14 @@ export default function ApiManagerPageClient() { {t("createFirstKey")}

+ ) : filteredKeys.length === 0 ? ( +
+
+ search_off +
+

{t("emptyFilterTitle")}

+ +
) : (
{/* Table Header */} @@ -618,7 +719,7 @@ export default function ApiManagerPageClient() {
{/* Table Rows */} - {keys.map((key) => { + {filteredKeys.map((key) => { const stats = usageStats[key.id]; const isRestricted = Array.isArray(key.allowedModels) && key.allowedModels.length > 0; const hasConnectionRestrictions = diff --git a/src/app/(dashboard)/dashboard/api-manager/apiManagerPageStorage.ts b/src/app/(dashboard)/dashboard/api-manager/apiManagerPageStorage.ts new file mode 100644 index 0000000000..7bb8626688 --- /dev/null +++ b/src/app/(dashboard)/dashboard/api-manager/apiManagerPageStorage.ts @@ -0,0 +1,41 @@ +export const ACTIVE_ONLY_STORAGE_KEY = "omniroute-api-manager-active-only"; + +interface StorageReader { + getItem(key: string): string | null; +} + +interface StorageWriter extends StorageReader { + setItem(key: string, value: string): void; + removeItem(key: string): void; +} + +function getBrowserStorage(): StorageWriter | null { + try { + return globalThis.localStorage ?? null; + } catch { + return null; + } +} + +export function parseActiveOnlyPreference(value: string | null | undefined): boolean { + return value === "true"; +} + +export function readActiveOnlyPreference( + storage: StorageReader | null = getBrowserStorage() +): boolean { + if (!storage) return false; + return parseActiveOnlyPreference(storage.getItem(ACTIVE_ONLY_STORAGE_KEY)); +} + +export function writeActiveOnlyPreference( + enabled: boolean, + storage: StorageWriter | null = getBrowserStorage() +): void { + if (!storage) return; + if (enabled) { + storage.setItem(ACTIVE_ONLY_STORAGE_KEY, "true"); + return; + } + storage.removeItem(ACTIVE_ONLY_STORAGE_KEY); +} diff --git a/src/app/(dashboard)/dashboard/api-manager/apiManagerPageUtils.ts b/src/app/(dashboard)/dashboard/api-manager/apiManagerPageUtils.ts new file mode 100644 index 0000000000..d6dd756900 --- /dev/null +++ b/src/app/(dashboard)/dashboard/api-manager/apiManagerPageUtils.ts @@ -0,0 +1,85 @@ +export type KeyStatus = "active" | "disabled" | "banned" | "expired"; + +// "manage" scope = management key; "restricted" = has model/connection allowlists; +// "standard" = no manage scope and no allowlists. +// Note: a "manage" key with allowlists is still classified as "manage" (manage takes priority). +export type KeyType = "standard" | "manage" | "restricted"; + +export interface ApiKeyShape { + isActive?: boolean; + isBanned?: boolean; + expiresAt?: string | null; + scopes?: string[]; + allowedModels?: string[] | null; + allowedConnections?: string[] | null; +} + +export function isKeyActive(k: ApiKeyShape): boolean { + if (k.isBanned === true) return false; + if (k.isActive === false) return false; + if (k.expiresAt) { + return new Date(k.expiresAt).getTime() > Date.now(); + } + return true; +} + +export function isExpired(k: ApiKeyShape): boolean { + if (!k.expiresAt) return false; + const ts = new Date(k.expiresAt).getTime(); + if (Number.isNaN(ts)) return false; + return ts < Date.now(); +} + +export function isRestricted(k: ApiKeyShape): boolean { + const hasModelRestrictions = Array.isArray(k.allowedModels) && k.allowedModels.length > 0; + const hasConnectionRestrictions = + Array.isArray(k.allowedConnections) && k.allowedConnections.length > 0; + return hasModelRestrictions || hasConnectionRestrictions; +} + +export function classifyKeyStatus(k: ApiKeyShape): KeyStatus { + if (k.isBanned === true) return "banned"; + if (isExpired(k)) return "expired"; + if (k.isActive === false) return "disabled"; + return "active"; +} + +export function classifyKeyType(k: ApiKeyShape): KeyType { + if (Array.isArray(k.scopes) && k.scopes.includes("manage")) return "manage"; + if (isRestricted(k)) return "restricted"; + return "standard"; +} + +export interface ApiKeyCounts { + total: number; + active: number; + disabled: number; + banned: number; + expired: number; + standard: number; + manage: number; + restricted: number; +} + +export function computeApiKeyCounts(keys: ApiKeyShape[]): ApiKeyCounts { + const counts: ApiKeyCounts = { + total: keys.length, + active: 0, + disabled: 0, + banned: 0, + expired: 0, + standard: 0, + manage: 0, + restricted: 0, + }; + + for (const k of keys) { + const status = classifyKeyStatus(k); + counts[status] += 1; + + const type = classifyKeyType(k); + counts[type] += 1; + } + + return counts; +} diff --git a/src/app/(dashboard)/dashboard/api-manager/components/ApiKeyFilterBar.tsx b/src/app/(dashboard)/dashboard/api-manager/components/ApiKeyFilterBar.tsx new file mode 100644 index 0000000000..81a0546648 --- /dev/null +++ b/src/app/(dashboard)/dashboard/api-manager/components/ApiKeyFilterBar.tsx @@ -0,0 +1,181 @@ +"use client"; + +import { useTranslations } from "next-intl"; +import { Card, Input, Toggle } from "@/shared/components"; +import ApiKeyFilterChip from "./ApiKeyFilterChip"; +import type { KeyStatus, KeyType, ApiKeyCounts } from "../apiManagerPageUtils"; + +interface ApiKeyFilterBarProps { + counts: ApiKeyCounts; + searchQuery: string; + onSearchChange: (q: string) => void; + activeOnly: boolean; + onActiveOnlyChange: (v: boolean) => void; + statusFilter: KeyStatus | null; + onStatusChange: (s: KeyStatus | null) => void; + typeFilter: KeyType | null; + onTypeChange: (t: KeyType | null) => void; +} + +export default function ApiKeyFilterBar({ + counts, + searchQuery, + onSearchChange, + activeOnly, + onActiveOnlyChange, + statusFilter, + onStatusChange, + typeFilter, + onTypeChange, +}: ApiKeyFilterBarProps) { + const t = useTranslations("apiManager"); + const tc = useTranslations("common"); + + const statusChips: Array<{ + value: KeyStatus | null; + label: string; + dotColor: string | null; + count: number; + }> = [ + { value: null, label: t("filterAll"), dotColor: null, count: counts.total }, + { + value: "active", + label: t("filterStatusActive"), + dotColor: "bg-green-500", + count: counts.active, + }, + { + value: "disabled", + label: t("filterStatusDisabled"), + dotColor: "bg-gray-500", + count: counts.disabled, + }, + { + value: "banned", + label: t("filterStatusBanned"), + dotColor: "bg-red-500", + count: counts.banned, + }, + { + value: "expired", + label: t("filterStatusExpired"), + dotColor: "bg-amber-500", + count: counts.expired, + }, + ]; + + const typeChips: Array<{ + value: KeyType | null; + label: string; + dotColor: string | null; + count: number; + }> = [ + { value: null, label: t("filterAll"), dotColor: null, count: counts.total }, + { + value: "standard", + label: t("filterTypeStandard"), + dotColor: "bg-slate-500", + count: counts.standard, + }, + { + value: "manage", + label: t("filterTypeManage"), + dotColor: "bg-rose-500", + count: counts.manage, + }, + { + value: "restricted", + label: t("filterTypeRestricted"), + dotColor: "bg-amber-500", + count: counts.restricted, + }, + ]; + + return ( + +
+ {/* Row 1: Search + Active Only toggle */} +
+
+ onSearchChange(e.target.value)} + placeholder={t("searchPlaceholder")} + aria-label={t("searchPlaceholder")} + icon="search" + inputClassName={searchQuery ? "pr-9" : ""} + /> + {searchQuery && ( + + )} +
+ { + onActiveOnlyChange(v); + // When enabling activeOnly, reset statusFilter if it's not "active" + if (v && statusFilter !== null && statusFilter !== "active") { + onStatusChange(null); + } + }} + label={t("activeOnly")} + className="rounded-lg border border-border bg-bg-subtle px-3 py-1.5" + /> +
+ + {/* STATUS + TYPE chips — single row on >=1280px, wraps below on smaller */} +
+
+ + {t("filterStatus")}: + + {statusChips.map((chip) => ( + { + onStatusChange(chip.value); + // If user picks a non-active status chip while activeOnly is on, turn off activeOnly + if (chip.value !== null && chip.value !== "active" && activeOnly) { + onActiveOnlyChange(false); + } + }} + /> + ))} +
+ + +
+ + ); +} diff --git a/src/app/(dashboard)/dashboard/api-manager/components/ApiKeyFilterChip.tsx b/src/app/(dashboard)/dashboard/api-manager/components/ApiKeyFilterChip.tsx new file mode 100644 index 0000000000..471b2f43d2 --- /dev/null +++ b/src/app/(dashboard)/dashboard/api-manager/components/ApiKeyFilterChip.tsx @@ -0,0 +1,39 @@ +"use client"; + +import { cn } from "@/shared/utils/cn"; + +interface ApiKeyFilterChipProps { + label: string; + count?: number; + isActive: boolean; + dotColor?: string | null; + onClick: () => void; +} + +export default function ApiKeyFilterChip({ + label, + count, + isActive, + dotColor, + onClick, +}: ApiKeyFilterChipProps) { + return ( + + ); +} diff --git a/src/app/(dashboard)/dashboard/combos/playground/ComboPlaygroundClient.tsx b/src/app/(dashboard)/dashboard/combos/playground/ComboPlaygroundClient.tsx new file mode 100644 index 0000000000..ac60c092f3 --- /dev/null +++ b/src/app/(dashboard)/dashboard/combos/playground/ComboPlaygroundClient.tsx @@ -0,0 +1,330 @@ +"use client"; + +import { useState, useEffect, useCallback } from "react"; +import Button from "@/shared/components/Button"; +import Card from "@/shared/components/Card"; +import Badge from "@/shared/components/Badge"; + +// ── Types ──────────────────────────────────────────────────────────────────── + +interface TargetSimulation { + provider: string; + model: string; + strategy: string; + rank: number; + estimatedCost: number; + estimatedLatencyMs: number; + status: "available" | "no_quota" | "degraded" | "error" | "unknown"; + maxTokens?: number; + contextWindow?: number; +} + +interface SimulateResponse { + comboId?: string; + comboName: string; + strategy: string; + targets: TargetSimulation[]; + totalEstimatedCost: number; + totalEstimatedLatencyMs: number; + warnings: string[]; + errors: string[]; +} + +interface Combo { + id: string; + name: string; + strategy: string; + targets: string; + isActive: boolean; +} + +// ── Status helpers ─────────────────────────────────────────────────────────── + +function StatusTag({ status }: { status: TargetSimulation["status"] }) { + const map: Record< + TargetSimulation["status"], + { label: string; variant: "success" | "error" | "warning" | "info" } + > = { + available: { label: "Available", variant: "success" }, + no_quota: { label: "No Quota", variant: "error" }, + degraded: { label: "Degraded", variant: "warning" }, + error: { label: "Error", variant: "error" }, + unknown: { label: "Unknown", variant: "info" }, + }; + return ( + + {map[status].label} + + ); +} + +// ── Component ──────────────────────────────────────────────────────────────── + +export default function ComboPlaygroundClient() { + const [combos, setCombos] = useState([]); + const [selectedComboId, setSelectedComboId] = useState(""); + const [promptTokens, setPromptTokens] = useState(500); + const [loading, setLoading] = useState(false); + const [result, setResult] = useState(null); + + // Fetch combos on mount + useEffect(() => { + fetch("/api/combos") + .then((r) => r.json()) + .then((data) => { + const list: Combo[] = Array.isArray(data) ? data : (data?.combos ?? data?.data ?? []); + setCombos(list); + if (list.length > 0) setSelectedComboId(list[0].id); + }) + .catch(() => {}); + }, []); + + const simulate = useCallback(async () => { + if (!selectedComboId) return; + setLoading(true); + setResult(null); + + try { + const res = await fetch("/api/playground/simulate-route", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + comboId: selectedComboId, + promptTokens, + }), + }); + const data = await res.json(); + if (res.ok) setResult(data); + else setResult(data); + } catch { + setResult({ + comboName: "Error", + strategy: "-", + targets: [], + totalEstimatedCost: 0, + totalEstimatedLatencyMs: 0, + warnings: [], + errors: ["Network error during simulation"], + }); + } finally { + setLoading(false); + } + }, [selectedComboId, promptTokens]); + + return ( +
+
+
+

Combo Playground

+

+ Simulate how requests will be routed through your combos +

+
+
+ + {/* Configuration Panel */} + +
+

Configuration

+ +
+ {/* Combo Selector */} +
+ + +
+ + {/* Prompt Tokens */} +
+ + setPromptTokens(Number(e.target.value))} + className="w-full" + /> +
+ 100 + 100K +
+
+
+ + +
+
+ + {/* Results */} + {result && ( + <> + {/* Combo Overview */} + +
+
+

Routing Path

+
+ + Strategy: {result.strategy} + + + Est. Cost: ${result.totalEstimatedCost.toFixed(6)} + + + Est. Latency: {result.totalEstimatedLatencyMs.toFixed(0)}ms + +
+
+ + {/* Visual Cascade */} +
+ {result.targets.map((t, i) => ( +
+ {/* Arrow between targets */} + {i > 0 && ( +
+
+ + + + {result.strategy === "priority" && ( + fallback + )} + {result.strategy === "weighted" && ( + weight {t.rank} + )} +
+
+ )} + + {/* Target Card */} +
+
+
+
+ {t.rank} +
+
+
{t.provider}
+
{t.model}
+
+
+
+ + + ${t.estimatedCost.toFixed(6)} + + {t.estimatedLatencyMs}ms + {t.contextWindow && ( + + {(t.contextWindow / 1000).toFixed(0)}K ctx + + )} +
+
+
+
+ ))} +
+
+
+ + {/* Warnings & Errors */} + {result.warnings.length > 0 && ( + +
+

+ Warnings ({result.warnings.length}) +

+ {result.warnings.map((w, i) => ( +

+ ⚠️ + {w} +

+ ))} +
+
+ )} + + {result.errors.length > 0 && ( + +
+

+ Errors ({result.errors.length}) +

+ {result.errors.map((e, i) => ( +

+ 🚫 + {e} +

+ ))} +
+
+ )} + + )} + + {/* Empty State */} + {!result && combos.length > 0 && ( + +
+

+ Select a combo and click Simulate Route to see the routing path. +

+
+
+ )} + + {combos.length === 0 && ( + +
+

+ No combos configured yet.{" "} + + Create one first + + . +

+
+
+ )} +
+ ); +} diff --git a/src/app/(dashboard)/dashboard/combos/playground/page.tsx b/src/app/(dashboard)/dashboard/combos/playground/page.tsx new file mode 100644 index 0000000000..373aa4f533 --- /dev/null +++ b/src/app/(dashboard)/dashboard/combos/playground/page.tsx @@ -0,0 +1,11 @@ +import type { Metadata } from "next"; +import ComboPlaygroundClient from "./ComboPlaygroundClient"; + +export const metadata: Metadata = { + title: "OmniRoute — Combo Playground", + description: "Simulate combo routing paths visually", +}; + +export default function ComboPlaygroundPage() { + return ; +} diff --git a/src/app/(dashboard)/dashboard/media-providers/[kind]/[id]/MediaProviderPageClient.tsx b/src/app/(dashboard)/dashboard/media-providers/[kind]/[id]/MediaProviderPageClient.tsx new file mode 100644 index 0000000000..10b13d9051 --- /dev/null +++ b/src/app/(dashboard)/dashboard/media-providers/[kind]/[id]/MediaProviderPageClient.tsx @@ -0,0 +1,192 @@ +"use client"; + +import { useState, useEffect } from "react"; +import { useTranslations } from "next-intl"; +import { Button } from "@/shared/components"; +import MediaProviderHeader from "../../components/MediaProviderHeader"; +import MediaProviderKindNav from "../../components/MediaProviderKindNav"; +import type { MediaKind } from "../../components/MediaProviderKindNav"; +import { EmbeddingExampleCard } from "../../components/EmbeddingExampleCard"; +import { ImageExampleCard } from "../../components/ImageExampleCard"; +import { TtsExampleCard } from "../../components/TtsExampleCard"; +import { SttExampleCard } from "../../components/SttExampleCard"; +import { WebSearchExampleCard } from "../../components/WebSearchExampleCard"; +import { WebFetchExampleCard } from "../../components/WebFetchExampleCard"; +import { VideoExampleCard } from "../../components/VideoExampleCard"; +import { MusicExampleCard } from "../../components/MusicExampleCard"; + +interface Connection { + id: string; + name?: string | null; + testStatus?: string | null; + isActive?: boolean; + lastErrorAt?: string | null; + lastError?: string | null; +} + +interface MediaProviderPageClientProps { + providerId: string; + providerName: string; + providerColor?: string; + kindLabel: string; + activeKind: MediaKind; + website?: string; + hasFree?: boolean; + freeNote?: string; +} + +function renderPlayground(kind: MediaKind, providerId: string) { + switch (kind) { + case "embedding": + return ; + case "image": + return ; + case "tts": + return ; + case "stt": + return ; + case "webSearch": + return ; + case "webFetch": + return ; + case "video": + return ; + case "music": + return ; + case "imageToText": + // Endpoint /api/v1/images/understanding does not exist yet — omitted. + return ( +
+
+ image_search +

Image to Text

+
+

+ Inline playground for Image-to-Text will be available when{" "} + + /api/v1/images/understanding + {" "} + is implemented. +

+
+ ); + default: + return null; + } +} + +export default function MediaProviderPageClient({ + providerId, + providerName, + providerColor, + kindLabel, + activeKind, + website, + hasFree, + freeNote, +}: MediaProviderPageClientProps) { + const t = useTranslations("media"); + const [connections, setConnections] = useState([]); + const [loading, setLoading] = useState(true); + + useEffect(() => { + const fetchConnections = async () => { + try { + const res = await fetch(`/api/providers?provider=${encodeURIComponent(providerId)}`); + if (res.ok) { + const data = await res.json(); + setConnections( + (data.connections ?? []).filter( + (c: Connection & { provider?: string }) => c.provider === providerId + ) + ); + } + } catch { + // ignore + } finally { + setLoading(false); + } + }; + void fetchConnections(); + }, [providerId]); + + const backHref = `/dashboard/media-providers/${activeKind}`; + + return ( +
+ + + + + {/* Connections */} +
+
+

+ {t("connections", { count: connections.length })} +

+ +
+ + {loading ? ( +
{t("loading")}
+ ) : connections.length === 0 ? ( +
+ key_off + {t("noConnections")} + +
+ ) : ( +
+ {connections.map((conn) => ( +
+ + {conn.name ?? conn.id} + {conn.isActive === false && ( + + disabled + + )} +
+ ))} +
+ )} +
+ + {/* Playground */} + {renderPlayground(activeKind, providerId)} +
+ ); +} diff --git a/src/app/(dashboard)/dashboard/media-providers/[kind]/[id]/page.tsx b/src/app/(dashboard)/dashboard/media-providers/[kind]/[id]/page.tsx new file mode 100644 index 0000000000..907ec38cc6 --- /dev/null +++ b/src/app/(dashboard)/dashboard/media-providers/[kind]/[id]/page.tsx @@ -0,0 +1,66 @@ +import { notFound } from "next/navigation"; +import { getTranslations } from "next-intl/server"; +import { AI_PROVIDERS } from "@/shared/constants/providers"; +import type { MediaKind } from "../../components/MediaProviderKindNav"; +import { MEDIA_KINDS } from "../../components/MediaProviderKindNav"; +import MediaProviderPageClient from "./MediaProviderPageClient"; + +interface PageProps { + params: Promise<{ kind: string; id: string }>; +} + +/** + * /dashboard/media-providers/[kind]/[id] + * + * Individual provider page for a media-service provider. + * Validates both kind and id; 404 if either is unknown or the provider + * does not declare the requested kind. + */ +export default async function MediaProviderDetailPage({ params }: PageProps) { + const { kind, id } = await params; + + // Validate kind + if (!MEDIA_KINDS.includes(kind as MediaKind)) { + notFound(); + } + + const validKind = kind as MediaKind; + + // Validate provider exists in AI_PROVIDERS + const provider = Object.values(AI_PROVIDERS).find((p) => p.id === id) as + | (Record & { + id: string; + name: string; + color?: string; + website?: string; + hasFree?: boolean; + freeNote?: string; + }) + | undefined; + + if (!provider) { + notFound(); + } + + // Validate that the provider declares this kind + const serviceKinds = provider.serviceKinds as string[] | undefined; + if (!Array.isArray(serviceKinds) || !serviceKinds.includes(validKind)) { + notFound(); + } + + const t = await getTranslations("media"); + const kindLabel = t(`kinds.${validKind}`); + + return ( + + ); +} diff --git a/src/app/(dashboard)/dashboard/media-providers/[kind]/page.tsx b/src/app/(dashboard)/dashboard/media-providers/[kind]/page.tsx new file mode 100644 index 0000000000..a9aa1c83bd --- /dev/null +++ b/src/app/(dashboard)/dashboard/media-providers/[kind]/page.tsx @@ -0,0 +1,91 @@ +import { notFound } from "next/navigation"; +import { getTranslations } from "next-intl/server"; +import Link from "next/link"; +import { AI_PROVIDERS } from "@/shared/constants/providers"; +import type { MediaKind } from "../components/MediaProviderKindNav"; +import MediaProviderKindNav, { MEDIA_KINDS } from "../components/MediaProviderKindNav"; +import ProviderIcon from "@/shared/components/ProviderIcon"; + +interface PageProps { + params: Promise<{ kind: string }>; +} + +/** + * /dashboard/media-providers/[kind] + * + * Lists all AI providers that declare the given serviceKind. + * Returns 404 for unknown kinds. + */ +export default async function MediaProviderKindPage({ params }: PageProps) { + const { kind } = await params; + + // Validate kind + if (!MEDIA_KINDS.includes(kind as MediaKind)) { + notFound(); + } + + const validKind = kind as MediaKind; + const t = await getTranslations("media"); + + // Filter providers that support this kind + const matchingProviders = Object.values(AI_PROVIDERS).filter((p) => { + const serviceKinds = (p as Record).serviceKinds as string[] | undefined; + return Array.isArray(serviceKinds) && serviceKinds.includes(validKind); + }); + + const kindLabel = t(`kinds.${validKind}`); + + return ( +
+
+

{kindLabel}

+

{t("noProviders")}

+
+ + + + {matchingProviders.length === 0 ? ( +
+ category +

{t("noProviders")}

+
+ ) : ( +
+ {matchingProviders.map((provider) => { + const p = provider as Record & { + id: string; + name: string; + color?: string; + hasFree?: boolean; + freeNote?: string; + }; + return ( + +
+
+
+ +
+ {p.name} +
+ {p.hasFree && ( + + Free + + )} +
+ + ); + })} +
+ )} +
+ ); +} diff --git a/src/app/(dashboard)/dashboard/media-providers/components/EmbeddingExampleCard.tsx b/src/app/(dashboard)/dashboard/media-providers/components/EmbeddingExampleCard.tsx new file mode 100644 index 0000000000..1024ea77a1 --- /dev/null +++ b/src/app/(dashboard)/dashboard/media-providers/components/EmbeddingExampleCard.tsx @@ -0,0 +1,122 @@ +"use client"; + +import { useState } from "react"; +import { useTranslations } from "next-intl"; +import { useApiKey } from "../../providers/hooks/useApiKey"; +import { useProviderModels } from "../../providers/hooks/useProviderModels"; +import { buildCurl } from "../../providers/utils/buildCurl"; +import { PlaygroundCard } from "./PlaygroundCard"; + +interface Props { + providerId: string; +} + +const ENDPOINT_PATH = "/api/v1/embeddings"; + +function extractError(data: unknown): string | null { + if (!data || typeof data !== "object") return null; + const d = data as Record; + const err = d.error as Record | undefined; + if (err?.message) return String(err.message); + if (typeof d.message === "string") return d.message; + return null; +} + +export function EmbeddingExampleCard({ providerId }: Props) { + const t = useTranslations("miniPlayground"); + const { apiKey } = useApiKey(); + const { models } = useProviderModels(providerId); + + const firstModel = models[0]?.id ?? ""; + const [model, setModel] = useState(""); + const [inputText, setInputText] = useState("Hello, world!"); + const [running, setRunning] = useState(false); + const [result, setResult] = useState<{ data: unknown; latencyMs: number } | undefined>(); + const [error, setError] = useState(null); + + const effectiveModel = model || firstModel; + + const buildBody = () => ({ model: effectiveModel, input: inputText }); + + const curlSnippet = buildCurl({ + endpoint: + (typeof window !== "undefined" ? window.location.origin : "http://localhost:20128") + + ENDPOINT_PATH, + headers: { + Authorization: `Bearer ${apiKey || ""}`, + "Content-Type": "application/json", + }, + body: buildBody(), + }); + + const handleRun = async () => { + setRunning(true); + setError(null); + setResult(undefined); + const t0 = performance.now(); + try { + const res = await fetch(ENDPOINT_PATH, { + method: "POST", + headers: { + Authorization: `Bearer ${apiKey}`, + "Content-Type": "application/json", + "x-connection-id": providerId, + }, + body: JSON.stringify(buildBody()), + }); + const data: unknown = await res.json(); + const latencyMs = performance.now() - t0; + const errMsg = extractError(data); + if (!res.ok || errMsg) { + setError(errMsg ?? `HTTP ${res.status}`); + } else { + setResult({ data, latencyMs }); + } + } catch (err) { + setError(err instanceof Error ? err.message : "Request failed"); + } finally { + setRunning(false); + } + }; + + const modelOptions = models.length > 0 ? models : [{ id: "text-embedding-3-small" }]; + + return ( + + {/* Model select */} +
+ + +
+ {/* Input text */} +
+ + setInputText(e.target.value)} + placeholder="Hello, world!" + className="w-full rounded-md border border-border bg-bg-subtle text-sm px-2 py-1.5 text-text-main focus:outline-none focus:ring-1 focus:ring-primary" + /> +
+
+ ); +} diff --git a/src/app/(dashboard)/dashboard/media-providers/components/ImageExampleCard.tsx b/src/app/(dashboard)/dashboard/media-providers/components/ImageExampleCard.tsx new file mode 100644 index 0000000000..2df7e2a28b --- /dev/null +++ b/src/app/(dashboard)/dashboard/media-providers/components/ImageExampleCard.tsx @@ -0,0 +1,168 @@ +"use client"; + +import { useState } from "react"; +import { useTranslations } from "next-intl"; +import { useApiKey } from "../../providers/hooks/useApiKey"; +import { useProviderModels } from "../../providers/hooks/useProviderModels"; +import { buildCurl } from "../../providers/utils/buildCurl"; +import { PlaygroundCard } from "./PlaygroundCard"; + +interface Props { + providerId: string; +} + +const ENDPOINT_PATH = "/api/v1/images/generations"; + +const IMAGE_SIZES = ["256x256", "512x512", "1024x1024", "1792x1024", "1024x1792"]; + +function extractError(data: unknown): string | null { + if (!data || typeof data !== "object") return null; + const d = data as Record; + const err = d.error as Record | undefined; + if (err?.message) return String(err.message); + if (typeof d.message === "string") return d.message; + return null; +} + +function ImageResultRenderer(data: unknown) { + if (!data || typeof data !== "object") return null; + const d = data as Record; + const items = Array.isArray(d.data) ? (d.data as Array>) : []; + if (items.length === 0) { + return
{JSON.stringify(data, null, 2)}
; + } + return ( +
+ {items.map((item, i) => { + const url = typeof item.url === "string" ? item.url : null; + const b64 = typeof item.b64_json === "string" ? item.b64_json : null; + const src = url ?? (b64 ? `data:image/png;base64,${b64}` : null); + if (!src) return null; + return ( + {`Generated + ); + })} +
+ ); +} + +export function ImageExampleCard({ providerId }: Props) { + const t = useTranslations("miniPlayground"); + const { apiKey } = useApiKey(); + const { models } = useProviderModels(providerId); + + const firstModel = models[0]?.id ?? "dall-e-3"; + const [model, setModel] = useState(""); + const [prompt, setPrompt] = useState("A serene landscape with mountains at sunset"); + const [size, setSize] = useState("1024x1024"); + const [running, setRunning] = useState(false); + const [result, setResult] = useState<{ data: unknown; latencyMs: number } | undefined>(); + const [error, setError] = useState(null); + + const effectiveModel = model || firstModel; + const buildBody = () => ({ model: effectiveModel, prompt, size, n: 1 }); + + const curlSnippet = buildCurl({ + endpoint: + (typeof window !== "undefined" ? window.location.origin : "http://localhost:20128") + + ENDPOINT_PATH, + headers: { + Authorization: `Bearer ${apiKey || ""}`, + "Content-Type": "application/json", + }, + body: buildBody(), + }); + + const handleRun = async () => { + setRunning(true); + setError(null); + setResult(undefined); + const t0 = performance.now(); + try { + const res = await fetch(ENDPOINT_PATH, { + method: "POST", + headers: { + Authorization: `Bearer ${apiKey}`, + "Content-Type": "application/json", + "x-connection-id": providerId, + }, + body: JSON.stringify(buildBody()), + }); + const data: unknown = await res.json(); + const latencyMs = performance.now() - t0; + const errMsg = extractError(data); + if (!res.ok || errMsg) { + setError(errMsg ?? `HTTP ${res.status}`); + } else { + setResult({ data, latencyMs }); + } + } catch (err) { + setError(err instanceof Error ? err.message : "Request failed"); + } finally { + setRunning(false); + } + }; + + const modelOptions = models.length > 0 ? models : [{ id: "dall-e-3" }]; + + return ( + + {/* Model */} +
+ + +
+ {/* Size */} +
+ + +
+ {/* Prompt */} +
+ +