Compare commits

...

157 Commits

Author SHA1 Message Date
diegosouzapw
1ffa58be76 feat(release): v2.0.3 — deferred tools cache_control fix, quota system hardening 2026-03-05 21:57:04 -03:00
Diego Rodrigues de Sa e Souza
a5cf51c0b9 Merge pull request #214 from DavyMassoneto/fix/claude-oauth-usage-endpoint
fix: harden quota system — code review fixes + build fix
2026-03-05 21:55:23 -03:00
Diego Rodrigues de Sa e Souza
3d38cbf70f Merge pull request #216 from DavyMassoneto/fix/defer-loading-cache-control-conflict
fix: skip cache_control on deferred tools + remove stale schemas.js
2026-03-05 21:55:14 -03:00
DavyMassoneto
196a4e037c fix: skip cache_control on deferred tools + remove stale schemas.js
- Skip tools with defer_loading=true when assigning cache_control
  (Anthropic API rejects the combination with 400)
- Delete stale schemas.js that shadowed the .ts source, causing
  missing cloudSyncActionSchema export

Fixes #215
2026-03-05 20:19:58 -03:00
diegosouzapw
6a0760a2c5 chore: bump to v2.0.2 (v2.0.1 was claimed on npm) 2026-03-05 20:11:15 -03:00
diegosouzapw
b0819404c7 feat(release): v2.0.1 — endpoint-aware models, 3 bug fixes (#212, #213, #200), 3 features (#204, #205, #206) 2026-03-05 20:04:41 -03:00
DavyMassoneto
bfe495931f fix(claude): correct utilization semantics, harden quota cache, fix premature model unavailability
- Fix inverted Claude OAuth utilization (remaining, not used)
- Add hasUtilization() guard to prevent false exhaustion from empty responses
- Centralize anthropic-version into CLAUDE_CONFIG.apiVersion
- Add parseDate() for safe date validation in quota cache
- Batch background refresh with MAX_CONCURRENT_REFRESHES=5
- Move setModelUnavailable to after all accounts exhausted, not first 429
- Extract safePercentage() to shared utils (dedup)
- Use isRecord() type guard in usage API route
- Exclude binary files from Tailwind v4 source scanning
2026-03-05 19:39:59 -03:00
DavyMassoneto
11bcdd810a feat: quota-aware account selection + fix premature model unavailability
- Move setModelUnavailable from per-account loop to all-accounts-exhausted path
- Clear model unavailability on successful fallback
- Add in-memory quota cache with background refresh (5min active, 20min exhausted)
- Integrate quota cache in account selection to skip exhausted accounts
- Mark accounts as exhausted from 429 when no cached quota data exists
- Populate quota cache from dashboard usage endpoint
2026-03-05 18:49:56 -03:00
diegosouzapw
228ebf436e feat: endpoint-aware model management + fix 3 bugs (#212, #213, #200)
Bug Fixes:
- #212: Auto-generate API_KEY_SECRET at startup (like JWT_SECRET)
- #213: Circuit breaker now scoped per-model instead of per-provider
- #200: Connectivity fallback for custom providers (Ollama, LM Studio)

Features:
- #204: API Format selector (Chat Completions / Responses API) for custom models
- #205: Combo endpoint field (chat / embeddings / images) in schema
- #206: Supported Endpoints checkboxes (chat, embeddings, images, audio)
- Custom models with endpoint tags appear in /v1/embeddings and /v1/images/generations
- Model catalog includes api_format, type, and supported_endpoints metadata
- Provider detail page shows badges for non-default endpoint configurations

Files changed: instrumentation.ts, combo.ts, validation.ts, models.ts,
schemas.ts, provider-models/route.ts, providers/[id]/page.tsx,
catalog.ts, embeddings/route.ts, images/generations/route.ts
2026-03-05 18:49:07 -03:00
diegosouzapw
d2b6624de4 Merge branch 'features-agente-mcp-a2a'
# Conflicts:
#	package-lock.json
2026-03-05 18:11:27 -03:00
diegosouzapw
0dd6d349b3 docs: consolidate v2.0.0 changelog — comprehensive release from v1.8.1
Merged all changes from the features-agente-mcp-a2a branch into a single
v2.0.0 release entry covering: MCP multi-transport (3 modes), 16 MCP tools,
A2A protocol, Auto-Combo engine, VS Code extension, consolidated Endpoints
dashboard with service toggles, 30-language i18n, full type safety overhaul,
and 1000+ tests.
2026-03-05 18:11:05 -03:00
diegosouzapw
ecb0774b7b feat(release): v2.0.1 — MCP multi-transport (stdio/SSE/Streamable HTTP), service toggles, endpoints consolidation
- MCP multi-transport: stdio, SSE (/api/mcp/sse), Streamable HTTP (/api/mcp/stream)
- Service enable/disable toggles with settings persistence (default: OFF)
- Consolidated Endpoints page with tabbed navigation
- Transport selector UI with connection URLs and Copy button
- Webpack barrel-file fix for settingsSchemas
2026-03-05 17:59:46 -03:00
diegosouzapw
6ab32b351f docs: update CHANGELOG and AGENTS.md with MCP multi-transport 2026-03-05 17:54:46 -03:00
diegosouzapw
e09d4a02a2 feat: add MCP multi-transport (stdio + SSE + Streamable HTTP)
- Created httpTransport.ts with singleton MCP server and WebStandard
  Streamable HTTP transport running inside Next.js process
- Added /api/mcp/sse route (GET+POST) for SSE transport
- Added /api/mcp/stream route (GET+POST+DELETE) for Streamable HTTP
- Added mcpTransport enum to settingsSchemas (stdio|sse|streamable-http)
- Updated /api/mcp/status to report HTTP transport state
- Added TransportSelector UI with mode buttons and connection URL display
- Routes guard against disabled MCP or wrong transport mode
2026-03-05 17:45:44 -03:00
diegosouzapw
3de8b4371a fix: extract updateSettingsSchema to bypass webpack barrel-file bug
- Created settingsSchemas.ts with updateSettingsSchema to avoid webpack
  tree-shaking the schema from the 908-line schemas.ts barrel file
- Updated settings/route.ts import to use new dedicated file
- Fixed focusRingColor lint error in ServiceToggle
2026-03-05 17:19:48 -03:00
diegosouzapw
396ab2bab5 feat: add MCP/A2A enable/disable toggle switches on Endpoints page
- Added mcpEnabled/a2aEnabled boolean fields to updateSettingsSchema
- Rewrote ServiceToggle as clickable on/off switch with status indicator
- Toggle persists state via PATCH /api/settings
- Both services default to disabled (OFF)
2026-03-05 16:56:04 -03:00
diegosouzapw
305fb56b62 docs: update AGENTS.md, README, CHANGELOG and all 30 i18n locales for Endpoints consolidation
- Rewrote AGENTS.md with v2.0.0 architecture (MCP, A2A, Auto-Combo, Endpoints tabs)
- Updated CHANGELOG unreleased section with Endpoints consolidation details
- Updated README references from Endpoint to Endpoints (screenshots, playbook, quickstart)
- Applied endpoints sidebar/header/namespace i18n to all 28 remaining language files
2026-03-05 16:51:11 -03:00
diegosouzapw
0f22f38f7e feat: consolidate Endpoint, MCP, A2A into tabbed Endpoints page
- Renamed sidebar 'Endpoint' to 'Endpoints', removed standalone MCP/A2A entries
- Created tabbed layout with SegmentedControl: Endpoint Proxy | MCP | A2A | API Endpoints
- Added inline ServiceToggle (online/offline status) for MCP and A2A tabs
- Created ApiEndpointsTab placeholder with Coming Soon badge
- Updated i18n in en.json and pt.json with new endpoints namespace
2026-03-05 16:36:58 -03:00
diegosouzapw
084b206ae6 fix: CORS headers on early-return error responses + auto-combo validation (#208, #209)
- Added CORS_HEADERS spread to 400/503 responses in chat/completions route
- Added createAutoComboSchema with Zod validation to /api/combos/auto
- Isolated JSON parsing errors with structured 400 response
- Prevented String(err) leakage on 500 errors
2026-03-05 15:56:17 -03:00
diegosouzapw
0d3728efa4 feat: Introduce combo readiness checks and strategy recommendations, updating i18n messages and e2e tests. 2026-03-05 14:38:03 -03:00
diegosouzapw
2b067c5d00 feat: Add i18n for new media and themes features, enhance combos with strategy guides and advanced settings, and introduce E2E tests for the combos flow. 2026-03-05 13:01:37 -03:00
diegosouzapw
21135407af feat: Introduce new A2A and MCP API routes, enhance dashboard UI, update READMEs, and add E2E tests. 2026-03-05 11:16:56 -03:00
diegosouzapw
c38a58fc98 chore: update lockfile to fix CI 2026-03-05 08:47:20 -03:00
Diego Rodrigues de Sa e Souza
20e4b1b011 Update open-sse/mcp-server/server.ts
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
2026-03-05 08:43:01 -03:00
Diego Rodrigues de Sa e Souza
9691469987 Update docs/openapi.yaml
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-05 08:42:21 -03:00
Diego Rodrigues de Sa e Souza
63114af08d Update src/app/api/auth/login/route.ts
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-05 08:42:04 -03:00
Diego Rodrigues de Sa e Souza
e78ede45b6 Potential fix for code scanning alert no. 54: Insecure randomness
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
2026-03-05 08:41:44 -03:00
diegosouzapw
751ff77b7c fix: extract validation helpers to fix webpack barrel-file resolution bug
Extracted validateBody, isValidationFailure, and loginSchema from the
935-line schemas.ts barrel file into a dedicated helpers.ts module.
Updated 70 API route files to import directly from helpers.ts.

Root cause: webpack on certain environments fails to resolve exports
from the bottom of large barrel files (schemas.ts), causing
'(0, O.Jb) is not a function' errors in production builds.

Fix: Split validation helpers into a small dedicated module (helpers.ts)
so webpack can correctly resolve all exports regardless of file size.

- TypeScript compiles with 0 errors
- All API routes updated to import from helpers.ts
- schemas.ts re-exports from helpers.ts for backward compatibility
2026-03-05 08:05:58 -03:00
diegosouzapw
5a53c17e81 feat: configurable tool name prefix (#199) and custom rpm/tpm rate limits (#198)
- Issue #199: proxy_ prefix on tool names is now automatically disabled
  when routing to non-Claude backends (Gemini, Compatible, etc.).
  Prevents tool name mismatches in OpenCode, Cursor, and other clients.

- Issue #198: Added customRpm/customTpm support per provider connection.
  Users can configure custom rate limits via connection settings,
  overriding the default auto-learned limits from response headers.
2026-03-05 01:41:34 -03:00
Diego Rodrigues de Sa e Souza
9c32c30daf Merge pull request #203 from DavyMassoneto/fix/claude-oauth-usage-endpoint
Approving — all review issues resolved. Claude OAuth usage endpoint with proper fallback to legacy API key method. Thanks @DavyMassoneto! 🎉
2026-03-05 01:26:52 -03:00
diegosouzapw
baa0208fa9 feat: v2.0.0 - MCP server, A2A agent, proxy improvements and docs update 2026-03-05 01:16:56 -03:00
DavyMassoneto
2ec0cd13cd fix(claude): correct utilization inversion and propagate remainingPercentage
- Fix createQuotaObject: utilization is percentage USED, not remaining
- Add optional chaining to window access in createQuotaObject
- Use typeof object guards for five_hour/seven_day instead of !== undefined
- Use nullish coalescing for extra_usage
- Propagate remainingPercentage in parseQuotaData claude case
2026-03-04 22:17:00 -03:00
diegosouzapw
0d8f28a4a4 feat: Introduce A2A lifecycle management, add type safety to ComfyUI and stream handling, and update various handlers and translators. 2026-03-04 21:02:56 -03:00
diegosouzapw
33dfbf0177 refactor: harden open-sse services, eliminate any casts, add dashboard pages
- Replace all `as any` casts in MCP advancedTools with typed helpers (toRecord, toString, toNumber)
- Harden open-sse services: rateLimitManager, sessionManager, usage, roleNormalizer, signatureCache, comboMetrics
- Improve responseSanitizer and responseTranslator type safety
- Remove deprecated openai-responses request translator
- Add dashboard pages: /a2a, /mcp, /auto-combo with live data
- Improve error/loading/not-found pages with consistent design
- Add root loading.tsx and typecheck tsconfig variants
- Add check-t11-any-budget.mjs audit script
2026-03-04 19:38:34 -03:00
diegosouzapw
85c6b63c8f feat: add error pages, harden DB layer and compliance module
- Add HTTP error pages (400, 401, 403, 408, 429, 500, 502, 503)
- Add maintenance, offline, and system status pages
- Harden db/core.ts, db/apiKeys.ts, db/cliToolState.ts, db/backup.ts
- Strengthen compliance/index.ts audit logging
- Improve container.ts DI registrations
- Fix dataPaths.ts and tokenHealthCheck.ts
2026-03-04 19:35:38 -03:00
DavyMassoneto
d19f336286 refactor: address PR review feedback
- Use parseResetTime() for resetAt fields (consistency with other fetchers)
- Extract createQuotaObject() helper to reduce duplication
- Remove unnecessary Content-Type header from GET requests
- Use CLAUDE_CONFIG.usageUrl with template interpolation in legacy fallback
2026-03-04 19:27:40 -03:00
diegosouzapw
052eb8d330 refactor: replace any types with generics and add Zod validation schemas
Eliminate `any` usage across the codebase by introducing proper generics,
typed interfaces (StatementLike, DbLike, PromptRow, etc.), and helper
conversion functions (toNumber, toString, parseVariables). Add
comprehensive Zod validation schemas for API endpoint inputs to enforce
runtime type safety alongside compile-time checks.
2026-03-04 18:59:27 -03:00
diegosouzapw
3510d8c0bc feat: add A2A protocol support and refactor API validation layer
Implement Agent-to-Agent (A2A) JSON-RPC 2.0 endpoint with smart routing
and quota management skills, SSE streaming, and task lifecycle management.

- Add /a2a route with message/send, message/stream, tasks/get, tasks/cancel
- Add /.well-known/agent.json agent card endpoint
- Introduce Zod-based request validation schemas for all v1 API routes
- Extract shared getUnifiedModelsResponse to reduce duplication across
  /models, /v1/models, and /a2a model listing
- Refactor chat, embeddings, moderations, and models routes to use
  centralized validation and error handling
- Add A2A task manager, routing logger, and streaming utilities
2026-03-04 18:52:25 -03:00
diegosouzapw
20860877b8 feat: add TypeScript types and modularize translator registry
- Add type annotations to all providerModels helper functions
- Introduce LegacyProvider interface in providerRegistry
- Refactor translator system to use self-registering module pattern
  with bootstrapTranslatorRegistry() and per-file imports
- Simplify translator/index.ts by delegating to modular translators
- Remove hardcoded Gemini OAuth client secret for security
2026-03-04 18:46:49 -03:00
diegosouzapw
bddec84f4e feat: add MCP server, A2A protocol, auto-combo engine & VS Code extension
Introduce full AI orchestration ecosystem:
- MCP Server with 16 tools, scoped auth, and audit logging
- A2A v0.3 server with JSON-RPC 2.0, SSE streaming, and task manager
- Auto-Combo engine with 6-factor scoring and self-healing
- VS Code extension with smart dispatch and budget tracking
- Harden CI pipeline: add static checks, remove continue-on-error
- Add translator schema validation tests
- Update .gitignore and CHANGELOG for release checklist
2026-03-04 18:45:02 -03:00
DavyMassoneto
aba12ad5db chore: sync package-lock.json with package.json v1.8.1 2026-03-04 18:29:05 -03:00
DavyMassoneto
6ea8d094b2 fix: invert utilization values — API returns remaining, not used
The OAuth usage endpoint returns utilization as percentage remaining,
not percentage used. Claude.ai showing 10% used corresponded to
utilization=90 from the API.
2026-03-04 18:19:17 -03:00
DavyMassoneto
b5a3a3d019 fix: use OAuth usage endpoint for Claude Code provider limits
The Limits page showed "error" 0% for Claude Code (OAuth) providers
because getClaudeUsage() called /v1/settings which requires API key
with org admin access — unavailable to consumer OAuth tokens.

Now uses https://api.anthropic.com/api/oauth/usage with the
anthropic-beta: oauth-2025-04-20 header, which returns five_hour and
seven_day utilization data for OAuth accounts.

Falls back to legacy /v1/settings endpoint for API key users.
2026-03-04 17:57:09 -03:00
diegosouzapw
5ecef5c90c feat: normalize quota and combos API responses with shared contracts
Introduce `normalizeQuotaResponse` and `normalizeCombosResponse` helpers
to handle varying API response shapes (array vs wrapped object)
consistently across MCP server and advanced tools. Add optional `meta`
field to checkQuotaOutput schema and update sourceEndpoints to reflect
current API routes.
2026-03-04 08:18:09 -03:00
diegosouzapw
fe9d9a5a5c feat: migrate tests to TypeScript and add MCP advanced tools test suite
- Add unit tests for 8 MCP advanced tool handlers (Phase 3)
- Migrate test files from JavaScript to TypeScript (.ts/.tsx)
- Restructure file paths from app/ to src/app/ across all tests
- Refactor route assertions into reusable assertRouteMethods helper
- Add tests for new API routes (compliance, audit-log, evals/[suiteId])
- Update barrel export tests to use consolidated assertion pattern
2026-03-04 00:41:30 -03:00
diegosouzapw
e18cfe1d80 feat: add Phase 3 advanced MCP tools and A2A smart routing skill
Register 8 new advanced MCP tools (simulate_route, set_budget_guard,
set_resilience_profile, test_combo, get_provider_metrics,
best_combo_for_task, explain_route, get_session_snapshot) with their
handler implementations. Add A2A smart routing skill that routes
prompts through the OmniRoute pipeline with routing explanation,
cost envelope, and resilience trace metadata.
2026-03-03 18:53:11 -03:00
diegosouzapw
7eb45b2e19 feat: add MCP server mode with --mcp flag for IDE integration
Add stdio-based MCP server support to OmniRoute CLI, enabling AI agents
in VS Code, Cursor, Claude Desktop, and Copilot to interact with
OmniRoute tools (health, combos, quota, routing). Update help text,
gitignore vscode-extension subproject, and include MCP/A2A strategy report.
2026-03-03 17:42:24 -03:00
diegosouzapw
70465ada4d feat(release): v1.8.1 — usage API proxy support 2026-03-03 12:06:42 -03:00
Diego Rodrigues de Sa e Souza
8ddea153d3 Merge pull request #195 from diegosouzapw/fix/issue-194-usage-proxy
fix: route usage API quota fetches through configured proxy (#194)
2026-03-03 12:05:55 -03:00
diegosouzapw
8dca8fba6b fix: route usage API quota fetches through configured proxy (#194) 2026-03-03 12:04:59 -03:00
diegosouzapw
f21ba7df64 feat(release): v1.8.0 — empty tool_use.name validation, Windows electron fix 2026-03-03 11:21:31 -03:00
Diego Rodrigues de Sa e Souza
ef917e42d1 Merge pull request #190 from benzntech/fix/electron-windows-collect-installers
fix: Windows electron release — collect portable exe by pattern
2026-03-03 11:19:53 -03:00
Diego Rodrigues de Sa e Souza
865a1b9b2c Merge pull request #193 from diegosouzapw/fix/issue-191-empty-tool-use-name
fix: validate empty tool_use.name to prevent Claude 400 errors (#191)
2026-03-03 11:19:45 -03:00
diegosouzapw
de8a0836a8 fix: validate empty tool_use.name to prevent Claude 400 errors (#191) 2026-03-03 11:18:53 -03:00
benzntech
b8272c55d7 fix: address review — break after first portable exe, remove debug ls 2026-03-03 09:27:31 +05:30
benzntech
8d93c13f9a fix: collect portable exe by pattern instead of hardcoded filename
electron-builder produces 'OmniRoute 1.6.9.exe' (with version) as the
portable exe, not 'OmniRoute.exe'. The hardcoded check failed, returning
exit code 1 and breaking every Windows build in the release workflow.

Now finds the portable exe by excluding 'Setup' (NSIS installer) and
blockmap files, then copies it as OmniRoute.exe for the release assets.
2026-03-03 09:20:24 +05:30
diegosouzapw
8152b030bf chore: bump version to 1.7.14 and update CHANGELOG 2026-03-02 19:18:38 -03:00
diegosouzapw
9352ac767f Merge PR #188: fix passthrough stream for Responses SSE (#186) 2026-03-02 19:17:38 -03:00
diegosouzapw
5f20029ff7 fix: make passthrough stream format-aware for Responses SSE (#186)
Passthrough mode now detects Responses SSE payloads (parsed.type starts
with 'response.') and skips Chat Completions-specific sanitization:
- sanitizeStreamingChunk() only runs on Chat Completions payloads
- fixInvalidId() and hasValuableContent() checks skipped for Responses
- Usage extraction still runs for both formats
- Content length tracking adapted for Responses delta format

This prevents potential stream corruption when Responses SSE data
triggers idFixed or other Chat Completions-specific rewrite conditions.
2026-03-02 19:13:34 -03:00
diegosouzapw
dbd00117c8 chore: bump to 1.7.13 (npm republish) 2026-03-02 18:51:24 -03:00
diegosouzapw
2902a0fe26 chore: bump version to 1.7.12 2026-03-02 18:47:07 -03:00
diegosouzapw
7ba57634c1 feat: add blackbox.ai to dashboard frontend (#175)
- Added blackbox provider to APIKEY_PROVIDERS in providers.ts
- Added blackbox pricing entries in pricing.ts
- Added blackbox to ProviderId typedef in types.ts
- Added blackbox models endpoint config in models/route.ts
2026-03-02 18:46:48 -03:00
diegosouzapw
211dde25d0 chore: bump version to 1.7.11 and update CHANGELOG 2026-03-02 18:33:08 -03:00
diegosouzapw
57ff59aef2 Merge PR #183: fix projectId warnings + add blackbox.ai provider (#175, #176)
- Added warning logs when generateProjectId() is used as fallback
- Prefer translator-set body.project before generating a new fallback
- Added blackbox.ai as OpenAI-compatible provider with 6 models + logo
- Includes improvement from Copilot PRs #184 and #185
2026-03-02 18:32:08 -03:00
Diego Rodrigues de Sa e Souza
c39faba2b5 Update open-sse/executors/antigravity.ts
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-02 18:22:49 -03:00
diegosouzapw
212bca2e1e fix: add projectId warning logs and blackbox.ai provider (#175, #176)
- Added warning logs when generateProjectId() is used as fallback
  in antigravity.ts and openai-to-gemini.ts (3 locations), helping
  admins diagnose 404 errors from fake GCP project IDs (#176)
- Added blackbox.ai as new OpenAI-compatible provider with 6 models
  (GPT-4o, Gemini 2.5 Flash, Claude Sonnet 4, DeepSeek V3,
  Blackbox AI, Blackbox AI Pro) and provider logo (#175)
2026-03-02 17:58:43 -03:00
Diego Rodrigues de Sa e Souza
f807c56e31 Merge pull request #182 from diegosouzapw/dependabot/npm_and_yarn/development-51b319602c
deps: bump the development group with 2 updates
2026-03-02 17:47:43 -03:00
Diego Rodrigues de Sa e Souza
5510c25040 Merge pull request #181 from diegosouzapw/dependabot/npm_and_yarn/production-d7c3d31362
deps: bump wreq-js from 2.0.1 to 2.1.1 in the production group
2026-03-02 17:47:26 -03:00
dependabot[bot]
9d884d2d60 deps: bump the development group with 2 updates
Bumps the development group with 2 updates: [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) and [lint-staged](https://github.com/lint-staged/lint-staged).


Updates `@types/node` from 25.3.0 to 25.3.3
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

Updates `lint-staged` from 16.2.7 to 16.3.1
- [Release notes](https://github.com/lint-staged/lint-staged/releases)
- [Changelog](https://github.com/lint-staged/lint-staged/blob/main/CHANGELOG.md)
- [Commits](https://github.com/lint-staged/lint-staged/compare/v16.2.7...v16.3.1)

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-version: 25.3.3
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development
- dependency-name: lint-staged
  dependency-version: 16.3.1
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: development
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-02 20:22:45 +00:00
dependabot[bot]
f26fa67374 deps: bump wreq-js from 2.0.1 to 2.1.1 in the production group
Bumps the production group with 1 update: [wreq-js](https://github.com/sqdshguy/wreq-js).


Updates `wreq-js` from 2.0.1 to 2.1.1
- [Release notes](https://github.com/sqdshguy/wreq-js/releases)
- [Commits](https://github.com/sqdshguy/wreq-js/compare/v2.0.1...v2.1.1)

---
updated-dependencies:
- dependency-name: wreq-js
  dependency-version: 2.1.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: production
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-02 20:22:16 +00:00
diegosouzapw
ccb314e065 feat: update agent workflows to use PR-based flow with user verification
Refactor resolve-issues and review-prs workflows to create branches
and PRs instead of committing directly. Add mandatory stop points
for user verification before merging, closing issues, or releasing.
Includes deploy step to local VPS after release.
2026-03-02 16:37:17 -03:00
diegosouzapw
0517dcf0b7 chore: bump version to 1.7.10 and update CHANGELOG 2026-03-02 15:31:38 -03:00
diegosouzapw
b7f0665ce9 fix: streaming tool calls missing id and wrong finish_reason in Responses→ChatCompletions translation (#180)
- Added tool_calls[].id and type:'function' to argument delta chunks
  so OpenAI-compatible clients can associate argument fragments with
  the correct tool call
- Changed finish_reason from hardcoded 'stop' to 'tool_calls' when
  tool calls were emitted (in flush, response.completed, and final chunk)
- Fixes state desync in agentic clients (OpenCode, Claude Code, etc.)
  where the assistant thinks tools already ran
2026-03-02 15:30:54 -03:00
diegosouzapw
144628755d chore: bump version to 1.7.9 and update CHANGELOG 2026-03-02 14:55:56 -03:00
diegosouzapw
7c5bb2c6b6 Merge PR #179: docs: update Cline URL to OpenClaw 2026-03-02 14:55:13 -03:00
diegosouzapw
afadb0fea1 Merge PR #178: fix: add JWT_SECRET env to electron release build step 2026-03-02 14:54:51 -03:00
MAINER4IK
b6c9c8a822 Change Cline to OpenClaw :)) 2026-03-02 22:28:01 +05:00
benzntech
11f43ca65c fix: add JWT_SECRET env to electron release build step
The Next.js build in electron-release.yml fails because the secrets
validator detects missing JWT_SECRET and exits with code 1. This adds
the env var to the build step, matching the pattern used in ci.yml.
2026-03-02 22:50:33 +05:30
diegosouzapw
8dce812a4d chore: bump version to 1.7.8 and update CHANGELOG 2026-03-02 12:14:58 -03:00
diegosouzapw
93047069b6 Merge PR #174: feat: add theme color settings and complete media/theme i18n 2026-03-02 12:13:53 -03:00
diegosouzapw
c4f1990aff fix: address agent review issues for theme color settings (#174)
Code Quality Improvements:
- Export COLOR_THEMES from themeStore.ts for reuse (DRY)
- Add coral preset to color list (fixes default inconsistency)
- Sync local customThemeColor state reactively via Zustand subscribe
- Add hex color validation with visual feedback (red border + disabled button)
- Remove dead /themes route from Header.tsx (page doesn't exist)
- Add CSS color-mix() fallback for older browsers
- Add themeCoral i18n key to all 30 locale files
2026-03-02 12:12:52 -03:00
mainer4ik
6e2816f08b feat: add theme color settings and complete media/theme i18n 2026-03-02 18:37:27 +05:00
diegosouzapw
227268024d chore: bump version to 1.7.7 and update CHANGELOG 2026-03-02 10:29:08 -03:00
diegosouzapw
8d5891a382 fix: sanitize tool schemas for Gemini provider (#173)
- Added cleanJSONSchemaForAntigravity() to openaiToGeminiBase() tool conversion
- Both OpenAI-format and Claude-format tool parameters are now sanitized
- Also sanitized response_format json_schema using the same function
- Removes unsupported JSON Schema keywords (additionalProperties, $schema, etc.)
- All Gemini paths (standard, CLI, Antigravity) now consistently sanitize schemas
2026-03-02 10:28:26 -03:00
diegosouzapw
7700fca501 chore: bump version to 1.7.6 and update CHANGELOG 2026-03-02 10:19:43 -03:00
diegosouzapw
527c542d6d fix: cloud proxy endpoint shows undefined/v1 when env var not set (#171)
- syncAndVerify now returns cloudUrl in API response for frontend to use
- EndpointPageClient uses dynamic cloudBaseUrl state instead of relying on env var
- Falls back gracefully when NEXT_PUBLIC_CLOUD_URL is not set (Docker deployments)
- Fixed setInterval in accountFallback.ts global scope for Cloudflare Workers compat
2026-03-02 10:18:21 -03:00
diegosouzapw
8fbae5e467 feat(release): v1.7.5 — OAuth re-auth duplicate fix (#170)
- Fixed OAuth re-auth creating duplicate connections instead of updating existing ones
- CHANGELOG.md updated with v1.7.5 section
- Version bumped to 1.7.5
2026-03-02 00:38:33 -03:00
diegosouzapw
4d2a5efd12 fix: OAuth re-auth now updates existing connection instead of creating duplicates (#170)
- Modified OAuth exchange route to use upsert logic at all 3 connection-save locations
- Before creating a new connection, checks for existing connections with same provider+email+authType
- If match found, calls updateProviderConnection() to refresh tokens instead of creating duplicate
- Falls back to createProviderConnection() for genuinely new connections
- Fixes: re-auth button creating new account entries instead of refreshing existing ones
2026-03-02 00:37:50 -03:00
diegosouzapw
5ffa14190a feat(release): v1.7.4 — OpenCode CLI integration, endpoint page restructure, i18n translations
- OpenCode CLI integration guide added to README (#169)
- Endpoint page restructured with 3 categories + Responses API & Models endpoints
- 21 new i18n keys (settings + endpoint) translated across 30 locales
- 30 translated READMEs synced with v1.7.3 features
- 3 new workflow files: update-docs, generate-release, issue-triage
2026-03-01 23:03:54 -03:00
diegosouzapw
7820145cbe docs: add OpenCode CLI integration guide (#169)
- Added OpenCode section to README.md CLI Integration with step-by-step instructions
- Uses @ai-sdk/openai-compatible adapter with custom opencode.json config
- Includes example models and baseURL configuration
- Closes #169
2026-03-01 22:59:10 -03:00
diegosouzapw
b7a6c563ac feat: add i18n translations for Model Aliases & Background Degradation + restructure Endpoint page
- Added 14 translated settings keys (modelAliasesTitle, backgroundDegradationTitle, enableDegradation, etc.) to all 30 locale files
- Added 7 translated endpoint keys (responsesDesc, listModelsDesc, categoryCore/Media/Utility) to all 30 locale files
- Restructured Endpoint page with 3 grouped categories: Core APIs, Media & Multi-Modal, Utility & Management
- Added Responses API (/v1/responses) and List Models (/v1/models) endpoint sections
- Fixed missing translation display issue where raw keys were shown instead of translated text
2026-03-01 22:50:07 -03:00
diegosouzapw
52221488d0 docs: sync all 30 language READMEs with v1.7.3 features + create workflow files
- Synced feature tables across all 28 translated READMEs (Model Aliases, Background Degradation, Rate Limit Persistence, Token Refresh Resilience)
- Updated 6 docs/i18n/*/FEATURES.md with new Settings description
- Created workflows: update-docs.md (with multi-language sync step), generate-release.md, issue-triage.md
2026-03-01 22:02:38 -03:00
diegosouzapw
4a1acb1446 feat(release): v1.7.3 — model deprecation, background degradation, rate limit persistence, thinking improvements, circuit breaker
Features:
- Model Deprecation Auto-Forward (10+ built-in aliases + custom via UI)
- Background Task Smart Degradation (19 patterns, degradation map)
- Rate Limit Persistence (SQLite, 60s debounce, 24h staleness)
- thinkingLevel string → budget conversion (high/medium/low/none)
- Claude -thinking model auto-injection
- Gemini 3.0/3.1 model registry distinction
- Token Refresh Circuit Breaker (5 failures → 30min cooldown)

Tests: 561 total (40+ new), 0 failures
2026-03-01 21:42:39 -03:00
diegosouzapw
dc90211222 feat: add /deploy-vps workflow for npm-based VPS deployment 2026-03-01 07:29:55 -03:00
diegosouzapw
378c9f321d docs: update CHANGELOG v1.7.2 and READMEs with new multi-modal features
- CHANGELOG: add new features section (multi-modal providers, media playground, unit tests, WFGY docs) and expand bug fixes
- README/README.pt-BR: add Video/Music to tagline, expand Pain Point #13 with all new modalities, update Multi-Modal APIs table with Video/Music Generation
2026-03-01 07:12:51 -03:00
diegosouzapw
e11bcc2848 feat: add unit tests for registryUtils, media playground page, TypeScript fixes
- 24 unit tests for parseModelFromRegistry, getAllModelsFromRegistry, buildAuthHeaders
- Integration tests for video/music registries
- Media Playground dashboard page (Image/Video/Music tabs with model selector)
- Sidebar navigation entry for Media page
- i18n translations (EN + PT-BR)
- Fix Record<string, any> → Record<string, unknown> in registryUtils.ts
- Update /resolve-issues workflow to wait for user validation before commit/release
2026-03-01 07:10:27 -03:00
Diego Rodrigues de Sa e Souza
3f10430150 Merge pull request #167 from ken2190/feat/new-providers-and-modalities
Approved — adds multi-modal support with new TTS/STT/Image/Video/Music providers. Follow-up commits will add frontend pages, unit tests, and configurable local provider URLs.
2026-03-01 07:03:14 -03:00
Diego Rodrigues de Sa e Souza
e8b72b54b3 Merge pull request #164 from onestardao/main
Approved — docs-only addition referencing the WFGY 16-problem RAG failure taxonomy in TROUBLESHOOTING.md.
2026-03-01 07:02:47 -03:00
Diego Rodrigues de Sa e Souza
d902dda4b1 Merge pull request #168 from benzntech/fix/electron-windows-shell
Approved — fixes Windows Electron release builds by adding `shell: bash` to the Collect installers step.
2026-03-01 07:02:39 -03:00
diegosouzapw
2538480b95 chore: release v1.7.2 — Gemini model import fix, Pino transport fallback 2026-03-01 06:49:27 -03:00
diegosouzapw
b9b8c93cb9 docs: enhance review-prs workflow with cross-layer analysis and thank-you step 2026-03-01 06:48:31 -03:00
diegosouzapw
68b7b35425 fix: log actual error and add sync pino.destination fallback (#165) 2026-03-01 06:48:02 -03:00
diegosouzapw
163c5feccc fix: strip models/ prefix from Gemini imported model IDs (#163) 2026-03-01 06:47:46 -03:00
Diego Rodrigues de Sa e Souza
0488f0536e Update docs/TROUBLESHOOTING.md
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
2026-03-01 06:33:22 -03:00
benzntech
09e90ec25b fix: use bash shell for Collect installers step on Windows runner 2026-03-01 14:25:22 +05:30
duongvdo
d97a11a54f fix: address code review issues (SSRF, saveCallLog, deduplication)
- Add path traversal validation for ElevenLabs voice_id and HuggingFace
  model_id URL concatenation (prevents SSRF via ../ sequences)
- Add saveCallLog usage tracking to video and music handlers for
  consistent analytics with imageGeneration.ts
- Extract shared upstreamErrorResponse() and audioStreamResponse()
  helpers to reduce error handling duplication in audioSpeech.ts
- Extract shared upstreamErrorResponse() and isValidPathSegment()
  helpers in audioTranscription.ts
- Add explicit format: "openai" to qwen TTS and STT provider entries
- Remove unused modelId parameter from handleCoquiSpeech and
  handleTortoiseSpeech
- Filter cloud video/music providers by active status in models route
  (local providers with authType: "none" always listed)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 14:34:10 +07:00
duongvdo
4a779dfe3c feat: add new providers & modalities (TTS, STT, Image, Video, Music)
Add 6 TTS providers (Nvidia NIM, ElevenLabs, HuggingFace, Coqui, Tortoise,
Qwen3), 3 STT providers (Nvidia NIM, HuggingFace, Qwen3), 2 local image
providers (SD WebUI, ComfyUI), and two new modalities — Text-to-Video
(/v1/videos/generations) and Text-to-Music (/v1/music/generations).

Key design decisions:
- Format-based unified providers: local providers grouped by API format
  (comfyui, sdwebui, coqui, tortoise, openai-compatible) with configurable
  base URLs and expandable model lists
- Cloud providers kept separate (unique auth and API shapes)
- Local providers use authType: "none" — credential checks bypassed at both
  route and handler level
- Shared ComfyUI client (comfyuiClient.ts) reused across image/video/music
- Shared registry utilities (registryUtils.ts) for model parsing and listing
- Qwen3 TTS/ASR use format: "openai" — no custom handler needed

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 13:31:29 +07:00
PSBigBig × MiniPS
61e09d545f docs: add optional RAG failure taxonomy to troubleshooting 2026-03-01 10:56:51 +08:00
diegosouzapw
3a68d7dabc fix: restore dashboard layout — Tailwind v4 @source for route groups
Tailwind CSS v4 auto-detection failed to scan Next.js route group
directories with parentheses (e.g. '(dashboard)'), causing responsive
grid utilities to be purged from production CSS. Added explicit @source
directives in globals.css to fix the content scanning.

CSS output: 10KB (broken) → 110KB (correct), 12 media queries restored.
2026-02-28 18:53:16 -03:00
diegosouzapw
22d318f201 chore(release): bump version to v1.7.0 2026-02-28 17:42:31 -03:00
diegosouzapw
afa2cea678 feat: 16 pain points docs, configurable User-Agent (#155), fix hardcoded $HOME (#156)
- Add collapsible '16 Real Pain Points' section to all 30 READMEs
- Fix 5 files bypassing dataPaths.ts with hardcoded os.homedir() (closes #156)
- Add per-provider User-Agent env var overrides in base executor (closes #155)
- Sync .env and .env.example with 9 provider UA defaults
- Update CHANGELOG.md for v1.7.0
2026-02-28 17:41:55 -03:00
diegosouzapw
6dce45505c chore(release): v1.6.9
- PR #160: CopilotToolCard URL fix + chat model filter (alpgul)
- PR #161: Proxy port preservation, credential encoding, cache invalidation (ken2190)
- CHANGELOG: v1.6.9 entry
- Version bump: 1.6.8 → 1.6.9
2026-02-28 16:29:25 -03:00
Diego Rodrigues de Sa e Souza
014732788c Merge pull request #161 from ken2190/fix/proxy-logic-and-docker-build
fix: preserve explicit proxy port and fix Docker build
2026-02-28 16:28:28 -03:00
Diego Rodrigues de Sa e Souza
0e75d838ab Merge pull request #160 from alpgul/fix/base-url-and-chat-filter
fix: improve API base URL handling and filter for chat models in CopilotToolCard
2026-02-28 16:28:26 -03:00
Alptekin Gülcan
8383da8a50 fix: improve API base URL handling and filter for chat models in CopilotToolCard 2026-02-28 19:10:17 +00:00
duongvdo
199d173816 fix: preserve explicit proxy port (80/443) instead of defaulting to 8080
The URL parser silently strips default ports (80 for HTTP, 443 for HTTPS)
when constructing URL objects. This caused proxy connections to use port
8080 instead of the user-specified port 80, resulting in connection
timeouts. Fix by extracting the port from the raw URL string before
parsing and building the normalized URL manually to avoid the serializer
stripping it.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 01:41:07 +07:00
diegosouzapw
f2829441f0 chore(release): v1.6.8
- Merged PR #159: Electron release workflow refactor (benzntech)
- Added app/ to .gitignore (Next.js App Router conflict fix)
- CHANGELOG: v1.6.8 entry
- Version bump: 1.6.7 → 1.6.8
2026-02-28 15:33:16 -03:00
diegosouzapw
21137bd84a fix: add app/ to .gitignore — prevents Next.js App Router conflict
The production standalone build directory (app/) created by scripts/prepublish.mjs
was conflicting with Next.js App Router detection. Next.js prioritizes root app/
over src/app/, causing all routes to return 404 in dev mode.

The package.json 'files' field still includes app/, so npm publish is unaffected.
2026-02-28 15:32:24 -03:00
Diego Rodrigues de Sa e Souza
a05e51a577 Merge pull request #159 from benzntech/fix/electron-release-filter
fix: filter Electron release assets to installers only
2026-02-28 15:30:59 -03:00
benzntech
09a094629c fix: include arm64 dmg in release assets
- Add explicit pattern for *-arm64.dmg files
- Fixes Kilo bot review: *.dmg doesn't match -arm64.dmg
2026-02-28 23:34:37 +05:30
benzntech
90de0fbf68 docs: add installation instructions with macOS Gatekeeper workaround 2026-02-28 23:21:39 +05:30
benzntech
c9cdd5109b feat: add Windows portable standalone exe
- NSIS installer: OmniRoute.Setup.X.Y.Z.exe (install to Program Files)
- Portable: OmniRoute.exe (run anywhere, no installation)
2026-02-28 23:19:20 +05:30
duongvdo
0e207dc5d2 fix: proxy logic bugs and Docker build failure
- URL-encode proxy credentials to handle special characters in passwords
- Decode URL-encoded credentials during legacy proxy migration
- Fix HTTPS proxy default port (443 instead of 8080) in frontend and migration
- Add dispatcher cache invalidation when proxy config changes
- Cast proxy port to number for SQLite INTEGER column in proxy logger
- Fix redundant .replace("//", "") in migration protocol parsing
- Copy postinstall script in Dockerfile before npm install

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 00:48:27 +07:00
benzntech
876a5a98f4 feat: add source code archives to releases
- Include .tar.gz and .zip of source code
- Uses git archive for clean export (excludes node_modules, build artifacts)
- Named: OmniRoute-vX.Y.Z.source.tar.gz / .zip
2026-02-28 23:16:43 +05:30
benzntech
8e82350d66 refactor: improve Electron release workflow
- Trigger on git tags (v*) instead of release.published
- Add manual workflow_dispatch for re-runs
- Add version validation step
- Use artifact upload/download pattern
- Single release job ensures all platforms complete first
- Prevents partial releases if one platform fails
2026-02-28 23:14:53 +05:30
benzntech
de75ed1551 fix: upload only installer files to releases
- Filter to *.dmg, *.exe, *.AppImage, *.blockmap only
- Prevents uploading unpacked app contents (DLLs, JS files, images)
2026-02-28 21:52:52 +05:30
benzntech
87266104a3 Merge origin/main - sync with v1.6.7 release 2026-02-28 21:52:38 +05:30
benzntech
fed8140404 chore: ignore electron build artifacts 2026-02-28 21:44:42 +05:30
diegosouzapw
e4d83e91bb chore(release): prepare release v1.6.7
- CHANGELOG: add v1.6.7 entry (Copilot Config Generator #142)
- FEATURES.md: add Copilot to CLI Tools section
- Version bump: 1.6.6 → 1.6.7 (package.json + electron/package.json)
2026-02-28 11:53:39 -03:00
diegosouzapw
a3153d893a feat: GitHub Copilot config generator for CLI Tools (#142)
Adds a Copilot configuration generator to the CLI Tools dashboard page.
Users can select models and generate the chatLanguageModels.json config
block for VS Code GitHub Copilot with the Azure vendor pattern.

Features:
- Bulk model selection from /v1/models (includes combos, custom, aliased)
- Search/filter for large model lists
- Configurable maxInputTokens, maxOutputTokens, toolCalling, vision
- One-click copy to clipboard
- Persistent model selection via localStorage
- Version compatibility warning (VS Code >= 1.109, Copilot >= v0.37)

Feedback from @alpgul applied:
- Use /v1/models instead of /api/models/alias (includes combo definitions)
- Use window.location.origin for URL (no port duplication in Docker)

Also: added electron/dist-electron/ to .gitignore (build artifact)
2026-02-28 11:31:55 -03:00
diegosouzapw
be219449f9 chore(release): bump version to v1.6.6 2026-02-28 11:17:26 -03:00
diegosouzapw
06d193f0d9 fix: prevent auth bypass after onboarding (#151)
The 'no password' auth bypass check was meant for fresh installs only,
but it also fired after onboarding was complete if the password row
was missing from the database (e.g. after DB migration in v1.6.3).

Fix: Added !settings.setupComplete guard so the bypass only applies
before onboarding is done. Once setupComplete=true, auth is always
required regardless of whether the password key exists in the DB.

Files changed:
- src/proxy.ts (dashboard middleware)
- src/shared/utils/apiAuth.ts (isAuthRequired)
2026-02-28 11:16:23 -03:00
diegosouzapw
4f413615d9 chore(release): prepare release v1.6.5
- Merge PR #154: official Electron icons and release workflow
- Fix electron-release.yml: npm ci → npm install (no package-lock.json)
- CHANGELOG: add v1.6.5 entry
- Version bump: 1.6.4 → 1.6.5
2026-02-28 10:27:58 -03:00
Benson
2a79b833fb feat(electron): add official icons and release workflow (#154)
* feat(electron): add app icons for Windows, macOS, and Linux releases

- icon.ico: Windows application icon (256x256 with multiple resolutions)
- icon.icns: macOS application icon bundle (16px to 1024px)
- icon.png: Linux/general purpose icon (512x512)
- tray-icon.png: System tray icon (32x32)

Icons generated from images/omniroute.png source logo.
Enables branded Electron desktop app builds for all platforms.

* chore: sync package-lock.json

* feat(electron): use official SVG logo and add release workflow

- Regenerated app icons from public/icon-192.svg (official OmniRoute logo)
- Added .github/workflows/electron-release.yml for automated builds
- Icons: icon.icns (macOS), icon.ico (Windows), icon.png (Linux), tray-icon.png
- Build workflow creates DMG (mac), EXE (win), AppImage (linux) on release

* ci: add npm cache to electron-release workflow
2026-02-28 10:26:46 -03:00
Diego Rodrigues de Sa e Souza
52e3d4b37b Merge pull request #153 from diegosouzapw/dependabot/npm_and_yarn/electron/npm_and_yarn-c9b74b4f42
chore(deps-dev): bump electron from 33.4.11 to 40.6.1 in /electron in the npm_and_yarn group across 1 directory
2026-02-28 10:21:25 -03:00
diegosouzapw
d9b393a308 docs: restructure all 30 READMEs — reorder sections, remove duplicates
Changes across README.md + 29 translations:
- Remove 🌐 English | Português (BR) language switcher from top
- Move Free AI Provider agents table below badges/links
- Move 📧 Support section right after agents table
- Move 💡 Key Features before 🎯 Use Cases
- Remove 📊 Available Models section
- Move 🔐 OAuth section inside Troubleshooting
- Remove entire 🇧🇷 Portuguese duplicate section at bottom
2026-02-28 09:56:32 -03:00
diegosouzapw
9a3d72c6a2 docs: update electron/README.md, USER_GUIDE.md, FEATURES.md with desktop app docs 2026-02-28 08:33:55 -03:00
dependabot[bot]
5b9b1cdd44 chore(deps-dev): bump electron
Bumps the npm_and_yarn group with 1 update in the /electron directory: [electron](https://github.com/electron/electron).


Updates `electron` from 33.4.11 to 40.6.1
- [Release notes](https://github.com/electron/electron/releases)
- [Commits](https://github.com/electron/electron/compare/v33.4.11...v40.6.1)

---
updated-dependencies:
- dependency-name: electron
  dependency-version: 40.6.1
  dependency-type: direct:development
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-28 11:16:30 +00:00
diegosouzapw
d624ddde03 fix(electron): code review hardening — 16 fixes for security, performance, robustness
## Critical Fixes
- #1: Server readiness — waitForServer() polls before loading window
- #2: Restart timeout — 5s + SIGKILL prevents IPC handler from hanging
- #3: changePort — now stops/restarts server on new port

## Important Fixes
- #4: Tray cleanup — destroy old Tray before recreating
- #5: IPC emission — server-status & port-changed events
- #6: Disposer pattern — replaces removeAllListeners
- #7: useSyncExternalStore — eliminates 5x re-renders

## Minor: #8-#16 (dead code, CSP, platform titlebar, types, errors, version)

Tests: 76 / 15 suites (was 64/9)
2026-02-28 08:15:04 -03:00
diegosouzapw
d3ace8d611 fix: security hardening, tests, docs for Electron desktop & memory optimization
## Security Fixes
- Sanitize OMNIROUTE_MEMORY_MB with parseInt + range validation (64-16384)
  to prevent command injection via spawn() args
- Validate URL protocol in shell.openExternal (http/https only)
  to prevent RCE in Electron renderer compromise
- Bump default memory from 256MB to 512MB

## Electron package-lock.json
- Added to .gitignore (5278 lines removed from tracking)

## Test Suite (64 tests, 9 suites)
- electron-main.test.mjs: URL validation, IPC channels, window handler
- electron-preload.test.mjs: channel whitelist, API surface, open-external
- cli-memory.test.mjs: injection prevention, boundary values, .env parsing

## Documentation
- Desktop App section added to all 30 READMEs (9 fully translated)
- USER_GUIDE.md updated with 512MB default
- .env.example reflects new defaults
2026-02-28 07:59:38 -03:00
Diego Rodrigues de Sa e Souza
177507bbc8 Merge pull request #150 from benzntech/feat/electron-desktop-app
feat: Electron desktop application
2026-02-28 07:52:31 -03:00
Diego Rodrigues de Sa e Souza
d915e2a868 Merge pull request #148 from benzntech/feat/memory-optimization-v2
feat: memory optimization for low-RAM configurations
2026-02-28 07:52:08 -03:00
benzntech
a3d15cf971 fix: address PR review comments
- Fix server restart logic to use event-based termination
- Use app.getPath('userData') for cross-platform data directory
- Implement VALID_CHANNELS validation in preload script
- Fix incorrect copy command in user guide
- Fix broken markdown table formatting

Refs: #150
2026-02-28 11:54:41 +05:30
benzntech
5e72cd34f0 fix: improve Electron security and functionality
- Add window control IPC handlers (minimize, maximize, close)
- Add URL validation for open-external to prevent security issues
- Add single instance lock to prevent multiple app instances
- Add deep link protocol support (omniroute://)

Refs: #149
2026-02-28 11:23:27 +05:30
benzntech
b004d0472b feat: add Electron desktop application support
- Add electron/ directory with main process, preload script, and types
- Add system tray integration and window management
- Add IPC communication for app info, external links, server controls
- Add useElectron React hooks for Next.js integration
- Add build scripts for Windows, macOS, and Linux
- Add development scripts for running Electron with Next.js

Ref: #149
2026-02-28 10:40:25 +05:30
benzntech
6ed98fb21c Merge branch 'main' of https://github.com/diegosouzapw/OmniRoute into feat/memory-optimization 2026-02-28 09:52:39 +05:30
diegosouzapw
d8bf4b1db8 chore(release): bump version to v1.6.3 2026-02-28 00:55:59 -03:00
diegosouzapw
fb2351ffe7 fix: sanitize hardcoded build-machine paths in standalone output (#147)
Next.js standalone bakes absolute build-time paths (outputFileTracingRoot,
appDir, turbopack root) into server.js and required-server-files.json.
When installed via npm on a different machine, these paths don't exist,
causing ENOENT errors. The prepublish script now replaces build-machine
absolute paths with '.' (relative) so they resolve correctly wherever
the package is installed.
2026-02-28 00:54:23 -03:00
diegosouzapw
12f7d2b484 fix: preserve database data on upgrade when old schema_migrations exists (#146)
Previously, the upgrade detection logic renamed the entire DB file when it
found a schema_migrations table (from older versions), causing data loss.
Now checks if the DB actually contains data (provider_connections) before
deciding to rename. If data exists, drops only the old migration tracking
table and lets the new CREATE TABLE IF NOT EXISTS schema take over.
2026-02-28 00:54:07 -03:00
diegosouzapw
2c40ef0964 chore(release): bump version to v1.6.2 2026-02-27 22:22:58 -03:00
diegosouzapw
ceb778a040 Merge branch 'feat/issue-121-provider-labels' into main (#121) 2026-02-27 22:21:46 -03:00
diegosouzapw
2c5a546759 chore(release): bump version to v1.6.1 2026-02-27 16:49:08 -03:00
diegosouzapw
3286f05b3b fix: auto-rebuild better-sqlite3 for cross-platform npm installs (#129)
The npm package ships with better-sqlite3 compiled for Linux x64.
On Windows and macOS, the binary is incompatible and fails to load.

Added postinstall script that:
- Detects if the bundled native module is incompatible
- Automatically runs 'npm rebuild better-sqlite3' to compile for the target platform
- Provides manual fallback instructions if rebuild fails
2026-02-27 16:48:28 -03:00
Diego Rodrigues de Sa e Souza
6a6a868845 Merge pull request #145 from diegosouzapw/dependabot/npm_and_yarn/minimatch-3.1.5
deps: bump minimatch from 3.1.2 to 3.1.5
2026-02-27 16:43:51 -03:00
dependabot[bot]
deb855f9d4 deps: bump minimatch from 3.1.2 to 3.1.5
Bumps [minimatch](https://github.com/isaacs/minimatch) from 3.1.2 to 3.1.5.
- [Changelog](https://github.com/isaacs/minimatch/blob/main/changelog.md)
- [Commits](https://github.com/isaacs/minimatch/compare/v3.1.2...v3.1.5)

---
updated-dependencies:
- dependency-name: minimatch
  dependency-version: 3.1.5
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-27 19:31:29 +00:00
diegosouzapw
d55b6e0b7a feat: show user-defined provider labels and full API key names (#121)
- Combo cards now resolve UUID-based provider IDs to user-defined names using providerNodes
- API key names shown in full instead of masked (Op***w → OpenAI-Prod-Key); key IDs remain masked
- Logs/Analytics: OAI-compatible and ANT-compatible providers now show user-defined names from providerNodes instead of generic OAI-COMPAT labels
- All views (combo cards, combos form modal, logs table, quick filters, dropdowns, analytics) updated consistently
2026-02-24 14:28:39 -03:00
benzntech
5a2fdacebe feat(memory): optimize RAM usage for low-memory deployments
- Add Node.js heap limit (256MB default) via OMNIROUTE_MEMORY_MB
- Convert LRU caches to byte-based limits (2-4MB)
- Reduce in-memory buffer sizes: 500→200 entries
- Add .env file loading in CLI for global npm installs
- Add PM2 ecosystem.config.js example for deployment
- Document memory tuning env vars in USER_GUIDE.md

Estimated RAM reduction: ~60-70% (from ~512MB+ to ~150-200MB)
2026-02-23 14:16:47 +05:30
460 changed files with 64786 additions and 12970 deletions

View File

@@ -0,0 +1,55 @@
---
description: Deploy the latest OmniRoute code to the Akamai VPS (69.164.221.35) via npm
---
# Deploy to VPS Workflow
Deploy OmniRoute to the production VPS using Node.js + PM2 (no Docker).
**VPS:** `69.164.221.35` (Akamai, Ubuntu 24.04, 1GB RAM + 2.5GB swap)
**App path:** `/opt/omniroute-app`
**Process manager:** PM2 (`omniroute`)
**Port:** `20128`
## Steps
### 1. Push to GitHub
Ensure all changes are committed and pushed:
```bash
git push origin main
```
### 2. SSH into VPS, pull latest code, rebuild, and restart
// turbo-all
```bash
ssh root@69.164.221.35 "
cd /opt/omniroute-app &&
git fetch origin &&
git reset --hard origin/main &&
export NODE_OPTIONS='--max-old-space-size=1536' &&
npm install --no-audit --no-fund &&
npm run build &&
pm2 restart omniroute &&
pm2 save &&
echo '✅ Deploy complete!'
"
```
### 3. Verify the deployment
```bash
ssh root@69.164.221.35 "pm2 list && curl -s -o /dev/null -w 'HTTP %{http_code}' http://localhost:20128/"
```
Expected: PM2 shows `online`, HTTP returns `307` (redirect to login).
## Notes
- The VPS has only 1GB RAM. `NODE_OPTIONS='--max-old-space-size=1536'` uses swap for the build.
- PM2 is configured with `pm2 startup` to auto-restart on reboot.
- The `.env` file is at `/opt/omniroute-app/.env` (copied from the old Docker setup at `/opt/omniroute/.env`).
- Nginx proxies `omniroute.online``localhost:20128`.

View File

@@ -0,0 +1,78 @@
---
description: Create a new release, bump version up to 1.x.10 threshold, update changelog, and manage Pull Requests
---
# Generate Release Workflow
Bump version, finalize CHANGELOG, commit, tag, push, publish to npm, and create GitHub release.
## Steps
### 1. Determine new version
Check current version in `package.json` and increment the patch number:
```bash
grep '"version"' package.json
```
Version format: `1.x.y` — increment `y` for patch, `x` for minor (threshold: y=10 triggers x+1).
### 2. Finalize CHANGELOG.md
Replace `[Unreleased]` header with the new version and date:
```markdown
## [1.x.y] — YYYY-MM-DD
```
### 3. Bump version in package.json
```bash
sed -i 's/"version": "OLD"/"version": "NEW"/' package.json
```
### 4. Stage, commit, and tag
// turbo-all
```bash
git add -A
git commit -m "feat(release): vX.Y.Z — summary of changes"
git tag -a vX.Y.Z -m "Release vX.Y.Z — summary"
```
### 5. Push to GitHub
```bash
git push origin main
git push origin vX.Y.Z
```
### 6. Publish to npm
```bash
npm publish
```
Wait for completion (prepublishOnly runs `npm run build:cli` automatically).
### 7. Create GitHub release
```bash
gh release create vX.Y.Z --title "Release vX.Y.Z" --notes-file /tmp/release_notes.md
```
### 8. Deploy to VPS (if requested)
See `/deploy-vps` workflow for Akamai VPS or use npm for local VPS:
```bash
ssh root@<VPS_IP> "npm install -g omniroute@X.Y.Z && pm2 restart omniroute"
```
## Notes
- Always run `/update-docs` BEFORE this workflow (ensures CHANGELOG and README are current)
- The `prepublishOnly` script runs `npm run build:cli` automatically during `npm publish`
- After npm publish, verify with `npm info omniroute version`

View File

@@ -0,0 +1,50 @@
---
description: How to respond to GitHub issues with insufficient information
---
# Issue Triage Workflow
Respond to GitHub issues that need more information before they can be investigated.
## Steps
### 1. Identify issues needing triage
```bash
gh issue list --state open --limit 20
```
### 2. Evaluate each issue
Check if the issue has:
- Clear reproduction steps
- Environment details (OS, Node.js version, OmniRoute version)
- Error logs/screenshots
- Expected vs actual behavior
### 3. Respond with triage template
For issues missing information:
```markdown
Thank you for reporting this issue! To help us investigate, please provide:
1. **OmniRoute version**: (`omniroute --version`)
2. **Node.js version**: (`node --version`)
3. **Operating system**: (e.g., Ubuntu 24.04, macOS 15, Windows 11)
4. **Installation method**: (npm, Docker, source)
5. **Steps to reproduce**: (exact commands/actions that trigger the issue)
6. **Error logs**: (paste relevant logs from the console)
7. **Expected behavior**: (what should happen)
This will help us debug and resolve your issue faster. 🙏
```
### 4. Label the issue
Add appropriate labels: `needs-info`, `bug`, `enhancement`, `question`, etc.
```bash
gh issue edit <NUMBER> --add-label "needs-info"
```

View File

@@ -1,12 +1,12 @@
---
description: Fetch all open GitHub issues, analyze bugs, resolve what's possible, triage the rest, then commit and release
description: Fetch all open GitHub issues, analyze bugs, resolve what's possible, triage the rest, wait for user validation, then commit and release
---
# /resolve-issues — Automated Issue Resolution Workflow
## Overview
This workflow fetches all open issues from the project's GitHub repository, classifies them, analyzes bugs, resolves what can be fixed, triages issues with insufficient information, and generates a release with all fixes.
This workflow fetches all open issues from the project's GitHub repository, classifies them, analyzes bugs, resolves what can be fixed, and triages issues with insufficient information. **It does NOT merge or release automatically** — it creates a PR and waits for user validation before merging.
## Steps
@@ -60,41 +60,61 @@ Call the `/issue-triage` workflow (located at `~/.gemini/antigravity/global_work
Proceed with resolution:
1. **Research** — Search the codebase for files related to the issue
2. **Root Cause** — Identify the root cause by reading the relevant source files
3. **Implement Fix** — Apply the fix following existing code patterns and conventions
4. **Test** — Build the project and run tests to verify the fix
5. **Commit**Commit with message format: `fix: <description> (#<issue_number>)`
1. **Create a fix branch**`git checkout -b fix/issue-<NUMBER>-<short-description>`
2. **Research** — Search the codebase for files related to the issue
3. **Root Cause** — Identify the root cause by reading the relevant source files
4. **Implement Fix** — Apply the fix following existing code patterns and conventions
5. **Test**Build the project and run tests to verify the fix
6. **Commit** — Commit with message format: `fix: <description> (#<issue_number>)`
### 5. Commit All Fixes
### 5. Generate Report & Wait for Validation
After processing all issues:
Present a summary report to the user via `notify_user` with `BlockedOnUser: true`:
- Ensure all fixes are committed with proper issue references
- Each fix should be its own commit for clean git history
| Issue | Title | Status | Action |
| ----- | ----- | ------------- | ----------------------------- |
| #N | Title | ✅ Ready | Files changed (not committed) |
| #N | Title | ❓ Needs Info | Triage comment posted |
| #N | Title | ⏭️ Skipped | Feature request / not a bug |
### 6. Close Resolved Issues
> **⚠️ IMPORTANT**: Do NOT commit, close issues, or generate releases at this step.
> Wait for the user to review the changes and respond with **OK** before proceeding.
For each successfully fixed issue:
// turbo
- If the user says **OK** or approves → Proceed to step 6
- If the user requests changes → Apply the requested adjustments first, then present the report again
- If the user rejects → Revert the changes and stop
- Close with a comment: `gh issue close <NUMBER> --repo <owner>/<repo> --comment "Fixed in <commit_hash>. The fix will be included in the next release."`
### 6. Commit & Push Fix Branch (only after user approval)
### 7. Generate Report
After the user validates:
Present a summary report to the user via `notify_user`:
- Commit each fix individually with message format: `fix: <description> (#<issue_number>)`
- Push the fix branch: `git push origin fix/issue-<NUMBER>-<short-description>`
- Create a PR: `gh pr create --title "fix: <description> (#<issue_number>)" --body "<details>" --base main`
| Issue | Title | Status | Action |
| ----- | ----- | ------------- | --------------------------- |
| #N | Title | ✅ Fixed | Commit hash |
| #N | Title | ❓ Needs Info | Triage comment posted |
| #N | Title | ⏭️ Skipped | Feature request / not a bug |
### 7. 🛑 WAIT — Notify User & Await PR Verification
### 8. Update Docs & Release
**This is a mandatory stop point.** Use `notify_user` with `BlockedOnUser: true`:
If any fixes were committed:
- Inform the user that the PR was created and is **awaiting their verification**
- Include the PR number, URL, and a summary of what was changed
- **DO NOT merge, close issues, generate releases, or deploy until the user confirms**
1. Run the `/update-docs` workflow (at `~/.gemini/antigravity/global_workflows/update-docs.md`) to update CHANGELOG and README
2. Run the `/generate-release` workflow (at `.agents/workflows/generate-release.md`) to bump version, tag, and publish
Wait for the user to respond:
- **User confirms** → Proceed to step 8
- **User requests changes** → Apply changes, push to the same branch, notify again
- **User rejects** → Close the PR and stop
### 8. Merge, Close Issues & Release (only after user confirms PR)
After the user confirms the PR:
1. **Merge** the PR: `gh pr merge <NUMBER> --merge --repo <owner>/<repo>` or via local merge
2. **Close** resolved issues with a comment: `gh issue close <NUMBER> --repo <owner>/<repo> --comment "Fixed in <commit_hash>. The fix will be included in the next release."`
3. **Switch to main**: `git checkout main && git pull`
4. Run the `/update-docs` workflow (at `~/.gemini/antigravity/global_workflows/update-docs.md`) to update CHANGELOG and README
5. Run the `/generate-release` workflow (at `.agents/workflows/generate-release.md`) to bump version, tag, and publish
6. Deploy to local VPS: `ssh root@192.168.0.15 "npm install -g omniroute@<VERSION> && pm2 restart omniroute"`
If NO fixes were committed, skip this step and just present the report.

View File

@@ -6,7 +6,7 @@ description: Analyze open Pull Requests from the project's GitHub repository, ge
## Overview
This workflow fetches all open PRs from the project's GitHub repository, performs a critical analysis of each one, generates a detailed report, and waits for user approval before proceeding with implementation.
This workflow fetches all open PRs from the project's GitHub repository, performs a critical analysis of each one, generates a detailed report, and waits for user approval before proceeding with implementation. **All improvements are committed on top of the PR branch** and the user must verify before merge.
## Steps
@@ -60,6 +60,21 @@ This workflow fetches all open PRs from the project's GitHub repository, perform
- Are edge cases covered?
- Would existing tests break?
#### 3f. Cross-Layer (Global) Analysis
Perform a **global impact assessment** to verify whether the PR changes are complete across all layers of the application:
- **Backend → Frontend check**: If the PR adds or modifies backend-only resources (new endpoints, services, data models), evaluate whether corresponding frontend changes are missing:
- Does a new endpoint require a new screen/page in the dashboard?
- Should there be a new action button, menu item, or navigation link?
- Are there new data fields that should be displayed or editable in the UI?
- Does a new feature need a toggle, configuration panel, or status indicator?
- **Frontend → Backend check**: If the PR adds frontend elements, verify the backend support exists:
- Are the required API endpoints implemented?
- Is the data model sufficient for the new UI components?
- **Cross-cutting concerns**: Check shared layers (types, DTOs, validation schemas, routes, middleware) for completeness
- **Document gaps** — If missing layers are detected, list them as **IMPORTANT** issues in the report with concrete suggestions for what should be added
### 4. Generate Report — Create a markdown report for each PR including:
- **PR Summary** — What it does, files affected, commit count
@@ -79,18 +94,52 @@ This workflow fetches all open PRs from the project's GitHub repository, perform
### 6. Implementation (if approved)
- Checkout the PR branch or apply changes locally
- Checkout the PR branch: `gh pr checkout <NUMBER>`
- Implement any required fixes identified in the analysis
- If the Cross-Layer Analysis (3f) identified missing frontend/backend counterparts, implement them
- **Commit improvements on top of the PR branch** with descriptive commit messages
- Run the project's test suite to verify nothing breaks
// turbo
- Run: `npm test` or equivalent test command
- Build the project to verify compilation
// turbo
- Run: `npm run build` or equivalent build command
- If all checks pass, prepare the merge
- Push the updated branch: `git push origin <branch-name>`
### 7. Post-Merge (if applicable)
### 7. 🛑 WAIT — Notify User & Await PR Verification
- Update CHANGELOG.md with the new feature
- Consider version bump if warranted
- Follow the `/generate-release` workflow if a release is needed
**This is a mandatory stop point.** Use `notify_user` with `BlockedOnUser: true`:
- Inform the user that the PR has been **improved and pushed**, and is **awaiting their verification**
- Include:
- PR number and URL
- Summary of improvements/fixes applied
- Build/test status
- List of files changed
- **DO NOT merge, generate releases, or deploy until the user confirms**
Wait for the user to respond:
- **User confirms** → Proceed to step 8
- **User requests more changes** → Apply changes, push to the same branch, notify again
- **User rejects** → Leave a review comment and stop
### 8. Thank the Contributor
- Post a **thank-you comment** on the PR via the GitHub API
- The message should:
- Thank the author by name/username for their contribution
- Briefly mention what the PR accomplishes and any improvements applied
- Be friendly, professional, and encouraging
- Example: _"Thanks @author for this great contribution! 🎉 The [feature/fix] is now merged and will be part of the next release. We appreciate your effort!"_
### 9. Merge & Release (only after user confirms PR)
After the user confirms the PR:
1. **Merge** the PR into main (local merge with `--no-ff` or via `gh pr merge`)
2. **Push** to main: `git push origin main`
3. **Clean up** the feature branch: `git branch -d <branch-name>`
4. **Update CHANGELOG.md** with the new feature/fix
5. Run the `/generate-release` workflow (at `.agents/workflows/generate-release.md`) to bump version, tag, and publish
6. Deploy to local VPS: `ssh root@192.168.0.15 "npm install -g omniroute@<VERSION> && pm2 restart omniroute"`

View File

@@ -0,0 +1,105 @@
---
description: How to automatically summarize recent changes and update README and CHANGELOG
---
# Update Documentation Workflow
Update CHANGELOG.md, README.md, docs/ files, and all multi-language translations whenever features are added or changed.
## Steps
### 1. Summarize recent changes
Review git log and identify new features, fixes, or changes since the last release tag:
```bash
git log $(git describe --tags --abbrev=0)..HEAD --oneline
```
### 2. Update English CHANGELOG.md
Add an `[Unreleased]` section (or version header if releasing) with:
- `### ✨ New Features` — each feature as a bullet point
- `### 🐛 Bug Fixes` — if applicable
- `### 🧪 Tests` — test count changes
- `### 📁 New Files` — table of new files with purpose
### 3. Update English README.md
Update the feature tables in these sections:
- **🧠 Routing & Intelligence** — for routing/model features
- **🛡️ Resilience & Security** — for security/resilience features
- **📊 Observability & Analytics** — for monitoring features
- **☁️ Deploy & Sync** — for deployment features
### 4. Update docs/ files
- `docs/FEATURES.md` — update the Settings section description
- `docs/API_REFERENCE.md` — add new API routes if any
- `docs/ARCHITECTURE.md` — update architecture if structural changes
### 5. 🌐 Sync Multi-Language Documentation (CRITICAL)
// turbo-all
**This step MUST be run after every README or docs update.**
The project has **30 language versions** of documentation:
**README files (root directory):**
```
README.md (English - source of truth)
README.pt-BR.md README.pt.md README.es.md README.fr.md README.it.md
README.de.md README.nl.md README.sv.md README.no.md README.da.md README.fi.md
README.ru.md README.uk-UA.md README.bg.md README.sk.md README.pl.md README.ro.md README.hu.md
README.ar.md README.he.md README.th.md README.in.md README.id.md README.ms.md README.vi.md
README.ja.md README.ko.md README.zh-CN.md README.phi.md
```
**docs/i18n/ directories (29 languages):**
```
docs/i18n/{ar,bg,da,de,es,fi,fr,he,hu,id,in,it,ja,ko,ms,nl,no,phi,pl,pt,pt-BR,ro,ru,sk,sv,th,uk-UA,vi,zh-CN}/
Each contains: API_REFERENCE.md, ARCHITECTURE.md, CODEBASE_DOCUMENTATION.md, FEATURES.md, TROUBLESHOOTING.md, USER_GUIDE.md
```
**Sync approach for feature table updates:**
a. Identify which feature table rows were added to English README.md
b. For each translated README, find the corresponding anchor lines:
- **Routing section:** Find the `💬` (System Prompt) table row — the line before it is always the last routing feature. Insert new routing features before System Prompt.
- **Resilience section:** Find the `📊` Rate Limits table row (the one in lines 590-600, NOT the quota tracking one in lines 560-570). Insert new resilience features after it.
c. The new feature entries can stay in English for technical features, matching the pattern used in the existing translations.
d. Use `sed` or similar tool to batch-insert across all 29 translated READMEs.
**Verification:**
```bash
# Verify all READMEs have the new features
grep -l "NEW_FEATURE_NAME" README.*.md | wc -l
# Should return 30 (all language versions)
```
**FEATURES.md sync:**
```bash
# Update Settings description in all docs/i18n/*/FEATURES.md
for dir in docs/i18n/*/; do
# Update the Settings section description to mention new features
# Check FEATURES.md in each directory
done
```
### 6. Verify documentation changes
```bash
# Check all modified files
git status --short
# Verify no broken markdown
# Optional: run markdownlint if available
```

View File

@@ -130,6 +130,22 @@ GEMINI_CLI_OAUTH_CLIENT_SECRET=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
# IFLOW_OAUTH_CLIENT_ID=
IFLOW_OAUTH_CLIENT_SECRET=4Z3YjXycVsQvyGF1etiNlIBB4RsqSDtW
# ─────────────────────────────────────────────────────────────────────────────
# Provider User-Agent Overrides (optional — customize per-provider UA headers)
# ─────────────────────────────────────────────────────────────────────────────
# Format: {PROVIDER_ID}_USER_AGENT=custom-value
# When set, overrides the default User-Agent header sent to that provider.
# Useful when providers update versions or block old user-agents.
CLAUDE_USER_AGENT=claude-cli/1.0.83 (external, cli)
CODEX_USER_AGENT=codex-cli/0.92.0 (Windows 10.0.26100; x64)
GITHUB_USER_AGENT=GitHubCopilotChat/0.26.7
ANTIGRAVITY_USER_AGENT=antigravity/1.104.0 darwin/arm64
KIRO_USER_AGENT=AWS-SDK-JS/3.0.0 kiro-ide/1.0.0
IFLOW_USER_AGENT=iFlow-Cli
QWEN_USER_AGENT=google-api-nodejs-client/9.15.1
CURSOR_USER_AGENT=connect-es/1.6.1
GEMINI_CLI_USER_AGENT=google-api-nodejs-client/9.15.1
# API Key Providers (Phase 1 + Phase 4)
# Add via Dashboard → Providers → Add API Key, or set here
# DEEPSEEK_API_KEY=
@@ -161,3 +177,24 @@ LOG_TO_FILE=true
# LOG_FILE_PATH=logs/application/app.log
# LOG_MAX_FILE_SIZE=50M
# LOG_RETENTION_DAYS=7
# ─────────────────────────────────────────────────────────────────────────────
# Memory Optimization (Low-RAM configurations)
# ─────────────────────────────────────────────────────────────────────────────
# Node.js heap limit in MB (default: 256 for Docker, system default for npm)
# OMNIROUTE_MEMORY_MB=256
# Prompt cache settings
# PROMPT_CACHE_MAX_SIZE=50
# PROMPT_CACHE_MAX_BYTES=2097152
# PROMPT_CACHE_TTL_MS=300000
# Semantic cache settings (temperature=0 responses)
# SEMANTIC_CACHE_MAX_SIZE=100
# SEMANTIC_CACHE_MAX_BYTES=4194304
# SEMANTIC_CACHE_TTL_MS=1800000
# In-memory log buffers
# PROXY_LOG_MAX_ENTRIES=200
# CALL_LOGS_MAX=200
# STREAM_HISTORY_MAX=50

View File

@@ -22,6 +22,12 @@ jobs:
cache: npm
- run: npm ci
- run: npm run lint
- run: npm run check:cycles
- run: npm run check:route-validation:t06
- run: npm run check:any-budget:t11
- run: npm run check:docs-sync
- run: npm run typecheck:core
- run: npm run typecheck:noimplicit:core
security:
name: Security Audit
@@ -127,7 +133,6 @@ jobs:
cache: npm
- run: npm ci
- run: npm run test:integration
continue-on-error: true
test-security:
name: Security Tests
@@ -144,4 +149,3 @@ jobs:
cache: npm
- run: npm ci
- run: npm run test:security
continue-on-error: true

177
.github/workflows/electron-release.yml vendored Normal file
View File

@@ -0,0 +1,177 @@
name: Build Electron Desktop App
on:
push:
tags:
- "v*"
workflow_dispatch:
inputs:
version:
description: "Release version (e.g., v1.6.8)"
required: true
type: string
permissions:
contents: write
jobs:
validate:
name: Validate version
runs-on: ubuntu-latest
outputs:
version: ${{ steps.validate.outputs.version }}
steps:
- name: Checkout code
uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Validate version format
id: validate
run: |
if [[ "${{ github.event_name }}" == "push" ]]; then
VERSION="${GITHUB_REF#refs/tags/}"
else
VERSION="${{ inputs.version }}"
fi
if [[ ! "$VERSION" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "Error: Invalid version format. Expected: v1.6.8"
exit 1
fi
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "✓ Valid version: $VERSION"
build:
name: Build Electron (${{ matrix.platform }})
needs: validate
runs-on: ${{ matrix.runner }}
strategy:
fail-fast: false
matrix:
include:
- platform: windows
runner: windows-latest
target: win
ext: .exe
- platform: macos-intel
runner: macos-latest
target: mac
ext: .dmg
- platform: macos-arm64
runner: macos-latest
target: mac
ext: -arm64.dmg
- platform: linux
runner: ubuntu-latest
target: linux
ext: .AppImage
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: 22
cache: npm
- name: Cache node_modules
uses: actions/cache@v4
with:
path: node_modules
key: ${{ runner.os }}-node-${{ hashFiles('package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-
- name: Install dependencies
run: npm ci
- name: Build Next.js standalone
env:
JWT_SECRET: ci-build-secret-with-sufficient-length-for-validation
run: npm run build
- name: Install Electron dependencies
working-directory: electron
run: npm install --no-audit --no-fund
- name: Build Electron for ${{ matrix.platform }}
working-directory: electron
run: npm run build:${{ matrix.target }}
- name: Collect installers
shell: bash
run: |
mkdir -p release-assets
cd electron/dist-electron
# Copy only installer files for this platform
for file in *${{ matrix.ext }}; do
[ -f "$file" ] && cp "$file" ../../release-assets/
done
# Windows: also copy portable standalone exe as OmniRoute.exe
if [ "${{ matrix.platform }}" = "windows" ]; then
for file in *.exe; do
# Skip the NSIS installer (contains "Setup")
case "$file" in *Setup*) continue ;; esac
[ -f "$file" ] && cp "$file" "../../release-assets/OmniRoute.exe" && break
done
fi
- name: Upload artifacts
uses: actions/upload-artifact@v4
with:
name: electron-${{ matrix.platform }}
path: release-assets/
release:
name: Create Release
needs: [validate, build]
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Download all artifacts
uses: actions/download-artifact@v4
with:
path: release-assets
merge-multiple: true
- name: Create source archives
run: |
# Create source code archives (excluding dev dependencies and build artifacts)
export TARBALL="OmniRoute-${{ needs.validate.outputs.version }}.source.tar.gz"
export ZIPBALL="OmniRoute-${{ needs.validate.outputs.version }}.source.zip"
# Use git archive for clean source export
git archive --format=tar.gz --prefix=OmniRoute-${{ needs.validate.outputs.version }}/ HEAD -o "release-assets/$TARBALL"
git archive --format=zip --prefix=OmniRoute-${{ needs.validate.outputs.version }}/ HEAD -o "release-assets/$ZIPBALL"
echo "✓ Created source archives:"
ls -lh "release-assets/$TARBALL" "release-assets/$ZIPBALL"
- name: List release files
run: ls -la release-assets/
- name: Create Release
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ needs.validate.outputs.version }}
draft: false
prerelease: false
generate_release_notes: true
files: |
release-assets/*.dmg
release-assets/*-arm64.dmg
release-assets/*.exe
release-assets/*.AppImage
release-assets/*.blockmap
release-assets/*.source.tar.gz
release-assets/*.source.zip
release-assets/OmniRoute.exe
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

20
.gitignore vendored
View File

@@ -63,6 +63,7 @@ docs/*
!docs/TASK_NEBIUS_BACKEND_ENABLEMENT.md
!docs/frontend-backend-provider-gap-report.md
!docs/openapi.yaml
!docs/RELEASE_CHECKLIST.md
!docs/PLANO-IMPLANTACAO.md
!docs/TASKS.md
!docs/FASE-*.md
@@ -102,3 +103,22 @@ app.log
# Backup directories
app.__qa_backup/
# Production standalone build (created by scripts/prepublish.mjs)
# Conflicts with Next.js App Router detection in dev (root app/ shadows src/app/)
# npm publish still includes it via package.json "files" field
/app/
# Electron (subproject dependency lock and build artifacts)
electron/package-lock.json
electron/dist-electron/
electron/node_modules/
icon.iconset/
# VS Code Extension (independent Git repo)
vscode-extension/
# SQLite residual files
*.sqlite-shm
*.sqlite-wal
*.sqlite-journal

View File

@@ -4,6 +4,7 @@
Unified AI proxy/router — route any LLM through one endpoint. Multi-provider support
(OpenAI, Anthropic, Gemini, DeepSeek, Groq, xAI, Mistral, Fireworks, Cohere, etc.)
with **MCP Server** (16 tools for agent control) and **A2A v0.3 Protocol** (Agent-to-Agent orchestration).
## Stack
@@ -13,6 +14,7 @@ Unified AI proxy/router — route any LLM through one endpoint. Multi-provider s
- **Streaming**: SSE via `open-sse` internal package
- **Styling**: Tailwind CSS v4
- **Docker**: Multi-stage Dockerfile, 3 profiles (base / cli / host)
- **i18n**: next-intl with 30 languages (`src/i18n/messages/`)
## Architecture
@@ -47,6 +49,56 @@ but the real logic lives in `src/lib/db/`.
Translation between provider formats: `open-sse/translator/`
### MCP Server (`open-sse/mcp-server/`)
16 tools for AI agent control via **3 transport modes**:
- **stdio** — Local IDE integration (Claude Desktop, Cursor, VS Code)
- **SSE** — Remote Server-Sent Events at `/api/mcp/sse`
- **Streamable HTTP** — Modern bidirectional HTTP at `/api/mcp/stream`
HTTP transports run in-process via `httpTransport.ts` singleton using `WebStandardStreamableHTTPServerTransport`.
| Category | Tools |
| ---------- | ------------------------------------------------------------------------------------------------------------------------- |
| Essential | `get_health`, `list_combos`, `get_combo_metrics`, `switch_combo`, `check_quota`, `route_request`, `cost_report`, `list_models_catalog` |
| Advanced | `simulate_route`, `set_budget_guard`, `set_resilience_profile`, `test_combo`, `get_provider_metrics`, `best_combo_for_task`, `explain_route`, `get_session_snapshot` |
- Scoped authorization (9 scopes), audit logging, Zod schemas
- IDE configs for Claude Desktop, Cursor, VS Code Copilot
### A2A Server (`src/lib/a2a/`)
Agent-to-Agent v0.3 protocol:
- JSON-RPC 2.0: `message/send`, `message/stream`, `tasks/get`, `tasks/cancel`
- Agent Card at `/.well-known/agent.json`
- Skills: `smart-routing`, `quota-management`
- SSE streaming with 15s heartbeat
- Task Manager with state machine and TTL-based cleanup
### Auto-Combo Engine (`open-sse/services/autoCombo/`)
Self-healing routing optimization:
- 6-factor scoring, 4 mode packs, bandit exploration
- Progressive cooldown, probe-based re-admission
### Dashboard (`src/app/(dashboard)/`)
| Page | Description |
| ---------------------------- | -------------------------------------------------------------- |
| `/dashboard` | Home with quick start, provider overview |
| `/dashboard/endpoint` | **Endpoints** (tabbed): Endpoint Proxy, MCP, A2A, API Endpoints |
| `/dashboard/providers` | Provider management and connections |
| `/dashboard/combos` | Combo configurations with routing strategies |
| `/dashboard/logs` | Request, Proxy, Audit, Console logs (tabbed) |
| `/dashboard/analytics` | Usage analytics and evaluations |
| `/dashboard/costs` | Cost tracking and breakdown |
| `/dashboard/health` | Uptime, circuit breakers, latency |
| `/dashboard/cli-tools` | CLI tool integrations (Claude, Codex, Antigravity, etc.) |
| `/dashboard/media` | Image, Video, Music generation playground |
| `/dashboard/settings` | System settings with multiple tabs |
| `/dashboard/api-manager` | API key management with model permissions |
### OAuth & Tokens (`src/lib/oauth/`)
18 modules handling OAuth flows, token refresh, and provider credentials.
@@ -76,7 +128,7 @@ overridable via env vars or `data/provider-credentials.json`.
- No hardcoded API keys or secrets in commits
- Auth middleware on all API routes
- Input validation on user-facing endpoints
- Input validation on user-facing endpoints (Zod schemas)
- SQLite encryption key must not be logged
### Architecture
@@ -85,6 +137,7 @@ overridable via env vars or `data/provider-credentials.json`.
- Provider requests flow through `open-sse/handlers/`
- Translations use `open-sse/translator/` modules
- `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module
- MCP and A2A pages are embedded as tabs inside `/dashboard/endpoint`, not standalone routes
### Code Quality
@@ -92,6 +145,7 @@ overridable via env vars or `data/provider-credentials.json`.
- Proper HTTP status codes
- No memory leaks in SSE streams (abort signals, cleanup)
- Rate limit headers must be parsed correctly
- All API inputs validated with Zod schemas
### Docker

View File

@@ -7,6 +7,571 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
---
## [2.0.3] — 2026-03-05
> ### 🐛 Bug Fixes & Quota System Hardening
### 🐛 Bug Fixes
- **#215 — Deferred tools 400 error** — Skip `cache_control` on tools with `defer_loading=true` when assigning prompt caching to the last tool. Previously, the API rejected requests with 400 when MCP tools (Playwright, etc.) had deferred loading enabled. Fix applied in both `claudeHelper.ts` and `openai-to-claude.ts` translation layers. PR #216 by @DavyMassoneto
- **Stale compiled schemas.js** — Deleted stale compiled `schemas.js` (912 lines) that shadowed the TypeScript `.ts` source, causing `cloudSyncActionSchema` warnings in the dashboard. PR #216 by @DavyMassoneto
- **#202 — False quota exhaustion** — Fixed empty API responses (`{}`) creating quota objects with `utilization ?? 0` = 0% remaining, incorrectly marking accounts as exhausted. Added `hasUtilization()` guard. PR #214 by @DavyMassoneto
- **Invalid date crash** — `parseDate()` now validates dates before comparison, handling `Invalid Date` from malformed `resetAt` values gracefully. PR #214 by @DavyMassoneto
- **`total=0` false infinite quota** — `normalizeQuotas` now defaults to 0% remaining when `total` is zero (was incorrectly reporting 100%). PR #214 by @DavyMassoneto
- **Tailwind v4 build failure** — Tailwind v4 scanned `*.sqlite-shm`/`.sqlite-wal` binary files, triggering "Invalid code point" errors. Added `@source not` exclusions in `globals.css`. PR #214 by @DavyMassoneto
### ✨ Improvements
- **Quota-aware account selection** — All load-balancing strategies (sticky, round-robin, p2c, random, least-used, cost-optimized, fill-first) now prioritize accounts with available quota over exhausted ones. PR #214 by @DavyMassoneto
- **Concurrent refresh protection** — `tickRunning` flag prevents overlapping background quota refresh ticks; `refreshingSet` deduplicates per-connection refreshes. Thundering herd prevention with `MAX_CONCURRENT_REFRESHES=5`. PR #214 by @DavyMassoneto
- **`clearModelUnavailability` on success** — Model unavailability is now cleared on every successful request, not only on fallback paths. PR #214 by @DavyMassoneto
- **Centralized `anthropic-version`** — Hardcoded `anthropic-version` header (3 occurrences) centralized into `CLAUDE_CONFIG.apiVersion`. PR #214 by @DavyMassoneto
- **Extracted `safePercentage()` utility** — Shared percentage validation function extracted to `src/shared/utils/formatting.ts`, eliminating duplication between backend and frontend. PR #214 by @DavyMassoneto
- **`isRecord()` type guard** — Replaces inline `typeof` chain in usage API route. PR #214 by @DavyMassoneto
### 📁 Files Changed
| File | Change |
| ------------------------------------------------------------------------------------- | ---------------------------------------------------------- |
| `open-sse/translator/helpers/claudeHelper.ts` | Skip `cache_control` on deferred tools |
| `open-sse/translator/request/openai-to-claude.ts` | Same fix in translator layer |
| `src/shared/validation/schemas.js` | **DELETED** — stale compiled JS |
| `.gitignore` | Exclude Tailwind binary scanning |
| `open-sse/services/usage.ts` | Legacy endpoint fallback logging |
| `src/domain/quotaCache.ts` | **NEW** — Core quota cache with hardening |
| `src/shared/utils/formatting.ts` | **NEW**`safePercentage()` utility |
| `src/instrumentation.ts` | Startup log for quota cache |
| `src/sse/handlers/chat.ts` | `clearModelUnavailability` + `markAccountExhaustedFrom429` |
| `src/sse/services/auth.ts` | Quota-aware account selection |
| `src/app/globals.css` | Tailwind `@source not` exclusions |
| `src/app/api/usage/[connectionId]/route.ts` | `isRecord()` type guard |
| `src/app/(dashboard)/dashboard/usage/components/ProviderLimits/ProviderLimitCard.tsx` | Use `remainingPercentage` directly |
| `src/app/(dashboard)/dashboard/usage/components/ProviderLimits/utils.tsx` | Use shared `safePercentage()` |
---
## [2.0.2] — 2026-03-05
> ### 🐛 Bug Fixes & ✨ Endpoint-Aware Model Management
### 🐛 Bug Fixes
- **#212 — API Key creation crash** — Auto-generate `API_KEY_SECRET` at startup (like `JWT_SECRET`) to prevent HMAC crashes
- **#213 — Circuit breaker scope** — Changed circuit breaker key from provider-level to model-level; a 429 on one account no longer blocks all accounts for the same provider
- **#200 — Custom provider connection check** — Added connectivity fallback for OpenAI-compatible providers (Ollama, LM Studio); if `/models` and `/chat/completions` fail, a simple HTTP ping to the base URL marks the provider as connected
### ✨ New Features
- **#204 — API Format selector** — Custom models can now specify `apiFormat`: `chat-completions` (default) or `responses` (for the Responses API)
- **#205 — Combo endpoint support** — Combos now accept an `endpoint` field in the schema (`chat` | `embeddings` | `images`), enabling fallback/rotation combos for non-chat endpoints
- **#206 — Supported Endpoints mapping** — When adding custom models, users can check which endpoints the model supports (💬 Chat, 📐 Embeddings, 🖼️ Images, 🔊 Audio). Models tagged for embeddings appear in `/v1/embeddings` and models tagged for images appear in `/v1/images/generations`
- **Visual badges** — Model rows now display colored badges for non-default API formats and endpoint types
- **Model catalog metadata** — `/v1/models` response includes `api_format`, `type`, and `supported_endpoints` for custom models
### 📁 Files Changed
| File | Change |
| ------------------------------------------------------- | ------------------------------------------------ |
| `src/instrumentation.ts` | Auto-generate `API_KEY_SECRET` |
| `open-sse/services/combo.ts` | Circuit breaker keyed per-model |
| `src/lib/providers/validation.ts` | Connectivity fallback ping |
| `src/lib/db/models.ts` | `apiFormat` + `supportedEndpoints` fields |
| `src/shared/schemas/validation.ts` | `endpoint` in `comboSchema` |
| `src/shared/validation/schemas.ts` | Extended `providerModelMutationSchema` |
| `src/app/api/provider-models/route.ts` | Pass new fields through API |
| `src/app/(dashboard)/dashboard/providers/[id]/page.tsx` | API format dropdown, endpoint checkboxes, badges |
| `src/app/api/v1/models/catalog.ts` | Custom model metadata enrichment |
| `src/app/api/v1/embeddings/route.ts` | Include custom embedding models |
| `src/app/api/v1/images/generations/route.ts` | Include custom image models |
---
## [2.0.0] — 2026-03-05
> ### 🚀 Major Release — MCP Multi-Transport, A2A Protocol, Auto-Combo Engine & Full Type Safety Overhaul
>
> **OmniRoute 2.0** transforms the AI gateway into a fully **agent-controllable platform**. AI agents can now discover, orchestrate, and optimize routing through 16 MCP tools (via 3 transports: stdio, SSE, Streamable HTTP) or the A2A v0.3 protocol. Accompanied by a self-healing Auto-Combo engine, VS Code extension, consolidated Endpoints dashboard with service toggles, and a comprehensive type safety overhaul across the entire codebase.
### 🔌 MCP Multi-Transport (3 Modes)
- **stdio** — Local transport for IDE integration (Claude Desktop, Cursor, VS Code Copilot). Launched via `omniroute --mcp`
- **SSE (Server-Sent Events)** — Remote HTTP transport at `/api/mcp/sse` (GET+POST). Runs in-process inside Next.js
- **Streamable HTTP** — Modern bidirectional HTTP transport at `/api/mcp/stream` (GET+POST+DELETE). Uses `WebStandardStreamableHTTPServerTransport` singleton
- **Transport Selector UI** — When MCP is enabled, a transport picker shows all 3 modes with connection URLs and a Copy button
- **Settings Persistence** — `mcpTransport` field in settings API (enum: `stdio` | `sse` | `streamable-http`)
### 🆕 MCP Server (16 Tools)
- **8 Essential Tools** — `get_health`, `list_combos`, `get_combo_metrics`, `switch_combo`, `check_quota`, `route_request`, `cost_report`, `list_models_catalog`
- **8 Advanced Tools** — `simulate_route`, `set_budget_guard`, `set_resilience_profile`, `test_combo`, `get_provider_metrics`, `best_combo_for_task`, `explain_route`, `get_session_snapshot`
- **Scoped Authorization** — 9 permission scopes (`read:health`, `read:combos`, `read:quota`, `read:usage`, `read:models`, `execute:completions`, `write:combos`, `write:budget`, `write:resilience`) with wildcard support
- **Audit Logging** — Every tool call logged to SQLite with SHA-256 input hashing, output summarization, and duration tracking
- **IDE Configs** — MCP configuration templates for Claude Desktop, Cursor, VS Code Copilot, and stdio transport
- **Type-Safe Schemas** — All 16 tools defined with Zod input/output schemas, descriptions, and scope declarations
- 📖 **Documentation** — [`open-sse/mcp-server/README.md`](open-sse/mcp-server/README.md) with architecture, tool reference, and client examples in Python, TypeScript, and Go
### 🤖 A2A Server (Agent-to-Agent v0.3)
- **JSON-RPC 2.0** — Full router with `message/send`, `message/stream`, `tasks/get`, `tasks/cancel`
- **Agent Card** — Dynamic `/.well-known/agent.json` with 2 skills and bearer auth
- **Skills** — `smart-routing` (routing explanation, cost envelope, resilience trace, policy verdict) and `quota-management` (natural language quota queries with ranking, free combo suggestions, and full summaries)
- **SSE Streaming** — Real-time task streaming with 15s heartbeat, chunk events, and completion metadata
- **Task Manager** — State machine (`submitted``working``completed`/`failed`/`cancelled`), TTL (5min default), auto-cleanup (2× TTL)
- **Routing Logger** — Decision audit trail with 7-day retention and routing statistics
- **Task Execution** — Generic executor with proper state transitions on success/failure
- 📖 **Documentation** — [`src/lib/a2a/README.md`](src/lib/a2a/README.md) with JSON-RPC methods, skill reference, client examples, and MCP vs A2A comparison
### ⚡ Auto-Combo Engine
- **6-Factor Scoring** — Quota, health, costInv, latencyInv, taskFit, stability (normalized 0-1)
- **Task Fitness Table** — 30+ models × 6 task types with wildcard boosts
- **4 Mode Packs** — Ship Fast, Cost Saver, Quality First, Offline Friendly
- **Self-Healing** — Progressive cooldown exclusion, probe-based re-admission, incident mode (>50% OPEN)
- **Bandit Exploration** — 5% exploratory routing for discovering better providers
- **Adaptation Persistence** — EMA scoring with disk persistence every 10 decisions
- **REST API** — `POST/GET /api/combos/auto` for CRUD operations
### 🎛️ Consolidated Endpoints Dashboard
- **Tabbed Navigation** — Merged standalone Endpoint, MCP, and A2A sidebar entries into a single **"Endpoints"** page using `SegmentedControl`. Four tabs: **Endpoint Proxy**, **MCP**, **A2A**, **API Endpoints**
- **Service Enable/Disable Toggles** — MCP and A2A tabs have clickable ON/OFF toggle switches with settings persistence (default: OFF)
- **Service Status Indicators** — Inline status badges (green "Online" / red "Offline") with 30s auto-refresh
- **API Endpoints Tab** — Placeholder page with "Coming Soon" badge, listing planned features: REST API catalog, webhooks, OpenAPI/Swagger spec, and per-endpoint auth management
- **Sidebar Cleanup** — Removed standalone MCP and A2A entries; renamed "Endpoint" to "Endpoints"
### 🧩 VS Code Extension — Advanced Features
- **MCP Client** — 16 tool wrappers with REST API fallback
- **A2A Client** — Agent discovery, message send/stream, task management
- **Smart Dispatch** — Task type detection, combo recommendation, risk scoring
- **Preflight Dialog** — Risk-based display (auto-skip low, info medium, modal high)
- **Budget Guard** — Session cost tracking with status bar indicator and threshold actions
- **Mode Pack Selector** — Quick-pick UI for switching optimization profiles
- **Health Monitor** — Circuit breaker state change notifications
- **Human Checkpoint** — Multi-factor confidence evaluation with handoff dialog
### 📊 Dashboard Pages
- **MCP Dashboard** — Tool listing, usage stats, audit log with 30s auto-refresh
- **A2A Dashboard** — Agent Card display, skill listing, task history with routing metadata
- **Auto-Combo Dashboard** — Provider score bars, factor breakdown, mode pack selector, incident indicator, exclusion list
- **Error Pages** — Custom error and not-found pages for the dashboard
### 🔗 Integrations
- **OpenClaw** — Dynamic `provider.order` endpoint at `/api/cli-tools/openclaw/auto-order`
- **Configurable Tool Name Prefix** — `TOOL_NAME_PREFIX` env var for custom MCP tool naming (#199)
- **Custom RPM/TPM Rate Limits** — Per-provider rate limit overrides (#198)
- **CORS Fix** — CORS headers on early-return error responses (#208)
- **Auto-Combo Validation** — Proper validation for auto-combo CRUD operations (#209)
### 🌐 i18n (30 Languages)
- **Endpoints Namespace** — Added `endpoints` i18n namespace with tab labels, toggle labels, and API Endpoints page translations across all 30 locales
- **Sidebar & Header Updates** — Updated sidebar key from `endpoint` to `endpoints` and header breadcrumb descriptions across all 30 locales
- **Media & Themes i18n** — Added media section and combo strategy guide translations across all 30 locales
### 🔧 Code Quality & Type Safety
- **Eliminated `any` types** — Replaced `any` casts across `open-sse/` services, translators, and handlers with proper generics and explicit types
- **Zod Validation Schemas** — Added Zod-based validation for all MCP tool inputs/outputs and API validation layer
- **Shared Contracts** — Normalized quota and combos API responses with shared contracts (`src/shared/contracts/quota.ts`) for consistent data shapes across MCP, A2A, and REST APIs
- **TypeScript Translator Types** — Added strict types and modularized the translator registry with proper interfaces
- **DB Layer Hardening** — Improved database layer with proper error handling and type safety in the compliance module
- **A2A Lifecycle Safety** — Enhanced A2A task lifecycle with type-safe state transitions, preventing invalid state changes on completed tasks
- **Stream Handling** — Improved ComfyUI and stream handling with proper type safety
- **Webpack Barrel-File Fix** — Extracted `updateSettingsSchema` into dedicated `settingsSchemas.ts` to bypass webpack tree-shaking bug
- **Security Fix** — Insecure randomness fix for code scanning alert #54
### 🧪 Tests
- **E2E Test Suite** — 6 scenarios covering MCP, A2A, Auto-Combo, OpenClaw, Stress (100+50 parallel), Security
- **Unit Tests** — Essential tools (139 tests), advanced tools (141 tests), Auto-Combo engine (162 tests), A2A lifecycle regression tests
- **Schema Hardening Tests** — `t06-schema-hardening.test.mjs` (132 tests) for input validation
- **Security Tests** — `t07-no-log-key-config.test.mjs` (138 tests), `t08-mcp-scope-enforcement.test.mjs` (72 tests)
- **Integration Tests** — `v1-contracts-behavior.test.mjs` (171 tests), `security-hardening.test.mjs` (103 tests)
- **Migrated Tests to TypeScript** — E2E ecosystem tests migrated from `.mjs` to `.ts` with proper typing
- **Combo E2E Tests** — Strategy guides, advanced settings, readiness checks
### 📝 Documentation
- **AGENTS.md** — Updated to v2.0.0 with MCP multi-transport, A2A Protocol, Auto-Combo Engine, consolidated Endpoints dashboard, and Zod validation references
- **README.md** — Updated Agent & Protocol feature table with 3 transport modes, consolidated endpoints, and service toggles
- **30 Translated READMEs** — Synced feature tables across all language versions
- **CHANGELOG.md** — Comprehensive release notes covering all v1.8.1 → v2.0.0 changes
### 📁 New Files (60+)
| Directory | Files |
| :------------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `open-sse/mcp-server/` | `server.ts`, `index.ts`, `audit.ts`, `scopeEnforcement.ts`, `httpTransport.ts`, `tools/advancedTools.ts`, `README.md` |
| `open-sse/mcp-server/schemas/` | `tools.ts`, `a2a.ts`, `audit.ts`, `index.ts` |
| `src/lib/a2a/` | `taskManager.ts`, `taskExecution.ts`, `streaming.ts`, `routingLogger.ts`, `README.md` |
| `src/lib/a2a/skills/` | `smartRouting.ts`, `quotaManagement.ts` |
| `src/app/a2a/` | `route.ts` (JSON-RPC 2.0 dispatch handler) |
| `src/app/api/mcp/sse/` | `route.ts` (SSE transport endpoint) |
| `src/app/api/mcp/stream/` | `route.ts` (Streamable HTTP transport endpoint) |
| `open-sse/services/autoCombo/` | `scoring.ts`, `taskFitness.ts`, `engine.ts`, `selfHealing.ts`, `modePacks.ts`, `persistence.ts`, `index.ts` |
| `src/shared/contracts/` | `quota.ts` (shared API contracts) |
| `src/shared/constants/` | `mcpScopes.ts` |
| `src/shared/validation/` | `settingsSchemas.ts` (extracted settings Zod schema) |
| `src/lib/db/migrations/` | `002_mcp_a2a_tables.sql` |
| `src/app/(dashboard)/` | `dashboard/mcp/page.tsx`, `dashboard/a2a/page.tsx`, `dashboard/auto-combo/page.tsx`, `dashboard/endpoint/ApiEndpointsTab.tsx` |
| `vscode-extension/src/services/` | `mcpClient.ts`, `a2aClient.ts`, `policyEngine.ts`, `preflightDialog.ts`, `budgetGuard.ts`, `healthMonitor.ts`, `modePackSelector.ts`, `humanCheckpoint.ts` |
| `scripts/` | `check-cycles.mjs`, `check-docs-sync.mjs`, `check-route-validation.mjs`, `check-t11-any-budget.mjs`, `run-playwright-tests.mjs`, `runtime-env.mjs` |
| `tests/` | `t06-schema-hardening.test.mjs`, `t07-no-log-key-config.test.mjs`, `t08-mcp-scope-enforcement.test.mjs`, `ecosystem.test.ts` |
| `docs/` | `mcp-server.md`, `a2a-server.md`, `auto-combo.md`, `vscode-extension.md`, `integrations/ide-configs.md`, `RELEASE_CHECKLIST.md` |
### 📝 Commit History (`features-agente-mcp-a2a` branch)
| Commit | Date | Description |
| :-------- | :--------- | :--------------------------------------------------------------------------------------- |
| `e0ddb22` | 2026-03-03 | feat: add MCP server mode with `--mcp` flag for IDE integration |
| `09a1748` | 2026-03-03 | feat: add Phase 3 advanced MCP tools and A2A smart routing skill |
| `1e1a9c9` | 2026-03-04 | feat: migrate tests to TypeScript and add MCP advanced tools test suite |
| `ab77452` | 2026-03-04 | feat: normalize quota and combos API responses with shared contracts |
| `88ad4cc` | 2026-03-04 | feat: add MCP server, A2A protocol, auto-combo engine & VS Code extension |
| `cc429d4` | 2026-03-04 | feat: add TypeScript types and modularize translator registry |
| `adc8fdf` | 2026-03-04 | feat: add A2A protocol support and refactor API validation layer |
| `500cae3` | 2026-03-04 | refactor: replace `any` types with generics and add Zod validation schemas |
| `889e2ba` | 2026-03-04 | feat: add error pages, harden DB layer and compliance module |
| `cbd0b1c` | 2026-03-04 | refactor: harden open-sse services, eliminate any casts, add dashboard pages |
| `b33a853` | 2026-03-04 | feat: Introduce A2A lifecycle management, add type safety to ComfyUI and stream handling |
| `a1a2610` | 2026-03-04 | feat: v2.0.0 - MCP server, A2A agent, proxy improvements and docs update |
| `d615ca5` | 2026-03-05 | feat: configurable tool name prefix (#199) and custom rpm/tpm rate limits (#198) |
| `6d8868b` | 2026-03-05 | fix: extract validation helpers to fix webpack barrel-file resolution bug |
| `bc2e60c` | 2026-03-05 | feat: Introduce new A2A and MCP API routes, enhance dashboard UI, E2E tests |
| `79c23df` | 2026-03-05 | feat: Add i18n for media/themes, enhance combos with strategy guides, E2E tests |
| `2490ba5` | 2026-03-05 | feat: Introduce combo readiness checks and strategy recommendations |
| `48dda26` | 2026-03-05 | fix: CORS headers on early-return error responses + auto-combo validation (#208, #209) |
| `078a42b` | 2026-03-05 | feat: consolidate Endpoint, MCP, A2A into tabbed Endpoints page |
| `6f1e6a0` | 2026-03-05 | feat: add MCP/A2A enable/disable toggle switches on Endpoints page |
| `bb9d85b` | 2026-03-05 | fix: extract updateSettingsSchema to bypass webpack barrel-file bug |
| `cc7e1a0` | 2026-03-05 | feat: add MCP multi-transport (stdio + SSE + Streamable HTTP) |
---
## [1.8.1] — 2026-03-03
### 🐛 Bug Fixes
- **Usage API Proxy Support** — Quota/usage fetch calls (`/api/usage/[connectionId]`) now route through the dashboard-configured proxy (Global → Provider → Key level). Previously, usage fetchers used bare `fetch()` which bypassed the Global Proxy setting, causing "fetch failed" errors in Docker deployments behind a proxy. Fixes #194
## [1.8.0] — 2026-03-03
### 🐛 Bug Fixes
- **Empty `tool_use.name` Validation** — Fixed intermittent HTTP 400 errors when using Claude Code through OmniRoute. Assistant messages with empty `tool_use.name` fields (from interrupted tool calls or malformed history) are now validated and filtered at two layers: the `openai-to-claude` request translator and the `prepareClaudeRequest` sanitizer. Fixes #191
- **Windows Electron Release** — Fixed the "Collect installers" step failing in every Windows build since v1.7.5+. `electron-builder` produces versioned portable exe filenames (e.g., `OmniRoute 1.6.9.exe`), not the hardcoded `OmniRoute.exe` the workflow expected. Now finds the portable exe dynamically by pattern. PR #190 by @benzntech
## [1.7.14] — 2026-03-02
### 🐛 Bug Fixes
- **Responses SSE Passthrough** — Passthrough mode is now format-aware: Responses SSE payloads (`response.*` type) skip Chat Completions-specific sanitization (`sanitizeStreamingChunk`, `fixInvalidId`, `hasValuableContent`), preventing potential stream corruption for Responses-native clients. Usage extraction still works for both formats. Fixes #186
### ✨ Features
- **Blackbox AI Dashboard** — Added blackbox.ai provider to the dashboard frontend (providers page, pricing, models endpoint). Completes #175
## [1.7.11] — 2026-03-02
### ✨ Features
- **Blackbox AI Provider** — Added blackbox.ai as a new OpenAI-compatible provider with 6 default models (GPT-4o, Gemini 2.5 Flash, Claude Sonnet 4, DeepSeek V3, Blackbox AI, Blackbox AI Pro) and provider logo. Fixes #175
### 🐛 Bug Fixes
- **Antigravity 404 Error** — Added warning logs when `generateProjectId()` generates a fallback project ID because `credentials.projectId` is null. The executor now prefers the translator-set `body.project` before generating a new fallback, eliminating duplicate warnings and ID mismatch. Fixes #176. Includes improvements from PRs #184 and #185
## [1.7.10] — 2026-03-02
### 🐛 Bug Fixes
- **Streaming Tool Calls (Responses→ChatCompletions)** — Fixed two issues in the `openaiResponsesToOpenAIResponse` translator that broke tool call execution in agentic clients (OpenCode, Claude Code, Cursor, etc.): (1) Argument delta chunks now include `tool_calls[].id` and `type: "function"` so clients can associate argument fragments correctly. (2) `finish_reason` is now `"tool_calls"` instead of hardcoded `"stop"` when tool calls occurred. Fixes #180
## [1.7.9] — 2026-03-02
### 🐛 Bug Fixes
- **Electron CI Build** — Added `JWT_SECRET` environment variable to the Electron release workflow `Build Next.js standalone` step, fixing build failures in GitHub Actions. PR #178 by @benzntech
### 📝 Documentation
- **README** — Updated OpenClaw link from `cline/cline` to `openclaw/openclaw` to reflect the project rename. PR #179 by @MAINER4IK
## [1.7.8] — 2026-03-02
### ✨ New Features
- **Theme Color Customization** — Users can now select from 7 preset accent colors (Coral, Blue, Red, Green, Violet, Orange, Cyan) or define a custom color via color picker/hex input. The chosen color dynamically updates `--color-primary` and `--color-primary-hover` CSS variables across the entire UI. PR #174 by @mainer4ik
### 🌐 Multi-Language Sync
- **Theme & Media i18n** — Added `themeCoral`, `themeBlue`, `themeRed`, `themeGreen`, `themeViolet`, `themeOrange`, `themeCyan`, `themeAccent`, `themeAccentDesc`, `themeCustom`, `themeCreate`, and media section translations across all **30 language locales**
### 🔧 Code Quality (Review Improvements)
- Exported `COLOR_THEMES` constant from `themeStore.ts` for DRY reuse
- Added hex color validation with visual feedback (red border + disabled apply button)
- Synced local state via Zustand `subscribe` pattern for cross-tab consistency
- Removed dead `/themes` route from Header.tsx
- Added CSS `color-mix()` fallback for older browsers
## [1.7.7] — 2026-03-02
### 🐛 Bug Fixes
- **Gemini Tool Schema Sanitization** — The standard Gemini provider now sanitizes OpenAI tool schemas before forwarding to Gemini API, removing unsupported JSON Schema keywords (`additionalProperties`, `$schema`, `const`, `default`, `not`, etc.). Previously, sanitization only ran in the CLI executor path, causing Gemini to reject tool calls when schemas contained unsupported constraints. Also applied sanitization to `response_format.json_schema`. Fixes #173
## [1.7.6] — 2026-03-02
### 🐛 Bug Fixes
- **Cloud Proxy `undefined/v1` Fix** — When the `NEXT_PUBLIC_CLOUD_URL` environment variable is not set (common in Docker deployments), the endpoint page now correctly falls back instead of showing `undefined/v1`. The cloud sync API now returns `cloudUrl` in its response so the frontend can use it dynamically. Fixes #171
### ✨ New Features
- **Cloud Worker `/v1/models` Endpoint** — The Cloud Worker now supports the `/v1/models` endpoint for both URL formats (`/v1/models` and `/{machineId}/v1/models`), returning all available models synced from the local OmniRoute instance
### 🔧 Infrastructure
- **Cloudflare Workers Compatibility** — Fixed `setInterval` in global scope issue in `accountFallback.ts` that blocked Cloud Worker deployment. Lazy initialization pattern ensures compatibility with Cloudflare Workers runtime restrictions
## [1.7.5] — 2026-03-02
### 🐛 Bug Fixes
- **OAuth Re-Auth Duplicate Fix** — Re-authenticating an expired OAuth connection now updates the existing connection instead of creating a duplicate entry. When re-auth is triggered, the system matches by `provider` + `email` + `authType` and refreshes tokens in-place. Fixes #170
## [1.7.4] — 2026-03-01
### ✨ New Features
- **OpenCode CLI Integration** — Added full integration guide for [OpenCode](https://opencode.ai) AI CLI tool using `@ai-sdk/openai-compatible` adapter with custom `opencode.json` config. Resolves #169
- **Endpoint Page Restructured** — Reorganized the Endpoint dashboard page into 3 grouped categories (Core APIs, Media & Multi-Modal, Utility & Management) with visual dividers. Added 2 new endpoint sections: **Responses API** (`/v1/responses`) and **List Models** (`/v1/models`)
- **Model Aliases & Background Degradation i18n** — Added 14 translated settings keys and 7 translated endpoint keys across all **30 language locales**. Fixed missing translations showing raw keys like `settings.modelAliasesTitle` in the UI
### 🌐 Multi-Language Sync
- **30 README translations synced** — All 28 translated READMEs updated with v1.7.3 feature entries (Model Aliases, Background Degradation, Rate Limit Persistence, Token Refresh Resilience)
- **6 docs/i18n FEATURES.md updated** — Settings description expanded in da, it, nl, phi, pl, sv
### 📁 New Files
| File | Purpose |
| --------------------------------------- | ----------------------------------------------------------- |
| `.agents/workflows/update-docs.md` | Documentation update workflow with multi-language sync step |
| `.agents/workflows/generate-release.md` | Release generation workflow (version bump, npm, GitHub) |
| `.agents/workflows/issue-triage.md` | Issue triage workflow for issues with insufficient info |
## [1.7.3] — 2026-03-01
### ✨ New Features
- **Model Deprecation Auto-Forward** — New `modelDeprecation.ts` service with 10+ built-in aliases for legacy Gemini, Claude, and OpenAI models. Deprecated model IDs (e.g., `gemini-pro`, `claude-2`) are automatically forwarded to their current replacements. Custom aliases configurable via new Settings → Routing → Model Aliases UI tab with full CRUD API (`/api/settings/model-aliases`)
- **Background Task Smart Degradation** — New `backgroundTaskDetector.ts` service detects background/utility requests (title generation, summarization, etc.) via 19 system prompt patterns and `X-Request-Priority` header, and automatically reroutes them to cheaper models. Configurable degradation map and detection patterns via new Settings → Routing → Background Degradation UI tab. Disabled by default (opt-in)
- **Rate Limit Persistence** — Learned rate limits from API response headers are now persisted to SQLite with 60-second debouncing and restored on startup (24h staleness filter). Rate limits survive server restarts instead of being lost in memory
- **thinkingLevel String Conversion** — `applyThinkingBudget()` now handles string-based `thinkingLevel` inputs (`"high"`, `"medium"`, `"low"`, `"none"`) by converting them to numeric token budgets. Supports `thinkingLevel`, `thinking_level`, and Gemini's `generationConfig.thinkingConfig.thinkingLevel` fields
- **Claude -thinking Model Auto-Injection** — Models ending with `-thinking` suffix (e.g., `claude-opus-4-6-thinking`) automatically get thinking parameters injected to prevent API errors. `hasThinkingCapableModel()` updated to recognize these suffixes
- **Gemini 3.0/3.1 Model Registry** — Updated provider registry to explicitly distinguish Gemini 3.1 (Pro, Flash) from 3.0 Preview variants across `gemini`, `gemini-cli`, and `antigravity` providers with clear naming conventions
- **Token Refresh Circuit Breaker** — Per-provider circuit breaker in `refreshWithRetry()`: 5 consecutive failures trigger a 30-minute cooldown to prevent infinite retry loops. Added 30-second timeout wrapper per refresh attempt. Exported `isProviderBlocked()` and `getCircuitBreakerStatus()` for diagnostics
### 🧪 Tests
- **40+ new unit tests** across 3 files: `model-deprecation.test.mjs` (14 tests), `background-task-detector.test.mjs` (14 tests), extended `thinking-budget.test.mjs` (+13 tests). Total suite: **561 tests, 0 failures**
### 📁 New Files
| File | Purpose |
| ---------------------------------------------------------------------- | --------------------------------------------------------------------- |
| `open-sse/services/modelDeprecation.ts` | Model deprecation alias resolver with built-in + custom aliases |
| `open-sse/services/backgroundTaskDetector.ts` | Background task detection with pattern matching and model degradation |
| `src/app/api/settings/model-aliases/route.ts` | CRUD API for model alias management |
| `src/app/api/settings/background-degradation/route.ts` | API for background degradation config |
| `src/app/(dashboard)/settings/components/ModelAliasesTab.tsx` | Settings UI for model alias management |
| `src/app/(dashboard)/settings/components/BackgroundDegradationTab.tsx` | Settings UI for background degradation |
| `tests/unit/model-deprecation.test.mjs` | 14 unit tests for model deprecation |
| `tests/unit/background-task-detector.test.mjs` | 14 unit tests for background task detection |
---
## [1.7.2] — 2026-03-01
### ✨ New Features
- **Multi-Modal Provider Support** — Added 6 TTS providers (ElevenLabs, Nvidia NIM, HuggingFace, Coqui, Tortoise, Qwen3), 3 STT providers, 2 image providers (SD WebUI, ComfyUI), and two new modalities: `/v1/videos/generations` (Text-to-Video) and `/v1/music/generations` (Text-to-Music). Shared abstractions via `registryUtils.ts` and `comfyuiClient.ts` ([PR #167](https://github.com/diegosouzapw/OmniRoute/pull/167) by @ken2190)
- **Media Playground Page** — New dashboard page at `/dashboard/media` with tabbed interface (Image/Video/Music), model selector, prompt input, and JSON result viewer
- **Unit Tests for Registry Utils** — 24 tests covering `parseModelFromRegistry`, `getAllModelsFromRegistry`, `buildAuthHeaders`, and integration with video/music registries
- **WFGY 16-Problem RAG Failure Map** — Added troubleshooting reference for RAG/LLM failure taxonomy in `docs/TROUBLESHOOTING.md` ([PR #164](https://github.com/diegosouzapw/OmniRoute/pull/164) by @onestardao)
### 🐛 Fixed
- **Gemini Imported Models Return 404** — Strip `models/` prefix from Gemini model IDs during import to prevent doubled paths ([#163](https://github.com/diegosouzapw/OmniRoute/issues/163))
- **Pino File Transport Fails in Next.js Production** — Log actual error + add sync `pino.destination()` fallback ([#165](https://github.com/diegosouzapw/OmniRoute/issues/165))
- **Windows Electron CI Build** — Added `shell: bash` to Collect installers step for Windows runners ([PR #168](https://github.com/diegosouzapw/OmniRoute/pull/168) by @benzntech)
- **TypeScript Safety** — Replaced `Record<string, any>` with `Record<string, unknown>` in `registryUtils.ts`
---
## [1.7.1] — 2026-02-28
### 🐛 Fixed
- **Dashboard Layout Breakage** — Tailwind CSS v4 auto-detection failed to scan Next.js route group directories with parentheses (e.g. `(dashboard)`), causing all responsive grid utilities (`sm:grid-cols-*`, `md:grid-cols-*`, `lg:grid-cols-*`, `xl:grid-cols-*`) to be purged from production CSS. Cards displayed in a single column instead of multi-column grids. Fixed by adding explicit `@source` directives in `globals.css`
---
## [1.7.0] — 2026-02-28
### ✨ New Features
- **16 Pain Points Documentation** — New collapsible section "🎯 What OmniRoute Solves — 16 Real Pain Points" added to the main README and all 29 language-specific READMEs. Each pain point uses `<details>/<summary>` tags for clean, expandable content
- **Configurable User-Agent per Provider** — User-Agent strings for OAuth providers (Claude, Codex, GitHub, Antigravity, Kiro, iFlow, Qwen, Cursor, Gemini CLI) are now configurable via environment variables. Format: `{PROVIDER_ID}_USER_AGENT=custom-value` ([#155](https://github.com/diegosouzapw/OmniRoute/issues/155))
### 🐛 Fixed
- **Hardcoded `$HOME` Path in Standalone/Bun Builds** — 5 files (`backupService.ts`, `mitm/manager.ts`, `mitm/server.ts`, `mitm/cert/generate.ts`, `codex-profiles/route.ts`) were bypassing the centralized `dataPaths.ts` and using `os.homedir()` directly. This caused paths to bake the build machine's `$HOME` into standalone/bun builds, producing `EACCES: permission denied` errors on other machines. All files now use `resolveDataDir()` from `dataPaths.ts`, respecting `DATA_DIR` env var and XDG conventions ([#156](https://github.com/diegosouzapw/OmniRoute/issues/156))
### 📝 Documentation
- **`.env` and `.env.example` Synced** — Added 9 User-Agent env vars with latest known default values to both environment files
- **30 README Translations Updated** — All language READMEs now include the 16 Pain Points section
---
## [1.6.9] — 2026-02-28
### 🐛 Fixed
- **Proxy Port Preservation** — `new URL()` silently strips default ports (80/443); proxy connections now extract the port from the raw URL string before parsing, preventing connection timeouts ([PR #161](https://github.com/diegosouzapw/OmniRoute/pull/161))
- **Proxy Credential Encoding** — URL-encode special characters in proxy username/password; decode during legacy migration ([PR #161](https://github.com/diegosouzapw/OmniRoute/pull/161))
- **HTTPS Proxy Default Port** — Changed from 8080 to 443 in frontend and migration logic ([PR #161](https://github.com/diegosouzapw/OmniRoute/pull/161))
- **Proxy Dispatcher Cache** — Invalidate cached dispatchers when proxy config is updated or deleted ([PR #161](https://github.com/diegosouzapw/OmniRoute/pull/161))
- **Proxy Logger SQLite Type** — Cast `proxyPort` to `Number` for INTEGER column ([PR #161](https://github.com/diegosouzapw/OmniRoute/pull/161))
- **CopilotToolCard URL** — Use `baseUrl` prop directly instead of redundant `window.location.origin`; filter to chat models only (`!m.type && !m.parent`) ([PR #160](https://github.com/diegosouzapw/OmniRoute/pull/160))
---
## [1.6.8] — 2026-02-28
### 🔧 Improved
- **Electron Release Workflow** — Refactored CI to trigger on git tags (`v*`) + manual dispatch, with version validation, artifact upload/download pattern across 3 platforms, and a single release job. Only installer files (`.dmg`, `.exe`, `.AppImage`) are uploaded — no more 5K+ unpacked files ([PR #159](https://github.com/diegosouzapw/OmniRoute/pull/159))
- **Windows Portable Exe** — Added standalone portable `.exe` build alongside the NSIS installer ([PR #159](https://github.com/diegosouzapw/OmniRoute/pull/159))
- **Source Code Archives** — Releases now include `OmniRoute-vX.Y.Z.source.tar.gz` and `.zip` via `git archive` ([PR #159](https://github.com/diegosouzapw/OmniRoute/pull/159))
- **Installation Docs** — Added platform-specific installation instructions with macOS Gatekeeper workaround ([PR #159](https://github.com/diegosouzapw/OmniRoute/pull/159))
### 🐛 Fixed
- **Next.js App Router Conflict** — Added `app/` (production standalone build) to `.gitignore`. This directory was conflicting with Next.js App Router detection in dev mode, causing all routes to return 404
- **Git Tracking** — Added `electron/node_modules/` to `.gitignore`
---
## [1.6.7] — 2026-02-28
### ✨ New Feature
- **GitHub Copilot Configuration Generator** — New tool on the CLI Tools dashboard page. Select models and generate the `chatLanguageModels.json` config block for VS Code GitHub Copilot using the Azure vendor pattern. Features: bulk model selection from `/v1/models` (includes combos/custom), search/filter, configurable tokens/tool-calling/vision, one-click copy, persistent selection via localStorage. Version compatibility warning for VS Code ≥ 1.109 / Copilot Chat ≥ v0.37 ([#142](https://github.com/diegosouzapw/OmniRoute/issues/142))
### 🧹 Housekeeping
- Added `electron/dist-electron/` to `.gitignore` (build artifact)
---
## [1.6.6] — 2026-02-28
### 🔒 Security Fix
- **Auth bypass after onboarding** — Fixed regression where users could access the dashboard without authentication after upgrading from older versions. The "no password" safeguard (for fresh installs) was incorrectly firing after onboarding was complete, allowing unauthenticated access when `setupComplete=true` but the password DB row was missing ([#151](https://github.com/diegosouzapw/OmniRoute/issues/151))
---
## [1.6.5] — 2026-02-28
### 🖥️ Electron Desktop
- **Official app icons** — Added proper platform-specific icons derived from the OmniRoute SVG logo: `.icns` (macOS), `.ico` (Windows), `.png` (Linux), and `tray-icon.png` (32×32) — via PR [#154](https://github.com/diegosouzapw/OmniRoute/pull/154)
- **Automated release workflow** — New GitHub Actions workflow (`electron-release.yml`) builds Electron for Windows/macOS/Linux on every GitHub release publish
- **CI fix** — Changed `npm ci``npm install` in the Electron build step since `electron/package-lock.json` is `.gitignored`
### 📖 Documentation
- **Desktop App section** — Added to all 30 README files (9 fully translated: PT-BR, ES, FR, DE, ZH-CN, JA, RU, KO, AR)
- **Electron Fix Plan** — Published detailed code review and fix documentation at `docs/ELECTRON_FIX_PLAN.md`
### 🐛 Issue Triage
- **#151** — Auth bypass after v1.6.3 upgrade — triaged, requesting more info from reporter
- **#142** — Copilot Config Generator — previously triaged, 5 comments
---
## [1.6.4] — 2026-02-28
### 🖥️ Electron Desktop — Code Review Hardening (16 Fixes)
#### 🔴 Critical
- **Server readiness** — Window now waits for server health check before loading URL; no more blank screens on cold start (#1)
- **Restart timeout** — `restart-server` IPC handler now has 5s timeout + `SIGKILL` to prevent indefinite hangs (#2)
- **Port change lifecycle** — `changePort()` now stops and restarts the server on the new port instead of just reloading the URL (#3)
#### 🟡 Important
- **Tray cleanup** — Old `Tray` instance is now destroyed before recreating, preventing duplicate icons and memory leaks (#4)
- **IPC event emission** — Main process now emits `server-status` and `port-changed` events to renderer, making React hooks functional (#5)
- **Listener accumulation** — Preload now returns disposer functions for precise listener cleanup instead of `removeAllListeners` (#6)
- **useIsElectron performance** — Replaced `useState`+`useEffect` with `useSyncExternalStore` to eliminate 5x unnecessary re-renders (#7)
#### 🔵 Minor
- Removed dead `isProduction` variable (#8)
- Platform-conditional `titleBarStyle``hiddenInset` only on macOS, `default` on Windows/Linux (#9)
- `stdio: pipe` — Server output captured for logging and readiness detection instead of `inherit` (#10)
- Shared `AppInfo` type — `useElectronAppInfo` now uses the shared interface from `types.d.ts` (#11)
- `useDataDir` error state — Now exposes errors instead of swallowing silently (#12)
- Synced `electron/package.json` version to `1.6.4` (#13)
- Removed dead `omniroute://` protocol config — no handler existed (#14)
- **Content Security Policy** — Added CSP via `session.webRequest.onHeadersReceived` (#15)
- Simplified preload validation — Generic `safeInvoke`/`safeSend`/`safeOn` wrappers reduce boilerplate (#16)
### 🧪 Test Suite Expansion
- **76 tests** across 15 suites (up from 64 tests / 9 suites)
- New: server readiness timeout, restart race condition, CSP directives, platform options, disposer pattern, generic IPC wrappers
---
## [1.6.3] — 2026-02-28
### 🐛 Bug Fixes
- **Database data preservation on upgrade** — Previously, upgrading from older versions (e.g. v1.2.0 → v1.6.x) could cause data loss by renaming the existing database when a legacy `schema_migrations` table was detected. Now checks for actual data before deciding to reset ([#146](https://github.com/diegosouzapw/OmniRoute/issues/146))
- **Hardcoded build-machine paths in npm package** — Next.js standalone output baked absolute paths from the build machine into `server.js` and `required-server-files.json`. On other machines these paths don't exist, causing `ENOENT` errors. The prepublish script now sanitizes all build paths to relative ([#147](https://github.com/diegosouzapw/OmniRoute/issues/147))
---
## [1.6.2] — 2026-02-27
### ✨ New Features
- **Provider labels in Combos** — Combo cards now show user-defined provider names instead of long UUID identifiers, making complex multi-provider combos easier to read ([#121](https://github.com/diegosouzapw/OmniRoute/issues/121))
- **Improved request log labels** — RequestLoggerV2 resolves OpenAI-compatible provider IDs to user-defined names via provider nodes lookup
- **Smarter API key display** — `formatApiKey()` now shows the full key name for named keys instead of truncating them
---
## [1.6.1] — 2026-02-27
### 🐛 Bug Fixes
- **Cross-platform npm install** — Added `postinstall` script to auto-rebuild `better-sqlite3` for the user's OS/architecture. Previously, the npm package shipped Linux x64 binaries that failed on Windows and macOS ([#129](https://github.com/diegosouzapw/OmniRoute/issues/129))
---
## [1.6.0] — 2026-02-27
> ### 🔀 Feature Release — Split-Port Mode
@@ -751,6 +1316,9 @@ New environment variables:
---
[1.6.3]: https://github.com/diegosouzapw/OmniRoute/releases/tag/v1.6.3
[1.6.2]: https://github.com/diegosouzapw/OmniRoute/releases/tag/v1.6.2
[1.6.1]: https://github.com/diegosouzapw/OmniRoute/releases/tag/v1.6.1
[1.6.0]: https://github.com/diegosouzapw/OmniRoute/releases/tag/v1.6.0
[1.5.0]: https://github.com/diegosouzapw/OmniRoute/releases/tag/v1.5.0
[1.4.11]: https://github.com/diegosouzapw/OmniRoute/releases/tag/v1.4.11

View File

@@ -2,6 +2,7 @@ FROM node:22-bookworm-slim AS builder
WORKDIR /app
COPY package*.json ./
COPY scripts/postinstall.mjs ./scripts/postinstall.mjs
RUN if [ -f package-lock.json ]; then npm ci --no-audit --no-fund; else npm install --no-audit --no-fund; fi
COPY . ./
@@ -19,6 +20,7 @@ LABEL org.opencontainers.image.title="omniroute" \
ENV NODE_ENV=production
ENV PORT=20128
ENV HOSTNAME=0.0.0.0
ENV NODE_OPTIONS="--max-old-space-size=256"
# Data directory inside Docker — must match the volume mount in docker-compose.yml
ENV DATA_DIR=/app/data

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -110,6 +110,35 @@ _Conecta cualquier IDE o herramienta CLI con IA a través de OmniRoute — gatew
---
## 🖼️
<div align="center">
<img src="./docs/screenshots/MainOmniRoute.png" alt="OmniRoute" width="800"/>
</div>
---
## 📸
<details>
<summary><b>...</b></summary>
| # | # |
| ----- | ---------------------------------------- |
| **1** | ![1](docs/screenshots/01-providers.png) |
| **2** | ![2](docs/screenshots/02-combos.png) |
| **3** | ![3](docs/screenshots/03-analytics.png) |
| **4** | ![4](docs/screenshots/04-health.png) |
| **5** | ![5](docs/screenshots/05-translator.png) |
| **6** | ![6](docs/screenshots/06-settings.png) |
| **7** | ![7](docs/screenshots/07-cli-tools.png) |
| **8** | ![8](docs/screenshots/08-usage.png) |
| **9** | ![9](docs/screenshots/09-endpoint.png) |
</details>
---
## 🤔 ¿Por qué OmniRoute?
**Deja de desperdiciar dinero y chocar con límites:**
@@ -128,6 +157,18 @@ _Conecta cualquier IDE o herramienta CLI con IA a través de OmniRoute — gatew
---
## 📧 Soporte
> 💬 **¡Únete a la comunidad!** [Grupo WhatsApp](https://chat.whatsapp.com/JI7cDQ1GyaiDHhVBpLxf8b?mode=gi_t) — Obtén ayuda, comparte consejos y mantente al día.
- **Website**: [omniroute.online](https://omniroute.online)
- **GitHub**: [github.com/diegosouzapw/OmniRoute](https://github.com/diegosouzapw/OmniRoute)
- **Issues**: [github.com/diegosouzapw/OmniRoute/issues](https://github.com/diegosouzapw/OmniRoute/issues)
- **WhatsApp**: [Grupo de la Comunidad](https://chat.whatsapp.com/JI7cDQ1GyaiDHhVBpLxf8b?mode=gi_t)
- **Proyecto Original**: [9router por decolua](https://github.com/decolua/9router)
---
## 🔄 Cómo Funciona
```
@@ -157,6 +198,497 @@ Resultado: Nunca dejes de programar, costo mínimo
---
## 🎯 Lo que resuelve OmniRoute: 30 puntos débiles reales y casos de uso
> **Todos los desarrolladores que utilizan herramientas de IA se enfrentan a estos problemas a diario.** OmniRoute se creó para resolverlos todos: desde sobrecostos hasta bloqueos regionales, desde flujos rotos de OAuth hasta operaciones de protocolo y observabilidad empresarial.
<details>
<summary><b>💸 1. "Pago una suscripción costosa pero aún así me interrumpen los límites"</b></summary>
Los desarrolladores pagan entre 20 y 200 dólares al mes por Claude Pro, Codex Pro o GitHub Copilot. Incluso pagando, la cuota tiene un límite: 5 horas de uso, límites semanales o límites de tarifa por minuto. A mitad de la sesión de codificación, el proveedor deja de responder y el desarrollador pierde flujo y productividad.
**Cómo lo resuelve OmniRoute:**
- **Reserva inteligente de 4 niveles**: si se agota la cuota de suscripción, se redirige automáticamente a la clave API → Barato → Gratis sin intervención manual
- **Seguimiento de cuotas en tiempo real**: muestra el consumo de tokens en tiempo real con cuenta regresiva de reinicio (5 h, diario, semanal)
- **Soporte multicuenta**: varias cuentas por proveedor con rotación automática: cuando una se agota, cambia a la siguiente
- **Combinaciones personalizadas**: cadenas de respaldo personalizables con 6 estrategias de equilibrio (completar primero, por turnos, P2C, aleatoria, menos utilizada, de costo optimizado)
- **Cuotas comerciales de Codex**: monitoreo de cuotas del espacio de trabajo empresarial/de equipo directamente en el panel
</details>
<details>
<summary><b>🔌 2. "Necesito usar varios proveedores pero cada uno tiene una API diferente"</b></summary>
OpenAI usa un formato, Claude (Anthropic) usa otro, Gemini otro más. Si un desarrollador quiere probar modelos de diferentes proveedores o recurrir a ellos, debe reconfigurar los SDK, cambiar los puntos finales y lidiar con formatos incompatibles. Los proveedores personalizados (FriendLI, NIM) tienen puntos finales de modelo no estándar.
**Cómo lo resuelve OmniRoute:**
- **Punto final unificado**: un único `http://localhost:20128/v1` sirve como proxy para los más de 36 proveedores
- **Traducción de formato**: automática y transparente: OpenAI ↔ Claude ↔ Gemini ↔ API de respuestas
- **Desinfección de respuesta**: elimina los campos no estándar (`x_groq`, `usage_breakdown`, `service_tier`) que interrumpen OpenAI SDK v1.83+
- **Normalización de roles**: convierte `developer``system` para proveedores que no son OpenAI; `system``user` para GLM/ERNIE
- **Think Tag Extraction**: extrae bloques `<think>` de modelos como DeepSeek R1 en `reasoning_content` estandarizado.
- **Salida estructurada para Gemini** — `json_schema``responseMimeType`/`responseSchema` conversión automática
- **`stream` por defecto es `false`**: se alinea con las especificaciones de OpenAI, evitando SSE inesperado en los SDK de Python/Rust/Go
</details>
<details>
<summary><b>🌐 3. "Mi proveedor de IA bloquea mi región/país"</b></summary>
Proveedores como OpenAI/Codex bloquean el acceso desde ciertas regiones geográficas. Los usuarios obtienen errores como `unsupported_country_region_territory` durante las conexiones OAuth y API. Esto resulta especialmente frustrante para los desarrolladores de los países en desarrollo.
**Cómo lo resuelve OmniRoute:**
- **Configuración de proxy de 3 niveles**: Proxy configurable en 3 niveles: global (todo el tráfico), por proveedor (un solo proveedor) y por conexión/clave.
- **Insignias de proxy codificadas por colores** — Indicadores visuales: 🟢 proxy global, 🟡 proxy de proveedor, 🔵 proxy de conexión, que siempre muestra la IP
- **Intercambio de tokens de OAuth a través de proxy**: el flujo de OAuth también pasa a través del proxy, lo que resuelve `unsupported_country_region_territory`
- **Pruebas de conexión a través de proxy**: las pruebas de conexión utilizan el proxy configurado (no más derivación directa)
- **Soporte SOCKS5**: soporte completo de proxy SOCKS5 para enrutamiento saliente
- **Suplantación de huellas dactilares TLS**: huella digital TLS similar a la de un navegador a través de `wreq-js` para evitar la detección de bots.
</details>
<details>
<summary><b>🆓 4. "Quiero usar IA para codificar pero no tengo dinero"</b></summary>
No todo el mundo puede pagar entre 20 y 200 dólares al mes por suscripciones a IA. Los estudiantes, desarrolladores de países emergentes, aficionados y autónomos necesitan acceso a modelos de calidad sin costo alguno.
**Cómo lo resuelve OmniRoute:**
- **Proveedores de nivel gratuito integrados**: soporte nativo para proveedores 100% gratuitos: iFlow (8 modelos ilimitados), Qwen (3 modelos ilimitados), Kiro (Claude gratis), Gemini CLI (180K/mes gratis)
- **Combos solo gratuitos**: cadena `gc/gemini-3-flash → if/kimi-k2-thinking → qw/qwen3-coder-plus` = $0/mes sin tiempo de inactividad
- **Créditos gratuitos NVIDIA NIM**: 1000 créditos gratuitos integrados
- **Estrategia de optimización de costos**: estrategia de enrutamiento que elige automáticamente el proveedor más barato disponible
</details>
<details>
<summary><b>🔒 5. "Necesito proteger mi puerta de enlace AI del acceso no autorizado"</b></summary>
Al exponer una puerta de enlace de IA a la red (LAN, VPS, Docker), cualquiera con la dirección puede consumir los tokens/cuota del desarrollador. Sin protección, las API son vulnerables al mal uso, la inyección rápida y el abuso.
**Cómo lo resuelve OmniRoute:**
- **Administración de claves API**: generación, rotación y alcance por proveedor con una página `/dashboard/api-manager` dedicada
- **Permisos a nivel de modelo**: restrinja las claves API a modelos específicos (`openai/*`, patrones comodín), con la opción Permitir todo/Restringir
- **API Endpoint Protection**: requiere una clave para `/v1/models` y bloquea proveedores específicos del listado
- **Auth Guard + Protección CSRF**: todas las rutas del panel protegidas con middleware `withAuth` + tokens CSRF
- **Limitador de velocidad**: limitación de velocidad por IP con ventanas configurables
- **Filtrado de IP**: lista permitida/lista bloqueada para control de acceso
- **Prompt injection guard**: desinfección contra patrones de avisos maliciosos
- **Cifrado AES-256-GCM**: credenciales cifradas en reposo
</details>
<details>
<summary><b>🛑 6. "Mi proveedor dejó de funcionar y perdí mi flujo de codificación"</b></summary>
Los proveedores de IA pueden volverse inestables, devolver errores 5xx o alcanzar límites de velocidad temporales. Si un desarrollador depende de un solo proveedor, se le interrumpe. Sin disyuntores, los reintentos repetidos pueden bloquear la aplicación.
**Cómo lo resuelve OmniRoute:**
- **Disyuntor por proveedor**: apertura/cierre automático con umbrales configurables y enfriamiento (cerrado/abierto/medio abierto)
- **Retroceso exponencial**: retrasos progresivos en los reintentos
- **Anti-Thundering Herd** — Mutex + protección de semáforo contra tormentas de reintentos simultáneos
- **Cadenas alternativas combinadas**: si el proveedor principal falla, automáticamente pasa por la cadena sin intervención.
- **Disyuntor combinado**: desactiva automáticamente los proveedores defectuosos dentro de una cadena combinada
- **Panel de estado**: monitoreo del tiempo de actividad, estados de disyuntores, bloqueos, estadísticas de caché, latencia p50/p95/p99
</details>
<details>
<summary><b>🔧 7. "Configurar cada herramienta de IA es tedioso y repetitivo"</b></summary>
Los desarrolladores utilizan Cursor, Claude Code, Codex CLI, OpenClaw, Gemini CLI, Kilo Code... Cada herramienta necesita una configuración diferente (punto final API, clave, modelo). Reconfigurar al cambiar de proveedor o modelo es una pérdida de tiempo.
**Cómo lo resuelve OmniRoute:**
- **Panel de herramientas CLI**: página dedicada con configuración con un solo clic para Claude Code, Codex CLI, OpenClaw, Kilo Code, Antigravity, Cline
- **Generador de configuración de GitHub Copilot**: genera `chatLanguageModels.json` para código VS con selección de modelo masivo
- **Asistente de incorporación**: configuración guiada de 4 pasos para usuarios nuevos
- **Un punto final, todos los modelos**: configure `http://localhost:20128/v1` una vez, acceda a más de 36 proveedores
</details>
<details>
<summary><b>🔑 8. "Administrar tokens OAuth de múltiples proveedores es un infierno"</b></summary>
Claude Code, Codex, Gemini CLI, Copilot: todos usan OAuth 2.0 con tokens que caducan. Los desarrolladores necesitan volver a autenticarse constantemente, lidiar con `client_secret is missing`, `redirect_uri_mismatch` y fallas en servidores remotos. OAuth en LAN/VPS es particularmente problemático.
**Cómo lo resuelve OmniRoute:**
- **Actualización automática de tokens**: los tokens de OAuth se actualizan en segundo plano antes de que caduquen
- **OAuth 2.0 (PKCE) integrado**: flujo automático para Claude Code, Codex, Gemini CLI, Copilot, Kiro, Qwen, iFlow
- **OAuth multicuenta**: varias cuentas por proveedor mediante extracción de token JWT/ID
- **OAuth LAN/Remote Fix** — Detección de IP privada para `redirect_uri` + modo URL manual para servidores remotos
- **OAuth detrás de Nginx**: utiliza `window.location.origin` para compatibilidad con proxy inverso
- **Guía remota de OAuth**: guía paso a paso para las credenciales de Google Cloud en VPS/Docker
</details>
<details>
<summary><b>📊 9. "No sé cuánto estoy gastando ni dónde"</b></summary>
Los desarrolladores utilizan múltiples proveedores pagos pero no tienen una visión unificada del gasto. Cada proveedor tiene su propio panel de facturación, pero no hay una vista consolidada. Los costos inesperados pueden acumularse.
**Cómo lo resuelve OmniRoute:**
- **Panel de análisis de costos**: seguimiento de costos por token y gestión de presupuesto por proveedor
- **Límites de presupuesto por nivel**: límite de gasto por nivel que activa el respaldo automático
- **Configuración de precios por modelo**: precios configurables por modelo
- **Estadísticas de uso por clave API**: recuento de solicitudes y marca de tiempo utilizada por última vez por clave
- **Panel de análisis**: tarjetas de estadísticas, tabla de uso de modelos, tabla de proveedores con tasas de éxito y latencia.
</details>
<details>
<summary><b>🐛 10. "No puedo diagnosticar errores y problemas en llamadas AI"</b></summary>
Cuando falla una llamada, el desarrollador no sabe si se trata de un límite de velocidad, un token caducado, un formato incorrecto o un error del proveedor. Registros fragmentados en diferentes terminales. Sin observabilidad, la depuración es de prueba y error.
**Cómo lo resuelve OmniRoute:**
- **Panel de registros unificados**: 4 pestañas: registros de solicitudes, registros de proxy, registros de auditoría y consola
- **Visor de registros de consola**: visor estilo terminal en tiempo real con niveles codificados por colores, desplazamiento automático, búsqueda y filtro
- **Registros de proxy SQLite**: registros persistentes que sobreviven a los reinicios del servidor
- **Translator Playground**: 4 modos de depuración: Playground (traducción de formato), Chat Tester (ida y vuelta), Test Bench (por lotes), Live Monitor (en tiempo real)
- **Solicitud de telemetría**: latencia p50/p95/p99 + seguimiento de X-Request-Id
- **Registro basado en archivos con rotación**: el interceptor de consola captura todo en el registro JSON con rotación basada en el tamaño.
</details>
<details>
<summary><b>🏗️ 11. "Implementar y mantener la puerta de enlace es complejo"</b></summary>
Instalar, configurar y mantener un proxy de IA en diferentes entornos (local, VPS, Docker, nube) requiere mucha mano de obra. Problemas como rutas codificadas, `EACCES` en directorios, conflictos de puertos y compilaciones multiplataforma añaden fricción.
**Cómo lo resuelve OmniRoute:**
- **instalación global de npm** — `npm install -g omniroute && omniroute` — hecho
- **Docker multiplataforma**: AMD64 + ARM64 nativo (Apple Silicon, AWS Graviton, Raspberry Pi)
- **Perfiles de Docker Compose**: `base` (sin herramientas CLI) y `cli` (con Claude Code, Codex, OpenClaw)
- **Aplicación de escritorio Electron**: aplicación nativa para Windows/macOS/Linux con bandeja del sistema, inicio automático y modo sin conexión
- **Modo de puerto dividido**: API y panel en puertos separados para escenarios avanzados (proxy inverso, redes de contenedores)
- **Cloud Sync**: sincronización de configuración entre dispositivos a través de Cloudflare Workers
- **Copias de seguridad de base de datos**: copia de seguridad, restauración, exportación e importación automáticas de todas las configuraciones
</details>
<details>
<summary><b>🌍 12. "La interfaz es solo en inglés y mi equipo no habla inglés"</b></summary>
Los equipos en países que no hablan inglés, especialmente en América Latina, Asia y Europa, tienen dificultades con las interfaces solo en inglés. Las barreras del idioma reducen la adopción y aumentan los errores de configuración.
**Cómo lo resuelve OmniRoute:**
- **Panel i18n — 30 idiomas** — Las más de 500 teclas traducidas, incluidas árabe, búlgaro, danés, alemán, español, finlandés, francés, hebreo, hindi, húngaro, indonesio, italiano, japonés, coreano, malayo, holandés, noruego, polaco, portugués (PT/BR), rumano, ruso, eslovaco, sueco, tailandés, ucraniano, vietnamita, chino, filipino, inglés.
- **Soporte RTL**: soporte de derecha a izquierda para árabe y hebreo
- **README multilingüe**: 30 traducciones de documentación completa
- **Selector de idioma**: ícono de globo en el encabezado para cambiar en tiempo real
</details>
<details>
<summary><b>🔄 13. "Necesito más que chat: necesito incrustaciones, imágenes y audio"</b></summary>
La IA no es solo completar un chat. Los desarrolladores necesitan generar imágenes, transcribir audio, crear incrustaciones para RAG, reclasificar documentos y moderar contenido. Cada API tiene un punto final y un formato diferentes.
**Cómo lo resuelve OmniRoute:**
- **Integraciones** — `/v1/embeddings` con 6 proveedores y más de 9 modelos
- **Generación de imágenes** — `/v1/images/generations` con 10 proveedores y más de 20 modelos (OpenAI, xAI, Together, Fireworks, Nebius, Hyperbolic, NanoBanana, Antigravity, SD WebUI, ComfyUI)
- **Texto a vídeo** — `/v1/videos/generations` — ComfyUI (AnimateDiff, SVD) y SD WebUI
- **Texto a música** — `/v1/music/generations` — ComfyUI (audio estable abierto, MusicGen)
- **Transcripción de audio** — `/v1/audio/transcriptions` — Whisper + Nvidia NIM, HuggingFace, Qwen3
- **Texto a voz** — `/v1/audio/speech` — ElevenLabs, Nvidia NIM, HuggingFace, Coqui, Tortoise, Qwen3 y proveedores existentes
- **Moderaciones** — `/v1/moderations` — Comprobaciones de seguridad del contenido
- **Reclasificación** — `/v1/rerank` — Reclasificación de relevancia del documento
- **API de respuestas**: compatibilidad total con `/v1/responses` para Codex
</details>
<details>
<summary><b>🧪 14. "No tengo forma de probar y comparar la calidad entre modelos"</b></summary>
Los desarrolladores quieren saber qué modelo es mejor para su caso de uso (código, traducción, razonamiento), pero comparar manualmente es lento. No existen herramientas de evaluación integradas.
**Cómo lo resuelve OmniRoute:**
- **Evaluaciones LLM**: pruebas de conjunto dorado con 10 casos precargados que cubren saludos, matemáticas, geografía, generación de código, cumplimiento de JSON, traducción, rebajas y rechazo de seguridad.
- **4 estrategias de coincidencia**: `exact`, `contains`, `regex`, `custom` (función JS)
- **Translator Playground Test Bench**: pruebas por lotes con múltiples entradas y resultados esperados, comparación entre proveedores
- **Chat Tester**: recorrido completo de ida y vuelta con representación de respuesta visual
- **Live Monitor**: flujo en tiempo real de todas las solicitudes que fluyen a través del proxy
</details>
<details>
<summary><b>📈 15. "Necesito escalar sin perder rendimiento"</b></summary>
A medida que crece el volumen de solicitudes, sin almacenar en caché las mismas preguntas generan costos duplicados. Sin idempotencia, las solicitudes duplicadas desperdician el procesamiento. Se deben respetar los límites de tarifas por proveedor.
**Cómo lo resuelve OmniRoute:**
- **Caché semántica**: la caché de dos niveles (firma + semántica) reduce el costo y la latencia
- **Solicitud de idempotencia**: ventana de deduplicación de 5 segundos para solicitudes idénticas
- **Detección de límite de velocidad**: RPM por proveedor, intervalo mínimo y seguimiento simultáneo máximo
- **Límites de velocidad editables**: valores predeterminados configurables en Configuración → Resiliencia con persistencia
- **Caché de validación de clave API**: caché de 3 niveles para rendimiento de producción
- **Panel de estado con telemetría**: latencia p50/p95/p99, estadísticas de caché, tiempo de actividad
</details>
<details>
<summary><b>🤖 16. "Quiero controlar el comportamiento del modelo globalmente"</b></summary>
Desarrolladores que quieran todas las respuestas en un idioma específico, con un tono específico o quieran limitar los tokens de razonamiento. Configurar esto en cada herramienta/solicitud no es práctico.
**Cómo lo resuelve OmniRoute:**
- **Inyección de aviso del sistema**: aviso global aplicado a todas las solicitudes
- **Thinking Budget Validation**: control de asignación de tokens de razonamiento por solicitud (transferencia, automática, personalizada, adaptativa)
- **6 estrategias de enrutamiento**: estrategias globales que determinan cómo se distribuyen las solicitudes
- **Enrutador comodín**: los patrones `provider/*` se enrutan dinámicamente a cualquier proveedor
- **Activar/desactivar combinación de alternar**: alterna combinaciones directamente desde el panel
- **Alternar proveedor**: activa/desactiva todas las conexiones de un proveedor con un solo clic
- **Proveedores bloqueados**: excluye proveedores específicos del listado `/v1/models`
</details>
<details>
<summary><b>🧰 17. "Necesito herramientas MCP como capacidades de producto de primera clase"</b></summary>
Muchas puertas de enlace de IA exponen MCP solo como un detalle de implementación oculto. Los equipos necesitan una capa operativa visible y manejable.
**Cómo lo resuelve OmniRoute:**
- MCP aparece en la pestaña de navegación del panel y protocolo de punto final
- Página de gestión de MCP dedicada con procesos, herramientas, alcances y auditoría
- Inicio rápido integrado para `omniroute --mcp` e incorporación de clientes
</details>
<details>
<summary><b>🧠 18. "Necesito orquestación A2A con rutas de tareas de sincronización + transmisión"</b></summary>
Los flujos de trabajo de los agentes necesitan respuestas directas y una ejecución continua de larga duración con control del ciclo de vida.
**Cómo lo resuelve OmniRoute:**
- Punto final A2A JSON-RPC (`POST /a2a`) con `message/send` y `message/stream`
- Transmisión SSE con propagación del estado terminal
- API de ciclo de vida de tareas para `tasks/get` y `tasks/cancel`
</details>
<details>
<summary><b>🛰️ 19. "Necesito un estado real del proceso MCP, no un estado adivinado"</b></summary>
Los equipos operativos necesitan saber si MCP está realmente activo, no solo si se puede acceder a una API.
**Cómo lo resuelve OmniRoute:**
- Archivo de latidos en tiempo de ejecución con PID, marcas de tiempo, transporte, recuento de herramientas y modo de alcance
- API de estado de MCP que combina latidos + actividad reciente
- Tarjetas de estado de la interfaz de usuario para el proceso/tiempo de actividad/actualización de latidos
</details>
<details>
<summary><b>📋 20. "Necesito ejecución de herramienta MCP auditable"</b></summary>
Cuando las herramientas modifican la configuración o desencadenan acciones de operaciones, los equipos necesitan trazabilidad forense.
**Cómo lo resuelve OmniRoute:**
- Registro de auditoría respaldado por SQLite para llamadas a herramientas MCP
- Filtros por herramienta, éxito/fracaso, clave API y paginación
- Tabla de auditoría del panel + puntos finales de estadísticas para automatización
</details>
<details>
<summary><b>🔐 21. "Necesito permisos MCP con alcance por integración"</b></summary>
Los diferentes clientes deberían tener acceso con privilegios mínimos a las categorías de herramientas.
**Cómo lo resuelve OmniRoute:**
- 9 alcances MCP granulares para acceso controlado a herramientas
- Aplicación del alcance y visibilidad en la interfaz de usuario de gestión de MCP
- Postura predeterminada segura para herramientas operativas
</details>
<details>
<summary><b>⚙️ 22. "Necesito controles operativos sin redistribuir"</b></summary>
Los equipos necesitan cambios rápidos en el tiempo de ejecución durante incidentes o eventos de costos.
**Cómo lo resuelve OmniRoute:**
- Cambie la activación combinada directamente desde el panel de MCP
- Aplicar perfiles de resiliencia de paquetes de políticas predefinidos
- Restablecer el estado del disyuntor desde el mismo panel de operaciones.
</details>
<details>
<summary><b>🔄 23. "Necesito visibilidad y cancelación del ciclo de vida de la tarea A2A en vivo"</b></summary>
Sin visibilidad del ciclo de vida, los incidentes de tareas se vuelven difíciles de clasificar.
**Cómo lo resuelve OmniRoute:**
- Listado de tareas/filtrado por estado/habilidad con paginación
- Profundización en metadatos, eventos y artefactos de tareas
- Punto final de cancelación de tarea y acción de UI con confirmación
</details>
<details>
<summary><b>🌊 24. "Necesito métricas de transmisión activas para la carga A2A"</b></summary>
Los flujos de trabajo de streaming requieren información operativa sobre la simultaneidad y las conexiones en vivo.
**Cómo lo resuelve OmniRoute:**
- Contadores de flujo activos integrados en el estado A2A
- Marca de tiempo de la última tarea y recuentos por estado
- Tarjetas de tablero A2A para monitoreo de operaciones en tiempo real
</details>
<details>
<summary><b>🪪 25. "Necesito descubrimiento de agente estándar para clientes"</b></summary>
Los clientes y orquestadores externos necesitan metadatos legibles por máquina para la incorporación.
**Cómo lo resuelve OmniRoute:**
- Tarjeta de agente expuesta en `/.well-known/agent.json`
- Capacidades y habilidades mostradas en la interfaz de usuario de gestión.
- La API de estado A2A incluye metadatos de descubrimiento para la automatización
</details>
<details>
<summary><b>🧭 26. "Necesito capacidad de descubrimiento de protocolo en la UX del producto"</b></summary>
Si los usuarios no pueden descubrir las superficies de protocolo, la calidad de la adopción y el soporte disminuye.
**Cómo lo resuelve OmniRoute:**
- Entradas de la barra lateral para MCP y A2A
- Pestaña Protocolos de la página del endpoint con inicio rápido y estado
- Enlaces desde la descripción general a paneles de gestión dedicados
</details>
<details>
<summary><b>🧪 27. "Necesito validación de protocolo de extremo a extremo con clientes reales"</b></summary>
Las pruebas simuladas no son suficientes para validar la compatibilidad del protocolo antes del lanzamiento.
**Cómo lo resuelve OmniRoute:**
- Suite E2E que inicia la aplicación y utiliza transporte de cliente MCP SDK real
- Pruebas de cliente A2A para descubrimiento, envío, transmisión, obtención y cancelación de flujos
- Verificar las afirmaciones con las API de auditoría MCP y tareas A2A.
</details>
<details>
<summary><b>📡 28. "Necesito observabilidad unificada en todas las interfaces"</b></summary>
Dividir la observabilidad por protocolo crea puntos ciegos y MTTR más largos.
**Cómo lo resuelve OmniRoute:**
- Paneles/registros/análisis unificados en un solo producto
- Salud + auditoría + solicitud de telemetría en capas OpenAI, MCP y A2A
- API operativas para estado y automatización.
</details>
<details>
<summary><b>💼 29. "Necesito un tiempo de ejecución para proxy + herramientas + orquestación de agentes"</b></summary>
La ejecución de muchos servicios separados aumenta los costos operativos y los modos de falla.
**Cómo lo resuelve OmniRoute:**
- Proxy compatible con OpenAI, servidor MCP y servidor A2A en una sola pila
- Autenticación compartida, resiliencia, almacenamiento de datos y observabilidad.
- Modelo de política consistente en todas las superficies de interacción.
</details>
<details>
<summary><b>🚀 30. "Necesito enviar flujos de trabajo agentes sin expansión de código adhesivo"</b></summary>
Los equipos pierden velocidad al unir múltiples scripts y servicios ad hoc.
**Cómo lo resuelve OmniRoute:**
- Estrategia de endpoint unificada para clientes y agentes
- UI de gestión de protocolos integradas y rutas de validación de humo
- Fundamentos listos para producción (seguridad, registro, resiliencia, respaldo)
</details>
### Guías de ejemplo (casos de uso integrados)
**Libro de estrategias A: maximizar la suscripción paga + copia de seguridad económica**
```txt
Combo: "maximize-claude"
1. cc/claude-opus-4-6
2. glm/glm-4.7
3. if/kimi-k2-thinking
Monthly cost: $20 + small backup spend
Outcome: higher quality, near-zero interruption
```
**Libro de estrategias B: pila de codificación de costo cero**
```txt
Combo: "free-forever"
1. gc/gemini-3-flash
2. if/kimi-k2-thinking
3. qw/qwen3-coder-plus
Monthly cost: $0
Outcome: stable free coding workflow
```
**Libro de estrategias C: cadena alternativa siempre disponible las 24 horas del día, los 7 días de la semana**
```txt
Combo: "always-on"
1. cc/claude-opus-4-6
2. cx/gpt-5.2-codex
3. glm/glm-4.7
4. minimax/MiniMax-M2.1
5. if/kimi-k2-thinking
Outcome: deep fallback depth for deadline-critical workloads
```
**Libro de jugadas D: Operaciones del agente con MCP + A2A**
```txt
1) Start MCP transport (`omniroute --mcp`) for tool-driven operations
2) Run A2A tasks via `message/send` and `message/stream`
3) Observe via /dashboard/mcp and /dashboard/a2a
4) Control incidents with resilience profile + task cancellation
```
---
## ⚡ Inicio Rápido
**1. Instala globalmente:**
@@ -247,6 +779,34 @@ docker compose --profile cli up -d
---
---
## 🖥️
> 🆕 **¡NUEVO!** OmniRoute ahora está disponible como **aplicación de escritorio nativa** para Windows, macOS y Linux.
Ejecuta OmniRoute como una aplicación de escritorio autónoma — sin terminal, sin navegador, sin internet necesario para modelos locales. La app basada en Electron incluye:
- 🖥️ **Ventana Nativa** — Ventana dedicada con integración en la bandeja del sistema
- 🔄 **Inicio Automático** — Inicia OmniRoute al iniciar sesión
- 🔔 **Notificaciones Nativas** — Recibe alertas sobre cuota o problemas de proveedores
-**Instalación con Un Clic** — NSIS (Windows), DMG (macOS), AppImage (Linux)
- 🌐 **Modo Sin Conexión** — Funciona completamente offline con servidor incluido
### Inicio Rápido
```bash
npm run electron:dev # Modo desarrollo
npm run electron:build # Plataforma actual
npm run electron:build:win # Windows (.exe)
npm run electron:build:mac # macOS (.dmg)
npm run electron:build:linux # Linux (.AppImage)
```
📖 Documentación completa: [`electron/README.md`](electron/README.md)
---
## 💰 Precios Resumidos
| Tier | Proveedor | Costo | Reset de Cuota | Mejor Para |
@@ -272,67 +832,6 @@ docker compose --profile cli up -d
---
## 🎯 Casos de Uso
### Caso 1: "Tengo suscripción Claude Pro"
**Problema:** La cuota expira sin usar, límites de tasa durante programación intensa
```
Combo: "maximize-claude"
1. cc/claude-opus-4-6 (usar suscripción al máximo)
2. glm/glm-4.7 (respaldo barato cuando la cuota se agota)
3. if/kimi-k2-thinking (fallback de emergencia gratuito)
Costo mensual: $20 (suscripción) + ~$5 (respaldo) = $25 total
vs. $20 + chocar con límites = frustración
```
### Caso 2: "Quiero costo cero"
**Problema:** No puede pagar suscripciones, necesita IA confiable para programar
```
Combo: "free-forever"
1. gc/gemini-3-flash (180K gratis/mes)
2. if/kimi-k2-thinking (ilimitado gratis)
3. qw/qwen3-coder-plus (ilimitado gratis)
Costo mensual: $0
Calidad: Modelos listos para producción
```
### Caso 3: "Necesito programar 24/7, sin interrupciones"
**Problema:** Plazos ajustados, no puede permitirse tiempo de inactividad
```
Combo: "always-on"
1. cc/claude-opus-4-6 (mejor calidad)
2. cx/gpt-5.2-codex (segunda suscripción)
3. glm/glm-4.7 (barato, reset diario)
4. minimax/MiniMax-M2.1 (más barato, reset 5h)
5. if/kimi-k2-thinking (gratuito ilimitado)
Resultado: 5 capas de fallback = cero tiempo de inactividad
```
### Caso 4: "Quiero IA GRATUITA en OpenClaw"
**Problema:** Necesita asistente de IA en apps de mensajería, completamente gratuito
```
Combo: "openclaw-free"
1. if/glm-4.7 (ilimitado gratis)
2. if/minimax-m2.1 (ilimitado gratis)
3. if/kimi-k2-thinking (ilimitado gratis)
Costo mensual: $0
Acceso vía: WhatsApp, Telegram, Slack, Discord, iMessage, Signal...
```
---
## 💡 Características Principales
### 🧠 Enrutamiento e Inteligencia
@@ -348,6 +847,8 @@ Acceso vía: WhatsApp, Telegram, Slack, Discord, iMessage, Signal...
| 🧩 **Modelos Personalizados** | Agrega cualquier ID de modelo a cualquier proveedor |
| 🌐 **Enrutador Wildcard** | Enruta patrones `provider/*` a cualquier proveedor dinámicamente |
| 🧠 **Presupuesto de Razonamiento** | Modos passthrough, auto, custom y adaptativo para modelos de razonamiento |
| 🔀 **Model Aliases** | Auto-forward deprecated model IDs to current replacements (built-in + custom) |
| ⚡ **Background Degradation** | Auto-route background tasks (titles, summaries) to cheaper models |
| 💬 **Inyección de System Prompt** | System prompt global aplicado en todas las solicitudes |
| 📄 **API Responses** | Soporte completo de la API Responses de OpenAI (`/v1/responses`) para Codex |
@@ -364,15 +865,18 @@ Acceso vía: WhatsApp, Telegram, Slack, Discord, iMessage, Signal...
### 🛡️ Resiliencia y Seguridad
| Característica | Qué Hace |
| ---------------------------------- | ---------------------------------------------------------------- |
| 🔌 **Circuit Breaker** | Auto-apertura/cierre por proveedor con umbrales configurables |
| 🛡️ **Anti-Thundering Herd** | Mutex + semáforo rate-limit para proveedores con API key |
| 🧠 **Caché Semántico** | Caché de dos niveles (firma + semántico) reduce costo y latencia |
| **Idempotencia de Solicitud** | Ventana de dedup de 5s para solicitudes duplicadas |
| 🔒 **Spoofing de Fingerprint TLS** | Bypass de detección de bot vía TLS con wreq-js |
| 🌐 **Filtrado de IP** | Allowlist/blocklist para control de acceso a la API |
| 📊 **Rate Limits Editables** | RPM, gap mínimo y concurrencia máxima configurables |
| Característica | Qué Hace |
| ---------------------------------- | ---------------------------------------------------------------------------- |
| 🔌 **Circuit Breaker** | Auto-apertura/cierre por proveedor con umbrales configurables |
| 🎯 **Endpoint-Aware Models** | Custom models declare supported endpoints + API format |
| 🛡️ **Anti-Thundering Herd** | Mutex + semáforo rate-limit para proveedores con API key |
| 🧠 **Caché Semántico** | Caché de dos niveles (firma + semántico) reduce costo y latencia |
| **Idempotencia de Solicitud** | Ventana de dedup de 5s para solicitudes duplicadas |
| 🔒 **Spoofing de Fingerprint TLS** | Bypass de detección de bot vía TLS con wreq-js |
| 🌐 **Filtrado de IP** | Allowlist/blocklist para control de acceso a la API |
| 📊 **Rate Limits Editables** | RPM, gap mínimo y concurrencia máxima configurables |
| 💾 **Rate Limit Persistence** | Learned limits survive restarts via SQLite with 60s debounce + 24h staleness |
| 🔄 **Token Refresh Resilience** | Per-provider circuit breaker (5 fails→30min) + 30s timeout per attempt |
### 📊 Observabilidad y Analytics
@@ -472,6 +976,27 @@ Traducción transparente entre formatos:
</details>
## 🧪 Evaluaciones (Evals)
OmniRoute incluye un framework de evaluación integrado para probar la calidad de respuestas de LLM contra un conjunto golden. Accede vía **Analytics → Evals** en el dashboard.
### Conjunto Golden Integrado
El "OmniRoute Golden Set" precargado contiene 10 casos de prueba que cubren:
- Saludos, matemáticas, geografía, generación de código
- Conformidad de formato JSON, traducción, markdown
- Rechazo de seguridad (contenido dañino), conteo, lógica booleana
### Estrategias de Evaluación
| Estrategia | Descripción | Ejemplo |
| ---------- | ---------------------------------------------------- | -------------------------------- |
| `exact` | La salida debe coincidir exactamente | `"4"` |
| `contains` | La salida debe contener subcadena (case-insensitive) | `"Paris"` |
| `regex` | La salida debe coincidir con el patrón regex | `"1.*2.*3"` |
| `custom` | Función JS personalizada retorna true/false | `(output) => output.length > 10` |
---
## 📖 Guía de Configuración
@@ -754,97 +1279,6 @@ Configuración → Configuración de API:
---
## 📊 Modelos Disponibles
<details>
<summary><b>Ver todos los modelos disponibles</b></summary>
**Claude Code (`cc/`)** - Pro/Max:
- `cc/claude-opus-4-6`
- `cc/claude-sonnet-4-5-20250929`
- `cc/claude-haiku-4-5-20251001`
**Codex (`cx/`)** - Plus/Pro:
- `cx/gpt-5.2-codex`
- `cx/gpt-5.1-codex-max`
**Gemini CLI (`gc/`)** - GRATUITO:
- `gc/gemini-3-flash-preview`
- `gc/gemini-2.5-pro`
**GitHub Copilot (`gh/`)**:
- `gh/gpt-5`
- `gh/claude-4.5-sonnet`
**NVIDIA NIM (`nvidia/`)** - Créditos GRATUITOS:
- `nvidia/llama-3.3-70b-instruct`
- `nvidia/mistral-7b-instruct`
- 50+ más modelos en [build.nvidia.com](https://build.nvidia.com)
**GLM (`glm/`)** - $0.6/1M:
- `glm/glm-4.7`
**MiniMax (`minimax/`)** - $0.2/1M:
- `minimax/MiniMax-M2.1`
**iFlow (`if/`)** - GRATUITO:
- `if/kimi-k2-thinking`
- `if/qwen3-coder-plus`
- `if/deepseek-r1`
- `if/glm-4.7`
- `if/minimax-m2`
**Qwen (`qw/`)** - GRATUITO:
- `qw/qwen3-coder-plus`
- `qw/qwen3-coder-flash`
**Kiro (`kr/`)** - GRATUITO:
- `kr/claude-sonnet-4.5`
- `kr/claude-haiku-4.5`
**OpenRouter (`or/`)** - 100+ modelos:
- `or/anthropic/claude-4-sonnet`
- `or/google/gemini-2.5-pro`
- Cualquier modelo de [openrouter.ai/models](https://openrouter.ai/models)
</details>
---
## 🧪 Evaluaciones (Evals)
OmniRoute incluye un framework de evaluación integrado para probar la calidad de respuestas de LLM contra un conjunto golden. Accede vía **Analytics → Evals** en el dashboard.
### Conjunto Golden Integrado
El "OmniRoute Golden Set" precargado contiene 10 casos de prueba que cubren:
- Saludos, matemáticas, geografía, generación de código
- Conformidad de formato JSON, traducción, markdown
- Rechazo de seguridad (contenido dañino), conteo, lógica booleana
### Estrategias de Evaluación
| Estrategia | Descripción | Ejemplo |
| ---------- | ---------------------------------------------------- | -------------------------------- |
| `exact` | La salida debe coincidir exactamente | `"4"` |
| `contains` | La salida debe contener subcadena (case-insensitive) | `"Paris"` |
| `regex` | La salida debe coincidir con el patrón regex | `"1.*2.*3"` |
| `custom` | Función JS personalizada retorna true/false | `(output) => output.length > 10` |
---
## 🐛 Solución de Problemas
<details>
@@ -900,7 +1334,7 @@ El "OmniRoute Golden Set" precargado contiene 10 casos de prueba que cubren:
---
## 🛠️ Stack Tecnológico
## 🛠️
- **Runtime**: Node.js 20+
- **Lenguaje**: TypeScript 5.9 — **100% TypeScript** en `src/` y `open-sse/` (v1.0.6)
@@ -931,17 +1365,7 @@ El "OmniRoute Golden Set" precargado contiene 10 casos de prueba que cubren:
---
## 📧 Soporte
> 💬 **¡Únete a la comunidad!** [Grupo WhatsApp](https://chat.whatsapp.com/JI7cDQ1GyaiDHhVBpLxf8b?mode=gi_t) — Obtén ayuda, comparte consejos y mantente al día.
- **Website**: [omniroute.online](https://omniroute.online)
- **GitHub**: [github.com/diegosouzapw/OmniRoute](https://github.com/diegosouzapw/OmniRoute)
- **Issues**: [github.com/diegosouzapw/OmniRoute/issues](https://github.com/diegosouzapw/OmniRoute/issues)
- **WhatsApp**: [Grupo de la Comunidad](https://chat.whatsapp.com/JI7cDQ1GyaiDHhVBpLxf8b?mode=gi_t)
- **Proyecto Original**: [9router por decolua](https://github.com/decolua/9router)
---
## 🗺️
## 👥 Contribuidores

File diff suppressed because it is too large Load Diff

View File

@@ -110,6 +110,35 @@ _Connectez n'importe quel IDE ou outil CLI alimenté par l'IA via OmniRoute —
---
## 🖼️
<div align="center">
<img src="./docs/screenshots/MainOmniRoute.png" alt="OmniRoute" width="800"/>
</div>
---
## 📸
<details>
<summary><b>...</b></summary>
| # | # |
| ----- | ---------------------------------------- |
| **1** | ![1](docs/screenshots/01-providers.png) |
| **2** | ![2](docs/screenshots/02-combos.png) |
| **3** | ![3](docs/screenshots/03-analytics.png) |
| **4** | ![4](docs/screenshots/04-health.png) |
| **5** | ![5](docs/screenshots/05-translator.png) |
| **6** | ![6](docs/screenshots/06-settings.png) |
| **7** | ![7](docs/screenshots/07-cli-tools.png) |
| **8** | ![8](docs/screenshots/08-usage.png) |
| **9** | ![9](docs/screenshots/09-endpoint.png) |
</details>
---
## 🤔 Pourquoi OmniRoute ?
**Arrêtez de gaspiller de l'argent et de vous heurter aux limites :**
@@ -128,6 +157,18 @@ _Connectez n'importe quel IDE ou outil CLI alimenté par l'IA via OmniRoute —
---
## 📧 Support
> 💬 **Rejoignez notre communauté !** [Groupe WhatsApp](https://chat.whatsapp.com/JI7cDQ1GyaiDHhVBpLxf8b?mode=gi_t) — Obtenez de l'aide, partagez des astuces et restez informé.
- **Site web** : [omniroute.online](https://omniroute.online)
- **GitHub** : [github.com/diegosouzapw/OmniRoute](https://github.com/diegosouzapw/OmniRoute)
- **Issues** : [github.com/diegosouzapw/OmniRoute/issues](https://github.com/diegosouzapw/OmniRoute/issues)
- **WhatsApp** : [Groupe communautaire](https://chat.whatsapp.com/JI7cDQ1GyaiDHhVBpLxf8b?mode=gi_t)
- **Projet original** : [9router par decolua](https://github.com/decolua/9router)
---
## 🔄 Comment ça fonctionne
```
@@ -157,6 +198,497 @@ Résultat : Ne jamais arrêter de coder, coût minimal
---
## 🎯 Ce qu'OmniRoute résout : 30 problèmes réels et cas d'utilisation
> **Tous les développeurs utilisant des outils d'IA sont confrontés quotidiennement à ces problèmes.** OmniRoute a été conçu pour tous les résoudre : des dépassements de coûts aux blocages régionaux, des flux OAuth interrompus aux opérations de protocole et à l'observabilité de l'entreprise.
<details>
<summary><b>💸 1. "Je paie un abonnement coûteux mais je suis quand même interrompu par des limites" </b></summary>
Les développeurs paient entre 20 et 200 $/mois pour Claude Pro, Codex Pro ou GitHub Copilot. Même payant, le quota est plafonné : 5 heures d'utilisation, limites hebdomadaires ou limites de tarif à la minute. En cours de session de codage, le fournisseur ne répond plus et le développeur perd en fluidité et en productivité.
**Comment OmniRoute le résout :**
- **Smart 4-Tier Fallback** — Si le quota d'abonnement est épuisé, redirige automatiquement vers la clé API → Pas cher → Gratuit sans intervention manuelle
- **Suivi des quotas en temps réel** — Affiche la consommation de jetons en temps réel avec un compte à rebours réinitialisé (5 h, quotidiennement, hebdomadairement)
- **Support multi-comptes** — Plusieurs comptes par fournisseur avec tourniquet automatique — lorsqu'un compte est épuisé, passe au suivant
- **Combos personnalisés** — Chaînes de secours personnalisables avec 6 stratégies d'équilibrage (remplir en premier, round-robin, P2C, aléatoire, les moins utilisées, optimisées en termes de coûts)
- **Codex Business Quotas** — Surveillance des quotas d'espace de travail Business/Équipe directement dans le tableau de bord
</details>
<details>
<summary><b>🔌 2. "Je dois utiliser plusieurs fournisseurs mais chacun a une API différente" </b></summary>
OpenAI utilise un format, Claude (Anthropic) en utilise un autre, Gemini encore un autre. Si un développeur souhaite tester des modèles de différents fournisseurs ou utiliser un modèle de secours entre eux, il doit reconfigurer les SDK, modifier les points de terminaison et gérer les formats incompatibles. Les fournisseurs personnalisés (FriendLI, NIM) ont des points de terminaison de modèle non standard.
**Comment OmniRoute le résout :**
- **Point de terminaison unifié** : un seul `http://localhost:20128/v1` sert de proxy pour les plus de 36 fournisseurs.
- **Traduction de format** — Automatique et transparente : OpenAI ↔ Claude ↔ Gemini ↔ API Responses
- **Response Sanitization** — Supprime les champs non standard (`x_groq`, `usage_breakdown`, `service_tier`) qui cassent OpenAI SDK v1.83+
- **Role Normalization** — Convertit `developer``system` pour les fournisseurs non OpenAI ; `system``user` pour GLM/ERNIE
- **Think Tag Extraction** — Extrait les blocs `<think>` de modèles comme DeepSeek R1 dans un `reasoning_content` standardisé.
- **Sortie structurée pour Gemini** — Conversion automatique `json_schema``responseMimeType`/`responseSchema`
- **`stream` est par défaut `false`** — S'aligne sur les spécifications OpenAI, évitant ainsi le SSE inattendu dans les SDK Python/Rust/Go
</details>
<details>
<summary><b>🌐 3. "Mon fournisseur d'IA bloque ma région/pays" </b></summary>
Des fournisseurs comme OpenAI/Codex bloquent laccès depuis certaines régions géographiques. Les utilisateurs obtiennent des erreurs telles que `unsupported_country_region_territory` lors des connexions OAuth et API. Ceci est particulièrement frustrant pour les développeurs des pays en développement.
**Comment OmniRoute le résout :**
- **Configuration proxy à 3 niveaux** — Proxy configurable à 3 niveaux : global (tout le trafic), par fournisseur (un seul fournisseur) et par connexion/clé
- **Badges proxy à code couleur** — Indicateurs visuels : 🟢 proxy global, 🟡 proxy fournisseur, 🔵 proxy de connexion, affichant toujours l'adresse IP
- **Échange de jetons OAuth via proxy** — Le flux OAuth passe également par le proxy, résolvant `unsupported_country_region_territory`
- **Tests de connexion via proxy** — Les tests de connexion utilisent le proxy configuré (plus de contournement direct)
- **Support SOCKS5** — Prise en charge complète du proxy SOCKS5 pour le routage sortant
- **TLS Fingerprint Spoofing** — Empreinte digitale TLS de type navigateur via `wreq-js` pour contourner la détection des robots
</details>
<details>
<summary><b>🆓 4. "Je veux utiliser l'IA pour coder mais je n'ai pas d'argent"</b></summary>
Tout le monde ne peut pas payer entre 20 et 200 $/mois pour des abonnements à lIA. Les étudiants, les développeurs des pays émergents, les amateurs et les indépendants doivent avoir accès à des modèles de qualité à un coût nul.
**Comment OmniRoute le résout :**
- **Fournisseurs gratuits intégrés** — Prise en charge native des fournisseurs 100 % gratuits : iFlow (8 modèles illimités), Qwen (3 modèles illimités), Kiro (Claude gratuit), Gemini CLI (180 000 /mois gratuits)
- **Combos gratuits uniquement** — Chaîne `gc/gemini-3-flash → if/kimi-k2-thinking → qw/qwen3-coder-plus` = 0 $/mois sans temps d'arrêt
- **Crédits gratuits NVIDIA NIM** — 1 000 crédits gratuits intégrés
- **Stratégie d'optimisation des coûts** — Stratégie de routage qui choisit automatiquement le fournisseur disponible le moins cher
</details>
<details>
<summary><b>🔒 5. "Je dois protéger ma passerelle IA contre les accès non autorisés" </b></summary>
Lors de l'exposition d'une passerelle IA au réseau (LAN, VPS, Docker), toute personne possédant l'adresse peut consommer les jetons/quota du développeur. Sans protection, les API sont vulnérables aux utilisations abusives, aux injections rapides et aux abus.
**Comment OmniRoute le résout :**
- **Gestion des clés API** — Génération, rotation et portée par fournisseur avec une page `/dashboard/api-manager` dédiée
- **Autorisations au niveau du modèle** : restreindre les clés API à des modèles spécifiques (`openai/*`, modèles génériques), avec la bascule Autoriser tout/Restreindre
- **API Endpoint Protection**  exige une clé pour `/v1/models` et bloque des fournisseurs spécifiques de la liste
- **Auth Guard + Protection CSRF** — Toutes les routes du tableau de bord protégées avec le middleware `withAuth` + les jetons CSRF
- **Rate Limiter** — Limitation du débit par IP avec fenêtres configurables
- **Filtrage IP**  Liste autorisée/liste de blocage pour le contrôle d'accès
- **Prompt Injection Guard** — Nettoyage contre les modèles d'invite malveillants
- **Chiffrement AES-256-GCM** — Informations d'identification chiffrées au repos
</details>
<details>
<summary><b>🛑 6. "Mon fournisseur est tombé en panne et j'ai perdu mon flux de codage"</b></summary>
Les fournisseurs dIA peuvent devenir instables, renvoyer des erreurs 5xx ou atteindre des limites de débit temporaires. Si un développeur dépend d'un seul fournisseur, il est interrompu. Sans disjoncteurs, des tentatives répétées peuvent faire planter lapplication.
**Comment OmniRoute le résout :**
- **Disjoncteur par fournisseur** — Ouverture/fermeture automatique avec seuils et temps de recharge configurables (Fermé/Ouvert/Semi-ouvert)
- **Exponential Backoff** — Délais progressifs entre les tentatives
- **Anti-Thundering Herd** — Protection mutex + sémaphore contre les tempêtes de nouvelles tentatives simultanées
- **Chaînes de secours combinées** — Si le fournisseur principal échoue, passe automatiquement à travers la chaîne sans intervention
- **Combo Circuit Breaker**  Désactive automatiquement les fournisseurs défaillants au sein d'une chaîne combo
- **Tableau de bord de santé** — Surveillance de la disponibilité, états des disjoncteurs, verrouillages, statistiques du cache, latence p50/p95/p99
</details>
<details>
<summary><b>🔧 7. "La configuration de chaque outil d'IA est fastidieuse et répétitive"</b></summary>
Les développeurs utilisent Cursor, Claude Code, Codex CLI, OpenClaw, Gemini CLI, Kilo Code... Chaque outil nécessite une configuration différente (point de terminaison API, clé, modèle). La reconfiguration lors du changement de fournisseur ou de modèle est une perte de temps.
**Comment OmniRoute le résout :**
- **CLI Tools Dashboard** — Page dédiée avec configuration en un clic pour Claude Code, Codex CLI, OpenClaw, Kilo Code, Antigravity, Cline
- **GitHub Copilot Config Generator** — Génère `chatLanguageModels.json` pour VS Code avec sélection groupée de modèles
- **Assistant d'intégration** — Configuration guidée en 4 étapes pour les nouveaux utilisateurs
- **Un point de terminaison, tous les modèles**  Configurez `http://localhost:20128/v1` une fois, accédez à plus de 36 fournisseurs
</details>
<details>
<summary><b>🔑 8. "Gérer les jetons OAuth de plusieurs fournisseurs est un enfer"</b></summary>
Claude Code, Codex, Gemini CLI, Copilot — tous utilisent OAuth 2.0 avec des jetons expirant. Les développeurs doivent se réauthentifier constamment, gérer `client_secret is missing`, `redirect_uri_mismatch` et les pannes sur les serveurs distants. OAuth sur LAN/VPS est particulièrement problématique.
**Comment OmniRoute le résout :**
- **Actualisation automatique des jetons** : les jetons OAuth sont actualisés en arrière-plan avant leur expiration.
- **OAuth 2.0 (PKCE) intégré** — Flux automatique pour Claude Code, Codex, Gemini CLI, Copilot, Kiro, Qwen, iFlow
- **Multi-Account OAuth** — Plusieurs comptes par fournisseur via l'extraction de jetons JWT/ID
- **OAuth LAN/Remote Fix** — Détection IP privée pour `redirect_uri` + mode URL manuel pour les serveurs distants
- **OAuth derrière Nginx** — Utilise `window.location.origin` pour la compatibilité du proxy inverse
- **Guide OAuth à distance** — Guide étape par étape pour les informations d'identification Google Cloud sur VPS/Docker
</details>
<details>
<summary><b>📊 9. "Je ne sais pas combien je dépense ni où" </b></summary>
Les développeurs utilisent plusieurs fournisseurs payants mais n'ont pas de vue unifiée des dépenses. Chaque fournisseur dispose de son propre tableau de bord de facturation, mais il n'existe pas de vue consolidée. Les coûts inattendus peuvent saccumuler.
**Comment OmniRoute le résout :**
- **Cost Analytics Dashboard** — Suivi des coûts par jeton et gestion du budget par fournisseur
- **Limites budgétaires par niveau** — Plafond de dépenses par niveau qui déclenche un repli automatique
- **Configuration de tarification par modèle** — Prix configurables par modèle
- **Statistiques d'utilisation par clé API** — Nombre de demandes et horodatage de la dernière utilisation par clé
- **Tableau de bord Analytics** — Cartes statistiques, tableau d'utilisation du modèle, tableau des fournisseurs avec taux de réussite et latence
</details>
<details>
<summary><b>🐛 10. "Je ne peux pas diagnostiquer les erreurs et les problèmes dans les appels IA" </b></summary>
Lorsqu'un appel échoue, le développeur ne sait pas s'il s'agit d'une limite de débit, d'un jeton expiré, d'un format incorrect ou d'une erreur du fournisseur. Journaux fragmentés sur différents terminaux. Sans observabilité, le débogage est un essai et une erreur.
**Comment OmniRoute le résout :**
- **Tableau de bord des journaux unifiés** — 4 onglets : journaux de requêtes, journaux proxy, journaux d'audit, console
- **Console Log Viewer** — Visualiseur de style terminal en temps réel avec niveaux de code couleur, défilement automatique, recherche, filtre
- **Journaux du proxy SQLite** — Journaux persistants qui survivent aux redémarrages du serveur
- **Translator Playground** — 4 modes de débogage : Playground (traduction de format), Chat Tester (aller-retour), Test Bench (batch), Live Monitor (temps réel)
- **Demande de télémétrie** — latence p50/p95/p99 + traçage X-Request-Id
- **Journalisation basée sur des fichiers avec rotation** — L'intercepteur de console capture tout dans le journal JSON avec une rotation basée sur la taille
</details>
<details>
<summary><b>🏗️ 11. "Le déploiement et la maintenance de la passerelle sont complexes"</b></summary>
L'installation, la configuration et la maintenance d'un proxy IA dans différents environnements (local, VPS, Docker, cloud) demandent beaucoup de main-d'œuvre. Des problèmes tels que les chemins codés en dur, `EACCES` sur les répertoires, les conflits de ports et les versions multiplateformes ajoutent des frictions.
**Comment OmniRoute le résout :**
- **Installation globale npm** — `npm install -g omniroute && omniroute` — terminée
- **Docker Multi-Platform** — AMD64 + ARM64 natif (Apple Silicon, AWS Graviton, Raspberry Pi)
- **Profils Docker Compose** — `base` (pas d'outils CLI) et `cli` (avec Claude Code, Codex, OpenClaw)
- **Electron Desktop App** — Application native pour Windows/macOS/Linux avec barre d'état système, démarrage automatique et mode hors ligne
- **Mode Split-Port** — API et tableau de bord sur des ports séparés pour des scénarios avancés (proxy inverse, réseau de conteneurs)
- **Cloud Sync**  Configurez la synchronisation entre les appareils via Cloudflare Workers
- **Sauvegardes DB** — Sauvegarde, restauration, exportation et importation automatiques de tous les paramètres
</details>
<details>
<summary><b>🌍 12. "L'interface est uniquement en anglais et mon équipe ne parle pas anglais" </b></summary>
Les équipes des pays non anglophones, notamment en Amérique latine, en Asie et en Europe, ont du mal à utiliser des interfaces uniquement en anglais. Les barrières linguistiques réduisent ladoption et augmentent les erreurs de configuration.
**Comment OmniRoute le résout :**
- **Tableau de bord i18n — 30 langues** — Plus de 500 touches traduites, dont arabe, bulgare, danois, allemand, espagnol, finnois, français, hébreu, hindi, hongrois, indonésien, italien, japonais, coréen, malais, néerlandais, norvégien, polonais, portugais (PT/BR), roumain, russe, slovaque, suédois, thaï, ukrainien, vietnamien, chinois, philippin, anglais.
- **Support RTL** — Prise en charge de droite à gauche pour l'arabe et l'hébreu
- ** README multilingues ** — 30 traductions complètes de la documentation
- **Sélecteur de langue** — Icône de globe dans l'en-tête pour une commutation en temps réel
</details>
<details>
<summary><b>🔄 13. "J'ai besoin de plus que du chat : j'ai besoin d'intégrations, d'images, d'audio" </b></summary>
L'IA ne se limite pas à la réalisation de discussions. Les développeurs doivent générer des images, transcrire l'audio, créer des intégrations pour RAG, reclasser les documents et modérer le contenu. Chaque API a un point de terminaison et un format différents.
**Comment OmniRoute le résout :**
- **Embeddings** — `/v1/embeddings` avec 6 fournisseurs et plus de 9 modèles
- **Génération d'images** — `/v1/images/generations` avec 10 fournisseurs et plus de 20 modèles (OpenAI, xAI, Together, Fireworks, Nebius, Hyperbolic, NanoBanana, Antigravity, SD WebUI, ComfyUI)
- **Texte vers vidéo** — `/v1/videos/generations` — ComfyUI (AnimateDiff, SVD) et SD WebUI
- **Texte en musique** — `/v1/music/generations` — ComfyUI (Stable Audio Open, MusicGen)
- **Transcription audio** — `/v1/audio/transcriptions` — Whisper + Nvidia NIM, HuggingFace, Qwen3
- **Text-to-Speech** — `/v1/audio/speech` — ElevenLabs, Nvidia NIM, HuggingFace, Coqui, Tortoise, Qwen3, + fournisseurs existants
- **Modérations** — `/v1/moderations` — Contrôles de sécurité du contenu
- **Reclassement** — `/v1/rerank` — Reclassement de la pertinence du document
- **API Réponses** — Prise en charge complète de `/v1/responses` pour le Codex
</details>
<details>
<summary><b>🧪 14. "Je n'ai aucun moyen de tester et de comparer la qualité des modèles" </b></summary>
Les développeurs veulent savoir quel modèle convient le mieux à leur cas d'utilisation (code, traduction, raisonnement) mais la comparaison manuelle est lente. Il nexiste aucun outil dévaluation intégré.
**Comment OmniRoute le résout :**
- **Évaluations LLM** — Tests Golden Set avec 10 cas préchargés couvrant les salutations, les mathématiques, la géographie, la génération de code, la conformité JSON, la traduction, la démarque, le refus de sécurité
- **4 stratégies de correspondance** — `exact`, `contains`, `regex`, `custom` (fonction JS)
- **Banc de test Translator Playground** — Tests par lots avec plusieurs entrées et sorties attendues, comparaison entre fournisseurs
- **Chat Tester** — Aller-retour complet avec rendu de réponse visuelle
- **Live Monitor** — Flux en temps réel de toutes les requêtes transitant par le proxy
</details>
<details>
<summary><b>📈 15. "J'ai besoin d'évoluer sans perdre en performances"</b></summary>
À mesure que le volume de demandes augmente, sans mettre en cache les mêmes questions, cela génère des coûts en double. Sans idempotence, les demandes en double gaspillent le traitement. Les limites tarifaires par fournisseur doivent être respectées.
**Comment OmniRoute le résout :**
- **Cache sémantique** — Le cache à deux niveaux (signature + sémantique) réduit les coûts et la latence
- **Request Idempotency** — Fenêtre de déduplication de 5 s pour des requêtes identiques
- **Détection de limite de débit** — RPM par fournisseur, écart minimum et suivi simultané maximum
- **Limites de débit modifiables** — Valeurs par défaut configurables dans Paramètres → Résilience avec persistance
- **Cache de validation de clé API** — Cache à 3 niveaux pour les performances de production
- **Tableau de bord de santé avec télémétrie** — latence p50/p95/p99, statistiques de cache, disponibilité
</details>
<details>
<summary><b>🤖 16. "Je souhaite contrôler le comportement du modèle à l'échelle mondiale" </b></summary>
Les développeurs qui souhaitent que toutes les réponses soient dans une langue spécifique, avec un ton spécifique, ou qui souhaitent limiter les jetons de raisonnement. Configurer cela dans chaque outil/demande nest pas pratique.
**Comment OmniRoute le résout :**
- **Injection d'invite système** — Invite globale appliquée à toutes les requêtes
- **Thinking Budget Validation** — Contrôle d'allocation de jetons de raisonnement par requête (passthrough, automatique, personnalisé, adaptatif)
- **6 Stratégies de routage**  Stratégies globales qui déterminent la façon dont les demandes sont distribuées
- **Wildcard Router** — Les modèles `provider/*` sont acheminés dynamiquement vers n'importe quel fournisseur.
- **Combo Enable/Disable Toggle** — Basculez les combos directement depuis le tableau de bord
- **Provider Toggle** — Activer/désactiver toutes les connexions pour un fournisseur en un seul clic
- **Fournisseurs bloqués** — Exclure des fournisseurs spécifiques de la liste `/v1/models`
</details>
<details>
<summary><b>🧰 17. "J'ai besoin d'outils MCP en tant que fonctionnalités de produit de première classe" </b></summary>
De nombreuses passerelles IA exposent MCP uniquement en tant que détail d'implémentation caché. Les équipes ont besoin dune couche opérationnelle visible et gérable.
**Comment OmniRoute le résout :**
- MCP apparaît dans l'onglet de navigation du tableau de bord et de protocole de point de terminaison
- Page de gestion MCP dédiée avec processus, outils, portées et audit
- Démarrage rapide intégré pour `omniroute --mcp` et intégration du client
</details>
<details>
<summary><b>🧠 18. "J'ai besoin d'une orchestration A2A avec des chemins de tâches de synchronisation + flux" </b></summary>
Les flux de travail des agents nécessitent à la fois des réponses directes et une exécution en continu de longue durée avec contrôle du cycle de vie.
**Comment OmniRoute le résout :**
- Point de terminaison A2A JSON-RPC (`POST /a2a`) avec `message/send` et `message/stream`
- Streaming SSE avec propagation de l'état terminal
- API de cycle de vie des tâches pour `tasks/get` et `tasks/cancel`
</details>
<details>
<summary><b>🛰️ 19. "J'ai besoin d'un véritable état de santé du processus MCP, et non d'un état deviné" </b></summary>
Les équipes opérationnelles doivent savoir si MCP est réellement actif, et pas seulement si une API est accessible.
**Comment OmniRoute le résout :**
- Fichier de battement de cœur d'exécution avec PID, horodatages, transport, nombre d'outils et mode de portée
- API de statut MCP combinant battement de coeur + activité récente
- Cartes d'état de l'interface utilisateur pour la fraîcheur des processus/disponibilité/battement de cœur
</details>
<details>
<summary><b>📋 20. "J'ai besoin d'une exécution vérifiable de l'outil MCP" </b></summary>
Lorsque les outils modifient la configuration ou déclenchent des actions opérationnelles, les équipes ont besoin d'une traçabilité médico-légale.
**Comment OmniRoute le résout :**
- Journalisation d'audit basée sur SQLite pour les appels d'outils MCP
- Filtres par outil, succès/échec, clé API et pagination
- Tableau d'audit du tableau de bord + points de terminaison de statistiques pour l'automatisation
</details>
<details>
<summary><b>🔐 21. "J'ai besoin d'autorisations MCP limitées par intégration" </b></summary>
Différents clients doivent avoir le moindre privilège daccès aux catégories doutils.
**Comment OmniRoute le résout :**
- 9 étendues MCP granulaires pour un accès contrôlé aux outils
- Application de la portée et visibilité dans l'interface utilisateur de gestion MCP
- Posture par défaut sûre pour les outils opérationnels
</details>
<details>
<summary><b>⚙️ 22. "J'ai besoin de contrôles opérationnels sans redéploiement"</b></summary>
Les équipes ont besoin de changements d'exécution rapides lors d'incidents ou d'événements de coûts.
**Comment OmniRoute le résout :**
- Activer le combo de commutation directement depuis le tableau de bord MCP
- Appliquer des profils de résilience à partir de packs de politiques prédéfinis
- Réinitialiser l'état du disjoncteur à partir du même panneau de commande
</details>
<details>
<summary><b>🔄 23. "J'ai besoin d'une visibilité et d'une annulation en direct du cycle de vie des tâches A2A" </b></summary>
Sans visibilité sur le cycle de vie, les incidents de tâches deviennent difficiles à trier.
**Comment OmniRoute le résout :**
- Liste des tâches/filtrage par état/compétence avec pagination
- Analyse approfondie des métadonnées, des événements et des artefacts des tâches
- Point de terminaison d'annulation de tâche et action de l'interface utilisateur avec confirmation
</details>
<details>
<summary><b>🌊 24. "J'ai besoin de métriques de flux actif pour la charge A2A" </b></summary>
Les flux de travail de streaming nécessitent une vision opérationnelle de la concurrence et des connexions en direct.
**Comment OmniRoute le résout :**
- Compteurs de flux actifs intégrés au statut A2A
- Horodatage de la dernière tâche et nombre par état
- Cartes de tableau de bord A2A pour la surveillance des opérations en temps réel
</details>
<details>
<summary><b>🪪 25. "J'ai besoin d'une découverte d'agent standard pour les clients" </b></summary>
Les clients et orchestrateurs externes ont besoin de métadonnées lisibles par machine pour l'intégration.
**Comment OmniRoute le résout :**
- Carte d'agent exposée à `/.well-known/agent.json`
- Capacités et compétences affichées dans l'interface utilisateur de gestion
- L'API de statut A2A inclut des métadonnées de découverte pour l'automatisation
</details>
<details>
<summary><b>🧭 26. "J'ai besoin de la possibilité de découvrir le protocole dans le produit UX"</b></summary>
Si les utilisateurs ne peuvent pas découvrir les surfaces de protocole, ladoption et la qualité du support chutent.
**Comment OmniRoute le résout :**
- Entrées de la barre latérale pour MCP et A2A
- Onglet Protocoles de la page du point de terminaison avec démarrage rapide et état
- Liens depuis l'aperçu vers les tableaux de bord de gestion dédiés
</details>
<details>
<summary><b>🧪 27. "J'ai besoin d'une validation de protocole de bout en bout avec de vrais clients"</b></summary>
Les tests simulés ne suffisent pas pour valider la compatibilité des protocoles avant la publication.
**Comment OmniRoute le résout :**
- Suite E2E qui démarre l'application et utilise un véritable transport client MCP SDK
- Tests client A2A pour les flux de découverte, d'envoi, de streaming, d'obtention et d'annulation
- Vérifier les assertions par rapport aux API d'audit MCP et de tâches A2A
</details>
<details>
<summary><b>📡 28. "J'ai besoin d'une observabilité unifiée sur toutes les interfaces"</b></summary>
Le fractionnement de l'observabilité par protocole crée des angles morts et un MTTR plus long.
**Comment OmniRoute le résout :**
- Tableaux de bord/journaux/analyses unifiés dans un seul produit
- Santé + audit + télémétrie des demandes sur les couches OpenAI, MCP et A2A
- API opérationnelles pour le statut et l'automatisation
</details>
<details>
<summary><b>💼 29. "J'ai besoin d'un environnement d'exécution pour l'orchestration proxy + outils + agent" </b></summary>
Lexécution de nombreux services distincts augmente les coûts opérationnels et les modes de défaillance.
**Comment OmniRoute le résout :**
- Proxy compatible OpenAI, serveur MCP et serveur A2A dans une seule pile
- Authentification partagée, résilience, stockage de données et observabilité
- Modèle de politique cohérent sur toutes les surfaces d'interaction
</details>
<details>
<summary><b>🚀 30. "Je dois expédier des flux de travail agentiques sans prolifération de codes adhésifs" </b></summary>
Les équipes perdent de la vitesse lors de lassemblage de plusieurs services et scripts ad hoc.
**Comment OmniRoute le résout :**
- Stratégie de point de terminaison unifiée pour les clients et les agents
- Interfaces utilisateur de gestion de protocole intégrées et chemins de validation de fumée
- Bases prêtes pour la production (sécurité, journalisation, résilience, sauvegarde)
</details>
### Exemples de playbooks (cas d'utilisation intégrés)
**Playbook A : Maximisez l'abonnement payant + sauvegarde bon marché**
```txt
Combo: "maximize-claude"
1. cc/claude-opus-4-6
2. glm/glm-4.7
3. if/kimi-k2-thinking
Monthly cost: $20 + small backup spend
Outcome: higher quality, near-zero interruption
```
**Playbook B : pile de codage à coût nul**
```txt
Combo: "free-forever"
1. gc/gemini-3-flash
2. if/kimi-k2-thinking
3. qw/qwen3-coder-plus
Monthly cost: $0
Outcome: stable free coding workflow
```
**Playbook C : chaîne de secours toujours active 24h/24 et 7j/7**
```txt
Combo: "always-on"
1. cc/claude-opus-4-6
2. cx/gpt-5.2-codex
3. glm/glm-4.7
4. minimax/MiniMax-M2.1
5. if/kimi-k2-thinking
Outcome: deep fallback depth for deadline-critical workloads
```
**Playbook D : Opérations d'agent avec MCP + A2A**
```txt
1) Start MCP transport (`omniroute --mcp`) for tool-driven operations
2) Run A2A tasks via `message/send` and `message/stream`
3) Observe via /dashboard/mcp and /dashboard/a2a
4) Control incidents with resilience profile + task cancellation
```
---
## ⚡ Démarrage rapide
**1. Installer globalement :**
@@ -247,6 +779,32 @@ docker compose --profile cli up -d
---
---
## 🖥️
> 🆕 **NOUVEAU !** OmniRoute est maintenant disponible en tant qu'**application de bureau native** pour Windows, macOS et Linux.
Exécutez OmniRoute en tant qu'application de bureau autonome — sans terminal, sans navigateur, sans internet requis pour les modèles locaux.
- 🖥️ **Fenêtre Native** — Fenêtre dédiée avec intégration dans la barre d'état système
- 🔄 **Démarrage Automatique** — Lancez OmniRoute à la connexion système
- 🔔 **Notifications Natives** — Alertes pour l'épuisement de quota ou les problèmes
-**Installation en Un Clic** — NSIS (Windows), DMG (macOS), AppImage (Linux)
- 🌐 **Mode Hors Ligne** — Fonctionne entièrement hors ligne avec serveur intégré
```bash
npm run electron:dev # Mode développement
npm run electron:build # Plateforme actuelle
npm run electron:build:win # Windows (.exe)
npm run electron:build:mac # macOS (.dmg)
npm run electron:build:linux # Linux (.AppImage)
```
📖 Documentation complète : [`electron/README.md`](electron/README.md)
---
## 💰 Aperçu des tarifs
| Tier | Fournisseur | Coût | Réinitialisation | Idéal pour |
@@ -272,67 +830,6 @@ docker compose --profile cli up -d
---
## 🎯 Cas d'utilisation
### Cas 1 : « J'ai un abonnement Claude Pro »
**Problème :** Le quota expire inutilisé, limites de débit pendant le codage intensif
```
Combo : "maximize-claude"
1. cc/claude-opus-4-6 (utiliser l'abonnement au maximum)
2. glm/glm-4.7 (backup économique quand le quota est épuisé)
3. if/kimi-k2-thinking (fallback d'urgence gratuit)
Coût mensuel : 20 $ (abonnement) + ~5 $ (backup) = 25 $ au total
vs. 20 $ + atteindre les limites = frustration
```
### Cas 2 : « Je veux zéro coût »
**Problème :** Impossible de payer des abonnements, besoin d'IA fiable pour coder
```
Combo : "free-forever"
1. gc/gemini-3-flash (180K gratuits/mois)
2. if/kimi-k2-thinking (illimité gratuit)
3. qw/qwen3-coder-plus (illimité gratuit)
Coût mensuel : 0 $
Qualité : Modèles prêts pour la production
```
### Cas 3 : « Je dois coder 24/7, sans interruption »
**Problème :** Délais serrés, ne peut pas se permettre de temps d'arrêt
```
Combo : "always-on"
1. cc/claude-opus-4-6 (meilleure qualité)
2. cx/gpt-5.2-codex (deuxième abonnement)
3. glm/glm-4.7 (économique, reset quotidien)
4. minimax/MiniMax-M2.1 (le moins cher, reset 5h)
5. if/kimi-k2-thinking (gratuit illimité)
Résultat : 5 niveaux de fallback = zéro temps d'arrêt
```
### Cas 4 : « Je veux l'IA GRATUITE dans OpenClaw »
**Problème :** Besoin d'assistant IA dans les apps de messagerie, entièrement gratuit
```
Combo : "openclaw-free"
1. if/glm-4.7 (illimité gratuit)
2. if/minimax-m2.1 (illimité gratuit)
3. if/kimi-k2-thinking (illimité gratuit)
Coût mensuel : 0 $
Accès via : WhatsApp, Telegram, Slack, Discord, iMessage, Signal...
```
---
## 💡 Fonctionnalités principales
### 🧠 Routage & Intelligence
@@ -348,6 +845,8 @@ Accès via : WhatsApp, Telegram, Slack, Discord, iMessage, Signal...
| 🧩 **Modèles personnalisés** | Ajoutez n'importe quel ID de modèle à n'importe quel fournisseur |
| 🌐 **Routeur wildcard** | Routez les patterns `provider/*` vers n'importe quel fournisseur dynamiquement |
| 🧠 **Budget de raisonnement** | Modes passthrough, auto, custom et adaptive pour les modèles de raisonnement |
| 🔀 **Model Aliases** | Auto-forward deprecated model IDs to current replacements (built-in + custom) |
| ⚡ **Background Degradation** | Auto-route background tasks (titles, summaries) to cheaper models |
| 💬 **Injection System Prompt** | System prompt global appliqué à toutes les requêtes |
| 📄 **API Responses** | Support complet de l'API Responses d'OpenAI (`/v1/responses`) pour Codex |
@@ -364,15 +863,18 @@ Accès via : WhatsApp, Telegram, Slack, Discord, iMessage, Signal...
### 🛡️ Résilience & Sécurité
| Fonctionnalité | Ce qu'elle fait |
| ------------------------------- | -------------------------------------------------------------------- |
| 🔌 **Circuit Breaker** | Ouverture/fermeture auto par fournisseur avec seuils configurables |
| 🛡️ **Anti-Thundering Herd** | Mutex + sémaphore de rate-limit pour les fournisseurs avec clé API |
| 🧠 **Cache sémantique** | Cache à deux niveaux (signature + sémantique) réduit coût et latence |
| **Idempotence des requêtes** | Fenêtre de dédup 5s pour les requêtes dupliquées |
| 🔒 **Spoofing TLS Fingerprint** | Contournement de détection de bot via wreq-js |
| 🌐 **Filtrage IP** | Allowlist/blocklist pour le contrôle d'accès API |
| 📊 **Rate limits éditables** | RPM configurable, intervalle minimum, concurrence max |
| Fonctionnalité | Ce qu'elle fait |
| ------------------------------- | ---------------------------------------------------------------------------- |
| 🔌 **Circuit Breaker** | Ouverture/fermeture auto par fournisseur avec seuils configurables |
| 🎯 **Endpoint-Aware Models** | Custom models declare supported endpoints + API format |
| 🛡️ **Anti-Thundering Herd** | Mutex + sémaphore de rate-limit pour les fournisseurs avec clé API |
| 🧠 **Cache sémantique** | Cache à deux niveaux (signature + sémantique) réduit coût et latence |
| **Idempotence des requêtes** | Fenêtre de dédup 5s pour les requêtes dupliquées |
| 🔒 **Spoofing TLS Fingerprint** | Contournement de détection de bot via wreq-js |
| 🌐 **Filtrage IP** | Allowlist/blocklist pour le contrôle d'accès API |
| 📊 **Rate limits éditables** | RPM configurable, intervalle minimum, concurrence max |
| 💾 **Rate Limit Persistence** | Learned limits survive restarts via SQLite with 60s debounce + 24h staleness |
| 🔄 **Token Refresh Resilience** | Per-provider circuit breaker (5 fails→30min) + 30s timeout per attempt |
### 📊 Observabilité & Analytique
@@ -472,6 +974,27 @@ Traduction transparente entre les formats :
</details>
## 🧪 Évaluations (Evals)
OmniRoute inclut un framework d'évaluation intégré pour tester la qualité des réponses LLM contre un golden set. Accès via **Analytics → Evals** dans le tableau de bord.
### Set intégré
Le « OmniRoute Golden Set » préchargé contient 10 cas de test :
- Salutations, mathématiques, géographie, génération de code
- Conformité format JSON, traduction, markdown
- Rejet de sécurité (contenu nocif), comptage, logique booléenne
### Stratégies d'évaluation
| Stratégie | Description | Exemple |
| ---------- | -------------------------------------------------------------- | -------------------------------- |
| `exact` | La sortie doit correspondre exactement | `"4"` |
| `contains` | La sortie doit contenir la sous-chaîne (insensible à la casse) | `"Paris"` |
| `regex` | La sortie doit correspondre au motif regex | `"1.*2.*3"` |
| `custom` | Fonction JS personnalisée retourne true/false | `(output) => output.length > 10` |
---
## 📖 Guide de configuration
@@ -754,97 +1277,6 @@ Paramètres → Configuration API :
---
## 📊 Modèles disponibles
<details>
<summary><b>Voir tous les modèles disponibles</b></summary>
**Claude Code (`cc/`)** - Pro/Max :
- `cc/claude-opus-4-6`
- `cc/claude-sonnet-4-5-20250929`
- `cc/claude-haiku-4-5-20251001`
**Codex (`cx/`)** - Plus/Pro :
- `cx/gpt-5.2-codex`
- `cx/gpt-5.1-codex-max`
**Gemini CLI (`gc/`)** - GRATUIT :
- `gc/gemini-3-flash-preview`
- `gc/gemini-2.5-pro`
**GitHub Copilot (`gh/`)** :
- `gh/gpt-5`
- `gh/claude-4.5-sonnet`
**NVIDIA NIM (`nvidia/`)** - Crédits GRATUITS :
- `nvidia/llama-3.3-70b-instruct`
- `nvidia/mistral-7b-instruct`
- 50+ modèles sur [build.nvidia.com](https://build.nvidia.com)
**GLM (`glm/`)** - $0.6/1M :
- `glm/glm-4.7`
**MiniMax (`minimax/`)** - $0.2/1M :
- `minimax/MiniMax-M2.1`
**iFlow (`if/`)** - GRATUIT :
- `if/kimi-k2-thinking`
- `if/qwen3-coder-plus`
- `if/deepseek-r1`
- `if/glm-4.7`
- `if/minimax-m2`
**Qwen (`qw/`)** - GRATUIT :
- `qw/qwen3-coder-plus`
- `qw/qwen3-coder-flash`
**Kiro (`kr/`)** - GRATUIT :
- `kr/claude-sonnet-4.5`
- `kr/claude-haiku-4.5`
**OpenRouter (`or/`)** - 100+ modèles :
- `or/anthropic/claude-4-sonnet`
- `or/google/gemini-2.5-pro`
- Tout modèle de [openrouter.ai/models](https://openrouter.ai/models)
</details>
---
## 🧪 Évaluations (Evals)
OmniRoute inclut un framework d'évaluation intégré pour tester la qualité des réponses LLM contre un golden set. Accès via **Analytics → Evals** dans le tableau de bord.
### Golden Set intégré
Le « OmniRoute Golden Set » préchargé contient 10 cas de test :
- Salutations, mathématiques, géographie, génération de code
- Conformité format JSON, traduction, markdown
- Rejet de sécurité (contenu nocif), comptage, logique booléenne
### Stratégies d'évaluation
| Stratégie | Description | Exemple |
| ---------- | -------------------------------------------------------------- | -------------------------------- |
| `exact` | La sortie doit correspondre exactement | `"4"` |
| `contains` | La sortie doit contenir la sous-chaîne (insensible à la casse) | `"Paris"` |
| `regex` | La sortie doit correspondre au motif regex | `"1.*2.*3"` |
| `custom` | Fonction JS personnalisée retourne true/false | `(output) => output.length > 10` |
---
## 🐛 Dépannage
<details>
@@ -900,7 +1332,7 @@ Le « OmniRoute Golden Set » préchargé contient 10 cas de test :
---
## 🛠️ Stack technologique
## 🛠️
- **Runtime** : Node.js 20+
- **Langage** : TypeScript 5.9 — **100% TypeScript** dans `src/` et `open-sse/` (v1.0.6)
@@ -931,17 +1363,7 @@ Le « OmniRoute Golden Set » préchargé contient 10 cas de test :
---
## 📧 Support
> 💬 **Rejoignez notre communauté !** [Groupe WhatsApp](https://chat.whatsapp.com/JI7cDQ1GyaiDHhVBpLxf8b?mode=gi_t) — Obtenez de l'aide, partagez des astuces et restez informé.
- **Site web** : [omniroute.online](https://omniroute.online)
- **GitHub** : [github.com/diegosouzapw/OmniRoute](https://github.com/diegosouzapw/OmniRoute)
- **Issues** : [github.com/diegosouzapw/OmniRoute/issues](https://github.com/diegosouzapw/OmniRoute/issues)
- **WhatsApp** : [Groupe communautaire](https://chat.whatsapp.com/JI7cDQ1GyaiDHhVBpLxf8b?mode=gi_t)
- **Projet original** : [9router par decolua](https://github.com/decolua/9router)
---
## 🗺️
## 👥 Contributeurs

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -110,6 +110,35 @@ _Connetti qualsiasi IDE o strumento CLI con IA tramite OmniRoute — gateway API
---
## 🖼️
<div align="center">
<img src="./docs/screenshots/MainOmniRoute.png" alt="OmniRoute" width="800"/>
</div>
---
## 📸
<details>
<summary><b>...</b></summary>
| # | # |
| ----- | ---------------------------------------- |
| **1** | ![1](docs/screenshots/01-providers.png) |
| **2** | ![2](docs/screenshots/02-combos.png) |
| **3** | ![3](docs/screenshots/03-analytics.png) |
| **4** | ![4](docs/screenshots/04-health.png) |
| **5** | ![5](docs/screenshots/05-translator.png) |
| **6** | ![6](docs/screenshots/06-settings.png) |
| **7** | ![7](docs/screenshots/07-cli-tools.png) |
| **8** | ![8](docs/screenshots/08-usage.png) |
| **9** | ![9](docs/screenshots/09-endpoint.png) |
</details>
---
## 🤔 Perché OmniRoute?
**Smetti di sprecare soldi e di sbattere contro i limiti:**
@@ -128,6 +157,19 @@ _Connetti qualsiasi IDE o strumento CLI con IA tramite OmniRoute — gateway API
---
## 📧 Supporto
> 💬 **Unisciti alla nostra community!** [Gruppo WhatsApp](https://chat.whatsapp.com/JI7cDQ1GyaiDHhVBpLxf8b?mode=gi_t) — Ottieni aiuto, condividi consigli e rimani aggiornato.
- **Sito Web**: [omniroute.online](https://omniroute.online)
- **GitHub**: [github.com/diegosouzapw/OmniRoute](https://github.com/diegosouzapw/OmniRoute)
- **Issues**: [github.com/diegosouzapw/OmniRoute/issues](https://github.com/diegosouzapw/OmniRoute/issues)
- **WhatsApp**: [Gruppo della comunità](https://chat.whatsapp.com/JI7cDQ1GyaiDHhVBpLxf8b?mode=gi_t)
- **WhatsApp**: [Gruppo della comunità](https://chat.whatsapp.com/JI7cDQ1GyaiDHhVBpLxf8b?mode=gi_t)
- **Progetto Originale**: [9router di decolua](https://github.com/decolua/9router)
---
## 🔄 Come Funziona
```
@@ -157,6 +199,497 @@ Risultato: Non smettere mai di programmare, costo minimo
---
## 🎯 Cosa risolve OmniRoute: 30 punti critici reali e casi d'uso
> **Ogni sviluppatore che utilizza strumenti di intelligenza artificiale affronta questi problemi quotidianamente.** OmniRoute è stato creato per risolverli tutti: dai superamenti dei costi ai blocchi regionali, dai flussi OAuth interrotti alle operazioni di protocollo e all'osservabilità aziendale.
<details>
<summary><b>💸 1. "Pago un abbonamento costoso ma vengo comunque interrotto dai limiti"</b></summary>
Gli sviluppatori pagano $ 20-200 al mese per Claude Pro, Codex Pro o GitHub Copilot. Anche pagando, la quota ha un tetto: 5 ore di utilizzo, limiti settimanali o limiti di tariffa al minuto. A metà sessione di codifica, il provider smette di rispondere e lo sviluppatore perde flusso e produttività.
**Come OmniRoute risolve il problema:**
- **Fallback intelligente a 4 livelli**: se la quota dell'abbonamento si esaurisce, reindirizza automaticamente alla chiave API → Economico → Gratuito senza alcun intervento manuale
- **Monitoraggio delle quote in tempo reale**: mostra il consumo di token in tempo reale con il conto alla rovescia ripristinato (5 ore, giornaliero, settimanale)
- **Supporto multi-account**: più account per fornitore con round robin automatico: quando uno si esaurisce, passa a quello successivo
- **Combo personalizzate** — Catene di fallback personalizzabili con 6 strategie di bilanciamento (fill-first, round-robin, P2C, casuale, meno utilizzato, ottimizzato in termini di costi)
- **Quote aziendali Codex**: monitoraggio delle quote dello spazio di lavoro aziendale/team direttamente nella dashboard
</details>
<details>
<summary><b>🔌 2. "Devo utilizzare più provider ma ognuno ha un'API diversa"</b></summary>
OpenAI utilizza un formato, Claude (Anthropic) ne utilizza un altro, Gemini ancora un altro. Se uno sviluppatore desidera testare modelli di fornitori diversi o eseguire il fallback tra di loro, deve riconfigurare gli SDK, modificare gli endpoint e gestire formati incompatibili. I provider personalizzati (FriendLI, NIM) hanno endpoint del modello non standard.
**Come OmniRoute risolve il problema:**
- **Endpoint unificato**: un singolo `http://localhost:20128/v1` funge da proxy per tutti gli oltre 36 provider
- **Traduzione del formato** — Automatica e trasparente: OpenAI ↔ Claude ↔ Gemini ↔ API di risposta
- **Sanitizzazione della risposta**: rimuove i campi non standard (`x_groq`, `usage_breakdown`, `service_tier`) che interrompono OpenAI SDK v1.83+
- **Normalizzazione del ruolo**: converte `developer``system` per provider non OpenAI; `system``user` per GLM/ERNIE
- **Think Tag Extraction** — Estrae i blocchi `<think>` da modelli come DeepSeek R1 in `reasoning_content` standardizzati
- **Uscita strutturata per Gemini** — `json_schema``responseMimeType`/`responseSchema` conversione automatica
- **`stream` per impostazione predefinita è `false`** — Si allinea con le specifiche OpenAI, evitando SSE imprevisti negli SDK Python/Rust/Go
</details>
<details>
<summary><b>🌐 3. "Il mio fornitore di intelligenza artificiale blocca la mia regione/paese"</b></summary>
Provider come OpenAI/Codex bloccano l'accesso da determinate regioni geografiche. Gli utenti ricevono errori come `unsupported_country_region_territory` durante le connessioni OAuth e API. Ciò è particolarmente frustrante per gli sviluppatori dei paesi in via di sviluppo.
**Come OmniRoute risolve il problema:**
- **Configurazione proxy a 3 livelli**: proxy configurabile a 3 livelli: globale (tutto il traffico), per provider (un solo provider) e per connessione/chiave
- **Badge proxy con codice colore** — Indicatori visivi: 🟢 proxy globale, 🟡 proxy provider, 🔵 proxy di connessione, che mostra sempre l'IP
- **Scambio di token OAuth tramite proxy**: anche il flusso OAuth passa attraverso il proxy, risolvendo `unsupported_country_region_territory`
- **Test di connessione tramite proxy**: i test di connessione utilizzano il proxy configurato (non più bypass diretto)
- **Supporto SOCKS5**: supporto completo del proxy SOCKS5 per il routing in uscita
- **Spoofing dell'impronta digitale TLS**: impronta digitale TLS simile a un browser tramite `wreq-js` per bypassare il rilevamento dei bot
</details>
<details>
<summary><b>🆓 4. "Voglio usare l'intelligenza artificiale per programmare ma non ho soldi"</b></summary>
Non tutti possono pagare $ 20-200 al mese per gli abbonamenti AI. Studenti, sviluppatori provenienti da paesi emergenti, hobbisti e liberi professionisti hanno bisogno di accedere a modelli di qualità a costo zero.
**Come OmniRoute risolve il problema:**
- **Fornitori del livello gratuito integrati**: supporto nativo per fornitori gratuiti al 100%: iFlow (8 modelli illimitati), Qwen (3 modelli illimitati), Kiro (Claude gratis), Gemini CLI (180.000/mese gratuiti)
- **Combo solo gratuiti** — Catena `gc/gemini-3-flash → if/kimi-k2-thinking → qw/qwen3-coder-plus` = $ 0/mese senza tempi di inattività
- **Crediti gratuiti NVIDIA NIM**: 1000 crediti gratuiti integrati
- **Strategia di ottimizzazione dei costi**: strategia di routing che sceglie automaticamente il fornitore più economico disponibile
</details>
<details>
<summary><b>🔒 5. "Devo proteggere il mio gateway AI da accessi non autorizzati"</b></summary>
Quando si espone un gateway AI alla rete (LAN, VPS, Docker), chiunque abbia l'indirizzo può consumare i token/la quota dello sviluppatore. Senza protezione, le API sono vulnerabili ad usi impropri, tempestive iniezioni e abusi.
**Come OmniRoute risolve il problema:**
- **Gestione delle chiavi API**: generazione, rotazione e ambito per provider con una pagina `/dashboard/api-manager` dedicata
- **Autorizzazioni a livello di modello**: limita le chiavi API a modelli specifici (`openai/*`, modelli con caratteri jolly), con l'interruttore Consenti tutto/Limita
- **API Endpoint Protection**: richiede una chiave per `/v1/models` e blocca provider specifici dall'elenco
- **Auth Guard + Protezione CSRF**: tutti i percorsi del dashboard protetti con middleware `withAuth` + token CSRF
- **Rate Limiter**: limitazione della velocità per IP con finestre configurabili
- **Filtro IP**: lista consentita/lista bloccata per il controllo degli accessi
- **Prompt Injection Guard**: sanificazione contro modelli di prompt dannosi
- **Crittografia AES-256-GCM**: credenziali crittografate a riposo
</details>
<details>
<summary><b>🛑 6. "Il mio provider è andato in tilt e ho perso il flusso di codifica"</b></summary>
I fornitori di intelligenza artificiale possono diventare instabili, restituire errori 5xx o raggiungere limiti di velocità temporanei. Se uno sviluppatore dipende da un singolo fornitore, viene interrotto. Senza interruttori automatici, tentativi ripetuti possono bloccare l'applicazione.
**Come OmniRoute risolve il problema:**
- **Interruttore automatico per provider**: apertura/chiusura automatica con soglie e raffreddamento configurabili (chiuso/aperto/semiaperto)
- **Backoff esponenziale**: ritardi progressivi tra i tentativi
- **Anti-Thundering Herd** — Mutex + protezione semaforo contro tempeste di tentativi simultanei
- **Catene di fallback combinate**: se il fornitore primario fallisce, cade automaticamente nella catena senza alcun intervento
- **Combo Circuit Breaker**: disabilita automaticamente i provider in errore all'interno di una catena combinata
- **Dashboard integrità**: monitoraggio del tempo di attività, stati degli interruttori automatici, blocchi, statistiche della cache, latenza p50/p95/p99
</details>
<details>
<summary><b>🔧 7. "Configurare ogni strumento AI è noioso e ripetitivo"</b></summary>
Gli sviluppatori utilizzano Cursor, Claude Code, Codex CLI, OpenClaw, Gemini CLI, Kilo Code... Ogni strumento necessita di una configurazione diversa (endpoint API, chiave, modello). La riconfigurazione quando si cambia fornitore o modello è una perdita di tempo.
**Come OmniRoute risolve il problema:**
- **Dashboard degli strumenti CLI**: pagina dedicata con configurazione con un clic per Claude Code, Codex CLI, OpenClaw, Kilo Code, Antigravity, Cline
- **GitHub Copilot Config Generator**: genera `chatLanguageModels.json` per VS Code con selezione di modelli in blocco
- **Procedura guidata di onboarding**: configurazione guidata in 4 passaggi per gli utenti alle prime armi
- **Un endpoint, tutti i modelli**: configura `http://localhost:20128/v1` una volta, accedi a oltre 36 provider
</details>
<details>
<summary><b>🔑 8. "Gestire token OAuth da più provider è un inferno"</b></summary>
Claude Code, Codex, Gemini CLI, Copilot: utilizzano tutti OAuth 2.0 con token in scadenza. Gli sviluppatori devono autenticarsi nuovamente costantemente, gestire `client_secret is missing`, `redirect_uri_mismatch` e errori sui server remoti. OAuth su LAN/VPS è particolarmente problematico.
**Come OmniRoute risolve il problema:**
- **Aggiornamento automatico dei token**: i token OAuth si aggiornano in background prima della scadenza
- **OAuth 2.0 (PKCE) integrato**: flusso automatico per Claude Code, Codex, Gemini CLI, Copilot, Kiro, Qwen, iFlow
- **OAuth multi-account**: account multipli per provider tramite estrazione di token JWT/ID
- **OAuth LAN/Correzione remota**: rilevamento IP privato per `redirect_uri` + modalità URL manuale per server remoti
- **OAuth Behind Nginx**: utilizza `window.location.origin` per la compatibilità con proxy inverso
- **Guida OAuth remota**: guida passo passo per le credenziali Google Cloud su VPS/Docker
</details>
<details>
<summary><b>📊 9. "Non so quanto sto spendendo né dove"</b></summary>
Gli sviluppatori utilizzano più fornitori a pagamento ma non hanno una visione unificata della spesa. Ogni fornitore ha il proprio dashboard di fatturazione, ma non esiste una visualizzazione consolidata. I costi imprevisti possono accumularsi.
**Come OmniRoute risolve il problema:**
- **Dashboard di analisi dei costi**: monitoraggio dei costi per token e gestione del budget per fornitore
- **Limiti di budget per livello**: massimale di spesa per livello che attiva il fallback automatico
- **Configurazione dei prezzi per modello**: prezzi configurabili per modello
- **Statistiche di utilizzo per chiave API**: conteggio delle richieste e timestamp dell'ultimo utilizzo per chiave
- **Dashboard di analisi**: schede statistiche, grafico di utilizzo del modello, tabella dei fornitori con percentuali di successo e latenza
</details>
<details>
<summary><b>🐛 10. "Non riesco a diagnosticare errori e problemi nelle chiamate AI"</b></summary>
Quando una chiamata fallisce, lo sviluppatore non sa se si trattava di un limite di velocità, di un token scaduto, di un formato errato o di un errore del provider. Registri frammentati su diversi terminali. Senza osservabilità, il debug è un processo per tentativi ed errori.
**Come OmniRoute risolve il problema:**
- **Dashboard dei registri unificati**: 4 schede: registri delle richieste, registri del proxy, registri di controllo, console
- **Visualizzatore log della console**: visualizzatore in stile terminale in tempo reale con livelli codificati a colori, scorrimento automatico, ricerca, filtro
- **Registri proxy SQLite**: registri persistenti che sopravvivono ai riavvii del server
- **Translator Playground** — 4 modalità di debug: Playground (traduzione del formato), Chat Tester (andata e ritorno), Test Bench (batch), Live Monitor (in tempo reale)
- **Telemetria richiesta**: latenza p50/p95/p99 + traccia X-Request-Id
- **Registrazione basata su file con rotazione**: l'interceptor della console acquisisce tutto nel registro JSON con rotazione basata sulle dimensioni
</details>
<details>
<summary><b>🏗️ 11. "L'implementazione e la manutenzione del gateway sono complesse"</b></summary>
L'installazione, la configurazione e la manutenzione di un proxy AI in diversi ambienti (locale, VPS, Docker, cloud) richiedono molto lavoro. Problemi come percorsi codificati, `EACCES` nelle directory, conflitti di porte e build multipiattaforma aggiungono attrito.
**Come OmniRoute risolve il problema:**
- **Installazione globale npm** — `npm install -g omniroute && omniroute` — completata
- **Docker multipiattaforma** — AMD64 + ARM64 nativo (Apple Silicon, AWS Graviton, Raspberry Pi)
- **Docker Compose Profiles** — `base` (senza strumenti CLI) e `cli` (con Claude Code, Codex, OpenClaw)
- **App desktop Electron**: app nativa per Windows/macOS/Linux con barra delle applicazioni, avvio automatico, modalità offline
- **Modalità porta divisa**: API e dashboard su porte separate per scenari avanzati (proxy inverso, rete di contenitori)
- **Cloud Sync**: configura la sincronizzazione tra dispositivi tramite Cloudflare Workers
- **Backup DB**: backup, ripristino, esportazione e importazione automatici di tutte le impostazioni
</details>
<details>
<summary><b>🌍 12. "L'interfaccia è solo inglese e il mio team non parla inglese"</b></summary>
I team nei paesi non anglofoni, soprattutto in America Latina, Asia ed Europa, hanno difficoltà con le interfacce solo in inglese. Le barriere linguistiche riducono l'adozione e aumentano gli errori di configurazione.
**Come OmniRoute risolve il problema:**
- **Dashboard i18n — 30 lingue** — Tutti gli oltre 500 tasti tradotti tra cui arabo, bulgaro, danese, tedesco, spagnolo, finlandese, francese, ebraico, hindi, ungherese, indonesiano, italiano, giapponese, coreano, malese, olandese, norvegese, polacco, portoghese (PT/BR), rumeno, russo, slovacco, svedese, tailandese, ucraino, vietnamita, cinese, filippino, inglese
- **Supporto RTL**: supporto da destra a sinistra per arabo ed ebraico
- **README multilingue**: 30 traduzioni complete di documentazione
- **Selettore lingua**: icona del globo nell'intestazione per la commutazione in tempo reale
</details>
<details>
<summary><b>🔄 13. "Ho bisogno di qualcosa di più della semplice chat: ho bisogno di incorporamenti, immagini, audio"</b></summary>
L'intelligenza artificiale non è solo il completamento della chat. Gli sviluppatori devono generare immagini, trascrivere audio, creare incorporamenti per RAG, riclassificare i documenti e moderare i contenuti. Ogni API ha un endpoint e un formato diversi.
**Come OmniRoute risolve il problema:**
- **Incorporamenti** — `/v1/embeddings` con 6 fornitori e oltre 9 modelli
- **Generazione di immagini** — `/v1/images/generations` con 10 provider e oltre 20 modelli (OpenAI, xAI, Together, Fireworks, Nebius, Hyperbolic, NanoBanana, Antigravity, SD WebUI, ComfyUI)
- **Da testo a video** — `/v1/videos/generations` — ComfyUI (AnimateDiff, SVD) e SD WebUI
- **Trasformazione testo in musica** — `/v1/music/generations` — ComfyUI (Stable Audio Open, MusicGen)
- **Trascrizione audio** — `/v1/audio/transcriptions` — Whisper + Nvidia NIM, HuggingFace, Qwen3
- **Sintesi vocale** — `/v1/audio/speech` — ElevenLabs, Nvidia NIM, HuggingFace, Coqui, Tortoise, Qwen3, + fornitori esistenti
- **Moderazioni** — `/v1/moderations` — Controlli di sicurezza dei contenuti
- **Riclassificazione** — `/v1/rerank`: riclassificazione della pertinenza del documento
- **API di risposta**: supporto `/v1/responses` completo per Codex
</details>
<details>
<summary><b>🧪 14. "Non ho modo di testare e confrontare la qualità tra i modelli"</b></summary>
Gli sviluppatori vogliono sapere quale modello è il migliore per il loro caso d'uso (codice, traduzione, ragionamento), ma il confronto manuale è lento. Non esistono strumenti di valutazione integrati.
**Come OmniRoute risolve il problema:**
- **Valutazioni LLM**: test Golden Set con 10 casi precaricati che coprono saluti, matematica, geografia, generazione di codice, conformità JSON, traduzione, ribasso, rifiuto di sicurezza
- **4 strategie di corrispondenza** — `exact`, `contains`, `regex`, `custom` (funzione JS)
- **Translator Playground Test Bench**: test in batch con input multipli e output previsti, confronto tra provider
- **Chat Tester**: andata e ritorno completo con rendering della risposta visiva
- **Live Monitor**: flusso in tempo reale di tutte le richieste che passano attraverso il proxy
</details>
<details>
<summary><b>📈 15. "Ho bisogno di scalare senza perdere prestazioni"</b></summary>
Man mano che il volume delle richieste cresce, senza la memorizzazione nella cache le stesse domande generano costi duplicati. Senza idempotenza, le richieste duplicate sprecano elaborazione. I limiti tariffari per fornitore devono essere rispettati.
**Come OmniRoute risolve il problema:**
- **Cache semantica**: la cache a due livelli (firma + semantica) riduce costi e latenza
- **Idempotenza richiesta**: finestra di deduplicazione di 5 secondi per richieste identiche
- **Rilevamento del limite di velocità**: RPM per provider, gap minimo e monitoraggio simultaneo massimo
- **Limiti di velocità modificabili**: impostazioni predefinite configurabili in Impostazioni → Resilienza con persistenza
- **Cache di convalida della chiave API**: cache a 3 livelli per prestazioni di produzione
- **Dashboard integrità con telemetria**: latenza p50/p95/p99, statistiche cache, tempo di attività
</details>
<details>
<summary><b>🤖 16. "Voglio controllare il comportamento del modello a livello globale"</b></summary>
Sviluppatori che desiderano tutte le risposte in una lingua specifica, con un tono specifico o che desiderano limitare i token di ragionamento. Configurarlo in ogni strumento/richiesta non è pratico.
**Come OmniRoute risolve il problema:**
- **Inserimento prompt di sistema**: prompt globale applicato a tutte le richieste
- **Thinking Budget Validation**: controllo dell'allocazione dei token tramite ragionamento per richiesta (passthrough, automatico, personalizzato, adattivo)
- **6 Strategie di routing**: strategie globali che determinano la modalità di distribuzione delle richieste
- **Wildcard Router**: i modelli `provider/*` instradano dinamicamente a qualsiasi provider
- **Abilita/Disabilita combo**: attiva/disattiva le combo direttamente dalla dashboard
- **Attiva/disattiva provider**: attiva/disattiva tutte le connessioni per un provider con un clic
- **Fornitori bloccati**: esclude fornitori specifici dall'elenco `/v1/models`
</details>
<details>
<summary><b>🧰 17. "Ho bisogno degli strumenti MCP come funzionalità di prodotto di prima classe"</b></summary>
Molti gateway AI espongono MCP solo come dettaglio di implementazione nascosto. I team hanno bisogno di un livello operativo visibile e gestibile.
**Come OmniRoute risolve il problema:**
- MCP viene visualizzato nella navigazione del dashboard e nella scheda del protocollo dell'endpoint
- Pagina di gestione MCP dedicata con processo, strumenti, ambiti e audit
- Avvio rapido integrato per `omniroute --mcp` e onboarding del client
</details>
<details>
<summary><b>🧠 18. "Ho bisogno dell'orchestrazione A2A con percorsi di attività di sincronizzazione + streaming"</b></summary>
I flussi di lavoro degli agenti necessitano sia di risposte dirette che di esecuzione in streaming di lunga durata con controllo del ciclo di vita.
**Come OmniRoute risolve il problema:**
- Endpoint A2A JSON-RPC (`POST /a2a`) con `message/send` e `message/stream`
- Streaming SSE con propagazione dello stato terminale
- API del ciclo di vita delle attività per `tasks/get` e `tasks/cancel`
</details>
<details>
<summary><b>🛰️ 19. "Ho bisogno dello stato reale del processo MCP, non di uno stato indovinato"</b></summary>
I team operativi devono sapere se MCP è effettivamente attivo, non solo se un'API è raggiungibile.
**Come OmniRoute risolve il problema:**
- File heartbeat di runtime con PID, timestamp, trasporto, conteggio strumenti e modalità ambito
- API di stato MCP che combina battito cardiaco + attività recente
- Schede di stato dell'interfaccia utente per l'aggiornamento di processo/tempo di attività/battito cardiaco
</details>
<details>
<summary><b>📋 20. "Ho bisogno dell'esecuzione verificabile dello strumento MCP"</b></summary>
Quando gli strumenti modificano la configurazione o attivano azioni operative, i team necessitano di tracciabilità forense.
**Come OmniRoute risolve il problema:**
- Registrazione di controllo supportata da SQLite per le chiamate allo strumento MCP
- Filtri per strumento, successo/fallimento, chiave API e impaginazione
- Tabella di controllo della dashboard + endpoint statistici per l'automazione
</details>
<details>
<summary><b>🔐 21. "Ho bisogno di autorizzazioni MCP con ambito per integrazione"</b></summary>
Client diversi dovrebbero avere accesso con privilegi minimi alle categorie di strumenti.
**Come OmniRoute risolve il problema:**
- 9 ambiti MCP granulari per l'accesso controllato agli strumenti
- Applicazione dell'ambito e visibilità nell'interfaccia utente di gestione MCP
- Postura predefinita sicura per gli strumenti operativi
</details>
<details>
<summary><b>⚙️ 22. "Ho bisogno di controlli operativi senza ridistribuirmi"</b></summary>
I team necessitano di rapidi cambiamenti di runtime durante incidenti o eventi di costo.
**Come OmniRoute risolve il problema:**
- Cambia l'attivazione combinata direttamente dalla dashboard MCP
- Applicare profili di resilienza da pacchetti di policy predefiniti
- Ripristinare lo stato dell'interruttore dallo stesso pannello operativo
</details>
<details>
<summary><b>🔄 23. "Ho bisogno di visibilità e cancellazione del ciclo di vita delle attività A2A in tempo reale"</b></summary>
Senza visibilità del ciclo di vita, gli incidenti relativi alle attività diventano difficili da valutare.
**Come OmniRoute risolve il problema:**
- Elenco/filtro delle attività per stato/competenza con impaginazione
- Esamina i metadati, gli eventi e gli artefatti delle attività
- Endpoint di annullamento dell'attività e azione dell'interfaccia utente con conferma
</details>
<details>
<summary><b>🌊 24. "Ho bisogno di metriche di flusso attive per il carico A2A"</b></summary>
I flussi di lavoro in streaming richiedono informazioni operative sulla concorrenza e sulle connessioni live.
**Come OmniRoute risolve il problema:**
- Contatori di flussi attivi integrati nello stato A2A
- Timestamp dell'ultima attività e conteggi per stato
- Schede dashboard A2A per il monitoraggio delle operazioni in tempo reale
</details>
<details>
<summary><b>🪪 25. "Ho bisogno del rilevamento degli agenti standard per i clienti"</b></summary>
I client e gli agenti di orchestrazione esterni necessitano di metadati leggibili dal computer per l'onboarding.
**Come OmniRoute risolve il problema:**
- Carta Agente esposta a `/.well-known/agent.json`
- Capacità e competenze mostrate nell'interfaccia utente di gestione
- L'API di stato A2A include metadati di rilevamento per l'automazione
</details>
<details>
<summary><b>🧭 26. "Ho bisogno della rilevabilità del protocollo nella UX del prodotto"</b></summary>
Se gli utenti non riescono a scoprire le superfici del protocollo, l'adozione e la qualità del supporto diminuiscono.
**Come OmniRoute risolve il problema:**
- Voci della barra laterale per MCP e A2A
- Scheda Protocolli della pagina Endpoint con avvio rapido e stato
- Collegamenti dalla panoramica alle dashboard di gestione dedicate
</details>
<details>
<summary><b>🧪 27. "Ho bisogno della convalida del protocollo end-to-end con clienti reali"</b></summary>
I test simulati non sono sufficienti per verificare la compatibilità del protocollo prima del rilascio.
**Come OmniRoute risolve il problema:**
- Suite E2E che avvia l'app e utilizza il trasporto client SDK MCP reale
- Test client A2A per i flussi di rilevamento, invio, streaming, acquisizione e annullamento
- Effettuare un controllo incrociato delle asserzioni con l'audit MCP e le API delle attività A2A
</details>
<details>
<summary><b>📡 28. "Ho bisogno di osservabilità unificata su tutte le interfacce"</b></summary>
Suddividere l'osservabilità per protocollo crea punti ciechi e un MTTR più lungo.
**Come OmniRoute risolve il problema:**
- Dashboard/registri/analisi unificati in un unico prodotto
- Salute + audit + richiesta di telemetria su livelli OpenAI, MCP e A2A
- API operative per stato e automazione
</details>
<details>
<summary><b>💼 29. "Ho bisogno di un runtime per proxy + strumenti + orchestrazione agente"</b></summary>
L'esecuzione di numerosi servizi separati aumenta i costi operativi e le modalità di guasto.
**Come OmniRoute risolve il problema:**
- Proxy compatibile con OpenAI, server MCP e server A2A in uno stack
- Autenticazione condivisa, resilienza, archivio dati e osservabilità
- Modello politico coerente su tutte le superfici di interazione
</details>
<details>
<summary><b>🚀 30. "Ho bisogno di spedire flussi di lavoro di agenti senza la proliferazione del codice adesivo"</b></summary>
I team perdono velocità quando uniscono più servizi e script ad hoc.
**Come OmniRoute risolve il problema:**
- Strategia endpoint unificata per clienti e agenti
- Interfacce utente di gestione del protocollo integrate e percorsi di convalida del fumo
- Fondamenti pronti per la produzione (sicurezza, registrazione, resilienza, backup)
</details>
### Playbook di esempio (casi d'uso integrati)
**Playbook A: massimizza l'abbonamento a pagamento + backup economico**
```txt
Combo: "maximize-claude"
1. cc/claude-opus-4-6
2. glm/glm-4.7
3. if/kimi-k2-thinking
Monthly cost: $20 + small backup spend
Outcome: higher quality, near-zero interruption
```
**Playbook B: stack di codifica a costo zero**
```txt
Combo: "free-forever"
1. gc/gemini-3-flash
2. if/kimi-k2-thinking
3. qw/qwen3-coder-plus
Monthly cost: $0
Outcome: stable free coding workflow
```
**Playbook C: catena di fallback sempre attiva 24 ore su 24, 7 giorni su 7**
```txt
Combo: "always-on"
1. cc/claude-opus-4-6
2. cx/gpt-5.2-codex
3. glm/glm-4.7
4. minimax/MiniMax-M2.1
5. if/kimi-k2-thinking
Outcome: deep fallback depth for deadline-critical workloads
```
**Playbook D: operazioni dell'agente con MCP + A2A**
```txt
1) Start MCP transport (`omniroute --mcp`) for tool-driven operations
2) Run A2A tasks via `message/send` and `message/stream`
3) Observe via /dashboard/mcp and /dashboard/a2a
4) Control incidents with resilience profile + task cancellation
```
---
## ⚡ Avvio Rapido
**1. Installa globalmente:**
@@ -247,6 +780,30 @@ docker compose --profile cli up -d
---
---
## 🖥️
> 🆕 **NEW!** OmniRoute is now available as a **native desktop application** for Windows, macOS, and Linux.
- 🖥️ **Native Window** — Dedicated app window with system tray integration
- 🔄 **Auto-Start** — Launch OmniRoute on system login
- 🔔 **Native Notifications** — Get alerts for quota exhaustion or provider issues
-**One-Click Install** — NSIS (Windows), DMG (macOS), AppImage (Linux)
- 🌐 **Offline Mode** — Works fully offline with bundled server
```bash
npm run electron:dev # Development mode
npm run electron:build # Current platform
npm run electron:build:win # Windows (.exe)
npm run electron:build:mac # macOS (.dmg)
npm run electron:build:linux # Linux (.AppImage)
```
📖 Full documentation: [`electron/README.md`](electron/README.md)
---
## 💰 Panoramica Prezzi
| Tier | Provider | Costo | Reset Quota | Ideale Per |
@@ -272,67 +829,6 @@ docker compose --profile cli up -d
---
## 🎯 Casi d'Uso
### Caso 1: "Ho un abbonamento Claude Pro"
**Problema:** La quota scade inutilizzata, limiti di rate durante la programmazione intensa
```
Combo: "maximize-claude"
1. cc/claude-opus-4-6 (usa l'abbonamento al massimo)
2. glm/glm-4.7 (backup economico quando la quota è esaurita)
3. if/kimi-k2-thinking (fallback d'emergenza gratuito)
Costo mensile: $20 (abbonamento) + ~$5 (backup) = $25 totale
vs. $20 + sbattere contro i limiti = frustrazione
```
### Caso 2: "Voglio costo zero"
**Problema:** Non può permettersi abbonamenti, ha bisogno di IA affidabile per programmare
```
Combo: "free-forever"
1. gc/gemini-3-flash (180K gratis/mese)
2. if/kimi-k2-thinking (illimitato gratis)
3. qw/qwen3-coder-plus (illimitato gratis)
Costo mensile: $0
Qualità: Modelli pronti per la produzione
```
### Caso 3: "Devo programmare 24/7, senza interruzioni"
**Problema:** Scadenze strette, non può permettersi downtime
```
Combo: "always-on"
1. cc/claude-opus-4-6 (migliore qualità)
2. cx/gpt-5.2-codex (secondo abbonamento)
3. glm/glm-4.7 (economico, reset giornaliero)
4. minimax/MiniMax-M2.1 (più economico, reset 5h)
5. if/kimi-k2-thinking (gratuito illimitato)
Risultato: 5 livelli di fallback = zero downtime
```
### Caso 4: "Voglio IA GRATUITA in OpenClaw"
**Problema:** Ha bisogno di assistente IA nelle app di messaggistica, completamente gratuito
```
Combo: "openclaw-free"
1. if/glm-4.7 (illimitato gratis)
2. if/minimax-m2.1 (illimitato gratis)
3. if/kimi-k2-thinking (illimitato gratis)
Costo mensile: $0
Accesso via: WhatsApp, Telegram, Slack, Discord, iMessage, Signal...
```
---
## 💡 Funzionalità Principali
### 🧠 Routing & Intelligenza
@@ -348,6 +844,8 @@ Accesso via: WhatsApp, Telegram, Slack, Discord, iMessage, Signal...
| 🧩 **Modelli personalizzati** | Aggiungi qualsiasi ID modello a qualsiasi provider |
| 🌐 **Router wildcard** | Instrada pattern `provider/*` verso qualsiasi provider dinamicamente |
| 🧠 **Budget di ragionamento** | Modalità passthrough, auto, custom e adaptive per modelli di ragionamento |
| 🔀 **Model Aliases** | Auto-forward deprecated model IDs to current replacements (built-in + custom) |
| ⚡ **Background Degradation** | Auto-route background tasks (titles, summaries) to cheaper models |
| 💬 **Iniezione System Prompt** | System prompt globale applicato a tutte le richieste |
| 📄 **API Responses** | Supporto completo per OpenAI Responses API (`/v1/responses`) per Codex |
@@ -364,15 +862,18 @@ Accesso via: WhatsApp, Telegram, Slack, Discord, iMessage, Signal...
### 🛡️ Resilienza & Sicurezza
| Funzionalità | Cosa Fa |
| ------------------------------- | -------------------------------------------------------------- |
| 🔌 **Circuit Breaker** | Apertura/chiusura auto per provider con soglie configurabili |
| 🛡️ **Anti-Thundering Herd** | Mutex + semaforo rate-limit per provider con API key |
| 🧠 **Cache semantica** | Cache a due livelli (firma + semantica) riduce costi e latenza |
| **Idempotenza richieste** | Finestra dedup 5s per richieste duplicate |
| 🔒 **Spoofing TLS Fingerprint** | Bypass rilevamento bot tramite wreq-js |
| 🌐 **Filtro IP** | Allowlist/blocklist per controllo accesso API |
| 📊 **Rate limit modificabili** | RPM, gap minimo e concorrenza massima configurabili |
| Funzionalità | Cosa Fa |
| ------------------------------- | ---------------------------------------------------------------------------- |
| 🔌 **Circuit Breaker** | Apertura/chiusura auto per provider con soglie configurabili |
| 🎯 **Endpoint-Aware Models** | Custom models declare supported endpoints + API format |
| 🛡️ **Anti-Thundering Herd** | Mutex + semaforo rate-limit per provider con API key |
| 🧠 **Cache semantica** | Cache a due livelli (firma + semantica) riduce costi e latenza |
| **Idempotenza richieste** | Finestra dedup 5s per richieste duplicate |
| 🔒 **Spoofing TLS Fingerprint** | Bypass rilevamento bot tramite wreq-js |
| 🌐 **Filtro IP** | Allowlist/blocklist per controllo accesso API |
| 📊 **Rate limit modificabili** | RPM, gap minimo e concorrenza massima configurabili |
| 💾 **Rate Limit Persistence** | Learned limits survive restarts via SQLite with 60s debounce + 24h staleness |
| 🔄 **Token Refresh Resilience** | Per-provider circuit breaker (5 fails→30min) + 30s timeout per attempt |
### 📊 Osservabilità & Analytics
@@ -472,6 +973,27 @@ Traduzione trasparente tra formati:
</details>
## 🧪 Valutazioni (Evals)
OmniRoute include un framework di valutazione integrato per testare la qualità delle risposte LLM contro un golden set. Accesso via **Analytics → Evals** nella dashboard.
### Set integrato
Il "OmniRoute Golden Set" precaricato contiene 10 casi di test:
- Saluti, matematica, geografia, generazione codice
- Conformità formato JSON, traduzione, markdown
- Rifiuto sicurezza (contenuto nocivo), conteggio, logica booleana
### Strategie di valutazione
| Strategia | Descrizione | Esempio |
| ---------- | ---------------------------------------------------------- | -------------------------------- |
| `exact` | L'output deve corrispondere esattamente | `"4"` |
| `contains` | L'output deve contenere la sottostringa (case-insensitive) | `"Paris"` |
| `regex` | L'output deve corrispondere al pattern regex | `"1.*2.*3"` |
| `custom` | Funzione JS personalizzata restituisce true/false | `(output) => output.length > 10` |
---
## 📖 Guida alla Configurazione
@@ -754,97 +1276,6 @@ Impostazioni → Configurazione API:
---
## 📊 Modelli Disponibili
<details>
<summary><b>Vedi tutti i modelli disponibili</b></summary>
**Claude Code (`cc/`)** - Pro/Max:
- `cc/claude-opus-4-6`
- `cc/claude-sonnet-4-5-20250929`
- `cc/claude-haiku-4-5-20251001`
**Codex (`cx/`)** - Plus/Pro:
- `cx/gpt-5.2-codex`
- `cx/gpt-5.1-codex-max`
**Gemini CLI (`gc/`)** - GRATUITO:
- `gc/gemini-3-flash-preview`
- `gc/gemini-2.5-pro`
**GitHub Copilot (`gh/`)**:
- `gh/gpt-5`
- `gh/claude-4.5-sonnet`
**NVIDIA NIM (`nvidia/`)** - Crediti GRATUITI:
- `nvidia/llama-3.3-70b-instruct`
- `nvidia/mistral-7b-instruct`
- 50+ modelli su [build.nvidia.com](https://build.nvidia.com)
**GLM (`glm/`)** - $0.6/1M:
- `glm/glm-4.7`
**MiniMax (`minimax/`)** - $0.2/1M:
- `minimax/MiniMax-M2.1`
**iFlow (`if/`)** - GRATUITO:
- `if/kimi-k2-thinking`
- `if/qwen3-coder-plus`
- `if/deepseek-r1`
- `if/glm-4.7`
- `if/minimax-m2`
**Qwen (`qw/`)** - GRATUITO:
- `qw/qwen3-coder-plus`
- `qw/qwen3-coder-flash`
**Kiro (`kr/`)** - GRATUITO:
- `kr/claude-sonnet-4.5`
- `kr/claude-haiku-4.5`
**OpenRouter (`or/`)** - 100+ modelli:
- `or/anthropic/claude-4-sonnet`
- `or/google/gemini-2.5-pro`
- Qualsiasi modello da [openrouter.ai/models](https://openrouter.ai/models)
</details>
---
## 🧪 Valutazioni (Evals)
OmniRoute include un framework di valutazione integrato per testare la qualità delle risposte LLM contro un golden set. Accesso via **Analytics → Evals** nella dashboard.
### Golden Set integrato
Il "OmniRoute Golden Set" precaricato contiene 10 casi di test:
- Saluti, matematica, geografia, generazione codice
- Conformità formato JSON, traduzione, markdown
- Rifiuto sicurezza (contenuto nocivo), conteggio, logica booleana
### Strategie di valutazione
| Strategia | Descrizione | Esempio |
| ---------- | ---------------------------------------------------------- | -------------------------------- |
| `exact` | L'output deve corrispondere esattamente | `"4"` |
| `contains` | L'output deve contenere la sottostringa (case-insensitive) | `"Paris"` |
| `regex` | L'output deve corrispondere al pattern regex | `"1.*2.*3"` |
| `custom` | Funzione JS personalizzata restituisce true/false | `(output) => output.length > 10` |
---
## 🐛 Risoluzione Problemi
<details>
@@ -900,7 +1331,7 @@ Il "OmniRoute Golden Set" precaricato contiene 10 casi di test:
---
## 🛠️ Stack Tecnologico
## 🛠️
- **Runtime**: Node.js 20+
- **Linguaggio**: TypeScript 5.9 — **100% TypeScript** in `src/` e `open-sse/` (v1.0.6)
@@ -931,18 +1362,7 @@ Il "OmniRoute Golden Set" precaricato contiene 10 casi di test:
---
## 📧 Supporto
> 💬 **Unisciti alla nostra community!** [Gruppo WhatsApp](https://chat.whatsapp.com/JI7cDQ1GyaiDHhVBpLxf8b?mode=gi_t) — Ottieni aiuto, condividi consigli e rimani aggiornato.
- **Sito Web**: [omniroute.online](https://omniroute.online)
- **GitHub**: [github.com/diegosouzapw/OmniRoute](https://github.com/diegosouzapw/OmniRoute)
- **Issues**: [github.com/diegosouzapw/OmniRoute/issues](https://github.com/diegosouzapw/OmniRoute/issues)
- **WhatsApp**: [Gruppo della comunità](https://chat.whatsapp.com/JI7cDQ1GyaiDHhVBpLxf8b?mode=gi_t)
- **WhatsApp**: [Gruppo della comunità](https://chat.whatsapp.com/JI7cDQ1GyaiDHhVBpLxf8b?mode=gi_t)
- **Progetto Originale**: [9router di decolua](https://github.com/decolua/9router)
---
## 🗺️
## 👥 Contributori

View File

@@ -110,6 +110,35 @@ _AI を活用した IDE または CLI ツールを、無制限のコーディン
---
## 🖼️
<div align="center">
<img src="./docs/screenshots/MainOmniRoute.png" alt="OmniRoute" width="800"/>
</div>
---
## 📸
<details>
<summary><b>...</b></summary>
| # | # |
| ----- | ---------------------------------------- |
| **1** | ![1](docs/screenshots/01-providers.png) |
| **2** | ![2](docs/screenshots/02-combos.png) |
| **3** | ![3](docs/screenshots/03-analytics.png) |
| **4** | ![4](docs/screenshots/04-health.png) |
| **5** | ![5](docs/screenshots/05-translator.png) |
| **6** | ![6](docs/screenshots/06-settings.png) |
| **7** | ![7](docs/screenshots/07-cli-tools.png) |
| **8** | ![8](docs/screenshots/08-usage.png) |
| **9** | ![9](docs/screenshots/09-endpoint.png) |
</details>
---
## 🤔 なぜオムニルートなのか?
**お金の無駄遣いや限界に達するのはやめましょう:**
@@ -128,6 +157,18 @@ _AI を活用した IDE または CLI ツールを、無制限のコーディン
---
## 📧 サポート
> 💬 **コミュニティに参加してください!** [WhatsApp Group](https://chat.whatsapp.com/JI7cDQ1GyaiDHhVBpLxf8b?mode=gi_t) — ヘルプを取得し、ヒントを共有し、最新情報を入手してください。
- **ウェブサイト**: [omniroute.online](https://omniroute.online)
- **GitHub**: [github.com/diegosouzapw/OmniRoute](https://github.com/diegosouzapw/OmniRoute)
- **問題**: [github.com/diegosouzapw/OmniRoute/issues](https://github.com/diegosouzapw/OmniRoute/issues)
- **WhatsApp**: [Community Group](https://chat.whatsapp.com/JI7cDQ1GyaiDHhVBpLxf8b?mode=gi_t)
- **オリジナル プロジェクト**: [9router by decolua](https://github.com/decolua/9router)
---
## 🔄 仕組み
```
@@ -157,6 +198,498 @@ Result: Never stop coding, minimal cost
---
## 🎯 OmniRoute が解決するもの — 30 の実際の問題点とユースケース
> **AI ツールを使用するすべての開発者は、これらの問題に日々直面しています。** OmniRoute は、コスト超過から地域ブロック、壊れた OAuth フローからプロトコル操作、企業の可観測性まで、それらすべてを解決するために構築されました。
<details>
<summary><b>💸 1. 「高価なサブスクリプションの料金を支払っているのに、制限によって中断されます」</b></summary>
開発者は、Claude Pro、Codex Pro、または GitHub Copilot に月額 20 200 ドルを支払います。有料であっても、割り当てには上限があり、5 時間の使用量、週ごとの制限、または分ごとのレート制限があります。コーディング セッションの途中でプロバイダーが応答を停止し、開発者はフローと生産性を失います。
**OmniRoute がそれを解決する方法:**
- **スマート 4 層フォールバック** — サブスクリプション クォータが不足すると、手動介入なしで自動的に API キー→格安→無料にリダイレクトされます。
- **リアルタイム クォータ トラッキング** — リセット カウントダウン (5 時間、毎日、毎週) でトークンの消費量をリアルタイムで表示します。
- **マルチアカウントのサポート** — 自動ラウンドロビンによるプロバイダーごとの複数のアカウント — 1 つのアカウントがなくなると、次のアカウントに切り替わります
- **カスタム コンボ** — 6 つのバランシング戦略 (フィルファースト、ラウンドロビン、P2C、ランダム、最小使用、コスト最適化) を備えたカスタマイズ可能なフォールバック チェーン
- **Codex Business クォータ** — ビジネス/チームのワークスペース クォータをダッシュボードで直接監視
</details>
<details>
<summary><b>🔌 2. 「複数のプロバイダーを使用する必要がありますが、それぞれに異なる API があります」</b></summary>
OpenAI は 1 つの形式を使用し、Claude (Anthropic) は別の形式を使用し、Gemini はさらに別の形式を使用します。開発者が異なるプロバイダーのモデルをテストしたり、プロバイダー間でフォールバックしたりする場合は、SDK を再構成し、エンドポイントを変更し、互換性のない形式に対処する必要があります。カスタム プロバイダー (FriendLI、NIM) には、非標準モデルのエンドポイントがあります。
**OmniRoute がそれを解決する方法:**
- **統合エンドポイント** - 単一の `http://localhost:20128/v1` が 36 を超えるすべてのプロバイダーのプロキシとして機能します
- **フォーマット変換** — 自動かつ透過的: OpenAI ↔ Claude ↔ Gemini ↔ Responses API
- **レスポンスのサニタイズ** — OpenAI SDK v1.83 以降を破壊する非標準フィールド (`x_groq``usage_breakdown``service_tier`) を除去します。
- **ロールの正規化** — 非 OpenAI プロバイダーに対して `developer``system` を変換します。 GLM/ERNIE 用 `system``user`
- **Think Tag Extraction** — DeepSeek R1 などのモデルから `<think>` ブロックを標準化された `reasoning_content` に抽出します。
- **Gemini の構造化出力** — `json_schema``responseMimeType`/`responseSchema` 自動変換
- **`stream` のデフォルトは `false`** — OpenAI 仕様に準拠し、Python/Rust/Go SDK での予期しない SSE を回避します
</details>
<details>
<summary><b>🌐 3. 「AI プロバイダーが私の地域/国をブロックしています」</b></summary>
OpenAI/Codex などのプロバイダーは、特定の地理的地域からのアクセスをブロックします。 OAuth および API 接続中に、ユーザーは `unsupported_country_region_territory` のようなエラーを受け取ります。これは、発展途上国の開発者にとって特にイライラさせられます。
**OmniRoute がそれを解決する方法:**
- **3 レベルのプロキシ構成** — 3 つのレベルで構成可能なプロキシ: グローバル (すべてのトラフィック)、プロバイダーごと (1 つのプロバイダーのみ)、および接続/キーごと
- **色分けされたプロキシ バッジ** — 視覚的なインジケーター: 🟢 グローバル プロキシ、🟡 プロバイダー プロキシ、🔵 接続プロキシ、常に IP を表示
- **プロキシを介した OAuth トークン交換** — OAuth フローもプロキシを通過し、`unsupported_country_region_territory` を解決します
- **プロキシ経由の接続テスト** — 接続テストは構成されたプロキシを使用します (直接バイパスはありません)。
- **SOCKS5 サポート** — アウトバウンド ルーティングに対する SOCKS5 プロキシの完全なサポート
- **TLS フィンガープリント スプーフィング** — `wreq-js` を介したブラウザーのような TLS フィンガープリントによりボット検出をバイパスします
</details>
<details>
<summary><b>🆓 4. 「AIを使ってコーディングしたいけどお金がない」</b></summary>
誰もが AI サブスクリプションに月額 20 200 ドルを支払えるわけではありません。学生、新興国の開発者、愛好家、フリーランサーは、高品質のモデルに無料でアクセスできる必要があります。
**OmniRoute がそれを解決する方法:**
- **無料利用枠プロバイダーの組み込み** — 100% 無料プロバイダーのネイティブ サポート: iFlow (8 つの無制限のモデル)、Qwen (3 つの無制限のモデル)、Kiro (無料の Claude)、Gemini CLI (180K/月無料)
- **無料のみのコンボ** — チェーン `gc/gemini-3-flash → if/kimi-k2-thinking → qw/qwen3-coder-plus` = 月額 0 ドル、ダウンタイムなし
- **NVIDIA NIM 無料クレジット** — 1000 の無料クレジットが統合されています
- **コスト最適化戦略** — 利用可能な最も安価なプロバイダーを自動的に選択するルーティング戦略
</details>
<details>
<summary><b>🔒 5. 「AI ゲートウェイを不正アクセスから保護する必要があります」</b></summary>
AI ゲートウェイをネットワーク (LAN、VPS、Docker) に公開すると、アドレスを持っている人は誰でも開発者のトークン/クォータを消費できます。保護がなければ、API は誤用、即時挿入、悪用に対して脆弱になります。
**OmniRoute がそれを解決する方法:**
- **API キー管理** — 専用の `/dashboard/api-manager` ページを使用したプロバイダーごとの生成、ローテーション、およびスコープ設定
- **モデルレベルの権限** — すべて許可/制限の切り替えにより、API キーを特定のモデル (`openai/*`、ワイルドカード パターン) に制限します
- **API エンドポイント保護** - `/v1/models` のキーを要求し、リストから特定のプロバイダーをブロックします
- **認証ガード + CSRF 保護** — すべてのダッシュボード ルートは `withAuth` ミドルウェア + CSRF トークンで保護されています
- **レート リミッター** — 構成可能なウィンドウによる IP ごとのレート制限
- **IP フィルタリング** — アクセス制御の許可リスト/ブロックリスト
- **プロンプト インジェクション ガード** — 悪意のあるプロンプト パターンに対するサニタイズ
- **AES-256-GCM 暗号化** — 認証情報は保存時に暗号化されます
</details>
<details>
<summary><b>🛑 6. 「プロバイダーがダウンしてコーディング フローが失われました」</b></summary>
AI プロバイダーが不安定になったり、5xx エラーを返したり、一時的なレート制限に達したりする可能性があります。開発者が単一のプロバイダーに依存している場合、それらは中断されます。サーキット ブレーカーがないと、再試行を繰り返すとアプリケーションがクラッシュする可能性があります。
**OmniRoute がそれを解決する方法:**
- **プロバイダーごとのサーキット ブレーカー** — 設定可能なしきい値とクールダウンによる自動開閉 (クローズ/オープン/ハーフオープン)
- **指数バックオフ** — 漸進的な再試行遅延
- **Anti-Thundering Herd** — 同時再試行の嵐に対するミューテックス + セマフォ保護
- **コンボ フォールバック チェーン** - プライマリ プロバイダーに障害が発生した場合、介入なしで自動的にチェーンを通過します。
- **コンボ サーキット ブレーカー** — コンボ チェーン内の障害が発生したプロバイダーを自動的に無効にします
- **ヘルス ダッシュボード** — 稼働時間モニタリング、サーキット ブレーカーの状態、ロックアウト、キャッシュ統計、p50/p95/p99 レイテンシ
</details>
<details>
<summary><b>🔧 7. 「各 AI ツールの設定は面倒で繰り返しが多い」</b></summary>
開発者は、Cursor、Claude Code、Codex CLI、OpenClaw、Gemini CLI、Kilo Code を使用します。各ツールには異なる構成 (API エンドポイント、キー、モデル) が必要です。プロバイダーや機種変更時に再設定するのは時間の無駄です。
**OmniRoute がそれを解決する方法:**
- **CLI ツール ダッシュボード** — Claude Code、Codex CLI、OpenClaw、Kilo Code、Antigravity、Cline をワンクリックでセットアップできる専用ページ
- **GitHub Copilot Config Generator** — モデルを一括選択して VS Code の `chatLanguageModels.json` を生成します
- **オンボーディング ウィザード** - 初めてユーザー向けのガイド付き 4 ステップ セットアップ
- **1 つのエンドポイント、すべてのモデル** — `http://localhost:20128/v1` を 1 回構成すると、36 を超えるプロバイダーにアクセスできます
</details>
<details>
<summary><b>🔑 8. 「複数のプロバイダーからの OAuth トークンを管理するのは地獄です」</b></summary>
Claude Code、Codex、Gemini CLI、Copilot — すべては有効期限切れのトークンを持つ OAuth 2.0 を使用します。開発者は定期的に再認証し、`client_secret is missing``redirect_uri_mismatch`、およびリモート サーバーの障害に対処する必要があります。 LAN/VPS 上の OAuth は特に問題があります。
**OmniRoute がそれを解決する方法:**
- **自動トークン更新** — OAuth トークンは有効期限が切れる前にバックグラウンドで更新されます。
- **OAuth 2.0 (PKCE) ビルトイン** — Claude Code、Codex、Gemini CLI、Copilot、Kiro、Qwen、iFlow の自動フロー
- **マルチアカウント OAuth** — JWT/ID トークン抽出によるプロバイダーごとの複数のアカウント
- **OAuth LAN/リモート修正** — `redirect_uri` のプライベート IP 検出 + リモート サーバーの手動 URL モード
- **Nginx の背後にある OAuth** — リバース プロキシの互換性のために `window.location.origin` を使用します
- **リモート OAuth ガイド** — VPS/Docker での Google Cloud 認証情報のステップバイステップ ガイド
</details>
<details>
<summary><b>📊 9. 「どこにいくら使っているのか分かりません」</b></summary>
開発者は複数の有料プロバイダーを使用していますが、支出について統一した見解がありません。各プロバイダーには独自の請求ダッシュボードがありますが、統合されたビューはありません。予期せぬ出費がかさむ可能性があります。
**OmniRoute がそれを解決する方法:**
- **コスト分析ダッシュボード** — トークンごとのコスト追跡とプロバイダーごとの予算管理
- **階層ごとの予算制限** — 自動フォールバックをトリガーする階層ごとの支出上限
- **モデルごとの価格構成** — モデルごとに構成可能な価格
- **API キーごとの使用統計** — キーごとのリクエスト数と最後に使用されたタイムスタンプ
- **分析ダッシュボード** — 統計カード、モデル使用状況グラフ、成功率と遅延を含むプロバイダー表
</details>
<details>
<summary><b>🐛 10. 「AI 呼び出しのエラーや問題を診断できません」</b></summary>
呼び出しが失敗すると、開発者はそれがレート制限なのか、トークンの期限切れなのか、間違った形式なのか、プロバイダーのエラーなのかわかりません。さまざまな端末にわたる断片化されたログ。可観測性がなければ、デバッグは試行錯誤になります。
**OmniRoute がそれを解決する方法:**
- **統合ログ ダッシュボード** — 4 つのタブ: リクエスト ログ、プロキシ ログ、監査ログ、コンソール
- **コンソール ログ ビューア** — 色分けされたレベル、自動スクロール、検索、フィルターを備えたリアルタイムのターミナル スタイルのビューア
- **SQLite プロキシ ログ** — サーバーの再起動後も存続する永続的なログ
- **Translator Playground** — 4 つのデバッグ モード: プレイグラウンド (形式変換)、チャット テスター (往復)、テストベンチ (バッチ)、ライブ モニター (リアルタイム)
- **リクエスト テレメトリ** — p50/p95/p99 レイテンシ + X-Request-Id トレース
- **ローテーションを使用したファイルベースのログ** — コンソール インターセプターは、サイズベースのローテーションを使用してすべてを JSON ログにキャプチャします。
</details>
<details>
<summary><b>🏗️ 11. 「ゲートウェイの導入と保守は複雑です」</b></summary>
さまざまな環境 (ローカル、VPS、Docker、クラウド) 間で AI プロキシをインストール、構成、維持するには、多大な労力がかかります。ハードコーディングされたパス、ディレクトリ上の `EACCES`、ポートの競合、クロスプラットフォーム ビルドなどの問題により、摩擦が増大します。
**OmniRoute がそれを解決する方法:**
- **npm グローバル インストール** — `npm install -g omniroute && omniroute` — 完了
- **Docker マルチプラットフォーム** — AMD64 + ARM64 ネイティブ (Apple Silicon、AWS Graviton、Raspberry Pi)
- **Docker Compose プロファイル** — `base` (CLI ツールなし) および `cli` (Claude Code、Codex、OpenClaw あり)
- **Electron デスクトップ アプリ** — システム トレイ、自動起動、オフライン モードを備えた Windows/macOS/Linux 用ネイティブ アプリ
- **分割ポート モード** - 高度なシナリオ (リバース プロキシ、コンテナ ネットワーキング) 向けに個別のポート上の API とダッシュボード
- **Cloud Sync** — Cloudflare Workers を介したデバイス間での設定の同期
- **DB バックアップ** — すべての設定の自動バックアップ、復元、エクスポート、インポート
</details>
<details>
<summary><b>🌍 12. 「インターフェースは英語のみで、私のチームは英語を話せません」</b></summary>
非英語圏の国、特にラテンアメリカ、アジア、ヨーロッパのチームは、英語のみのインターフェースに苦労しています。言語の壁があると採用が減り、構成エラーが増加します。
**OmniRoute がそれを解決する方法:**
- **ダッシュボード i18n — 30 言語** — アラビア語、ブルガリア語、デンマーク語、ドイツ語、スペイン語、フィンランド語、フランス語、ヘブライ語、ヒンディー語、ハンガリー語、インドネシア語、イタリア語、日本語、韓国語、マレー語、オランダ語、ノルウェー語、ポーランド語、ポルトガル語 (PT/BR)、ルーマニア語、ロシア語、スロバキア語、スウェーデン語、タイ語、ウクライナ語、ベトナム語、中国語、フィリピン語、英語
- **RTL サポート** — アラビア語とヘブライ語の右から左へのサポート
- **多言語 README** — 30 件の完全なドキュメント翻訳
- **言語セレクター** — リアルタイム切り替えのためのヘッダーの地球儀アイコン
</details>
<details>
<summary><b>🔄 13. 「チャット以上のものが必要です — 埋め込み、画像、音声が必要です」</b></summary>
AIは単なるチャット補完ではありません。開発者は、画像の生成、音声の文字起こし、RAG の埋め込みの作成、ドキュメントの再ランク付け、およびコンテンツの管理を行う必要があります。各 API には異なるエンドポイントと形式があります。
**OmniRoute がそれを解決する方法:**
- **エンベディング** — 6 つのプロバイダーと 9 つ以上のモデルを備えた `/v1/embeddings`
- **画像生成** — 10 プロバイダーと 20 以上のモデルを備えた `/v1/images/generations` (OpenAI、xAI、Togetter、Fireworks、Nebius、Hyperbolic、NanoBanana、Antigravity、SD WebUI、ComfyUI)
- **Text-to-Video** — `/v1/videos/generations` — ComfyUI (AnimateDiff、SVD) および SD WebUI
- **テキストから音楽へ** — `/v1/music/generations` — ComfyUI (安定したオーディオ オープン、MusicGen)
- **音声転写** — `/v1/audio/transcriptions` — Whisper + Nvidia NIM、HuggingFace、Qwen3
- **テキスト読み上げ** — `/v1/audio/speech` — イレブンラボ、Nvidia NIM、HuggingFace、Coqui、Tortoise、Qwen3、+ 既存のプロバイダー
- **モデレーション** — `/v1/moderations` — コンテンツの安全性チェック
- **再ランキング** — `/v1/rerank` — ドキュメントの関連性の再ランキング
- **応答 API** — Codex の `/v1/responses` の完全サポート
</details>
<details>
<summary><b>🧪 14. 「モデル間で品質をテストして比較する方法がありません」</b></summary>
開発者は、コード、翻訳、推論などのユースケースにどのモデルが最適であるかを知りたいと考えていますが、手動で比較するのは時間がかかります。統合された評価ツールは存在しません。
**OmniRoute がそれを解決する方法:**
- **LLM 評価** — 挨拶、数学、地理、コード生成、JSON 準拠、翻訳、マークダウン、安全性の拒否をカバーする 10 個の事前ロードされたケースによるゴールデン セット テスト
- **4 つのマッチ戦略** — `exact``contains``regex``custom` (JS 関数)
- **Translator Playground Test Bench** — 複数の入力と予想される出力を使用したバッチ テスト、クロスプロバイダー比較
- **チャット テスター** — 視覚的な応答レンダリングによる完全な往復
- **ライブ モニター** — プロキシを通過するすべてのリクエストのリアルタイム ストリーム
</details>
<details>
<summary><b>📈 15. 「パフォーマンスを落とさずにスケールする必要がある」</b></summary>
リクエストの量が増えると、同じ質問をキャッシュしないと重複したコストが発生します。冪等性がないと、重複したリクエストにより処理が無駄になります。プロバイダーごとのレート制限を遵守する必要があります。
**OmniRoute がそれを解決する方法:**
- **セマンティック キャッシュ** — 2 層キャッシュ (署名 + セマンティック) によりコストと遅延が削減されます。
- **リクエストの冪等性** — 同一のリクエストに対する重複排除ウィンドウは 5 秒です
- **レート制限検出** — プロバイダーごとの RPM、最小ギャップ、最大同時トラッキング
- **編集可能なレート制限** — [設定] → [永続性を伴う回復力] で構成可能なデフォルト
- **API キー検証キャッシュ** — 運用パフォーマンスのための 3 層キャッシュ
- **テレメトリ付きヘルス ダッシュボード** — p50/p95/p99 レイテンシ、キャッシュ統計、稼働時間
</details>
<details>
<summary><b>🤖 16. 「モデルの動作をグローバルに制御したい」</b></summary>
すべての応答を特定の言語、特定の口調で行いたい、または推論トークンを制限したい開発者。すべてのツール/リクエストでこれを設定するのは現実的ではありません。
**OmniRoute がそれを解決する方法:**
- **システム プロンプト インジェクション** — すべてのリクエストに適用されるグローバル プロンプト
- **思考予算検証** — リクエストごとの推論トークン割り当て制御 (パススルー、自動、カスタム、適応)
- **6 ルーティング戦略** — リクエストの分散方法を決定するグローバル戦略
- **ワイルドカード ルーター** - `provider/*` パターンは任意のプロバイダーに動的にルーティングします
- **コンボ有効/無効切り替え** — ダッシュボードから直接コンボを切り替えます
- **プロバイダー切り替え** — ワンクリックでプロバイダーのすべての接続を有効/無効にします。
- **ブロックされたプロバイダー** — `/v1/models` リストから特定のプロバイダーを除外します
</details>
<details>
<summary><b>🧰 17. 「一流の製品機能として MCP ツールが必要です」</b></summary>
多くの AI ゲートウェイは、MCP を非表示の実装詳細としてのみ公開します。チームには、目に見えて管理しやすいオペレーション レイヤーが必要です。
**OmniRoute がそれを解決する方法:**
- MCP はダッシュボードのナビゲーションとエンドポイント プロトコル タブに表示されます
- プロセス、ツール、スコープ、監査を備えた専用の MCP 管理ページ
- `omniroute --mcp` およびクライアントのオンボーディング用の組み込みクイックスタート
</details>
<details>
<summary><b>🧠 18. 「同期 + ストリーム タスク パスを備えた A2A オーケストレーションが必要です」</b></summary>
エージェント ワークフローには、直接応答と、ライフサイクル制御による長時間実行のストリーミング実行の両方が必要です。
**OmniRoute がそれを解決する方法:**
- A2A JSON-RPC エンドポイント (`POST /a2a`) (`message/send` および `message/stream`)
- 端末状態の伝播を伴う SSE ストリーミング
- `tasks/get` および `tasks/cancel` のタスク ライフサイクル API
</details>
<details>
<summary><b>🛰️ 19. 「推測されたステータスではなく、実際の MCP プロセスの健全性が必要です」</b></summary>
運用チームは、API が到達可能かどうかだけでなく、MCP が実際に生きているかどうかを知る必要があります。
**OmniRoute がそれを解決する方法:**
- PID、タイムスタンプ、トランスポート、ツール数、およびスコープモードを含むランタイムハートビートファイル
- ハートビートと最近のアクティビティを組み合わせた MCP ステータス API
- プロセス/稼働時間/ハートビートの鮮度を示す UI ステータス カード
</details>
<details>
<summary><b>📋 20. 「監査可能な MCP ツールの実行が必要です」</b></summary>
ツールが構成を変更したり、運用アクションをトリガーしたりする場合、チームはフォレンジックなトレーサビリティを必要とします。
**OmniRoute がそれを解決する方法:**
- MCP ツール呼び出しの SQLite ベースの監査ログ
- ツール、成功/失敗、API キー、ページネーションによるフィルター
- ダッシュボード監査テーブル + 自動化のための統計エンドポイント
</details>
<details>
<summary><b>🔐 21. 「統合ごとにスコープ指定された MCP 権限が必要です」</b></summary>
異なるクライアントには、ツール カテゴリへの最小限の特権アクセスが必要です。
**OmniRoute がそれを解決する方法:**
- 制御されたツールアクセスのための 9 つの詳細な MCP スコープ
- MCP 管理 UI でのスコープの適用と可視性
- 運用ツールの安全なデフォルト姿勢
</details>
<details>
<summary><b>⚙️ 22. 「再デプロイせずに運用制御が必要です」</b></summary>
チームは、インシデントやコスト イベントが発生した際に、実行時の変更を迅速に行う必要があります。
**OmniRoute がそれを解決する方法:**
- MCP ダッシュボードからコンボのアクティブ化を直接切り替えます
- 事前定義されたポリシーパックから復元プロファイルを適用
- 同じ操作パネルからサーキットブレーカーの状態をリセット
| 🎯 **Endpoint-Aware Models** | Custom models declare supported endpoints + API format |
</details>
<details>
<summary><b>🔄 23. 「ライブ A2A タスクのライフサイクルの可視化とキャンセルが必要です」</b></summary>
ライフサイクルの可視性がなければ、タスク インシデントの優先順位付けが困難になります。
**OmniRoute がそれを解決する方法:**
- ページネーションを使用した状態/スキルによるタスクのリスト/フィルタリング
- タスクのメタデータ、イベント、アーティファクトのドリルダウン
- タスクキャンセルエンドポイントと確認付きの UI アクション
</details>
<details>
<summary><b>🌊 24. 「A2A ロード用のアクティブ ストリーム メトリクスが必要です」</b></summary>
ストリーミング ワークフローには、同時実行性とライブ接続に関する運用上の洞察が必要です。
**OmniRoute がそれを解決する方法:**
- アクティブ ストリーム カウンターが A2A ステータスに統合されました
- 最後のタスクのタイムスタンプと状態ごとのカウント
- リアルタイム運用監視用の A2A ダッシュボード カード
</details>
<details>
<summary><b>🪪 25. 「クライアントの標準エージェント検出が必要です」</b></summary>
外部クライアントとオーケストレーターには、オンボーディング用の機械可読メタデータが必要です。
**OmniRoute がそれを解決する方法:**
- エージェント カードが `/.well-known/agent.json` で公開される
- 管理UIに表示される能力とスキル
- A2A ステータス API には自動化のための検出メタデータが含まれています
</details>
<details>
<summary><b>🧭 26. 「製品 UX にプロトコルの検出機能が必要です」</b></summary>
ユーザーがプロトコルの表面を発見できない場合、導入とサポートの品質が低下します。
**OmniRoute がそれを解決する方法:**
- MCP および A2A のサイドバー エントリ
- クイックスタートとステータスを含むエンドポイント ページの [プロトコル] タブ
- 概要から専用の管理ダッシュボードへのリンク
</details>
<details>
<summary><b>🧪 27. 「実際のクライアントを使用したエンドツーエンドのプロトコル検証が必要です」</b></summary>
模擬テストは、リリース前にプロトコルの互換性を検証するには十分ではありません。
**OmniRoute がそれを解決する方法:**
- アプリを起動し、実際の MCP SDK クライアント トランスポートを使用する E2E スイート
- A2A クライアントは、フローの検出、送信、ストリーミング、取得、キャンセルをテストします。
- MCP 監査および A2A タスク API に対するアサーションのクロスチェック
</details>
<details>
<summary><b>📡 28. 「すべてのインターフェースにわたって統合された可観測性が必要です」</b></summary>
プロトコルごとに可観測性を分割すると、盲点が生じ、MTTR が長くなります。
**OmniRoute がそれを解決する方法:**
- ダッシュボード/ログ/分析を 1 つの製品に統合
- OpenAI、MCP、A2A レイヤーにわたるヘルス + 監査 + リクエスト テレメトリ
- ステータスと自動化のための運用 API
</details>
<details>
<summary><b>💼 29. 「プロキシ + ツール + エージェント オーケストレーション用のランタイムが 1 つ必要です」</b></summary>
多くの個別のサービスを実行すると、運用コストが増加し、障害モードが増加します。
**OmniRoute がそれを解決する方法:**
- OpenAI互換プロキシ、MCPサーバー、A2Aサーバーを1つのスタックに搭載
- 共有認証、復元力、データストア、可観測性
- すべての対話面にわたる一貫したポリシー モデル
</details>
<details>
<summary><b>🚀 30. 「グルーコードのスプロールなしでエージェント ワークフローを出荷する必要がある」</b></summary>
複数のアドホック サービスとスクリプトをつなぎ合わせると、チームの速度が低下します。
**OmniRoute がそれを解決する方法:**
- クライアントとエージェント向けの統合エンドポイント戦略
- 組み込みのプロトコル管理 UI とスモーク検証パス
- 本番環境に対応した基盤 (セキュリティ、ロギング、復元力、バックアップ)
</details>
### プレイブックの例 (統合されたユースケース)
**戦略 A: 有料サブスクリプション + 安価なバックアップを最大限に活用する**
```txt
Combo: "maximize-claude"
1. cc/claude-opus-4-6
2. glm/glm-4.7
3. if/kimi-k2-thinking
Monthly cost: $20 + small backup spend
Outcome: higher quality, near-zero interruption
```
**プレイブック B: ゼロコストのコーディング スタック**
```txt
Combo: "free-forever"
1. gc/gemini-3-flash
2. if/kimi-k2-thinking
3. qw/qwen3-coder-plus
Monthly cost: $0
Outcome: stable free coding workflow
```
**プレイブック C: 24 時間年中無休の常時オンのフォールバック チェーン**
```txt
Combo: "always-on"
1. cc/claude-opus-4-6
2. cx/gpt-5.2-codex
3. glm/glm-4.7
4. minimax/MiniMax-M2.1
5. if/kimi-k2-thinking
Outcome: deep fallback depth for deadline-critical workloads
```
**プレイブック D: MCP + A2A を使用したエージェントの運用**
```txt
1) Start MCP transport (`omniroute --mcp`) for tool-driven operations
2) Run A2A tasks via `message/send` and `message/stream`
3) Observe via /dashboard/mcp and /dashboard/a2a
4) Control incidents with resilience profile + task cancellation
```
---
## ⚡ クイックスタート
**1.グローバルにインストール:**
@@ -247,6 +780,30 @@ docker compose --profile cli up -d
---
---
## 🖥️
> 🆕 **新機能!** OmniRouteが**ネイティブデスクトップアプリケーション**としてWindows、macOS、Linuxで利用可能になりました。
- 🖥️ **ネイティブウィンドウ** — システムトレイ統合付きの専用ウィンドウ
- 🔄 **自動起動** — システムログイン時にOmniRouteを起動
- 🔔 **ネイティブ通知** — クォータ枯渇やプロバイダー問題のアラート
-**ワンクリックインストール** — NSIS (Windows)、DMG (macOS)、AppImage (Linux)
- 🌐 **オフラインモード** — 内蔵サーバーで完全オフライン動作
```bash
npm run electron:dev # 開発モード
npm run electron:build # 現在のプラットフォーム
npm run electron:build:win # Windows (.exe)
npm run electron:build:mac # macOS (.dmg)
npm run electron:build:linux # Linux (.AppImage)
```
📖 完全なドキュメント:[`electron/README.md`](electron/README.md)
---
## 💰 価格の概要
| 階層 | プロバイダー | コスト | クォータのリセット | 最適な用途 |
@@ -272,67 +829,6 @@ docker compose --profile cli up -d
---
## 🎯 使用例
### ケース 1: 「Claude Pro サブスクリプションを持っています」
**問題:** 大量のコーディング中にクォータが使用されずに期限切れになり、レート制限が発生する
```
Combo: "maximize-claude"
1. cc/claude-opus-4-6 (use subscription fully)
2. glm/glm-4.7 (cheap backup when quota out)
3. if/kimi-k2-thinking (free emergency fallback)
Monthly cost: $20 (subscription) + ~$5 (backup) = $25 total
vs. $20 + hitting limits = frustration
```
### ケース 2: 「コストをゼロにしたい」
**問題:** サブスクリプションを購入する余裕がないため、信頼性の高い AI コーディングが必要です
```
Combo: "free-forever"
1. gc/gemini-3-flash (180K free/month)
2. if/kimi-k2-thinking (unlimited free)
3. qw/qwen3-coder-plus (unlimited free)
Monthly cost: $0
Quality: Production-ready models
```
### ケース 3: 「24 時間年中無休でコーディングが必要で、中断はありません」
**問題:** 締め切りが迫っており、ダウンタイムを許すことができません
```
Combo: "always-on"
1. cc/claude-opus-4-6 (best quality)
2. cx/gpt-5.2-codex (second subscription)
3. glm/glm-4.7 (cheap, resets daily)
4. minimax/MiniMax-M2.1 (cheapest, 5h reset)
5. if/kimi-k2-thinking (free unlimited)
Result: 5 layers of fallback = zero downtime
```
### ケース 4: 「OpenClaw に無料の AI が欲しい」
**問題:** メッセージング アプリには AI アシスタントが必要ですが、完全に無料です
```
Combo: "openclaw-free"
1. if/glm-4.7 (unlimited free)
2. if/minimax-m2.1 (unlimited free)
3. if/kimi-k2-thinking (unlimited free)
Monthly cost: $0
Access via: WhatsApp, Telegram, Slack, Discord, iMessage, Signal...
```
---
## 💡 主な機能
### 🧠 コアルーティングとインテリジェンス
@@ -348,6 +844,8 @@ Access via: WhatsApp, Telegram, Slack, Discord, iMessage, Signal...
| 🧩 **カスタムモデル** | 任意のモデル ID を任意のプロバイダーに追加する |
| 🌐 **ワイルドカードルーター** | `provider/*` パターンを任意のプロバイダーに動的にルーティングする |
| 🧠 **予算を考える** | 推論モデルのパススルー、自動、カスタム、および適応モード |
| 🔀 **Model Aliases** | Auto-forward deprecated model IDs to current replacements (built-in + custom) |
| ⚡ **Background Degradation** | Auto-route background tasks (titles, summaries) to cheaper models |
| 💬 **システム プロンプト インジェクション** | すべてのリクエストに適用されるグローバル システム プロンプト |
| 📄 **レスポンス API** | Codex の OpenAI Response API (`/v1/responses`) の完全なサポート |
@@ -367,12 +865,15 @@ Access via: WhatsApp, Telegram, Slack, Discord, iMessage, Signal...
| 特集 | 何をするのか |
| -------------------------------- | ---------------------------------------------------------------------------- |
| 🔌 **サーキットブレーカー** | 設定可能なしきい値によるプロバイダーごとの自動開閉 |
| 🎯 **Endpoint-Aware Models** | Custom models declare supported endpoints + API format |
| 🛡️ **対雷鳴の群れ** | API キープロバイダーのミューテックス + セマフォのレート制限 |
| 🧠 **セマンティック キャッシュ** | 2 層キャッシュ (シグネチャ + セマンティック) によりコストと遅延が削減 |
| ⚡ **冪等性のリクエスト** | 重複リクエストに対する 5 秒の重複除去ウィンドウ |
| 🔒 **TLS 指紋スプーフィング** | wreq-js 経由で TLS ベースのボット検出をバイパスする |
| 🌐 **IP フィルタリング** | API アクセス制御の許可リスト/ブロックリスト |
| 📊 **編集可能なレート制限** | システム レベルで構成可能な RPM、最小ギャップ、最大同時実行 |
| 💾 **Rate Limit Persistence** | Learned limits survive restarts via SQLite with 60s debounce + 24h staleness |
| 🔄 **Token Refresh Resilience** | Per-provider circuit breaker (5 fails→30min) + 30s timeout per attempt |
| 🛡 **API エンドポイント保護** | `/models` エンドポイントの認証ゲート + プロバイダー ブロック |
| 🔒 **プロキシの可視性** | 色分けされたバッジ: 🟢 グローバル、🟡 プロバイダー、🔵 IP 表示による接続ごと |
| 🌐 **3 レベルのプロキシ構成** | グローバル、プロバイダーごと、または接続ごとのレベルでプロキシを構成する |
@@ -465,6 +966,7 @@ Combo: "my-coding-stack"
- システムステータス (稼働時間、バージョン、メモリ使用量)
- プロバイダーごとのサーキットブレーカーの状態 (クローズ/オープン/ハーフオープン)
| 🎯 **Endpoint-Aware Models** | Custom models declare supported endpoints + API format |
- レート制限ステータスとアクティブなロックアウト
- 署名キャッシュ統計
- レイテンシ テレメトリ (p50/p95/p99) + プロンプト キャッシュ
@@ -491,6 +993,27 @@ OmniRoute には、API 翻訳のデバッグ、テスト、監視のための **
</details>
## 🧪 評価 (Evals)
OmniRoute には、ゴールデン セットに対して LLM 応答品質をテストするための評価フレームワークが組み込まれています。ダッシュボードの **Analytics → Evals** からアクセスします。
### 内蔵ゴールデンセット
プリロードされた「OmniRoute Golden Set」には、以下をカバーする 10 のテスト ケースが含まれています。
- 挨拶、数学、地理、コード生成
- JSON形式への準拠、翻訳、マークダウン
- 安全拒否(有害なコンテンツ)、カウント、ブール論理
### 評価戦略
| 戦略 | 説明 | 例 |
| ---------- | ------------------------------------------------------------------------------------------- | ----------- |
| `exact` | 出力は正確に一致する必要があります | `"4"` |
| `contains` | 出力には部分文字列が含まれている必要があります (大文字と小文字は区別されません)。 `"Paris"` |
| `regex` | 出力は正規表現パターンと一致する必要があります | `"1.*2.*3"` |
| `custom` | カスタム JS 関数は true/false を返します。 `(output) => output.length > 10` |
---
## 📖 セットアップガイド
@@ -772,104 +1295,64 @@ Settings → API Configuration:
---
## 📊 利用可能なモデル
## 🐛 トラブルシューティング
<details>
<summary><b>利用可能なモデルをすべて表示</b></summary>
<summary><b>クリックしてトラブルシューティング ガイドを展開</b></summary>
**クロード コード (`cc/`)** - Pro/Max:
**「言語モデルがメッセージを提供しませんでした」**
- `cc/claude-opus-4-6`
- `cc/claude-sonnet-4-5-20250929`
- `cc/claude-haiku-4-5-20251001`
- プロバイダー クォータが枯渇した → ダッシュボード クォータ トラッカーを確認してください
- 解決策: コンボフォールバックを使用するか、より安価なレベルに切り替える
**コーデックス (`cx/`)** - プラス/プロ:
**レート制限**
- `cx/gpt-5.2-codex`
- `cx/gpt-5.1-codex-max`
- サブスクリプション クォータ アウト → GLM/MiniMax へのフォールバック
- コンボを追加: `cc/claude-opus-4-6 → glm/glm-4.7 → if/kimi-k2-thinking`
**Gemini CLI (`gc/`)** - 無料:
**OAuth トークンの有効期限が切れました**
- `gc/gemini-3-flash-preview`
- `gc/gemini-2.5-pro`
- OmniRouteによる自動更新
- 問題が解決しない場合: ダッシュボード → プロバイダー → 再接続
**GitHub コパイロット (`gh/`)**:
**高コスト**
- `gh/gpt-5`
- `gh/claude-4.5-sonnet`
- [ダッシュボード] → [コスト] で使用状況の統計を確認します。
- プライマリ モデルを GLM/MiniMax に切り替えます
- 重要ではないタスクには無料枠 (Gemini CLI、iFlow) を使用する
**NVIDIA NIM (`nvidia/`)** - 無料クレジット:
**ダッシュボードが間違ったポートで開きます**
- `nvidia/llama-3.3-70b-instruct`
- `nvidia/mistral-7b-instruct`
- [build.nvidia.com](https://build.nvidia.com) には 50 以上のモデル
- `PORT=20128` および `NEXT_PUBLIC_BASE_URL=http://localhost:20128` を設定します
**GLM (`glm/`)** - 0.6 ドル/100 万:
**クラウド同期エラー**
- `glm/glm-4.7`
- `BASE_URL` が実行中のインスタンスを指していることを確認します
- `CLOUD_URL` が予想されるクラウド エンドポイントを指していることを確認します
- `NEXT_PUBLIC_*` 値をサーバー側の値と一致させます。
**MiniMax (`minimax/`)** - $0.2/100 万:
**最初のログインが機能しない**
- `minimax/MiniMax-M2.1`
- `.env``INITIAL_PASSWORD` を確認してください
- 設定されていない場合、フォールバック パスワードは `123456` です
**iFlow (`if/`)** - 無料:
**リクエストログなし**
- `if/kimi-k2-thinking`
- `if/qwen3-coder-plus`
- `if/deepseek-r1`
- `if/glm-4.7`
- `if/minimax-m2`
- `.env``ENABLE_REQUEST_LOGS=true` を設定します
**クウェン (`qw/`)** - 無料:
**OpenAI 互換プロバイダーの接続テストで「無効」と表示される**
- `qw/qwen3-coder-plus`
- `qw/qwen3-coder-flash`
- 多くのプロバイダーは `/models` エンドポイントを公開していません
- OmniRoute v1.0.6+ には、チャット完了によるフォールバック検証が含まれています
- ベース URL に `/v1` サフィックスが含まれていることを確認してください
**キロ (`kr/`)** - 無料:
- `kr/claude-sonnet-4.5`
- `kr/claude-haiku-4.5`
**OpenRouter (`or/`)** - 100 以上のモデル:
- `or/anthropic/claude-4-sonnet`
- `or/google/gemini-2.5-pro`
- [openrouter.ai/models](https://openrouter.ai/models) の任意のモデル
</details>
---
## 🧪 評価 (Evals)
OmniRoute には、ゴールデン セットに対して LLM 応答品質をテストするための評価フレームワークが組み込まれています。ダッシュボードの **Analytics → Evals** からアクセスします。
### 内蔵ゴールデンセット
プリロードされた「OmniRoute Golden Set」には、以下をカバーする 10 のテスト ケースが含まれています。
- 挨拶、数学、地理、コード生成
- JSON形式への準拠、翻訳、マークダウン
- 安全拒否(有害なコンテンツ)、カウント、ブール論理
### 評価戦略
| 戦略 | 説明 | 例 |
| ---------- | ------------------------------------------------------------------------------------------- | ----------- |
| `exact` | 出力は正確に一致する必要があります | `"4"` |
| `contains` | 出力には部分文字列が含まれている必要があります (大文字と小文字は区別されません)。 `"Paris"` |
| `regex` | 出力は正規表現パターンと一致する必要があります | `"1.*2.*3"` |
| `custom` | カスタム JS 関数は true/false を返します。 `(output) => output.length > 10` |
---
## 🔐 サーバーリモートの OAuth (リモート OAuth セットアップ)
### 🔐 サーバーリモートの OAuth (リモート OAuth セットアップ)
<a name="oauth-em-servidor-remoto"></a>
> **⚠️ VPS/Docker/サーバーリモートの OmniRoute に関する重要事項**
### Antigravity / Gemini CLI で OAuth を実行すると、リモートのサービスが提供されますか?
### OAuth
**反重力****Gemini CLI** を使用して **Google OAuth 2.0** を認証します。 O Google exige que a `redirect_uri` usada no fluxo OAuth seja **exatamente** uma das URIs pré-cadastradas Google Cloud Console でアプリケーションを実行できません。
@@ -954,64 +1437,11 @@ docker restart omniroute
> 自動回避策の機能は URL から独立してリダイレクトされます。
---
## 🐛 トラブルシューティング
<details>
<summary><b>クリックしてトラブルシューティング ガイドを展開</b></summary>
**「言語モデルがメッセージを提供しませんでした」**
- プロバイダー クォータが枯渇した → ダッシュボード クォータ トラッカーを確認してください
- 解決策: コンボフォールバックを使用するか、より安価なレベルに切り替える
**レート制限**
- サブスクリプション クォータ アウト → GLM/MiniMax へのフォールバック
- コンボを追加: `cc/claude-opus-4-6 → glm/glm-4.7 → if/kimi-k2-thinking`
**OAuth トークンの有効期限が切れました**
- OmniRouteによる自動更新
- 問題が解決しない場合: ダッシュボード → プロバイダー → 再接続
**高コスト**
- [ダッシュボード] → [コスト] で使用状況の統計を確認します。
- プライマリ モデルを GLM/MiniMax に切り替えます
- 重要ではないタスクには無料枠 (Gemini CLI、iFlow) を使用する
**ダッシュボードが間違ったポートで開きます**
- `PORT=20128` および `NEXT_PUBLIC_BASE_URL=http://localhost:20128` を設定します
**クラウド同期エラー**
- `BASE_URL` が実行中のインスタンスを指していることを確認します
- `CLOUD_URL` が予想されるクラウド エンドポイントを指していることを確認します
- `NEXT_PUBLIC_*` 値をサーバー側の値と一致させます。
**最初のログインが機能しない**
- `.env``INITIAL_PASSWORD` を確認してください
- 設定されていない場合、フォールバック パスワードは `123456` です
**リクエストログなし**
- `.env``ENABLE_REQUEST_LOGS=true` を設定します
**OpenAI 互換プロバイダーの接続テストで「無効」と表示される**
- 多くのプロバイダーは `/models` エンドポイントを公開していません
- OmniRoute v1.0.6+ には、チャット完了によるフォールバック検証が含まれています
- ベース URL に `/v1` サフィックスが含まれていることを確認してください
</details>
---
## 🛠️ 技術スタック
## 🛠️
- **ランタイム**: Node.js 1822 LTS (⚠️ Node.js 24+ は **サポートされていません**`better-sqlite3` ネイティブ バイナリは互換性がありません)
- **言語**: TypeScript 5.9 — `src/` および `open-sse/`**100% TypeScript** (v1.0.6)
@@ -1063,7 +1493,7 @@ docker restart omniroute
---
## 🗺️ ロードマップ
## 🗺️
OmniRoute には、複数の開発フェーズにわたって **210 以上の機能が計画されています**。主要な領域は次のとおりです。
@@ -1088,18 +1518,6 @@ OmniRoute には、複数の開発フェーズにわたって **210 以上の機
---
## 📧 サポート
> 💬 **コミュニティに参加してください!** [WhatsApp Group](https://chat.whatsapp.com/JI7cDQ1GyaiDHhVBpLxf8b?mode=gi_t) — ヘルプを取得し、ヒントを共有し、最新情報を入手してください。
- **ウェブサイト**: [omniroute.online](https://omniroute.online)
- **GitHub**: [github.com/diegosouzapw/OmniRoute](https://github.com/diegosouzapw/OmniRoute)
- **問題**: [github.com/diegosouzapw/OmniRoute/issues](https://github.com/diegosouzapw/OmniRoute/issues)
- **WhatsApp**: [Community Group](https://chat.whatsapp.com/JI7cDQ1GyaiDHhVBpLxf8b?mode=gi_t)
- **オリジナル プロジェクト**: [9router by decolua](https://github.com/decolua/9router)
---
## 👥 貢献者
[![Contributors](https://contrib.rocks/image?repo=diegosouzapw/OmniRoute&max=100&columns=20&anon=1)](https://github.com/diegosouzapw/OmniRoute/graphs/contributors)

File diff suppressed because it is too large Load Diff

1407
README.md

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -7,7 +7,7 @@
_Seu proxy de API universal — um endpoint, 36+ provedores, zero tempo de inatividade._
**Chat Completions • Embeddings • Geração de Imagem • Áudio • Reranking • 100% TypeScript**
**Chat Completions • Embeddings • Geração de Imagem • Vídeo • Música • Áudio • Reranking • 100% TypeScript**
---
@@ -110,6 +110,35 @@ _Conecte qualquer IDE ou ferramenta CLI com IA através do OmniRoute — gateway
---
## 🖼️
<div align="center">
<img src="./docs/screenshots/MainOmniRoute.png" alt="OmniRoute" width="800"/>
</div>
---
## 📸
<details>
<summary><b>...</b></summary>
| # | # |
| ----- | ---------------------------------------- |
| **1** | ![1](docs/screenshots/01-providers.png) |
| **2** | ![2](docs/screenshots/02-combos.png) |
| **3** | ![3](docs/screenshots/03-analytics.png) |
| **4** | ![4](docs/screenshots/04-health.png) |
| **5** | ![5](docs/screenshots/05-translator.png) |
| **6** | ![6](docs/screenshots/06-settings.png) |
| **7** | ![7](docs/screenshots/07-cli-tools.png) |
| **8** | ![8](docs/screenshots/08-usage.png) |
| **9** | ![9](docs/screenshots/09-endpoint.png) |
</details>
---
## 🤔 Por que OmniRoute?
**Pare de desperdiçar dinheiro e bater em limites:**
@@ -128,6 +157,18 @@ _Conecte qualquer IDE ou ferramenta CLI com IA através do OmniRoute — gateway
---
## 📧 Suporte
> 💬 **Participe da comunidade!** [Grupo WhatsApp](https://chat.whatsapp.com/JI7cDQ1GyaiDHhVBpLxf8b?mode=gi_t) — Tire dúvidas, compartilhe dicas e fique atualizado.
- **Website**: [omniroute.online](https://omniroute.online)
- **GitHub**: [github.com/diegosouzapw/OmniRoute](https://github.com/diegosouzapw/OmniRoute)
- **Issues**: [github.com/diegosouzapw/OmniRoute/issues](https://github.com/diegosouzapw/OmniRoute/issues)
- **WhatsApp**: [Grupo da Comunidade](https://chat.whatsapp.com/JI7cDQ1GyaiDHhVBpLxf8b?mode=gi_t)
- **Projeto Original**: [9router por decolua](https://github.com/decolua/9router)
---
## 🔄 Como Funciona
```
@@ -157,6 +198,473 @@ Resultado: Nunca pare de programar, custo mínimo
---
## 🎯 O que o OmniRoute resolve — 30 dores reais e casos de uso
> **Todo desenvolvedor que usa ferramentas de IA enfrenta esses problemas diariamente.** O OmniRoute foi criado para resolver todos eles, desde estouro de custos e bloqueios regionais até operações de protocolo e observabilidade de nível produção.
<details>
<summary><b>💸 1. "Pago uma assinatura cara e ainda sou interrompido por limites"</b></summary>
Desenvolvedores pagam de $20 a $200/mês por Claude Pro, Codex Pro ou GitHub Copilot. Mesmo pagando, há teto de cota, limite de 5h, limites semanais ou por minuto. No meio da sessão de coding, o provedor para de responder e o desenvolvedor perde fluxo e produtividade.
**Como o OmniRoute resolve isso:**
- **Fallback Inteligente em 4 Tiers** — Se a cota de assinatura acabar, redireciona automaticamente para API Key → Barato → Gratuito sem intervenção manual
- **Rastreamento de Cota em Tempo Real** — Exibe consumo de tokens ao vivo com contagem regressiva de reset (5h, diário, semanal)
- **Suporte Multi-Conta** — Várias contas por provedor com round-robin automático; quando uma esgota, passa para a próxima
- **Combos Personalizados** — Cadeias de fallback customizáveis com 6 estratégias (fill-first, round-robin, P2C, aleatório, least-used, cost-optimized)
- **Cotas Business do Codex** — Monitoramento de cota de workspace Business/Team direto no dashboard
</details>
<details>
<summary><b>🔌 2. "Preciso usar múltiplos provedores, mas cada um tem uma API diferente"</b></summary>
OpenAI usa um formato, Claude (Anthropic) usa outro, Gemini usa outro. Se o dev quer testar modelos de provedores diferentes ou fazer fallback entre eles, precisa reconfigurar SDKs, trocar endpoints e lidar com formatos incompatíveis. Provedores customizados (FriendLI, NIM) também têm endpoints não padronizados.
**Como o OmniRoute resolve isso:**
- **Endpoint Unificado** — Um único `http://localhost:20128/v1` serve como proxy para 36+ provedores
- **Tradução de Formato** — Conversão automática e transparente: OpenAI ↔ Claude ↔ Gemini ↔ Responses API
- **Sanitização de Resposta** — Remove campos fora do padrão (`x_groq`, `usage_breakdown`, `service_tier`) que quebram OpenAI SDK v1.83+
- **Normalização de Roles** — Converte `developer``system` para provedores não-OpenAI; `system``user` para GLM/ERNIE
- **Extração de Tags Think** — Extrai blocos `<think>` de modelos como DeepSeek R1 para `reasoning_content` padronizado
- **Saída Estruturada no Gemini** — Conversão automática de `json_schema``responseMimeType`/`responseSchema`
- **`stream` padrão `false`** — Alinha com a especificação OpenAI e evita SSE inesperado em SDKs Python/Rust/Go
</details>
<details>
<summary><b>🌐 3. "Meu provedor de IA bloqueia minha região/país"</b></summary>
Provedores como OpenAI/Codex bloqueiam acesso em determinadas regiões. Usuários recebem erros como `unsupported_country_region_territory` durante OAuth e conexões de API. Isso é especialmente frustrante para desenvolvedores de países emergentes.
**Como o OmniRoute resolve isso:**
- **Config de Proxy em 3 Níveis** — Proxy configurável em nível global (todo tráfego), por provedor e por conexão/chave
- **Badges de Proxy por Cor** — Indicadores visuais: 🟢 proxy global, 🟡 proxy do provedor, 🔵 proxy da conexão, sempre mostrando o IP
- **Troca de Token OAuth via Proxy** — O fluxo OAuth também passa pelo proxy, resolvendo `unsupported_country_region_territory`
- **Teste de Conexão via Proxy** — Testes usam o proxy configurado (sem bypass direto)
- **Suporte SOCKS5** — Suporte completo a proxy SOCKS5 para roteamento de saída
- **Spoofing de Impressão TLS** — Fingerprint TLS estilo navegador via `wreq-js` para contornar detecção de bot
</details>
<details>
<summary><b>🆓 4. "Quero usar IA para programar, mas não tenho dinheiro"</b></summary>
Nem todo mundo pode pagar $20200/mês em assinaturas de IA. Estudantes, devs de países emergentes, hobistas e freelancers precisam de acesso a modelos de qualidade com custo zero.
**Como o OmniRoute resolve isso:**
- **Provedores Gratuitos nativos** — Suporte nativo a provedores 100% free: iFlow (8 modelos ilimitados), Qwen (3 ilimitados), Kiro (Claude grátis), Gemini CLI (180K/mês grátis)
- **Combos Apenas Gratuitos** — Cadeia `gc/gemini-3-flash → if/kimi-k2-thinking → qw/qwen3-coder-plus` = $0/mês com zero downtime
- **Créditos Gratuitos NVIDIA NIM** — 1000 créditos free integrados
- **Estratégia Cost Optimized** — Estratégia que escolhe automaticamente o provedor mais barato disponível
</details>
<details>
<summary><b>🔒 5. "Preciso proteger meu gateway de IA contra acesso não autorizado"</b></summary>
Ao expor um gateway de IA na rede (LAN, VPS, Docker), qualquer pessoa com o endereço pode consumir tokens/cota do desenvolvedor. Sem proteção, as APIs ficam vulneráveis a uso indevido, prompt injection e abuso.
**Como o OmniRoute resolve isso:**
- **Gestão de API Keys** — Geração, rotação e escopo por provedor com página dedicada em `/dashboard/api-manager`
- **Permissões por Modelo** — Restringe chaves a modelos específicos (`openai/*`, padrões wildcard), com toggle Allow All/Restrict
- **Proteção de Endpoint de API** — Exige chave para `/v1/models` e bloqueia provedores específicos da listagem
- **Auth Guard + CSRF Protection** — Todas as rotas do dashboard protegidas com middleware `withAuth` + tokens CSRF
- **Rate Limiter** — Limite por IP com janelas configuráveis
- **Filtragem por IP** — Allowlist/blocklist para controle de acesso
- **Proteção contra Prompt Injection** — Sanitização contra padrões maliciosos
- **Criptografia AES-256-GCM** — Credenciais criptografadas em repouso
</details>
<details>
<summary><b>🛑 6. "Meu provedor caiu e eu perdi meu fluxo de programação"</b></summary>
Provedores de IA podem ficar instáveis, retornar erro 5xx ou atingir limites temporários de taxa. Se o dev depende de um único provedor, ele é interrompido. Sem circuit breaker, retries repetidos podem derrubar a aplicação.
**Como o OmniRoute resolve isso:**
- **Circuit Breaker por modelo** — Abre/fecha automaticamente com limiares e cooldown configuráveis (Closed/Open/Half-Open)
- **Exponential Backoff** — Atrasos progressivos de retry
- **Anti-Thundering Herd** — Proteção com mutex + semáforo contra tempestade de retries concorrentes
- **Cadeias de Fallback em Combo** — Se o primário falhar, avança automaticamente na cadeia sem intervenção
- **Circuit Breaker de Combo** — Desativa automaticamente provedores com falha dentro da cadeia
- **Health Dashboard** — Monitoramento de uptime, estados de breaker, lockouts, estatísticas de cache e latência p50/p95/p99
</details>
<details>
<summary><b>🔧 7. "Configurar cada ferramenta de IA é tedioso e repetitivo"</b></summary>
Desenvolvedores usam Cursor, Claude Code, Codex CLI, OpenClaw, Gemini CLI, Kilo Code... Cada ferramenta pede configuração diferente (endpoint, chave, modelo). Reconfigurar ao trocar de provedor ou modelo é perda de tempo.
**Como o OmniRoute resolve isso:**
- **Dashboard de Ferramentas CLI** — Página dedicada com setup em 1 clique para Claude Code, Codex CLI, OpenClaw, Kilo Code, Antigravity e Cline
- **Gerador de Config do GitHub Copilot** — Gera `chatLanguageModels.json` para VS Code com seleção em lote de modelos
- **Onboarding Wizard** — Fluxo guiado de 4 etapas para novos usuários
- **Um endpoint para todos os modelos** — Configure `http://localhost:20128/v1` uma vez e acesse 36+ provedores
</details>
<details>
<summary><b>🔑 8. "Gerenciar tokens OAuth de múltiplos provedores é um caos"</b></summary>
Claude Code, Codex, Gemini CLI e Copilot usam OAuth 2.0 com tokens que expiram. Devs precisam reautenticar o tempo todo e lidar com erros como `client_secret is missing`, `redirect_uri_mismatch` e falhas em servidores remotos. OAuth em LAN/VPS é especialmente problemático.
**Como o OmniRoute resolve isso:**
- **Auto Token Refresh** — Tokens OAuth renovados em background antes da expiração
- **OAuth 2.0 (PKCE) nativo** — Fluxo automático para Claude Code, Codex, Gemini CLI, Copilot, Kiro, Qwen e iFlow
- **OAuth Multi-Conta** — Múltiplas contas por provedor via extração de JWT/ID token
- **Correções OAuth LAN/Remoto** — Detecção de IP privado para `redirect_uri` + modo manual de URL para servidores remotos
- **OAuth atrás de Nginx** — Usa `window.location.origin` para compatibilidade com reverse proxy
- **Guia de OAuth Remoto** — Passo a passo para credenciais Google Cloud em VPS/Docker
</details>
<details>
<summary><b>📊 9. "Não sei quanto estou gastando nem onde"</b></summary>
Desenvolvedores usam vários provedores pagos, mas não têm visão unificada de gastos. Cada provedor tem seu dashboard de billing, sem consolidação. Custos inesperados podem se acumular.
**Como o OmniRoute resolve isso:**
- **Dashboard de Analytics de Custo** — Rastreamento de custo por token e gestão de orçamento por provedor
- **Limites de Orçamento por Tier** — Teto de gasto por tier que aciona fallback automático
- **Configuração de Preço por Modelo** — Preços configuráveis por modelo
- **Estatísticas de Uso por API Key** — Contagem de requests e timestamp de último uso por chave
- **Analytics Dashboard** — Cards, gráfico de uso por modelo e tabela de provedores com taxa de sucesso e latência
</details>
<details>
<summary><b>🐛 10. "Não consigo diagnosticar erros e problemas nas chamadas de IA"</b></summary>
Quando uma chamada falha, o dev não sabe se foi rate limit, token expirado, formato incorreto ou erro do provedor. Logs ficam fragmentados em terminais diferentes. Sem observabilidade, debug vira tentativa e erro.
**Como o OmniRoute resolve isso:**
- **Dashboard de Logs Unificado** — 4 abas: Request Logs, Proxy Logs, Audit Logs e Console
- **Visualizador de Console** — Viewer em tempo real estilo terminal com níveis por cor, auto-scroll, busca e filtros
- **Proxy Logs em SQLite** — Logs persistentes que sobrevivem a reinícios do servidor
- **Playground do Tradutor** — 4 modos de debug: Playground (tradução), Chat Tester (round-trip), Test Bench (lote), Live Monitor (tempo real)
- **Telemetria de Request** — Latência p50/p95/p99 + rastreamento por X-Request-Id
- **Logging em Arquivo com Rotação** — Interceptador de console grava tudo em JSON com rotação por tamanho
</details>
<details>
<summary><b>🏗️ 11. "Implantar e manter o gateway é complexo"</b></summary>
Instalar, configurar e manter um proxy de IA em ambientes diferentes (local, VPS, Docker, cloud) exige muito trabalho. Problemas como caminhos hardcoded, `EACCES` em diretórios, conflito de portas e build cross-platform aumentam a fricção.
**Como o OmniRoute resolve isso:**
- **Instalação global via npm** — `npm install -g omniroute && omniroute` e pronto
- **Docker Multi-Platform** — AMD64 + ARM64 nativo (Apple Silicon, AWS Graviton, Raspberry Pi)
- **Perfis Docker Compose** — `base` (sem ferramentas CLI) e `cli` (com Claude Code, Codex, OpenClaw)
- **App Desktop Electron** — App nativo para Windows/macOS/Linux com bandeja, auto-start e modo offline
- **Modo de Porta Separada** — API e Dashboard em portas distintas para cenários avançados (reverse proxy, rede de containers)
- **Cloud Sync** — Sincronização de configuração entre dispositivos via Cloudflare Workers
- **Backups de DB** — Backup automático, restauração, export e import de todas as configurações
</details>
<details>
<summary><b>🌍 12. "A interface é só em inglês e meu time não fala inglês"</b></summary>
Times em países não anglófonos, especialmente na América Latina, Ásia e Europa, sofrem com interfaces só em inglês. A barreira de idioma reduz adoção e aumenta erros de configuração.
**Como o OmniRoute resolve isso:**
- **i18n do Dashboard — 30 idiomas** — Mais de 500 chaves traduzidas, incluindo árabe, búlgaro, dinamarquês, alemão, espanhol, finlandês, francês, hebraico, hindi, húngaro, indonésio, italiano, japonês, coreano, malaio, holandês, norueguês, polonês, português (PT/BR), romeno, russo, eslovaco, sueco, tailandês, ucraniano, vietnamita, chinês, filipino e inglês
- **Suporte RTL** — Suporte right-to-left para árabe e hebraico
- **READMEs multilíngues** — 30 traduções completas de documentação
- **Seletor de Idioma** — Ícone de globo no header para troca em tempo real
</details>
<details>
<summary><b>🔄 13. "Preciso de mais do que chat: embeddings, imagens, áudio"</b></summary>
IA não é só chat completion. Devs precisam gerar imagens, transcrever áudio, criar embeddings para RAG, reranquear documentos e moderar conteúdo. Cada API tem endpoint e formato diferentes.
**Como o OmniRoute resolve isso:**
- **Embeddings** — `/v1/embeddings` com 6 provedores e 9+ modelos
- **Geração de Imagem** — `/v1/images/generations` com 10 provedores e 20+ modelos (OpenAI, xAI, Together, Fireworks, Nebius, Hyperbolic, NanoBanana, Antigravity, SD WebUI, ComfyUI)
- **Texto para Vídeo** — `/v1/videos/generations` com ComfyUI (AnimateDiff, SVD) e SD WebUI
- **Texto para Música** — `/v1/music/generations` com ComfyUI (Stable Audio Open, MusicGen)
- **Transcrição de Áudio** — `/v1/audio/transcriptions` com Whisper + Nvidia NIM, HuggingFace e Qwen3
- **Texto para Fala (TTS)** — `/v1/audio/speech` com ElevenLabs, Nvidia NIM, HuggingFace, Coqui, Tortoise e Qwen3
- **Moderações** — `/v1/moderations` para checagens de segurança de conteúdo
- **Reranking** — `/v1/rerank` para relevância de documentos
- **Responses API** — Suporte completo a `/v1/responses` para Codex
</details>
<details>
<summary><b>🧪 14. "Não tenho como testar e comparar qualidade entre modelos"</b></summary>
Desenvolvedores querem saber qual modelo é melhor para cada caso de uso (código, tradução, raciocínio), mas comparar manualmente é lento. Não existem ferramentas integradas de avaliação na maioria das stacks.
**Como o OmniRoute resolve isso:**
- **Avaliações de LLM** — Golden set com 10 casos pré-carregados cobrindo saudação, matemática, geografia, geração de código, conformidade JSON, tradução, markdown e recusa de conteúdo inseguro
- **4 Estratégias de Match** — `exact`, `contains`, `regex`, `custom` (função JS)
- **Test Bench do Playground do Tradutor** — Testes em lote com múltiplas entradas/saídas esperadas e comparação entre provedores
- **Chat Tester** — Round-trip completo com renderização visual da resposta
- **Live Monitor** — Stream em tempo real de todas as requisições que passam pelo proxy
</details>
<details>
<summary><b>📈 15. "Preciso escalar sem perder performance"</b></summary>
À medida que o volume cresce, sem cache as mesmas perguntas geram custos duplicados. Sem idempotência, requisições duplicadas desperdiçam processamento. Também é necessário respeitar rate limits por provedor.
**Como o OmniRoute resolve isso:**
- **Cache Semântico** — Cache em duas camadas (assinatura + semântico) para reduzir custo e latência
- **Idempotência de Request** — Janela de deduplicação de 5s para requisições idênticas
- **Detecção de Rate Limit** — Rastreamento por provedor de RPM, intervalo mínimo e concorrência máxima
- **Rate Limits Editáveis** — Padrões configuráveis em Settings → Resilience com persistência
- **Cache de Validação de API Key** — Cache em 3 camadas para performance em produção
- **Health Dashboard com Telemetria** — Latência p50/p95/p99, estatísticas de cache e uptime
</details>
<details>
<summary><b>🤖 16. "Quero controlar o comportamento dos modelos globalmente"</b></summary>
Desenvolvedores podem querer todas as respostas em um idioma específico, com tom específico ou com limite de tokens de raciocínio. Configurar isso em cada ferramenta/requisição é impraticável.
**Como o OmniRoute resolve isso:**
- **Injeção de System Prompt** — Prompt global aplicado a todas as requisições
- **Validação de Thinking Budget** — Controle de alocação de tokens de raciocínio por requisição (passthrough, auto, custom, adaptive)
- **6 Estratégias de Roteamento** — Estratégias globais que definem como as requisições são distribuídas
- **Wildcard Router** — Padrões `provider/*` roteiam dinamicamente para qualquer provedor
- **Toggle de Combo** — Ativa/desativa combos diretamente no dashboard
- **Toggle de Provedor** — Ativa/desativa todas as conexões de um provedor com um clique
- **Provedores Bloqueados** — Exclui provedores específicos da listagem de `/v1/models`
</details>
<details>
<summary><b>🧰 17. "Preciso de ferramentas MCP como capacidades de primeira classe do produto"</b></summary>
Muitos gateways de IA expõem MCP apenas como detalhe de implementação oculto. Times precisam de uma camada operacional visível e gerenciável.
**Como o OmniRoute resolve isso:**
- MCP aparece no menu do dashboard e na aba de protocolos em Endpoint
- Página dedicada de gestão MCP com processo, ferramentas, escopos e auditoria
- Quick-start embutido para `omniroute --mcp` e onboarding de clientes
</details>
<details>
<summary><b>🧠 18. "Preciso de orquestração A2A com caminhos síncronos + streaming"</b></summary>
Fluxos de agentes precisam de respostas diretas e também de execuções longas com streaming e controle de ciclo de vida.
**Como o OmniRoute resolve isso:**
- Endpoint A2A JSON-RPC (`POST /a2a`) com `message/send` e `message/stream`
- Streaming SSE com propagação de estado terminal
- APIs de ciclo de vida de tarefas para `tasks/get` e `tasks/cancel`
</details>
<details>
<summary><b>🛰️ 19. "Preciso de saúde real do processo MCP, não status estimado"</b></summary>
Times operacionais precisam saber se o MCP está realmente ativo, não apenas se uma API está respondendo.
**Como o OmniRoute resolve isso:**
- Arquivo de heartbeat em runtime com PID, timestamps, transporte, quantidade de ferramentas e modo de escopo
- API de status MCP combinando heartbeat + atividade recente
- Cards de status na UI para processo/uptime/frescor do heartbeat
</details>
<details>
<summary><b>📋 20. "Preciso de execução auditável das ferramentas MCP"</b></summary>
Quando ferramentas alteram configuração ou disparam ações operacionais, os times precisam de rastreabilidade forense.
**Como o OmniRoute resolve isso:**
- Auditoria de chamadas MCP baseada em SQLite
- Filtros por ferramenta, sucesso/falha, chave de API e paginação
- Tabela de auditoria no dashboard + endpoints de métricas para automação
</details>
<details>
<summary><b>🔐 21. "Preciso de permissões MCP por escopo para cada integração"</b></summary>
Clientes diferentes devem operar com privilégio mínimo por categoria de ferramenta.
**Como o OmniRoute resolve isso:**
- 9 escopos MCP granulares para controle de acesso às ferramentas
- Aplicação de escopo e visibilidade na UI de gestão MCP
- Postura segura por padrão para operações sensíveis
</details>
<details>
<summary><b>⚙️ 22. "Preciso de controles operacionais sem redeploy"</b></summary>
Times precisam de mudanças rápidas em runtime durante incidentes e picos de custo.
**Como o OmniRoute resolve isso:**
- Troca de ativação de combo direto no dashboard de MCP
- Aplicação de perfis de resiliência via pacotes de política prontos
- Reset de circuit breaker no mesmo painel operacional
</details>
<details>
<summary><b>🔄 23. "Preciso de visibilidade ao vivo do ciclo de vida A2A e cancelamento"</b></summary>
Sem visibilidade de lifecycle, incidentes de tarefas ficam difíceis de investigar e corrigir.
**Como o OmniRoute resolve isso:**
- Listagem/filtragem de tarefas por estado/skill com paginação
- Drill-down de metadados, eventos e artefatos da tarefa
- Endpoint de cancelamento + ação de UI com confirmação
</details>
<details>
<summary><b>🌊 24. "Preciso de métricas de streams ativos para carga A2A"</b></summary>
Fluxos em streaming exigem visão operacional de concorrência e conexões ativas.
**Como o OmniRoute resolve isso:**
- Contadores de streams ativos integrados ao status A2A
- Timestamp da última tarefa e contagens por estado
- Cards no dashboard A2A para monitoramento operacional em tempo real
</details>
<details>
<summary><b>🪪 25. "Preciso de descoberta padrão de agente para clientes"</b></summary>
Clientes externos e orquestradores precisam de metadados legíveis por máquina para onboarding automático.
**Como o OmniRoute resolve isso:**
- Agent Card exposto em `/.well-known/agent.json`
- Capacidades e skills exibidas na UI de gestão
- API de status A2A inclui metadados de descoberta para automação
</details>
<details>
<summary><b>🧭 26. "Preciso de descobribilidade de protocolos na experiência do produto"</b></summary>
Se os usuários não encontram superfícies de protocolo, adoção e qualidade de suporte caem.
**Como o OmniRoute resolve isso:**
- Entradas MCP e A2A na sidebar
- Aba Protocolos em Endpoint com quick-start e status
- Links do overview para dashboards dedicados de gestão
</details>
<details>
<summary><b>🧪 27. "Preciso de validação end-to-end de protocolo com clientes reais"</b></summary>
Testes mockados não bastam para validar compatibilidade de protocolo antes do release.
**Como o OmniRoute resolve isso:**
- Suíte E2E que sobe a aplicação e usa transporte real do SDK MCP
- Testes de cliente A2A para discovery, send, stream, get e cancel
- Cross-check das validações com APIs de auditoria MCP e tarefas A2A
</details>
<details>
<summary><b>📡 28. "Preciso de observabilidade unificada em todas as interfaces"</b></summary>
Separar observabilidade por protocolo cria pontos cegos e aumenta o MTTR.
**Como o OmniRoute resolve isso:**
- Dashboards/logs/analytics unificados no mesmo produto
- Saúde + auditoria + telemetria de requisição em OpenAI, MCP e A2A
- APIs operacionais de status para automação
</details>
<details>
<summary><b>💼 29. "Preciso de um runtime único para proxy + tools + orquestração de agentes"</b></summary>
Manter vários serviços separados aumenta custo operacional e modos de falha.
**Como o OmniRoute resolve isso:**
- Proxy OpenAI-compatible, servidor MCP e servidor A2A na mesma stack
- Autenticação, resiliência, armazenamento e observabilidade compartilhados
- Modelo de políticas consistente em todas as superfícies de interação
</details>
<details>
<summary><b>🚀 30. "Preciso entregar workflows agênticos sem sprawl de glue code"</b></summary>
Times perdem velocidade quando precisam costurar múltiplos serviços e scripts ad hoc.
**Como o OmniRoute resolve isso:**
- Estratégia de endpoint unificada para clientes e agentes
- UIs de gestão de protocolo e fluxos de validação/smoke embutidos
- Base pronta para produção (segurança, logging, resiliência e backup)
</details>
### Exemplos de Playbooks (Casos de Uso Integrados)
**Playbook A: Maximizar assinatura paga + backup barato**
```txt
Combo: "maximize-claude"
1. cc/claude-opus-4-6
2. glm/glm-4.7
3. if/kimi-k2-thinking
Custo mensal: $20 + pequeno gasto de backup
Resultado: qualidade maior, interrupção quase zero
```
**Playbook B: Stack de programação com custo zero**
```txt
Combo: "free-forever"
1. gc/gemini-3-flash
2. if/kimi-k2-thinking
3. qw/qwen3-coder-plus
Custo mensal: $0
Resultado: fluxo de coding gratuito e estável
```
## ⚡ Início Rápido
**1. Instale globalmente:**
@@ -247,6 +755,45 @@ docker compose --profile cli up -d
---
---
## 🖥️
> 🆕 **NOVO!** O OmniRoute agora está disponível como **aplicativo desktop nativo** para Windows, macOS e Linux.
Execute o OmniRoute como um aplicativo desktop autônomo — sem terminal, sem navegador, sem internet necessária para modelos locais. O app baseado em Electron inclui:
- 🖥️ **Janela Nativa** — Janela dedicada com integração à bandeja do sistema
- 🔄 **Iniciar Automaticamente** — Inicie o OmniRoute ao fazer login no sistema
- 🔔 **Notificações Nativas** — Receba alertas sobre esgotamento de cota ou problemas
-**Instalação com Um Clique** — NSIS (Windows), DMG (macOS), AppImage (Linux)
- 🌐 **Modo Offline** — Funciona totalmente offline com servidor embarcado
### Início Rápido
```bash
# Modo desenvolvimento
npm run electron:dev
# Compilar para sua plataforma
npm run electron:build # Plataforma atual
npm run electron:build:win # Windows (.exe)
npm run electron:build:mac # macOS (.dmg) — x64 & arm64
npm run electron:build:linux # Linux (.AppImage)
```
### Bandeja do Sistema
Quando minimizado, o OmniRoute fica na bandeja do sistema com ações rápidas:
- Abrir dashboard
- Alterar porta do servidor
- Sair do aplicativo
📖 Documentação completa: [`electron/README.md`](electron/README.md)
---
## 💰 Preços Resumidos
| Tier | Provedor | Custo | Reset de Cota | Melhor Para |
@@ -272,110 +819,71 @@ docker compose --profile cli up -d
---
## 🎯 Casos de Uso
### Caso 1: "Tenho assinatura Claude Pro"
**Problema:** Cota expira sem uso, limites de taxa durante programação intensa
```
Combo: "maximize-claude"
1. cc/claude-opus-4-6 (usar assinatura ao máximo)
2. glm/glm-4.7 (backup barato quando a cota acabar)
3. if/kimi-k2-thinking (fallback de emergência gratuito)
Custo mensal: $20 (assinatura) + ~$5 (backup) = $25 total
vs. $20 + bater em limites = frustração
```
### Caso 2: "Quero custo zero"
**Problema:** Não pode pagar assinaturas, precisa de IA confiável para programar
```
Combo: "free-forever"
1. gc/gemini-3-flash (180K grátis/mês)
2. if/kimi-k2-thinking (ilimitado grátis)
3. qw/qwen3-coder-plus (ilimitado grátis)
Custo mensal: $0
Qualidade: Modelos prontos para produção
```
### Caso 3: "Preciso programar 24/7, sem interrupções"
**Problema:** Prazos apertados, não pode ter tempo de inatividade
```
Combo: "always-on"
1. cc/claude-opus-4-6 (melhor qualidade)
2. cx/gpt-5.2-codex (segunda assinatura)
3. glm/glm-4.7 (barato, reset diário)
4. minimax/MiniMax-M2.1 (mais barato, reset 5h)
5. if/kimi-k2-thinking (gratuito ilimitado)
Resultado: 5 camadas de fallback = zero tempo de inatividade
```
### Caso 4: "Quero IA GRATUITA no OpenClaw"
**Problema:** Precisa de assistente de IA em aplicativos de mensagens, completamente gratuito
```
Combo: "openclaw-free"
1. if/glm-4.7 (ilimitado grátis)
2. if/minimax-m2.1 (ilimitado grátis)
3. if/kimi-k2-thinking (ilimitado grátis)
Custo mensal: $0
Acesso via: WhatsApp, Telegram, Slack, Discord, iMessage, Signal...
```
---
## 💡 Funcionalidades Principais
### 🧭 Gestão MCP + A2A (Camada Operacional)
A maioria dos gateways de IA expõe MCP/A2A apenas como endpoints “escondidos”. O OmniRoute traz operação de primeira classe para os dois protocolos:
- **Descoberta na interface** — Entradas `MCP` e `A2A` na sidebar e aba `Protocolos` na página de Endpoint com quick-start e cartões de status.
- **Painel operacional MCP** (`/dashboard/mcp`) — Status real do processo por heartbeat, inventário de ferramentas/scopes, auditoria com filtros e controles operacionais (trocar combo, aplicar perfil de resiliência, resetar breakers).
- **Painel operacional A2A** (`/dashboard/a2a`) — Visão do agent card, ciclo de vida de tarefas por estado, contagem de streams ativos, drill-down/cancelamento de tasks e smoke tests de `message/send` e `message/stream`.
- **APIs de monitoramento** — Endpoints `/api/mcp/*` e `/api/a2a/*` para status, tasks, auditoria e automações externas.
Por que isso é relevante:
- **Um runtime, três papéis**: router/proxy OpenAI-compatible + servidor de ferramentas MCP + servidor agente A2A.
- **Governança unificada**: autenticação, auditoria e controles de resiliência compartilhados.
- **Operação confiável**: times conseguem validar, monitorar e depurar comportamento dos protocolos sem sair do produto.
### 🧠 Roteamento e Inteligência
| Funcionalidade | O que Faz |
| ----------------------------------------- | ------------------------------------------------------------------------------- |
| 🎯 **Fallback Inteligente 4 Tiers** | Auto-roteamento: Assinatura → API Key → Barato → Gratuito |
| 📊 **Rastreamento de Cota em Tempo Real** | Contagem de tokens ao vivo + countdown de reset por provedor |
| 🔄 **Tradução de Formato** | OpenAI ↔ Claude ↔ Gemini ↔ Cursor ↔ Kiro transparente |
| 👥 **Suporte Multi-Conta** | Múltiplas contas por provedor com seleção inteligente |
| 🔄 **Renovação Automática de Token** | Tokens OAuth renovam automaticamente com retry |
| 🎨 **Combos Personalizados** | 6 estratégias: fill-first, round-robin, p2c, random, least-used, cost-optimized |
| 🧩 **Modelos Personalizados** | Adicione qualquer ID de modelo a qualquer provedor |
| 🌐 **Roteador Wildcard** | Roteie padrões `provider/*` para qualquer provedor dinamicamente |
| 🧠 **Budget de Raciocínio** | Modos passthrough, auto, custom e adaptativo para modelos de raciocínio |
| 💬 **Injeção de System Prompt** | System prompt global aplicado em todas as requisições |
| 📄 **API Responses** | Suporte completo à API Responses da OpenAI (`/v1/responses`) para Codex |
| Funcionalidade | O que Faz |
| ----------------------------------------- | ---------------------------------------------------------------------------------- |
| 🎯 **Fallback Inteligente 4 Tiers** | Auto-roteamento: Assinatura → API Key → Barato → Gratuito |
| 📊 **Rastreamento de Cota em Tempo Real** | Contagem de tokens ao vivo + countdown de reset por provedor |
| 🔄 **Tradução de Formato** | OpenAI ↔ Claude ↔ Gemini ↔ Cursor ↔ Kiro transparente |
| 👥 **Suporte Multi-Conta** | Múltiplas contas por provedor com seleção inteligente |
| 🔄 **Renovação Automática de Token** | Tokens OAuth renovam automaticamente com retry |
| 🎨 **Combos Personalizados** | 6 estratégias: fill-first, round-robin, p2c, random, least-used, cost-optimized |
| 🧩 **Modelos Personalizados** | Adicione qualquer ID de modelo a qualquer provedor |
| 🌐 **Roteador Wildcard** | Roteie padrões `provider/*` para qualquer provedor dinamicamente |
| 🧠 **Budget de Raciocínio** | Modos passthrough, auto, custom e adaptativo para modelos de raciocínio |
| <EFBFBD> **Aliases de Modelo** | Redireciona IDs de modelos depreciados para substitutos atuais (built-in + custom) |
| **Degradação em Background** | Redireciona tarefas em background (títulos, resumos) para modelos mais baratos |
| <20>💬 **Injeção de System Prompt** | System prompt global aplicado em todas as requisições |
| 📄 **API Responses** | Suporte completo à API Responses da OpenAI (`/v1/responses`) para Codex |
### 🎵 APIs Multi-Modal
| Funcionalidade | O que Faz |
| --------------------------- | ---------------------------------------------------- |
| 🖼️ **Geração de Imagem** | `/v1/images/generations`4 provedores, 9+ modelos |
| 📐 **Embeddings** | `/v1/embeddings` — 6 provedores, 9+ modelos |
| 🎤 **Transcrição de Áudio** | `/v1/audio/transcriptions`Compatível com Whisper |
| 🔊 **Texto para Fala** | `/v1/audio/speech`Síntese de áudio multi-provedor |
| 🛡️ **Moderações** | `/v1/moderations`Verificações de segurança |
| 🔀 **Reranking** | `/v1/rerank` — Reranking de relevância de documentos |
| Funcionalidade | O que Faz |
| --------------------------- | -------------------------------------------------------------------------------- |
| 🖼️ **Geração de Imagem** | `/v1/images/generations`10 provedores, 20+ modelos (cloud + local) |
| 📐 **Embeddings** | `/v1/embeddings` — 6 provedores, 9+ modelos |
| 🎤 **Transcrição de Áudio** | `/v1/audio/transcriptions`Whisper + Nvidia NIM, HuggingFace, Qwen3 |
| 🔊 **Texto para Fala** | `/v1/audio/speech`ElevenLabs, Nvidia NIM, HuggingFace, Coqui, Tortoise, Qwen3 |
| 🎬 **Geração de Vídeo** | `/v1/videos/generations`ComfyUI (AnimateDiff, SVD), SD WebUI |
| 🎵 **Geração de Música** | `/v1/music/generations` — ComfyUI (Stable Audio Open, MusicGen) |
| 🛡️ **Moderações** | `/v1/moderations` — Verificações de segurança |
| 🔀 **Reranking** | `/v1/rerank` — Reranking de relevância de documentos |
### 🛡️ Resiliência e Segurança
| Funcionalidade | O que Faz |
| ---------------------------------- | --------------------------------------------------------------------------- |
| 🔌 **Circuit Breaker** | Auto-abertura/fechamento por provedor com limites configuráveis |
| 🛡️ **Anti-Thundering Herd** | Mutex + semáforo rate-limit para provedores com API key |
| 🧠 **Cache Semântico** | Cache de duas camadas (assinatura + semântico) reduz custo e latência |
| **Idempotência de Requisição** | Janela de dedup de 5s para requisições duplicadas |
| 🔒 **Spoofing de Fingerprint TLS** | Bypass de detecção de bot via TLS com wreq-js |
| 🌐 **Filtragem de IP** | Allowlist/blocklist para controle de acesso à API |
| 📊 **Rate Limits Editáveis** | RPM, gap mínimo e concorrência máxima configuráveis |
| 🛡 **Proteção de Endpoint API** | Gateway de Auth + bloqueio de provedores para o endpoint `/models` |
| 🔒 **Visibilidade de Proxy** | Badges coloridos: 🟢 global, 🟡 provedor, 🔵 por-conexão com exibição de IP |
| 🌐 **Proxy em 3 Níveis** | Configure proxies em nível global, por provedor ou por conexão |
| Funcionalidade | O que Faz |
| ----------------------------------- | ----------------------------------------------------------------------------- |
| 🔌 **Circuit Breaker** | Trip/recover por modelo com limites configuráveis |
| 🎯 **Endpoint-Aware Models** | Custom models declare supported endpoints + API format |
| 🛡️ **Anti-Thundering Herd** | Mutex + semáforo rate-limit para provedores com API key |
| 🧠 **Cache Semântico** | Cache de duas camadas (assinatura + semântico) reduz custo e latência |
| **Idempotência de Requisição** | Janela de dedup de 5s para requisições duplicadas |
| 🔒 **Spoofing de Fingerprint TLS** | Bypass de detecção de bot via TLS com wreq-js |
| 🌐 **Filtragem de IP** | Allowlist/blocklist para controle de acesso à API |
| 📊 **Rate Limits Editáveis** | RPM, gap mínimo e concorrência máxima configuráveis |
| 💾 **Persistência de Rate Limits** | Limites aprendidos persistem via SQLite com debounce de 60s + 24h de validade |
| 🔄 **Resiliência de Token Refresh** | Circuit breaker por provedor (5 falhas→30min) + timeout de 30s por tentativa |
| 🛡 **Proteção de Endpoint API** | Gateway de Auth + bloqueio de provedores para o endpoint `/models` |
| 🔒 **Visibilidade de Proxy** | Badges coloridos: 🟢 global, 🟡 provedor, 🔵 por-conexão com exibição de IP |
| 🌐 **Proxy em 3 Níveis** | Configure proxies em nível global, por provedor ou por conexão |
### 📊 Observabilidade e Analytics
@@ -487,6 +995,29 @@ O OmniRoute inclui um poderoso Playground de Tradução integrado com **4 modos*
---
## 🧪 Avaliações (Evals)
OmniRoute inclui um framework de avaliação integrado para testar a qualidade de respostas de LLM contra um conjunto golden. Acesse via **Analytics → Evals** no dashboard.
### Conjunto Golden Integrado
O "OmniRoute Golden Set" pré-carregado contém 10 casos de teste cobrindo:
- Saudações, matemática, geografia, geração de código
- Conformidade de formato JSON, tradução, markdown
- Recusa de segurança (conteúdo prejudicial), contagem, lógica booleana
### Estratégias de Avaliação
| Estratégia | Descrição | Exemplo |
| ---------- | ---------------------------------------------- | -------------------------------- |
| `exact` | Saída deve corresponder exatamente | `"4"` |
| `contains` | Saída deve conter substring (case-insensitive) | `"Paris"` |
| `regex` | Saída deve corresponder ao padrão regex | `"1.*2.*3"` |
| `custom` | Função JS customizada retorna true/false | `(output) => output.length > 10` |
---
## 📖 Guia de Configuração
<details>
@@ -767,97 +1298,6 @@ Configurações → Configuração de API:
---
## 📊 Modelos Disponíveis
<details>
<summary><b>Ver todos os modelos disponíveis</b></summary>
**Claude Code (`cc/`)** - Pro/Max:
- `cc/claude-opus-4-6`
- `cc/claude-sonnet-4-5-20250929`
- `cc/claude-haiku-4-5-20251001`
**Codex (`cx/`)** - Plus/Pro:
- `cx/gpt-5.2-codex`
- `cx/gpt-5.1-codex-max`
**Gemini CLI (`gc/`)** - GRATUITO:
- `gc/gemini-3-flash-preview`
- `gc/gemini-2.5-pro`
**GitHub Copilot (`gh/`)**:
- `gh/gpt-5`
- `gh/claude-4.5-sonnet`
**NVIDIA NIM (`nvidia/`)** - Créditos GRATUITOS:
- `nvidia/llama-3.3-70b-instruct`
- `nvidia/mistral-7b-instruct`
- 50+ mais modelos em [build.nvidia.com](https://build.nvidia.com)
**GLM (`glm/`)** - $0.6/1M:
- `glm/glm-4.7`
**MiniMax (`minimax/`)** - $0.2/1M:
- `minimax/MiniMax-M2.1`
**iFlow (`if/`)** - GRATUITO:
- `if/kimi-k2-thinking`
- `if/qwen3-coder-plus`
- `if/deepseek-r1`
- `if/glm-4.7`
- `if/minimax-m2`
**Qwen (`qw/`)** - GRATUITO:
- `qw/qwen3-coder-plus`
- `qw/qwen3-coder-flash`
**Kiro (`kr/`)** - GRATUITO:
- `kr/claude-sonnet-4.5`
- `kr/claude-haiku-4.5`
**OpenRouter (`or/`)** - 100+ modelos:
- `or/anthropic/claude-4-sonnet`
- `or/google/gemini-2.5-pro`
- Qualquer modelo de [openrouter.ai/models](https://openrouter.ai/models)
</details>
---
## 🧪 Avaliações (Evals)
OmniRoute inclui um framework de avaliação integrado para testar a qualidade de respostas de LLM contra um conjunto golden. Acesse via **Analytics → Evals** no dashboard.
### Conjunto Golden Integrado
O "OmniRoute Golden Set" pré-carregado contém 10 casos de teste cobrindo:
- Saudações, matemática, geografia, geração de código
- Conformidade de formato JSON, tradução, markdown
- Recusa de segurança (conteúdo prejudicial), contagem, lógica booleana
### Estratégias de Avaliação
| Estratégia | Descrição | Exemplo |
| ---------- | ---------------------------------------------- | -------------------------------- |
| `exact` | Saída deve corresponder exatamente | `"4"` |
| `contains` | Saída deve conter substring (case-insensitive) | `"Paris"` |
| `regex` | Saída deve corresponder ao padrão regex | `"1.*2.*3"` |
| `custom` | Função JS customizada retorna true/false | `(output) => output.length > 10` |
---
## 🐛 Solução de Problemas
<details>
@@ -913,7 +1353,7 @@ O "OmniRoute Golden Set" pré-carregado contém 10 casos de teste cobrindo:
---
## 🛠️ Stack Tecnológico
## 🛠️
- **Runtime**: Node.js 20+
- **Linguagem**: TypeScript 5.9 — **100% TypeScript** em `src/` e `open-sse/` (v1.0.6)
@@ -965,7 +1405,7 @@ O "OmniRoute Golden Set" pré-carregado contém 10 casos de teste cobrindo:
---
## 🗺️ Roadmap
## 🗺️
O OmniRoute tem **210+ funcionalidades planejadas** em múltiplas fases de desenvolvimento. Áreas principais:
@@ -990,18 +1430,6 @@ O OmniRoute tem **210+ funcionalidades planejadas** em múltiplas fases de desen
---
## 📧 Suporte
> 💬 **Participe da comunidade!** [Grupo WhatsApp](https://chat.whatsapp.com/JI7cDQ1GyaiDHhVBpLxf8b?mode=gi_t) — Tire dúvidas, compartilhe dicas e fique atualizado.
- **Website**: [omniroute.online](https://omniroute.online)
- **GitHub**: [github.com/diegosouzapw/OmniRoute](https://github.com/diegosouzapw/OmniRoute)
- **Issues**: [github.com/diegosouzapw/OmniRoute/issues](https://github.com/diegosouzapw/OmniRoute/issues)
- **WhatsApp**: [Grupo da Comunidade](https://chat.whatsapp.com/JI7cDQ1GyaiDHhVBpLxf8b?mode=gi_t)
- **Projeto Original**: [9router por decolua](https://github.com/decolua/9router)
---
## 👥 Contribuidores
[![Contributors](https://contrib.rocks/image?repo=diegosouzapw/OmniRoute&max=100&columns=20&anon=1)](https://github.com/diegosouzapw/OmniRoute/graphs/contributors)

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -110,6 +110,35 @@ _Подключайте любую IDE или CLI-инструмент с AI ч
---
## 🖼️
<div align="center">
<img src="./docs/screenshots/MainOmniRoute.png" alt="OmniRoute" width="800"/>
</div>
---
## 📸
<details>
<summary><b>...</b></summary>
| # | # |
| ----- | ---------------------------------------- |
| **1** | ![1](docs/screenshots/01-providers.png) |
| **2** | ![2](docs/screenshots/02-combos.png) |
| **3** | ![3](docs/screenshots/03-analytics.png) |
| **4** | ![4](docs/screenshots/04-health.png) |
| **5** | ![5](docs/screenshots/05-translator.png) |
| **6** | ![6](docs/screenshots/06-settings.png) |
| **7** | ![7](docs/screenshots/07-cli-tools.png) |
| **8** | ![8](docs/screenshots/08-usage.png) |
| **9** | ![9](docs/screenshots/09-endpoint.png) |
</details>
---
## 🤔 Почему OmniRoute?
**Перестаньте тратить деньги и упираться в лимиты:**
@@ -128,6 +157,18 @@ _Подключайте любую IDE или CLI-инструмент с AI ч
---
## 📧 Поддержка
> 💬 **Присоединяйтесь к сообществу!** [Группа WhatsApp](https://chat.whatsapp.com/JI7cDQ1GyaiDHhVBpLxf8b?mode=gi_t) — Получайте помощь, делитесь советами и оставайтесь в курсе.
- **Сайт**: [omniroute.online](https://omniroute.online)
- **GitHub**: [github.com/diegosouzapw/OmniRoute](https://github.com/diegosouzapw/OmniRoute)
- **Issues**: [github.com/diegosouzapw/OmniRoute/issues](https://github.com/diegosouzapw/OmniRoute/issues)
- **WhatsApp**: [Группа сообщества](https://chat.whatsapp.com/JI7cDQ1GyaiDHhVBpLxf8b?mode=gi_t)
- **Оригинальный проект**: [9router от decolua](https://github.com/decolua/9router)
---
## 🔄 Как это работает
```
@@ -157,6 +198,497 @@ _Подключайте любую IDE или CLI-инструмент с AI ч
---
## 🎯 Что решает OmniRoute — 30 реальных проблем и вариантов использования
> **Каждый разработчик, использующий инструменты искусственного интеллекта, ежедневно сталкивается с этими проблемами.** OmniRoute был создан для решения всех этих проблем — от перерасхода средств до региональных блоков, от нарушенных потоков OAuth до операций протокола и наблюдения за предприятием.
<details>
<summary><b>💸 1. «Я плачу за дорогую подписку, но меня все равно прерывают лимиты» </b></summary>
Разработчики платят 20200 долларов в месяц за Claude Pro, Codex Pro или GitHub Copilot. Даже при оплате квота имеет потолок — 5 часов использования, еженедельные лимиты или поминутные ограничения. В середине сеанса кодирования провайдер перестает отвечать, и разработчик теряет поток и производительность.
**Как OmniRoute решает эту проблему:**
- **Умный 4-уровневый резерв** — если квота подписки исчерпана, происходит автоматическое перенаправление на API-ключ → Дешево → Бесплатно без вмешательства вручную.
- **Отслеживание квот в реальном времени** — показывает потребление токенов в режиме реального времени с обратным отсчетом сброса (5 часов, ежедневно, еженедельно).
- **Поддержка нескольких учетных записей** — Несколько учетных записей у каждого провайдера с автоматическим циклическим перебором — когда один из них заканчивается, переключается на следующий
- **Пользовательские комбинации** — Настраиваемые резервные цепочки с 6 стратегиями балансировки (сначала заполняемые, циклический, P2C, случайные, наименее используемые, с оптимизацией затрат)
- **Бизнес-квоты Кодекса** — мониторинг квот рабочего пространства для бизнеса/команды непосредственно на панели управления.
</details>
<details>
<summary><b>🔌 2. «Мне нужно использовать несколько поставщиков, но у каждого свой API» </b></summary>
OpenAI использует один формат, Claude (Anthropic) — другой, Gemini — третий. Если разработчик хочет протестировать модели от разных поставщиков или использовать резервный вариант между ними, ему необходимо перенастроить SDK, изменить конечные точки, разобраться с несовместимыми форматами. Пользовательские поставщики (FriendLI, NIM) имеют нестандартные конечные точки модели.
**Как OmniRoute решает эту проблему:**
- **Единая конечная точка** — один `http://localhost:20128/v1` служит прокси для всех 36+ провайдеров.
- **Перевод формата** — Автоматический и прозрачный: OpenAI ↔ Claude ↔ Gemini ↔ API ответов
- **Очистка ответов** — удаляются нестандартные поля (`x_groq`, `usage_breakdown`, `service_tier`), которые нарушают OpenAI SDK v1.83+.
- **Нормализация ролей** — преобразует `developer` в `system` для поставщиков, не поддерживающих OpenAI; `system``user` для GLM/ERNIE
- **Think Tag Extraction** — извлекает блоки `<think>` из таких моделей, как DeepSeek R1, в стандартизированный `reasoning_content`.
- **Структурированный вывод для Gemini** — автоматическое преобразование `json_schema``responseMimeType`/`responseSchema`.
- **`stream` по умолчанию — `false`** — соответствует спецификации OpenAI, что позволяет избежать неожиданного SSE в SDK Python/Rust/Go.
</details>
<details>
<summary><b>🌐 3. «Мой провайдер ИИ блокирует мой регион/страну» </b></summary>
Такие провайдеры, как OpenAI/Codex, блокируют доступ из определенных географических регионов. Пользователи получают ошибки типа `unsupported_country_region_territory` во время подключений OAuth и API. Особенно это расстраивает разработчиков из развивающихся стран.
**Как OmniRoute решает эту проблему:**
- **3-уровневая конфигурация прокси** — настраиваемый прокси-сервер на трех уровнях: глобальный (весь трафик), для каждого провайдера (только один провайдер) и для каждого соединения/ключа.
- **Значки прокси с цветной кодировкой** — Визуальные индикаторы: 🟢 глобальный прокси, 🟡 прокси-сервер провайдера, 🔵 прокси-сервер подключения, всегда показывающий IP-адрес.
- **Обмен токенов OAuth через прокси** — поток OAuth также проходит через прокси, решая проблему `unsupported_country_region_territory`.
- **Тесты подключения через прокси** — тесты подключения используют настроенный прокси-сервер (прямого обхода больше нет)
- **Поддержка SOCKS5** — Полная поддержка прокси-сервера SOCKS5 для исходящей маршрутизации.
- **Подмена отпечатка пальца TLS** — отпечаток TLS, подобный браузеру, через `wreq-js` для обхода обнаружения ботов.
</details>
<details>
<summary><b>🆓 4. «Я хочу использовать ИИ для кодирования, но у меня нет денег» </b></summary>
Не каждый может платить 20200 долларов в месяц за подписку на ИИ. Студентам, разработчикам из развивающихся стран, любителям и фрилансерам нужен доступ к качественным моделям по нулевой цене.
**Как OmniRoute решает эту проблему:**
- **Встроенные провайдеры уровня бесплатного пользования** — Встроенная поддержка 100% бесплатных провайдеров: iFlow (8 моделей с неограниченным количеством пользователей), Qwen (3 модели с неограниченным количеством пользователей), Kiro (Claude бесплатно), Gemini CLI (180 тысяч в месяц бесплатно).
- **Комбинации только бесплатно** — цепочка `gc/gemini-3-flash → if/kimi-k2-thinking → qw/qwen3-coder-plus` = 0 долларов США в месяц без простоев.
- **Бесплатные кредиты NVIDIA NIM** — интегрировано 1000 бесплатных кредитов.
- **Стратегия оптимизации затрат** — стратегия маршрутизации, которая автоматически выбирает самого дешевого доступного провайдера.
</details>
<details>
<summary><b>🔒 5. «Мне нужно защитить мой AI-шлюз от несанкционированного доступа» </b></summary>
При предоставлении доступа к сети AI-шлюза (LAN, VPS, Docker) любой, у кого есть адрес, может использовать токены/квоту разработчика. Без защиты API уязвимы для неправильного использования, быстрого внедрения и злоупотреблений.
**Как OmniRoute решает эту проблему:**
- **Управление ключами API** — генерация, ротация и определение области действия для каждого поставщика с помощью специальной страницы `/dashboard/api-manager`.
- **Разрешения на уровне модели** — Ограничьте использование ключей API определенными моделями (`openai/*`, шаблоны подстановочных знаков) с помощью переключателя Разрешить все/Ограничить.
- **API Endpoint Protection** — требует ключ для `/v1/models` и блокирует определенных поставщиков из списка.
- **Auth Guard + защита CSRF** — все маршруты информационной панели защищены промежуточным программным обеспечением `withAuth` + токенами CSRF.
- **Ограничитель скорости** — ограничение скорости для каждого IP с помощью настраиваемых окон.
- **IP-фильтрация** — список разрешенных/блокированных для контроля доступа.
- **Prompt Injection Guard** — очистка от вредоносных шаблонов подсказок.
- **Шифрование AES-256-GCM** — неактивные учетные данные зашифрованы.
</details>
<details>
<summary><b>🛑 6. «Мой провайдер вышел из строя, и я потерял процесс кодирования» </b></summary>
Поставщики ИИ могут работать нестабильно, возвращать ошибки 5xx или достигать временных ограничений скорости. Если разработчик зависит от одного провайдера, его работу прерывают. Без автоматических выключателей повторные попытки могут привести к сбою приложения.
**Как OmniRoute решает эту проблему:**
- **Выключатель для каждого поставщика** — автоматическое открытие/закрытие с настраиваемыми пороговыми значениями и временем восстановления (закрыто/открыто/полуоткрыто).
- **Экспоненциальная задержка**  прогрессивная задержка повторных попыток.
- **Anti-Thundering Herd** — Мьютекс + защита семафора от одновременных штормов повторных попыток.
- **Комбо-резервные цепочки** — в случае сбоя основного поставщика автоматически проходит через цепочку без вмешательства.
- **Комбо-выключатель** — автоматически отключает неисправных поставщиков в комбинированной цепочке.
- **Панель работоспособности** — мониторинг работоспособности, состояния автоматических выключателей, блокировки, статистика кэша, задержка p50/p95/p99.
</details>
<details>
<summary><b>🔧 7. «Настройка каждого инструмента искусственного интеллекта утомительна и повторяется» </b></summary>
Разработчики используют Cursor, Claude Code, Codex CLI, OpenClaw, Gemini CLI, Kilo Code... Для каждого инструмента требуется своя конфигурация (конечная точка API, ключ, модель). Перенастройка при смене провайдера или модели — пустая трата времени.
**Как OmniRoute решает эту проблему:**
- **Панель инструментов CLI** — выделенная страница с настройкой в один клик Claude Code, Codex CLI, OpenClaw, Kilo Code, Antigravity, Cline.
- **Генератор конфигураций GitHub Copilot** — генерирует `chatLanguageModels.json` для кода VS с массовым выбором модели.
- **Мастер адаптации** — пошаговая пошаговая настройка для начинающих пользователей.
- **Одна конечная точка, все модели** — настройте `http://localhost:20128/v1` один раз и получите доступ к более чем 36 поставщикам услуг.
</details>
<details>
<summary><b>🔑 8. «Управление токенами OAuth от нескольких провайдеров — это ад» </b></summary>
Claude Code, Codex, Gemini CLI, Copilot — все используют OAuth 2.0 с токенами с истекающим сроком действия. Разработчикам необходимо постоянно проходить повторную аутентификацию, иметь дело с `client_secret is missing`, `redirect_uri_mismatch` и сбоями на удаленных серверах. OAuth в LAN/VPS особенно проблематичен.
**Как OmniRoute решает эту проблему:**
- **Автоматическое обновление токенов** — токены OAuth обновляются в фоновом режиме до истечения срока их действия.
- **Встроенный OAuth 2.0 (PKCE)** — автоматический поток для Claude Code, Codex, Gemini CLI, Copilot, Kiro, Qwen, iFlow.
- **OAuth с несколькими учетными записями**  несколько учетных записей для каждого провайдера посредством извлечения токена JWT/ID.
- **OAuth LAN/Remote Fix** — обнаружение частного IP-адреса для `redirect_uri` + ручной режим URL-адреса для удаленных серверов.
- **OAuth за Nginx** — использует `window.location.origin` для совместимости с обратным прокси-сервером.
- **Руководство по удаленному OAuth** — пошаговое руководство по учетным данным Google Cloud на VPS/Docker.
</details>
<details>
<summary><b>📊 9. «Я не знаю, сколько и куда я трачу» </b></summary>
Разработчики используют нескольких платных поставщиков, но не имеют единого представления о расходах. У каждого провайдера есть своя панель выставления счетов, но единого представления нет. Неожиданные расходы могут накопиться.
**Как OmniRoute решает эту проблему:**
- **Панель анализа затрат** — отслеживание затрат на каждый токен и управление бюджетом для каждого поставщика.
- **Ограничения бюджета на уровень** — потолок расходов на уровень, который запускает автоматический возврат к резервному варианту.
- **Конфигурация цен на модель** — настраиваемые цены на модель.
- **Статистика использования каждого ключа API** — количество запросов и временная метка последнего использования для каждого ключа.
- **Панель аналитики** — карточки статистики, диаграмма использования модели, таблица поставщиков с показателями успеха и задержкой.
</details>
<details>
<summary><b>🐛 10. «Я не могу диагностировать ошибки и проблемы в вызовах ИИ» </b></summary>
Когда вызов завершается неудачей, разработчик не знает, было ли это ограничением скорости, сроком действия токена, неправильным форматом или ошибкой провайдера. Фрагментированные журналы на разных терминалах. Без наблюдаемости отладка осуществляется методом проб и ошибок.
**Как OmniRoute решает эту проблему:**
- **Панель управления унифицированными журналами** — 4 вкладки: журналы запросов, журналы прокси, журналы аудита, консоль.
- **Консольный просмотр журнала** — просмотрщик в режиме терминала в режиме реального времени с уровнями с цветовой кодировкой, автоматической прокруткой, поиском и фильтрацией.
- **Журналы прокси-сервера SQLite** — постоянные журналы, сохраняющиеся после перезапуска сервера.
- **Площадка переводчика** — 4 режима отладки: Площадка (перевод формата), Тестер чата (туда и обратно), Тестовый стенд (пакетный), Мониторинг в реальном времени (в режиме реального времени).
- **Запрос телеметрии** — задержка p50/p95/p99 + отслеживание X-Request-Id
- **Журналирование на основе файлов с ротацией** — перехватчик консоли записывает все в журнал JSON с ротацией на основе размера.
</details>
<details>
<summary><b>🏗️ 11. «Развертывание и обслуживание шлюза сложны» </b></summary>
Установка, настройка и обслуживание прокси-сервера AI в различных средах (локальных, VPS, Docker, облаке) — трудоемкий процесс. Такие проблемы, как жестко запрограммированные пути, `EACCES` в каталогах, конфликты портов и кроссплатформенные сборки, добавляют проблем.
**Как OmniRoute решает эту проблему:**
- **глобальная установка npm** — `npm install -g omniroute && omniroute` — выполнено
- **Мультиплатформенность Docker** — встроенная версия AMD64 + ARM64 (Apple Silicon, AWS Graviton, Raspberry Pi)
- **Профили Docker Compose** — `base` (без инструментов CLI) и `cli` (с Claude Code, Codex, OpenClaw)
- **Electron Desktop App** — собственное приложение для Windows/macOS/Linux с панелью задач, автозапуском и автономным режимом.
- **Режим разделения портов** — API и панель мониторинга на отдельных портах для расширенных сценариев (обратный прокси-сервер, сеть контейнеров).
- **Cloud Sync** — синхронизация конфигурации между устройствами через Cloudflare Workers.
- **Резервные копии БД** — автоматическое резервное копирование, восстановление, экспорт и импорт всех настроек.
</details>
<details>
<summary><b>🌍 12. «Интерфейс только на английском языке, и моя команда не говорит по-английски» </b></summary>
Команды в неанглоязычных странах, особенно в Латинской Америке, Азии и Европе, испытывают трудности с интерфейсами только на английском языке. Языковые барьеры сокращают внедрение и увеличивают количество ошибок в конфигурации.
**Как OmniRoute решает эту проблему:**
- **Панель управления i18n — 30 языков** — Все более 500 клавиш переведены, включая арабский, болгарский, датский, немецкий, испанский, финский, французский, иврит, хинди, венгерский, индонезийский, итальянский, японский, корейский, малайский, голландский, норвежский, польский, португальский (PT/BR), румынский, русский, словацкий, шведский, тайский, украинский, вьетнамский, китайский, филиппинский, английский
- **Поддержка RTL** — поддержка написания справа налево для арабского языка и иврита.
- **Многоязычные файлы README** — 30 полных переводов документации.
- **Выбор языка** — значок глобуса в заголовке для переключения в реальном времени.
</details>
<details>
<summary><b>🔄 13. «Мне нужно больше, чем просто чат — мне нужны вложения, изображения, аудио»</b></summary>
ИИ — это не просто завершение чата. Разработчикам необходимо генерировать изображения, расшифровывать аудио, создавать вложения для RAG, изменять ранжирование документов и модерировать контент. Каждый API имеет свою конечную точку и формат.
**Как OmniRoute решает эту проблему:**
- **Встраивания** — `/v1/embeddings` с 6 поставщиками и более чем 9 моделями.
- **Генерация изображений** — `/v1/images/generations` с 10 поставщиками и более чем 20 моделями (OpenAI, xAI, Together, Fireworks, Nebius, Hyperbolic, NanoBanana, Antigradity, SD WebUI, ComfyUI)
- **Преобразование текста в видео** — `/v1/videos/generations` — ComfyUI (AnimateDiff, SVD) и SD WebUI.
- **Преобразование текста в музыку** — `/v1/music/generations` — ComfyUI (Stable Audio Open, MusicGen)
- **Аудиотранскрипция** — `/v1/audio/transcriptions` — Whisper + Nvidia NIM, HuggingFace, Qwen3
- **Преобразование текста в речь** — `/v1/audio/speech` — ElevenLabs, Nvidia NIM, HuggingFace, Coqui, Tortoise, Qwen3 и + существующие поставщики
- **Модерация** — `/v1/moderations` — Проверка безопасности контента.
- **Реранжирование** — `/v1/rerank` — Изменение ранжирования релевантности документа.
- **API ответов** — полная поддержка `/v1/responses` для Кодекса.
</details>
<details>
<summary><b>🧪 14. «У меня нет возможности тестировать и сравнивать качество разных моделей» </b></summary>
Разработчики хотят знать, какая модель лучше всего подходит для их варианта использования (код, перевод, рассуждения), но сравнивать вручную — это медленно. Интегрированных инструментов оценки не существует.
**Как OmniRoute решает эту проблему:**
- **Оценки LLM** — тестирование золотого набора с 10 предварительно загруженными вариантами, охватывающими приветствия, математику, географию, генерацию кода, соответствие JSON, перевод, уценку, отказ от безопасности.
- **4 стратегии сопоставления** — `exact`, `contains`, `regex`, `custom` (функция JS)
- **Тестовый стенд Translator Playground** — пакетное тестирование с несколькими входными данными и ожидаемыми результатами, сравнение между поставщиками.
- **Тестер чата** — полный цикл с визуальным отображением ответов.
- **Живой монитор** — поток всех запросов, проходящих через прокси, в реальном времени.
</details>
<details>
<summary><b>📈 15. «Мне нужно масштабироваться без потери производительности» </b></summary>
По мере роста объема запросов без кэширования одних и тех же вопросов возникают дублирующие затраты. Без идемпотентности дублирование запросов приводит к отходам обработки. Необходимо соблюдать ограничения по тарифам для каждого поставщика.
**Как OmniRoute решает эту проблему:**
- **Семантический кеш** — двухуровневый кеш (сигнатура + семантика) снижает стоимость и задержку.
- **Request Idempotency** — окно дедупликации 5 с для идентичных запросов.
- **Обнаружение ограничения скорости** — число оборотов в минуту для каждого провайдера, минимальный разрыв и максимальное одновременное отслеживание.
- **Редактируемые ограничения скорости** — настраиваемые значения по умолчанию в меню «Настройки» → «Устойчивость с постоянством».
- **Кэш проверки ключей API** — трехуровневый кеш для повышения производительности.
- **Панель состояния с телеметрией** — задержка p50/p95/p99, статистика кэша, время безотказной работы.
</details>
<details>
<summary><b>🤖 16. «Я хочу глобально контролировать поведение модели» </b></summary>
Разработчики, которым нужны все ответы на определенном языке, с определенным тоном или которые хотят ограничить количество токенов рассуждения. Настраивать это в каждом инструменте/запросе непрактично.
**Как OmniRoute решает эту проблему:**
- **Внедрение системных подсказок** — глобальное приглашение применяется ко всем запросам.
- **Продуманная проверка бюджета** — контроль распределения токенов для каждого запроса (сквозной, автоматический, пользовательский, адаптивный)
- **6 стратегий маршрутизации** — глобальные стратегии, определяющие распределение запросов.
- **Маршрутизатор с подстановочными знаками** — шаблоны `provider/*` динамически маршрутизируются к любому поставщику.
- **Переключение/включение комбо** — переключение комбо непосредственно с панели управления.
- **Переключение поставщика** — включение/отключение всех подключений к провайдеру одним щелчком мыши.
- **Заблокированные поставщики** — исключить определенных поставщиков из списка `/v1/models`.
</details>
<details>
<summary><b>🧰 17. «Мне нужны инструменты MCP как первоклассные возможности продукта» </b></summary>
Многие шлюзы AI предоставляют MCP только как скрытую деталь реализации. Командам нужен видимый и управляемый операционный уровень.
**Как OmniRoute решает эту проблему:**
- MCP отображается на панели навигации панели управления и на вкладке протокола конечной точки.
- Отдельная страница управления MCP с процессами, инструментами, объемами работ и аудитом.
- Встроенное краткое руководство по `omniroute --mcp` и адаптации клиентов.
</details>
<details>
<summary><b>🧠 18. «Мне нужна оркестровка A2A с путями задач синхронизации и потоковой передачи» </b></summary>
Рабочие процессы агента требуют как прямых ответов, так и длительного потокового выполнения с контролем жизненного цикла.
**Как OmniRoute решает эту проблему:**
- Конечная точка A2A JSON-RPC (`POST /a2a`) с `message/send` и `message/stream`.
- Потоковая передача SSE с распространением состояния терминала
- API жизненного цикла задач для `tasks/get` и `tasks/cancel`.
</details>
<details>
<summary><b>🛰️ 19. «Мне нужно реальное состояние процесса MCP, а не угаданный статус» </b></summary>
Оперативным группам необходимо знать, действительно ли MCP работает, а не только доступен ли API.
**Как OmniRoute решает эту проблему:**
- Файл контрольного сигнала времени выполнения с PID, временными метками, транспортом, количеством инструментов и режимом области действия.
- API статуса MCP, объединяющий пульс + недавнюю активность
- Карты состояния пользовательского интерфейса для актуальности процессов, времени безотказной работы и пульса.
</details>
<details>
<summary><b>📋 20. «Мне нужно проверяемое выполнение инструмента MCP» </b></summary>
Когда инструменты изменяют конфигурацию или запускают действия операционной системы, командам необходима судебно-медицинская отслеживаемость.
**Как OmniRoute решает эту проблему:**
- Ведение журнала аудита на основе SQLite для вызовов инструментов MCP.
- Фильтры по инструменту, успеху/неуспеху, ключу API и нумерации страниц.
- Таблица аудита панели мониторинга + конечные точки статистики для автоматизации
</details>
<details>
<summary><b>🔐 21. «Мне нужны ограниченные разрешения MCP для каждой интеграции» </b></summary>
Разные клиенты должны иметь минимальный доступ к категориям инструментов.
**Как OmniRoute решает эту проблему:**
- 9 детальных областей MCP для контролируемого доступа к инструментам
- Обеспечение соблюдения границ и видимость в пользовательском интерфейсе управления MCP.
- Безопасное положение по умолчанию для рабочих инструментов.
</details>
<details>
<summary><b>⚙️ 22. «Мне нужен оперативный контроль без передислокации» </b></summary>
Командам необходимы быстрые изменения во время выполнения во время инцидентов или событий, связанных с затратами.
**Как OmniRoute решает эту проблему:**
- Переключение комбо-активации прямо с панели управления MCP.
- Применение профилей устойчивости из предварительно определенных пакетов политик.
- Сброс состояния автоматического выключателя с той же панели управления.
</details>
<details>
<summary><b>🔄 23. «Мне нужна оперативная видимость и отмена жизненного цикла задачи A2A» </b></summary>
Без прозрачности жизненного цикла инциденты с задачами становится трудно сортировать.
**Как OmniRoute решает эту проблему:**
- Список задач/фильтрация по состоянию/навыку с нумерацией страниц
- Детализация метаданных задачи, событий и артефактов.
- Конечная точка отмены задачи и действие пользовательского интерфейса с подтверждением.
</details>
<details>
<summary><b>🌊 24. «Мне нужны метрики активного потока для загрузки A2A» </b></summary>
Рабочие процессы потоковой передачи требуют оперативного понимания параллелизма и живых соединений.
**Как OmniRoute решает эту проблему:**
- Счетчики активных потоков интегрированы в статус A2A
- Временная метка последней задачи и количество состояний
- Карты информационной панели A2A для мониторинга операций в реальном времени.
</details>
<details>
<summary><b>🪪 25. «Мне нужно стандартное обнаружение агента для клиентов» </b></summary>
Внешним клиентам и оркестраторам для адаптации необходимы машиночитаемые метаданные.
**Как OmniRoute решает эту проблему:**
- Карта агента открыта по адресу `/.well-known/agent.json`.
- Возможности и навыки, отображаемые в пользовательском интерфейсе управления.
- API статуса A2A включает метаданные обнаружения для автоматизации.
</details>
<details>
<summary><b>🧭 26. «Мне нужна возможность обнаружения протокола в UX продукта» </b></summary>
Если пользователи не могут обнаружить поверхности протокола, качество внедрения и поддержки снижается.
**Как OmniRoute решает эту проблему:**
- Записи на боковой панели для MCP и A2A.
- Вкладка «Протоколы» на странице конечной точки с быстрым запуском и статусом.
- Ссылки из обзора на специальные панели управления.
</details>
<details>
<summary><b>🧪 27. «Мне нужна сквозная проверка протокола с реальными клиентами» </b></summary>
Пробных тестов недостаточно для проверки совместимости протокола перед выпуском.
**Как OmniRoute решает эту проблему:**
- Пакет E2E, который загружает приложение и использует настоящий клиентский транспорт MCP SDK.
- Клиент A2A тестирует потоки обнаружения, отправки, потоковой передачи, получения и отмены.
- Перекрестная проверка утверждений с помощью API-интерфейсов аудита MCP и задач A2A.
</details>
<details>
<summary><b>📡 28. «Мне нужна унифицированная наблюдаемость на всех интерфейсах» </b></summary>
Разделение наблюдаемости по протоколам создает «слепые зоны» и увеличивает MTTR.
**Как OmniRoute решает эту проблему:**
- Унифицированные дашборды/логи/аналитика в одном продукте
- Здоровье + аудит + телеметрия запросов на уровнях OpenAI, MCP и A2A.
- Операционные API для статуса и автоматизации
</details>
<details>
<summary><b>💼 29. «Мне нужна одна среда выполнения для прокси + инструментов + оркестровки агентов» </b></summary>
Запуск множества отдельных служб увеличивает эксплуатационные расходы и количество видов сбоев.
**Как OmniRoute решает эту проблему:**
- OpenAI-совместимый прокси, сервер MCP и сервер A2A в одном стеке
- Общая аутентификация, устойчивость, хранилище данных и наблюдаемость.
- Согласованная модель политики на всех поверхностях взаимодействия.
</details>
<details>
<summary><b>🚀 30. «Мне нужно реализовать агентские рабочие процессы без разрастания связующего кода» </b></summary>
Команды теряют скорость при объединении нескольких специальных сервисов и сценариев.
**Как OmniRoute решает эту проблему:**
- Единая стратегия конечных точек для клиентов и агентов
- Встроенные пользовательские интерфейсы управления протоколами и пути проверки дыма.
- Готовые к работе основы (безопасность, ведение журналов, отказоустойчивость, резервное копирование)
</details>
### Примеры сборников сценариев (интегрированные варианты использования)
**Пособие А: максимальное использование платной подписки + дешевое резервное копирование**
```txt
Combo: "maximize-claude"
1. cc/claude-opus-4-6
2. glm/glm-4.7
3. if/kimi-k2-thinking
Monthly cost: $20 + small backup spend
Outcome: higher quality, near-zero interruption
```
**Пособие Б: стек кодирования с нулевой стоимостью**
```txt
Combo: "free-forever"
1. gc/gemini-3-flash
2. if/kimi-k2-thinking
3. qw/qwen3-coder-plus
Monthly cost: $0
Outcome: stable free coding workflow
```
**Пособие C: Всегда работающая резервная цепочка 24 часа в сутки, 7 дней в неделю**
```txt
Combo: "always-on"
1. cc/claude-opus-4-6
2. cx/gpt-5.2-codex
3. glm/glm-4.7
4. minimax/MiniMax-M2.1
5. if/kimi-k2-thinking
Outcome: deep fallback depth for deadline-critical workloads
```
**Сборник D: Операции агента с помощью MCP + A2A**
```txt
1) Start MCP transport (`omniroute --mcp`) for tool-driven operations
2) Run A2A tasks via `message/send` and `message/stream`
3) Observe via /dashboard/mcp and /dashboard/a2a
4) Control incidents with resilience profile + task cancellation
```
---
## ⚡ Быстрый старт
**1. Установите глобально:**
@@ -247,6 +779,30 @@ docker compose --profile cli up -d
---
---
## 🖥️
> 🆕 **НОВИНКА!** OmniRoute теперь доступен как **нативное настольное приложение** для Windows, macOS и Linux.
- 🖥️ **Нативное Окно** — Выделенное окно с интеграцией в системный трей
- 🔄 **Автозапуск** — Запуск OmniRoute при входе в систему
- 🔔 **Нативные Уведомления** — Оповещения об исчерпании квоты
-**Установка в Один Клик** — NSIS (Windows), DMG (macOS), AppImage (Linux)
- 🌐 **Оффлайн Режим** — Полностью работает без интернета
```bash
npm run electron:dev # Режим разработки
npm run electron:build # Текущая платформа
npm run electron:build:win # Windows (.exe)
npm run electron:build:mac # macOS (.dmg)
npm run electron:build:linux # Linux (.AppImage)
```
📖 Полная документация: [`electron/README.md`](electron/README.md)
---
## 💰 Обзор цен
| Tier | Провайдер | Стоимость | Сброс квоты | Лучше всего для |
@@ -272,67 +828,6 @@ docker compose --profile cli up -d
---
## 🎯 Сценарии использования
### Сценарий 1: «У меня подписка Claude Pro»
**Проблема:** Квота истекает неиспользованной, лимиты скорости во время интенсивного программирования
```
Combo: "maximize-claude"
1. cc/claude-opus-4-6 (используйте подписку полностью)
2. glm/glm-4.7 (дешёвый бэкап при исчерпании квоты)
3. if/kimi-k2-thinking (бесплатный аварийный fallback)
Месячная стоимость: $20 (подписка) + ~$5 (бэкап) = $25 итого
vs. $20 + упирание в лимиты = разочарование
```
### Сценарий 2: «Хочу нулевую стоимость»
**Проблема:** Не может позволить подписки, нужен надёжный AI для программирования
```
Combo: "free-forever"
1. gc/gemini-3-flash (180K бесплатно/мес)
2. if/kimi-k2-thinking (неограниченно бесплатно)
3. qw/qwen3-coder-plus (неограниченно бесплатно)
Месячная стоимость: $0
Качество: Модели готовые к продакшену
```
### Сценарий 3: «Мне нужно программировать 24/7, без перерывов»
**Проблема:** Дедлайны, не может позволить простой
```
Combo: "always-on"
1. cc/claude-opus-4-6 (лучшее качество)
2. cx/gpt-5.2-codex (вторая подписка)
3. glm/glm-4.7 (дешёвый, ежедневный сброс)
4. minimax/MiniMax-M2.1 (самый дешёвый, сброс 5ч)
5. if/kimi-k2-thinking (бесплатно неограниченно)
Результат: 5 уровней fallback = нулевой простой
```
### Сценарий 4: «Хочу БЕСПЛАТНЫЙ AI в OpenClaw»
**Проблема:** Нужен AI-ассистент в мессенджерах, полностью бесплатно
```
Combo: "openclaw-free"
1. if/glm-4.7 (неограниченно бесплатно)
2. if/minimax-m2.1 (неограниченно бесплатно)
3. if/kimi-k2-thinking (неограниченно бесплатно)
Месячная стоимость: $0
Доступ через: WhatsApp, Telegram, Slack, Discord, iMessage, Signal...
```
---
## 💡 Основные функции
### 🧠 Маршрутизация и интеллект
@@ -348,6 +843,8 @@ Combo: "openclaw-free"
| 🧩 **Пользовательские модели** | Добавьте любой ID модели к любому провайдеру |
| 🌐 **Wildcard-маршрутизатор** | Маршрутизируйте паттерны `provider/*` к любому провайдеру динамически |
| 🧠 **Бюджет рассуждений** | Режимы passthrough, auto, custom и adaptive для моделей рассуждений |
| 🔀 **Model Aliases** | Auto-forward deprecated model IDs to current replacements (built-in + custom) |
| ⚡ **Background Degradation** | Auto-route background tasks (titles, summaries) to cheaper models |
| 💬 **Инъекция System Prompt** | Глобальный system prompt для всех запросов |
| 📄 **API Responses** | Полная поддержка OpenAI Responses API (`/v1/responses`) для Codex |
@@ -364,15 +861,18 @@ Combo: "openclaw-free"
### 🛡️ Устойчивость и безопасность
| Функция | Что делает |
| -------------------------------- | -------------------------------------------------------------- |
| 🔌 **Circuit Breaker** | Авто-открытие/закрытие по провайдеру с настраиваемыми порогами |
| 🛡️ **Anti-Thundering Herd** | Mutex + семафор для API key провайдеров |
| 🧠 **Семантический кеш** | Двухуровневый кеш (сигнатура + семантика) снижает стоимость |
| **Идемпотентность запросов** | 5с окно дедупликации для дублирующихся запросов |
| 🔒 **Спуфинг TLS Fingerprint** | Обход обнаружения ботов через wreq-js |
| 🌐 **Фильтрация IP** | Allowlist/blocklist для контроля доступа к API |
| 📊 **Настраиваемые Rate Limits** | Настраиваемые RPM, минимальный интервал, макс. конкуррентность |
| Функция | Что делает |
| -------------------------------- | ---------------------------------------------------------------------------- |
| 🔌 **Circuit Breaker** | Авто-открытие/закрытие по провайдеру с настраиваемыми порогами |
| 🎯 **Endpoint-Aware Models** | Custom models declare supported endpoints + API format |
| 🛡️ **Anti-Thundering Herd** | Mutex + семафор для API key провайдеров |
| 🧠 **Семантический кеш** | Двухуровневый кеш (сигнатура + семантика) снижает стоимость |
| **Идемпотентность запросов** | 5с окно дедупликации для дублирующихся запросов |
| 🔒 **Спуфинг TLS Fingerprint** | Обход обнаружения ботов через wreq-js |
| 🌐 **Фильтрация IP** | Allowlist/blocklist для контроля доступа к API |
| 📊 **Настраиваемые Rate Limits** | Настраиваемые RPM, минимальный интервал, макс. конкуррентность |
| 💾 **Rate Limit Persistence** | Learned limits survive restarts via SQLite with 60s debounce + 24h staleness |
| 🔄 **Token Refresh Resilience** | Per-provider circuit breaker (5 fails→30min) + 30s timeout per attempt |
### 📊 Наблюдаемость и аналитика
@@ -472,6 +972,27 @@ Combo: "my-coding-stack"
</details>
## 🧪 Оценки (Evals)
OmniRoute включает встроенный фреймворк оценки для тестирования качества ответов LLM по golden set. Доступ через **Analytics → Evals** в dashboard.
### Встроенный Set
Предзагруженный «OmniRoute Golden Set» содержит 10 тестов:
- Приветствия, математика, география, генерация кода
- Соответствие формату JSON, перевод, markdown
- Отказ от небезопасного контента, подсчёт, булева логика
### Стратегии оценки
| Стратегия | Описание | Пример |
| ---------- | ----------------------------------------------------- | -------------------------------- |
| `exact` | Вывод должен совпадать точно | `"4"` |
| `contains` | Вывод должен содержать подстроку (без учёта регистра) | `"Paris"` |
| `regex` | Вывод должен соответствовать regex-паттерну | `"1.*2.*3"` |
| `custom` | Пользовательская JS-функция возвращает true/false | `(output) => output.length > 10` |
---
## 📖 Руководство по настройке
@@ -754,97 +1275,6 @@ Dashboard → CLI Tools → OpenClaw → Выбрать модель → При
---
## 📊 Доступные модели
<details>
<summary><b>Посмотреть все доступные модели</b></summary>
**Claude Code (`cc/`)** - Pro/Max:
- `cc/claude-opus-4-6`
- `cc/claude-sonnet-4-5-20250929`
- `cc/claude-haiku-4-5-20251001`
**Codex (`cx/`)** - Plus/Pro:
- `cx/gpt-5.2-codex`
- `cx/gpt-5.1-codex-max`
**Gemini CLI (`gc/`)** - БЕСПЛАТНО:
- `gc/gemini-3-flash-preview`
- `gc/gemini-2.5-pro`
**GitHub Copilot (`gh/`)**:
- `gh/gpt-5`
- `gh/claude-4.5-sonnet`
**NVIDIA NIM (`nvidia/`)** - БЕСПЛАТНЫЕ кредиты:
- `nvidia/llama-3.3-70b-instruct`
- `nvidia/mistral-7b-instruct`
- 50+ моделей на [build.nvidia.com](https://build.nvidia.com)
**GLM (`glm/`)** - $0.6/1M:
- `glm/glm-4.7`
**MiniMax (`minimax/`)** - $0.2/1M:
- `minimax/MiniMax-M2.1`
**iFlow (`if/`)** - БЕСПЛАТНО:
- `if/kimi-k2-thinking`
- `if/qwen3-coder-plus`
- `if/deepseek-r1`
- `if/glm-4.7`
- `if/minimax-m2`
**Qwen (`qw/`)** - БЕСПЛАТНО:
- `qw/qwen3-coder-plus`
- `qw/qwen3-coder-flash`
**Kiro (`kr/`)** - БЕСПЛАТНО:
- `kr/claude-sonnet-4.5`
- `kr/claude-haiku-4.5`
**OpenRouter (`or/`)** - 100+ моделей:
- `or/anthropic/claude-4-sonnet`
- `or/google/gemini-2.5-pro`
- Любая модель с [openrouter.ai/models](https://openrouter.ai/models)
</details>
---
## 🧪 Оценки (Evals)
OmniRoute включает встроенный фреймворк оценки для тестирования качества ответов LLM по golden set. Доступ через **Analytics → Evals** в dashboard.
### Встроенный Golden Set
Предзагруженный «OmniRoute Golden Set» содержит 10 тестов:
- Приветствия, математика, география, генерация кода
- Соответствие формату JSON, перевод, markdown
- Отказ от небезопасного контента, подсчёт, булева логика
### Стратегии оценки
| Стратегия | Описание | Пример |
| ---------- | ----------------------------------------------------- | -------------------------------- |
| `exact` | Вывод должен совпадать точно | `"4"` |
| `contains` | Вывод должен содержать подстроку (без учёта регистра) | `"Paris"` |
| `regex` | Вывод должен соответствовать regex-паттерну | `"1.*2.*3"` |
| `custom` | Пользовательская JS-функция возвращает true/false | `(output) => output.length > 10` |
---
## 🐛 Устранение неполадок
<details>
@@ -900,7 +1330,7 @@ OmniRoute включает встроенный фреймворк оценки
---
## 🛠️ Технологический стек
## 🛠️
- **Runtime**: Node.js 20+
- **Язык**: TypeScript 5.9 — **100% TypeScript** в `src/` и `open-sse/` (v1.0.6)
@@ -931,17 +1361,7 @@ OmniRoute включает встроенный фреймворк оценки
---
## 📧 Поддержка
> 💬 **Присоединяйтесь к сообществу!** [Группа WhatsApp](https://chat.whatsapp.com/JI7cDQ1GyaiDHhVBpLxf8b?mode=gi_t) — Получайте помощь, делитесь советами и оставайтесь в курсе.
- **Сайт**: [omniroute.online](https://omniroute.online)
- **GitHub**: [github.com/diegosouzapw/OmniRoute](https://github.com/diegosouzapw/OmniRoute)
- **Issues**: [github.com/diegosouzapw/OmniRoute/issues](https://github.com/diegosouzapw/OmniRoute/issues)
- **WhatsApp**: [Группа сообщества](https://chat.whatsapp.com/JI7cDQ1GyaiDHhVBpLxf8b?mode=gi_t)
- **Оригинальный проект**: [9router от decolua](https://github.com/decolua/9router)
---
## 🗺️
## 👥 Участники

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -110,6 +110,35 @@ _通过 OmniRoute 连接任何 AI 驱动的 IDE 或 CLI 工具 — 免费 API
---
## 🖼️
<div align="center">
<img src="./docs/screenshots/MainOmniRoute.png" alt="OmniRoute" width="800"/>
</div>
---
## 📸
<details>
<summary><b>...</b></summary>
| # | # |
| ----- | ---------------------------------------- |
| **1** | ![1](docs/screenshots/01-providers.png) |
| **2** | ![2](docs/screenshots/02-combos.png) |
| **3** | ![3](docs/screenshots/03-analytics.png) |
| **4** | ![4](docs/screenshots/04-health.png) |
| **5** | ![5](docs/screenshots/05-translator.png) |
| **6** | ![6](docs/screenshots/06-settings.png) |
| **7** | ![7](docs/screenshots/07-cli-tools.png) |
| **8** | ![8](docs/screenshots/08-usage.png) |
| **9** | ![9](docs/screenshots/09-endpoint.png) |
</details>
---
## 🤔 为什么选择 OmniRoute
**停止浪费金钱和遭遇限制:**
@@ -128,6 +157,18 @@ _通过 OmniRoute 连接任何 AI 驱动的 IDE 或 CLI 工具 — 免费 API
---
## 📧 支持
> 💬 **加入我们的社区!** [WhatsApp 群组](https://chat.whatsapp.com/JI7cDQ1GyaiDHhVBpLxf8b?mode=gi_t) — 获取帮助、分享技巧、了解最新动态。
- **网站**: [omniroute.online](https://omniroute.online)
- **GitHub**: [github.com/diegosouzapw/OmniRoute](https://github.com/diegosouzapw/OmniRoute)
- **Issues**: [github.com/diegosouzapw/OmniRoute/issues](https://github.com/diegosouzapw/OmniRoute/issues)
- **WhatsApp**: [社区群组](https://chat.whatsapp.com/JI7cDQ1GyaiDHhVBpLxf8b?mode=gi_t)
- **原始项目**: [decolua 的 9router](https://github.com/decolua/9router)
---
## 🔄 工作原理
```
@@ -157,6 +198,497 @@ _通过 OmniRoute 连接任何 AI 驱动的 IDE 或 CLI 工具 — 免费 API
---
## 🎯 OmniRoute 解决了什么 — 30 个真正的痛点和用例
> **每个使用 AI 工具的开发人员每天都会面临这些问题。** OmniRoute 的构建是为了解决所有这些问题 - 从成本超支到区域封锁,从损坏的 OAuth 流程到协议操作和企业可观察性。
<details>
<summary><b>💸 1.“我支付了昂贵的订阅费用,但仍然受到限制的干扰”</b></summary>
开发人员每月为 Claude Pro、Codex Pro 或 GitHub Copilot 支付 20-200 美元。即使付费配额也有上限——5 小时的使用时间、每周限制或每分钟的费率限制。在编码会话中,提供商停止响应,开发人员失去流量和生产力。
**OmniRoute 如何解决:**
- **智能 4 层回退** — 如果订阅配额用完,自动重定向到 API 密钥 → 便宜 → 免费,零手动干预
- **实时配额跟踪** — 实时显示代币消耗情况并重置倒计时5 小时、每日、每周)
- **多帐户支持** — 每个提供商有多个帐户,具有自动循环 — 当一个帐户用完时,切换到下一个帐户
- **自定义组合** — 可定制的后备链,具有 6 种平衡策略先填充、循环、P2C、随机、最少使用、成本优化
- **Codex Business Quotas** — 直接在仪表板中监控业务/团队工作空间配额
</details>
<details>
<summary><b>🔌 2.“我需要使用多个提供程序,但每个提供程序都有不同的 API”</b></summary>
OpenAI 使用一种格式ClaudeAnthropic使用另一种格式Gemini 使用另一种格式。如果开发人员想要测试来自不同提供商的模型或在它们之间进行回退,他们需要重新配置 SDK、更改端点、处理不兼容的格式。自定义提供程序FriendLI、NIM具有非标准模型端点。
**OmniRoute 如何解决:**
- **统一端点** — 单个 `http://localhost:20128/v1` 充当所有 36 个以上提供商的代理
- **格式翻译** — 自动且透明OpenAI ↔ Claude ↔ Gemini ↔ Responses API
- **响应清理** — 删除破坏 OpenAI SDK v1.83+ 的非标准字段(`x_groq``usage_breakdown``service_tier`
- **角色标准化** — 对于非 OpenAI 提供商,将 `developer``system` 转换; `system``user` 适用于 GLM/ERNIE
- **Think Tag Extraction** — 将 DeepSeek R1 等模型中的 `<think>` 块提取为标准化 `reasoning_content`
- **Gemini 的结构化输出** — `json_schema``responseMimeType`/`responseSchema` 自动转换
- **`stream` 默认为 `false`** — 与 OpenAI 规范保持一致,避免 Python/Rust/Go SDK 中出现意外的 SSE
</details>
<details>
<summary><b>🌐 3.“我的人工智能提供商封锁了我的地区/国家”</b></summary>
OpenAI/Codex 等提供商会阻止来自某些地理区域的访问。用户在 OAuth 和 API 连接期间收到类似 `unsupported_country_region_territory` 的错误。这对于发展中国家的开发商来说尤其令人沮丧。
**OmniRoute 如何解决:**
- **3 级代理配置** — 3 级可配置代理:全局(所有流量)、每个提供商(仅一个提供商)和每个连接/密钥
- **颜色编码的代理徽章** — 视觉指示器:🟢 全局代理、🟡 提供商代理、🔵 连接代理,始终显示 IP
- **通过代理进行 OAuth 令牌交换** — OAuth 流程也通过代理,解决了 `unsupported_country_region_territory`
- **通过代理进行连接测试** — 连接测试使用配置的代理(不再直接绕过)
- **SOCKS5 支持** — 对出站路由的完整 SOCKS5 代理支持
- **TLS 指纹欺骗** — 通过 `wreq-js` 的类似浏览器的 TLS 指纹来绕过机器人检测
</details>
<details>
<summary><b>🆓 4.“我想用AI编码但我没有钱”</b></summary>
并不是每个人都能每月支付 20-200 美元来订阅 AI。来自新兴国家的学生、开发人员、业余爱好者和自由职业者需要以零成本获得优质模型。
**OmniRoute 如何解决:**
- **内置免费层级提供商** — 对 100% 免费提供商的本机支持iFlow8 个无限型号、Qwen3 个无限型号、KiroClaude 免费、Gemini CLI180K/月免费)
- **仅限免费组合** — 链 `gc/gemini-3-flash → if/kimi-k2-thinking → qw/qwen3-coder-plus` = 0 美元/月,零停机时间
- **NVIDIA NIM 免费积分** — 集成 1000 个免费积分
- **成本优化策略** — 自动选择最便宜的可用提供商的路由策略
</details>
<details>
<summary><b>🔒 5.“我需要保护我的AI网关免遭未经授权的访问”</b></summary>
当将人工智能网关暴露到网络LAN、VPS、Docker任何拥有该地址的人都可以消耗开发者的代币/配额。如果没有保护API 很容易被误用、提示注入和滥用。
**OmniRoute 如何解决:**
- **API 密钥管理** — 使用专用的 `/dashboard/api-manager` 页面为每个提供商生成、轮换和范围界定
- **模型级权限** — 将 API 密钥限制为特定模型(`openai/*`、通配符模式),并具有“允许全部”/“限制”切换功能
- **API 端点保护** — 需要 `/v1/models` 的密钥并阻止列表中的特定提供商
- **Auth Guard + CSRF 保护** — 所有仪表板路由均受 `withAuth` 中间件 + CSRF 令牌保护
- **速率限制器** — 通过可配置窗口限制每个 IP 的速率
- **IP 过滤** — 用于访问控制的允许列表/阻止列表
- **Prompt Injection Guard** — 针对恶意提示模式的清理
- **AES-256-GCM 加密** — 静态加密的凭证
</details>
<details>
<summary><b>🛑 6.“我的提供商宕机了,我失去了编码流程”</b></summary>
AI 提供商可能会变得不稳定、返回 5xx 错误或达到临时速率限制。如果开发人员依赖于单一提供商,他们就会受到干扰。如果没有断路器,重复重试可能会使应用程序崩溃。
**OmniRoute 如何解决:**
- **每个提供商的断路器** — 自动打开/关闭,具有可配置的阈值和冷却时间(关闭/打开/半打开)
- **指数退避** — 渐进式重试延迟
- **Anti-Thundering Herd** — 互斥锁 + 信号量保护,防止并发重试风暴
- **组合后备链** — 如果主要提供商发生故障,则自动从该链中掉下来,无需干预
- **组合断路器** — 自动禁用组合链中出现故障的提供商
- **运行状况仪表板** — 正常运行时间监控、断路器状态、锁定、缓存统计、p50/p95/p99 延迟
</details>
<details>
<summary><b>🔧 7. “配置每个AI工具都是繁琐且重复的”</b></summary>
开发人员使用 Cursor、Claude Code、Codex CLI、OpenClaw、Gemini CLI、Kilo Code...每个工具都需要不同的配置API 端点、密钥、模型)。切换提供商或模型时重新配置是浪费时间。
**OmniRoute 如何解决:**
- **CLI 工具仪表板** — 专用页面,可一键设置 Claude Code、Codex CLI、OpenClaw、Kilo Code、Antigravity、Cline
- **GitHub Copilot 配置生成器** — 通过批量模型选择为 VS Code 生成 `chatLanguageModels.json`
- **入门向导** — 为首次使用的用户提供 4 步设置指导
- **一个端点,所有型号** — 配置 `http://localhost:20128/v1` 一次,访问 36 个以上提供商
</details>
<details>
<summary><b>🔑 8.“管理来自多个提供商的 OAuth 令牌简直就是地狱”</b></summary>
Claude Code、Codex、Gemini CLI、Copilot — 全部使用带有过期令牌的 OAuth 2.0。开发人员需要不断地重新进行身份验证,处理`client_secret is missing``redirect_uri_mismatch`以及远程服务器上的故障。 LAN/VPS 上的 OAuth 问题尤其严重。
**OmniRoute 如何解决:**
- **自动令牌刷新** — OAuth 令牌在过期前在后台刷新
- **OAuth 2.0 (PKCE) 内置** — Claude Code、Codex、Gemini CLI、Copilot、Kiro、Qwen、iFlow 的自动流程
- **多帐户 OAuth** — 每个提供商通过 JWT/ID 令牌提取多个帐户
- **OAuth LAN/远程修复** — `redirect_uri` 的私有 IP 检测 + 远程服务器的手动 URL 模式
- **Nginx 背后的 OAuth** — 使用 `window.location.origin` 实现反向代理兼容性
- **远程 OAuth 指南** — VPS/Docker 上的 Google Cloud 凭据分步指南
</details>
<details>
<summary><b>📊 9.“我不知道我花了多少钱或在哪里”</b></summary>
开发商使用多个付费提供商,但对支出没有统一的看法。每个提供商都有自己的计费仪表板,但没有统一的视图。意外的成本可能会不断增加。
**OmniRoute 如何解决:**
- **成本分析仪表板** — 每个提供商的每个代币成本跟踪和预算管理
- **每层预算限制** — 触发自动回退的每层支出上限
- **按型号定价配置** — 每个型号的可配置价格
- **每个 API 密钥的使用统计信息** — 每个密钥的请求计数和上次使用的时间戳
- **分析仪表板** — 统计卡、模型使用图表、包含成功率和延迟的提供商表
</details>
<details>
<summary><b>🐛 10.“我无法诊断人工智能调用中的错误和问题”</b></summary>
当调用失败时,开发人员不知道这是否是速率限制、令牌过期、格式错误或提供商错误。跨不同终端的碎片日志。如果没有可观察性,调试就是反复试验。
**OmniRoute 如何解决:**
- **统一日志仪表板** — 4 个选项卡:请求日志、代理日志、审核日志、控制台
- **控制台日志查看器** — 实时终端式查看器,具有颜色编码级别、自动滚动、搜索、过滤功能
- **SQLite 代理日志** — 服务器重新启动后仍保留的持久日志
- **Translator Playground** — 4 种调试模式Playground格式翻译、Chat Tester往返、Test Bench批量、Live Monitor实时
- **请求遥测** — p50/p95/p99 延迟 + X-Request-Id 跟踪
- **基于文件的日志记录与旋转** — 控制台拦截器通过基于大小的旋转将所有内容捕获到 JSON 日志
</details>
<details>
<summary><b>🏗️ 11.“部署和维护网关很复杂”</b></summary>
跨不同环境本地、VPS、Docker、云安装、配置和维护 AI 代理是一项劳动密集型工作。硬编码路径、目录上的 `EACCES`、端口冲突和跨平台构建等问题会增加摩擦。
**OmniRoute 如何解决:**
- **npm 全局安装** — `npm install -g omniroute && omniroute` — 完成
- **Docker 多平台** — AMD64 + ARM64 本机Apple Silicon、AWS Graviton、Raspberry Pi
- **Docker Compose Profiles** — `base`(无 CLI 工具)和 `cli`(带有 Claude Code、Codex、OpenClaw
- **Electron 桌面应用程序** — 适用于 Windows/macOS/Linux 的本机应用程序,带系统托盘、自动启动、离线模式
- **分割端口模式** — API 和仪表板位于单独的端口上,适用于高级场景(反向代理、容器网络)
- **云同步** — 通过 Cloudflare Workers 跨设备配置同步
- **数据库备份** — 自动备份、恢复、导出和导入所有设置
</details>
<details>
<summary><b>🌍 12.“界面只有英文,我的团队不会说英语”</b></summary>
非英语国家的团队,尤其是拉丁美洲、亚洲和欧洲的团队,在纯英文界面上遇到了困难。语言障碍会降低采用率并增加配置错误。
**OmniRoute 如何解决:**
- **仪表板 i18n — 30 种语言** — 所有 500 多个按键已翻译包括阿拉伯语、保加利亚语、丹麦语、德语、西班牙语、芬兰语、法语、希伯来语、印地语、匈牙利语、印度尼西亚语、意大利语、日语、韩语、马来语、荷兰语、挪威语、波兰语、葡萄牙语PT/BR、罗马尼亚语、俄语、斯洛伐克语、瑞典语、泰语、乌克兰语、越南语、中文、菲律宾语、英语
- **RTL 支持** — 从右到左支持阿拉伯语和希伯来语
- **多语言自述文件** — 30 个完整的文档翻译
- **语言选择器** — 标题中的地球图标用于实时切换
</details>
<details>
<summary><b>🔄 13.“我需要的不仅仅是聊天 - 我需要嵌入、图像、音频”</b></summary>
人工智能不仅仅是完成聊天。开发人员需要生成图像、转录音频、为 RAG 创建嵌入、重新排列文档以及审核内容。每个 API 都有不同的端点和格式。
**OmniRoute 如何解决:**
- **嵌入** — `/v1/embeddings` 具有 6 个提供商和 9 个以上模型
- **图像生成** — `/v1/images/generations` 具有 10 个提供商和 20 多个模型OpenAI、xAI、Together、Fireworks、Nebius、Hyperbolic、NanoBanana、Antigravity、SD WebUI、ComfyUI
- **文本到视频** — `/v1/videos/generations` — ComfyUIAnimateDiff、SVD和 SD WebUI
- **文本转音乐** — `/v1/music/generations` — ComfyUI稳定音频打开MusicGen
- **音频转录** — `/v1/audio/transcriptions` — Whisper + Nvidia NIM、HuggingFace、Qwen3
- **文本转语音** — `/v1/audio/speech` — ElevenLabs、Nvidia NIM、HuggingFace、Coqui、Tortoise、Qwen3 以及现有提供商
- **审核** — `/v1/moderations` — 内容安全检查
- **重新排名** — `/v1/rerank` — 文档相关性重新排名
- **响应 API** — 对 Codex 的完整 `/v1/responses` 支持
</details>
<details>
<summary><b>🧪 14.“我无法测试和比较不同型号的质量”</b></summary>
开发人员想知道哪种模型最适合他们的用例(代码、翻译、推理),但手动比较速度很慢。不存在集成的评估工具。
**OmniRoute 如何解决:**
- **LLM 评估** — 黄金套装测试,包含 10 个预加载案例涵盖问候语、数学、地理、代码生成、JSON 合规性、翻译、降价、安全拒绝
- **4种匹配策略** — `exact``contains``regex``custom`JS函数
- **Translator Playground 测试台** — 使用多个输入和预期输出进行批量测试、跨提供商比较
- **聊天测试器** — 带有视觉响应渲染的完整往返
- **实时监控** — 流经代理的所有请求的实时流
</details>
<details>
<summary><b>📈 15.“我需要在不损失性能的情况下进行扩展”</b></summary>
随着请求量的增长,如果不缓存相同的问题,就会产生重复的成本。如果没有幂等性,重复的请求就会浪费处理。必须遵守每个提供商的速率限制。
**OmniRoute 如何解决:**
- **语义缓存** - 两层缓存(签名+语义)降低成本和延迟
- **请求幂等性** — 相同请求的 5 秒重复数据删除窗口
- **速率限制检测** — 每个提供商的 RPM、最小间隙和最大并发跟踪
- **可编辑的速率限制** — 可在“设置”→“持久弹性”中配置默认值
- **API 密钥验证缓存** — 用于提高生产性能的 3 层缓存
- **带有遥测功能的运行状况仪表板** — p50/p95/p99 延迟、缓存统计数据、正常运行时间
</details>
<details>
<summary><b>🤖 16.“我想全局控制模型行为”</b></summary>
希望所有响应都以特定语言、特定语气或想要限制推理标记的开发人员。在每个工具/请求中配置此功能是不切实际的。
**OmniRoute 如何解决:**
- **系统提示注入** — 全局提示应用于所有请求
- **思考预算验证** - 每个请求的推理令牌分配控制(直通、自动、自定义、自适应)
- **6 种路由策略** — 确定如何分发请求的全局策略
- **通配符路由器** — `provider/*` 模式动态路由到任何提供商
- **组合启用/禁用切换** — 直接从仪表板切换组合
- **提供商切换** — 一键启用/禁用提供商的所有连接
- **阻止的提供商** — 从 `/v1/models` 列表中排除特定提供商
</details>
<details>
<summary><b>🧰 17.“我需要MCP工具作为一流的产品能力”</b></summary>
许多 AI 网关仅将 MCP 作为隐藏的实现细节公开。团队需要一个可见的、可管理的操作层。
**OmniRoute 如何解决:**
- MCP 显示在仪表板导航和端点协议选项卡中
- 专用 MCP 管理页面,包含流程、工具、范围和审计
- `omniroute --mcp` 和客户入门的内置快速启动
</details>
<details>
<summary><b>🧠 18.“我需要具有同步+流任务路径的 A2A 编排”</b></summary>
代理工作流程需要直接回复和具有生命周期控制的长时间运行的流式执行。
**OmniRoute 如何解决:**
- A2A JSON-RPC 端点 (`POST /a2a`) 与 `message/send``message/stream`
- 具有终端状态传播的 SSE 流式传输
- `tasks/get``tasks/cancel` 的任务生命周期 API
</details>
<details>
<summary><b>🛰️ 19.“我需要真实的 MCP 进程运行状况,而不是猜测的状态”</b></summary>
运营团队需要知道 MCP 是否确实存在,而不仅仅是 API 是否可访问。
**OmniRoute 如何解决:**
- 带有 PID、时间戳、传输、工具计数和范围模式的运行时心跳文件
- MCP状态API结合心跳+最近的活动
- 用于流程/正常运行时间/心跳新鲜度的 UI 状态卡
</details>
<details>
<summary><b>📋 20.“我需要可审核的 MCP 工具执行”</b></summary>
当工具改变配置或触发操作操作时,团队需要取证可追溯性。
**OmniRoute 如何解决:**
- SQLite 支持的 MCP 工具调用审核日志记录
- 按工具、成功/失败、API 密钥和分页过滤
- 仪表板审核表+自动化统计端点
</details>
<details>
<summary><b>🔐 21.“每次集成我都需要范围内的 MCP 权限”</b></summary>
不同的客户端应该具有对工具类别的最低权限访问权限。
**OmniRoute 如何解决:**
- 9 个粒度 MCP 范围,用于受控工具访问
- MCP 管理 UI 中的范围执行和可见性
- 操作工具的安全默认姿势
</details>
<details>
<summary><b>⚙️ 22.“我需要操作控制而不重新部署”</b></summary>
团队需要在事件或成本事件期间快速更改运行时。
**OmniRoute 如何解决:**
- 直接从 MCP 仪表板切换组合激活
- 应用预定义策略包中的弹性配置文件
- 从同一操作面板重置断路器状态
</details>
<details>
<summary><b>🔄 23.“我需要实时 A2A 任务生命周期可见性和取消”</b></summary>
如果没有生命周期可见性,任务事件就很难分类。
**OmniRoute 如何解决:**
- 任务列表/按状态/技能过滤并分页
- 深入了解任务元数据、事件和工件
- 任务取消端点和带有确认的 UI 操作
</details>
<details>
<summary><b>🌊 24.“我需要 A2A 负载的活动流指标”</b></summary>
流媒体工作流程需要对并发和实时连接的操作洞察。
**OmniRoute 如何解决:**
- 活动流计数器集成到 A2A 状态中
- 最后任务时间戳和每个状态计数
- 用于实时操作监控的 A2A 仪表板卡
</details>
<details>
<summary><b>🪪 25.“我需要为客户发现标准代理”</b></summary>
外部客户端和协调器需要机器可读的元数据来进行引导。
**OmniRoute 如何解决:**
- 特工卡暴露在`/.well-known/agent.json`
- 管理 UI 中显示的能力和技能
- A2A 状态 API 包括用于自动化的发现元数据
</details>
<details>
<summary><b>🧭 26.“我需要产品 UX 中的协议可发现性”</b></summary>
如果用户无法发现协议表面,采用和支持质量就会下降。
**OmniRoute 如何解决:**
- MCP 和 A2A 的侧边栏条目
- 端点页面“协议”选项卡包含快速启动和状态
- 从概述到专用管理仪表板的链接
</details>
<details>
<summary><b>🧪 27.“我需要与真实客户端进行端到端协议验证”</b></summary>
模拟测试不足以在发布前验证协议兼容性。
**OmniRoute 如何解决:**
- E2E 套件,可启动应用程序并使用真正的 MCP SDK 客户端传输
- A2A 客户端测试发现、发送、流式传输、获取和取消流程
- 针对 MCP 审计和 A2A 任务 API 交叉检查断言
</details>
<details>
<summary><b>📡 28.“我需要跨所有接口的统一可观察性”</b></summary>
按协议分割可观察性会产生盲点和更长的 MTTR。
**OmniRoute 如何解决:**
- 一个产品中的统一仪表板/日志/分析
- 跨 OpenAI、MCP 和 A2A 层的运行状况 + 审计 + 请求遥测
- 用于状态和自动化的操作 API
</details>
<details>
<summary><b>💼 29.“我需要一个用于代理+工具+代理编排的运行时”</b></summary>
运行许多单独的服务会增加运营成本和故障模式。
**OmniRoute 如何解决:**
- 兼容 OpenAI 的代理、MCP 服务器和 A2A 服务器位于一个堆栈中
- 共享身份验证、弹性、数据存储和可观察性
- 所有交互界面上一致的策略模型
</details>
<details>
<summary><b>🚀 30.“我需要在没有胶水代码蔓延的情况下交付代理工作流程”</b></summary>
拼接多个临时服务和脚本时,团队会失去速度。
**OmniRoute 如何解决:**
- 客户端和代理的统一端点策略
- 内置协议管理 UI 和烟雾验证路径
- 生产就绪的基础(安全性、日志记录、弹性、备份)
</details>
### 示例手册(集成用例)
**剧本 A最大化付费订阅 + 廉价备份**
```txt
Combo: "maximize-claude"
1. cc/claude-opus-4-6
2. glm/glm-4.7
3. if/kimi-k2-thinking
Monthly cost: $20 + small backup spend
Outcome: higher quality, near-zero interruption
```
**剧本 B零成本编码堆栈**
```txt
Combo: "free-forever"
1. gc/gemini-3-flash
2. if/kimi-k2-thinking
3. qw/qwen3-coder-plus
Monthly cost: $0
Outcome: stable free coding workflow
```
**剧本 C24/7 始终在线的后备链**
```txt
Combo: "always-on"
1. cc/claude-opus-4-6
2. cx/gpt-5.2-codex
3. glm/glm-4.7
4. minimax/MiniMax-M2.1
5. if/kimi-k2-thinking
Outcome: deep fallback depth for deadline-critical workloads
```
**剧本 D使用 MCP + A2A 的特工操作**
```txt
1) Start MCP transport (`omniroute --mcp`) for tool-driven operations
2) Run A2A tasks via `message/send` and `message/stream`
3) Observe via /dashboard/mcp and /dashboard/a2a
4) Control incidents with resilience profile + task cancellation
```
---
## ⚡ 快速开始
**1. 全局安装:**
@@ -247,6 +779,30 @@ docker compose --profile cli up -d
---
---
## 🖥️
> 🆕 **全新!** OmniRoute 现已提供适用于 Windows、macOS 和 Linux 的**原生桌面应用程序**。
- 🖥️ **原生窗口** — 专属应用窗口,集成系统托盘
- 🔄 **自动启动** — 系统登录时启动 OmniRoute
- 🔔 **原生通知** — 配额耗尽或提供商问题时收到提醒
-**一键安装** — NSIS (Windows)、DMG (macOS)、AppImage (Linux)
- 🌐 **离线模式** — 内置服务器,完全离线工作
```bash
npm run electron:dev # 开发模式
npm run electron:build # 当前平台
npm run electron:build:win # Windows (.exe)
npm run electron:build:mac # macOS (.dmg)
npm run electron:build:linux # Linux (.AppImage)
```
📖 完整文档:[`electron/README.md`](electron/README.md)
---
## 💰 定价概览
| 层级 | 提供商 | 费用 | 配额重置 | 最适合 |
@@ -272,84 +828,25 @@ docker compose --profile cli up -d
---
## 🎯 使用场景
### 场景 1"我有 Claude Pro 订阅"
**问题:** 配额未使用就过期,编程高峰期遇到速率限制
```
Combo: "maximize-claude"
1. cc/claude-opus-4-6 (充分使用订阅)
2. glm/glm-4.7 (配额用完时的便宜备用)
3. if/kimi-k2-thinking (免费应急后备)
每月成本:$20订阅+ ~$5备用= $25 总计
对比:$20 + 遇到限制 = 受挫
```
### 场景 2"我想要零成本"
**问题:** 无法承担订阅费用,需要可靠的 AI 编程
```
Combo: "free-forever"
1. gc/gemini-3-flash (每月 180K 免费)
2. if/kimi-k2-thinking (无限免费)
3. qw/qwen3-coder-plus (无限免费)
每月成本:$0
质量:生产级模型
```
### 场景 3"我需要 24/7 编程,不中断"
**问题:** 截止日期紧迫,不能有停机时间
```
Combo: "always-on"
1. cc/claude-opus-4-6 (最佳质量)
2. cx/gpt-5.2-codex (第二个订阅)
3. glm/glm-4.7 (便宜,每日重置)
4. minimax/MiniMax-M2.1 最便宜5小时重置
5. if/kimi-k2-thinking (免费无限制)
结果5 层故障转移 = 零停机
```
### 场景 4"我想在 OpenClaw 中使用免费 AI"
**问题:** 需要在消息应用中使用 AI 助手,完全免费
```
Combo: "openclaw-free"
1. if/glm-4.7 (无限免费)
2. if/minimax-m2.1 (无限免费)
3. if/kimi-k2-thinking (无限免费)
每月成本:$0
访问方式WhatsApp、Telegram、Slack、Discord、iMessage、Signal...
```
---
## 💡 核心功能
### 🧠 路由与智能
| 功能 | 功能描述 |
| ------------------------- | -------------------------------------------------------------------------- |
| 🎯 **智能 4 层故障转移** | 自动路由:订阅 → API Key → 低价 → 免费 |
| 📊 **实时配额追踪** | 实时 Token 计数 + 每个提供商的重置倒计时 |
| 🔄 **格式转换** | OpenAI ↔ Claude ↔ Gemini ↔ Cursor ↔ Kiro 无缝切换 |
| 👥 **多账号支持** | 每个提供商多个账号,智能选择 |
| 🔄 **自动令牌刷新** | OAuth 令牌自动刷新并重试 |
| 🎨 **自定义组合** | 6 种策略fill-first、round-robin、p2c、random、least-used、cost-optimized |
| 🧩 **自定义模型** | 为任何提供商添加任何模型 ID |
| 🌐 **通配符路由** | 动态路由 `provider/*` 模式到任何提供商 |
| 🧠 **推理预算** | passthrough、auto、custom 和 adaptive 模式用于推理模型 |
| 💬 **System Prompt 注入** | 全局 System Prompt 应用于所有请求 |
| 📄 **Responses API** | 完整支持 OpenAI Responses API (`/v1/responses`) 用于 Codex |
| 功能 | 功能描述 |
| ----------------------------- | ----------------------------------------------------------------------------- |
| 🎯 **智能 4 层故障转移** | 自动路由:订阅 → API Key → 低价 → 免费 |
| 📊 **实时配额追踪** | 实时 Token 计数 + 每个提供商的重置倒计时 |
| 🔄 **格式转换** | OpenAI ↔ Claude ↔ Gemini ↔ Cursor ↔ Kiro 无缝切换 |
| 👥 **多账号支持** | 每个提供商多个账号,智能选择 |
| 🔄 **自动令牌刷新** | OAuth 令牌自动刷新并重试 |
| 🎨 **自定义组合** | 6 种策略fill-first、round-robin、p2c、random、least-used、cost-optimized |
| 🧩 **自定义模型** | 为任何提供商添加任何模型 ID |
| 🌐 **通配符路由** | 动态路由 `provider/*` 模式到任何提供商 |
| 🧠 **推理预算** | passthrough、auto、custom 和 adaptive 模式用于推理模型 |
| 🔀 **Model Aliases** | Auto-forward deprecated model IDs to current replacements (built-in + custom) |
| **Background Degradation** | Auto-route background tasks (titles, summaries) to cheaper models |
| 💬 **System Prompt 注入** | 全局 System Prompt 应用于所有请求 |
| 📄 **Responses API** | 完整支持 OpenAI Responses API (`/v1/responses`) 用于 Codex |
### 🎵 多模态 API
@@ -364,15 +861,17 @@ Combo: "openclaw-free"
### 🛡️ 弹性与安全
| 功能 | 功能描述 |
| --------------------- | -------------------------------------- |
| 🔌 **断路器** | 每个提供商自动打开/关闭,可配置阈值 |
| 🛡️ **反惊群** | Mutex + 信号量限速用于 API Key 提供商 |
| 🧠 **语义缓存** | 两层缓存(签名 + 语义)降低成本和延迟 |
| ⚡ **请求幂等性** | 5 秒去重窗口防止重复请求 |
| 🔒 **TLS 指纹伪装** | 通过 wreq-js 绕过基于 TLS 的机器人检测 |
| 🌐 **IP 过滤** | 白名单/黑名单用于 API 访问控制 |
| 📊 **可编辑速率限制** | 可配置的 RPM、最小间隔和最大并发 |
| 功能 | 功能描述 |
| ------------------------------- | ---------------------------------------------------------------------------- |
| 🔌 **断路器** | 每个提供商自动打开/关闭,可配置阈值 |
| 🛡️ **反惊群** | Mutex + 信号量限速用于 API Key 提供商 |
| 🧠 **语义缓存** | 两层缓存(签名 + 语义)降低成本和延迟 |
| ⚡ **请求幂等性** | 5 秒去重窗口防止重复请求 |
| 🔒 **TLS 指纹伪装** | 通过 wreq-js 绕过基于 TLS 的机器人检测 |
| 🌐 **IP 过滤** | 白名单/黑名单用于 API 访问控制 |
| 📊 **可编辑速率限制** | 可配置的 RPM、最小间隔和最大并发 |
| 💾 **Rate Limit Persistence** | Learned limits survive restarts via SQLite with 60s debounce + 24h staleness |
| 🔄 **Token Refresh Resilience** | Per-provider circuit breaker (5 fails→30min) + 30s timeout per attempt |
### 📊 可观察性与分析
@@ -472,6 +971,27 @@ Combo: "my-coding-stack"
</details>
## 🧪 评估 (Evals)
OmniRoute 包含内置评估框架,用于针对黄金集测试 LLM 响应质量。通过仪表板中的 **Analytics → Evals** 访问。
### 内置黄金集
预加载的「OmniRoute Golden Set」包含 10 个测试用例:
- 问候、数学、地理、代码生成
- JSON 格式合规性、翻译、markdown
- 安全拒绝(有害内容)、计数、布尔逻辑
### 评估策略
| 策略 | 描述 | 示例 |
| ---------- | -------------------------------- | -------------------------------- |
| `exact` | 输出必须完全匹配 | `"4"` |
| `contains` | 输出必须包含子串(不区分大小写) | `"Paris"` |
| `regex` | 输出必须匹配正则表达式模式 | `"1.*2.*3"` |
| `custom` | 自定义 JS 函数返回 true/false | `(output) => output.length > 10` |
---
## 📖 设置指南
@@ -754,97 +1274,6 @@ codex "your prompt"
---
## 📊 可用模型
<details>
<summary><b>查看所有可用模型</b></summary>
**Claude Code (`cc/`)** - Pro/Max:
- `cc/claude-opus-4-6`
- `cc/claude-sonnet-4-5-20250929`
- `cc/claude-haiku-4-5-20251001`
**Codex (`cx/`)** - Plus/Pro:
- `cx/gpt-5.2-codex`
- `cx/gpt-5.1-codex-max`
**Gemini CLI (`gc/`)** - 免费:
- `gc/gemini-3-flash-preview`
- `gc/gemini-2.5-pro`
**GitHub Copilot (`gh/`)**:
- `gh/gpt-5`
- `gh/claude-4.5-sonnet`
**NVIDIA NIM (`nvidia/`)** - 免费积分:
- `nvidia/llama-3.3-70b-instruct`
- `nvidia/mistral-7b-instruct`
- 50+ 更多模型在 [build.nvidia.com](https://build.nvidia.com)
**GLM (`glm/`)** - $0.6/1M:
- `glm/glm-4.7`
**MiniMax (`minimax/`)** - $0.2/1M:
- `minimax/MiniMax-M2.1`
**iFlow (`if/`)** - 免费:
- `if/kimi-k2-thinking`
- `if/qwen3-coder-plus`
- `if/deepseek-r1`
- `if/glm-4.7`
- `if/minimax-m2`
**Qwen (`qw/`)** - 免费:
- `qw/qwen3-coder-plus`
- `qw/qwen3-coder-flash`
**Kiro (`kr/`)** - 免费:
- `kr/claude-sonnet-4.5`
- `kr/claude-haiku-4.5`
**OpenRouter (`or/`)** - 100+ 模型:
- `or/anthropic/claude-4-sonnet`
- `or/google/gemini-2.5-pro`
- [openrouter.ai/models](https://openrouter.ai/models) 上的任何模型
</details>
---
## 🧪 评估 (Evals)
OmniRoute 包含内置评估框架,用于针对黄金集测试 LLM 响应质量。通过仪表板中的 **Analytics → Evals** 访问。
### 内置黄金集
预加载的「OmniRoute Golden Set」包含 10 个测试用例:
- 问候、数学、地理、代码生成
- JSON 格式合规性、翻译、markdown
- 安全拒绝(有害内容)、计数、布尔逻辑
### 评估策略
| 策略 | 描述 | 示例 |
| ---------- | -------------------------------- | -------------------------------- |
| `exact` | 输出必须完全匹配 | `"4"` |
| `contains` | 输出必须包含子串(不区分大小写) | `"Paris"` |
| `regex` | 输出必须匹配正则表达式模式 | `"1.*2.*3"` |
| `custom` | 自定义 JS 函数返回 true/false | `(output) => output.length > 10` |
---
## 🐛 故障排除
<details>
@@ -900,7 +1329,7 @@ OmniRoute 包含内置评估框架,用于针对黄金集测试 LLM 响应质
---
## 🛠️ 技术栈
## 🛠️
- **运行时**: Node.js 20+
- **语言**: TypeScript 5.9 — `src/``open-sse/`**100% TypeScript**v1.0.6
@@ -919,29 +1348,20 @@ OmniRoute 包含内置评估框架,用于针对黄金集测试 LLM 响应质
## 📖 文档
| 文档 | 描述 |
| ----------------------------------- | ---------------------------- |
| [用户指南](docs/USER_GUIDE.md) | 提供商、组合、CLI 集成、部署 |
| [API 参考](docs/API_REFERENCE.md) | 所有端点及示例 |
| [故障排除](docs/TROUBLESHOOTING.md) | 常见问题和解决方案 |
| [架构](docs/ARCHITECTURE.md) | 系统架构和内部机制 |
| [贡献指南](CONTRIBUTING.md) | 开发设置和指南 |
| [OpenAPI 规范](docs/openapi.yaml) | OpenAPI 3.0 规范 |
| [安全策略](SECURITY.md) | 漏洞报告和安全实践 |
| 文档 | 描述 |
| ----------------------------------- | ------------------------------------------------------ |
| 🎯 **Endpoint-Aware Models** | Custom models declare supported endpoints + API format |
| [用户指南](docs/USER_GUIDE.md) | 提供商、组合、CLI 集成、部署 |
| [API 参考](docs/API_REFERENCE.md) | 所有端点及示例 |
| [故障排除](docs/TROUBLESHOOTING.md) | 常见问题和解决方案 |
| [架构](docs/ARCHITECTURE.md) | 系统架构和内部机制 |
| [贡献指南](CONTRIBUTING.md) | 开发设置和指南 |
| [OpenAPI 规范](docs/openapi.yaml) | OpenAPI 3.0 规范 |
| [安全策略](SECURITY.md) | 漏洞报告和安全实践 |
---
## 📧 支持
> 💬 **加入我们的社区!** [WhatsApp 群组](https://chat.whatsapp.com/JI7cDQ1GyaiDHhVBpLxf8b?mode=gi_t) — 获取帮助、分享技巧、了解最新动态。
- **网站**: [omniroute.online](https://omniroute.online)
- **GitHub**: [github.com/diegosouzapw/OmniRoute](https://github.com/diegosouzapw/OmniRoute)
- **Issues**: [github.com/diegosouzapw/OmniRoute/issues](https://github.com/diegosouzapw/OmniRoute/issues)
- **WhatsApp**: [社区群组](https://chat.whatsapp.com/JI7cDQ1GyaiDHhVBpLxf8b?mode=gi_t)
- **原始项目**: [decolua 的 9router](https://github.com/decolua/9router)
---
## 🗺️
## 👥 贡献者

65
bin/mcp-server.mjs Normal file
View File

@@ -0,0 +1,65 @@
#!/usr/bin/env node
import { spawn } from "node:child_process";
import { existsSync } from "node:fs";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const ROOT = join(__dirname, "..");
function resolveMcpEntry(rootDir = ROOT) {
const candidates = [
// Preferred distributable JS entry (npm publish artifact)
join(rootDir, "app", "open-sse", "mcp-server", "server.js"),
// Local workspace TypeScript source fallback
join(rootDir, "open-sse", "mcp-server", "server.ts"),
];
for (const entry of candidates) {
if (existsSync(entry)) return entry;
}
return null;
}
function formatSpawnError(exitCode, signal) {
if (signal) return `MCP server exited by signal ${signal}`;
return `MCP server exited with code ${exitCode ?? 1}`;
}
export async function startMcpCli(rootDir = ROOT) {
const mcpEntry = resolveMcpEntry(rootDir);
if (!mcpEntry) {
throw new Error(
"MCP server entrypoint not found. Expected app/open-sse/mcp-server/server.js or open-sse/mcp-server/server.ts."
);
}
// `tsx` loader is only required for local `.ts` fallback; JS entry works without it.
const loaderArgs = mcpEntry.endsWith(".ts") ? ["--import", "tsx/esm"] : [];
await new Promise((resolve, reject) => {
const child = spawn(process.execPath, [...loaderArgs, mcpEntry], {
cwd: rootDir,
env: process.env,
stdio: "inherit",
});
child.once("error", reject);
child.once("exit", (code, signal) => {
if ((code ?? 0) === 0 && !signal) {
resolve(undefined);
return;
}
reject(new Error(formatSpawnError(code, signal)));
});
});
}
if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) {
startMcpCli().catch((err) => {
console.error("\x1b[31m✖ Failed to start MCP server:\x1b[0m", err?.message || err);
process.exit(1);
});
}

View File

@@ -7,20 +7,75 @@
* omniroute Start the server (default port 20128)
* omniroute --port 3000 Start on custom port
* omniroute --no-open Start without opening browser
* omniroute --mcp Start MCP server (stdio transport for IDEs)
* omniroute --help Show help
* omniroute --version Show version
*/
import { spawn } from "node:child_process";
import { existsSync } from "node:fs";
import { existsSync, readFileSync } from "node:fs";
import { join, dirname } from "node:path";
import { fileURLToPath } from "node:url";
import { homedir, platform } from "node:os";
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const ROOT = join(__dirname, "..");
const APP_DIR = join(ROOT, "app");
// ── Load .env file (for global npm install) ─────────────────
function loadEnvFile() {
const envPaths = [];
// 1. DATA_DIR/.env if set
if (process.env.DATA_DIR) {
envPaths.push(join(process.env.DATA_DIR, ".env"));
}
// 2. ~/.omniroute/.env (default data dir)
const home = homedir();
if (home) {
if (platform() === "win32") {
const appData = process.env.APPDATA || join(home, "AppData", "Roaming");
envPaths.push(join(appData, "omniroute", ".env"));
} else {
envPaths.push(join(home, ".omniroute", ".env"));
}
}
// 3. ./.env (current working directory)
envPaths.push(join(process.cwd(), ".env"));
for (const envPath of envPaths) {
try {
if (existsSync(envPath)) {
const content = readFileSync(envPath, "utf-8");
for (const line of content.split("\n")) {
const trimmed = line.trim();
// Skip empty lines and comments
if (!trimmed || trimmed.startsWith("#")) continue;
const eqIdx = trimmed.indexOf("=");
if (eqIdx > 0) {
const key = trimmed.slice(0, eqIdx).trim();
const value = trimmed.slice(eqIdx + 1).trim();
// Don't override existing env vars
if (process.env[key] === undefined) {
// Remove surrounding quotes
process.env[key] = value.replace(/^["']|["']$/g, "");
}
}
}
console.log(` \x1b[2m📋 Loaded env from ${envPath}\x1b[0m`);
return;
}
} catch {
// Ignore errors reading env files
}
}
}
loadEnvFile();
// ── Parse args ─────────────────────────────────────────────
const args = process.argv.slice(2);
@@ -32,9 +87,21 @@ if (args.includes("--help") || args.includes("-h")) {
omniroute Start the server
omniroute --port <port> Use custom API port (default: 20128)
omniroute --no-open Don't open browser automatically
omniroute --mcp Start MCP server (stdio transport for IDEs)
omniroute --help Show this help
omniroute --version Show version
\x1b[1mMCP Integration:\x1b[0m
The --mcp flag starts an MCP server over stdio, exposing OmniRoute
tools for AI agents in VS Code, Cursor, Claude Desktop, and Copilot.
Available tools: omniroute_get_health, omniroute_list_combos,
omniroute_check_quota, omniroute_route_request, and more.
\x1b[1mConfig:\x1b[0m
Loads .env from: ~/.omniroute/.env or ./.env
Memory limit: OMNIROUTE_MEMORY_MB (default: 512)
\x1b[1mAfter starting:\x1b[0m
Dashboard: http://localhost:<dashboard-port>
API: http://localhost:<api-port>/v1
@@ -58,6 +125,18 @@ if (args.includes("--version") || args.includes("-v")) {
process.exit(0);
}
// ── MCP Server Mode ───────────────────────────────────────
if (args.includes("--mcp")) {
try {
const { startMcpCli } = await import(join(ROOT, "bin", "mcp-server.mjs"));
await startMcpCli(ROOT);
} catch (err) {
console.error("\x1b[31m✖ Failed to start MCP server:\x1b[0m", err.message || err);
process.exit(1);
}
process.exit(0);
}
function parsePort(value, fallback) {
const parsed = parseInt(String(value), 10);
return Number.isFinite(parsed) && parsed > 0 && parsed <= 65535 ? parsed : fallback;
@@ -83,11 +162,11 @@ const noOpen = args.includes("--no-open");
// ── Banner ─────────────────────────────────────────────────
console.log(`
\x1b[36m ____ _ ____ _
/ __ \\ (_) __ \\ | |
| | | |_ __ ___ _ __ _| |__) |___ _ _| |_ ___
| | | | '_ \` _ \\| '_ \\ | _ // _ \\| | | | __/ _ \\
| |__| | | | | | | | | | | | \\ \\ (_) | |_| | || __/
\\____/|_| |_| |_|_| |_|_|_| \\_\\___/ \\__,_|\\__\\___|
/ __ \\ (_) __ \\ | |
| | | |_ __ ___ _ __ _| |__) |___ _ _| |_ ___
| | | | '_ \` _ \\| '_ \\ | _ // _ \\| | | | __/ _ \\
| |__| | | | | | | | | | | | \\ \\ (_) | |_| | || __/
\\____/|_| |_| |_|_| |_|_|_| \\_\\___/ \\__,_|\\__\\___|
\x1b[0m`);
// ── Node.js version check ──────────────────────────────────
@@ -117,6 +196,11 @@ if (!existsSync(serverJs)) {
// ── Start server ───────────────────────────────────────────
console.log(` \x1b[2m⏳ Starting server...\x1b[0m\n`);
// Sanitize memory limit — parseInt to prevent command injection (#150)
const rawMemory = parseInt(process.env.OMNIROUTE_MEMORY_MB || "512", 10);
const memoryLimit =
Number.isFinite(rawMemory) && rawMemory >= 64 && rawMemory <= 16384 ? rawMemory : 512;
const env = {
...process.env,
OMNIROUTE_PORT: String(port),
@@ -125,9 +209,10 @@ const env = {
API_PORT: String(apiPort),
HOSTNAME: "0.0.0.0",
NODE_ENV: "production",
NODE_OPTIONS: `--max-old-space-size=${memoryLimit}`,
};
const server = spawn("node", [serverJs], {
const server = spawn("node", [`--max-old-space-size=${memoryLimit}`, serverJs], {
cwd: APP_DIR,
env,
stdio: "pipe",

View File

@@ -2,7 +2,7 @@
🌐 **Languages:** 🇺🇸 [English](ARCHITECTURE.md) | 🇧🇷 [Português (Brasil)](i18n/pt-BR/ARCHITECTURE.md) | 🇪🇸 [Español](i18n/es/ARCHITECTURE.md) | 🇫🇷 [Français](i18n/fr/ARCHITECTURE.md) | 🇮🇹 [Italiano](i18n/it/ARCHITECTURE.md) | 🇷🇺 [Русский](i18n/ru/ARCHITECTURE.md) | 🇨🇳 [中文 (简体)](i18n/zh-CN/ARCHITECTURE.md) | 🇩🇪 [Deutsch](i18n/de/ARCHITECTURE.md) | 🇮🇳 [हिन्दी](i18n/in/ARCHITECTURE.md) | 🇹🇭 [ไทย](i18n/th/ARCHITECTURE.md) | 🇺🇦 [Українська](i18n/uk-UA/ARCHITECTURE.md) | 🇸🇦 [العربية](i18n/ar/ARCHITECTURE.md) | 🇯🇵 [日本語](i18n/ja/ARCHITECTURE.md) | 🇻🇳 [Tiếng Việt](i18n/vi/ARCHITECTURE.md) | 🇧🇬 [Български](i18n/bg/ARCHITECTURE.md) | 🇩🇰 [Dansk](i18n/da/ARCHITECTURE.md) | 🇫🇮 [Suomi](i18n/fi/ARCHITECTURE.md) | 🇮🇱 [עברית](i18n/he/ARCHITECTURE.md) | 🇭🇺 [Magyar](i18n/hu/ARCHITECTURE.md) | 🇮🇩 [Bahasa Indonesia](i18n/id/ARCHITECTURE.md) | 🇰🇷 [한국어](i18n/ko/ARCHITECTURE.md) | 🇲🇾 [Bahasa Melayu](i18n/ms/ARCHITECTURE.md) | 🇳🇱 [Nederlands](i18n/nl/ARCHITECTURE.md) | 🇳🇴 [Norsk](i18n/no/ARCHITECTURE.md) | 🇵🇹 [Português (Portugal)](i18n/pt/ARCHITECTURE.md) | 🇷🇴 [Română](i18n/ro/ARCHITECTURE.md) | 🇵🇱 [Polski](i18n/pl/ARCHITECTURE.md) | 🇸🇰 [Slovenčina](i18n/sk/ARCHITECTURE.md) | 🇸🇪 [Svenska](i18n/sv/ARCHITECTURE.md) | 🇵🇭 [Filipino](i18n/phi/ARCHITECTURE.md)
_Last updated: 2026-02-18_
_Last updated: 2026-03-04_
## Executive Summary
@@ -81,8 +81,8 @@ flowchart LR
API[V1 Compatibility API\n/v1/*]
DASH[Dashboard + Management API\n/api/*]
CORE[SSE + Translation Core\nopen-sse + src/sse]
DB[(db.json)]
UDB[(usage.json + log.txt)]
DB[(storage.sqlite)]
UDB[(usage tables + log artifacts)]
end
subgraph Upstreams[Upstream Providers]
@@ -144,7 +144,7 @@ Management domains:
- Providers/connections: `src/app/api/providers*`
- Provider nodes: `src/app/api/provider-nodes*`
- Custom models: `src/app/api/provider-models` (GET/POST/DELETE)
- Model catalog: `src/app/api/models/catalog` (GET)
- Model catalog: `src/app/api/models/route.ts` (GET)
- Proxy config: `src/app/api/settings/proxy` (GET/PUT/DELETE) + `src/app/api/settings/proxy/test` (POST)
- OAuth: `src/app/api/oauth/*`
- Keys/aliases/combos/pricing: `src/app/api/keys*`, `src/app/api/models/alias`, `src/app/api/combos*`, `src/app/api/pricing`
@@ -225,18 +225,19 @@ OAuth provider modules (12 individual files under `src/lib/oauth/providers/`):
## 3) Persistence Layer
Primary state DB:
Primary state DB (SQLite):
- `src/lib/localDb.ts`
- file: `${DATA_DIR}/db.json` (or `$XDG_CONFIG_HOME/omniroute/db.json` when set, else `~/.omniroute/db.json`)
- entities: providerConnections, providerNodes, modelAliases, combos, apiKeys, settings, pricing, **customModels**, **proxyConfig**, **ipFilter**, **thinkingBudget**, **systemPrompt**
- Core infra: `src/lib/db/core.ts` (better-sqlite3, migrations, WAL)
- Re-export facade: `src/lib/localDb.ts` (thin compatibility layer for callers)
- file: `${DATA_DIR}/storage.sqlite` (or `$XDG_CONFIG_HOME/omniroute/storage.sqlite` when set, else `~/.omniroute/storage.sqlite`)
- entities (tables + KV namespaces): providerConnections, providerNodes, modelAliases, combos, apiKeys, settings, pricing, **customModels**, **proxyConfig**, **ipFilter**, **thinkingBudget**, **systemPrompt**
Usage DB:
Usage persistence:
- `src/lib/usageDb.ts`
- files: `${DATA_DIR}/usage.json`, `${DATA_DIR}/log.txt`, `${DATA_DIR}/call_logs/`
- follows same base directory policy as `localDb` (`DATA_DIR`, then `XDG_CONFIG_HOME/omniroute` when set)
- decomposed into focused sub-modules: `migrations.ts`, `usageHistory.ts`, `costCalculator.ts`, `usageStats.ts`, `callLogs.ts`
- facade: `src/lib/usageDb.ts` (decomposed modules in `src/lib/usage/*`)
- SQLite tables in `storage.sqlite`: `usage_history`, `call_logs`, `proxy_logs`
- optional file artifacts remain for compatibility/debug (`${DATA_DIR}/log.txt`, `${DATA_DIR}/call_logs/`, `<repo>/logs/...`)
- legacy JSON files are migrated to SQLite by startup migrations when present
Domain State DB (SQLite):
@@ -505,9 +506,9 @@ erDiagram
Physical storage files:
- main state: `${DATA_DIR}/db.json` (or `$XDG_CONFIG_HOME/omniroute/db.json` when set, else `~/.omniroute/db.json`)
- usage stats: `${DATA_DIR}/usage.json`
- request log lines: `${DATA_DIR}/log.txt`
- primary runtime DB: `${DATA_DIR}/storage.sqlite`
- request log lines: `${DATA_DIR}/log.txt` (compat/debug artifact)
- structured call payload archives: `${DATA_DIR}/call_logs/`
- optional translator/request debug sessions: `<repo>/logs/...`
## Deployment Topology
@@ -522,8 +523,8 @@ flowchart LR
subgraph ContainerOrProcess[OmniRoute Runtime]
Next[Next.js Server\nPORT=20128]
Core[SSE Core + Executors]
MainDB[(db.json)]
UsageDB[(usage.json/log.txt)]
MainDB[(storage.sqlite)]
UsageDB[(usage tables + log artifacts)]
end
subgraph External[External Services]
@@ -550,7 +551,7 @@ flowchart LR
- `src/app/api/providers*`: provider CRUD, validation, testing
- `src/app/api/provider-nodes*`: custom compatible node management
- `src/app/api/provider-models`: custom model management (CRUD)
- `src/app/api/models/catalog`: full model catalog API (all types grouped by provider)
- `src/app/api/models/route.ts`: model catalog API (aliases + custom models)
- `src/app/api/oauth/*`: OAuth/device-code flows
- `src/app/api/keys*`: local API key lifecycle
- `src/app/api/models/alias`: alias management
@@ -582,8 +583,9 @@ flowchart LR
### Persistence
- `src/lib/localDb.ts`: persistent config/state
- `src/lib/usageDb.ts`: usage history and rolling request logs
- `src/lib/db/*`: persistent config/state and domain persistence on SQLite
- `src/lib/localDb.ts`: compatibility re-export for DB modules
- `src/lib/usageDb.ts`: usage history/call logs facade on top of SQLite tables
## Provider Executor Coverage (Strategy Pattern)
@@ -724,23 +726,23 @@ Files are written to `<repo>/logs/<session>/` for each request session.
## 5) Data Integrity
- DB shape migration/repair for missing keys
- corrupt JSON reset safeguards for localDb and usageDb
- SQLite schema migrations and auto-upgrade hooks at startup
- legacy JSON → SQLite migration compatibility path
## Observability and Operational Signals
Runtime visibility sources:
- console logs from `src/sse/utils/logger.ts`
- per-request usage aggregates in `usage.json`
- textual request status log in `log.txt`
- per-request usage aggregates in SQLite (`usage_history`, `call_logs`, `proxy_logs`)
- textual request status log in `log.txt` (optional/compat)
- optional deep request/translation logs under `logs/` when `ENABLE_REQUEST_LOGS=true`
- dashboard usage endpoints (`/api/usage/*`) for UI consumption
## Security-Sensitive Boundaries
- JWT secret (`JWT_SECRET`) secures dashboard session cookie verification/signing
- Initial password fallback (`INITIAL_PASSWORD`, default `123456`) must be overridden in real deployments
- Initial password bootstrap (`INITIAL_PASSWORD`) should be explicitly configured for first-run provisioning
- API key HMAC secret (`API_KEY_SECRET`) secures generated local API key format
- Provider secrets (API keys/tokens) are persisted in local DB and should be protected at filesystem level
- Cloud sync endpoints rely on API key auth + machine id semantics
@@ -762,13 +764,13 @@ Environment variables actively used by code:
## Known Architectural Notes
1. `usageDb` and `localDb` now share the same base directory policy (`DATA_DIR` -> `XDG_CONFIG_HOME/omniroute` -> `~/.omniroute`) with legacy file migration.
2. `/api/v1/route.ts` returns a static model list and is not the main models source used by `/v1/models`.
1. `usageDb` and `localDb` share the same base directory policy (`DATA_DIR` -> `XDG_CONFIG_HOME/omniroute` -> `~/.omniroute`) with legacy file migration.
2. `/api/v1/route.ts` delegates to the same unified catalog builder used by `/api/v1/models` (`src/app/api/v1/models/catalog.ts`) to avoid semantic drift.
3. Request logger writes full headers/body when enabled; treat log directory as sensitive.
4. Cloud behavior depends on correct `NEXT_PUBLIC_BASE_URL` and cloud endpoint reachability.
5. The `open-sse/` directory is published as the `@omniroute/open-sse` **npm workspace package**. Source code imports it via `@omniroute/open-sse/...` (resolved by Next.js `transpilePackages`). File paths in this document still use the directory name `open-sse/` for consistency.
6. Charts in the dashboard use **Recharts** (SVG-based) for accessible, interactive analytics visualizations (model usage bar charts, provider breakdown tables with success rates).
7. E2E tests use **Playwright** (`tests/e2e/`), run via `npm run test:e2e`. Unit tests use **Node.js test runner** (`tests/unit/`), run via `npm run test:plan3`. Source code under `src/` is **TypeScript** (`.ts`/`.tsx`); the `open-sse/` workspace remains JavaScript (`.js`).
7. E2E tests use **Playwright** (`tests/e2e/`), run via `npm run test:e2e`. Unit tests use **Node.js test runner** (`tests/unit/`), run via `npm run test:unit`. Source code under `src/` is **TypeScript** (`.ts`/`.tsx`); the `open-sse/` workspace remains JavaScript (`.js`).
8. Settings page is organized into 5 tabs: Security, Routing (6 global strategies: fill-first, round-robin, p2c, random, least-used, cost-optimized), Resilience (editable rate limits, circuit breaker, policies), AI (thinking budget, system prompt, prompt cache), Advanced (proxy).
## Operational Verification Checklist

View File

@@ -48,7 +48,7 @@ Four modes for debugging API translations: **Playground** (format converter), **
## ⚙️ Settings
General settings, system storage, backup management (export/import database), appearance (dark/light mode), security (includes API endpoint protection and custom provider blocking), routing, resilience, and advanced configuration.
General settings, system storage, backup management (export/import database), appearance (dark/light mode), security (includes API endpoint protection and custom provider blocking), routing (model aliases, background task degradation), resilience (rate limit persistence), and advanced configuration.
![Settings Dashboard](screenshots/06-settings.png)
@@ -56,7 +56,7 @@ General settings, system storage, backup management (export/import database), ap
## 🔧 CLI Tools
One-click configuration for AI coding tools: Claude Code, Codex CLI, Gemini CLI, OpenClaw, Kilo Code, and Antigravity.
One-click configuration for AI coding tools: Claude Code, Codex CLI, Gemini CLI, OpenClaw, Kilo Code, Antigravity, and **GitHub Copilot** (config generator for `chatLanguageModels.json`).
![CLI Tools Dashboard](screenshots/07-cli-tools.png)
@@ -75,3 +75,19 @@ Real-time request logging with filtering by provider, model, account, and API ke
Your unified API endpoint with capability breakdown: Chat Completions, Embeddings, Image Generation, Reranking, Audio Transcription, and registered API keys.
![Endpoint Dashboard](screenshots/09-endpoint.png)
---
## 🖥️ Desktop Application
Native Electron desktop app for Windows, macOS, and Linux. Run OmniRoute as a standalone application with system tray integration, offline support, and one-click install.
Key features:
- Server readiness polling (no blank screen on cold start)
- System tray with port management
- Content Security Policy
- Single-instance lock
- Platform-conditional UI (macOS traffic lights, Windows/Linux default titlebar)
📖 See [`electron/README.md`](../electron/README.md) for full documentation.

33
docs/RELEASE_CHECKLIST.md Normal file
View File

@@ -0,0 +1,33 @@
# Release Checklist
Use this checklist before tagging or publishing a new OmniRoute release.
## Version and Changelog
1. Bump `package.json` version (`x.y.z`) in the release branch.
2. Move release notes from `## [Unreleased]` in `CHANGELOG.md` to a dated section:
- `## [x.y.z] — YYYY-MM-DD`
3. Keep `## [Unreleased]` as the first changelog section for upcoming work.
4. Ensure the latest semver section in `CHANGELOG.md` equals `package.json` version.
## API Docs
1. Update `docs/openapi.yaml`:
- `info.version` must equal `package.json` version.
2. Validate endpoint examples if API contracts changed.
## Runtime Docs
1. Review `docs/ARCHITECTURE.md` for storage/runtime drift.
2. Review `docs/TROUBLESHOOTING.md` for env var and operational drift.
3. Update localized docs if source docs changed significantly.
## Automated Check
Run the sync guard locally before opening PR:
```bash
npm run check:docs-sync
```
CI also runs this check in `.github/workflows/ci.yml` (lint job).

View File

@@ -10,7 +10,7 @@ Common problems and solutions for OmniRoute.
| Problem | Solution |
| ----------------------------- | ------------------------------------------------------------------ |
| First login not working | Check `INITIAL_PASSWORD` in `.env` (default: `123456`) |
| First login not working | Set `INITIAL_PASSWORD` in `.env` (no hardcoded default) |
| Dashboard opens on wrong port | Set `PORT=20128` and `NEXT_PUBLIC_BASE_URL=http://localhost:20128` |
| No request logs under `logs/` | Set `ENABLE_REQUEST_LOGS=true` |
| EACCES: permission denied | Set `DATA_DIR=/path/to/writable/dir` to override `~/.omniroute` |
@@ -120,8 +120,8 @@ curl http://localhost:20128/api/monitoring/health
### Runtime Storage
- Main state: `${DATA_DIR}/db.json` (providers, combos, aliases, keys, settings)
- Usage: `${DATA_DIR}/usage.json`, `${DATA_DIR}/log.txt`, `${DATA_DIR}/call_logs/`
- Main state: `${DATA_DIR}/storage.sqlite` (providers, combos, aliases, keys, settings)
- Usage: SQLite tables in `storage.sqlite` (`usage_history`, `call_logs`, `proxy_logs`) + optional `${DATA_DIR}/log.txt` and `${DATA_DIR}/call_logs/`
- Request logs: `<repo>/logs/...` (when `ENABLE_REQUEST_LOGS=true`)
---
@@ -210,6 +210,41 @@ When many concurrent requests hit a rate-limited provider, OmniRoute uses mutex
---
## Optional RAG / LLM failure taxonomy (16 problems)
Some OmniRoute users place the gateway in front of RAG or agent stacks. In those setups it is common to see a strange pattern: OmniRoute looks healthy (providers up, routing profiles ok, no rate limit alerts) but the final answer is still wrong.
In practice these incidents usually come from the downstream RAG pipeline, not from the gateway itself.
If you want a shared vocabulary to describe those failures you can use the WFGY ProblemMap, an external MIT license text resource that defines sixteen recurring RAG / LLM failure patterns. At a high level it covers:
- retrieval drift and broken context boundaries
- empty or stale indexes and vector stores
- embedding versus semantic mismatch
- prompt assembly and context window issues
- logic collapse and overconfident answers
- long chain and agent coordination failures
- multi agent memory and role drift
- deployment and bootstrap ordering problems
The idea is simple:
1. When you investigate a bad response, capture:
- user task and request
- route or provider combo in OmniRoute
- any RAG context used downstream (retrieved documents, tool calls, etc)
2. Map the incident to one or two WFGY ProblemMap numbers (`No.1``No.16`).
3. Store the number in your own dashboard, runbook, or incident tracker next to the OmniRoute logs.
4. Use the corresponding WFGY page to decide whether you need to change your RAG stack, retriever, or routing strategy.
Full text and concrete recipes live here (MIT license, text only):
[WFGY ProblemMap README](https://github.com/onestardao/WFGY/blob/main/ProblemMap/README.md)
You can ignore this section if you do not run RAG or agent pipelines behind OmniRoute.
---
## Still Stuck?
- **GitHub Issues**: [github.com/diegosouzapw/OmniRoute/issues](https://github.com/diegosouzapw/OmniRoute/issues)

View File

@@ -318,6 +318,25 @@ Model: cc/claude-opus-4-6
## 🚀 Deployment
### Global npm install (Recommended)
```bash
npm install -g omniroute
# Create config directory
mkdir -p ~/.omniroute
# Create .env file (see .env.example)
cp .env.example ~/.omniroute/.env
# Start server
omniroute
# Or with custom port:
omniroute --port 3000
```
The CLI automatically loads `.env` from `~/.omniroute/.env` or `./.env`.
### VPS Deployment
```bash
@@ -337,6 +356,43 @@ npm run start
# Or: pm2 start npm --name omniroute -- start
```
### PM2 Deployment (Low Memory)
For servers with limited RAM, use the memory limit option:
```bash
# With 512MB limit (default)
pm2 start npm --name omniroute -- start
# Or with custom memory limit
OMNIROUTE_MEMORY_MB=512 pm2 start npm --name omniroute -- start
# Or using ecosystem.config.js
pm2 start ecosystem.config.js
```
Create `ecosystem.config.js`:
```javascript
module.exports = {
apps: [
{
name: "omniroute",
script: "npm",
args: "start",
env: {
NODE_ENV: "production",
OMNIROUTE_MEMORY_MB: "512",
JWT_SECRET: "your-secret",
INITIAL_PASSWORD: "your-password",
},
node_args: "--max-old-space-size=512",
max_memory_restart: "300M",
},
],
};
```
### Docker
```bash
@@ -351,20 +407,23 @@ For host-integrated mode with CLI binaries, see the Docker section in the main d
### Environment Variables
| Variable | Default | Description |
| --------------------- | ------------------------------------ | ------------------------------------------------------- |
| `JWT_SECRET` | `omniroute-default-secret-change-me` | JWT signing secret (**change in production**) |
| `INITIAL_PASSWORD` | `123456` | First login password |
| `DATA_DIR` | `~/.omniroute` | Data directory (db, usage, logs) |
| `PORT` | framework default | Service port (`20128` in examples) |
| `HOSTNAME` | framework default | Bind host (Docker defaults to `0.0.0.0`) |
| `NODE_ENV` | runtime default | Set `production` for deploy |
| `BASE_URL` | `http://localhost:20128` | Server-side internal base URL |
| `CLOUD_URL` | `https://omniroute.dev` | Cloud sync endpoint base URL |
| `API_KEY_SECRET` | `endpoint-proxy-api-key-secret` | HMAC secret for generated API keys |
| `REQUIRE_API_KEY` | `false` | Enforce Bearer API key on `/v1/*` |
| `ENABLE_REQUEST_LOGS` | `false` | Enables request/response logs |
| `AUTH_COOKIE_SECURE` | `false` | Force `Secure` auth cookie (behind HTTPS reverse proxy) |
| Variable | Default | Description |
| ------------------------- | ------------------------------------ | ------------------------------------------------------- |
| `JWT_SECRET` | `omniroute-default-secret-change-me` | JWT signing secret (**change in production**) |
| `INITIAL_PASSWORD` | `123456` | First login password |
| `DATA_DIR` | `~/.omniroute` | Data directory (db, usage, logs) |
| `PORT` | framework default | Service port (`20128` in examples) |
| `HOSTNAME` | framework default | Bind host (Docker defaults to `0.0.0.0`) |
| `NODE_ENV` | runtime default | Set `production` for deploy |
| `BASE_URL` | `http://localhost:20128` | Server-side internal base URL |
| `CLOUD_URL` | `https://omniroute.dev` | Cloud sync endpoint base URL |
| `API_KEY_SECRET` | `endpoint-proxy-api-key-secret` | HMAC secret for generated API keys |
| `REQUIRE_API_KEY` | `false` | Enforce Bearer API key on `/v1/*` |
| `ENABLE_REQUEST_LOGS` | `false` | Enables request/response logs |
| `AUTH_COOKIE_SECURE` | `false` | Force `Secure` auth cookie (behind HTTPS reverse proxy) |
| `OMNIROUTE_MEMORY_MB` | `512` | Node.js heap limit in MB |
| `PROMPT_CACHE_MAX_SIZE` | `50` | Max prompt cache entries |
| `SEMANTIC_CACHE_MAX_SIZE` | `100` | Max semantic cache entries |
For the full environment variable reference, see the [README](../README.md).
@@ -696,3 +755,55 @@ Access via **Dashboard → Health**. Real-time system health overview with 6 car
| **Latency Telemetry** | p50/p95/p99 latency aggregation per provider |
**Pro Tip:** The Health page auto-refreshes every 10 seconds. Use the circuit breaker card to identify which providers are experiencing issues.
---
## 🖥️ Desktop Application (Electron)
OmniRoute is available as a native desktop application for Windows, macOS, and Linux.
### Installation
```bash
# From the electron directory:
cd electron
npm install
# Development mode (connect to running Next.js dev server):
npm run dev
# Production mode (uses standalone build):
npm start
```
### Building Installers
```bash
cd electron
npm run build # Current platform
npm run build:win # Windows (.exe NSIS)
npm run build:mac # macOS (.dmg universal)
npm run build:linux # Linux (.AppImage)
```
Output → `electron/dist-electron/`
### Key Features
| Feature | Description |
| --------------------------- | ---------------------------------------------------- |
| **Server Readiness** | Polls server before showing window (no blank screen) |
| **System Tray** | Minimize to tray, change port, quit from tray menu |
| **Port Management** | Change server port from tray (auto-restarts server) |
| **Content Security Policy** | Restrictive CSP via session headers |
| **Single Instance** | Only one app instance can run at a time |
| **Offline Mode** | Bundled Next.js server works without internet |
### Environment Variables
| Variable | Default | Description |
| --------------------- | ------- | -------------------------------- |
| `OMNIROUTE_PORT` | `20128` | Server port |
| `OMNIROUTE_MEMORY_MB` | `512` | Node.js heap limit (6416384 MB) |
📖 Full documentation: [`electron/README.md`](../electron/README.md)

View File

@@ -16,7 +16,7 @@ Administrer AI-udbyderforbindelser: OAuth-udbydere (Claude Code, Codex, Gemini C
## 🎨 Kombinationer
Opret modelrouting-kombinationer med 6 strategier: Fyld-først, round-robin, power-of-to-choices, tilfældig, mindst brugt og omkostningsoptimeret. Hver combo kæder flere modeller med automatisk fallback.
Opret modelrouting (model aliases, background task degradation)-kombinationer med 6 strategier: Fyld-først, round-robin, power-of-to-choices, tilfældig, mindst brugt og omkostningsoptimeret. Hver combo kæder flere modeller med automatisk fallback.
![Combos Dashboard](screenshots/02-combos.png)

View File

@@ -16,7 +16,7 @@ Gestisci le connessioni dei provider AI: provider OAuth (Claude Code, Codex, Gem
## 🎨Combo
Crea combinazioni di routing del modello con 6 strategie: riempimento prima, round robin, scelta potenza di due, casuale, meno utilizzata e con ottimizzazione dei costi. Ogni combo concatena più modelli con fallback automatico.
Crea combinazioni di routing (model aliases, background task degradation) del modello con 6 strategie: riempimento prima, round robin, scelta potenza di due, casuale, meno utilizzata e con ottimizzazione dei costi. Ogni combo concatena più modelli con fallback automatico.
![Combos Dashboard](screenshots/02-combos.png)

View File

@@ -48,7 +48,7 @@ Vier modi voor het debuggen van API-vertalingen: **Playground** (formaatconverte
## ⚙️ Instellingen
Algemene instellingen, systeemopslag, back-upbeheer (database exporteren/importeren), uiterlijk (donker/licht-modus), beveiliging (inclusief API-eindpuntbescherming en aangepaste providerblokkering), routing, veerkracht en geavanceerde configuratie.
Algemene instellingen, systeemopslag, back-upbeheer (database exporteren/importeren), uiterlijk (donker/licht-modus), beveiliging (inclusief API-eindpuntbescherming en aangepaste providerblokkering), routing (model aliases, background task degradation), veerkracht en geavanceerde configuratie.
![Settings Dashboard](screenshots/06-settings.png)

View File

@@ -48,7 +48,7 @@ Apat na mode para sa pag-debug ng mga pagsasalin ng API: **Playground** (format
## ⚙️ Mga Setting
Mga pangkalahatang setting, system storage, backup management (export/import database), hitsura (dark/light mode), seguridad (kasama ang API endpoint protection at custom provider blocking), routing, resilience, at advanced configuration.
Mga pangkalahatang setting, system storage, backup management (export/import database), hitsura (dark/light mode), seguridad (kasama ang API endpoint protection at custom provider blocking), routing (model aliases, background task degradation), resilience, at advanced configuration.
![Settings Dashboard](screenshots/06-settings.png)

View File

@@ -16,7 +16,7 @@ Zarządzaj połączeniami dostawców AI: dostawcy OAuth (Claude Code, Codex, Gem
## 🎨 Kombinacje
Twórz kombinacje routingu modeli za pomocą 6 strategii: najpierw wypełnij, okrężnie, siła dwóch wyborów, losowa, najrzadziej używana i zoptymalizowana pod względem kosztów. Każda kombinacja łączy wiele modeli z automatycznym cofaniem.
Twórz kombinacje routing (model aliases, background task degradation)u modeli za pomocą 6 strategii: najpierw wypełnij, okrężnie, siła dwóch wyborów, losowa, najrzadziej używana i zoptymalizowana pod względem kosztów. Każda kombinacja łączy wiele modeli z automatycznym cofaniem.
![Combos Dashboard](screenshots/02-combos.png)

View File

@@ -16,7 +16,7 @@ Hantera AI-leverantörsanslutningar: OAuth-leverantörer (Claude Code, Codex, Ge
## 🎨 Combos
Skapa modell routing-kombinationer med 6 strategier: fyll först, round-robin, kraft-av-två-val, slumpmässig, minst använda och kostnadsoptimerad. Varje combo kedjer flera modeller med automatisk reserv.
Skapa modell routing (model aliases, background task degradation)-kombinationer med 6 strategier: fyll först, round-robin, kraft-av-två-val, slumpmässig, minst använda och kostnadsoptimerad. Varje combo kedjer flera modeller med automatisk reserv.
![Combos Dashboard](screenshots/02-combos.png)

View File

@@ -1,7 +1,7 @@
openapi: 3.1.0
info:
title: OmniRoute API
version: 0.5.0
version: 2.0.0
description: |
OmniRoute is a local-first AI API proxy router. It provides an OpenAI-compatible
endpoint that routes requests to multiple AI providers with load balancing,

250
electron/README.md Normal file
View File

@@ -0,0 +1,250 @@
# OmniRoute Electron Desktop App
This directory contains the Electron desktop application wrapper for OmniRoute.
## Architecture (v1.6.4)
```
electron/
├── main.js # Main process — window, tray, server lifecycle, CSP, IPC
├── preload.js # Preload script — secure IPC bridge with disposer pattern
├── package.json # Electron-specific dependencies & electron-builder config
├── types.d.ts # TypeScript definitions (AppInfo, ServerStatus, ElectronAPI)
└── assets/ # Application icons and resources
src/shared/hooks/
└── useElectron.ts # React hooks — useSyncExternalStore, zero re-renders
```
## Key Design Decisions
| Decision | Rationale |
| ----------------------------- | ------------------------------------------------------------------------------------------------ |
| `waitForServer()` polling | Prevents blank screen on cold start — polls `http://localhost:PORT` before loading |
| `stdio: 'pipe'` | Captures server stdout/stderr for logging + readiness detection (not `inherit`) |
| Disposer pattern | `onServerStatus()` returns `() => void` for precise listener cleanup (no `removeAllListeners`) |
| `useSyncExternalStore` | Zero re-renders for `useIsElectron()` — no `useState` + `useEffect` cycle |
| CSP via session headers | `Content-Security-Policy` restricts `script-src`, `connect-src` etc. per Electron best practices |
| Platform-conditional titlebar | `titleBarStyle: 'hiddenInset'` only on macOS; `default` on Windows/Linux |
## Development
### Prerequisites
1. Build the Next.js app first:
```bash
npm run build
```
2. Install Electron dependencies:
```bash
cd electron
npm install
```
### Running in Development
1. Start the Next.js development server:
```bash
npm run dev
```
2. In another terminal, start Electron:
```bash
cd electron
npm run dev
```
### Running in Production Mode
1. Build Next.js in standalone mode:
```bash
npm run build
```
2. Start Electron:
```bash
cd electron
npm start
```
## Building
### Build for Current Platform
```bash
cd electron
npm run build
```
### Build for Specific Platforms
```bash
# Windows
npm run build:win
# macOS (x64 + arm64)
npm run build:mac
# Linux
npm run build:linux
```
## Output
Built applications are placed in `dist-electron/`:
- Windows: `.exe` installer (NSIS) + portable `.exe`
- macOS: `.dmg` installer (Intel + Apple Silicon)
- Linux: `.AppImage`
## Installation
### macOS
1. Download the latest `.dmg` from the [Releases](https://github.com/diegosouzapw/OmniRoute/releases) page.
2. Open the `.dmg` file.
3. Drag `OmniRoute.app` to the Applications folder.
4. Launch from Applications.
> ⚠️ **Note:** The app is not signed with an Apple Developer certificate yet. If macOS blocks the app, run:
> ```bash
> xattr -cr /Applications/OmniRoute.app
> ```
> Or right-click the app → Open → Open (to bypass Gatekeeper on first launch).
### Windows
**Installer (Recommended):**
1. Download `OmniRoute.Setup.*.exe` from [Releases](https://github.com/diegosouzapw/OmniRoute/releases).
2. Run the installer.
3. Launch from Start Menu or Desktop shortcut.
**Portable (No Installation):**
1. Download `OmniRoute.exe` from [Releases](https://github.com/diegosouzapw/OmniRoute/releases).
2. Run directly from any folder.
### Linux
1. Download the `.AppImage` from [Releases](https://github.com/diegosouzapw/OmniRoute/releases).
2. Make it executable:
```bash
chmod +x OmniRoute-*.AppImage
```
3. Run:
```bash
./OmniRoute-*.AppImage
```
## Features
- **Server Readiness** — Waits for health check before showing window
- **System Tray** — Minimize to tray with quick actions (open, port change, quit)
- **Port Management** — Change port from tray menu (server restarts automatically)
- **Window Controls** — Custom minimize, maximize, close via IPC
- **Content Security Policy** — Restrictive CSP via session headers
- **Offline Support** — Bundled Next.js standalone server
- **Single Instance** — Only one app instance can run at a time
## Configuration
### Environment Variables
| Variable | Default | Description |
| --------------------- | ------------ | --------------------------------- |
| `OMNIROUTE_PORT` | `20128` | Server port |
| `OMNIROUTE_MEMORY_MB` | `512` | Node.js heap limit (6416384 MB) |
| `NODE_ENV` | `production` | Set to `development` for dev mode |
### Custom Icon
Place your icons in `assets/`:
- `icon.ico` — Windows icon (256×256)
- `icon.icns` — macOS icon bundle
- `icon.png` — Linux/general use (512×512)
- `tray-icon.png` — System tray icon (16×16 or 32×32)
## IPC Channels
### Invoke (Renderer → Main, async)
| Channel | Returns | Description |
| ---------------- | ------------- | --------------------------------------------- |
| `get-app-info` | `AppInfo` | App name, version, platform, isDev, port |
| `open-external` | `void` | Open URL in default browser (http/https only) |
| `get-data-dir` | `string` | Get userData directory path |
| `restart-server` | `{ success }` | Stop + restart server (5s timeout + SIGKILL) |
### Send (Renderer → Main, fire-and-forget)
| Channel | Description |
| ----------------- | ------------------------------- |
| `window-minimize` | Minimize window |
| `window-maximize` | Toggle maximize/restore |
| `window-close` | Close window (minimize to tray) |
### Receive (Main → Renderer, events)
| Channel | Payload | Emitted When |
| --------------- | -------------- | ----------------------------------------- |
| `server-status` | `ServerStatus` | Server starts, stops, errors, or restarts |
| `port-changed` | `number` | Port change via tray menu |
> **Note**: Listeners return disposer functions for precise cleanup. See `useServerStatus` and `usePortChanged` hooks.
## Security
| Feature | Implementation |
| ----------------- | ------------------------------------------------------------------------------- |
| Context Isolation | `contextIsolation: true` — renderer cannot access Node.js |
| Node Integration | `nodeIntegration: false` — no `require()` in renderer |
| IPC Whitelist | Channel names validated in preload via `safeInvoke`/`safeSend`/`safeOn` |
| URL Validation | `shell.openExternal()` only allows `http:` / `https:` protocols |
| CSP | `Content-Security-Policy` header set via `session.webRequest.onHeadersReceived` |
| Web Security | `webSecurity: true` — same-origin policy enforced |
## React Hooks
| Hook | Returns | Description |
| ---------------------- | ------------------------------- | ------------------------------------------------ |
| `useIsElectron()` | `boolean` | Zero-render detection via `useSyncExternalStore` |
| `useElectronAppInfo()` | `{ appInfo, loading, error }` | App info from main process |
| `useDataDir()` | `{ dataDir, loading, error }` | User data directory |
| `useWindowControls()` | `{ minimize, maximize, close }` | Window control actions |
| `useOpenExternal()` | `{ openExternal }` | Open URLs in browser |
| `useServerControls()` | `{ restart, restarting }` | Server restart control |
| `useServerStatus(cb)` | Disposer | Listen for server status events |
| `usePortChanged(cb)` | Disposer | Listen for port change events |
## Troubleshooting
### App Won't Start
1. Check if port 20128 is available: `lsof -i :20128`
2. Check console logs for `[Electron]` prefix
3. Verify the build output exists in `.next/standalone`
### White Screen
1. Verify Next.js build exists — server readiness waits 30s max
2. Check `[Server]` and `[Server:err]` log output
3. Look for CSP violations in developer console
### Build Fails
Ensure you have build tools installed:
- Windows: Visual Studio Build Tools
- macOS: Xcode Command Line Tools
- Linux: `build-essential`, `libsecret-1-dev`
## License
MIT

BIN
electron/assets/icon.icns Normal file

Binary file not shown.

BIN
electron/assets/icon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 138 KiB

BIN
electron/assets/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 713 B

441
electron/main.js Normal file
View File

@@ -0,0 +1,441 @@
/**
* OmniRoute Electron Desktop App - Main Process
*
* This is the entry point for the Electron desktop application.
* It manages the main window, system tray, server lifecycle, and IPC communication.
*
* Code Review Fixes Applied:
* #1 Server readiness — wait for health check before loading window
* #2 Restart timeout — 5s timeout + SIGKILL to prevent hanging
* #3 changePort — stop + restart server on new port
* #4 Tray cleanup — destroy old tray before recreating
* #5 Emit server-status/port-changed IPC events
* #8 Removed dead isProduction variable
* #9 Platform-conditional titleBarStyle
* #10 stdio: pipe + stdout/stderr capture for readiness detection
* #14 Removed dead omniroute:// protocol (no handler existed)
* #15 Content Security Policy via session headers
*/
const {
app,
BrowserWindow,
ipcMain,
Tray,
Menu,
nativeImage,
shell,
session,
} = require("electron");
const path = require("path");
const { spawn } = require("child_process");
const fs = require("fs");
// ── Single Instance Lock ───────────────────────────────────
const gotTheLock = app.requestSingleInstanceLock();
if (!gotTheLock) {
app.quit();
process.exit(0);
}
app.on("second-instance", () => {
if (mainWindow) {
if (mainWindow.isMinimized()) mainWindow.restore();
mainWindow.show();
mainWindow.focus();
}
});
// ── Environment Detection ──────────────────────────────────
const isDev = process.env.NODE_ENV === "development" || !app.isPackaged;
// ── Paths ──────────────────────────────────────────────────
const APP_PATH = app.getAppPath();
const RESOURCES_PATH = !isDev ? process.resourcesPath : APP_PATH;
const NEXT_SERVER_PATH = path.join(RESOURCES_PATH, "app");
// ── State ──────────────────────────────────────────────────
let mainWindow = null;
let tray = null;
let nextServer = null;
let serverPort = 20128;
const getServerUrl = () => `http://localhost:${serverPort}`;
// ── Helper: Send IPC event to renderer (#5) ────────────────
function sendToRenderer(channel, data) {
if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.webContents.send(channel, data);
}
}
// ── Helper: Wait for server readiness (#1, #10) ────────────
async function waitForServer(url, timeoutMs = 30000) {
const start = Date.now();
while (Date.now() - start < timeoutMs) {
try {
const res = await fetch(url);
if (res.ok || res.status < 500) return true;
} catch {
/* server not ready yet */
}
await new Promise((r) => setTimeout(r, 500));
}
console.warn("[Electron] Server readiness timeout — showing window anyway");
return false;
}
// ── Helper: Wait for server process exit with timeout (#2) ─
async function waitForServerExit(proc, timeoutMs = 5000) {
if (!proc) return;
await Promise.race([
new Promise((r) => proc.once("exit", r)),
new Promise((r) =>
setTimeout(() => {
try {
proc.kill("SIGKILL");
} catch {
/* already dead */
}
r();
}, timeoutMs)
),
]);
}
// ── Content Security Policy (#15) ──────────────────────────
function setupContentSecurityPolicy() {
session.defaultSession.webRequest.onHeadersReceived((details, callback) => {
const csp = [
"default-src 'self'",
`connect-src 'self' http://localhost:* ws://localhost:* https://*.omniroute.online https://*.omniroute.dev`,
"script-src 'self' 'unsafe-inline' 'unsafe-eval'",
"style-src 'self' 'unsafe-inline' https://fonts.googleapis.com",
"font-src 'self' https://fonts.gstatic.com data:",
"img-src 'self' data: blob: https:",
"media-src 'self'",
].join("; ");
callback({
responseHeaders: {
...details.responseHeaders,
"Content-Security-Policy": [csp],
},
});
});
}
// ── Create Window ──────────────────────────────────────────
function createWindow() {
// Platform-conditional options (#9)
const platformWindowOptions =
process.platform === "darwin"
? { titleBarStyle: "hiddenInset", trafficLightPosition: { x: 16, y: 16 } }
: { titleBarStyle: "default" };
mainWindow = new BrowserWindow({
width: 1400,
height: 900,
minWidth: 1024,
minHeight: 700,
title: "OmniRoute",
icon: path.join(RESOURCES_PATH, "assets", "icon.png"),
webPreferences: {
preload: path.join(__dirname, "preload.js"),
contextIsolation: true,
nodeIntegration: false,
webSecurity: true,
},
show: false,
backgroundColor: "#0a0a0a",
...platformWindowOptions,
});
// Load the Next.js app
mainWindow.loadURL(getServerUrl());
if (isDev) {
mainWindow.webContents.openDevTools({ mode: "detach" });
}
// Show window when ready
mainWindow.once("ready-to-show", () => {
mainWindow.show();
});
// Handle external links — validate URL protocol to prevent RCE
mainWindow.webContents.setWindowOpenHandler(({ url }) => {
try {
const parsedUrl = new URL(url);
if (["http:", "https:"].includes(parsedUrl.protocol)) {
shell.openExternal(url);
} else {
console.warn("[Electron] Blocked unsafe protocol:", parsedUrl.protocol);
}
} catch {
console.error("[Electron] Blocked invalid URL:", url);
}
return { action: "deny" };
});
// Handle window close — minimize to tray
mainWindow.on("close", (event) => {
if (!app.isQuitting) {
event.preventDefault();
mainWindow.hide();
}
return false;
});
mainWindow.on("closed", () => {
mainWindow = null;
});
}
// ── System Tray ────────────────────────────────────────────
function createTray() {
// Fix #4: Destroy old tray before recreating
if (tray) {
tray.destroy();
tray = null;
}
const iconPath = path.join(RESOURCES_PATH, "assets", "tray-icon.png");
let icon;
try {
icon = nativeImage.createFromPath(iconPath);
if (icon.isEmpty()) icon = nativeImage.createEmpty();
} catch {
icon = nativeImage.createEmpty();
}
tray = new Tray(icon);
const contextMenu = Menu.buildFromTemplate([
{
label: "Open OmniRoute",
click: () => {
if (mainWindow) {
mainWindow.show();
mainWindow.focus();
}
},
},
{
label: "Open Dashboard",
click: () => shell.openExternal(getServerUrl()),
},
{ type: "separator" },
{
label: "Server Port",
submenu: [
{ label: `Port: ${serverPort}`, enabled: false },
{ type: "separator" },
{ label: "20128", click: () => changePort(20128) },
{ label: "3000", click: () => changePort(3000) },
{ label: "8080", click: () => changePort(8080) },
],
},
{ type: "separator" },
{
label: "Quit",
click: () => {
app.isQuitting = true;
app.quit();
},
},
]);
tray.setToolTip("OmniRoute");
tray.setContextMenu(contextMenu);
tray.on("double-click", () => {
if (mainWindow) {
mainWindow.show();
mainWindow.focus();
}
});
}
// ── Change Port (#3: now restarts server) ──────────────────
async function changePort(newPort) {
if (newPort === serverPort) return;
const oldPort = serverPort;
serverPort = newPort;
sendToRenderer("server-status", { status: "restarting", port: newPort });
// Stop current server and wait for exit
const serverToStop = nextServer;
stopNextServer();
await waitForServerExit(serverToStop);
// Start server on new port
startNextServer();
await waitForServer(getServerUrl());
// Reload window and update tray
if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.loadURL(getServerUrl());
}
createTray();
sendToRenderer("port-changed", serverPort);
sendToRenderer("server-status", { status: "running", port: serverPort });
console.log(`[Electron] Port changed: ${oldPort}${serverPort}`);
}
// ── Server Lifecycle (#1, #5, #10) ─────────────────────────
function startNextServer() {
if (isDev) {
console.log("[Electron] Dev mode — connect to existing Next.js server");
sendToRenderer("server-status", { status: "running", port: serverPort });
return;
}
const serverScript = path.join(NEXT_SERVER_PATH, "server.js");
if (!fs.existsSync(serverScript)) {
console.error("[Electron] Server script not found:", serverScript);
sendToRenderer("server-status", { status: "error", port: serverPort });
return;
}
console.log("[Electron] Starting Next.js server on port", serverPort);
sendToRenderer("server-status", { status: "starting", port: serverPort });
// Fix #10: Use pipe instead of inherit for logging & readiness detection
nextServer = spawn("node", [serverScript], {
cwd: NEXT_SERVER_PATH,
env: {
...process.env,
PORT: String(serverPort),
NODE_ENV: "production",
},
stdio: "pipe",
});
// Capture server output for logging
nextServer.stdout?.on("data", (data) => {
const text = data.toString();
process.stdout.write(`[Server] ${text}`);
// Detect server ready
if (text.includes("Ready") || text.includes("started") || text.includes("listening")) {
sendToRenderer("server-status", { status: "running", port: serverPort });
}
});
nextServer.stderr?.on("data", (data) => {
process.stderr.write(`[Server:err] ${data}`);
});
nextServer.on("error", (err) => {
console.error("[Electron] Failed to start server:", err);
sendToRenderer("server-status", { status: "error", port: serverPort });
});
nextServer.on("exit", (code) => {
console.log("[Electron] Server exited with code:", code);
sendToRenderer("server-status", { status: "stopped", port: serverPort });
nextServer = null;
});
}
function stopNextServer() {
if (nextServer) {
nextServer.kill("SIGTERM");
nextServer = null;
}
}
// ── IPC Handlers ───────────────────────────────────────────
function setupIpcHandlers() {
ipcMain.handle("get-app-info", () => ({
name: app.getName(),
version: app.getVersion(),
platform: process.platform,
isDev,
port: serverPort,
}));
ipcMain.handle("open-external", (_event, url) => {
try {
const parsedUrl = new URL(url);
if (["http:", "https:"].includes(parsedUrl.protocol)) {
shell.openExternal(url);
}
} catch {
console.error("[Electron] Blocked invalid URL:", url);
}
});
ipcMain.handle("get-data-dir", () => app.getPath("userData"));
// Fix #2: Add timeout to restart
ipcMain.handle("restart-server", async () => {
const serverToStop = nextServer;
stopNextServer();
await waitForServerExit(serverToStop);
startNextServer();
await waitForServer(getServerUrl());
return { success: true };
});
// Window controls
ipcMain.on("window-minimize", () => mainWindow?.minimize());
ipcMain.on("window-maximize", () => {
if (mainWindow) {
mainWindow.isMaximized() ? mainWindow.unmaximize() : mainWindow.maximize();
}
});
ipcMain.on("window-close", () => mainWindow?.close());
}
// ── App Lifecycle ──────────────────────────────────────────
app.whenReady().then(async () => {
// Fix #15: Set up CSP before any content loads
setupContentSecurityPolicy();
// Fix #1: Start server and WAIT for readiness before showing window
startNextServer();
if (!isDev) {
await waitForServer(getServerUrl());
}
createWindow();
createTray();
setupIpcHandlers();
// macOS: recreate window when dock icon clicked
app.on("activate", () => {
if (BrowserWindow.getAllWindows().length === 0) {
createWindow();
} else if (mainWindow) {
mainWindow.show();
}
});
});
// Quit when all windows closed (except macOS)
app.on("window-all-closed", () => {
if (process.platform !== "darwin") {
app.quit();
}
});
// Clean up before quit
app.on("before-quit", () => {
app.isQuitting = true;
stopNextServer();
});
// Global error handlers
process.on("uncaughtException", (error) => {
console.error("[Electron] Uncaught Exception:", error);
});
process.on("unhandledRejection", (reason) => {
console.error("[Electron] Unhandled Rejection:", reason);
});

124
electron/package.json Normal file
View File

@@ -0,0 +1,124 @@
{
"name": "omniroute-desktop",
"version": "1.6.9",
"description": "OmniRoute Desktop Application",
"main": "main.js",
"author": "OmniRoute Team",
"license": "MIT",
"homepage": "https://omniroute.online",
"scripts": {
"start": "electron .",
"dev": "electron . --no-sandbox",
"build": "electron-builder",
"build:win": "electron-builder --win",
"build:mac": "electron-builder --mac",
"build:linux": "electron-builder --linux",
"pack": "electron-builder --dir"
},
"dependencies": {},
"devDependencies": {
"electron": "^40.6.1",
"electron-builder": "^25.1.8"
},
"build": {
"appId": "online.omniroute.desktop",
"productName": "OmniRoute",
"copyright": "Copyright © 2025 OmniRoute",
"directories": {
"output": "dist-electron",
"buildResources": "assets"
},
"files": [
"main.js",
"preload.js",
"package.json"
],
"extraResources": [
{
"from": "../.next/standalone",
"to": "app",
"filter": [
"**/*"
]
},
{
"from": "../.next/static",
"to": "app/.next/static",
"filter": [
"**/*"
]
},
{
"from": "../public",
"to": "app/public",
"filter": [
"**/*"
]
}
],
"win": {
"target": [
{
"target": "nsis",
"arch": [
"x64"
]
},
{
"target": "portable",
"arch": [
"x64"
]
}
],
"icon": "assets/icon.ico"
},
"mac": {
"target": [
{
"target": "dmg",
"arch": [
"x64",
"arm64"
]
}
],
"icon": "assets/icon.icns",
"category": "public.app-category.productivity"
},
"linux": {
"target": [
{
"target": "AppImage",
"arch": [
"x64"
]
}
],
"icon": "assets/icons",
"category": "Utility"
},
"nsis": {
"oneClick": false,
"allowToChangeInstallationDirectory": true,
"createDesktopShortcut": true,
"createStartMenuShortcut": true,
"installerIcon": "assets/icon.ico",
"uninstallerIcon": "assets/icon.ico"
},
"dmg": {
"contents": [
{
"x": 130,
"y": 220
},
{
"x": 410,
"y": 220,
"type": "link",
"path": "/Applications"
}
]
}
}
}

65
electron/preload.js Normal file
View File

@@ -0,0 +1,65 @@
/**
* OmniRoute Electron Desktop App - Preload Script
*
* Secure bridge between renderer (Next.js) and main process (Electron).
* Uses contextIsolation: true for maximum security.
*
* Code Review Fixes Applied:
* #6 Listener accumulation — return disposer functions instead of using removeAllListeners
* #16 Simplified channel validation — generic wrapper reduces boilerplate
*/
const { contextBridge, ipcRenderer } = require("electron");
// ── Channel Whitelist ──────────────────────────────────────
const VALID_CHANNELS = {
invoke: ["get-app-info", "open-external", "get-data-dir", "restart-server"],
send: ["window-minimize", "window-maximize", "window-close"],
receive: ["server-status", "port-changed"],
};
// ── Fix #16: Generic IPC wrappers ──────────────────────────
function safeInvoke(channel, ...args) {
if (!VALID_CHANNELS.invoke.includes(channel)) {
return Promise.reject(new Error(`Blocked IPC invoke: ${channel}`));
}
return ipcRenderer.invoke(channel, ...args);
}
function safeSend(channel, ...args) {
if (VALID_CHANNELS.send.includes(channel)) {
ipcRenderer.send(channel, ...args);
}
}
// Fix #6: Return disposer function for proper listener cleanup
function safeOn(channel, callback) {
if (!VALID_CHANNELS.receive.includes(channel)) return () => {};
const handler = (_event, data) => callback(data);
ipcRenderer.on(channel, handler);
// Return a disposer — caller removes only THIS specific listener
return () => ipcRenderer.removeListener(channel, handler);
}
// ── Expose API to Renderer ─────────────────────────────────
contextBridge.exposeInMainWorld("electronAPI", {
// ── Invoke (async, returns Promise) ──────────────────────
getAppInfo: () => safeInvoke("get-app-info"),
openExternal: (url) => safeInvoke("open-external", url),
getDataDir: () => safeInvoke("get-data-dir"),
restartServer: () => safeInvoke("restart-server"),
// ── Send (fire-and-forget) ───────────────────────────────
minimizeWindow: () => safeSend("window-minimize"),
maximizeWindow: () => safeSend("window-maximize"),
closeWindow: () => safeSend("window-close"),
// ── Receive (event listeners) ────────────────────────────
// Fix #6: Returns a disposer function for precise cleanup
onServerStatus: (callback) => safeOn("server-status", callback),
onPortChanged: (callback) => safeOn("port-changed", callback),
// ── Static Properties ────────────────────────────────────
isElectron: true,
platform: process.platform,
});

51
electron/types.d.ts vendored Normal file
View File

@@ -0,0 +1,51 @@
/**
* OmniRoute Electron Types
*
* TypeScript definitions for the Electron API exposed to the renderer process.
*
* Updated to reflect:
* - Fix #6: onServerStatus/onPortChanged return disposer functions
* - Removed removeServerStatusListener/removePortChangedListener (replaced by disposers)
*/
export interface AppInfo {
name: string;
version: string;
platform: "win32" | "darwin" | "linux";
isDev: boolean;
port: number;
}
export interface ServerStatus {
status: "starting" | "running" | "stopped" | "restarting" | "error";
port: number;
}
export interface ElectronAPI {
// ── Invoke (async) ─────────────────────────────────────
getAppInfo(): Promise<AppInfo>;
openExternal(url: string): Promise<void>;
getDataDir(): Promise<string>;
restartServer(): Promise<{ success: boolean }>;
// ── Send (fire-and-forget) ─────────────────────────────
minimizeWindow(): void;
maximizeWindow(): void;
closeWindow(): void;
// ── Receive (returns disposer for cleanup) ─────────────
onServerStatus(callback: (data: ServerStatus) => void): () => void;
onPortChanged(callback: (port: number) => void): () => void;
// ── Static Properties ──────────────────────────────────
isElectron: boolean;
platform: "win32" | "darwin" | "linux";
}
declare global {
interface Window {
electronAPI: ElectronAPI;
}
}
export {};

View File

@@ -70,6 +70,40 @@ export const AUDIO_TRANSCRIPTION_PROVIDERS: Record<string, AudioProvider> = {
{ id: "universal-2", name: "Universal 2" },
],
},
nvidia: {
id: "nvidia",
baseUrl: "https://integrate.api.nvidia.com/v1/audio/transcriptions",
authType: "apikey",
authHeader: "bearer",
format: "nvidia-asr",
models: [
{ id: "nvidia/parakeet-ctc-1.1b-asr", name: "Parakeet CTC 1.1B" },
],
},
huggingface: {
id: "huggingface",
baseUrl: "https://api-inference.huggingface.co/models",
authType: "apikey",
authHeader: "bearer",
format: "huggingface-asr",
models: [
{ id: "openai/whisper-large-v3", name: "Whisper Large v3 (HF)" },
{ id: "openai/whisper-small", name: "Whisper Small (HF)" },
],
},
qwen: {
id: "qwen",
baseUrl: "http://localhost:8000/v1/audio/transcriptions",
authType: "none",
authHeader: "none",
format: "openai",
models: [
{ id: "qwen3-asr", name: "Qwen3 ASR" },
],
},
};
export const AUDIO_SPEECH_PROVIDERS: Record<string, AudioProvider> = {
@@ -106,6 +140,75 @@ export const AUDIO_SPEECH_PROVIDERS: Record<string, AudioProvider> = {
{ id: "aura-stella-en", name: "Aura Stella (EN)" },
],
},
nvidia: {
id: "nvidia",
baseUrl: "https://integrate.api.nvidia.com/v1/audio/speech",
authType: "apikey",
authHeader: "bearer",
format: "nvidia-tts",
models: [
{ id: "nvidia/fastpitch", name: "FastPitch" },
{ id: "nvidia/tacotron2", name: "Tacotron2" },
],
},
elevenlabs: {
id: "elevenlabs",
baseUrl: "https://api.elevenlabs.io/v1/text-to-speech",
authType: "apikey",
authHeader: "xi-api-key",
format: "elevenlabs",
models: [
{ id: "eleven_multilingual_v2", name: "Eleven Multilingual v2" },
{ id: "eleven_turbo_v2_5", name: "Eleven Turbo v2.5" },
],
},
huggingface: {
id: "huggingface",
baseUrl: "https://api-inference.huggingface.co/models",
authType: "apikey",
authHeader: "bearer",
format: "huggingface-tts",
models: [
{ id: "facebook/mms-tts-eng", name: "MMS TTS English" },
{ id: "microsoft/speecht5_tts", name: "SpeechT5 TTS" },
],
},
coqui: {
id: "coqui",
baseUrl: "http://localhost:5002/api/tts",
authType: "none",
authHeader: "none",
format: "coqui",
models: [
{ id: "tts_models/en/ljspeech/tacotron2-DDC", name: "Tacotron2 DDC (LJSpeech)" },
],
},
tortoise: {
id: "tortoise",
baseUrl: "http://localhost:5000/api/tts",
authType: "none",
authHeader: "none",
format: "tortoise",
models: [
{ id: "tortoise-v2", name: "Tortoise v2" },
],
},
qwen: {
id: "qwen",
baseUrl: "http://localhost:8000/v1/audio/speech",
authType: "none",
authHeader: "none",
format: "openai",
models: [
{ id: "qwen3-tts", name: "Qwen3 TTS" },
],
},
};
/**

View File

@@ -55,7 +55,7 @@ When you are running with \`approval_policy == on-request\`, and sandboxing enab
- You are about to take a potentially destructive action such as an \`rm\` or \`git reset\` that the user did not explicitly ask for
- (for all of these, you should weigh alternative paths that do not require approval)
When \`sandbox_mode\` is set to read-only, you'll need to request approval for any command that isn't a read.
When \`sandbox_mode\` is set to read-only, you'll need to request approval for each command that isn't a read.
You will be told what filesystem sandboxing, network sandboxing, and approval mode are active in a developer or user message. If you are not told about this, assume that you are running with workspace-write, network sandboxing enabled, and approval on-failure.
@@ -68,7 +68,7 @@ When requesting approval to execute a command that will require escalated privil
## Special user requests
- If the user makes a simple request (such as asking for the time) which you can fulfill by running a terminal command (such as \`date\`), you should do so.
- If the user asks for a "review", default to a code review mindset: prioritise identifying bugs, risks, behavioural regressions, and missing tests. Findings must be the primary focus of the response - keep summaries or overviews brief and only after enumerating the issues. Present findings first (ordered by severity with file/line references), follow with open questions or assumptions, and offer a change-summary only as a secondary detail. If no findings are discovered, state that explicitly and mention any residual risks or testing gaps.
- If the user asks for a "review", default to a code review mindset: prioritise identifying bugs, risks, behavioural regressions, and missing tests. Findings must be the primary focus of the response - keep summaries or overviews brief and only after enumerating the issues. Present findings first (ordered by severity with file/line references), follow with open questions or assumptions, and offer a change-summary only as a secondary detail. If no findings are discovered, state that explicitly and mention residual risks or testing gaps.
## Frontend tasks
When doing frontend design tasks, avoid collapsing into "AI slop" or safe, average-looking layouts.

View File

@@ -108,6 +108,32 @@ export const IMAGE_PROVIDERS = {
],
supportedSizes: ["1024x1024"],
},
sdwebui: {
id: "sdwebui",
baseUrl: "http://localhost:7860/sdapi/v1/txt2img",
authType: "none",
authHeader: "none",
format: "sdwebui",
models: [
{ id: "stable-diffusion-v1-5", name: "Stable Diffusion v1.5" },
{ id: "sdxl-base-1.0", name: "SDXL Base 1.0" },
],
supportedSizes: ["512x512", "768x768", "1024x1024"],
},
comfyui: {
id: "comfyui",
baseUrl: "http://localhost:8188",
authType: "none",
authHeader: "none",
format: "comfyui",
models: [
{ id: "flux-dev", name: "FLUX Dev" },
{ id: "sdxl", name: "SDXL" },
],
supportedSizes: ["512x512", "768x768", "1024x1024"],
},
};
/**
@@ -131,7 +157,7 @@ export function parseImageModel(modelStr) {
}
}
// No provider prefix — try to find the model in any provider
// No provider prefix — try to find the model in every provider
for (const [providerId, config] of Object.entries(IMAGE_PROVIDERS)) {
if (config.models.some((m) => m.id === modelStr)) {
return { provider: providerId, model: modelStr };

View File

@@ -0,0 +1,57 @@
/**
* Music Generation Provider Registry
*
* Defines providers that support the /v1/music/generations endpoint.
* Currently supports local providers (ComfyUI with audio models).
*/
import { parseModelFromRegistry, getAllModelsFromRegistry } from "./registryUtils.ts";
interface MusicModel {
id: string;
name: string;
}
interface MusicProvider {
id: string;
baseUrl: string;
authType: string;
authHeader: string;
format: string;
models: MusicModel[];
}
export const MUSIC_PROVIDERS: Record<string, MusicProvider> = {
comfyui: {
id: "comfyui",
baseUrl: "http://localhost:8188",
authType: "none",
authHeader: "none",
format: "comfyui",
models: [
{ id: "stable-audio-open", name: "Stable Audio Open" },
{ id: "musicgen-medium", name: "MusicGen Medium" },
],
},
};
/**
* Get music provider config by ID
*/
export function getMusicProvider(providerId: string): MusicProvider | null {
return MUSIC_PROVIDERS[providerId] || null;
}
/**
* Parse music model string (format: "provider/model" or just "model")
*/
export function parseMusicModel(modelStr: string | null) {
return parseModelFromRegistry(modelStr, MUSIC_PROVIDERS);
}
/**
* Get all music models as a flat list
*/
export function getAllMusicModels() {
return getAllModelsFromRegistry(MUSIC_PROVIDERS);
}

View File

@@ -1,4 +1,4 @@
import { generateModels, generateAliasMap } from "./providerRegistry.ts";
import { generateModels, generateAliasMap, type RegistryModel } from "./providerRegistry.ts";
// Provider models - Generated from providerRegistry.js (single source of truth)
export const PROVIDER_MODELS = generateModels();
@@ -7,37 +7,41 @@ export const PROVIDER_MODELS = generateModels();
export const PROVIDER_ID_TO_ALIAS = generateAliasMap();
// Helper functions
export function getProviderModels(aliasOrId) {
export function getProviderModels(aliasOrId: string): RegistryModel[] {
return PROVIDER_MODELS[aliasOrId] || [];
}
export function getDefaultModel(aliasOrId) {
export function getDefaultModel(aliasOrId: string): string | null {
const models = PROVIDER_MODELS[aliasOrId];
return models?.[0]?.id || null;
}
export function isValidModel(aliasOrId, modelId, passthroughProviders = new Set()) {
export function isValidModel(
aliasOrId: string,
modelId: string,
passthroughProviders = new Set<string>()
): boolean {
if (passthroughProviders.has(aliasOrId)) return true;
const models = PROVIDER_MODELS[aliasOrId];
if (!models) return false;
return models.some((m) => m.id === modelId);
}
export function findModelName(aliasOrId, modelId) {
export function findModelName(aliasOrId: string, modelId: string): string {
const models = PROVIDER_MODELS[aliasOrId];
if (!models) return modelId;
const found = models.find((m) => m.id === modelId);
return found?.name || modelId;
}
export function getModelTargetFormat(aliasOrId, modelId) {
export function getModelTargetFormat(aliasOrId: string, modelId: string): string | null {
const models = PROVIDER_MODELS[aliasOrId];
if (!models) return null;
const found = models.find((m) => m.id === modelId);
return found?.targetFormat || null;
}
export function getModelsByProviderId(providerId) {
export function getModelsByProviderId(providerId: string): RegistryModel[] {
const alias = PROVIDER_ID_TO_ALIAS[providerId] || providerId;
return PROVIDER_MODELS[alias] || [];
}

View File

@@ -49,6 +49,21 @@ export interface RegistryEntry {
passthroughModels?: boolean;
}
interface LegacyProvider {
format: string;
baseUrl?: string;
baseUrls?: string[];
responsesBaseUrl?: string;
headers?: Record<string, string>;
clientId?: string;
clientSecret?: string;
tokenUrl?: string;
refreshUrl?: string;
authUrl?: string;
chatPath?: string;
clientVersion?: string;
}
// ── Registry ──────────────────────────────────────────────────────────────
export const REGISTRY: Record<string, RegistryEntry> = {
@@ -108,10 +123,13 @@ export const REGISTRY: Record<string, RegistryEntry> = {
clientIdEnv: "GEMINI_OAUTH_CLIENT_ID",
clientIdDefault: "681255809395-oo8ft2oprdrnp9e3aqf6av3hmdib135j.apps.googleusercontent.com",
clientSecretEnv: "GEMINI_OAUTH_CLIENT_SECRET",
clientSecretDefault: "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",
clientSecretDefault: "",
},
models: [
{ id: "gemini-3-pro-preview", name: "Gemini 3 Pro Preview" },
{ id: "gemini-3.1-pro", name: "Gemini 3.1 Pro" },
{ id: "gemini-3.1-flash", name: "Gemini 3.1 Flash" },
{ id: "gemini-3-pro-preview", name: "Gemini 3.0 Pro Preview" },
{ id: "gemini-3-flash-preview", name: "Gemini 3.0 Flash Preview" },
{ id: "gemini-2.5-pro", name: "Gemini 2.5 Pro" },
{ id: "gemini-2.5-flash", name: "Gemini 2.5 Flash" },
{ id: "gemini-2.5-flash-lite", name: "Gemini 2.5 Flash Lite" },
@@ -134,11 +152,13 @@ export const REGISTRY: Record<string, RegistryEntry> = {
clientIdEnv: "GEMINI_CLI_OAUTH_CLIENT_ID",
clientIdDefault: "681255809395-oo8ft2oprdrnp9e3aqf6av3hmdib135j.apps.googleusercontent.com",
clientSecretEnv: "GEMINI_CLI_OAUTH_CLIENT_SECRET",
clientSecretDefault: "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",
clientSecretDefault: "",
},
models: [
{ id: "gemini-3-flash-preview", name: "Gemini 3 Flash Preview" },
{ id: "gemini-3-pro-preview", name: "Gemini 3 Pro Preview" },
{ id: "gemini-3.1-pro", name: "Gemini 3.1 Pro" },
{ id: "gemini-3.1-flash", name: "Gemini 3.1 Flash" },
{ id: "gemini-3-flash-preview", name: "Gemini 3.0 Flash Preview" },
{ id: "gemini-3-pro-preview", name: "Gemini 3.0 Pro Preview" },
{ id: "gemini-2.5-pro", name: "Gemini 2.5 Pro" },
{ id: "gemini-2.5-flash", name: "Gemini 2.5 Flash" },
{ id: "gemini-2.5-flash-lite", name: "Gemini 2.5 Flash Lite" },
@@ -223,7 +243,7 @@ export const REGISTRY: Record<string, RegistryEntry> = {
clientIdEnv: "IFLOW_OAUTH_CLIENT_ID",
clientIdDefault: "10009311001",
clientSecretEnv: "IFLOW_OAUTH_CLIENT_SECRET",
clientSecretDefault: "4Z3YjXycVsQvyGF1etiNlIBB4RsqSDtW",
clientSecretDefault: "",
tokenUrl: "https://iflow.cn/oauth/token",
authUrl: "https://iflow.cn/oauth",
},
@@ -261,14 +281,15 @@ export const REGISTRY: Record<string, RegistryEntry> = {
clientIdEnv: "ANTIGRAVITY_OAUTH_CLIENT_ID",
clientIdDefault: "1071006060591-tmhssin2h21lcre235vtolojh4g403ep.apps.googleusercontent.com",
clientSecretEnv: "ANTIGRAVITY_OAUTH_CLIENT_SECRET",
clientSecretDefault: "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",
clientSecretDefault: "",
},
models: [
{ id: "claude-opus-4-6-thinking", name: "Claude Opus 4.6 Thinking" },
{ id: "claude-sonnet-4-6", name: "Claude Sonnet 4.6" },
{ id: "gemini-3.1-pro-high", name: "Gemini 3.1 Pro High" },
{ id: "gemini-3.1-pro-low", name: "Gemini 3.1 Pro Low" },
{ id: "gemini-3-flash", name: "Gemini 3 Flash" },
{ id: "gemini-3.1-flash", name: "Gemini 3.1 Flash" },
{ id: "gemini-3-flash", name: "Gemini 3.0 Flash" },
{ id: "gpt-oss-120b-medium", name: "GPT OSS 120B Medium" },
],
},
@@ -635,6 +656,25 @@ export const REGISTRY: Record<string, RegistryEntry> = {
],
},
blackbox: {
id: "blackbox",
alias: "bb",
format: "openai",
executor: "default",
baseUrl: "https://api.blackbox.ai/v1/chat/completions",
modelsUrl: "https://api.blackbox.ai/v1/models",
authType: "apikey",
authHeader: "bearer",
models: [
{ id: "gpt-4o", name: "GPT-4o" },
{ id: "gemini-2.5-flash", name: "Gemini 2.5 Flash" },
{ id: "claude-sonnet-4", name: "Claude Sonnet 4" },
{ id: "deepseek-v3", name: "DeepSeek V3" },
{ id: "blackboxai", name: "Blackbox AI" },
{ id: "blackboxai-pro", name: "Blackbox AI Pro" },
],
},
xai: {
id: "xai",
alias: "xai",
@@ -819,10 +859,10 @@ export const REGISTRY: Record<string, RegistryEntry> = {
// ── Generator Functions ───────────────────────────────────────────────────
/** Generate legacy PROVIDERS object shape for constants.js backward compatibility */
export function generateLegacyProviders(): Record<string, any> {
const providers: Record<string, any> = {};
export function generateLegacyProviders(): Record<string, LegacyProvider> {
const providers: Record<string, LegacyProvider> = {};
for (const [id, entry] of Object.entries(REGISTRY)) {
const p: Record<string, any> = { format: entry.format };
const p: LegacyProvider = { format: entry.format };
// URL(s)
if (entry.baseUrls) {
@@ -892,7 +932,7 @@ export function generateAliasMap(): Record<string, string> {
// ── Registry Lookup Helpers ───────────────────────────────────────────────
const _byAlias = new Map();
const _byAlias = new Map<string, RegistryEntry>();
for (const entry of Object.values(REGISTRY)) {
if (entry.alias && entry.alias !== entry.id) {
_byAlias.set(entry.alias, entry);

View File

@@ -0,0 +1,99 @@
/**
* Shared Registry Utilities
*
* Common interfaces and helpers used by all provider registries
* (audio, image, video, music). Extracts duplicated patterns into
* reusable functions.
*/
export interface BaseModel {
id: string;
name: string;
}
export interface BaseProvider<M extends BaseModel = BaseModel> {
id: string;
baseUrl: string;
authType: string; // "apikey" | "oauth" | "none"
authHeader: string; // "bearer" | "token" | "xi-api-key" | "x-api-key" | "none"
format?: string;
models: M[];
}
/**
* Parse a "provider/model" string against a registry.
* Supports both "provider/model" prefix and bare "model" lookup.
*/
export function parseModelFromRegistry<P extends BaseProvider>(
modelStr: string | null,
registry: Record<string, P>
): { provider: string | null; model: string | null } {
if (!modelStr) return { provider: null, model: null };
// Try each provider prefix
for (const [providerId] of Object.entries(registry)) {
if (modelStr.startsWith(providerId + "/")) {
return { provider: providerId, model: modelStr.slice(providerId.length + 1) };
}
}
// No provider prefix — try to find the model in every provider
for (const [providerId, config] of Object.entries(registry)) {
if (config.models.some((m) => m.id === modelStr)) {
return { provider: providerId, model: modelStr };
}
}
return { provider: null, model: modelStr };
}
/**
* Flatten all models from a registry into a list with provider info.
* Optionally merge extra fields per provider via the `extra` callback.
*/
export function getAllModelsFromRegistry<P extends BaseProvider>(
registry: Record<string, P>,
extra?: (providerId: string, config: P) => Record<string, unknown>
): Array<{ id: string; name: string; provider: string } & Record<string, unknown>> {
const models: Array<{ id: string; name: string; provider: string } & Record<string, unknown>> =
[];
for (const [providerId, config] of Object.entries(registry)) {
const extraFields = extra ? extra(providerId, config) : {};
for (const model of config.models) {
models.push({
id: `${providerId}/${model.id}`,
name: model.name,
provider: providerId,
...extraFields,
});
}
}
return models;
}
/**
* Build auth headers for a provider.
* Handles bearer, token, xi-api-key, x-api-key, and none.
*/
export function buildAuthHeaders(
provider: BaseProvider,
token: string | null
): Record<string, string> {
if (provider.authType === "none" || provider.authHeader === "none" || !token) {
return {};
}
switch (provider.authHeader) {
case "token":
return { Authorization: `Token ${token}` };
case "xi-api-key":
return { "xi-api-key": token };
case "x-api-key":
return { "x-api-key": token };
case "bearer":
default:
return { Authorization: `Bearer ${token}` };
}
}

View File

@@ -0,0 +1,68 @@
/**
* Video Generation Provider Registry
*
* Defines providers that support the /v1/videos/generations endpoint.
* Currently supports local providers (ComfyUI, SD WebUI with AnimateDiff).
*/
import { parseModelFromRegistry, getAllModelsFromRegistry } from "./registryUtils.ts";
interface VideoModel {
id: string;
name: string;
}
interface VideoProvider {
id: string;
baseUrl: string;
authType: string;
authHeader: string;
format: string;
models: VideoModel[];
}
export const VIDEO_PROVIDERS: Record<string, VideoProvider> = {
comfyui: {
id: "comfyui",
baseUrl: "http://localhost:8188",
authType: "none",
authHeader: "none",
format: "comfyui",
models: [
{ id: "animatediff", name: "AnimateDiff" },
{ id: "svd-xt", name: "Stable Video Diffusion XT" },
],
},
sdwebui: {
id: "sdwebui",
baseUrl: "http://localhost:7860",
authType: "none",
authHeader: "none",
format: "sdwebui-video",
models: [
{ id: "animatediff-webui", name: "AnimateDiff (WebUI)" },
],
},
};
/**
* Get video provider config by ID
*/
export function getVideoProvider(providerId: string): VideoProvider | null {
return VIDEO_PROVIDERS[providerId] || null;
}
/**
* Parse video model string (format: "provider/model" or just "model")
*/
export function parseVideoModel(modelStr: string | null) {
return parseModelFromRegistry(modelStr, VIDEO_PROVIDERS);
}
/**
* Get all video models as a flat list
*/
export function getAllVideoModels() {
return getAllModelsFromRegistry(VIDEO_PROVIDERS);
}

View File

@@ -5,7 +5,7 @@ import { PROVIDERS, OAUTH_ENDPOINTS, HTTP_STATUS } from "../config/constants.ts"
const MAX_RETRY_AFTER_MS = 10000;
/**
* Strip any provider prefix (e.g. "antigravity/model" → "model").
* Strip provider prefixes (e.g. "antigravity/model" → "model").
* Ensures the model name sent to the upstream API never contains a routing prefix.
*/
function cleanModelName(model: string): string {
@@ -36,7 +36,18 @@ export class AntigravityExecutor extends BaseExecutor {
}
transformRequest(model, body, stream, credentials) {
const projectId = credentials?.projectId || this.generateProjectId();
const bodyProjectId = body?.project;
const credentialsProjectId = credentials?.projectId;
const hasExplicitProject = !!(bodyProjectId || credentialsProjectId);
const projectId = bodyProjectId || credentialsProjectId || this.generateProjectId();
if (!hasExplicitProject) {
console.warn(
`[Antigravity] ⚠️ No projectId provided via body or credentials — using generated fallback "${projectId}". ` +
`This may cause 404 errors if the account has no active GCP project. ` +
`Ensure the OAuth token includes a valid project or the request includes a project field.`
);
}
// Fix contents for Claude models via Antigravity
const normalizedContents =

View File

@@ -1,15 +1,75 @@
import { HTTP_STATUS, FETCH_TIMEOUT_MS } from "../config/constants.ts";
type JsonRecord = Record<string, unknown>;
export type ProviderConfig = {
id?: string;
baseUrl?: string;
baseUrls?: string[];
responsesBaseUrl?: string;
chatPath?: string;
clientVersion?: string;
clientId?: string;
clientSecret?: string;
tokenUrl?: string;
refreshUrl?: string;
authUrl?: string;
headers?: Record<string, string>;
};
export type ProviderCredentials = {
accessToken?: string;
refreshToken?: string;
apiKey?: string;
expiresAt?: string;
providerSpecificData?: JsonRecord;
};
export type ExecutorLog = {
debug?: (tag: string, message: string) => void;
info?: (tag: string, message: string) => void;
warn?: (tag: string, message: string) => void;
error?: (tag: string, message: string) => void;
};
export type ExecuteInput = {
model: string;
body: unknown;
stream: boolean;
credentials: ProviderCredentials;
signal?: AbortSignal | null;
log?: ExecutorLog | null;
};
function mergeAbortSignals(primary: AbortSignal, secondary: AbortSignal): AbortSignal {
const controller = new AbortController();
const abortBoth = () => {
if (!controller.signal.aborted) {
controller.abort();
}
};
if (primary.aborted || secondary.aborted) {
abortBoth();
return controller.signal;
}
primary.addEventListener("abort", abortBoth, { once: true });
secondary.addEventListener("abort", abortBoth, { once: true });
return controller.signal;
}
/**
* BaseExecutor - Base class for provider executors.
* Implements the Strategy pattern: subclasses override specific methods
* (buildUrl, buildHeaders, transformRequest, etc.) for each provider.
*/
export class BaseExecutor {
provider: any;
config: any;
provider: string;
config: ProviderConfig;
constructor(provider: any, config: any) {
constructor(provider: string, config: ProviderConfig) {
this.provider = provider;
this.config = config;
}
@@ -26,9 +86,19 @@ export class BaseExecutor {
return this.getBaseUrls().length || 1;
}
buildUrl(model, stream, urlIndex = 0, credentials = null) {
buildUrl(
model: string,
stream: boolean,
urlIndex = 0,
credentials: ProviderCredentials | null = null
) {
void model;
void stream;
if (this.provider?.startsWith?.("openai-compatible-")) {
const baseUrl = credentials?.providerSpecificData?.baseUrl || "https://api.openai.com/v1";
const baseUrl =
typeof credentials?.providerSpecificData?.baseUrl === "string"
? credentials.providerSpecificData.baseUrl
: "https://api.openai.com/v1";
const normalized = baseUrl.replace(/\/$/, "");
const path = this.provider.includes("responses") ? "/responses" : "/chat/completions";
return `${normalized}${path}`;
@@ -37,12 +107,25 @@ export class BaseExecutor {
return baseUrls[urlIndex] || baseUrls[0] || this.config.baseUrl;
}
buildHeaders(credentials, stream = true) {
const headers = {
buildHeaders(credentials: ProviderCredentials, stream = true): Record<string, string> {
const headers: Record<string, string> = {
"Content-Type": "application/json",
...this.config.headers,
};
// Allow per-provider User-Agent override via environment variable.
// Example: CLAUDE_USER_AGENT="my-agent/2.0" overrides the default for the Claude provider.
const providerId = this.config?.id || this.provider;
if (providerId) {
const envKey = `${providerId.toUpperCase().replace(/[^A-Z0-9]/g, "_")}_USER_AGENT`;
const envUA = process.env[envKey]?.trim();
if (envUA) {
// Override both common casing variants
headers["User-Agent"] = envUA;
if (headers["user-agent"]) headers["user-agent"] = envUA;
}
}
if (credentials.accessToken) {
headers["Authorization"] = `Bearer ${credentials.accessToken}`;
} else if (credentials.apiKey) {
@@ -57,32 +140,42 @@ export class BaseExecutor {
}
// Override in subclass for provider-specific transformations
transformRequest(model, body, stream, credentials) {
transformRequest(
model: string,
body: unknown,
stream: boolean,
credentials: ProviderCredentials
): unknown {
void model;
void stream;
void credentials;
return body;
}
shouldRetry(status, urlIndex) {
shouldRetry(status: number, urlIndex: number) {
return status === HTTP_STATUS.RATE_LIMITED && urlIndex + 1 < this.getFallbackCount();
}
// Override in subclass for provider-specific refresh
async refreshCredentials(credentials, log) {
async refreshCredentials(credentials: ProviderCredentials, log: ExecutorLog | null) {
void credentials;
void log;
return null;
}
needsRefresh(credentials) {
needsRefresh(credentials: ProviderCredentials) {
if (!credentials.expiresAt) return false;
const expiresAtMs = new Date(credentials.expiresAt).getTime();
return expiresAtMs - Date.now() < 5 * 60 * 1000;
}
parseError(response, bodyText) {
parseError(response: Response, bodyText: string) {
return { status: response.status, message: bodyText || `HTTP ${response.status}` };
}
async execute({ model, body, stream, credentials, signal, log }) {
async execute({ model, body, stream, credentials, signal, log }: ExecuteInput) {
const fallbackCount = this.getFallbackCount();
let lastError = null;
let lastError: unknown = null;
let lastStatus = 0;
for (let urlIndex = 0; urlIndex < fallbackCount; urlIndex++) {
@@ -96,10 +189,10 @@ export class BaseExecutor {
const timeoutSignal = !stream ? AbortSignal.timeout(FETCH_TIMEOUT_MS) : null;
const combinedSignal =
signal && timeoutSignal
? AbortSignal.any([signal, timeoutSignal])
? mergeAbortSignals(signal, timeoutSignal)
: signal || timeoutSignal;
const fetchOptions: Record<string, any> = {
const fetchOptions: RequestInit = {
method: "POST",
headers,
body: JSON.stringify(transformedBody),
@@ -117,15 +210,16 @@ export class BaseExecutor {
return { response, url, headers, transformedBody };
} catch (error) {
// Distinguish timeout errors from other abort errors
if (error.name === "TimeoutError") {
const err = error instanceof Error ? error : new Error(String(error));
if (err.name === "TimeoutError") {
log?.warn?.("TIMEOUT", `Fetch timeout after ${FETCH_TIMEOUT_MS}ms on ${url}`);
}
lastError = error;
lastError = err;
if (urlIndex + 1 < fallbackCount) {
log?.debug?.("RETRY", `Error on ${url}, trying fallback ${urlIndex + 1}`);
continue;
}
throw error;
throw err;
}
}

View File

@@ -1,4 +1,4 @@
declare var EdgeRuntime: any;
declare const EdgeRuntime: string | undefined;
/**
* CursorExecutor — Handles communication with the Cursor IDE API.
*
@@ -121,13 +121,19 @@ function createErrorResponse(jsonError) {
);
}
type CursorHttpResponse = {
status: number;
headers: Record<string, unknown>;
body: Buffer;
};
export class CursorExecutor extends BaseExecutor {
constructor() {
super("cursor", PROVIDERS.cursor);
}
buildUrl() {
return `${this.config.baseUrl}${this.config.chatPath}`;
return `${this.config.baseUrl}${this.config.chatPath || ""}`;
}
// Jyh cipher checksum for Cursor API authentication
@@ -217,27 +223,37 @@ export class CursorExecutor extends BaseExecutor {
return generateCursorBody(messages, model, tools, reasoningEffort);
}
async makeFetchRequest(url, headers, body, signal) {
async makeFetchRequest(
url: string,
headers: Record<string, string>,
body: Uint8Array,
signal?: AbortSignal
): Promise<CursorHttpResponse> {
const response = await fetch(url, {
method: "POST",
headers,
body,
body: body as unknown as BodyInit,
signal,
});
return {
status: response.status,
headers: Object.fromEntries((response.headers as any).entries()),
headers: Object.fromEntries(response.headers.entries()),
body: Buffer.from(await response.arrayBuffer()),
};
}
makeHttp2Request(url, headers, body, signal) {
makeHttp2Request(
url: string,
headers: Record<string, string>,
body: Uint8Array,
signal?: AbortSignal
): Promise<CursorHttpResponse> {
if (!http2) {
throw new Error("http2 module not available");
}
return new Promise((resolve, reject) => {
return new Promise<CursorHttpResponse>((resolve, reject) => {
const urlObj = new URL(url);
const client = http2.connect(`https://${urlObj.host}`);
const chunks = [];
@@ -262,7 +278,10 @@ export class CursorExecutor extends BaseExecutor {
req.on("end", () => {
client.close();
resolve({
status: responseHeaders[":status"],
status:
typeof responseHeaders[":status"] === "number"
? responseHeaders[":status"]
: Number(responseHeaders[":status"] || HTTP_STATUS.SERVER_ERROR),
headers: responseHeaders,
body: Buffer.concat(chunks),
});
@@ -291,7 +310,7 @@ export class CursorExecutor extends BaseExecutor {
const transformedBody = this.transformRequest(model, body, stream, credentials);
try {
const response: any = http2
const response: CursorHttpResponse = http2
? await this.makeHttp2Request(url, headers, transformedBody, signal)
: await this.makeFetchRequest(url, headers, transformedBody, signal);
@@ -459,7 +478,8 @@ export class CursorExecutor extends BaseExecutor {
console.log(`[CURSOR BUFFER] Final toolCalls count: ${toolCalls.length}`);
const message: Record<string, any> = { role: "assistant",
const message: Record<string, unknown> = {
role: "assistant",
content: totalContent || null,
};

View File

@@ -74,7 +74,7 @@ export class DefaultExecutor extends BaseExecutor {
/**
* For compatible providers, ensure the model name sent upstream
* is the clean model name without any internal routing prefix.
* is the clean model name without internal routing prefixes.
* e.g. "openapi-chat-anti/claude-opus-4-6-thinking" → "claude-opus-4-6-thinking"
*/
transformRequest(model, body, stream, credentials) {

View File

@@ -2,6 +2,11 @@ import crypto from "crypto";
import { BaseExecutor } from "./base.ts";
import { PROVIDERS } from "../config/constants.ts";
type IFlowCredentials = {
apiKey?: string;
accessToken?: string;
};
/**
* IFlowExecutor - Executor for iFlow API with HMAC-SHA256 signature.
*
@@ -41,7 +46,7 @@ export class IFlowExecutor extends BaseExecutor {
* Build headers with iFlow-specific HMAC-SHA256 signature.
* Includes session-id, x-iflow-timestamp, and x-iflow-signature.
*/
buildHeaders(credentials: any, stream = true) {
buildHeaders(credentials: IFlowCredentials, stream = true) {
// Generate session ID and timestamp
const sessionID = `session-${crypto.randomUUID()}`;
const timestamp = Date.now();
@@ -82,14 +87,26 @@ export class IFlowExecutor extends BaseExecutor {
/**
* Build URL for iFlow API — uses baseUrl directly.
*/
buildUrl(model: string, stream: boolean, urlIndex = 0, credentials: any = null) {
buildUrl(
model: string,
stream: boolean,
urlIndex = 0,
credentials: IFlowCredentials | null = null
) {
void model;
void stream;
void urlIndex;
void credentials;
return this.config.baseUrl;
}
/**
* Transform request body (passthrough for iFlow).
*/
transformRequest(model: string, body: any, stream: boolean, credentials: any) {
transformRequest(model: string, body: unknown, stream: boolean, credentials: IFlowCredentials) {
void model;
void stream;
void credentials;
return body;
}
}

View File

@@ -1,8 +1,39 @@
import { BaseExecutor } from "./base.ts";
import {
BaseExecutor,
type ExecuteInput,
type ExecutorLog,
type ProviderCredentials,
} from "./base.ts";
import { PROVIDERS } from "../config/constants.ts";
import { v4 as uuidv4 } from "uuid";
import { refreshKiroToken } from "../services/tokenRefresh.ts";
type JsonRecord = Record<string, unknown>;
type UsageSummary = {
prompt_tokens: number;
completion_tokens: number;
total_tokens: number;
};
type KiroStreamState = {
endDetected: boolean;
finishEmitted: boolean;
hasToolCalls: boolean;
toolCallIndex: number;
seenToolIds: Map<string, number>;
totalContentLength?: number;
contextUsagePercentage?: number;
hasContextUsage?: boolean;
hasMeteringEvent?: boolean;
usage?: UsageSummary;
};
type EventFrame = {
headers: Record<string, string>;
payload: JsonRecord | null;
};
// ── CRC32 lookup table (IEEE polynomial, no dependency) ──
const CRC32_TABLE = new Uint32Array(256);
for (let i = 0; i < 256; i++) {
@@ -13,7 +44,7 @@ for (let i = 0; i < 256; i++) {
CRC32_TABLE[i] = c >>> 0;
}
function crc32(buf) {
function crc32(buf: Uint8Array) {
let crc = 0xffffffff;
for (let i = 0; i < buf.length; i++) {
crc = CRC32_TABLE[(crc ^ buf[i]) & 0xff] ^ (crc >>> 8);
@@ -30,7 +61,8 @@ export class KiroExecutor extends BaseExecutor {
super("kiro", PROVIDERS.kiro);
}
buildHeaders(credentials, stream = true) {
buildHeaders(credentials: ProviderCredentials, stream = true) {
void stream;
const headers = {
...this.config.headers,
"Amz-Sdk-Request": "attempt=1; max=3",
@@ -44,14 +76,17 @@ export class KiroExecutor extends BaseExecutor {
return headers;
}
transformRequest(model, body, stream, credentials) {
transformRequest(model: string, body: unknown, stream: boolean, credentials: unknown): unknown {
void model;
void stream;
void credentials;
return body;
}
/**
* Custom execute for Kiro - handles AWS EventStream binary response
*/
async execute({ model, body, stream, credentials, signal, log }) {
async execute({ model, body, stream, credentials, signal, log }: ExecuteInput) {
const url = this.buildUrl(model, stream, 0);
const headers = this.buildHeaders(credentials, stream);
const transformedBody = this.transformRequest(model, body, stream, credentials);
@@ -78,12 +113,13 @@ export class KiroExecutor extends BaseExecutor {
* Transform AWS EventStream binary response to SSE text stream
* Using TransformStream instead of ReadableStream.pull() to avoid Workers timeout
*/
transformEventStreamToSSE(response, model) {
transformEventStreamToSSE(response: Response, model: string) {
let buffer = new Uint8Array(0);
let chunkIndex = 0;
const responseId = `chatcmpl-${Date.now()}`;
const created = Math.floor(Date.now() / 1000);
const state: Record<string, any> = { endDetected: false,
const state: KiroStreamState = {
endDetected: false,
finishEmitted: false,
hasToolCalls: false,
toolCallIndex: 0,
@@ -121,11 +157,14 @@ export class KiroExecutor extends BaseExecutor {
if (!state.contextUsagePercentage) state.contextUsagePercentage = 0;
// Handle assistantResponseEvent
if (eventType === "assistantResponseEvent" && event.payload?.content) {
const content = event.payload.content;
if (eventType === "assistantResponseEvent") {
const content = typeof event.payload?.content === "string" ? event.payload.content : "";
if (!content) {
continue;
}
state.totalContentLength += content.length;
const chunk: Record<string, any> = {
const chunk: JsonRecord = {
id: responseId,
object: "chat.completion.chunk",
created,
@@ -144,7 +183,7 @@ export class KiroExecutor extends BaseExecutor {
// Handle codeEvent
if (eventType === "codeEvent" && event.payload?.content) {
const chunk: Record<string, any> = {
const chunk: JsonRecord = {
id: responseId,
object: "chat.completion.chunk",
created,
@@ -256,7 +295,7 @@ export class KiroExecutor extends BaseExecutor {
// Handle messageStopEvent
if (eventType === "messageStopEvent") {
const chunk: Record<string, any> = {
const chunk: JsonRecord = {
id: responseId,
object: "chat.completion.chunk",
created,
@@ -274,8 +313,15 @@ export class KiroExecutor extends BaseExecutor {
}
// Handle contextUsageEvent to extract contextUsagePercentage
if (eventType === "contextUsageEvent" && event.payload?.contextUsagePercentage) {
state.contextUsagePercentage = event.payload.contextUsagePercentage;
if (eventType === "contextUsageEvent") {
const contextUsage =
typeof event.payload?.contextUsagePercentage === "number"
? event.payload.contextUsagePercentage
: 0;
if (contextUsage <= 0) {
continue;
}
state.contextUsagePercentage = contextUsage;
// Mark that we received context usage event
state.hasContextUsage = true;
}
@@ -290,8 +336,14 @@ export class KiroExecutor extends BaseExecutor {
// Extract usage data from metricsEvent payload
const metrics = event.payload?.metricsEvent || event.payload;
if (metrics && typeof metrics === "object") {
const inputTokens = metrics.inputTokens || 0;
const outputTokens = metrics.outputTokens || 0;
const inputTokens =
typeof (metrics as JsonRecord).inputTokens === "number"
? ((metrics as JsonRecord).inputTokens as number)
: 0;
const outputTokens =
typeof (metrics as JsonRecord).outputTokens === "number"
? ((metrics as JsonRecord).outputTokens as number)
: 0;
if (inputTokens > 0 || outputTokens > 0) {
state.usage = {
@@ -329,7 +381,7 @@ export class KiroExecutor extends BaseExecutor {
};
}
const finishChunk: Record<string, any> = {
const finishChunk: JsonRecord = {
id: responseId,
object: "chat.completion.chunk",
created,
@@ -398,7 +450,7 @@ export class KiroExecutor extends BaseExecutor {
});
}
async refreshCredentials(credentials, log) {
async refreshCredentials(credentials: ProviderCredentials, log?: ExecutorLog | null) {
if (!credentials.refreshToken) return null;
try {
@@ -411,7 +463,8 @@ export class KiroExecutor extends BaseExecutor {
return result;
} catch (error) {
log?.error?.("TOKEN", `Kiro refresh error: ${error.message}`);
const err = error instanceof Error ? error : new Error(String(error));
log?.error?.("TOKEN", `Kiro refresh error: ${err.message}`);
return null;
}
}
@@ -420,7 +473,7 @@ export class KiroExecutor extends BaseExecutor {
/**
* Parse AWS EventStream frame
*/
function parseEventFrame(data) {
function parseEventFrame(data: Uint8Array): EventFrame | null {
try {
const view = new DataView(data.buffer, data.byteOffset);
const totalLength = view.getUint32(0, false);
@@ -447,7 +500,7 @@ function parseEventFrame(data) {
return null;
}
// Parse headers
const headers = {};
const headers: Record<string, string> = {};
let offset = 12; // After prelude
const headerEnd = 12 + headersLength;
@@ -480,7 +533,7 @@ function parseEventFrame(data) {
const payloadStart = 12 + headersLength;
const payloadEnd = data.length - 4; // Exclude message CRC
let payload = null;
let payload: JsonRecord | null = null;
if (payloadEnd > payloadStart) {
const payloadStr = new TextDecoder().decode(data.slice(payloadStart, payloadEnd));
@@ -492,9 +545,10 @@ function parseEventFrame(data) {
try {
payload = JSON.parse(payloadStr);
} catch (parseError) {
const err = parseError instanceof Error ? parseError : new Error(String(parseError));
// Log parse error for debugging
console.warn(
`[Kiro] Failed to parse payload: ${parseError.message} | payload: ${payloadStr.substring(0, 100)}`
`[Kiro] Failed to parse payload: ${err.message} | payload: ${payloadStr.substring(0, 100)}`
);
payload = { raw: payloadStr };
}
@@ -502,7 +556,8 @@ function parseEventFrame(data) {
return { headers, payload };
} catch (err) {
console.warn(`[Kiro] Frame parse error: ${err.message}`);
const error = err instanceof Error ? err : new Error(String(err));
console.warn(`[Kiro] Frame parse error: ${error.message}`);
return null;
}
}

View File

@@ -6,22 +6,54 @@ import { getCorsOrigin } from "../utils/cors.ts";
* Returns audio binary stream.
*
* Supported provider formats:
* - OpenAI: standard JSON → audio stream proxy
* - OpenAI / Qwen3 (openai-compatible): standard JSON → audio stream proxy
* - Hyperbolic: POST { text } → { audio: base64 }
* - Deepgram: POST { text } with model via query param, Token auth
* - ElevenLabs: POST { text, model_id } to /v1/text-to-speech/{voice_id}
* - Nvidia NIM: POST { input: { text }, voice, model } → audio binary
* - HuggingFace Inference: POST { inputs: text } to /models/{model_id}
* - Coqui TTS: POST { text, speaker_id } → WAV audio (local, no auth)
* - Tortoise TTS: POST { text, voice } → audio binary (local, no auth)
*/
import { getSpeechProvider, parseSpeechModel } from "../config/audioRegistry.ts";
import { buildAuthHeaders } from "../config/registryUtils.ts";
import { errorResponse } from "../utils/error.ts";
/**
* Build auth header for a speech provider
* Return a CORS error response from an upstream fetch failure
*/
function buildAuthHeader(providerConfig, token) {
if (providerConfig.authHeader === "token") {
return { Authorization: `Token ${token}` };
}
return { Authorization: `Bearer ${token}` };
function upstreamErrorResponse(res, errText) {
return new Response(errText, {
status: res.status,
headers: {
"Content-Type": "application/json",
"Access-Control-Allow-Origin": getCorsOrigin(),
},
});
}
/**
* Return a CORS audio stream response
*/
function audioStreamResponse(res, defaultContentType = "audio/mpeg") {
const contentType = res.headers.get("content-type") || defaultContentType;
return new Response(res.body, {
status: 200,
headers: {
"Content-Type": contentType,
"Access-Control-Allow-Origin": getCorsOrigin(),
"Transfer-Encoding": "chunked",
},
});
}
/**
* Validate a path segment to prevent path traversal / SSRF.
* Returns true if safe, false if it contains traversal sequences.
*/
function isValidPathSegment(segment: string): boolean {
return !segment.includes("..") && !segment.includes("//");
}
/**
@@ -32,20 +64,13 @@ async function handleHyperbolicSpeech(providerConfig, body, token) {
method: "POST",
headers: {
"Content-Type": "application/json",
...buildAuthHeader(providerConfig, token),
...buildAuthHeaders(providerConfig, token),
},
body: JSON.stringify({ text: body.input }),
});
if (!res.ok) {
const errText = await res.text();
return new Response(errText, {
status: res.status,
headers: {
"Content-Type": "application/json",
"Access-Control-Allow-Origin": getCorsOrigin(),
},
});
return upstreamErrorResponse(res, await res.text());
}
const data = await res.json();
@@ -72,29 +97,153 @@ async function handleDeepgramSpeech(providerConfig, body, modelId, token) {
method: "POST",
headers: {
"Content-Type": "application/json",
...buildAuthHeader(providerConfig, token),
...buildAuthHeaders(providerConfig, token),
},
body: JSON.stringify({ text: body.input }),
});
if (!res.ok) {
const errText = await res.text();
return new Response(errText, {
status: res.status,
headers: {
"Content-Type": "application/json",
"Access-Control-Allow-Origin": getCorsOrigin(),
},
});
return upstreamErrorResponse(res, await res.text());
}
const contentType = res.headers.get("content-type") || "audio/mpeg";
return audioStreamResponse(res);
}
/**
* Handle ElevenLabs TTS
* POST {baseUrl}/{voice_id} with { text, model_id }
* voice_id is mapped from the OpenAI `voice` parameter
*/
async function handleElevenLabsSpeech(providerConfig, body, modelId, token) {
// ElevenLabs uses voice_id in URL path; default to "21m00Tcm4TlvDq8ikWAM" (Rachel)
const voiceId = body.voice || "21m00Tcm4TlvDq8ikWAM";
if (!isValidPathSegment(voiceId)) {
return errorResponse(400, "Invalid voice ID");
}
const url = `${providerConfig.baseUrl}/${voiceId}`;
const res = await fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/json",
...buildAuthHeaders(providerConfig, token),
},
body: JSON.stringify({
text: body.input,
model_id: modelId,
}),
});
if (!res.ok) {
return upstreamErrorResponse(res, await res.text());
}
return audioStreamResponse(res);
}
/**
* Handle Nvidia NIM TTS
* POST with { input: { text }, voice, model } → audio binary
*/
async function handleNvidiaTtsSpeech(providerConfig, body, modelId, token) {
const res = await fetch(providerConfig.baseUrl, {
method: "POST",
headers: {
"Content-Type": "application/json",
...buildAuthHeaders(providerConfig, token),
},
body: JSON.stringify({
input: { text: body.input },
voice: body.voice || "default",
model: modelId,
}),
});
if (!res.ok) {
return upstreamErrorResponse(res, await res.text());
}
return audioStreamResponse(res, "audio/wav");
}
/**
* Handle HuggingFace Inference TTS
* POST {baseUrl}/{model_id} with { inputs: text } → audio binary
*/
async function handleHuggingFaceTtsSpeech(providerConfig, body, modelId, token) {
if (!isValidPathSegment(modelId)) {
return errorResponse(400, "Invalid model ID");
}
const url = `${providerConfig.baseUrl}/${modelId}`;
const res = await fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/json",
...buildAuthHeaders(providerConfig, token),
},
body: JSON.stringify({ inputs: body.input }),
});
if (!res.ok) {
return upstreamErrorResponse(res, await res.text());
}
return audioStreamResponse(res, "audio/wav");
}
/**
* Handle Coqui TTS (local, no auth)
* POST {baseUrl} with { text, speaker_id } → WAV audio
*/
async function handleCoquiSpeech(providerConfig, body) {
const res = await fetch(providerConfig.baseUrl, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
text: body.input,
speaker_id: body.voice || undefined,
}),
});
if (!res.ok) {
return upstreamErrorResponse(res, await res.text());
}
const contentType = res.headers.get("content-type") || "audio/wav";
return new Response(res.body, {
status: 200,
headers: {
"Content-Type": contentType,
"Access-Control-Allow-Origin": getCorsOrigin(),
},
});
}
/**
* Handle Tortoise TTS (local, no auth)
* POST {baseUrl} with { text, voice } → audio binary
*/
async function handleTortoiseSpeech(providerConfig, body) {
const res = await fetch(providerConfig.baseUrl, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
text: body.input,
voice: body.voice || "random",
}),
});
if (!res.ok) {
return upstreamErrorResponse(res, await res.text());
}
const contentType = res.headers.get("content-type") || "audio/wav";
return new Response(res.body, {
status: 200,
headers: {
"Content-Type": contentType,
"Access-Control-Allow-Origin": getCorsOrigin(),
"Transfer-Encoding": "chunked",
},
});
}
@@ -107,7 +256,7 @@ async function handleDeepgramSpeech(providerConfig, body, modelId, token) {
* @param {Object} options.credentials - Provider credentials { apiKey }
* @returns {Response}
*/
/** @returns {Promise<any>} */
/** @returns {Promise<unknown>} */
export async function handleAudioSpeech({ body, credentials }) {
if (!body.model) {
return errorResponse(400, "model is required");
@@ -122,12 +271,14 @@ export async function handleAudioSpeech({ body, credentials }) {
if (!providerConfig) {
return errorResponse(
400,
`No speech provider found for model "${body.model}". Available: openai, hyperbolic, deepgram`
`No speech provider found for model "${body.model}". Available: openai, hyperbolic, deepgram, nvidia, elevenlabs, huggingface, coqui, tortoise, qwen`
);
}
const token = credentials?.apiKey || credentials?.accessToken;
if (!token) {
// Skip credential check for local providers (authType: "none")
const token =
providerConfig.authType === "none" ? null : credentials?.apiKey || credentials?.accessToken;
if (providerConfig.authType !== "none" && !token) {
return errorResponse(401, `No credentials for speech provider: ${providerId}`);
}
@@ -141,12 +292,32 @@ export async function handleAudioSpeech({ body, credentials }) {
return handleDeepgramSpeech(providerConfig, body, modelId, token);
}
// Default: OpenAI-compatible JSON → audio stream proxy
if (providerConfig.format === "elevenlabs") {
return handleElevenLabsSpeech(providerConfig, body, modelId, token);
}
if (providerConfig.format === "nvidia-tts") {
return handleNvidiaTtsSpeech(providerConfig, body, modelId, token);
}
if (providerConfig.format === "huggingface-tts") {
return handleHuggingFaceTtsSpeech(providerConfig, body, modelId, token);
}
if (providerConfig.format === "coqui") {
return handleCoquiSpeech(providerConfig, body);
}
if (providerConfig.format === "tortoise") {
return handleTortoiseSpeech(providerConfig, body);
}
// Default: OpenAI-compatible JSON → audio stream proxy (also used by Qwen3)
const res = await fetch(providerConfig.baseUrl, {
method: "POST",
headers: {
"Content-Type": "application/json",
...buildAuthHeader(providerConfig, token),
...buildAuthHeaders(providerConfig, token),
},
body: JSON.stringify({
model: modelId,
@@ -158,26 +329,10 @@ export async function handleAudioSpeech({ body, credentials }) {
});
if (!res.ok) {
const errText = await res.text();
return new Response(errText, {
status: res.status,
headers: {
"Content-Type": "application/json",
"Access-Control-Allow-Origin": getCorsOrigin(),
},
});
return upstreamErrorResponse(res, await res.text());
}
// Stream audio response back to client
const contentType = res.headers.get("content-type") || "audio/mpeg";
return new Response(res.body, {
status: 200,
headers: {
"Content-Type": contentType,
"Access-Control-Allow-Origin": getCorsOrigin(),
"Transfer-Encoding": "chunked",
},
});
return audioStreamResponse(res);
} catch (err) {
return errorResponse(500, `Speech request failed: ${err.message}`);
}

View File

@@ -6,22 +6,44 @@ import { getCorsOrigin } from "../utils/cors.ts";
* Proxies multipart/form-data to upstream providers.
*
* Supported provider formats:
* - OpenAI/Groq: standard multipart form-data proxy
* - OpenAI/Groq/Qwen3: standard multipart form-data proxy
* - Deepgram: raw binary audio POST with model via query param
* - AssemblyAI: async workflow (upload → submit → poll)
* - Nvidia NIM: multipart POST, transform response to { text }
* - HuggingFace Inference: POST raw binary to /models/{model_id}
*/
import { getTranscriptionProvider, parseTranscriptionModel } from "../config/audioRegistry.ts";
import { buildAuthHeaders } from "../config/registryUtils.ts";
import { errorResponse } from "../utils/error.ts";
type TranscriptionCredentials = {
apiKey?: string;
accessToken?: string;
};
/**
* Build auth header for a transcription provider
* Return a CORS error response from an upstream fetch failure
*/
function buildAuthHeader(providerConfig, token) {
if (providerConfig.authHeader === "token") {
return { Authorization: `Token ${token}` };
}
return { Authorization: `Bearer ${token}` };
function upstreamErrorResponse(res, errText) {
return new Response(errText, {
status: res.status,
headers: {
"Content-Type": "application/json",
"Access-Control-Allow-Origin": getCorsOrigin(),
},
});
}
/**
* Validate a path segment to prevent path traversal / SSRF.
*/
function isValidPathSegment(segment: string): boolean {
return !segment.includes("..") && !segment.includes("//");
}
function getUploadedFileName(file: Blob & { name?: unknown }): string {
return typeof file.name === "string" && file.name.length > 0 ? file.name : "audio.wav";
}
/**
@@ -37,21 +59,14 @@ async function handleDeepgramTranscription(providerConfig, file, modelId, token)
const res = await fetch(url.toString(), {
method: "POST",
headers: {
...buildAuthHeader(providerConfig, token),
...buildAuthHeaders(providerConfig, token),
"Content-Type": file.type || "audio/wav",
},
body: arrayBuffer,
});
if (!res.ok) {
const errText = await res.text();
return new Response(errText, {
status: res.status,
headers: {
"Content-Type": "application/json",
"Access-Control-Allow-Origin": getCorsOrigin(),
},
});
return upstreamErrorResponse(res, await res.text());
}
const data = await res.json();
@@ -65,7 +80,7 @@ async function handleDeepgramTranscription(providerConfig, file, modelId, token)
* Handle AssemblyAI transcription (async: upload file → submit → poll)
*/
async function handleAssemblyAITranscription(providerConfig, file, modelId, token) {
const authHeaders = buildAuthHeader(providerConfig, token);
const authHeaders = buildAuthHeaders(providerConfig, token);
// Step 1: Upload the audio file
const arrayBuffer = await file.arrayBuffer();
@@ -79,14 +94,7 @@ async function handleAssemblyAITranscription(providerConfig, file, modelId, toke
});
if (!uploadRes.ok) {
const errText = await uploadRes.text();
return new Response(errText, {
status: uploadRes.status,
headers: {
"Content-Type": "application/json",
"Access-Control-Allow-Origin": getCorsOrigin(),
},
});
return upstreamErrorResponse(uploadRes, await uploadRes.text());
}
const { upload_url } = await uploadRes.json();
@@ -106,14 +114,7 @@ async function handleAssemblyAITranscription(providerConfig, file, modelId, toke
});
if (!submitRes.ok) {
const errText = await submitRes.text();
return new Response(errText, {
status: submitRes.status,
headers: {
"Content-Type": "application/json",
"Access-Control-Allow-Origin": getCorsOrigin(),
},
});
return upstreamErrorResponse(submitRes, await submitRes.text());
}
const { id: transcriptId } = await submitRes.json();
@@ -146,6 +147,63 @@ async function handleAssemblyAITranscription(providerConfig, file, modelId, toke
return errorResponse(504, "AssemblyAI transcription timed out after 120s");
}
/**
* Handle Nvidia NIM transcription
* Multipart POST, transform response to { text }
*/
async function handleNvidiaTranscription(providerConfig, file, modelId, token) {
const upstreamForm = new FormData();
upstreamForm.append("file", file, getUploadedFileName(file));
upstreamForm.append("model", modelId);
const res = await fetch(providerConfig.baseUrl, {
method: "POST",
headers: buildAuthHeaders(providerConfig, token),
body: upstreamForm,
});
if (!res.ok) {
return upstreamErrorResponse(res, await res.text());
}
const data = await res.json();
// Normalize to { text } — Nvidia may return { text } directly or nested
const text = data.text || data.transcript || "";
return Response.json({ text }, { headers: { "Access-Control-Allow-Origin": getCorsOrigin() } });
}
/**
* Handle HuggingFace Inference transcription
* POST raw binary audio to {baseUrl}/{model_id}, returns { text }
*/
async function handleHuggingFaceTranscription(providerConfig, file, modelId, token) {
if (!isValidPathSegment(modelId)) {
return errorResponse(400, "Invalid model ID");
}
const url = `${providerConfig.baseUrl}/${modelId}`;
const arrayBuffer = await file.arrayBuffer();
const res = await fetch(url, {
method: "POST",
headers: {
...buildAuthHeaders(providerConfig, token),
"Content-Type": file.type || "audio/wav",
},
body: arrayBuffer,
});
if (!res.ok) {
return upstreamErrorResponse(res, await res.text());
}
const data = await res.json();
// HuggingFace returns { text } directly
const text = data.text || "";
return Response.json({ text }, { headers: { "Access-Control-Allow-Origin": getCorsOrigin() } });
}
/**
* Handle audio transcription request
*
@@ -154,17 +212,23 @@ async function handleAssemblyAITranscription(providerConfig, file, modelId, toke
* @param {Object} options.credentials - Provider credentials { apiKey }
* @returns {Response}
*/
/** @returns {Promise<any>} */
export async function handleAudioTranscription({ formData, credentials }) {
export async function handleAudioTranscription({
formData,
credentials,
}: {
formData: FormData;
credentials?: TranscriptionCredentials | null;
}): Promise<Response> {
const model = formData.get("model");
if (!model) {
if (typeof model !== "string" || !model) {
return errorResponse(400, "model is required");
}
const file = formData.get("file");
if (!file) {
const fileEntry = formData.get("file");
if (!(fileEntry instanceof Blob)) {
return errorResponse(400, "file is required");
}
const file = fileEntry as Blob & { name?: unknown };
const { provider: providerId, model: modelId } = parseTranscriptionModel(model);
const providerConfig = providerId ? getTranscriptionProvider(providerId) : null;
@@ -172,12 +236,14 @@ export async function handleAudioTranscription({ formData, credentials }) {
if (!providerConfig) {
return errorResponse(
400,
`No transcription provider found for model "${model}". Available: openai, groq, deepgram, assemblyai`
`No transcription provider found for model "${model}". Available: openai, groq, deepgram, assemblyai, nvidia, huggingface, qwen`
);
}
const token = credentials?.apiKey || credentials?.accessToken;
if (!token) {
// Skip credential check for local providers (authType: "none")
const token =
providerConfig.authType === "none" ? null : credentials?.apiKey || credentials?.accessToken;
if (providerConfig.authType !== "none" && !token) {
return errorResponse(401, `No credentials for transcription provider: ${providerId}`);
}
@@ -190,13 +256,17 @@ export async function handleAudioTranscription({ formData, credentials }) {
return handleAssemblyAITranscription(providerConfig, file, modelId, token);
}
// Default: OpenAI/Groq-compatible multipart proxy
if (providerConfig.format === "nvidia-asr") {
return handleNvidiaTranscription(providerConfig, file, modelId, token);
}
if (providerConfig.format === "huggingface-asr") {
return handleHuggingFaceTranscription(providerConfig, file, modelId, token);
}
// Default: OpenAI/Groq/Qwen3-compatible multipart proxy
const upstreamForm = new FormData();
upstreamForm.append(
"file",
/** @type {Blob} */ file,
/** @type {any} */ file.name || "audio.wav"
);
upstreamForm.append("file", file, getUploadedFileName(file));
upstreamForm.append("model", modelId);
// Forward optional parameters
@@ -216,19 +286,12 @@ export async function handleAudioTranscription({ formData, credentials }) {
try {
const res = await fetch(providerConfig.baseUrl, {
method: "POST",
headers: buildAuthHeader(providerConfig, token),
headers: buildAuthHeaders(providerConfig, token),
body: upstreamForm,
});
if (!res.ok) {
const errText = await res.text();
return new Response(errText, {
status: res.status,
headers: {
"Content-Type": "application/json",
"Access-Control-Allow-Origin": getCorsOrigin(),
},
});
return upstreamErrorResponse(res, await res.text());
}
const data = await res.text();
@@ -239,6 +302,7 @@ export async function handleAudioTranscription({ formData, credentials }) {
headers: { "Content-Type": contentType, "Access-Control-Allow-Origin": getCorsOrigin() },
});
} catch (err) {
return errorResponse(500, `Transcription request failed: ${err.message}`);
const error = err instanceof Error ? err : new Error(String(err));
return errorResponse(500, `Transcription request failed: ${error.message}`);
}
}

View File

@@ -54,7 +54,6 @@ import { createProgressTransform, wantsProgress } from "../utils/progressTracker
* @param {string} options.connectionId - Connection ID for usage tracking
* @param {object} options.apiKeyInfo - API key metadata for usage attribution
*/
/** @param {any} options */
export async function handleChatCore({
body,
modelInfo,
@@ -135,7 +134,7 @@ export async function handleChatCore({
// Create request logger for this session: sourceFormat_targetFormat_model
const reqLogger = await createRequestLogger(sourceFormat, targetFormat, model);
// 0. Log client raw request (before any conversion)
// 0. Log client raw request (before format conversion)
if (clientRawRequest) {
reqLogger.logClientRawRequest(
clientRawRequest.endpoint,
@@ -152,11 +151,18 @@ export async function handleChatCore({
// Translate request (pass reqLogger for intermediate logging)
let translatedBody = body;
try {
// Issue #199: Disable tool name prefix when routing Claude-format requests
// to non-Claude backends (prefix causes tool name mismatches)
const claudeProviders = ["claude", "anthropic"];
if (targetFormat === FORMATS.CLAUDE && !claudeProviders.includes(provider?.toLowerCase?.())) {
translatedBody = { ...translatedBody, _disableToolPrefix: true };
}
translatedBody = translateRequest(
sourceFormat,
targetFormat,
model,
body,
translatedBody,
stream,
credentials,
provider,
@@ -203,6 +209,7 @@ export async function handleChatCore({
// Extract toolNameMap for response translation (Claude OAuth)
const toolNameMap = translatedBody._toolNameMap;
delete translatedBody._toolNameMap;
delete translatedBody._disableToolPrefix;
// Update model in body
translatedBody.model = model;
@@ -283,6 +290,7 @@ export async function handleChatCore({
comboName,
apiKeyId: apiKeyInfo?.id || null,
apiKeyName: apiKeyInfo?.name || null,
noLog: apiKeyInfo?.noLog === true,
}).catch(() => {});
if (error.name === "AbortError") {
streamController.handleError(error);
@@ -298,11 +306,14 @@ export async function handleChatCore({
providerResponse.status === HTTP_STATUS.UNAUTHORIZED ||
providerResponse.status === HTTP_STATUS.FORBIDDEN
) {
const newCredentials = await refreshWithRetry(
const newCredentials = (await refreshWithRetry(
() => executor.refreshCredentials(credentials, log),
3,
log
);
)) as null | {
accessToken?: string;
copilotToken?: string;
};
if (newCredentials?.accessToken || newCredentials?.copilotToken) {
log?.info?.("TOKEN", `${provider.toUpperCase()} | refreshed`);
@@ -363,6 +374,7 @@ export async function handleChatCore({
comboName,
apiKeyId: apiKeyInfo?.id || null,
apiKeyName: apiKeyInfo?.name || null,
noLog: apiKeyInfo?.noLog === true,
}).catch(() => {});
const errMsg = formatProviderError(new Error(message), provider, model, statusCode);
console.log(`${COLORS.red}[ERROR] ${errMsg}${COLORS.reset}`);
@@ -454,6 +466,7 @@ export async function handleChatCore({
comboName,
apiKeyId: apiKeyInfo?.id || null,
apiKeyName: apiKeyInfo?.name || null,
noLog: apiKeyInfo?.noLog === true,
}).catch(() => {});
if (usage && typeof usage === "object") {
const msg = `[${new Date().toLocaleTimeString("en-US", { hour12: false, hour: "2-digit", minute: "2-digit" })}] 📊 [USAGE] ${provider.toUpperCase()} | in=${usage?.prompt_tokens || 0} | out=${usage?.completion_tokens || 0}${connectionId ? ` | account=${connectionId.slice(0, 8)}...` : ""}`;
@@ -489,7 +502,7 @@ export async function handleChatCore({
const buffered = addBufferToUsage(translatedResponse.usage);
translatedResponse.usage = filterUsageForFormat(buffered, sourceFormat);
} else {
// Fallback: estimate usage when provider didn't return any
// Fallback: estimate usage when provider returned no usage block
const contentLength = JSON.stringify(
translatedResponse?.choices?.[0]?.message?.content || ""
).length;
@@ -556,6 +569,7 @@ export async function handleChatCore({
comboName,
apiKeyId: apiKeyInfo?.id || null,
apiKeyName: apiKeyInfo?.name || null,
noLog: apiKeyInfo?.noLog === true,
}).catch(() => {});
};

View File

@@ -52,7 +52,7 @@ export async function handleEmbedding({ body, credentials, log }) {
}
// Build upstream request
const upstreamBody: Record<string, any> = {
const upstreamBody: Record<string, unknown> = {
model: model,
input: body.input,
};

View File

@@ -17,6 +17,12 @@
import { getImageProvider, parseImageModel } from "../config/imageRegistry.ts";
import { saveCallLog } from "@/lib/usageDb";
import {
submitComfyWorkflow,
pollComfyResult,
fetchComfyOutput,
extractComfyOutputFiles,
} from "../utils/comfyuiClient.ts";
/**
* Handle image generation request
@@ -72,6 +78,14 @@ export async function handleImageGeneration({ body, credentials, log }) {
});
}
if (providerConfig.format === "sdwebui") {
return handleSDWebUIImageGeneration({ model, provider, providerConfig, body, log });
}
if (providerConfig.format === "comfyui") {
return handleComfyUIImageGeneration({ model, provider, providerConfig, body, log });
}
return handleOpenAIImageGeneration({ model, provider, providerConfig, body, credentials, log });
}
@@ -232,7 +246,7 @@ async function handleOpenAIImageGeneration({
};
// Build upstream request (OpenAI-compatible format)
const upstreamBody: Record<string, any> = {
const upstreamBody: Record<string, unknown> = {
model: model,
prompt: body.prompt,
};
@@ -560,3 +574,194 @@ async function handleNanoBananaImageGeneration({
return { success: false, status: 502, error: `Image provider error: ${err.message}` };
}
}
/**
* Handle SD WebUI image generation (local, no auth)
* POST {baseUrl} with { prompt, negative_prompt, width, height, steps }
* Response: { images: ["base64..."] }
*/
async function handleSDWebUIImageGeneration({ model, provider, providerConfig, body, log }) {
const startTime = Date.now();
const [width, height] = (body.size || "512x512").split("x").map(Number);
const upstreamBody = {
prompt: body.prompt,
negative_prompt: body.negative_prompt || "",
width: width || 512,
height: height || 512,
steps: body.steps || 20,
cfg_scale: body.cfg_scale || 7,
sampler_name: body.sampler || "Euler a",
batch_size: body.n || 1,
override_settings: {
sd_model_checkpoint: model,
},
};
if (log) {
const promptPreview = String(body.prompt ?? "").slice(0, 60);
log.info("IMAGE", `${provider}/${model} (sdwebui) | prompt: "${promptPreview}..."`);
}
try {
const response = await fetch(providerConfig.baseUrl, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(upstreamBody),
});
if (!response.ok) {
const errorText = await response.text();
if (log)
log.error("IMAGE", `${provider} error ${response.status}: ${errorText.slice(0, 200)}`);
saveCallLog({
method: "POST",
path: "/v1/images/generations",
status: response.status,
model: `${provider}/${model}`,
provider,
duration: Date.now() - startTime,
error: errorText.slice(0, 500),
}).catch(() => {});
return { success: false, status: response.status, error: errorText };
}
const data = await response.json();
// SD WebUI returns { images: ["base64...", ...] }
const images = (data.images || []).map((b64) => ({
b64_json: b64,
revised_prompt: body.prompt,
}));
saveCallLog({
method: "POST",
path: "/v1/images/generations",
status: 200,
model: `${provider}/${model}`,
provider,
duration: Date.now() - startTime,
responseBody: { images_count: images.length },
}).catch(() => {});
return {
success: true,
data: { created: Math.floor(Date.now() / 1000), data: images },
};
} catch (err) {
if (log) log.error("IMAGE", `${provider} sdwebui error: ${err.message}`);
saveCallLog({
method: "POST",
path: "/v1/images/generations",
status: 502,
model: `${provider}/${model}`,
provider,
duration: Date.now() - startTime,
error: err.message,
}).catch(() => {});
return { success: false, status: 502, error: `Image provider error: ${err.message}` };
}
}
/**
* Handle ComfyUI image generation (local, no auth)
* Submits a txt2img workflow, polls for completion, fetches output
*/
async function handleComfyUIImageGeneration({ model, provider, providerConfig, body, log }) {
const startTime = Date.now();
const [width, height] = (body.size || "1024x1024").split("x").map(Number);
// Default txt2img workflow template for ComfyUI
const workflow = {
"3": {
class_type: "KSampler",
inputs: {
seed: Math.floor(Math.random() * 2 ** 32),
steps: body.steps || 20,
cfg: body.cfg_scale || 7,
sampler_name: "euler",
scheduler: "normal",
denoise: 1,
model: ["4", 0],
positive: ["6", 0],
negative: ["7", 0],
latent_image: ["5", 0],
},
},
"4": {
class_type: "CheckpointLoaderSimple",
inputs: { ckpt_name: model },
},
"5": {
class_type: "EmptyLatentImage",
inputs: { width: width || 1024, height: height || 1024, batch_size: body.n || 1 },
},
"6": {
class_type: "CLIPTextEncode",
inputs: { text: body.prompt, clip: ["4", 1] },
},
"7": {
class_type: "CLIPTextEncode",
inputs: { text: body.negative_prompt || "", clip: ["4", 1] },
},
"8": {
class_type: "VAEDecode",
inputs: { samples: ["3", 0], vae: ["4", 2] },
},
"9": {
class_type: "SaveImage",
inputs: { filename_prefix: "omniroute", images: ["8", 0] },
},
};
if (log) {
const promptPreview = String(body.prompt ?? "").slice(0, 60);
log.info("IMAGE", `${provider}/${model} (comfyui) | prompt: "${promptPreview}..."`);
}
try {
const promptId = await submitComfyWorkflow(providerConfig.baseUrl, workflow);
const historyEntry = await pollComfyResult(providerConfig.baseUrl, promptId);
const outputFiles = extractComfyOutputFiles(historyEntry);
const images = [];
for (const file of outputFiles) {
const buffer = await fetchComfyOutput(
providerConfig.baseUrl,
file.filename,
file.subfolder,
file.type
);
const base64 = Buffer.from(buffer).toString("base64");
images.push({ b64_json: base64, revised_prompt: body.prompt });
}
saveCallLog({
method: "POST",
path: "/v1/images/generations",
status: 200,
model: `${provider}/${model}`,
provider,
duration: Date.now() - startTime,
responseBody: { images_count: images.length },
}).catch(() => {});
return {
success: true,
data: { created: Math.floor(Date.now() / 1000), data: images },
};
} catch (err) {
if (log) log.error("IMAGE", `${provider} comfyui error: ${err.message}`);
saveCallLog({
method: "POST",
path: "/v1/images/generations",
status: 502,
model: `${provider}/${model}`,
provider,
duration: Date.now() - startTime,
error: err.message,
}).catch(() => {});
return { success: false, status: 502, error: `Image provider error: ${err.message}` };
}
}

View File

@@ -16,7 +16,7 @@ import { errorResponse } from "../utils/error.ts";
* @param {Object} options.credentials - Provider credentials { apiKey }
* @returns {Response}
*/
/** @returns {Promise<any>} */
/** @returns {Promise<unknown>} */
export async function handleModeration({ body, credentials }) {
if (!body.input) {
return errorResponse(400, "input is required");

View File

@@ -0,0 +1,159 @@
/**
* Music Generation Handler
*
* Handles POST /v1/music/generations requests.
* Proxies to upstream music generation providers.
*
* Supported provider formats:
* - ComfyUI: submit audio workflow → poll → fetch output
*
* Response format (OpenAI-like):
* {
* "created": 1234567890,
* "data": [{ "b64_json": "...", "format": "wav" }]
* }
*/
import { getMusicProvider, parseMusicModel } from "../config/musicRegistry.ts";
import {
submitComfyWorkflow,
pollComfyResult,
fetchComfyOutput,
extractComfyOutputFiles,
} from "../utils/comfyuiClient.ts";
import { saveCallLog } from "@/lib/usageDb";
/**
* Handle music generation request
*/
export async function handleMusicGeneration({ body, credentials, log }) {
const { provider, model } = parseMusicModel(body.model);
if (!provider) {
return {
success: false,
status: 400,
error: `Invalid music model: ${body.model}. Use format: provider/model`,
};
}
const providerConfig = getMusicProvider(provider);
if (!providerConfig) {
return {
success: false,
status: 400,
error: `Unknown music provider: ${provider}`,
};
}
if (providerConfig.format === "comfyui") {
return handleComfyUIMusicGeneration({ model, provider, providerConfig, body, log });
}
return { success: false, status: 400, error: `Unsupported music format: ${providerConfig.format}` };
}
/**
* Handle ComfyUI music generation
* Submits an audio generation workflow (Stable Audio / MusicGen), polls, fetches output
*/
async function handleComfyUIMusicGeneration({ model, provider, providerConfig, body, log }) {
const startTime = Date.now();
const duration = body.duration || 10; // seconds
// Audio generation workflow template for ComfyUI
const workflow = {
"1": {
class_type: "CheckpointLoaderSimple",
inputs: { ckpt_name: model },
},
"2": {
class_type: "CLIPTextEncode",
inputs: { text: body.prompt, clip: ["1", 1] },
},
"3": {
class_type: "CLIPTextEncode",
inputs: { text: body.negative_prompt || "", clip: ["1", 1] },
},
"4": {
class_type: "EmptyLatentAudio",
inputs: { seconds: duration },
},
"5": {
class_type: "KSampler",
inputs: {
seed: Math.floor(Math.random() * 2 ** 32),
steps: body.steps || 100,
cfg: body.cfg_scale || 7,
sampler_name: "euler",
scheduler: "normal",
denoise: 1,
model: ["1", 0],
positive: ["2", 0],
negative: ["3", 0],
latent_image: ["4", 0],
},
},
"6": {
class_type: "VAEDecodeAudio",
inputs: { samples: ["5", 0], vae: ["1", 2] },
},
"7": {
class_type: "SaveAudio",
inputs: {
filename_prefix: "omniroute_music",
audio: ["6", 0],
},
},
};
if (log) {
const promptPreview = String(body.prompt ?? "").slice(0, 60);
log.info("MUSIC", `${provider}/${model} (comfyui) | prompt: "${promptPreview}..." | duration: ${duration}s`);
}
try {
const promptId = await submitComfyWorkflow(providerConfig.baseUrl, workflow);
const historyEntry = await pollComfyResult(providerConfig.baseUrl, promptId, 300_000);
const outputFiles = extractComfyOutputFiles(historyEntry);
const audioFiles = [];
for (const file of outputFiles) {
const buffer = await fetchComfyOutput(
providerConfig.baseUrl,
file.filename,
file.subfolder,
file.type
);
const base64 = Buffer.from(buffer).toString("base64");
audioFiles.push({ b64_json: base64, format: "wav" });
}
saveCallLog({
method: "POST",
path: "/v1/music/generations",
status: 200,
model: `${provider}/${model}`,
provider,
duration: Date.now() - startTime,
responseBody: { audio_count: audioFiles.length },
}).catch(() => {});
return {
success: true,
data: { created: Math.floor(Date.now() / 1000), data: audioFiles },
};
} catch (err) {
if (log) log.error("MUSIC", `${provider} comfyui error: ${err.message}`);
saveCallLog({
method: "POST",
path: "/v1/music/generations",
status: 502,
model: `${provider}/${model}`,
provider,
duration: Date.now() - startTime,
error: err.message,
}).catch(() => {});
return { success: false, status: 502, error: `Music provider error: ${err.message}` };
}
}

View File

@@ -70,7 +70,7 @@ function transformResponseFromProvider(providerConfig, data) {
* @param {Object} options.credentials - Provider credentials { apiKey, accessToken }
* @returns {Response}
*/
/** @returns {Promise<any>} */
/** @returns {Promise<unknown>} */
export async function handleRerank({
model,
query,

View File

@@ -9,17 +9,6 @@
* 4. Converts developer role → system for non-OpenAI providers
*/
// ── Standard OpenAI ChatCompletion fields ──────────────────────────────────
const ALLOWED_TOP_LEVEL_FIELDS = new Set([
"id",
"object",
"created",
"model",
"choices",
"usage",
"system_fingerprint",
]);
const ALLOWED_USAGE_FIELDS = new Set([
"prompt_tokens",
"completion_tokens",
@@ -28,16 +17,20 @@ const ALLOWED_USAGE_FIELDS = new Set([
"completion_tokens_details",
]);
const ALLOWED_MESSAGE_FIELDS = new Set([
"role",
"content",
"tool_calls",
"function_call",
"refusal",
"reasoning_content",
]);
type JsonRecord = Record<string, unknown>;
const ALLOWED_CHOICE_FIELDS = new Set(["index", "message", "delta", "finish_reason", "logprobs"]);
function toRecord(value: unknown): JsonRecord | null {
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
return value as JsonRecord;
}
function toString(value: unknown): string | undefined {
return typeof value === "string" ? value : undefined;
}
function toNumber(value: unknown): number | undefined {
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
}
// ── Think tag regex ────────────────────────────────────────────────────────
// Matches <think>...</think> blocks (greedy, dotAll)
@@ -81,33 +74,34 @@ export function extractThinkingFromContent(text: string): {
* Sanitize a non-streaming OpenAI ChatCompletion response.
* Strips non-standard fields and normalizes required fields.
*/
export function sanitizeOpenAIResponse(body: any): any {
if (!body || typeof body !== "object") return body;
export function sanitizeOpenAIResponse(body: unknown): unknown {
const bodyRecord = toRecord(body);
if (!bodyRecord) return body;
// Build sanitized response with only allowed top-level fields
const sanitized: Record<string, any> = {};
const sanitized: JsonRecord = {};
// Ensure required fields exist
sanitized.id = normalizeResponseId(body.id);
sanitized.object = body.object || "chat.completion";
sanitized.created = body.created || Math.floor(Date.now() / 1000);
sanitized.model = body.model || "unknown";
sanitized.id = normalizeResponseId(bodyRecord.id);
sanitized.object = toString(bodyRecord.object) || "chat.completion";
sanitized.created = toNumber(bodyRecord.created) ?? Math.floor(Date.now() / 1000);
sanitized.model = toString(bodyRecord.model) || "unknown";
// Sanitize choices
if (Array.isArray(body.choices)) {
sanitized.choices = body.choices.map((choice: any, idx: number) => sanitizeChoice(choice, idx));
if (Array.isArray(bodyRecord.choices)) {
sanitized.choices = bodyRecord.choices.map((choice, idx) => sanitizeChoice(choice, idx));
} else {
sanitized.choices = [];
}
// Sanitize usage
if (body.usage && typeof body.usage === "object") {
sanitized.usage = sanitizeUsage(body.usage);
if (bodyRecord.usage !== undefined) {
sanitized.usage = sanitizeUsage(bodyRecord.usage);
}
// Keep system_fingerprint if present (it's a valid OpenAI field)
if (body.system_fingerprint) {
sanitized.system_fingerprint = body.system_fingerprint;
if (bodyRecord.system_fingerprint) {
sanitized.system_fingerprint = bodyRecord.system_fingerprint;
}
return sanitized;
@@ -116,23 +110,32 @@ export function sanitizeOpenAIResponse(body: any): any {
/**
* Sanitize a single choice object.
*/
function sanitizeChoice(choice: any, defaultIndex: number): any {
const sanitized: Record<string, any> = {
index: choice.index ?? defaultIndex,
finish_reason: choice.finish_reason || null,
function sanitizeChoice(choice: unknown, defaultIndex: number): JsonRecord {
const choiceRecord = toRecord(choice);
const sanitized: JsonRecord = {
index: defaultIndex,
finish_reason: null,
};
// Sanitize message (non-streaming) or delta (streaming)
if (choice.message) {
sanitized.message = sanitizeMessage(choice.message);
if (choiceRecord?.index !== undefined) {
sanitized.index = choiceRecord.index;
}
if (choice.delta) {
sanitized.delta = sanitizeMessage(choice.delta);
if (choiceRecord?.finish_reason !== undefined) {
sanitized.finish_reason = choiceRecord.finish_reason;
}
// Sanitize message (non-streaming) or delta (streaming)
if (choiceRecord?.message !== undefined) {
sanitized.message = sanitizeMessage(choiceRecord.message);
}
if (choiceRecord?.delta !== undefined) {
sanitized.delta = sanitizeMessage(choiceRecord.delta);
}
// Keep logprobs if present
if (choice.logprobs !== undefined) {
sanitized.logprobs = choice.logprobs;
if (choiceRecord?.logprobs !== undefined) {
sanitized.logprobs = choiceRecord.logprobs;
}
return sanitized;
@@ -141,41 +144,42 @@ function sanitizeChoice(choice: any, defaultIndex: number): any {
/**
* Sanitize a message object, extracting <think> tags if present.
*/
function sanitizeMessage(msg: any): any {
if (!msg || typeof msg !== "object") return msg;
function sanitizeMessage(msg: unknown): unknown {
const msgRecord = toRecord(msg);
if (!msgRecord) return msg;
const sanitized: Record<string, any> = {};
const sanitized: JsonRecord = {};
// Copy only allowed fields
if (msg.role) sanitized.role = msg.role;
if (msg.refusal !== undefined) sanitized.refusal = msg.refusal;
if (msgRecord.role) sanitized.role = msgRecord.role;
if (msgRecord.refusal !== undefined) sanitized.refusal = msgRecord.refusal;
// Handle content — extract <think> tags
if (typeof msg.content === "string") {
const { content, thinking } = extractThinkingFromContent(msg.content);
if (typeof msgRecord.content === "string") {
const { content, thinking } = extractThinkingFromContent(msgRecord.content);
sanitized.content = content;
// Set reasoning_content from <think> tags (if not already set)
if (thinking && !msg.reasoning_content) {
if (thinking && !msgRecord.reasoning_content) {
sanitized.reasoning_content = thinking;
}
} else if (msg.content !== undefined) {
sanitized.content = msg.content;
} else if (msgRecord.content !== undefined) {
sanitized.content = msgRecord.content;
}
// Preserve existing reasoning_content (from providers that natively support it)
if (msg.reasoning_content && !sanitized.reasoning_content) {
sanitized.reasoning_content = msg.reasoning_content;
if (msgRecord.reasoning_content && !sanitized.reasoning_content) {
sanitized.reasoning_content = msgRecord.reasoning_content;
}
// Preserve tool_calls
if (msg.tool_calls) {
sanitized.tool_calls = msg.tool_calls;
if (msgRecord.tool_calls) {
sanitized.tool_calls = msgRecord.tool_calls;
}
// Preserve function_call (legacy)
if (msg.function_call) {
sanitized.function_call = msg.function_call;
if (msgRecord.function_call) {
sanitized.function_call = msgRecord.function_call;
}
return sanitized;
@@ -184,22 +188,25 @@ function sanitizeMessage(msg: any): any {
/**
* Sanitize usage object — keep only standard fields.
*/
function sanitizeUsage(usage: any): any {
if (!usage || typeof usage !== "object") return usage;
function sanitizeUsage(usage: unknown): unknown {
const usageRecord = toRecord(usage);
if (!usageRecord) return usage;
const sanitized: Record<string, any> = {};
const sanitized: JsonRecord = {};
for (const key of ALLOWED_USAGE_FIELDS) {
if (usage[key] !== undefined) {
sanitized[key] = usage[key];
if (usageRecord[key] !== undefined) {
sanitized[key] = usageRecord[key];
}
}
// Ensure required fields
if (sanitized.prompt_tokens === undefined) sanitized.prompt_tokens = 0;
if (sanitized.completion_tokens === undefined) sanitized.completion_tokens = 0;
if (sanitized.total_tokens === undefined) {
sanitized.total_tokens = sanitized.prompt_tokens + sanitized.completion_tokens;
}
const promptTokens = toNumber(sanitized.prompt_tokens) ?? 0;
const completionTokens = toNumber(sanitized.completion_tokens) ?? 0;
const totalTokens = toNumber(sanitized.total_tokens) ?? promptTokens + completionTokens;
sanitized.prompt_tokens = promptTokens;
sanitized.completion_tokens = completionTokens;
sanitized.total_tokens = totalTokens;
return sanitized;
}
@@ -207,7 +214,7 @@ function sanitizeUsage(usage: any): any {
/**
* Normalize response ID to use chatcmpl- prefix.
*/
function normalizeResponseId(id: any): string {
function normalizeResponseId(id: unknown): string {
if (!id || typeof id !== "string") {
return `chatcmpl-${crypto.randomUUID().replace(/-/g, "").slice(0, 29)}`;
}
@@ -221,48 +228,60 @@ function normalizeResponseId(id: any): string {
* Sanitize a streaming SSE chunk for passthrough mode.
* Lighter than full sanitization — only strips problematic extra fields.
*/
export function sanitizeStreamingChunk(parsed: any): any {
if (!parsed || typeof parsed !== "object") return parsed;
export function sanitizeStreamingChunk(parsed: unknown): unknown {
const parsedRecord = toRecord(parsed);
if (!parsedRecord) return parsed;
// Build sanitized chunk
const sanitized: Record<string, any> = {};
const sanitized: JsonRecord = {};
// Keep only standard fields
if (parsed.id !== undefined) sanitized.id = parsed.id;
sanitized.object = parsed.object || "chat.completion.chunk";
if (parsed.created !== undefined) sanitized.created = parsed.created;
if (parsed.model !== undefined) sanitized.model = parsed.model;
if (parsedRecord.id !== undefined) sanitized.id = parsedRecord.id;
sanitized.object = toString(parsedRecord.object) || "chat.completion.chunk";
if (parsedRecord.created !== undefined) sanitized.created = parsedRecord.created;
if (parsedRecord.model !== undefined) sanitized.model = parsedRecord.model;
// Sanitize choices with delta
if (Array.isArray(parsed.choices)) {
sanitized.choices = parsed.choices.map((choice: any) => {
const c: Record<string, any> = {
index: choice.index ?? 0,
};
if (choice.delta !== undefined) {
c.delta = {};
const delta = choice.delta;
if (delta.role !== undefined) c.delta.role = delta.role;
if (delta.content !== undefined) c.delta.content = delta.content;
if (delta.reasoning_content !== undefined)
c.delta.reasoning_content = delta.reasoning_content;
if (delta.tool_calls !== undefined) c.delta.tool_calls = delta.tool_calls;
if (delta.function_call !== undefined) c.delta.function_call = delta.function_call;
if (Array.isArray(parsedRecord.choices)) {
sanitized.choices = parsedRecord.choices.map((choice) => {
const c: JsonRecord = { index: 0 };
const choiceRecord = toRecord(choice);
if (!choiceRecord) return c;
c.index = toNumber(choiceRecord.index) ?? 0;
if (choiceRecord.delta !== undefined) {
const deltaRecord = toRecord(choiceRecord.delta);
if (deltaRecord) {
const delta: JsonRecord = {};
if (deltaRecord.role !== undefined) delta.role = deltaRecord.role;
if (deltaRecord.content !== undefined) delta.content = deltaRecord.content;
if (deltaRecord.reasoning_content !== undefined) {
delta.reasoning_content = deltaRecord.reasoning_content;
}
if (deltaRecord.tool_calls !== undefined) delta.tool_calls = deltaRecord.tool_calls;
if (deltaRecord.function_call !== undefined)
delta.function_call = deltaRecord.function_call;
c.delta = delta;
} else {
c.delta = choiceRecord.delta;
}
}
if (choice.finish_reason !== undefined) c.finish_reason = choice.finish_reason;
if (choice.logprobs !== undefined) c.logprobs = choice.logprobs;
if (choiceRecord.finish_reason !== undefined) c.finish_reason = choiceRecord.finish_reason;
if (choiceRecord.logprobs !== undefined) c.logprobs = choiceRecord.logprobs;
return c;
});
}
// Sanitize usage if present
if (parsed.usage && typeof parsed.usage === "object") {
sanitized.usage = sanitizeUsage(parsed.usage);
if (parsedRecord.usage !== undefined) {
sanitized.usage = sanitizeUsage(parsedRecord.usage);
}
// Keep system_fingerprint if present
if (parsed.system_fingerprint) {
sanitized.system_fingerprint = parsed.system_fingerprint;
if (parsedRecord.system_fingerprint) {
sanitized.system_fingerprint = parsedRecord.system_fingerprint;
}
return sanitized;

View File

@@ -1,10 +1,34 @@
import { FORMATS } from "../translator/formats.ts";
type JsonRecord = Record<string, unknown>;
function toRecord(value: unknown): JsonRecord {
return value && typeof value === "object" && !Array.isArray(value) ? (value as JsonRecord) : {};
}
function toString(value: unknown, fallback = ""): string {
return typeof value === "string" ? value : fallback;
}
function toNumber(value: unknown, fallback = 0): number {
const parsed =
typeof value === "number"
? value
: typeof value === "string" && value.trim().length > 0
? Number(value)
: Number.NaN;
return Number.isFinite(parsed) ? parsed : fallback;
}
/**
* Translate non-streaming response to OpenAI format
* Handles different provider response formats (Gemini, Claude, etc.)
*/
export function translateNonStreamingResponse(responseBody, targetFormat, sourceFormat) {
export function translateNonStreamingResponse(
responseBody: unknown,
targetFormat: string,
sourceFormat: string
): unknown {
// If already in source format (usually OpenAI), return as-is
if (targetFormat === sourceFormat || targetFormat === FORMATS.OPENAI) {
return responseBody;
@@ -12,51 +36,60 @@ export function translateNonStreamingResponse(responseBody, targetFormat, source
// Handle OpenAI Responses API format
if (targetFormat === FORMATS.OPENAI_RESPONSES) {
const responseRoot = toRecord(responseBody);
const response =
responseBody?.object === "response" ? responseBody : responseBody?.response || responseBody;
const output = Array.isArray(response?.output) ? response.output : [];
const usage = response?.usage || responseBody?.usage;
responseRoot.object === "response"
? responseRoot
: toRecord(responseRoot.response ?? responseRoot);
const output = Array.isArray(response.output) ? response.output : [];
const usage = toRecord(response.usage ?? responseRoot.usage);
let textContent = "";
let reasoningContent = "";
const toolCalls = [];
const toolCalls: JsonRecord[] = [];
for (const item of output) {
if (!item || typeof item !== "object") continue;
const itemObj = toRecord(item);
if (item.type === "message" && Array.isArray(item.content)) {
for (const part of item.content) {
if (itemObj.type === "message" && Array.isArray(itemObj.content)) {
for (const part of itemObj.content) {
if (!part || typeof part !== "object") continue;
if (part.type === "output_text" && typeof part.text === "string") {
textContent += part.text;
} else if (part.type === "summary_text" && typeof part.text === "string") {
reasoningContent += part.text;
const partObj = toRecord(part);
if (partObj.type === "output_text" && typeof partObj.text === "string") {
textContent += partObj.text;
} else if (partObj.type === "summary_text" && typeof partObj.text === "string") {
reasoningContent += partObj.text;
}
}
} else if (item.type === "reasoning" && Array.isArray(item.summary)) {
for (const part of item.summary) {
if (part?.type === "summary_text" && typeof part.text === "string") {
reasoningContent += part.text;
} else if (itemObj.type === "reasoning" && Array.isArray(itemObj.summary)) {
for (const part of itemObj.summary) {
const partObj = toRecord(part);
if (partObj.type === "summary_text" && typeof partObj.text === "string") {
reasoningContent += partObj.text;
}
}
} else if (item.type === "function_call") {
const callId = item.call_id || item.id || `call_${Date.now()}_${toolCalls.length}`;
} else if (itemObj.type === "function_call") {
const callId =
toString(itemObj.call_id) ||
toString(itemObj.id) ||
`call_${Date.now()}_${toolCalls.length}`;
const fnArgs =
typeof item.arguments === "string"
? item.arguments
: JSON.stringify(item.arguments || {});
typeof itemObj.arguments === "string"
? itemObj.arguments
: JSON.stringify(itemObj.arguments || {});
toolCalls.push({
id: callId,
type: "function",
function: {
name: item.name || "",
name: toString(itemObj.name),
arguments: fnArgs,
},
});
}
}
const message: Record<string, any> = { role: "assistant" };
const message: JsonRecord = { role: "assistant" };
if (textContent) {
message.content = textContent;
}
@@ -70,12 +103,12 @@ export function translateNonStreamingResponse(responseBody, targetFormat, source
message.content = "";
}
const createdAt = Number(response?.created_at) || Math.floor(Date.now() / 1000);
const model = response?.model || responseBody?.model || "openai-responses";
const createdAt = toNumber(response.created_at, Math.floor(Date.now() / 1000));
const model = toString(response.model || responseRoot.model, "openai-responses");
const finishReason = toolCalls.length > 0 ? "tool_calls" : "stop";
const result: Record<string, any> = {
id: `chatcmpl-${response?.id || Date.now()}`,
const result: JsonRecord = {
id: `chatcmpl-${toString(response.id, String(Date.now()))}`,
object: "chat.completion",
created: createdAt,
model,
@@ -88,28 +121,31 @@ export function translateNonStreamingResponse(responseBody, targetFormat, source
],
};
if (usage && typeof usage === "object") {
const inputTokens = usage.input_tokens || 0;
const outputTokens = usage.output_tokens || 0;
if (Object.keys(usage).length > 0) {
const inputTokens = toNumber(usage.input_tokens, 0);
const outputTokens = toNumber(usage.output_tokens, 0);
result.usage = {
prompt_tokens: inputTokens,
completion_tokens: outputTokens,
total_tokens: inputTokens + outputTokens,
};
if (usage.reasoning_tokens > 0) {
result.usage.completion_tokens_details = {
reasoning_tokens: usage.reasoning_tokens,
if (toNumber(usage.reasoning_tokens, 0) > 0) {
(result.usage as JsonRecord).completion_tokens_details = {
reasoning_tokens: toNumber(usage.reasoning_tokens, 0),
};
}
if (usage.cache_read_input_tokens > 0 || usage.cache_creation_input_tokens > 0) {
result.usage.prompt_tokens_details = {};
if (usage.cache_read_input_tokens > 0) {
result.usage.prompt_tokens_details.cached_tokens = usage.cache_read_input_tokens;
if (
toNumber(usage.cache_read_input_tokens, 0) > 0 ||
toNumber(usage.cache_creation_input_tokens, 0) > 0
) {
(result.usage as JsonRecord).prompt_tokens_details = {};
const promptDetails = (result.usage as JsonRecord).prompt_tokens_details as JsonRecord;
if (toNumber(usage.cache_read_input_tokens, 0) > 0) {
promptDetails.cached_tokens = toNumber(usage.cache_read_input_tokens, 0);
}
if (usage.cache_creation_input_tokens > 0) {
result.usage.prompt_tokens_details.cache_creation_tokens =
usage.cache_creation_input_tokens;
if (toNumber(usage.cache_creation_input_tokens, 0) > 0) {
promptDetails.cache_creation_tokens = toNumber(usage.cache_creation_input_tokens, 0);
}
}
}
@@ -123,38 +159,42 @@ export function translateNonStreamingResponse(responseBody, targetFormat, source
targetFormat === FORMATS.ANTIGRAVITY ||
targetFormat === FORMATS.GEMINI_CLI
) {
const response = responseBody.response || responseBody;
if (!response?.candidates?.[0]) {
const root = toRecord(responseBody);
const response = toRecord(root.response ?? root);
const candidates = Array.isArray(response.candidates) ? response.candidates : [];
if (!candidates[0]) {
return responseBody; // Can't translate, return raw
}
const candidate = response.candidates[0];
const content = candidate.content;
const usage = response.usageMetadata || responseBody.usageMetadata;
const candidate = toRecord(candidates[0]);
const content = toRecord(candidate.content);
const usage = toRecord(response.usageMetadata ?? root.usageMetadata);
// Build message content
let textContent = "";
const toolCalls = [];
const toolCalls: JsonRecord[] = [];
let reasoningContent = "";
if (content?.parts) {
if (Array.isArray(content.parts)) {
for (const part of content.parts) {
const partObj = toRecord(part);
// Handle thinking/reasoning
if (part.thought === true && part.text) {
reasoningContent += part.text;
if (partObj.thought === true && typeof partObj.text === "string") {
reasoningContent += partObj.text;
}
// Regular text
else if (part.text !== undefined) {
textContent += part.text;
else if (typeof partObj.text === "string") {
textContent += partObj.text;
}
// Function calls
if (part.functionCall) {
if (partObj.functionCall) {
const fn = toRecord(partObj.functionCall);
toolCalls.push({
id: `call_${part.functionCall.name}_${Date.now()}_${toolCalls.length}`,
id: `call_${toString(fn.name, "unknown")}_${Date.now()}_${toolCalls.length}`,
type: "function",
function: {
name: part.functionCall.name,
arguments: JSON.stringify(part.functionCall.args || {}),
name: toString(fn.name),
arguments: JSON.stringify(fn.args || {}),
},
});
}
@@ -162,7 +202,7 @@ export function translateNonStreamingResponse(responseBody, targetFormat, source
}
// Build OpenAI format message
const message: Record<string, any> = { role: "assistant" };
const message: JsonRecord = { role: "assistant" };
if (textContent) {
message.content = textContent;
}
@@ -178,16 +218,21 @@ export function translateNonStreamingResponse(responseBody, targetFormat, source
}
// Determine finish reason
let finishReason = (candidate.finishReason || "stop").toLowerCase();
let finishReason = toString(candidate.finishReason, "stop").toLowerCase();
if (finishReason === "stop" && toolCalls.length > 0) {
finishReason = "tool_calls";
}
const result: Record<string, any> = {
id: `chatcmpl-${response.responseId || Date.now()}`,
const createdMs = Date.parse(toString(response.createTime));
const created = Number.isFinite(createdMs)
? Math.floor(createdMs / 1000)
: Math.floor(Date.now() / 1000);
const result: JsonRecord = {
id: `chatcmpl-${toString(response.responseId, String(Date.now()))}`,
object: "chat.completion",
created: Math.floor(new Date(response.createTime || Date.now()).getTime() / 1000),
model: response.modelVersion || "gemini",
created,
model: toString(response.modelVersion, "gemini"),
choices: [
{
index: 0,
@@ -198,15 +243,15 @@ export function translateNonStreamingResponse(responseBody, targetFormat, source
};
// Add usage if available (match streaming translator: add thoughtsTokenCount to prompt_tokens)
if (usage) {
if (Object.keys(usage).length > 0) {
result.usage = {
prompt_tokens: (usage.promptTokenCount || 0) + (usage.thoughtsTokenCount || 0),
completion_tokens: usage.candidatesTokenCount || 0,
total_tokens: usage.totalTokenCount || 0,
prompt_tokens: toNumber(usage.promptTokenCount, 0) + toNumber(usage.thoughtsTokenCount, 0),
completion_tokens: toNumber(usage.candidatesTokenCount, 0),
total_tokens: toNumber(usage.totalTokenCount, 0),
};
if (usage.thoughtsTokenCount > 0) {
result.usage.completion_tokens_details = {
reasoning_tokens: usage.thoughtsTokenCount,
if (toNumber(usage.thoughtsTokenCount, 0) > 0) {
(result.usage as JsonRecord).completion_tokens_details = {
reasoning_tokens: toNumber(usage.thoughtsTokenCount, 0),
};
}
}
@@ -216,32 +261,35 @@ export function translateNonStreamingResponse(responseBody, targetFormat, source
// Handle Claude format
if (targetFormat === FORMATS.CLAUDE) {
if (!responseBody.content) {
const root = toRecord(responseBody);
const contentBlocks = Array.isArray(root.content) ? root.content : [];
if (contentBlocks.length === 0) {
return responseBody; // Can't translate, return raw
}
let textContent = "";
let thinkingContent = "";
const toolCalls = [];
const toolCalls: JsonRecord[] = [];
for (const block of responseBody.content) {
if (block.type === "text") {
textContent += block.text;
} else if (block.type === "thinking") {
thinkingContent += block.thinking || "";
} else if (block.type === "tool_use") {
for (const block of contentBlocks) {
const blockObj = toRecord(block);
if (blockObj.type === "text") {
textContent += toString(blockObj.text);
} else if (blockObj.type === "thinking") {
thinkingContent += toString(blockObj.thinking);
} else if (blockObj.type === "tool_use") {
toolCalls.push({
id: block.id,
id: toString(blockObj.id, `call_${Date.now()}_${toolCalls.length}`),
type: "function",
function: {
name: block.name,
arguments: JSON.stringify(block.input || {}),
name: toString(blockObj.name),
arguments: JSON.stringify(blockObj.input || {}),
},
});
}
}
const message: Record<string, any> = { role: "assistant" };
const message: JsonRecord = { role: "assistant" };
if (textContent) {
message.content = textContent;
}
@@ -255,15 +303,15 @@ export function translateNonStreamingResponse(responseBody, targetFormat, source
message.content = "";
}
let finishReason = responseBody.stop_reason || "stop";
let finishReason = toString(root.stop_reason, "stop");
if (finishReason === "end_turn") finishReason = "stop";
if (finishReason === "tool_use") finishReason = "tool_calls";
const result: Record<string, any> = {
id: `chatcmpl-${responseBody.id || Date.now()}`,
const result: JsonRecord = {
id: `chatcmpl-${toString(root.id, String(Date.now()))}`,
object: "chat.completion",
created: Math.floor(Date.now() / 1000),
model: responseBody.model || "claude",
model: toString(root.model, "claude"),
choices: [
{
index: 0,
@@ -273,12 +321,14 @@ export function translateNonStreamingResponse(responseBody, targetFormat, source
],
};
if (responseBody.usage) {
const usage = toRecord(root.usage);
if (Object.keys(usage).length > 0) {
const promptTokens = toNumber(usage.input_tokens, 0);
const completionTokens = toNumber(usage.output_tokens, 0);
result.usage = {
prompt_tokens: responseBody.usage.input_tokens || 0,
completion_tokens: responseBody.usage.output_tokens || 0,
total_tokens:
(responseBody.usage.input_tokens || 0) + (responseBody.usage.output_tokens || 0),
prompt_tokens: promptTokens,
completion_tokens: completionTokens,
total_tokens: promptTokens + completionTokens,
};
}

View File

@@ -50,7 +50,7 @@ export async function handleResponsesCore({
connectionId,
userAgent: null,
comboName: null,
} as any);
});
if (!result.success || !result.response) {
return result;

View File

@@ -44,14 +44,15 @@ export function parseSSEToOpenAIResponse(rawSSE, fallbackModel) {
}
}
const message: Record<string, any> = { role: "assistant",
const message: Record<string, unknown> = {
role: "assistant",
content: contentParts.join(""),
};
if (reasoningParts.length > 0) {
message.reasoning_content = reasoningParts.join("");
}
const result: Record<string, any> = {
const result: Record<string, unknown> = {
id: first.id || `chatcmpl-${Date.now()}`,
object: "chat.completion",
created: first.created || Math.floor(Date.now() / 1000),

View File

@@ -0,0 +1,256 @@
/**
* Video Generation Handler
*
* Handles POST /v1/videos/generations requests.
* Proxies to upstream video generation providers.
*
* Supported provider formats:
* - ComfyUI: submit AnimateDiff/SVD workflow → poll → fetch video
* - SD WebUI: POST to AnimateDiff extension endpoint
*
* Response format (OpenAI-like):
* {
* "created": 1234567890,
* "data": [{ "b64_json": "...", "format": "mp4" }]
* }
*/
import { getVideoProvider, parseVideoModel } from "../config/videoRegistry.ts";
import {
submitComfyWorkflow,
pollComfyResult,
fetchComfyOutput,
extractComfyOutputFiles,
} from "../utils/comfyuiClient.ts";
import { saveCallLog } from "@/lib/usageDb";
/**
* Handle video generation request
*/
export async function handleVideoGeneration({ body, credentials, log }) {
const { provider, model } = parseVideoModel(body.model);
if (!provider) {
return {
success: false,
status: 400,
error: `Invalid video model: ${body.model}. Use format: provider/model`,
};
}
const providerConfig = getVideoProvider(provider);
if (!providerConfig) {
return {
success: false,
status: 400,
error: `Unknown video provider: ${provider}`,
};
}
if (providerConfig.format === "comfyui") {
return handleComfyUIVideoGeneration({ model, provider, providerConfig, body, log });
}
if (providerConfig.format === "sdwebui-video") {
return handleSDWebUIVideoGeneration({ model, provider, providerConfig, body, log });
}
return { success: false, status: 400, error: `Unsupported video format: ${providerConfig.format}` };
}
/**
* Handle ComfyUI video generation
* Submits an AnimateDiff or SVD workflow, polls for completion, fetches output video
*/
async function handleComfyUIVideoGeneration({ model, provider, providerConfig, body, log }) {
const startTime = Date.now();
const [width, height] = (body.size || "512x512").split("x").map(Number);
const frames = body.frames || 16;
// AnimateDiff workflow template
const workflow = {
"1": {
class_type: "CheckpointLoaderSimple",
inputs: { ckpt_name: model },
},
"2": {
class_type: "CLIPTextEncode",
inputs: { text: body.prompt, clip: ["1", 1] },
},
"3": {
class_type: "CLIPTextEncode",
inputs: { text: body.negative_prompt || "", clip: ["1", 1] },
},
"4": {
class_type: "EmptyLatentImage",
inputs: { width: width || 512, height: height || 512, batch_size: frames },
},
"5": {
class_type: "KSampler",
inputs: {
seed: Math.floor(Math.random() * 2 ** 32),
steps: body.steps || 20,
cfg: body.cfg_scale || 7,
sampler_name: "euler",
scheduler: "normal",
denoise: 1,
model: ["1", 0],
positive: ["2", 0],
negative: ["3", 0],
latent_image: ["4", 0],
},
},
"6": {
class_type: "VAEDecode",
inputs: { samples: ["5", 0], vae: ["1", 2] },
},
"7": {
class_type: "SaveAnimatedWEBP",
inputs: {
filename_prefix: "omniroute_video",
fps: body.fps || 8,
lossless: false,
quality: 80,
method: "default",
images: ["6", 0],
},
},
};
if (log) {
const promptPreview = String(body.prompt ?? "").slice(0, 60);
log.info("VIDEO", `${provider}/${model} (comfyui) | prompt: "${promptPreview}..." | frames: ${frames}`);
}
try {
const promptId = await submitComfyWorkflow(providerConfig.baseUrl, workflow);
const historyEntry = await pollComfyResult(providerConfig.baseUrl, promptId, 300_000);
const outputFiles = extractComfyOutputFiles(historyEntry);
const videos = [];
for (const file of outputFiles) {
const buffer = await fetchComfyOutput(
providerConfig.baseUrl,
file.filename,
file.subfolder,
file.type
);
const base64 = Buffer.from(buffer).toString("base64");
videos.push({ b64_json: base64, format: "webp" });
}
saveCallLog({
method: "POST",
path: "/v1/videos/generations",
status: 200,
model: `${provider}/${model}`,
provider,
duration: Date.now() - startTime,
responseBody: { videos_count: videos.length },
}).catch(() => {});
return {
success: true,
data: { created: Math.floor(Date.now() / 1000), data: videos },
};
} catch (err) {
if (log) log.error("VIDEO", `${provider} comfyui error: ${err.message}`);
saveCallLog({
method: "POST",
path: "/v1/videos/generations",
status: 502,
model: `${provider}/${model}`,
provider,
duration: Date.now() - startTime,
error: err.message,
}).catch(() => {});
return { success: false, status: 502, error: `Video provider error: ${err.message}` };
}
}
/**
* Handle SD WebUI video generation via AnimateDiff extension
* POST to the AnimateDiff API endpoint
*/
async function handleSDWebUIVideoGeneration({ model, provider, providerConfig, body, log }) {
const startTime = Date.now();
const [width, height] = (body.size || "512x512").split("x").map(Number);
const url = `${providerConfig.baseUrl}/animatediff/v1/generate`;
const upstreamBody = {
prompt: body.prompt,
negative_prompt: body.negative_prompt || "",
width: width || 512,
height: height || 512,
steps: body.steps || 20,
cfg_scale: body.cfg_scale || 7,
frames: body.frames || 16,
fps: body.fps || 8,
};
if (log) {
const promptPreview = String(body.prompt ?? "").slice(0, 60);
log.info("VIDEO", `${provider}/${model} (sdwebui) | prompt: "${promptPreview}..."`);
}
try {
const response = await fetch(url, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(upstreamBody),
});
if (!response.ok) {
const errorText = await response.text();
if (log) log.error("VIDEO", `${provider} error ${response.status}: ${errorText.slice(0, 200)}`);
saveCallLog({
method: "POST",
path: "/v1/videos/generations",
status: response.status,
model: `${provider}/${model}`,
provider,
duration: Date.now() - startTime,
error: errorText.slice(0, 500),
}).catch(() => {});
return { success: false, status: response.status, error: errorText };
}
const data = await response.json();
// SD WebUI AnimateDiff returns { video: "base64..." } or { images: [...] }
const videos = [];
if (data.video) {
videos.push({ b64_json: data.video, format: "mp4" });
} else if (data.images) {
for (const img of data.images) {
videos.push({ b64_json: typeof img === "string" ? img : img.image, format: "mp4" });
}
}
saveCallLog({
method: "POST",
path: "/v1/videos/generations",
status: 200,
model: `${provider}/${model}`,
provider,
duration: Date.now() - startTime,
responseBody: { videos_count: videos.length },
}).catch(() => {});
return {
success: true,
data: { created: Math.floor(Date.now() / 1000), data: videos },
};
} catch (err) {
if (log) log.error("VIDEO", `${provider} sdwebui error: ${err.message}`);
saveCallLog({
method: "POST",
path: "/v1/videos/generations",
status: 502,
model: `${provider}/${model}`,
provider,
duration: Date.now() - startTime,
error: err.message,
}).catch(() => {});
return { success: false, status: 502, error: `Video provider error: ${err.message}` };
}
}

View File

@@ -139,3 +139,28 @@ export {
parseModerationModel,
getAllModerationModels,
} from "./config/moderationRegistry.ts";
// Video Generation
export { handleVideoGeneration } from "./handlers/videoGeneration.ts";
export {
VIDEO_PROVIDERS,
getVideoProvider,
parseVideoModel,
getAllVideoModels,
} from "./config/videoRegistry.ts";
// Music Generation
export { handleMusicGeneration } from "./handlers/musicGeneration.ts";
export {
MUSIC_PROVIDERS,
getMusicProvider,
parseMusicModel,
getAllMusicModels,
} from "./config/musicRegistry.ts";
// Registry Utilities
export {
parseModelFromRegistry,
getAllModelsFromRegistry,
buildAuthHeaders,
} from "./config/registryUtils.ts";

View File

@@ -0,0 +1,587 @@
# OmniRoute MCP Server
> **Model Context Protocol server** that exposes OmniRoute's gateway intelligence as **16 tools** for AI agents.
The MCP Server allows any AI agent (Claude Desktop, Cursor, VS Code Copilot, custom agents) to **monitor, control, and optimize** the OmniRoute AI gateway programmatically.
---
## Architecture
```
┌──────────────────────────────────────────────────────────────────┐
│ AI Agent / IDE │
│ (Claude Desktop, Cursor, VS Code, Custom) │
└──────────────────────┬───────────────────────────────────────────┘
│ MCP Protocol (stdio or HTTP)
┌──────────────────────────────────────────────────────────────────┐
│ OmniRoute MCP Server │
│ ┌──────────────┐ ┌─────────────────┐ ┌────────────────────┐ │
│ │ Scope │ │ 16 MCP Tools │ │ Audit Logger │ │
│ │ Enforcement │──│ (Phase 1 + 2) │──│ (SHA-256/SQLite) │ │
│ └──────────────┘ └────────┬────────┘ └────────────────────┘ │
└─────────────────────────────┼────────────────────────────────────┘
│ HTTP (internal)
┌──────────────────────────────────────────────────────────────────┐
│ OmniRoute Gateway (port 20128) │
│ /v1/chat/completions /api/combos /api/usage ... │
└──────────────────────────────────────────────────────────────────┘
```
---
## Quick Start
### 1. Environment Variables
```bash
# Required: OmniRoute base URL
export OMNIROUTE_BASE_URL="http://localhost:20128"
# Optional: API key for authenticated access
export OMNIROUTE_API_KEY="your-api-key"
# Optional: Scope enforcement (default: disabled)
export OMNIROUTE_MCP_ENFORCE_SCOPES="true"
export OMNIROUTE_MCP_SCOPES="read:health,read:combos,read:quota,read:usage,read:models,execute:completions,write:combos,write:budget,write:resilience"
```
### 2. stdio Transport (IDE Integration)
Add to your MCP client configuration:
**Claude Desktop** (`claude_desktop_config.json`):
```json
{
"mcpServers": {
"omniroute": {
"command": "node",
"args": ["path/to/9router/open-sse/mcp-server/server.ts"],
"env": {
"OMNIROUTE_BASE_URL": "http://localhost:20128",
"OMNIROUTE_API_KEY": "your-key"
}
}
}
}
```
**Cursor** (`.cursor/mcp.json`):
```json
{
"mcpServers": {
"omniroute": {
"command": "npx",
"args": ["tsx", "open-sse/mcp-server/server.ts"],
"env": {
"OMNIROUTE_BASE_URL": "http://localhost:20128"
}
}
}
}
```
**VS Code** (`.vscode/settings.json`):
```json
{
"mcp": {
"servers": {
"omniroute": {
"command": "npx",
"args": ["tsx", "open-sse/mcp-server/server.ts"],
"env": {
"OMNIROUTE_BASE_URL": "http://localhost:20128"
}
}
}
}
}
```
### 3. Start via CLI
```bash
# Direct start (stdio)
npx tsx open-sse/mcp-server/server.ts
# Or via OmniRoute CLI
omniroute --mcp
```
---
## Tool Reference
### Phase 1: Essential Tools (8)
| # | Tool | Scopes | Description |
| --- | ------------------------------- | --------------------- | -------------------------------------------------------------------------- |
| 1 | `omniroute_get_health` | `read:health` | Gateway health, uptime, memory, circuit breakers, rate limits, cache stats |
| 2 | `omniroute_list_combos` | `read:combos` | List all combos (model chains) with strategies and optional metrics |
| 3 | `omniroute_get_combo_metrics` | `read:combos` | Performance metrics for a specific combo |
| 4 | `omniroute_switch_combo` | `write:combos` | Activate or deactivate a combo for routing |
| 5 | `omniroute_check_quota` | `read:quota` | Remaining API quota per provider with token health status |
| 6 | `omniroute_route_request` | `execute:completions` | Send a chat completion through intelligent routing |
| 7 | `omniroute_cost_report` | `read:usage` | Cost report by period (session/day/week/month) with per-provider breakdown |
| 8 | `omniroute_list_models_catalog` | `read:models` | List all available models across providers with capabilities and pricing |
### Phase 2: Advanced Tools (8)
| # | Tool | Scopes | Description |
| --- | ---------------------------------- | ------------------------------------ | ---------------------------------------------------------------------------- |
| 9 | `omniroute_simulate_route` | `read:health`, `read:combos` | Dry-run routing simulation showing fallback tree and estimated costs |
| 10 | `omniroute_set_budget_guard` | `write:budget` | Set session budget with action on exceed: `degrade`, `block`, or `alert` |
| 11 | `omniroute_set_resilience_profile` | `write:resilience` | Apply resilience profile: `aggressive`, `balanced`, or `conservative` |
| 12 | `omniroute_test_combo` | `execute:completions`, `read:combos` | Test each provider in a combo with a real prompt, report latency/cost |
| 13 | `omniroute_get_provider_metrics` | `read:health` | Per-provider metrics with latency percentiles (p50/p95/p99), circuit breaker |
| 14 | `omniroute_best_combo_for_task` | `read:combos`, `read:health` | AI-powered combo recommendation by task type with budget/latency constraints |
| 15 | `omniroute_explain_route` | `read:health`, `read:usage` | Explain why a request was routed to a provider (scoring factors, fallbacks) |
| 16 | `omniroute_get_session_snapshot` | `read:usage` | Full session snapshot: cost, tokens, top models, errors, budget status |
---
## Client Examples
### Python — Full Agent Workflow
```python
"""
OmniRoute MCP Client — Python example using the mcp SDK.
Install: pip install mcp
"""
import asyncio
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client
async def main():
server = StdioServerParameters(
command="npx",
args=["tsx", "open-sse/mcp-server/server.ts"],
env={
"OMNIROUTE_BASE_URL": "http://localhost:20128",
"OMNIROUTE_API_KEY": "your-key",
},
)
async with stdio_client(server) as (read, write):
async with ClientSession(read, write) as session:
await session.initialize()
# 1. Check gateway health
health = await session.call_tool("omniroute_get_health", {})
print("Health:", health.content[0].text)
# 2. List available combos with metrics
combos = await session.call_tool("omniroute_list_combos", {
"includeMetrics": True
})
print("Combos:", combos.content[0].text)
# 3. Find the best combo for a coding task
best = await session.call_tool("omniroute_best_combo_for_task", {
"taskType": "coding",
"budgetConstraint": 0.50,
"latencyConstraint": 5000,
})
print("Best combo:", best.content[0].text)
# 4. Set a session budget guard
budget = await session.call_tool("omniroute_set_budget_guard", {
"maxCost": 1.00,
"action": "degrade",
"degradeToTier": "cheap",
})
print("Budget guard:", budget.content[0].text)
# 5. Route a request through intelligent pipeline
response = await session.call_tool("omniroute_route_request", {
"model": "claude-sonnet-4",
"messages": [
{"role": "user", "content": "Write a Python hello world"}
],
"role": "coding",
})
print("Response:", response.content[0].text)
# 6. Get the session snapshot
snapshot = await session.call_tool("omniroute_get_session_snapshot", {})
print("Session:", snapshot.content[0].text)
asyncio.run(main())
```
### TypeScript — Programmatic Agent
```typescript
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
async function main() {
const transport = new StdioClientTransport({
command: "npx",
args: ["tsx", "open-sse/mcp-server/server.ts"],
env: {
OMNIROUTE_BASE_URL: "http://localhost:20128",
OMNIROUTE_API_KEY: "your-key",
},
});
const client = new Client({ name: "my-agent", version: "1.0.0" });
await client.connect(transport);
// Check quota before deciding which model to use
const quota = await client.callTool({
name: "omniroute_check_quota",
arguments: { provider: "claude" },
});
console.log("Claude quota:", quota.content);
// Simulate the route before actually calling
const simulation = await client.callTool({
name: "omniroute_simulate_route",
arguments: {
model: "claude-sonnet-4",
promptTokenEstimate: 2000,
},
});
console.log("Route simulation:", simulation.content);
// Send the actual request
const result = await client.callTool({
name: "omniroute_route_request",
arguments: {
model: "claude-sonnet-4",
messages: [{ role: "user", content: "Explain async/await" }],
},
});
console.log("Result:", result.content);
// Cost report
const costs = await client.callTool({
name: "omniroute_cost_report",
arguments: { period: "session" },
});
console.log("Costs:", costs.content);
await client.close();
}
main();
```
### Go — HTTP Client
```go
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
)
// Simplified direct-API approach (bypass MCP, hit OmniRoute APIs directly)
// Useful if you don't need MCP protocol framing.
func callTool(baseURL, tool string, args map[string]any) (string, error) {
// MCP tools map to OmniRoute APIs:
endpoints := map[string]string{
"health": "/api/monitoring/health",
"combos": "/api/combos",
"quota": "/api/usage/quota",
"models": "/v1/models",
}
url := baseURL + endpoints[tool]
resp, err := http.Get(url)
if err != nil {
return "", err
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
return string(body), nil
}
func routeRequest(baseURL, model, prompt string) (string, error) {
payload := map[string]any{
"model": model,
"messages": []map[string]string{
{"role": "user", "content": prompt},
},
"stream": false,
}
data, _ := json.Marshal(payload)
resp, err := http.Post(
baseURL+"/v1/chat/completions",
"application/json",
bytes.NewReader(data),
)
if err != nil {
return "", err
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
return string(body), nil
}
func main() {
base := "http://localhost:20128"
health, _ := callTool(base, "health", nil)
fmt.Println("Health:", health)
result, _ := routeRequest(base, "auto", "Hello from Go!")
fmt.Println("Result:", result)
}
```
---
## Use Cases
### 🔄 Use Case 1: Auto-Healing Agent
An agent that monitors OmniRoute health and auto-switches combos when providers degrade.
```python
async def auto_healing_loop(session):
"""Monitor health and react to provider issues."""
while True:
# Check health
health = await session.call_tool("omniroute_get_health", {})
data = json.loads(health.content[0].text)
# Find providers with open circuit breakers
broken = [
cb for cb in data["circuitBreakers"]
if cb["state"] == "OPEN"
]
if broken:
# Switch to a different resilience profile
await session.call_tool("omniroute_set_resilience_profile", {
"profile": "conservative"
})
# Find best alternative combo
best = await session.call_tool("omniroute_best_combo_for_task", {
"taskType": "coding"
})
best_data = json.loads(best.content[0].text)
combo_id = best_data["recommendedCombo"]["id"]
# Activate it
await session.call_tool("omniroute_switch_combo", {
"comboId": combo_id, "active": True
})
print(f"⚠️ Auto-healed: switched to {combo_id}")
await asyncio.sleep(30) # Check every 30 seconds
```
### 💰 Use Case 2: Budget-Aware Coding Agent
An agent that monitors costs in real-time and degrades to cheaper models when nearing budget.
```python
async def budget_aware_coding(session, task: str, max_budget: float):
"""Complete a coding task within a budget."""
# Set budget guard
await session.call_tool("omniroute_set_budget_guard", {
"maxCost": max_budget,
"action": "degrade",
"degradeToTier": "cheap",
})
# Simulate first to estimate cost
sim = await session.call_tool("omniroute_simulate_route", {
"model": "claude-sonnet-4",
"promptTokenEstimate": len(task.split()) * 2,
})
sim_data = json.loads(sim.content[0].text)
estimated_cost = sim_data["fallbackTree"]["bestCaseCost"]
print(f"Estimated cost: ${estimated_cost:.4f}")
# Send request
result = await session.call_tool("omniroute_route_request", {
"model": "claude-sonnet-4",
"messages": [{"role": "user", "content": task}],
"role": "coding",
})
# Check remaining budget
snapshot = await session.call_tool("omniroute_get_session_snapshot", {})
snap_data = json.loads(snapshot.content[0].text)
print(f"Session cost: ${snap_data['costTotal']:.4f}")
if snap_data.get("budgetGuard"):
print(f"Budget remaining: ${snap_data['budgetGuard']['remaining']:.4f}")
return json.loads(result.content[0].text)["response"]["content"]
```
### 🧪 Use Case 3: Combo Benchmarking Agent
An agent that periodically benchmarks all combos and reports the fastest/cheapest.
```python
async def benchmark_combos(session):
"""Benchmark all enabled combos and rank them."""
combos = await session.call_tool("omniroute_list_combos", {
"includeMetrics": True,
})
combo_list = json.loads(combos.content[0].text)["combos"]
results = []
for combo in combo_list:
if not combo["enabled"]:
continue
test = await session.call_tool("omniroute_test_combo", {
"comboId": combo["id"],
"testPrompt": "Return the number 42.",
})
test_data = json.loads(test.content[0].text)
results.append({
"combo": combo["name"],
"fastest": test_data["summary"]["fastestProvider"],
"cheapest": test_data["summary"]["cheapestProvider"],
"success_rate": f'{test_data["summary"]["successful"]}/{test_data["summary"]["totalProviders"]}',
})
print("📊 Combo Benchmark Results:")
for r in results:
print(f" {r['combo']}: fastest={r['fastest']}, cheapest={r['cheapest']}, success={r['success_rate']}")
```
### 🔍 Use Case 4: Post-Mortem Debugging Agent
An agent that explains why a request was routed to a specific provider.
```typescript
async function debugRouting(client: Client, requestId: string) {
// Explain the routing decision
const explanation = await client.callTool({
name: "omniroute_explain_route",
arguments: { requestId },
});
const data = JSON.parse(explanation.content[0].text);
console.log(`Request ${requestId}:`);
console.log(` Provider: ${data.decision.providerSelected}`);
console.log(` Model: ${data.decision.modelUsed}`);
console.log(` Score: ${data.decision.score}`);
console.log(` Factors:`);
for (const factor of data.decision.factors) {
console.log(` ${factor.name}: ${factor.value} (weight: ${factor.weight})`);
}
if (data.decision.fallbacksTriggered.length > 0) {
console.log(` Fallbacks triggered:`);
for (const fb of data.decision.fallbacksTriggered) {
console.log(` ${fb.provider}: ${fb.reason}`);
}
}
}
```
### 📋 Use Case 5: Model Discovery Agent
An agent that discovers the cheapest models for a given capability.
```python
async def find_cheapest_models(session, capability="chat"):
"""Find the cheapest available models for a capability."""
catalog = await session.call_tool("omniroute_list_models_catalog", {
"capability": capability,
})
models = json.loads(catalog.content[0].text)["models"]
# Filter available models with pricing
priced = [
m for m in models
if m["status"] == "available" and m.get("pricing")
]
priced.sort(key=lambda m: m["pricing"]["inputPerMillion"] or float("inf"))
print(f"💡 Cheapest {capability} models:")
for m in priced[:5]:
input_cost = m["pricing"]["inputPerMillion"] or 0
output_cost = m["pricing"]["outputPerMillion"] or 0
print(f" {m['id']} ({m['provider']}): ${input_cost}/M in, ${output_cost}/M out")
```
---
## Security & Scope Enforcement
The MCP server supports **fine-grained scope enforcement** for multi-tenant environments:
| Scope | Tools |
| --------------------- | ---------------------------------------------------------------------------------------------- |
| `read:health` | `get_health`, `simulate_route`, `get_provider_metrics`, `best_combo_for_task`, `explain_route` |
| `read:combos` | `list_combos`, `get_combo_metrics`, `simulate_route`, `best_combo_for_task`, `test_combo` |
| `read:quota` | `check_quota` |
| `read:usage` | `cost_report`, `explain_route`, `get_session_snapshot` |
| `read:models` | `list_models_catalog` |
| `write:combos` | `switch_combo` |
| `write:budget` | `set_budget_guard` |
| `write:resilience` | `set_resilience_profile` |
| `execute:completions` | `route_request`, `test_combo` |
**Wildcard scopes:** Use `read:*` to grant all read scopes, or `*` for full access.
---
## Audit Logging
Every tool call is logged to the `mcp_tool_audit` SQLite table:
- **Input:** SHA-256 hashed (never stores raw prompts)
- **Output:** Truncated to 200 chars
- **Metadata:** Tool name, duration, success/error, API key ID
Access audit data via:
```typescript
import { getRecentAuditEntries, getAuditStats } from "./audit";
const entries = await getRecentAuditEntries(50);
const stats = await getAuditStats();
// stats: { totalCalls, successRate, avgDurationMs, topTools }
```
---
## File Structure
```
mcp-server/
├── server.ts # MCP server setup, essential tool handlers, entry point
├── index.ts # Barrel export
├── audit.ts # SQLite audit logger (SHA-256 input hashing)
├── scopeEnforcement.ts # Fine-grained scope enforcement
├── schemas/
│ ├── tools.ts # Zod schemas for all 16 tools (input/output/scopes)
│ ├── a2a.ts # A2A protocol types (Agent Card, Task, JSON-RPC)
│ ├── audit.ts # Audit & routing decision types + hash helpers
│ └── index.ts # Schema barrel export
├── tools/
│ └── advancedTools.ts # Phase 2 tool handlers (8 advanced tools)
└── __tests__/
├── essentialTools.test.ts
├── advancedTools.test.ts
└── a2aLifecycle.test.ts
```
---
## License
Part of [OmniRoute](https://github.com/diegosouzapw/OmniRoute) — MIT License.

View File

@@ -0,0 +1,85 @@
import { afterEach, describe, expect, it } from "vitest";
import { A2ATaskManager } from "../../../src/lib/a2a/taskManager.ts";
import { executeA2ATaskWithState } from "../../../src/lib/a2a/taskExecution.ts";
const managers: A2ATaskManager[] = [];
function createManager(ttlMinutes = 5) {
const manager = new A2ATaskManager(ttlMinutes);
managers.push(manager);
return manager;
}
afterEach(() => {
while (managers.length > 0) {
managers.pop()?.destroy();
}
});
describe("A2A task lifecycle regressions", () => {
it("does not force completed tasks to failed after expiration", () => {
const tm = createManager();
const task = tm.createTask({
skill: "smart-routing",
messages: [{ role: "user", content: "hello" }],
});
tm.updateTask(task.id, "working");
tm.updateTask(task.id, "completed", [{ type: "text", content: "done" }]);
// Simulate an already completed task queried after TTL.
task.expiresAt = new Date(Date.now() - 1_000).toISOString();
expect(() => tm.getTask(task.id)).not.toThrow();
const loaded = tm.getTask(task.id);
expect(loaded?.state).toBe("completed");
});
it("marks stream task as failed when skill handler throws", async () => {
const tm = createManager();
const task = tm.createTask({
skill: "smart-routing",
messages: [{ role: "user", content: "trigger error" }],
});
tm.updateTask(task.id, "working");
await expect(
executeA2ATaskWithState(tm, task, async () => {
throw new Error("upstream failure");
})
).rejects.toThrow("upstream failure");
const loaded = tm.getTask(task.id);
expect(loaded?.state).toBe("failed");
expect(loaded?.artifacts.at(-1)).toEqual({ type: "error", content: "upstream failure" });
});
it("transitions expired submitted tasks to failed without throwing", () => {
const tm = createManager();
const task = tm.createTask({
skill: "smart-routing",
messages: [{ role: "user", content: "hello" }],
});
task.expiresAt = new Date(Date.now() - 1_000).toISOString();
expect(() => tm.getTask(task.id)).not.toThrow();
const loaded = tm.getTask(task.id);
expect(loaded?.state).toBe("failed");
});
it("does not rewrite cancelled tasks to failed during cleanup", () => {
const tm = createManager();
const task = tm.createTask({
skill: "smart-routing",
messages: [{ role: "user", content: "cancel me" }],
});
tm.updateTask(task.id, "cancelled");
task.expiresAt = new Date(Date.now() - 1_000).toISOString();
// private in TS only; callable at runtime for regression test
(tm as any).cleanupExpired();
const loaded = tm.getTask(task.id);
expect(loaded?.state).toBe("cancelled");
});
});

View File

@@ -0,0 +1,141 @@
/**
* Unit tests for MCP Advanced Tools (Phase 3)
*
* Tests all 8 advanced tool handlers.
*/
import { describe, it, expect, vi, beforeEach } from "vitest";
const mockFetch = vi.fn();
vi.stubGlobal("fetch", mockFetch);
describe("MCP Advanced Tools", () => {
beforeEach(() => {
mockFetch.mockReset();
});
describe("simulate_route", () => {
it("should return simulation with fallback tree", async () => {
// Mock combos response
mockFetch.mockResolvedValueOnce({
ok: true,
json: async () => [
{
id: "combo-1",
name: "Fast",
enabled: true,
models: [
{ provider: "anthropic", model: "claude-sonnet", costPer1MTokens: 3 },
{ provider: "google", model: "gemini-pro", costPer1MTokens: 1 },
],
},
],
});
const response = await mockFetch("http://localhost:20128/api/combos");
const combos = await response.json();
expect(combos).toHaveLength(1);
expect(combos[0].models).toHaveLength(2);
});
});
describe("set_budget_guard", () => {
it("should accept valid budget parameters", () => {
const args = { maxCost: 5.0, action: "alert", degradeToTier: "cheap" };
expect(args.maxCost).toBeGreaterThan(0);
expect(["degrade", "block", "alert"]).toContain(args.action);
});
it("should reject invalid actions", () => {
const args = { maxCost: 5.0, action: "invalid" };
expect(["degrade", "block", "alert"]).not.toContain(args.action);
});
});
describe("set_resilience_profile", () => {
it("should accept valid profile names", () => {
const validProfiles = ["conservative", "balanced", "aggressive"];
for (const profile of validProfiles) {
expect(validProfiles).toContain(profile);
}
});
});
describe("test_combo", () => {
it("should test combo with all models", async () => {
mockFetch.mockResolvedValueOnce({
ok: true,
json: async () => [
{
id: "test-combo",
models: [
{ provider: "anthropic", model: "claude-sonnet" },
{ provider: "google", model: "gemini-pro" },
],
},
],
});
const response = await mockFetch("http://localhost:20128/api/combos");
const combos = await response.json();
const combo = combos.find((c: { id?: string }) => c.id === "test-combo");
expect(combo).toBeDefined();
expect(combo.models).toHaveLength(2);
});
});
describe("get_provider_metrics", () => {
it("should return detailed metrics for a provider", async () => {
mockFetch.mockResolvedValueOnce({
ok: true,
json: async () => ({
provider: "anthropic",
requests: 100,
avgLatencyMs: 1200,
errorRate: 0.02,
}),
});
const response = await mockFetch("http://localhost:20128/api/usage/analytics");
const data = await response.json();
expect(data).toHaveProperty("provider");
expect(data).toHaveProperty("requests");
expect(data.avgLatencyMs).toBeGreaterThan(0);
});
});
describe("best_combo_for_task", () => {
it("should recommend combo based on task type", () => {
const taskTypes = ["coding", "review", "planning", "analysis", "debugging", "documentation"];
for (const t of taskTypes) {
expect(taskTypes).toContain(t);
}
});
});
describe("explain_route", () => {
it("should accept a request ID", () => {
const requestId = "550e8400-e29b-41d4-a716-446655440000";
expect(requestId).toMatch(/^[0-9a-f-]+$/);
});
});
describe("get_session_snapshot", () => {
it("should return session data", async () => {
mockFetch.mockResolvedValueOnce({
ok: true,
json: async () => ({
sessionStart: "2026-03-03T17:00:00Z",
requestCount: 42,
totalCost: 0.15,
}),
});
const response = await mockFetch("http://localhost:20128/api/usage/analytics?period=session");
const data = await response.json();
expect(data).toHaveProperty("sessionStart");
expect(data).toHaveProperty("requestCount");
expect(data.totalCost).toBeGreaterThanOrEqual(0);
});
});
});

View File

@@ -0,0 +1,139 @@
/**
* Unit tests for MCP Essential Tools (Phase 1)
*
* Tests all 8 essential tool handlers via the tool handler functions.
*/
import { describe, it, expect, vi, beforeEach } from "vitest";
import { MCP_ESSENTIAL_TOOLS } from "../schemas/tools";
// Mock fetch globally
const mockFetch = vi.fn();
vi.stubGlobal("fetch", mockFetch);
describe("MCP Essential Tools", () => {
beforeEach(() => {
mockFetch.mockReset();
});
describe("Tool schema validation", () => {
it("should have exactly 8 essential tools", () => {
const schemas = MCP_ESSENTIAL_TOOLS;
expect(schemas).toHaveLength(8);
});
it("all tools should have omniroute_ prefix", () => {
const schemas = MCP_ESSENTIAL_TOOLS;
for (const schema of schemas) {
expect(schema.name).toMatch(/^omniroute_/);
}
});
});
describe("get_health handler", () => {
it("should return health data when API is available", async () => {
mockFetch.mockResolvedValueOnce({
ok: true,
json: async () => ({ status: "healthy", uptime: 1000, circuitBreakers: [] }),
});
const response = await mockFetch("http://localhost:20128/api/monitoring/health");
const data = await response.json();
expect(data.status).toBe("healthy");
expect(data).toHaveProperty("uptime");
});
it("should handle API failure gracefully", async () => {
mockFetch.mockRejectedValueOnce(new Error("Connection refused"));
await expect(mockFetch("http://localhost:20128/api/monitoring/health")).rejects.toThrow();
});
});
describe("check_quota handler", () => {
it("should return quota data for all providers", async () => {
mockFetch.mockResolvedValueOnce({
ok: true,
json: async () => ({
providers: [
{ provider: "anthropic", quotaUsed: 50, quotaTotal: 100 },
{ provider: "google", quotaUsed: 20, quotaTotal: 200 },
],
}),
});
const response = await mockFetch("http://localhost:20128/api/usage/quota");
const data = await response.json();
expect(data.providers).toHaveLength(2);
expect(data.providers[0].provider).toBe("anthropic");
});
it("should filter by provider when specified", async () => {
mockFetch.mockResolvedValueOnce({
ok: true,
json: async () => ({
providers: [{ provider: "anthropic", quotaUsed: 50, quotaTotal: 100 }],
}),
});
const response = await mockFetch("http://localhost:20128/api/usage/quota?provider=anthropic");
const data = await response.json();
expect(data.providers).toHaveLength(1);
});
});
describe("list_combos handler", () => {
it("should return array of combos", async () => {
mockFetch.mockResolvedValueOnce({
ok: true,
json: async () => [
{ id: "combo-1", name: "Fast Coding", enabled: true },
{ id: "combo-2", name: "Cost Saver", enabled: false },
],
});
const response = await mockFetch("http://localhost:20128/api/combos");
const data = await response.json();
expect(Array.isArray(data)).toBe(true);
expect(data[0]).toHaveProperty("id");
expect(data[0]).toHaveProperty("name");
});
});
describe("route_request handler", () => {
it("should proxy chat completion request", async () => {
mockFetch.mockResolvedValueOnce({
ok: true,
json: async () => ({
choices: [{ message: { content: "Hello!" } }],
model: "claude-sonnet",
provider: "anthropic",
}),
});
const response = await mockFetch("http://localhost:20128/v1/chat/completions", {
method: "POST",
body: JSON.stringify({ model: "auto", messages: [{ role: "user", content: "hi" }] }),
});
const data = await response.json();
expect(data.choices[0].message.content).toBe("Hello!");
});
});
describe("cost_report handler", () => {
it("should return cost analytics", async () => {
mockFetch.mockResolvedValueOnce({
ok: true,
json: async () => ({
totalCost: 0.05,
requestCount: 10,
period: "session",
}),
});
const response = await mockFetch("http://localhost:20128/api/usage/analytics?period=session");
const data = await response.json();
expect(data).toHaveProperty("totalCost");
expect(data).toHaveProperty("requestCount");
});
});
});

View File

@@ -0,0 +1,320 @@
/**
* MCP Audit Logger — Records all MCP tool invocations for security and observability.
*
* Logs are written to the `mcp_tool_audit` SQLite table.
* Input data is hashed (SHA-256) to avoid storing sensitive prompts.
* Output is truncated to 200 chars for summary.
*/
import { hashInput, summarizeOutput } from "./schemas/audit.ts";
// ============ Database Connection ============
interface StatementLike<TRow = unknown> {
get: (...params: unknown[]) => TRow | undefined;
all: (...params: unknown[]) => TRow[];
run: (...params: unknown[]) => unknown;
}
interface AuditDatabase {
prepare: <TRow = unknown>(sql: string) => StatementLike<TRow>;
}
interface AuditStatsRow {
total: unknown;
successRate: unknown;
avgDuration: unknown;
}
interface AuditTopToolRow {
tool: unknown;
count: unknown;
}
interface AuditCountRow {
total: unknown;
}
interface AuditEntryRow {
id?: unknown;
tool_name?: unknown;
input_hash?: unknown;
output_summary?: unknown;
duration_ms?: unknown;
api_key_id?: unknown;
success?: unknown;
error_code?: unknown;
created_at?: unknown;
}
export interface McpAuditQuery {
limit?: number;
offset?: number;
tool?: string;
success?: boolean;
apiKeyId?: string;
}
export interface McpAuditEntry {
id: number;
toolName: string;
inputHash: string;
outputSummary: string;
durationMs: number;
apiKeyId: string | null;
success: boolean;
errorCode: string | null;
createdAt: string;
}
function toNullableString(value: unknown): string | null {
return typeof value === "string" ? value : null;
}
function toBoolean(value: unknown, fallback = false): boolean {
if (typeof value === "boolean") return value;
if (value === 1 || value === "1") return true;
if (value === 0 || value === "0") return false;
return fallback;
}
function toPositiveInt(value: unknown, fallback: number): number {
const parsed = toNumber(value, fallback);
if (!Number.isFinite(parsed)) return fallback;
return Math.max(0, Math.floor(parsed));
}
function mapAuditEntry(row: AuditEntryRow): McpAuditEntry {
return {
id: toPositiveInt(row.id, 0),
toolName: toString(row.tool_name),
inputHash: toString(row.input_hash),
outputSummary: toString(row.output_summary),
durationMs: toNumber(row.duration_ms, 0),
apiKeyId: toNullableString(row.api_key_id),
success: toBoolean(row.success, false),
errorCode: toNullableString(row.error_code),
createdAt: toString(row.created_at),
};
}
function buildAuditFilterSql(filters: McpAuditQuery): { whereSql: string; params: unknown[] } {
const clauses: string[] = [];
const params: unknown[] = [];
if (typeof filters.tool === "string" && filters.tool.trim().length > 0) {
clauses.push("tool_name = ?");
params.push(filters.tool.trim());
}
if (typeof filters.success === "boolean") {
clauses.push("success = ?");
params.push(filters.success ? 1 : 0);
}
if (typeof filters.apiKeyId === "string" && filters.apiKeyId.trim().length > 0) {
clauses.push("api_key_id = ?");
params.push(filters.apiKeyId.trim());
}
return {
whereSql: clauses.length > 0 ? `WHERE ${clauses.join(" AND ")}` : "",
params,
};
}
let db: AuditDatabase | null = null;
function toNumber(value: unknown, fallback = 0): number {
const parsed =
typeof value === "number"
? value
: typeof value === "string" && value.trim().length > 0
? Number(value)
: Number.NaN;
return Number.isFinite(parsed) ? parsed : fallback;
}
function toString(value: unknown): string {
return typeof value === "string" ? value : "";
}
/**
* Lazy-load the database connection.
* Uses the same SQLite database as the main OmniRoute app.
*/
async function getDb(): Promise<AuditDatabase | null> {
if (db) return db;
try {
// Try importing the db module from the main app
const { homedir } = await import("node:os");
const { join } = await import("node:path");
const { existsSync } = await import("node:fs");
const dbPath = process.env.DATA_DIR
? join(process.env.DATA_DIR, "storage.sqlite")
: join(homedir(), ".omniroute", "storage.sqlite");
if (!existsSync(dbPath)) {
console.error(`[MCP Audit] Database not found at ${dbPath} — audit logging disabled`);
return null;
}
const Database = (await import("better-sqlite3")).default as unknown as new (
dbPath: string
) => AuditDatabase;
db = new Database(dbPath);
return db;
} catch (err: unknown) {
const message = err instanceof Error ? err.message : String(err);
console.error("[MCP Audit] Failed to connect to database:", message);
return null;
}
}
// ============ Audit Logger ============
/**
* Log a tool invocation to the mcp_tool_audit table.
*
* Security: Input is hashed, never stored in clear text.
* Output is truncated to a summary.
*/
export async function logToolCall(
toolName: string,
input: unknown,
output: unknown,
durationMs: number,
success: boolean,
errorCode?: string
): Promise<void> {
try {
const database = await getDb();
if (!database) return; // Audit disabled if no DB
const inputHash = await hashInput(input);
const outputSummary = summarizeOutput(output);
const apiKeyId = process.env.OMNIROUTE_API_KEY_ID || null;
database
.prepare(
`INSERT INTO mcp_tool_audit (tool_name, input_hash, output_summary, duration_ms, api_key_id, success, error_code)
VALUES (?, ?, ?, ?, ?, ?, ?)`
)
.run(
toolName,
inputHash,
outputSummary,
durationMs,
apiKeyId,
success ? 1 : 0,
errorCode || null
);
} catch (err: unknown) {
// Never let audit failure break tool execution
const message = err instanceof Error ? err.message : String(err);
console.error("[MCP Audit] Failed to log:", message);
}
}
/**
* Get recent audit entries (for dashboard/monitoring).
*/
export async function queryAuditEntries(
filters: McpAuditQuery = {}
): Promise<{ entries: McpAuditEntry[]; total: number; limit: number; offset: number }> {
try {
const database = await getDb();
const limit = Math.max(1, Math.min(500, toPositiveInt(filters.limit, 50)));
const offset = Math.max(0, toPositiveInt(filters.offset, 0));
if (!database) return { entries: [], total: 0, limit, offset };
const { whereSql, params } = buildAuditFilterSql(filters);
const totalRow = database
.prepare<AuditCountRow>(`SELECT COUNT(*) as total FROM mcp_tool_audit ${whereSql}`)
.get(...params);
const rows = database
.prepare<AuditEntryRow>(
`SELECT
id,
tool_name,
input_hash,
output_summary,
duration_ms,
api_key_id,
success,
error_code,
created_at
FROM mcp_tool_audit
${whereSql}
ORDER BY created_at DESC
LIMIT ? OFFSET ?`
)
.all(...params, limit, offset);
return {
entries: rows.map(mapAuditEntry),
total: toPositiveInt(totalRow?.total, 0),
limit,
offset,
};
} catch {
return { entries: [], total: 0, limit: 50, offset: 0 };
}
}
/**
* Backward compatible helper for existing callers.
*/
export async function getRecentAuditEntries(limit = 50): Promise<McpAuditEntry[]> {
const result = await queryAuditEntries({ limit, offset: 0 });
return result.entries;
}
/**
* Get audit stats for monitoring.
*/
export async function getAuditStats(): Promise<{
totalCalls: number;
successRate: number;
avgDurationMs: number;
topTools: Array<{ tool: string; count: number }>;
}> {
try {
const database = await getDb();
if (!database) return { totalCalls: 0, successRate: 0, avgDurationMs: 0, topTools: [] };
const stats = database
.prepare(
`SELECT
COUNT(*) as total,
AVG(CASE WHEN success = 1 THEN 1.0 ELSE 0.0 END) as successRate,
AVG(duration_ms) as avgDuration
FROM mcp_tool_audit
WHERE created_at > datetime('now', '-24 hours')`
)
.get() as AuditStatsRow | undefined;
const topTools = database
.prepare(
`SELECT tool_name as tool, COUNT(*) as count
FROM mcp_tool_audit
WHERE created_at > datetime('now', '-24 hours')
GROUP BY tool_name
ORDER BY count DESC
LIMIT 10`
)
.all() as AuditTopToolRow[];
return {
totalCalls: toNumber(stats?.total, 0),
successRate: toNumber(stats?.successRate, 0),
avgDurationMs: toNumber(stats?.avgDuration, 0),
topTools: (topTools || []).map((entry) => ({
tool: toString(entry.tool),
count: toNumber(entry.count, 0),
})),
};
} catch {
return { totalCalls: 0, successRate: 0, avgDurationMs: 0, topTools: [] };
}
}

View File

@@ -0,0 +1,120 @@
/**
* MCP HTTP Transport Layer — Singleton server + SSE/Streamable HTTP handlers.
*
* Runs the MCP server **inside** the Next.js process so it can be toggled
* from the dashboard without requiring `omniroute --mcp`.
*
* Transport modes:
* - SSE: GET /api/mcp/sse (event stream) + POST /api/mcp/sse (messages)
* - Streamable HTTP: POST /api/mcp/stream (messages) + GET /api/mcp/stream (SSE stream) + DELETE /api/mcp/stream (session end)
*/
import { randomUUID } from "node:crypto";
import { createMcpServer } from "./server.ts";
import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
// ────── Singleton ──────────────────────────────────────────
let _server: McpServer | null = null;
let _transport: WebStandardStreamableHTTPServerTransport | null = null;
let _startedAt: number | null = null;
let _activeTransportMode: "sse" | "streamable-http" | null = null;
function ensureServer(mode: "sse" | "streamable-http"): {
server: McpServer;
transport: WebStandardStreamableHTTPServerTransport;
} {
if (_server && _transport && _activeTransportMode === mode) {
return { server: _server, transport: _transport };
}
// Shutdown previous if switching modes
if (_transport) {
try { _transport.close(); } catch { /* ignore */ }
}
_server = createMcpServer();
_transport = new WebStandardStreamableHTTPServerTransport({
sessionIdGenerator: () => randomUUID(),
});
_activeTransportMode = mode;
_startedAt = Date.now();
// Connect server to transport (fire-and-forget, will be ready by first request)
void _server.connect(_transport);
console.log(`[MCP] HTTP transport started (${mode})`);
return { server: _server, transport: _transport };
}
// ────── Streamable HTTP Handler ────────────────────────────
/**
* Handle Streamable HTTP requests (POST / GET / DELETE).
* Used by the Next.js route at /api/mcp/stream.
*/
export async function handleMcpStreamableHTTP(request: Request): Promise<Response> {
const { transport } = ensureServer("streamable-http");
try {
return await transport.handleRequest(request);
} catch (err) {
console.error("[MCP] Streamable HTTP error:", err);
return new Response(
JSON.stringify({ error: "MCP transport error" }),
{ status: 500, headers: { "Content-Type": "application/json" } },
);
}
}
/**
* Handle SSE requests.
* SSE transport is implemented via Streamable HTTP transport with GET for SSE stream
* and POST for messages (the Streamable HTTP transport supports both patterns).
*/
export async function handleMcpSSE(request: Request): Promise<Response> {
const { transport } = ensureServer("sse");
try {
return await transport.handleRequest(request);
} catch (err) {
console.error("[MCP] SSE error:", err);
return new Response(
JSON.stringify({ error: "MCP SSE transport error" }),
{ status: 500, headers: { "Content-Type": "application/json" } },
);
}
}
// ────── Status & Lifecycle ─────────────────────────────────
export function getMcpHttpStatus(): {
online: boolean;
transport: string | null;
startedAt: number | null;
uptime: string | null;
} {
const online = _transport !== null && _activeTransportMode !== null;
return {
online,
transport: _activeTransportMode,
startedAt: _startedAt,
uptime: _startedAt ? `${Math.floor((Date.now() - _startedAt) / 1000)}s` : null,
};
}
export function shutdownMcpHttp(): void {
if (_transport) {
try { _transport.close(); } catch { /* ignore */ }
}
_server = null;
_transport = null;
_activeTransportMode = null;
_startedAt = null;
console.log("[MCP] HTTP transport shutdown");
}
export function isMcpHttpActive(): boolean {
return _transport !== null;
}

Some files were not shown because too many files have changed in this diff Show More