Compare commits

...

55 Commits

Author SHA1 Message Date
diegosouzapw 8b9abcb6cc fix(3.0.0-rc.2): resolve issues #536, #535, #524
Build Electron Desktop App / Validate version (push) Failing after 34s
Build Electron Desktop App / Build Electron (macos-arm64) (push) Has been skipped
Build Electron Desktop App / Build Electron (linux) (push) Has been skipped
Build Electron Desktop App / Build Electron (macos-intel) (push) Has been skipped
Build Electron Desktop App / Build Electron (windows) (push) Has been skipped
Build Electron Desktop App / Create Release (push) Has been skipped
fix(providers): LongCat AI key validation — correct base URL and auth header (#536)
  - baseUrl: longcat.chat/api/v1/chat/completions -> api.longcat.chat/openai
  - authHeader: 'bearer' -> 'Authorization' + authPrefix: 'Bearer'

fix(combo): implement pinnedModel override in comboAgentMiddleware (#535)
  - Previously: pinnedModel was detected but body.model was never updated
  - Now: body = { ...body, model: pinnedModel } when context_cache_protection fires

fix(cli-tools): add OpenCode config save to guide-settings endpoint (#524)
  - Added 'opencode' case to switch in guide-settings/[toolId]/route.ts
  - saveOpenCodeConfig(): XDG_CONFIG_HOME aware, writes [provider.omniroute] TOML block
2026-03-22 13:31:56 -03:00
diegosouzapw 1ecc1908c7 chore(3.0.0-rc.1): bump version to 3.0.0-rc.1, close resolved issues, update CHANGELOG
Build Electron Desktop App / Validate version (push) Failing after 30s
Build Electron Desktop App / Build Electron (macos-arm64) (push) Has been skipped
Build Electron Desktop App / Build Electron (linux) (push) Has been skipped
Build Electron Desktop App / Build Electron (macos-intel) (push) Has been skipped
Build Electron Desktop App / Build Electron (windows) (push) Has been skipped
Build Electron Desktop App / Create Release (push) Has been skipped
- package.json: 2.9.5 → 3.0.0-rc.1
- docs/openapi.yaml: version → 3.0.0-rc.1
- CHANGELOG.md: add [3.0.0-rc.1] section with all batch1-3 fixes
- scripts/check-docs-sync.mjs: isSemver now accepts pre-release versions (X.Y.Z-prerelease.N)

Closed issues: #489, #492, #510, #513, #520, #521, #522, #525, #527, #532
RC versioning: rc.1 → rc.2 → rc.N on each VPS deploy until v3.0.0 is approved
2026-03-22 12:25:30 -03:00
diegosouzapw 6a2c7b467d fix(3.0.0-rc/batch3): convert tool_result blocks to text to stop Codex loop (#527)
fix(chat): convert tool_result content blocks to [Tool Result: id] text (#527)
  - Previously, tool_result blocks in user messages were silently dropped
  - This caused an infinite loop when Claude Code + superpowers routed to Codex:
    Codex never received the tool response and kept re-requesting the tool
  - Now: tool_result → text block '[Tool Result: {id}]\n{content}'
  - Handles string, array-of-text, and JSON-serialized content types

docs(issues): add Turbopack postinstall workaround on #509 and #508
docs(issues): note that #464 (API key provisioning) is on the v3.0 roadmap
2026-03-22 11:47:39 -03:00
diegosouzapw 0acef57865 fix(3.0.0-rc/batch2): resolve issues #510, #492, and improve #520, #529
fix(cli): normalize MSYS2/Git-Bash paths in cliRuntime.ts (#510)
  - Add normalizeMsys2Path() helper: /c/Program Files/... → C:\Program Files\...
  - Apply to both Windows 'where' and Unix 'command -v' path resolution
  - Fixes 'CLI not detected' on Windows when running Git Bash / MSYS2

fix(cli-launcher): detect mise/nvm on server.js not found error (#492)
  - Show targeted fix instructions based on which Node manager is in use
  - mise users: told to use npx or mise exec
  - nvm users: reminded to nvm use --lts before reinstalling

docs(issues): add pnpm bindings workaround comment (#520)
docs(issues): note OpenCode/Lobehub icons coming in v3.0.0 (#529)
2026-03-22 11:41:04 -03:00
diegosouzapw 43046ee649 fix(3.0.0-rc/batch1): resolve issues #521, #522, #525, #532, #489
fix(login): redirect to /dashboard/onboarding when API returns needsSetup:true (#521)
  - Handle the case where user skips password setup and lands on login
  - Instead of showing a cryptic error, redirect to onboarding flow

fix(api-manager): replace useless 'copy masked key' button with lock tooltip (#522)
  - Copying a masked key (sk-proj123****abcd) is misleading and useless
  - Show a lock icon on hover explaining key is only available at creation time
  - Add i18n key 'keyOnlyAvailableAtCreation'

fix(opencode-go): use zen/v1 for API key validation, not zen/go/v1 (#532)
  - Added testKeyBaseUrl field to RegistryEntry interface
  - opencode-go: testKeyBaseUrl → zen/v1 (same key authenticates both tiers)
  - validation.ts: resolveBaseUrl for key testing now prefers testKeyBaseUrl

fix(antigravity): return structured 422 error when projectId is missing (#489)
  - Instead of throwing (crash), executor returns an OpenAI-format error JSON
  - Client receives message with instruction to reconnect OAuth
  - Prevents opaque 500 errors in the proxy logs

chore: close #525 (OmniRoute = 9router — same project, different name)
docs: add Docker password reset comment on #513 with INITIAL_PASSWORD workaround
2026-03-22 11:31:34 -03:00
Diego Rodrigues de Sa e Souza a15fda0c08 Merge pull request #534 from diegosouzapw/release/v2.9.5
Build Electron Desktop App / Validate version (push) Failing after 33s
Build Electron Desktop App / Build Electron (macos-arm64) (push) Has been skipped
Build Electron Desktop App / Build Electron (linux) (push) Has been skipped
Build Electron Desktop App / Build Electron (macos-intel) (push) Has been skipped
Build Electron Desktop App / Build Electron (windows) (push) Has been skipped
Build Electron Desktop App / Create Release (push) Has been skipped
chore(release): v2.9.5 — OpenCode providers, embedding fix, CLI masked key fix
2026-03-22 10:32:33 -03:00
diegosouzapw e5988764ce chore(release): v2.9.5 — OpenCode providers, embedding credentials fix, CLI masked key fix, CACHE_TAG_PATTERN fix
- feat(providers): add OpenCode Zen and Go providers with multi-format executor (PR #530 by @kang-heewon)
- fix(embeddings): use provider node ID for custom embedding provider credential lookup (PR #528 by @jacob2826)
- fix(cli-tools): resolve real API key from DB (keyId) before writing to CLI config files (#523, #526)
- fix(combo): update CACHE_TAG_PATTERN to match literal \\n prefix/suffix around omniModel tag (#531)
- chore: bump version to 2.9.5 in package.json + docs/openapi.yaml
- docs: update CHANGELOG.md with v2.9.5 release notes
2026-03-22 10:30:04 -03:00
diegosouzapw 9c9d9b5a8d feat(providers): add OpenCode Zen and Go providers (#530) 2026-03-22 10:25:15 -03:00
kang-heewon 44dc564d85 chore: remove GHCR workflow from upstream PR 2026-03-22 10:24:50 -03:00
kang-heewon 83e367afab ci: add GHCR publish workflow for fork deployments 2026-03-22 10:24:50 -03:00
kang-heewon 8b7e7c2669 test(providers): improve OpencodeExecutor tests to avoid internal state coupling 2026-03-22 10:24:50 -03:00
kang-heewon 53474021b7 test(providers): add unit tests for OpencodeExecutor 2026-03-22 10:24:50 -03:00
kang-heewon da1ed1b5b2 feat(providers): register opencode-zen and opencode-go in provider registry 2026-03-22 10:24:50 -03:00
kang-heewon e08d661600 feat(providers): register opencode executors and add UI metadata
- Register OpencodeExecutor for 'opencode-zen' and 'opencode-go' in executors map
- Add OpencodeExecutor export in index.ts
- Add UI metadata for both providers in APIKEY_PROVIDERS:
  - OpenCode Zen: https://opencode.ai/zen
  - OpenCode Go: https://opencode.ai/zen/go
- Both use 'opencode' icon with #6366f1 color
2026-03-22 10:24:50 -03:00
kang-heewon 1aa1bc7a26 feat(providers): add OpencodeExecutor for opencode-zen/go multi-format routing 2026-03-22 10:23:32 -03:00
Diego Rodrigues de Sa e Souza 47634e942e Merge pull request #533 from diegosouzapw/fix/issues-521-523-526-531
fix: resolve masked key in CLI config saves + CACHE_TAG_PATTERN \n handling (#523, #526, #531)
2026-03-22 10:23:19 -03:00
Diego Rodrigues de Sa e Souza 15466cbf1a Merge pull request #528 from jacob2826/codex/fix-embedding-compatible-provider-credentials
fix: use provider node credentials for custom embedding providers
2026-03-22 10:23:16 -03:00
diegosouzapw 2a749db427 fix: resolve masked key bug in CLI config saves, fix CACHE_TAG_PATTERN for \n prefix (#523, #526, #531)
fix(cli-tools): save real API key to CLI config files instead of masked string (#523, #526)
  - claude-settings/route.ts: accept keyId, look up real key from DB (getApiKeyById)
  - cline-settings/route.ts: same keyId resolution pattern
  - openclaw-settings/route.ts: same keyId resolution pattern
  - ClaudeToolCard.tsx: store key.id as selected value, send keyId in POST body
  The /api/keys endpoint returns masked strings (first8+****+last4) which were being
  written verbatim to ~/.claude/settings.json and similar config files, causing auth
  failures on CLI tool launch.

fix(combo): update CACHE_TAG_PATTERN to strip surrounding \\n sequences (#531)
  - comboAgentMiddleware.ts: non-global regex now matches literal \\n (backslash-n)
    and actual newline U+000A that combo.ts injects around the <omniModel> tag.
2026-03-22 09:49:03 -03:00
jacob2826 ecccce86e4 fix: use provider node credentials for embeddings 2026-03-22 16:22:58 +08:00
Diego Rodrigues de Sa e Souza bf3f64bea4 Merge pull request #519 from diegosouzapw/release/2.9.4
Build Electron Desktop App / Validate version (push) Failing after 32s
Build Electron Desktop App / Build Electron (macos-arm64) (push) Has been skipped
Build Electron Desktop App / Build Electron (linux) (push) Has been skipped
Build Electron Desktop App / Build Electron (macos-intel) (push) Has been skipped
Build Electron Desktop App / Build Electron (windows) (push) Has been skipped
Build Electron Desktop App / Create Release (push) Has been skipped
chore(release): v2.9.4 — bug fixes (#491, #515, #517)
2026-03-21 17:40:23 -03:00
diegosouzapw 2f2d6b8535 chore(release): v2.9.4 — bug fixes (#491, #515, #517)
- fix(translator): preserve prompt_cache_key in Responses API translation (#517)
- fix(combo): escape \n in tagContent for valid JSON injection (#515)
- fix(usage): sync expired token status back to DB on live auth failure (#491)
- chore: bump version to 2.9.4 in package.json + docs/openapi.yaml
- docs: update CHANGELOG.md with v2.9.4 release notes
2026-03-21 17:37:51 -03:00
Diego Rodrigues de Sa e Souza d68c884649 Merge pull request #518 from diegosouzapw/fix/issue-517-515-prompt-cache-key-tagcontent
fix: preserve prompt_cache_key in Responses API, escape \n in tagContent (#517, #515)
2026-03-21 17:32:24 -03:00
diegosouzapw 8b556de03b fix: preserve prompt_cache_key in Responses API translation, escape \n in tagContent (#517, #515)
fix(translator): preserve prompt_cache_key when translating Responses API requests
  (#517) — prompt_cache_key is an account-affinity signal used by Codex for
  prompt cache routing. Deleting it from the translated request prevented full
  cache effectiveness. Removed delete from openai-responses.ts and
  responsesApiHelper.ts cleanup blocks.

fix(combo): escape \n in tagContent so injected JSON string is valid (#515)
  — omniModel tag content used template literal newlines (U+000A) which produce
  unescaped newline chars inside a JSON string value. Replaced with literal \n
  escape sequences for valid JSON injection in streaming SSE content chunks.
2026-03-21 17:09:13 -03:00
Diego Rodrigues de Sa e Souza 7229af53c3 Merge pull request #516 from diegosouzapw/release/2.9.3
Build Electron Desktop App / Validate version (push) Failing after 25s
Build Electron Desktop App / Build Electron (macos-arm64) (push) Has been skipped
Build Electron Desktop App / Build Electron (linux) (push) Has been skipped
Build Electron Desktop App / Build Electron (macos-intel) (push) Has been skipped
Build Electron Desktop App / Build Electron (windows) (push) Has been skipped
Build Electron Desktop App / Create Release (push) Has been skipped
feat(providers): 5 new free AI providers — v2.9.3
2026-03-21 16:55:29 -03:00
diegosouzapw 81b3034c2f feat(providers/logos): add logos for 5 new free providers
- public/providers/longcat.png — pink cat icon (generated)
- public/providers/pollinations.png — pixel bee icon (generated)
- public/providers/aimlapi.png — indigo neural network icon (generated)
- public/providers/cloudflare-ai.svg — Cloudflare official SVG (simpleicons.org)
- public/providers/scaleway.svg — Scaleway official SVG (simpleicons.org)

Icons serve at /providers/{id}.png (PNG fallback to SVG)
2026-03-21 16:47:49 -03:00
diegosouzapw f0419396b5 chore(release): bump version to 2.9.3, update CHANGELOG
- Version bumped from 2.9.2 → 2.9.3 in package.json + docs/openapi.yaml
- CHANGELOG.md updated with full release notes for 2.9.3
  (5 new free providers, 2 metadata updates, 2 custom executors, docs)
2026-03-21 15:44:35 -03:00
diegosouzapw 6b9c2754e8 feat(providers): add LongCat AI, Pollinations, Cloudflare AI, Scaleway, AI/ML API
New free providers:
- LongCat AI (lc/): 50M tokens/day free during public beta
- Pollinations AI (pol/): no API key needed, GPT-5/Claude/DeepSeek/Llama free
- Cloudflare Workers AI (cf/): 10K Neurons/day, ~150 LLM responses, Whisper free
- Scaleway AI (scw/): 1M free tokens for new accounts (EU/GDPR, Paris)
- AI/ML API (aiml/): $0.025/day credits, 200+ models via single endpoint

Provider metadata updates:
- Together AI: hasFree=true + 3 permanently free model IDs (Llama 70B, Vision, DeepSeek)
- Gemini: hasFree=true + freeNote (1,500 req/day free, no credit card)
- NVIDIA NIM: already had hasFree=true, confirmed correct

New executors:
- open-sse/executors/pollinations.ts: optional auth (no key support)
- open-sse/executors/cloudflare-ai.ts: dynamic URL with accountId credential

Documentation:
- README.md: 11-provider Ultimate Free Stack, 4 new pricing table rows
- README.md: LongCat/Pollinations/Cloudflare AI/Scaleway provider detail sections
- docs/i18n/pt-BR/README.md: updated pricing table + 4 new free provider sections
- docs/i18n/cs/README.md: combo stack updated

Tests: 821/821 pass (no regressions)
2026-03-21 15:40:05 -03:00
diegosouzapw 8edb131f8b docs: add npm downloads and Docker Hub pulls badges to README 2026-03-21 14:48:48 -03:00
Diego Rodrigues de Sa e Souza d6f6520a79 Merge pull request #514 from diegosouzapw/release/v2.9.2
Build Electron Desktop App / Validate version (push) Failing after 35s
Build Electron Desktop App / Build Electron (macos-arm64) (push) Has been skipped
Build Electron Desktop App / Build Electron (linux) (push) Has been skipped
Build Electron Desktop App / Build Electron (macos-intel) (push) Has been skipped
Build Electron Desktop App / Build Electron (windows) (push) Has been skipped
Build Electron Desktop App / Create Release (push) Has been skipped
chore(release): v2.9.2 — Transcription Content-Type fix, Deepgram language detection, TTS error display
2026-03-21 14:03:33 -03:00
diegosouzapw cc2bb4d719 chore: update generate-release workflow to two-phase PR-first flow
Phase 1: bump, docs, i18n, commit, push, open PR → STOP for user confirmation
Phase 2 (post-merge): tag, GitHub release, Docker Hub, deploy both VPS
2026-03-21 13:58:08 -03:00
diegosouzapw 3859f1c9ae chore(release): v2.9.2 — transcription Content-Type fix, Deepgram language detection, TTS error display
- fix(transcription): resolveAudioContentType() maps video/mp4 → audio/mp4 for Deepgram/HuggingFace
- fix(transcription): detect_language=true + punctuate=true for Deepgram auto-detection
- fix(tts): upstreamErrorResponse() correctly extracts string from nested error objects
- docs: README transcription/TTS rows updated with provider counts and capabilities
- i18n: sync 29/30 language README files with updated feature descriptions
- chore: bump version 2.9.1 → 2.9.2
2026-03-21 13:54:22 -03:00
diegosouzapw 5f8d774e19 fix: [object Object] error display in TTS/transcription upstream errors
upstreamErrorResponse() now guards against parsed.error being an
object (e.g. ElevenLabs { error: { message, status_code } }) instead
of blindly using it as the error message string.
Both audioSpeech.ts and audioTranscription.ts fixed.
2026-03-21 10:47:55 -03:00
diegosouzapw 538a3e855c fix: transcription Content-Type + language detection for Deepgram/HuggingFace
- Add resolveAudioContentType() to map video/* MIME to audio/* (fixes .mp4 uploads returning 'no speech detected')
- Add detect_language=true for Deepgram auto-language detection (fixes non-English audio)
- Add punctuate=true for better output quality
- Forward language form param to Deepgram when provided
- Apply same Content-Type fix to HuggingFace handler
2026-03-21 10:38:57 -03:00
diegosouzapw 03f2ef1e2b fix: omniModel SSE tag data loss + v2.9.1 release (#511)
Build Electron Desktop App / Validate version (push) Failing after 38s
Build Electron Desktop App / Build Electron (macos-arm64) (push) Has been skipped
Build Electron Desktop App / Build Electron (linux) (push) Has been skipped
Build Electron Desktop App / Build Electron (macos-intel) (push) Has been skipped
Build Electron Desktop App / Build Electron (windows) (push) Has been skipped
Build Electron Desktop App / Create Release (push) Has been skipped
2026-03-21 08:55:28 -03:00
Diego Rodrigues de Sa e Souza 237d0746cf Merge pull request #512 from zhangqiang8vip/feat/zws-v6
feat: per-protocol model compatibility, HMR leak fixes, and dev performance (V2-V5)
2026-03-21 08:53:54 -03:00
zhang-qiang 33b6c58087 fix(compat): store explicit false for per-protocol normalizeToolCallId
The truthy check treated false as falsy and deleted the property, preventing users from explicitly disabling normalization for a specific protocol when the top-level flag was true. Now stores both true and false values, consistent with preserveOpenAIDeveloperRole handling.

Made-with: Cursor
2026-03-21 16:38:46 +08:00
zhang-qiang e96b023d04 fix(ci): reword comment in default.ts to avoid t11 any-budget false positive
The word 'any' in a JSDoc comment was matched by the regex-based t11 checker. Reworded to 'prefixes' to eliminate the false positive.

Made-with: Cursor
2026-03-21 16:33:44 +08:00
zhang-qiang 7ac1d4621b Merge remote-tracking branch 'upstream/main' into feat/zws-v6 2026-03-21 16:32:57 +08:00
zhang-qiang a2d7cbe8fe feat(compat): per-protocol model compatibility config (V5)
Add per-protocol compatibility options (compatByProtocol) allowing users to configure normalizeToolCallId and preserveOpenAIDeveloperRole per client request protocol (OpenAI Chat, Responses API, Anthropic Messages) instead of globally. Includes frontend Map lookup optimization, type safety improvements, and client-safe constant extraction.

Made-with: Cursor
2026-03-21 15:23:42 +08:00
diegosouzapw c74ed29739 chore(release): v2.9.0 — cross-platform machineId, per-key rate limits, streaming cache, Alibaba DashScope, search analytics, ZWS v5, 8 issues closed
Build Electron Desktop App / Validate version (push) Failing after 33s
Build Electron Desktop App / Build Electron (macos-arm64) (push) Has been skipped
Build Electron Desktop App / Build Electron (linux) (push) Has been skipped
Build Electron Desktop App / Build Electron (macos-intel) (push) Has been skipped
Build Electron Desktop App / Build Electron (windows) (push) Has been skipped
Build Electron Desktop App / Create Release (push) Has been skipped
2026-03-20 20:12:34 -03:00
diegosouzapw 6c8501f122 fix: cross-platform machineId without process.platform branching (#506)
Rewrite getMachineIdRaw() to use a try/catch waterfall instead of
process.platform conditionals. Next.js SWC bundler evaluates
process.platform at BUILD time, so when built on Linux, the win32
branch was dead-code-eliminated — causing 'head is not recognized'
errors on Windows.

New approach:
1. Try Windows REG.exe (existsSync check, not platform check)
2. Try macOS ioreg command
3. Try reading /etc/machine-id directly (no head/pipe)
4. Try hostname command
5. Fallback to os.hostname()

Also eliminates the patch-machine-id.cjs post-install workaround.
2026-03-20 20:07:19 -03:00
diegosouzapw 941e945f74 Merge branch 'feat/zws-v5' 2026-03-20 19:36:25 -03:00
diegosouzapw f2844d59e4 Merge branch 'feat/search-provider-routing' 2026-03-20 19:36:17 -03:00
diegosouzapw 047ff187f6 Merge branch 'feat/custom-endpoint-paths'
# Conflicts:
#	src/shared/constants/providers.ts
2026-03-20 19:34:10 -03:00
diegosouzapw 1136c40811 Merge branch 'fix/tools-filter-claude-format' 2026-03-20 19:33:08 -03:00
diegosouzapw 5a78dc864f Merge branch 'fix/issue-456-458-combo-schema-mitm-windows' 2026-03-20 19:33:08 -03:00
diegosouzapw 15c98c3048 Merge branch 'fix/developer-role-param-error' 2026-03-20 19:33:07 -03:00
diegosouzapw 0a5b005ce5 fix: resolve multiple issues (#493, #490, #452)
- #493: Fix custom provider model naming — removed incorrect prefix
  stripping in DefaultExecutor.transformRequest() that broke org-scoped
  model IDs like 'zai-org/GLM-5-FP8'

- #490: Enable context cache protection for streaming responses using
  TransformStream to inject omniModel tag as final SSE content delta
  before [DONE] marker

- #452: Add per-API-key request-count limits (max_requests_per_day,
  max_requests_per_minute) with in-memory sliding window counter,
  schema auto-migration, and Check 5 in enforceApiKeyPolicy()
2026-03-20 19:26:21 -03:00
diegosouzapw 47959ee395 Merge branch 'main' into feat/zws-v5 2026-03-20 16:10:59 -03:00
diegosouzapw c54a57838e fix: cleanup PR #494 — remove ZWS_README, fix KIRO MITM card (#487), generify AntigravityToolCard 2026-03-20 12:19:33 -03:00
zhang-qiang 13c45807ef feat: protocol-scoped model compat (V3)
- compatByProtocol per openai/openai-responses/claude

- getters take sourceFormat; chatCore passes it

- UI: protocol selector in compat popover, dark mode select

- shared/constants/modelCompat for client-safe import (fix node:crypto build)

- ZWS_README_V3.md

Made-with: Cursor
2026-03-20 22:06:03 +08:00
diegosouzapw 00df10c29a "fix: resolved UI combo setting schema strip (#458)"
"fix: safe crypto fallback for MITM on windows (#456)"
2026-03-18 17:16:30 -03:00
diegosouzapw 41d91d628a feat(search/analytics): add Search tab to analytics dashboard + GET /api/v1/search/analytics
- SearchAnalyticsTab: provider breakdown, cache hit rate, cost summary, KPI cards
- /api/v1/search/analytics: query call_logs (request_type='search') for stats
- analytics/page.tsx: added 'Search' tab alongside Overview and Evals

Closes missing dashboard tracking identified in PR review.
2026-03-17 16:15:28 -03:00
diegosouzapw 605c3f9be1 feat(provider): add Alibaba Cloud DashScope + path validation for custom endpoint paths
- Add Alibaba Cloud (DashScope) as OpenAI-compatible provider with 12 Qwen models:
  qwen-max, qwen-plus, qwen-turbo, qwen3-coder-plus/flash, qwq-plus,
  qwq-32b, qwen3-32b, qwen3-235b-a22b
  International endpoint: dashscope-intl.aliyuncs.com/compatible-mode/v1
  Auth: Bearer API key (same as groq/xai/mistral)

- Add path traversal protection to custom endpoint paths (PR #400):
  sanitizePath() validates chatPath/modelsPath values:
  must start with '/', no '..' segments, no null bytes, max 512 chars

Closes #400 (custom endpoint paths), part of Alibaba provider integration
2026-03-16 09:44:17 -03:00
diegosouzapw 2f0894c220 test: add unit tests for Anthropic-format tools filter fix (PR #397)
8 tests covering:
- Valid OpenAI format tools (tool.function.name) preserved
- Valid Anthropic format tools (tool.name) preserved
- Empty names in both formats filtered
- Mixed format array handling
- Null/whitespace edge cases

Regression tests verify the fix from PR #397 prevents all anthropic-
format tools from being silently dropped by the empty-name filter.
2026-03-16 09:38:34 -03:00
90 changed files with 3480 additions and 870 deletions
+108 -20
View File
@@ -4,16 +4,36 @@ description: Create a new release, bump version up to 1.x.10 threshold, update c
# Generate Release Workflow
Bump version, finalize CHANGELOG, commit, tag, push, publish to npm, and create GitHub release.
Bump version, finalize CHANGELOG, commit, open a **PR to main** and wait for user confirmation before tagging, publishing, and deploying.
> **VERSION RULE: Always use PATCH bumps (2.x.y → 2.x.y+1)**
> NEVER use `npm version minor` or `npm version major`.
> Always use: `npm version patch --no-git-tag-version`
> The threshold rule: when `y` reaches 10, bump to `2.(x+1).0` — e.g. `2.1.10` → `2.2.0`.
## Steps
---
### 1. Determine new version
## ⚠️ Two-Phase Flow
```
Phase 1 (automated): bump → docs → i18n → commit → push → open PR
↕ 🛑 STOP: Notify user, wait for PR confirmation
Phase 2 (post-merge): tag → publish → GitHub release → Docker → deploy
```
**NEVER push directly to main or create tags before the user confirms the PR.**
---
## Phase 1: Pre-Merge
### 1. Create release branch
```bash
git checkout -b release/v2.x.y
```
### 2. Determine new version
Check current version in `package.json` and increment the **patch** number only:
@@ -27,11 +47,6 @@ Version format: `2.x.y` — examples:
- `2.1.9``2.1.10` (patch)
- `2.1.10``2.2.0` (minor threshold — do manually with `sed`)
```bash
# ALWAYS use patch:
npm version patch --no-git-tag-version
```
> **⚠️ ATOMIC COMMIT RULE — Version bump MUST happen before committing feature files.**
>
> **CORRECT order:**
@@ -53,7 +68,7 @@ npm version patch --no-git-tag-version
> This ensures that `git show v2.x.y` always contains both code changes and the version bump together.
> The GitHub release tag will point to a commit that includes ALL changes for that version.
### 2. Regenerate lock file (REQUIRED after version bump)
### 3. Regenerate lock file (REQUIRED after version bump)
**Mandatory** — skipping causes `@swc/helpers` lock mismatch and CI failures:
@@ -61,7 +76,7 @@ npm version patch --no-git-tag-version
npm install
```
### 3. Finalize CHANGELOG.md
### 4. Finalize CHANGELOG.md
Replace `[Unreleased]` header with the new version and date.
Keep an empty `## [Unreleased]` section above it.
@@ -74,7 +89,7 @@ Keep an empty `## [Unreleased]` section above it.
## [2.x.y] — YYYY-MM-DD
```
### 4. Update openapi.yaml version ⚠️ MANDATORY
### 5. Update openapi.yaml version ⚠️ MANDATORY
> **CI will fail** if `docs/openapi.yaml` version ≠ `package.json` version (`check:docs-sync` enforces this).
@@ -84,33 +99,97 @@ Keep an empty `## [Unreleased]` section above it.
VERSION=$(node -p "require('./package.json').version") && sed -i "s/ version: .*/ version: $VERSION/" docs/openapi.yaml && echo "✓ openapi.yaml → $VERSION"
```
### 5. Stage, commit, and tag
### 6. Update README.md and i18n docs
Run `/update-docs` workflow steps to:
- Update feature table rows in `README.md`
- Sync changes to all 29 language `docs/i18n/*/README.md` files
- Update `docs/FEATURES.md` if Settings section changed
### 7. Run tests
// turbo
```bash
npm test
```
All tests must pass before creating the PR.
### 8. Stage, commit, and push
// turbo-all
```bash
git add package.json package-lock.json CHANGELOG.md docs/openapi.yaml
git add -A
git commit -m "chore(release): v2.x.y — summary of changes"
git push origin release/v2.x.y
```
### 9. Open PR to main
```bash
gh pr create \
--repo diegosouzapw/OmniRoute \
--base main \
--head release/v2.x.y \
--title "chore(release): v2.x.y — summary" \
--body "## 🚀 Release v2.x.y
### Changes
...
### Tests
- X/X tests pass
### ⚠️ After merging: run Phase 2 steps to tag, publish, and deploy."
```
### 10. 🛑 STOP — Notify User & Await PR Confirmation
**This is a mandatory stop point.** Use `notify_user` with `BlockedOnUser: true`:
Inform the user:
- PR URL
- Summary of changes
- Test results
- List of files changed
**DO NOT proceed to Phase 2 until the user confirms the PR looks good and merges it.**
---
## Phase 2: Post-Merge (only after user confirms)
> Run these steps only AFTER the user has merged the PR.
### 11. Pull main and create tag
```bash
git checkout main
git pull origin main
git tag -a v2.x.y -m "Release v2.x.y"
```
### 6. Push to GitHub
### 12. Push tag to GitHub
```bash
git push origin main --tags
git push origin --tags
```
### 7. Create GitHub release
### 13. Create GitHub release
```bash
gh release create v2.x.y --title "v2.x.y — summary" --notes "..."
```
### 8. 🐳 Trigger Docker Hub build (MANDATORY — keep npm and Docker in sync)
### 14. 🐳 Trigger Docker Hub build (MANDATORY — keep npm and Docker in sync)
> **CRITICAL**: Docker Hub and npm MUST always publish the same version.
> The Docker image is built automatically via GitHub Actions when a new tag is pushed.
> After pushing the tag in step 5-6, **verify the workflow runs**:
> After pushing the tag in step 11-12, **verify the workflow runs**:
```bash
# Verify the Docker workflow triggered
@@ -129,7 +208,7 @@ If the Docker build was not triggered automatically, trigger it manually:
gh workflow run docker-publish.yml --repo diegosouzapw/OmniRoute --ref v2.x.y
```
### 9. Deploy to BOTH VPS environments (MANDATORY)
### 15. Deploy to BOTH VPS environments (MANDATORY)
> Always deploy to **both** environments after every release.
> See `/deploy-vps` workflow for detailed steps.
@@ -151,18 +230,27 @@ curl -s -o /dev/null -w "LOCAL: HTTP %{http_code}\n" http://192.168.0.15:20128/
curl -s -o /dev/null -w "AKAMAI: HTTP %{http_code}\n" http://69.164.221.35:20128/
```
### 16. Clean up release branch
```bash
git branch -d release/v2.x.y
```
---
## 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`
- Lock file sync errors are caused by skipping `npm install` after version bump
- Use `gh auth switch -u diegosouzapw` if git push fails with wrong account
## Known CI Pitfalls
| CI failure | Cause | Fix |
| ------------------------------------------------------------------------- | -------------------------------------------------------- | ---------------------------------------------------------------------- |
| `[docs-sync] FAIL - OpenAPI version differs from package.json` | Skipped step 4`docs/openapi.yaml` version not updated | Run step 4 (`sed -i ...`) and commit |
| `[docs-sync] FAIL - OpenAPI version differs from package.json` | Skipped step 5`docs/openapi.yaml` version not updated | Run step 5 (`sed -i ...`) and commit |
| `[docs-sync] FAIL - CHANGELOG.md first section must be "## [Unreleased]"` | `## [Unreleased]` missing or not at top of CHANGELOG | Add `## [Unreleased]\n\n---\n` before the first versioned `## [x.y.z]` |
| Electron Linux `.deb` build fails (`FpmTarget` error) | `fpm` Ruby gem not installed on `ubuntu-latest` runner | Already fixed in `electron-release.yml` (`gem install fpm` step) |
| Docker Hub `502 error writing layer blob` | Transient Docker Hub network error during ARM64 push | Re-run the Docker publish workflow; no code change needed |
+190
View File
@@ -4,6 +4,196 @@
---
## [3.0.0-rc.2] - 2026-03-22
### 🔧 Bug Fixes
- **#536** — LongCat AI key validation: fixed baseUrl (`api.longcat.chat/openai`) and authHeader (`Authorization: Bearer`)
- **#535** — Pinned model override: `body.model` is now set to `pinnedModel` when context-cache protection detects a pinned model
- **#524** — OpenCode config now saved correctly: added `saveOpenCodeConfig()` handler (XDG_CONFIG_HOME aware, writes TOML)
---
## [3.0.0-rc.1] - 2026-03-22
### 🔧 Bug Fixes
- **#521** — Login no longer gets stuck after skipping password setup (redirects to onboarding)
- **#522** — API Manager: Removed misleading "Copy masked key" button (replaced with lock icon tooltip)
- **#527** — Claude Code + Codex superpowers loop: `tool_result` blocks now converted to text instead of dropped
- **#532** — OpenCode GO API key validation now uses the correct `zen/v1` endpoint (`testKeyBaseUrl`)
- **#489** — Antigravity: missing `googleProjectId` returns structured 422 error with reconnect guidance
- **#510** — Windows: MSYS2/Git-Bash paths (`/c/Program Files/...`) are now normalized to `C:\\Program Files\\...`
- **#492** — `omniroute` CLI now detects `mise`/`nvm` when `app/server.js` is missing and shows targeted fix
### 📖 Documentation
- **#513** — Docker password reset: `INITIAL_PASSWORD` env var workaround documented
- **#520** — pnpm: `pnpm approve-builds better-sqlite3` documented
### ✅ Closed Issues
#489, #492, #510, #513, #520, #521, #522, #525, #527, #532
---
## [2.9.5] — 2026-03-22
> Sprint: New OpenCode providers, embedding credentials fix, CLI masked key bug, CACHE_TAG_PATTERN fix.
### 🐛 Bug Fixes
- **CLI tools save masked API key to config files** — `claude-settings`, `cline-settings`, and `openclaw-settings` POST routes now accept a `keyId` param and resolve the real API key from DB before writing to disk. `ClaudeToolCard` updated to send `keyId` instead of the masked display string. Fixes #523, #526.
- **Custom embedding providers: `No credentials` error** — `/v1/embeddings` now tracks `credentialsProviderId` separately from the routing prefix, so credentials are fetched from the matching provider node ID rather than the public prefix string. Fixes a regression where `google/gemini-embedding-001` and similar custom-provider models would always fail with a credentials error. Fixes #532-related. (PR #528 by @jacob2826)
- **Context cache protection regex misses `\n` prefix** — `CACHE_TAG_PATTERN` in `comboAgentMiddleware.ts` updated to match both literal `\n` (backslash-n) and actual newline U+000A that `combo.ts` streaming injects around the `<omniModel>` tag after fix #515. Fixes #531.
### ✨ New Providers
- **OpenCode Zen** — Free tier gateway at `opencode.ai/zen/v1` with 3 models: `minimax-m2.5-free`, `big-pickle`, `gpt-5-nano`
- **OpenCode Go** — Subscription service at `opencode.ai/zen/go/v1` with 4 models: `glm-5`, `kimi-k2.5`, `minimax-m2.7` (Claude format), `minimax-m2.5` (Claude format)
- Both providers use the new `OpencodeExecutor` which routes dynamically to `/chat/completions`, `/messages`, `/responses`, or `/models/{model}:generateContent` based on the requested model. (PR #530 by @kang-heewon)
---
## [2.9.4] — 2026-03-21
> Sprint: Bug fixes — preserve Codex prompt cache key, fix tagContent JSON escaping, sync expired token status to DB.
### 🐛 Bug Fixes
- **fix(translator)**: Preserve `prompt_cache_key` in Responses API → Chat Completions translation (#517)
— The field is a cache-affinity signal used by Codex; stripping it was preventing prompt cache hits.
Fixed in `openai-responses.ts` and `responsesApiHelper.ts`.
- **fix(combo)**: Escape `\n` in `tagContent` so injected JSON string is valid (#515)
— Template literal newlines (U+000A) are not allowed unescaped inside JSON string values.
Replaced with `\\n` literal sequences in `open-sse/services/combo.ts`.
- **fix(usage)**: Sync expired token status back to DB on live auth failure (#491)
— When the Limits & Quotas live check returns 401/403, the connection `testStatus` is now updated
to `"expired"` in the database so the Providers page reflects the same degraded state.
Fixed in `src/app/api/usage/[connectionId]/route.ts`.
---
## [2.9.3] — 2026-03-21
> Sprint: Add 5 new free AI providers — LongCat, Pollinations, Cloudflare AI, Scaleway, AI/ML API.
### ✨ New Providers
- **feat(providers/longcat)**: Add LongCat AI (`lc/`) — 50M tokens/day free (Flash-Lite) + 500K/day (Chat/Thinking) during public beta. OpenAI-compatible, standard Bearer auth.
- **feat(providers/pollinations)**: Add Pollinations AI (`pol/`) — no API key required. Proxies GPT-5, Claude, Gemini, DeepSeek V3, Llama 4 (1 req/15s free). Custom executor handles optional auth.
- **feat(providers/cloudflare-ai)**: Add Cloudflare Workers AI (`cf/`) — 10K Neurons/day free (~150 LLM responses or 500s Whisper audio). 50+ models on global edge. Custom executor builds dynamic URL with `accountId` from credentials.
- **feat(providers/scaleway)**: Add Scaleway Generative APIs (`scw/`) — 1M free tokens for new accounts. EU/GDPR compliant (Paris). Qwen3 235B, Llama 3.1 70B, Mistral Small 3.2.
- **feat(providers/aimlapi)**: Add AI/ML API (`aiml/`) — $0.025/day free credit, 200+ models (GPT-4o, Claude, Gemini, Llama) via single aggregator endpoint.
### 🔄 Provider Updates
- **feat(providers/together)**: Add `hasFree: true` + 3 permanently free model IDs: `Llama-3.3-70B-Instruct-Turbo-Free`, `Llama-Vision-Free`, `DeepSeek-R1-Distill-Llama-70B-Free`
- **feat(providers/gemini)**: Add `hasFree: true` + `freeNote` (1,500 req/day, no credit card needed, aistudio.google.com)
- **chore(providers/gemini)**: Rename display name to `Gemini (Google AI Studio)` for clarity
### ⚙️ Infrastructure
- **feat(executors/pollinations)**: New `PollinationsExecutor` — omits `Authorization` header when no API key provided
- **feat(executors/cloudflare-ai)**: New `CloudflareAIExecutor` — dynamic URL construction requires `accountId` in provider credentials
- **feat(executors)**: Register `pollinations`, `pol`, `cloudflare-ai`, `cf` executor mappings
### 📝 Documentation
- **docs(readme)**: Expanded free combo stack to 11 providers ($0 forever)
- **docs(readme)**: Added 4 new free provider sections (LongCat, Pollinations, Cloudflare AI, Scaleway) with model tables
- **docs(readme)**: Updated pricing table with 4 new free tier rows
- **docs(i18n/pt-BR)**: Updated pricing table + added LongCat/Pollinations/Cloudflare AI/Scaleway sections in Portuguese
- **docs(new-features/ai)**: 10 task spec files + master implementation plan in `docs/new-features/ai/`
### 🧪 Tests
- Test suite: **821 tests, 0 failures** (unchanged)
---
## [2.9.2] — 2026-03-21
> Sprint: Fix media transcription (Deepgram/HuggingFace Content-Type, language detection) and TTS error display.
### 🐛 Bug Fixes
- **fix(transcription)**: Deepgram and HuggingFace audio transcription now correctly map `video/mp4``audio/mp4` and other media MIME types via new `resolveAudioContentType()` helper. Previously, uploading `.mp4` files consistently returned "No speech detected" because Deepgram was receiving `Content-Type: video/mp4`.
- **fix(transcription)**: Added `detect_language=true` to Deepgram requests — auto-detects audio language (Portuguese, Spanish, etc.) instead of defaulting to English. Fixes non-English transcriptions returning empty or garbage results.
- **fix(transcription)**: Added `punctuate=true` to Deepgram requests for higher-quality transcription output with correct punctuation.
- **fix(tts)**: `[object Object]` error display in Text-to-Speech responses fixed in both `audioSpeech.ts` and `audioTranscription.ts`. The `upstreamErrorResponse()` function now correctly extracts nested string messages from providers like ElevenLabs that return `{ error: { message: "...", status_code: 401 } }` instead of a flat error string.
### 🧪 Tests
- Test suite: **821 tests, 0 failures** (unchanged)
### Triaged Issues
- **#508** — Tool call format regression: requested proxy logs and provider chain info (`needs-info`)
- **#510** — Windows CLI healthcheck path: requested shell/Node version info (`needs-info`)
- **#485** — Kiro MCP tool calls: closed as external Kiro issue (not OmniRoute)
- **#442** — Baseten /models endpoint: closed (documented manual workaround)
- **#464** — Key provisioning API: acknowledged as roadmap item
---
## [2.9.1] — 2026-03-21
> Sprint: Fix SSE omniModel data loss, merge per-protocol model compatibility.
### Bug Fixes
- **#511** — Critical: `<omniModel>` tag was sent after `finish_reason:stop` in SSE streams, causing data loss. Tag is now injected into the first non-empty content chunk, guaranteeing delivery before SDKs close the connection.
### Merged PRs
- **PR #512** (@zhangqiang8vip): Per-protocol model compatibility — `normalizeToolCallId` and `preserveOpenAIDeveloperRole` can now be configured per client protocol (OpenAI, Claude, Responses API). New `compatByProtocol` field in model config with Zod validation.
### Triaged Issues
- **#510** — Windows CLI healthcheck_failed: requested PATH/version info
- **#509** — Turbopack Electron regression: upstream Next.js bug, documented workarounds
- **#508** — macOS black screen: suggested `--disable-gpu` workaround
---
## [2.9.0] — 2026-03-20
> Sprint: Cross-platform machineId fix, per-API-key rate limits, streaming context cache, Alibaba DashScope, search analytics, ZWS v5, and 8 issues closed.
### ✨ New Features
- **feat(search)**: Search Analytics tab in `/dashboard/analytics` — provider breakdown, cache hit rate, cost tracking. New API: `GET /api/v1/search/analytics` (#feat/search-provider-routing)
- **feat(provider)**: Alibaba Cloud DashScope added with custom endpoint path validation — configurable `chatPath` and `modelsPath` per node (#feat/custom-endpoint-paths)
- **feat(api)**: Per-API-key request-count limits — `max_requests_per_day` and `max_requests_per_minute` columns with in-memory sliding-window enforcement returning HTTP 429 (#452)
- **feat(dev)**: ZWS v5 — HMR leak fix (485 DB connections → 1), memory 2.4GB → 195MB, `globalThis` singletons, Edge Runtime warning fix (@zhangqiang8vip)
### 🐛 Bug Fixes
- **fix(#506)**: Cross-platform `machineId``getMachineIdRaw()` rewritten with try/catch waterfall (Windows REG.exe → macOS ioreg → Linux file read → hostname → `os.hostname()`). Eliminates `process.platform` branching that Next.js bundler dead-code-eliminated, fixing `'head' is not recognized` on Windows. Also fixes #466.
- **fix(#493)**: Custom provider model naming — removed incorrect prefix stripping in `DefaultExecutor.transformRequest()` that mangled org-scoped model IDs like `zai-org/GLM-5-FP8`.
- **fix(#490)**: Streaming + context cache protection — `TransformStream` intercepts SSE to inject `<omniModel>` tag before `[DONE]` marker, enabling context cache protection for streaming responses.
- **fix(#458)**: Combo schema validation — `system_message`, `tool_filter_regex`, `context_cache_protection` fields now pass Zod validation on save.
- **fix(#487)**: KIRO MITM card cleanup — removed ZWS_README, generified `AntigravityToolCard` to use dynamic tool metadata.
### 🧪 Tests
- Added Anthropic-format tools filter unit tests (PR #397) — 8 regression tests for `tool.name` without `.function` wrapper
- Test suite: **821 tests, 0 failures** (up from 813)
### 📋 Issues Closed (8)
- **#506** — Windows machineId `head` not recognized (fixed)
- **#493** — Custom provider model naming (fixed)
- **#490** — Streaming context cache (fixed)
- **#452** — Per-API-key request limits (implemented)
- **#466** — Windows login failure (same root cause as #506)
- **#504** — MITM inactive (expected behavior)
- **#462** — Gemini CLI PSA (resolved)
- **#434** — Electron app crash (duplicate of #402)
## [2.8.9] — 2026-03-20
> Sprint: Merge community PRs, fix KIRO MITM card, dependency updates.
+87 -28
View File
@@ -11,7 +11,9 @@ _Your universal API proxy — one endpoint, 44+ providers, zero downtime. Now wi
<div align="center">
[![npm version](https://img.shields.io/npm/v/omniroute?color=cb3837&logo=npm)](https://www.npmjs.com/package/omniroute)
[![npm downloads](https://img.shields.io/npm/dm/omniroute?color=cb3837&logo=npm&label=npm%20downloads)](https://www.npmjs.com/package/omniroute)
[![Docker Hub](https://img.shields.io/docker/v/diegosouzapw/omniroute?label=Docker%20Hub&logo=docker&color=2496ED)](https://hub.docker.com/r/diegosouzapw/omniroute)
[![Docker Pulls](https://img.shields.io/docker/pulls/diegosouzapw/omniroute?logo=docker&color=2496ED&label=docker%20pulls)](https://hub.docker.com/r/diegosouzapw/omniroute)
[![License](https://img.shields.io/github/license/diegosouzapw/OmniRoute)](https://github.com/diegosouzapw/OmniRoute/blob/main/LICENSE)
[![Website](https://img.shields.io/badge/Website-omniroute.online-blue?logo=google-chrome&logoColor=white)](https://omniroute.online)
[![WhatsApp](https://img.shields.io/badge/WhatsApp-Community-25D366?logo=whatsapp&logoColor=white)](https://chat.whatsapp.com/JI7cDQ1GyaiDHhVBpLxf8b?mode=gi_t)
@@ -716,7 +718,7 @@ Outcome: deep fallback depth for deadline-critical workloads
**Point any IDE/CLI to:** `http://localhost:20128/v1` · API Key: `any-string` · Done.
> **Optional extra coverage (also free):** Groq API key (30 RPM free), NVIDIA NIM (40 RPM free, 70+ models), Cerebras (1M tok/day).
> **Optional extra coverage (also free):** Groq API key (30 RPM free), NVIDIA NIM (40 RPM free, 70+ models), Cerebras (1M tok/day), LongCat API key (50M tokens/day!), Cloudflare Workers AI (10K Neurons/day, 50+ models).
## ⚡ Quick Start
@@ -921,18 +923,28 @@ When minimized, OmniRoute lives in your system tray with quick actions:
| **🆓 FREE** | iFlow | **$0** | Unlimited | 5 models unlimited |
| | Qwen | **$0** | Unlimited | 4 models unlimited |
| | Kiro | **$0** | Unlimited | Claude Sonnet/Haiku (AWS Builder) |
| | LongCat Flash-Lite 🆕 | **$0** (50M tok/day 🔥) | 1 RPS | Largest free quota on Earth |
| | Pollinations AI 🆕 | **$0** (no key needed) | 1 req/15s | GPT-5, Claude, DeepSeek, Llama 4 |
| | Cloudflare Workers AI 🆕 | **$0** (10K Neurons/day) | ~150 resp/day | 50+ models, global edge |
| | Scaleway AI 🆕 | **$0** (1M tokens total) | Rate limited | EU/GDPR, Qwen3 235B, Llama 70B |
> 🆕 **New models added (Mar 2026):** Grok-4 Fast family at $0.20/$0.50/M (benchmarked at 1143ms — 30% faster than Gemini 2.5 Flash), GLM-5 via Z.AI with 128K output, MiniMax M2.5 reasoning, DeepSeek V3.2 updated pricing, Kimi K2.5 via Moonshot direct API.
**💡 $0 Combo Stack — The Complete Free Setup:**
```
Gemini CLI (180K/mo free)
→ iFlow (unlimited: kimi-k2-thinking, qwen3-coder-plus, deepseek-r1)
→ Kiro (Claude Sonnet 4.5 + Haiku — unlimited, via AWS Builder ID)
→ Qwen (4 models — unlimited)
→ Groq (14.4K req/day — ultra-fast)
→ NVIDIA NIM (70+ models — 40 RPM forever)
# 🆓 Ultimate Free Stack 2026 — 11 Providers, $0 Forever
Kiro (kr/) → Claude Sonnet/Haiku UNLIMITED
iFlow (if/) → kimi-k2-thinking, qwen3-coder-plus, deepseek-r1 UNLIMITED
LongCat Lite (lc/) → LongCat-Flash-Lite — 50M tokens/day 🔥
Pollinations (pol/) → GPT-5, Claude, DeepSeek, Llama 4 — no key needed
Qwen (qw/) → qwen3-coder-plus, qwen3-coder-flash, qwen3-coder-next UNLIMITED
Gemini (gemini/) → Gemini 2.5 Flash — 1,500 req/day free API key
Cloudflare AI (cf/) → Llama 70B, Gemma 3, Mistral — 10K Neurons/day
Scaleway (scw/) → Qwen3 235B, Llama 70B — 1M free tokens (EU)
Groq (groq/) → Llama/Gemma ultra-fast — 14.4K req/day
NVIDIA NIM (nvidia/) → 70+ open models — 40 RPM forever
Cerebras (cerebras/) → Llama/Qwen world-fastest — 1M tok/day
```
**Zero cost. Never stops coding.** Configure this as one OmniRoute combo and all fallbacks happen automatically — no manual switching ever.
@@ -1003,19 +1015,66 @@ Available free: `llama-3.3-70b`, `llama-3.1-8b`, `deepseek-r1-distill-llama-70b`
Available free: `llama-3.3-70b-versatile`, `gemma2-9b-it`, `mixtral-8x7b`, `whisper-large-v3`
> **💡 The Ultimate Free Stack:**
### 🔴 LONGCAT AI (Free API Key — longcat.chat) 🆕
| Model | Prefix | Daily Free Quota | Notes |
| ----------------------------- | ------ | ----------------- | ----------------------- |
| `LongCat-Flash-Lite` | `lc/` | **50M tokens** 💥 | Largest free quota ever |
| `LongCat-Flash-Chat` | `lc/` | 500K tokens | Multi-turn chat |
| `LongCat-Flash-Thinking` | `lc/` | 500K tokens | Reasoning / CoT |
| `LongCat-Flash-Thinking-2601` | `lc/` | 500K tokens | Jan 2026 version |
| `LongCat-Flash-Omni-2603` | `lc/` | 500K tokens | Multimodal |
> 100% free while in public beta. Sign up at [longcat.chat](https://longcat.chat) with email or phone. Resets daily 00:00 UTC.
### 🟢 POLLINATIONS AI (No API Key Required) 🆕
| Model | Prefix | Rate Limit | Provider Behind |
| ---------- | ------ | ---------- | ------------------ |
| `openai` | `pol/` | 1 req/15s | GPT-5 |
| `claude` | `pol/` | 1 req/15s | Anthropic Claude |
| `gemini` | `pol/` | 1 req/15s | Google Gemini |
| `deepseek` | `pol/` | 1 req/15s | DeepSeek V3 |
| `llama` | `pol/` | 1 req/15s | Meta Llama 4 Scout |
| `mistral` | `pol/` | 1 req/15s | Mistral AI |
> ✨ **Zero friction:** No signup, no API key. Add the Pollinations provider with an empty key field and it works immediately.
### 🟠 CLOUDFLARE WORKERS AI (Free API Key — cloudflare.com) 🆕
| Tier | Daily Neurons | Equivalent Usage | Notes |
| ---- | ------------- | --------------------------------------- | ----------------------- |
| Free | **10,000** | ~150 LLM resp / 500s audio / 15K embeds | Global edge, 50+ models |
Popular free models: `@cf/meta/llama-3.3-70b-instruct`, `@cf/google/gemma-3-12b-it`, `@cf/openai/whisper-large-v3-turbo` (free audio!), `@cf/qwen/qwen2.5-coder-15b-instruct`
> Requires API Token + Account ID from [dash.cloudflare.com](https://dash.cloudflare.com). Store Account ID in provider settings.
### 🟣 SCALEWAY AI (1M Free Tokens — scaleway.com) 🆕
| Tier | Free Quota | Location | Notes |
| ---- | ------------- | ------------ | ----------------------------------- |
| Free | **1M tokens** | 🇫🇷 Paris, EU | No credit card needed within limits |
Available free: `qwen3-235b-a22b-instruct-2507` (Qwen3 235B!), `llama-3.1-70b-instruct`, `mistral-small-3.2-24b-instruct-2506`, `deepseek-v3-0324`
> EU/GDPR compliant. Get API key at [console.scaleway.com](https://console.scaleway.com).
> **💡 The Ultimate Free Stack (11 Providers, $0 Forever):**
>
> ```
> Kiro (Claude, unlimited)
> iFlow (5 models, unlimited)
> → Qwen (4 models, unlimited)
> → Gemini CLI (180K/mo)
> → Cerebras (1M tok/day)
> → Groq (14.4K req/day)
> → NVIDIA NIM (40 RPM, 70+ models)
> Kiro (kr/) → Claude Sonnet/Haiku UNLIMITED
> iFlow (if/) → kimi-k2-thinking, qwen3-coder-plus, deepseek-r1 UNLIMITED
> LongCat Lite (lc/) → LongCat-Flash-Lite — 50M tokens/day 🔥
> Pollinations (pol/) → GPT-5, Claude, DeepSeek, Llama 4 — no key needed
> Qwen (qw/) → qwen3-coder models UNLIMITED
> Gemini (gemini/) → Gemini 2.5 Flash — 1,500 req/day free
> Cloudflare AI (cf/) → 50+ models — 10K Neurons/day
> Scaleway (scw/) → Qwen3 235B, Llama 70B — 1M free tokens (EU)
> Groq (groq/) → Llama/Gemma — 14.4K req/day ultra-fast
> NVIDIA NIM (nvidia/) → 70+ open models — 40 RPM forever
> Cerebras (cerebras/) → Llama/Qwen world-fastest — 1M tok/day
> ```
>
> Configure this as an OmniRoute combo and you'll never pay for AI again.
## 🎙️ Free Transcription Combo
@@ -1105,17 +1164,17 @@ OmniRoute v2.0 is built as an operational platform, not just a relay proxy.
### 🎵 Multi-Modal APIs
| Feature | What It Does |
| -------------------------- | ------------------------------------------------------------------------------------------------------------ |
| 🖼️ **Image Generation** | `/v1/images/generations` with cloud and local backends |
| 📐 **Embeddings** | `/v1/embeddings` for search and RAG pipelines |
| 🎤 **Audio Transcription** | `/v1/audio/transcriptions` (Whisper and additional providers) |
| 🔊 **Text-to-Speech** | `/v1/audio/speech` (multiple engines/providers) |
| 🎬 **Video Generation** | `/v1/videos/generations` (ComfyUI + SD WebUI workflows) |
| 🎵 **Music Generation** | `/v1/music/generations` (ComfyUI workflows) |
| 🛡️ **Moderations** | `/v1/moderations` safety checks |
| 🔀 **Reranking** | `/v1/rerank` for relevance scoring |
| 🔍 **Web Search** 🆕 | `/v1/search` — 5 providers (Serper, Brave, Perplexity, Exa, Tavily), 6,500+ free/month, auto-failover, cache |
| Feature | What It Does |
| -------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 🖼️ **Image Generation** | `/v1/images/generations` with cloud and local backends |
| 📐 **Embeddings** | `/v1/embeddings` for search and RAG pipelines |
| 🎤 **Audio Transcription** | `/v1/audio/transcriptions` — 7 providers (Deepgram Nova 3, AssemblyAI, Groq Whisper, HuggingFace, ElevenLabs, OpenAI, Azure), auto-language detection, MP4/MP3/WAV support |
| 🔊 **Text-to-Speech** | `/v1/audio/speech` — 10 providers (ElevenLabs, OpenAI, Deepgram, Cartesia, PlayHT, HuggingFace, Nvidia NIM, Inworld, Coqui, Tortoise) with correct error messages |
| 🎬 **Video Generation** | `/v1/videos/generations` (ComfyUI + SD WebUI workflows) |
| 🎵 **Music Generation** | `/v1/music/generations` (ComfyUI workflows) |
| 🛡️ **Moderations** | `/v1/moderations` safety checks |
| 🔀 **Reranking** | `/v1/rerank` for relevance scoring |
| 🔍 **Web Search** 🆕 | `/v1/search` — 5 providers (Serper, Brave, Perplexity, Exa, Tavily), 6,500+ free/month, auto-failover, cache |
### 🛡️ Resilience, Security & Governance
+332
View File
@@ -0,0 +1,332 @@
# ZWS_README_V5 — 按协议配置模型兼容性 + 前端性能优化
V4 内容(HMR 泄漏修复、Edge 警告消除、测试稳定性)已完成;V5 在 V4 基础上实现**按协议维度配置模型兼容性**,新增前端查找性能优化与类型安全改进。
---
## 一、如何发现问题
### 现象
- 同一模型被 **OpenAI Chat Completions**、**OpenAI Responses API**、**Anthropic Messages** 三种客户端请求形态调用时,V2 的兼容性开关(工具 ID 9 位、不保留 developer 角色)是**全局生效**的——无法为不同协议设置不同的兼容策略。
- 例如:用户希望 OpenAI Responses API 请求时不保留 developer 角色(MiniMax 422 修复),但 OpenAI Chat Completions 请求时保留。V2 下只能二选一。
- 前端兼容性弹层未标明当前配置对应哪种协议,容易误导。
- 前端组件中 `Array.find()` 在每次渲染时对 customModels 和 modelCompatOverrides 做 O(n) 线性扫描,模型数量多时存在不必要的性能开销。
- `ModelCompatPatch` 类型定义与运行时逻辑不一致:`preserveOpenAIDeveloperRole` 字段需要支持 `null`(表示取消设置/恢复默认),但类型仅允许 `boolean`
### 排查过程
1. **需求分析**:梳理 `detectFormat(body)` 返回的三种协议键(`openai``openai-responses``claude`),确认每种协议对 developer 角色和 tool call ID 的需求不同。
2. **数据模型设计**:在现有 `normalizeToolCallId` / `preserveOpenAIDeveloperRole` 顶层字段基础上,设计 `compatByProtocol` 嵌套结构,按协议键细分。
3. **构建问题**:客户端 `"use client"` 组件直接从 `@/lib/localDb` 引入常量时,间接拉入了 `node:crypto`(经由 `db/proxies.ts`),触发 Webpack `UnhandledSchemeError`。需将常量拆到 `shared/` 层。
4. **前端性能**:通过 React DevTools 和代码审计发现 `effectiveNormalizeForProtocol` 等函数每次调用都对数组做 `find()`,在渲染列表时存在 O(n²) 的隐患。
---
## 二、根因分析
### 根因 1(P0):兼容选项无协议维度
V2 的 `normalizeToolCallId` / `preserveOpenAIDeveloperRole` 存储在模型级别的顶层字段,无法区分请求来源协议。`chatCore.ts` 中的 getter 函数只接收 `(providerId, modelId)` 两个参数,不感知当前请求的 `sourceFormat`
**影响**:跨协议场景下用户只能设置一个全局值,无法精确控制。
### 根因 2P1):客户端构建拉入 Node.js 模块
`page.tsx`"use client")→ `@/lib/localDb``db/proxies.ts``import { randomUUID } from "node:crypto"`
Webpack 无法处理 `node:` URI scheme,报 `UnhandledSchemeError`。虽然 V4 已将 `node:crypto``crypto` 修复了 `proxies.ts`,但 `localDb.ts` 的 barrel export 链仍然存在风险——客户端组件不应引入任何可能传递到 Node.js 模块的路径。
### 根因 3P2):前端查找性能
`effectiveNormalizeForProtocol``effectivePreserveForProtocol``anyNormalizeCompatBadge``anyNoPreserveCompatBadge` 四个函数每次调用都使用 `Array.find()``customModels``modelCompatOverrides` 数组中查找目标模型。在模型列表渲染时,每个模型行会调用多次这些函数,导致 O(n × m) 的查找开销(n = 模型数,m = 每行调用次数)。
### 根因 4(P2):类型定义与运行时不一致
```typescript
// V3 暂存区版本(有问题)
export type ModelCompatPatch = Partial<
Pick<
ModelCompatOverride,
"normalizeToolCallId" | "preserveOpenAIDeveloperRole" | "compatByProtocol"
>
>;
```
`ModelCompatOverride.preserveOpenAIDeveloperRole` 类型为 `boolean | undefined`,但 `mergeModelCompatOverride()` 内部有 `=== null` 判断(用于取消设置/恢复默认),类型层面无法覆盖。
---
## 三、修复方案
### 修复 1`compatByProtocol` 存储与读取(models.ts
**新增数据结构**
```typescript
type CompatByProtocolMap = Partial<Record<ModelCompatProtocolKey, ModelCompatPerProtocol>>;
export type ModelCompatOverride = {
id: string;
normalizeToolCallId?: boolean;
preserveOpenAIDeveloperRole?: boolean;
compatByProtocol?: CompatByProtocolMap; // 新增
};
```
**读取优先级链**(适用于 `getModelNormalizeToolCallId``getModelPreserveOpenAIDeveloperRole`):
```
compatByProtocol[sourceFormat].field → 顶层 field → 默认值
```
1. 若 `sourceFormat` 属于已知协议键(`openai` / `openai-responses` / `claude`),且 `compatByProtocol[sourceFormat]` 中存在目标字段,使用该值。
2. 否则回退到顶层字段。
3. 顶层字段也不存在时使用默认值(normalizeToolCallId=falsepreserveOpenAIDeveloperRole=undefined)。
**深度合并逻辑** `deepMergeCompatByProtocol()`
- 对每个协议键,逐字段合并而非覆盖。
- `normalizeToolCallId=false` 时删除该字段(不存储 false,减少冗余)。
- 合并后若整个协议条目为空对象,删除该协议条目。
- 协议键通过 `isCompatProtocolKey()` 白名单校验,拒绝未知键。
**Getter 签名扩展**(向后兼容,第三参数可选):
```typescript
export function getModelNormalizeToolCallId(
providerId: string,
modelId: string,
sourceFormat?: string | null
): boolean;
export function getModelPreserveOpenAIDeveloperRole(
providerId: string,
modelId: string,
sourceFormat?: string | null
): boolean | undefined;
```
**优点**
- 完全向后兼容:无 `sourceFormat` 参数时行为与 V2 完全一致。
- 协议键白名单校验防止存储污染。
- 深度合并保留未变更协议的配置。
**缺点/注意**
- JSON 存储体积略增(每个模型最多增加 3 个协议条目)。
- 新增 ~80 行 TypeScript 代码。
### 修复 2:请求管线传入 sourceFormatchatCore.ts
```typescript
const normalizeToolCallId = getModelNormalizeToolCallId(
provider || "",
model || "",
sourceFormat // 新增第三参
);
const preserveDeveloperRole = getModelPreserveOpenAIDeveloperRole(
provider || "",
model || "",
sourceFormat // 新增第三参
);
```
`sourceFormat` 由已有的 `detectFormat(body)` 返回,无需新增检测逻辑。
**优点**
- 改动仅 2 行,精准传参。
- 不影响其他 handlerembeddings、imageGeneration 等不涉及 developer 角色和 tool call ID)。
### 修复 3API 路由支持 compatByProtocolroute.ts
**PUT 请求体扩展**
- 解构 `compatByProtocol` 并传入 `updateCustomModel()`
- `compatOnly` 判断扩展:仅含 `provider` + `modelId` + 兼容字段时,走 `mergeModelCompatOverride()` 路径。
- 使用 `ModelCompatPatch` 类型替代行内类型定义,统一类型来源。
**Zod 校验 schema**
```typescript
const modelCompatPerProtocolSchema = z.object({
normalizeToolCallId: z.boolean().optional(),
preserveOpenAIDeveloperRole: z.boolean().optional(),
}).strict(); // strict: 拒绝额外字段
compatByProtocol: z
.record(z.enum(["openai", "openai-responses", "claude"]), modelCompatPerProtocolSchema)
.optional(),
```
**优点**
- `.strict()` 防止客户端注入额外字段。
- `z.enum()` 限定协议键,与后端白名单一致。
- 仅传 `compatByProtocol` 即可更新,前端无需拼装完整模型对象。
### 修复 4:客户端安全常量拆分(modelCompat.ts
**新增** `src/shared/constants/modelCompat.ts`
```typescript
export const MODEL_COMPAT_PROTOCOL_KEYS = ["openai", "openai-responses", "claude"] as const;
export type ModelCompatProtocolKey = (typeof MODEL_COMPAT_PROTOCOL_KEYS)[number];
```
- 不依赖 Node.js / DB 代码,客户端组件可安全引入。
- `models.ts` 从此模块引入并再导出。
- `localDb.ts` 新增 `ModelCompatPatch` 类型导出(供 route.ts 使用),不导出协议常量。
- `page.tsx` 改为从 `@/shared/constants/modelCompat` 引入。
**优点**
- 彻底切断客户端 → localDb → db → proxies → node:crypto 的依赖链。
- 协议键定义单一来源(Single Source of Truth)。
### 修复 5:前端协议选择器与按协议解析(page.tsx)
**ModelCompatPopover 重构**
- 新增协议下拉选择器(`<select>`),可选 OpenAI Chat / OpenAI Responses / Anthropic Messages。
- 两个开关(工具 ID 9 位、不保留 developer**针对选中协议**生效。
- 选择 Claude 协议时隐藏 developer 角色开关(developer 仅对 OpenAI 系有意义)。
- 保存时以 `{ compatByProtocol: { [protocol]: payload } }` 形式提交,后端按协议合并。
- 深色模式适配:下拉框使用 `bg-white dark:bg-zinc-800``text-zinc-900 dark:text-zinc-100`
**Props 接口重构**
旧接口(4 个独立值/回调):
```typescript
(normalizeToolCallId, preserveDeveloperRole, onNormalizeChange, onPreserveChange);
```
新接口(3 个函数式 props):
```typescript
effectiveModelNormalize: (protocol: string) => boolean
effectiveModelPreserveDeveloper: (protocol: string) => boolean
onCompatPatch: (protocol: string, payload: {...}) => void
```
所有消费方(`ModelRow``PassthroughModelRow``CustomModelsSection``CompatibleModelsSection`)已同步更新。
**角标显示逻辑**
- `anyNormalizeCompatBadge()`:任意协议或顶层存在 `normalizeToolCallId=true` 即显示「ID×9」角标。
- `anyNoPreserveCompatBadge()`:任意协议或顶层存在 `preserveOpenAIDeveloperRole=false` 即显示「不保留」角标。
**CustomModelsSection 增强**
- 新增 `modelCompatOverrides` 状态,从 API 响应中获取。
- 新增 `saveCustomCompat()` 函数,支持仅传 `compatByProtocol` 的独立保存。
### 修复 6:前端 Map 查找性能优化(page.tsx
**问题**`effectiveNormalizeForProtocol` 等函数对 `customModels``modelCompatOverrides``Array.find()` 做 O(n) 查找,在列表渲染时每个模型行多次调用。
**方案**:使用 `useMemo` + `Map` 将数组预建为 O(1) 查找表。
```typescript
type CompatModelMap = Map<string, CompatModelRow>;
function buildCompatMap(rows: CompatModelRow[]): CompatModelMap {
const m = new Map<string, CompatModelRow>();
for (const r of rows) if (r.id) m.set(r.id, r);
return m;
}
// 在组件内
const customMap = useMemo(() => buildCompatMap(modelMeta.customModels), [modelMeta.customModels]);
const overrideMap = useMemo(
() => buildCompatMap(modelMeta.modelCompatOverrides),
[modelMeta.modelCompatOverrides]
);
```
所有查找函数签名从 `(modelId, protocol, customModels[], overrides[])` 改为 `(modelId, protocol, customMap, overrideMap)`,内部使用 `Map.get()` 替代 `Array.find()`
**优点**
- 查找从 O(n) 降为 O(1)。
- `useMemo` 依赖项正确,仅在数据变化时重建 Map。
- `CustomModelsSection` 内部也独立构建 Map,不依赖父组件。
### 修复 7ModelCompatPatch 类型修正(models.ts
```typescript
// 修复后 — 显式允许 null
export type ModelCompatPatch = {
normalizeToolCallId?: boolean;
preserveOpenAIDeveloperRole?: boolean | null; // null = 取消设置/恢复默认
compatByProtocol?: CompatByProtocolMap;
};
```
`mergeModelCompatOverride()` 内的 `=== null` 判断逻辑一致,类型安全。
### 修复 8CompatByProtocolMap 类型收紧(page.tsx
客户端 `CompatByProtocolMap``Record<string, ...>` 改为 `Record<ModelCompatProtocolKey, ...>`,增强类型安全,防止传入未知协议键。
### 修复 9i18n 文案新增
| 键名 | 中文 | 英文 |
| ------------------------------- | --------------------------------------------- | -------------------------------------------------------------- |
| `compatProtocolLabel` | 客户端请求协议 | Client request protocol |
| `compatProtocolHint` | 以下选项在 OmniRoute 识别到该请求形态时生效。 | These options apply when OmniRoute detects this request shape. |
| `compatProtocolOpenAI` | OpenAI Chat Completions | OpenAI Chat Completions |
| `compatProtocolOpenAIResponses` | OpenAI Responses API | OpenAI Responses API |
| `compatProtocolClaude` | Anthropic Messages | Anthropic Messages |
---
## 四、使用方式
1. 点击模型行的 **「兼容性」** 按钮。
2. 在弹层内先选择 **「客户端请求协议」**OpenAI Chat / OpenAI Responses / Anthropic Messages)。
3. 勾选该协议下的「工具 ID 9 位」或「不保留 developer 角色」。
4. 保存后,仅在该协议形态的请求下生效。
5. 未配置某协议时,该协议下行为回退到顶层兼容字段(若存在),再回退到默认值(保留 developer、不规范化 tool id)。
6. 角标「ID×9」「不保留」在任意协议存在对应配置时显示。
---
## 五、预期效果
| 指标 | 修复前 | 修复后 |
| ------------------------- | --------------------- | ------------------------------------------ |
| 兼容性配置维度 | 全局(模型级) | 按协议(OpenAI Chat / Responses / Claude |
| developer 角色精确控制 | 不支持 | 支持(如:仅 Responses API 不保留) |
| 前端兼容性查找性能 | O(n) Array.find | O(1) Map.getuseMemo 缓存) |
| ModelCompatPatch 类型安全 | null 值无类型覆盖 | 显式 `boolean \| null` |
| 客户端构建风险 | 可能引入 Node.js 模块 | 已隔离(shared/constants 层) |
| API 验证 | 无 compatByProtocol | Zod strict schema 校验 |
| 深色模式 | 协议选择器不可读 | bg/text 适配 dark 主题 |
---
## 六、涉及文件清单
| 区域 | 文件 | 改动类型 |
| ---------- | ----------------------------------------------------------- | ------------------------------------------------------------------------------------------------ |
| 协议常量 | `src/shared/constants/modelCompat.ts` | **新建**,客户端安全的协议键与类型 |
| 存储与读写 | `src/lib/db/models.ts` | `compatByProtocol` 数据结构、深度合并、getter 第三参 `sourceFormat``ModelCompatPatch` 类型修正 |
| 再导出层 | `src/lib/localDb.ts` | 新增 `ModelCompatPatch` 类型导出 |
| API 路由 | `src/app/api/provider-models/route.ts` | PUT 支持 `compatByProtocol`,使用 `ModelCompatPatch` 类型 |
| 输入校验 | `src/shared/validation/schemas.ts` | `modelCompatPerProtocolSchema`strict+ `compatByProtocol` 记录校验 |
| 请求管线 | `open-sse/handlers/chatCore.ts` | `getModelNormalizeToolCallId` / `getModelPreserveOpenAIDeveloperRole` 传入 `sourceFormat` |
| 前端 UI | `src/app/(dashboard)/dashboard/providers/[id]/page.tsx` | 协议选择器、按协议解析/保存、角标逻辑、Map 性能优化、类型收紧 |
| i18n | `src/i18n/messages/en.json``src/i18n/messages/zh-CN.json` | 5 条新文案 |
---
## 七、回退方案
- **禁用按协议配置**:删除 `compatByProtocol` 字段后,getter 自动回退到顶层字段,行为与 V2 一致。
- **前端 Map 优化回退**:将 `Map.get()` 改回 `Array.find()` 即可,纯性能优化无功能耦合。
- **客户端常量回退**:将 `MODEL_COMPAT_PROTOCOL_KEYS` 定义移回 `models.ts` 并从 `localDb.ts` 导出(需同时确保 `node:crypto` 问题不再存在)。
- **生产环境**:以上修复对生产构建无负面影响。`compatByProtocol` 为可选字段,未配置时默认行为不变。API Zod 校验确保不会接受畸形数据。
+21 -2
View File
@@ -189,8 +189,27 @@ const serverJs = join(APP_DIR, "server.js");
if (!existsSync(serverJs)) {
console.error("\x1b[31m✖ Server not found at:\x1b[0m", serverJs);
console.error(" This usually means the package was not built correctly.");
console.error(" Try reinstalling: npm install -g omniroute");
console.error(" The package may not have been built correctly.");
console.error("");
// (#492) Detect common non-standard Node managers that cause this issue
const nodeExec = process.execPath || "";
const isMise = nodeExec.includes("mise") || nodeExec.includes(".local/share/mise");
const isNvm = nodeExec.includes(".nvm") || nodeExec.includes("nvm");
if (isMise) {
console.error(
" \x1b[33m⚠ mise detected:\x1b[0m If you installed via `npm install -g omniroute`,"
);
console.error(" try: \x1b[36mnpx omniroute@latest\x1b[0m (downloads a fresh copy)");
console.error(" or: \x1b[36mmise exec -- npx omniroute\x1b[0m");
} else if (isNvm) {
console.error(
" \x1b[33m⚠ nvm detected:\x1b[0m Try reinstalling after loading the correct Node version:"
);
console.error(" \x1b[36mnvm use --lts && npm install -g omniroute\x1b[0m");
} else {
console.error(" Try: \x1b[36mnpm install -g omniroute\x1b[0m (reinstall)");
console.error(" Or: \x1b[36mnpx omniroute@latest\x1b[0m");
}
process.exit(1);
}
+2 -2
View File
@@ -932,8 +932,8 @@ npm run electron:build:linux # Linux (.AppImage)
| ميزة | ماذا يفعل || -------------------------- | ------------------------------------------------------------- |
| 🖼️ **إنشاء الصور** | `/v1/images/generations` مع الواجهات الخلفية السحابية والمحلية |
| 📐 **المضامين** | `/v1/embeddings` للبحث وخطوط أنابيب RAG |
| 🎤 **نسخ صوتي** | `/v1/audio/transcriptions` (مقدمو خدمات الهمس والإضافيون) |
| 🔊 **تحويل النص إلى كلام** | `/v1/audio/speech` (محركات/موفرو متعددون) |
| 🎤 **نسخ صوتي** | `/v1/audio/transcriptions` — 7 providers (Deepgram Nova 3, AssemblyAI, Groq Whisper, HuggingFace, ElevenLabs, OpenAI, Azure), auto-language detection, MP4/MP3/WAV support |
| 🔊 **تحويل النص إلى كلام** | `/v1/audio/speech` — 10 providers (ElevenLabs, OpenAI, Deepgram, Cartesia, PlayHT, HuggingFace, Nvidia NIM, Inworld, Coqui, Tortoise) |
| 🎬 **توليد الفيديو** | `/v1/videos/generations` (سير عمل ComfyUI + SD WebUI) |
| 🎵 **جيل الموسيقى** | `/v1/music/generations` (سير عمل ComfyUI) |
| 🛡️ **اعتدالات** | فحوصات السلامة `/v1/moderations` |
+2 -2
View File
@@ -933,8 +933,8 @@ OmniRoute v2.0 е създаден като операционна платфо
| Характеристика | Какво прави || -------------------------- | ------------------------------------------------------------ |
| 🖼️ **Генериране на изображения** | `/v1/images/generations` с облак и локален бекенд |
| 📐 **Вграждания** | `/v1/embeddings` за търсене и RAG тръбопроводи |
| 🎤 **Аудио транскрипция** | `/v1/audio/transcriptions` (Whisper и допълнителни доставчици) |
| 🔊 **Текст към говор** | `/v1/audio/speech` (множество машини/доставчици) |
| 🎤 **Аудио транскрипция** | `/v1/audio/transcriptions` — 7 providers (Deepgram Nova 3, AssemblyAI, Groq Whisper, HuggingFace, ElevenLabs, OpenAI, Azure), auto-language detection, MP4/MP3/WAV support |
| 🔊 **Текст към говор** | `/v1/audio/speech` — 10 providers (ElevenLabs, OpenAI, Deepgram, Cartesia, PlayHT, HuggingFace, Nvidia NIM, Inworld, Coqui, Tortoise) |
| 🎬 **Видео генериране** | `/v1/videos/generations` (работни процеси ComfyUI + SD WebUI) |
| 🎵 **Музикално поколение** | `/v1/music/generations` (работни процеси на ComfyUI) |
| 🛡️ **Модерации** | `/v1/moderations` проверки за безопасност |
+225 -349
View File
@@ -2,7 +2,7 @@
### Nikdy nepřestávejte s kódováním. Chytré směrování k **BEZPLATNÝM a levným modelům AI** s automatickým přepínáním mezi záložními systémy.
*Váš univerzální API proxy jeden endpoint, více než 44 poskytovatelů, nulové výpadky. Nyní s orchestrací agentů **MCP a A2A** .*
_Váš univerzální API proxy jeden endpoint, více než 44 poskytovatelů, nulové výpadky. Nyní s orchestrací agentů **MCP a A2A** ._
**Dokončení chatu • Vkládání • Generování obrázků • Video • Hudba • Audio • Změna pořadí • **Vyhledávání na webu** • MCP server • A2A protokol • 100% TypeScript**
@@ -30,26 +30,23 @@
<summary><b>Kliknutím zobrazíte snímky obrazovky z řídicího panelu</b></summary>
</details>
Strana | Snímek obrazovky
--- | ---
**Poskytovatelé** | ![Poskytovatelé](docs/screenshots/01-providers.png)
**Kombinace** | ![Kombinace](docs/screenshots/02-combos.png)
**Analytika** | ![Analytika](docs/screenshots/03-analytics.png)
**Zdraví** | ![Zdraví](docs/screenshots/04-health.png)
**Překladatel** | ![Překladatel](docs/screenshots/05-translator.png)
**Nastavení** | ![Nastavení](docs/screenshots/06-settings.png)
**Nástroje CLI** | ![Nástroje CLI](docs/screenshots/07-cli-tools.png)
**Protokoly používání** | ![Používání](docs/screenshots/08-usage.png)
**Koncové body** | ![Koncové body](docs/screenshots/09-endpoint.png)
| Strana | Snímek obrazovky |
| ----------------------- | --------------------------------------------------- |
| **Poskytovatelé** | ![Poskytovatelé](docs/screenshots/01-providers.png) |
| **Kombinace** | ![Kombinace](docs/screenshots/02-combos.png) |
| **Analytika** | ![Analytika](docs/screenshots/03-analytics.png) |
| **Zdraví** | ![Zdraví](docs/screenshots/04-health.png) |
| **Překladatel** | ![Překladatel](docs/screenshots/05-translator.png) |
| **Nastavení** | ![Nastavení](docs/screenshots/06-settings.png) |
| **Nástroje CLI** | ![Nástroje CLI](docs/screenshots/07-cli-tools.png) |
| **Protokoly používání** | ![Používání](docs/screenshots/08-usage.png) |
| **Koncové body** | ![Koncové body](docs/screenshots/09-endpoint.png) |
---
### 🤖 Bezplatný poskytovatel umělé inteligence pro vaše oblíbené programátory
*Připojte libovolný nástroj IDE nebo CLI s umělou inteligencí přes OmniRoute — bezplatnou API bránu pro neomezené kódování.*
_Připojte libovolný nástroj IDE nebo CLI s umělou inteligencí přes OmniRoute — bezplatnou API bránu pro neomezené kódování._
<table>
<tr>
@@ -68,7 +65,6 @@ Strana | Snímek obrazovky
</tr>
</table>
<sub>📡 Všichni agenti se připojují přes <code>http://localhost:20128/v1</code> nebo <code>http://cloud.omniroute.online/v1</code> — jedna konfigurace, neomezené modely a kvóty</sub>
---
@@ -161,9 +157,6 @@ Vývojáři platí za Claude Pro, Codex Pro nebo GitHub Copilot 20200 dolarů
- **Vlastní kombinace** — Přizpůsobitelné záložní řetězce se 6 strategiemi vyvažování (fill-first, round robin, P2C, náhodné, nejméně používané, nákladově optimalizované)
- **Codex Business Quotas** — Sledování kvót pracovního prostoru firmy/týmu přímo v dashboardu
<details>
<summary><b>🔌 2. „Potřebuji použít více poskytovatelů, ale každý má jiné API“</b></summary>
</details>
@@ -180,9 +173,6 @@ OpenAI používá jeden formát, Claude (Anthropic) jiný a Gemini ještě třet
- **Strukturovaný výstup pro Gemini**`json_schema` → automatická konverze `responseMimeType` / `responseSchema`
- **Výchozí hodnota `stream` je `false`** Odpovídá specifikaci OpenAI, čímž se zabrání neočekávanému SSE v Python/Rust/Go SDK.
<details>
<summary><b>🌐 3. „Můj poskytovatel AI blokuje můj region/zemi“</b></summary>
</details>
@@ -199,9 +189,6 @@ Poskytovatelé jako OpenAI/Codex blokují přístup z určitých geografických
- **TLS Fingerprint Spoofing** — Otisk prstu TLS podobný prohlížeči pomocí `wreq-js` pro obcházení detekce botů
- **🔏 Porovnávání otisků prstů v CLI** — Změní pořadí záhlaví a polí v těle serveru tak, aby odpovídala nativním binárním podpisům v CLI, čímž drasticky snižuje riziko nahlašování účtu. IP adresa proxy je zachována — získáte současně stealth **i** maskování IP adresy.
<details>
<summary><b>🆓 4. „Chci používat umělou inteligenci pro kódování, ale nemám peníze“</b></summary>
</details>
@@ -216,9 +203,6 @@ Ne každý si může dovolit zaplatit 20200 dolarů měsíčně za předplatn
- **NVIDIA NIM Free Access** — ~40 RPM developerský přístup k více než 70 modelům na build.nvidia.com (přechod z kreditů na čisté limity rychlosti)
- **Strategie optimalizace nákladů** Strategie směrování, která automaticky vybere nejlevnějšího dostupného poskytovatele
<details>
<summary><b>🔒 5. „Potřebuji chránit svou bránu umělé inteligence před neoprávněným přístupem“</b></summary>
</details>
@@ -236,9 +220,6 @@ Při zpřístupnění brány umělé inteligence síti (LAN, VPS, Docker) může
- **Ochrana proti vkládání výzev** Sanitizace proti škodlivým vzorcům výzev
- **Šifrování AES-256-GCM** přihlašovací údaje jsou v klidovém stavu šifrovány
<details>
<summary><b>🛑 6. „Můj poskytovatel selhal a já ztratil/a programovací tok“</b></summary>
</details>
@@ -254,9 +235,6 @@ Poskytovatelé umělé inteligence se mohou stát nestabilními, vracet chyby 5x
- **Kombinovaný jistič** Automaticky deaktivuje selhávajícího poskytovatele v rámci kombinovaného řetězce
- **Dashboard stavu** — Monitorování provozuschopnosti, stavy jističů, uzamčení, statistiky mezipaměti, latence p50/p95/p99
<details>
<summary><b>🔧 7. „Konfigurace každého nástroje umělé inteligence je zdlouhavá a opakující se“</b></summary>
</details>
@@ -270,9 +248,6 @@ Vývojáři používají Cursor, Claude Code, Codex CLI, OpenClaw, Gemini CLI, K
- **Průvodce zaváděním** 4krokové nastavení pro začínající uživatele
- **Jeden koncový bod, všechny modely** jednou nakonfigurujte `http://localhost:20128/v1` a získejte přístup k více než 44 poskytovatelům
<details>
<summary><b>🔑 8. „Správa OAuth tokenů od více poskytovatelů je peklo“</b></summary>
</details>
@@ -288,9 +263,6 @@ Claude Code, Codex, Gemini CLI, Copilot všechny používají OAuth 2.0 s to
- **OAuth Behind Nginx** — Používá `window.location.origin` pro kompatibilitu s reverzní proxy
- **Průvodce vzdáleným OAuth** Podrobný návod k přihlašovacím údajům Google Cloud na VPS/Dockeru
<details>
<summary><b>📊 9. „Nevím, kolik utrácím ani kde“</b></summary>
</details>
@@ -305,9 +277,6 @@ Vývojáři používají více placených poskytovatelů, ale nemají jednotný
- **Statistiky použití pro každý klíč API** — Počet požadavků a časové razítko posledního použití pro každý klíč
- **Analytický panel** Statistické karty, graf využití modelu, tabulka poskytovatelů s mírou úspěšnosti a latencí
<details>
<summary><b>🐛 10. „Nedokážu diagnostikovat chyby a problémy ve volání umělé inteligence.“</b></summary>
</details>
@@ -324,9 +293,6 @@ Když volání selže, vývojář neví, zda se jednalo o limit rychlosti, vypr
- **Souborové protokolování s rotací** Konzolový interceptor zachycuje vše do protokolu JSON s rotací na základě velikosti
- **Zpráva o systémových informacích** — příkaz `npm run system-info` vygeneruje `system-info.txt` s kompletním popisem vašeho prostředí (verze uzlu, verze OmniRoute, operační systém, nástroje CLI, stav Dockeru/PM2). Přiložte jej při hlášení problémů pro okamžité třídění.
<details>
<summary><b>🏗️ 11. „Nasazení a údržba brány je složitá“</b></summary>
</details>
@@ -343,9 +309,6 @@ Instalace, konfigurace a údržba AI proxy v různých prostředích (lokální,
- **Cloud Sync** Konfigurace synchronizace mezi zařízeními pomocí Cloudflare Workers
- **Zálohy databází** — Automatické zálohování, obnovení, export a import všech nastavení
<details>
<summary><b>🌍 12. „Rozhraní je pouze v angličtině a můj tým nemluví anglicky“</b></summary>
</details>
@@ -359,9 +322,6 @@ Týmy v neanglicky mluvících zemích, zejména v Latinské Americe, Asii a Evr
- **Vícejazyčné soubory README** — 30 kompletních překladů dokumentace
- **Výběr jazyka** — Ikona glóbu v záhlaví pro přepínání v reálném čase
<details>
<summary><b>🔄 13. „Potřebuji víc než jen chat potřebuji vložené soubory, obrázky, zvuk.“</b></summary>
</details>
@@ -380,9 +340,6 @@ Umělá inteligence není jen dokončování chatu. Vývojáři potřebují gene
- **Změna pořadí**`/v1/rerank` — Změna pořadí relevance dokumentu
- **Responses API** — Plná podpora `/v1/responses` pro Codex
<details>
<summary><b>🧪 14. „Nemám způsob, jak testovat a porovnávat kvalitu napříč modely.“</b></summary>
</details>
@@ -397,9 +354,6 @@ Vývojáři chtějí vědět, který model je pro jejich případ použití nejl
- **Tester chatu** — Kompletní okružní cesta s vizuálním vykreslováním odpovědí
- **Živý monitor** — Stream všech požadavků procházejících proxy serverem v reálném čase
<details>
<summary><b>📈 15. „Potřebuji škálovat bez ztráty výkonu“</b></summary>
</details>
@@ -415,9 +369,6 @@ S rostoucím objemem požadavků generují stejné otázky bez ukládání do me
- **Mezipaměť pro ověření klíčů API** — třívrstvá mezipaměť pro výkon produkčního prostředí
- **Dashboard s telemetrií** latence p50/p95/p99, statistiky mezipaměti, dostupnost
<details>
<summary><b>🤖 16. „Chci mít chování modelů globálně pod kontrolou“</b></summary>
</details>
@@ -434,9 +385,6 @@ Vývojáři, kteří chtějí všechny odpovědi v určitém jazyce, se specific
- **Přepínání poskytovatele** Povolení/zakázání všech připojení pro poskytovatele jedním kliknutím
- **Blokovaní poskytovatelé** Vyloučení konkrétních poskytovatelů ze seznamu `/v1/models`
<details>
<summary><b>🧰 17. „Potřebuji nástroje MCP jako prvotřídní produktové funkce.“</b></summary>
</details>
@@ -449,9 +397,6 @@ Mnoho bran umělé inteligence odhaluje MCP pouze jako skrytý implementační d
- Vyhrazená stránka pro správu MCP s procesy, nástroji, rozsahy a auditem
- Vestavěný rychlý start pro `omniroute --mcp` a onboarding klienta
<details>
<summary><b>🧠 18. „Potřebuji orchestraci A2A se synchronizací a cestami úloh streamu.“</b></summary>
</details>
@@ -464,9 +409,6 @@ Pracovní postupy agentů vyžadují jak přímé odpovědi, tak dlouhodobé str
- Streamování SSE s šířením stavu terminálu
- Rozhraní API životního cyklu úloh pro `tasks/get` a `tasks/cancel`
<details>
<summary><b>🛰️ 19. „Potřebuji skutečný stav procesu MCP, ne odhadovaný stav.“</b></summary>
</details>
@@ -479,9 +421,6 @@ Provozní týmy potřebují vědět, zda je MCP skutečně aktivní, nejen zda j
- API stavu MCP kombinující prezenční signál a nedávnou aktivitu
- Karty stavu uživatelského rozhraní pro zobrazení aktuálnosti procesů/provozuschopnosti/prezenčního signálu
<details>
<summary><b>📋 20. „Potřebuji auditovatelné provedení nástroje MCP“</b></summary>
</details>
@@ -494,9 +433,6 @@ Když nástroje mění konfiguraci nebo spouštějí operační akce, týmy pot
- Filtruje podle nástroje, úspěchu/neúspěchu, klíče API a stránkování
- Tabulka auditu dashboardu + koncové body statistik pro automatizaci
<details>
<summary><b>🔐 21. „Potřebuji omezená oprávnění MCP pro každou integraci.“</b></summary>
</details>
@@ -509,9 +445,6 @@ Různí klienti by měli mít přístup ke kategoriím nástrojů s nejnižším
- Vynucení rozsahu a viditelnost v uživatelském rozhraní správy MCP
- Bezpečná výchozí poloha pro provozní nástroje
<details>
<summary><b>⚙️ 22. „Potřebuji provozní kontroly bez nutnosti přesouvání“</b></summary>
</details>
@@ -524,9 +457,6 @@ Týmy potřebují rychlé změny v běhovém prostředí během incidentů nebo
- Používejte profily odolnosti z předdefinovaných balíčků zásad
- Resetujte stav jističe ze stejného ovládacího panelu
<details>
<summary><b>🔄 23. „Potřebuji živý přehled o životním cyklu úkolů A2A a jejich zrušení.“</b></summary>
</details>
@@ -539,9 +469,6 @@ Bez přehledu o životním cyklu je obtížné třídit incidenty úkolů.
- Podrobný přehled metadat úloh, událostí a artefaktů
- Koncový bod zrušení úlohy a akce uživatelského rozhraní s potvrzením
<details>
<summary><b>🌊 24. „Potřebuji metriky aktivního streamu pro A2A zátěž“</b></summary>
</details>
@@ -554,9 +481,6 @@ Streamovací pracovní postupy vyžadují provozní přehled o souběžnosti a
- Časové razítko posledního úkolu a počty pro jednotlivé stavy
- Karty A2A dashboardu pro monitorování provozu v reálném čase
<details>
<summary><b>🪪 25. „Potřebuji standardní vyhledávání agentů pro klienty“</b></summary>
</details>
@@ -569,9 +493,6 @@ Externí klienti a orchestratoři potřebují pro onboarding strojově čitelná
- Schopnosti a dovednosti zobrazené v uživatelském rozhraní pro správu
- API pro stav A2A zahrnuje metadata pro zjišťování pro automatizaci
<details>
<summary><b>🧭 26. „Potřebuji v uživatelském rozhraní produktu zjistitelnost protokolu.“</b></summary>
</details>
@@ -584,9 +505,6 @@ Pokud uživatelé nemohou objevit protokolové povrchy, kvalita přijetí a podp
- Přepínání stavu inline služby (Online/Offline) pro MCP a A2A
- Odkazy z přehledu na vyhrazené karty pro správu
<details>
<summary><b>🧪 27. „Potřebuji komplexní ověření protokolu se skutečnými klienty.“</b></summary>
</details>
@@ -599,9 +517,6 @@ Simulované testy nestačí k ověření kompatibility protokolu před vydáním
- Klientské testy A2A pro toky zjišťování, odesílání, streamování, načítání a zrušení
- Křížová kontrola tvrzení oproti API pro audit MCP a úkoly A2A
<details>
<summary><b>📡 28. „Potřebuji jednotnou pozorovatelnost napříč všemi rozhraními“</b></summary>
</details>
@@ -614,9 +529,6 @@ Rozdělení pozorovatelnosti podle protokolu vytváří slepá místa a delší
- Stav + audit + telemetrie požadavků napříč vrstvami OpenAI, MCP a A2A
- Provozní API pro stav a automatizaci
<details>
<summary><b>💼 29. „Potřebuji jeden runtime pro proxy + nástroje + orchestraci agentů“</b></summary>
</details>
@@ -629,9 +541,6 @@ Spouštění mnoha samostatných služeb zvyšuje provozní náklady a počet po
- Sdílené ověřování, odolnost, úložiště dat a pozorovatelnost
- Konzistentní model politik napříč všemi interakčními plochami
<details>
<summary><b>🚀 30. „Potřebuji agentské pracovní postupy bez slepení kódu.“</b></summary>
</details>
@@ -644,9 +553,6 @@ Týmy ztrácejí rychlost při spojování více ad-hoc služeb a skriptů.
- Vestavěná uživatelská rozhraní pro správu protokolů a cesty pro ověřování kouře
- Základy připravené pro produkční prostředí (zabezpečení, protokolování, odolnost, zálohování)
### Příklady herních plánů (integrované případy užití)
**Příručka A: Maximalizace placeného předplatného + levné zálohování**
@@ -701,13 +607,13 @@ Outcome: deep fallback depth for deadline-critical workloads
> Nastavte si kódování s umělou inteligencí během několika minut za **0 $/měsíc** . Propojte tyto bezplatné účty a využijte vestavěnou kombinaci **Free Stack** .
Krok | Akce | Poskytovatelé odemčeni
--- | --- | ---
1 | Připojení **Kiro** (AWS Builder ID OAuth) | Claude Sonnet 4.5, Haiku 4.5 **neomezeně**
2 | Připojení k **iFlow** (Google OAuth) | kimi-k2-myšlení, qwen3-coder-plus, deepseek-r1... — **neomezeně**
3 | Připojení **Qwen** (kód zařízení) | qwen3-coder-plus, qwen3-coder-flash... — **neomezeně**
4 | Připojení **rozhraní příkazového řádku Gemini** (Google OAuth) | gemini-3-flash, gemini-2.5-pro — **180 000 GBP/měsíc zdarma**
5 | `/dashboard/combos` → Šablona **Free Stack (0 $)** | Automatické zařazení všech bezplatných poskytovatelů do routingu
| Krok | Akce | Poskytovatelé odemčeni |
| ---- | -------------------------------------------------------------- | ----------------------------------------------------------------- |
| 1 | Připojení **Kiro** (AWS Builder ID OAuth) | Claude Sonnet 4.5, Haiku 4.5 **neomezeně** |
| 2 | Připojení k **iFlow** (Google OAuth) | kimi-k2-myšlení, qwen3-coder-plus, deepseek-r1... — **neomezeně** |
| 3 | Připojení **Qwen** (kód zařízení) | qwen3-coder-plus, qwen3-coder-flash... — **neomezeně** |
| 4 | Připojení **rozhraní příkazového řádku Gemini** (Google OAuth) | gemini-3-flash, gemini-2.5-pro — **180 000 GBP/měsíc zdarma** |
| 5 | `/dashboard/combos` → Šablona **Free Stack (0 $)** | Automatické zařazení všech bezplatných poskytovatelů do routingu |
**V libovolném IDE/CLI naveďte:** `http://localhost:20128/v1` · Klíč API: `any-string` · Hotovo.
@@ -732,13 +638,13 @@ omniroute
Dashboard se otevírá na `http://localhost:20128` a základní URL API je `http://localhost:20128/v1` .
Příkaz | Popis
--- | ---
`omniroute` | Spuštění serveru ( `PORT=20128` , API a dashboard na stejném portu)
`omniroute --port 3000` | Nastavte kanonický/API port na 3000
`omniroute --mcp` | Spuštění MCP serveru (transport stdio)
`omniroute --no-open` | Neotevírat prohlížeč automaticky
`omniroute --help` | Zobrazit nápovědu
| Příkaz | Popis |
| ----------------------- | ------------------------------------------------------------------- |
| `omniroute` | Spuštění serveru ( `PORT=20128` , API a dashboard na stejném portu) |
| `omniroute --port 3000` | Nastavte kanonický/API port na 3000 |
| `omniroute --mcp` | Spuštění MCP serveru (transport stdio) |
| `omniroute --no-open` | Neotevírat prohlížeč automaticky |
| `omniroute --help` | Zobrazit nápovědu |
Volitelný režim s rozděleným portem:
@@ -847,10 +753,10 @@ docker compose --profile base up -d
docker compose --profile cli up -d
```
Obraz | Štítek | Velikost | Popis
--- | --- | --- | ---
`diegosouzapw/omniroute` | `latest` | ~250 MB | Nejnovější stabilní verze
`diegosouzapw/omniroute` | `1.0.3` | ~250 MB | Aktuální verze
| Obraz | Štítek | Velikost | Popis |
| ------------------------ | -------- | -------- | ------------------------- |
| `diegosouzapw/omniroute` | `latest` | ~250 MB | Nejnovější stabilní verze |
| `diegosouzapw/omniroute` | `1.0.3` | ~250 MB | Aktuální verze |
---
@@ -893,41 +799,47 @@ Po minimalizaci se OmniRoute nachází v systémové liště a nabízí rychlé
## 💰 Přehled cen
Úroveň | Poskytovatel | Náklady | Obnovení kvóty | Nejlepší pro
--- | --- | --- | --- | ---
**💳 PŘEDPLATNÉ** | Claude Code (profesionál) | 20 dolarů měsíčně | 5 hodin + týdně | Již přihlášen/a k odběru
| Kodex (Plus/Pro) | 20200 USD/měsíc | 5 hodin + týdně | Uživatelé OpenAI
| Rozhraní příkazového řádku Gemini | **UVOLNIT** | 180 tisíc měsíčně + 1 tisíc denně | Každý!
| GitHub Copilot | 1019 USD/měsíc | Měsíční | Uživatelé GitHubu
**🔑 KLÍČ API** | NVIDIA NIM | **ZDARMA** (vývoj navždy) | ~40 ot./min | 70+ otevřených modelů
| Mozky | **ZDARMA** (1 milion tok/den) | 60 000 otáček za minutu / 30 ot./min | Nejrychlejší na světě
| Groq | **ZDARMA** (30 ot./min.) | 14,4 tisíc otáček za minutu | Ultrarychlá lama/gema
| DeepSeek V3.2 | 0,27/1,10 USD za 1 milion | Žádný | Nejlepší zdůvodnění ceny a kvality
| xAI Grok-4 Rychlý | **0,20/0,50 USD za 1 milion** 🆕 | Žádný | Nejrychlejší + volání nástroje, ultranízké
| xAI Grok-4 (standardní) | 0,20/1,50 USD za 1 milion 🆕 | Žádný | Vlajková loď Reasoning od xAI
| Mistral | Zkušební verze zdarma + placené | Omezená sazba | Evropská umělá inteligence
| OpenRouter | Platba za použití | Žádný | Více než 100 modelů agregováno.
**💰 LEVNÉ** | GLM-5 (přes Z.AI) 🆕 | 0,5 USD/1 milion | Denně v 10:00 | Výstup 128 tisíc obrazových bodů, nejnovější vlajková loď
| GLM-4.7 | 0,6 USD/1 milion | Denně v 10:00 | Záloha rozpočtu
| MiniMax M2.5 🆕 | Vstup 0,3 USD/1 milion | 5hodinové válcování | Úvaha + agentní úkoly
| MiniMax M2.1 | 0,2 USD/1 milion | 5hodinové válcování | Nejlevnější varianta
| Kimi K2.5 (Moonshot API) 🆕 | Platba za použití | Žádný | Přímý přístup k Moonshot API
| Kimi K2 | 9 dolarů měsíčně bez závazků | 10 milionů tokenů/měsíc | Předvídatelné náklady
**🆓 ZDARMA** | iFlow | **0 dolarů** | Neomezený | 5 modelů neomezeně
| Qwen | **0 dolarů** | Neomezený | 4 modely neomezeně
| Kiro | **0 dolarů** | Neomezený | Claude Sonnet/Haiku (tvorce AWS)
| Úroveň | Poskytovatel | Náklady | Obnovení kvóty | Nejlepší pro |
| --------------------------------- | -------------------------------- | ------------------------------------ | ------------------------------------------ | --------------------------------------------------------- |
| **💳 PŘEDPLATNÉ** | Claude Code (profesionál) | 20 dolarů měsíčně | 5 hodin + týdně | Již přihlášen/a k odběru |
| Kodex (Plus/Pro) | 20200 USD/měsíc | 5 hodin + týdně | Uživatelé OpenAI |
| Rozhraní příkazového řádku Gemini | **UVOLNIT** | 180 tisíc měsíčně + 1 tisíc denně | Každý! |
| GitHub Copilot | 1019 USD/měsíc | Měsíční | Uživatelé GitHubu |
| **🔑 KLÍČ API** | NVIDIA NIM | **ZDARMA** (vývoj navždy) | ~40 ot./min | 70+ otevřených modelů |
| Mozky | **ZDARMA** (1 milion tok/den) | 60 000 otáček za minutu / 30 ot./min | Nejrychlejší na světě |
| Groq | **ZDARMA** (30 ot./min.) | 14,4 tisíc otáček za minutu | Ultrarychlá lama/gema |
| DeepSeek V3.2 | 0,27/1,10 USD za 1 milion | Žádný | Nejlepší zdůvodnění ceny a kvality |
| xAI Grok-4 Rychlý | **0,20/0,50 USD za 1 milion** 🆕 | Žádný | Nejrychlejší + volání nástroje, ultranízké |
| xAI Grok-4 (standardní) | 0,20/1,50 USD za 1 milion 🆕 | Žádný | Vlajková loď Reasoning od xAI |
| Mistral | Zkušební verze zdarma + placené | Omezená sazba | Evropská umělá inteligence |
| OpenRouter | Platba za použití | Žádný | Více než 100 modelů agregováno. |
| **💰 LEVNÉ** | GLM-5 (přes Z.AI) 🆕 | 0,5 USD/1 milion | Denně v 10:00 | Výstup 128 tisíc obrazových bodů, nejnovější vlajková loď |
| GLM-4.7 | 0,6 USD/1 milion | Denně v 10:00 | Záloha rozpočtu |
| MiniMax M2.5 🆕 | Vstup 0,3 USD/1 milion | 5hodinové válcování | Úvaha + agentní úkoly |
| MiniMax M2.1 | 0,2 USD/1 milion | 5hodinové válcování | Nejlevnější varianta |
| Kimi K2.5 (Moonshot API) 🆕 | Platba za použití | Žádný | Přímý přístup k Moonshot API |
| Kimi K2 | 9 dolarů měsíčně bez závazků | 10 milionů tokenů/měsíc | Předvídatelné náklady |
| **🆓 ZDARMA** | iFlow | **0 dolarů** | Neomezený | 5 modelů neomezeně |
| Qwen | **0 dolarů** | Neomezený | 4 modely neomezeně |
| Kiro | **0 dolarů** | Neomezený | Claude Sonnet/Haiku (tvorce AWS) |
> 🆕 **Přidány nové modely (březen 2026):** řada Grok-4 Fast za 0,20 USD/0,50 USD/M (benchmarkováno na 1143 ms o 30 % rychlejší než Gemini 2.5 Flash), GLM-5 přes Z.AI s výstupem 128K, uvažování MiniMax M2.5, aktualizované ceny DeepSeek V3.2, Kimi K2.5 přes Moonshot Direct API.
**💡 Kombinovaný balík za 0 $ — Kompletní bezplatná instalace:**
```
Gemini CLI (180K/mo free)
→ iFlow (unlimited: kimi-k2-thinking, qwen3-coder-plus, deepseek-r1)
→ Kiro (Claude Sonnet 4.5 + Haiku — unlimited, via AWS Builder ID)
→ Qwen (4 models — unlimited)
→ Groq (14.4K req/day — ultra-fast)
→ NVIDIA NIM (70+ models — 40 RPM forever)
# 🆓 Ultimate Free Stack 2026 — 11 Providers, $0 Forever
Kiro (kr/) → Claude Sonnet/Haiku UNLIMITED
iFlow (if/) → kimi-k2-thinking, qwen3-coder-plus, deepseek-r1 UNLIMITED
LongCat Lite (lc/) → LongCat-Flash-Lite — 50M tokens/day 🔥
Pollinations (pol/) → GPT-5, Claude, DeepSeek, Llama 4 — no key needed
Qwen (qw/) → qwen3-coder-plus, qwen3-coder-flash, qwen3-coder-next UNLIMITED
Gemini (gemini/) → Gemini 2.5 Flash — 1,500 req/day free API key
Cloudflare AI (cf/) → Llama 70B, Gemma 3, Mistral — 10K Neurons/day
Scaleway (scw/) → Qwen3 235B, Llama 70B — 1M free tokens (EU)
Groq (groq/) → Llama/Gemma ultra-fast — 14.4K req/day
NVIDIA NIM (nvidia/) → 70+ open models — 40 RPM forever
Cerebras (cerebras/) → Llama/Qwen world-fastest — 1M tok/day
```
**Nulové náklady. Nikdy nepřestávejte s kódováním.** Nakonfigurujte si to jako jednu kombinaci OmniRoute a všechny záložní režimy se provede automaticky žádné ruční přepínání.
@@ -942,59 +854,59 @@ Gemini CLI (180K/mo free)
### 🔵 CLAUDE MODELS (přes Kiro — AWS Builder ID)
Model | Předpona | Omezit | Limit rychlosti
--- | --- | --- | ---
`claude-sonnet-4.5` | `kr/` | **Neomezený** | Žádný hlášený denní limit
`claude-haiku-4.5` | `kr/` | **Neomezený** | Žádný hlášený denní limit
`claude-opus-4.6` | `kr/` | **Neomezený** | Nejnovější opus od Kira
| Model | Předpona | Omezit | Limit rychlosti |
| ------------------- | -------- | ------------- | ------------------------- |
| `claude-sonnet-4.5` | `kr/` | **Neomezený** | Žádný hlášený denní limit |
| `claude-haiku-4.5` | `kr/` | **Neomezený** | Žádný hlášený denní limit |
| `claude-opus-4.6` | `kr/` | **Neomezený** | Nejnovější opus od Kira |
### 🟢 MODELY IFLOW (Bezplatné OAuth — bez nutnosti platit kreditní kartou)
Model | Předpona | Omezit | Limit rychlosti
--- | --- | --- | ---
`kimi-k2-thinking` | `if/` | **Neomezený** | Žádný hlášený strop
`qwen3-coder-plus` | `if/` | **Neomezený** | Žádný hlášený strop
`deepseek-r1` | `if/` | **Neomezený** | Žádný hlášený strop
`minimax-m2.1` | `if/` | **Neomezený** | Žádný hlášený strop
`kimi-k2` | `if/` | **Neomezený** | Žádný hlášený strop
| Model | Předpona | Omezit | Limit rychlosti |
| ------------------ | -------- | ------------- | ------------------- |
| `kimi-k2-thinking` | `if/` | **Neomezený** | Žádný hlášený strop |
| `qwen3-coder-plus` | `if/` | **Neomezený** | Žádný hlášený strop |
| `deepseek-r1` | `if/` | **Neomezený** | Žádný hlášený strop |
| `minimax-m2.1` | `if/` | **Neomezený** | Žádný hlášený strop |
| `kimi-k2` | `if/` | **Neomezený** | Žádný hlášený strop |
### 🟡 MODELY QWEN (Ověření kódu zařízení)
Model | Předpona | Omezit | Limit rychlosti
--- | --- | --- | ---
`qwen3-coder-plus` | `qw/` | **Neomezený** | Žádný hlášený strop
`qwen3-coder-flash` | `qw/` | **Neomezený** | Žádný hlášený strop
`qwen3-coder-next` | `qw/` | **Neomezený** | Žádný hlášený strop
`vision-model` | `qw/` | **Neomezený** | Multimodální (obrázky)
| Model | Předpona | Omezit | Limit rychlosti |
| ------------------- | -------- | ------------- | ---------------------- |
| `qwen3-coder-plus` | `qw/` | **Neomezený** | Žádný hlášený strop |
| `qwen3-coder-flash` | `qw/` | **Neomezený** | Žádný hlášený strop |
| `qwen3-coder-next` | `qw/` | **Neomezený** | Žádný hlášený strop |
| `vision-model` | `qw/` | **Neomezený** | Multimodální (obrázky) |
### 🟣 Rozhraní GEMINI CLI (Google OAuth)
Model | Předpona | Omezit | Limit rychlosti
--- | --- | --- | ---
`gemini-3-flash-preview` | `gc/` | **180 tisíc tok/měsíc** + 1 tisíc/den | Měsíční reset
`gemini-2.5-pro` | `gc/` | 180 tisíc měsíčně (sdílený bazén) | Vysoká kvalita
| Model | Předpona | Omezit | Limit rychlosti |
| ------------------------ | -------- | ------------------------------------- | --------------- |
| `gemini-3-flash-preview` | `gc/` | **180 tisíc tok/měsíc** + 1 tisíc/den | Měsíční reset |
| `gemini-2.5-pro` | `gc/` | 180 tisíc měsíčně (sdílený bazén) | Vysoká kvalita |
### ⚫ NVIDIA NIM (Bezplatný klíč API — build.nvidia.com)
Úroveň | Denní limit | Limit rychlosti | Poznámky
--- | --- | --- | ---
Zdarma (vývojář) | Žádný limit tokenů | **~40 ot./min** | Více než 70 modelů; přechod na čisté limity sazeb v polovině roku 2025
| Úroveň | Denní limit | Limit rychlosti | Poznámky |
| ---------------- | ------------------ | --------------- | ---------------------------------------------------------------------- |
| Zdarma (vývojář) | Žádný limit tokenů | **~40 ot./min** | Více než 70 modelů; přechod na čisté limity sazeb v polovině roku 2025 |
Oblíbené bezplatné modely: `moonshotai/kimi-k2.5` (Kimi K2.5), `z-ai/glm4.7` (GLM 4.7), `deepseek-ai/deepseek-v3.2` (DeepSeek V3.2), `nvidia/llama-3.3-70b-instruct` , `deepseek/deepseek-r1`
### ⚪ CEREBRAS (Bezplatný klíč API — inference.cerebras.ai)
Úroveň | Denní limit | Limit rychlosti | Poznámky
--- | --- | --- | ---
Uvolnit | **1 milion tokenů/den** | 60 000 otáček za minutu / 30 ot./min | Nejrychlejší inference LLM na světě; denně se resetuje
| Úroveň | Denní limit | Limit rychlosti | Poznámky |
| ------- | ----------------------- | ------------------------------------ | ------------------------------------------------------ |
| Uvolnit | **1 milion tokenů/den** | 60 000 otáček za minutu / 30 ot./min | Nejrychlejší inference LLM na světě; denně se resetuje |
Dostupné zdarma: `llama-3.3-70b` , `llama-3.1-8b` , `deepseek-r1-distill-llama-70b`
### 🔴 GROQ (Bezplatný API klíč — console.groq.com)
Úroveň | Denní limit | Limit rychlosti | Poznámky
--- | --- | --- | ---
Uvolnit | **14,4 tisíc otáček za minutu** | 30 ot./min na model | Žádná kreditní karta; limit 429, neúčtováno
| Úroveň | Denní limit | Limit rychlosti | Poznámky |
| ------- | ------------------------------- | ------------------- | ------------------------------------------- |
| Uvolnit | **14,4 tisíc otáček za minutu** | 30 ot./min na model | Žádná kreditní karta; limit 429, neúčtováno |
K dispozici zdarma: `llama-3.3-70b-versatile` , `gemma2-9b-it` , `mixtral-8x7b` , `whisper-large-v3`
@@ -1016,11 +928,11 @@ K dispozici zdarma: `llama-3.3-70b-versatile` , `gemma2-9b-it` , `mixtral-8x7b`
> Přepisujte libovolné audio/video za **0 $** Deepgram leady za 200 $ zdarma, AssemblyAI za 50 $ jako záložní nástroj, Groq Whisper jako neomezená nouzová záloha.
Poskytovatel | Bezplatné kredity | Nejlepší model | Limit rychlosti
--- | --- | --- | ---
🟢 **Deepgram** | **200 dolarů zdarma** (registrace) | `nova-3` — nejvyšší přesnost, více než 30 jazyků | Žádný limit RPM pro kredity zdarma
🔵 **AssemblyAI** | **50 dolarů zdarma** (registrace) | `universal-3-pro` — kapitoly, sentiment, osobní údaje | Žádný limit RPM pro kredity zdarma
🔴 **Groq** | **Navždy zdarma** | `whisper-large-v3` — OpenAI Šepot | 30 ot./min (omezená rychlost)
| Poskytovatel | Bezplatné kredity | Nejlepší model | Limit rychlosti |
| ----------------- | ---------------------------------- | ----------------------------------------------------- | ---------------------------------- |
| 🟢 **Deepgram** | **200 dolarů zdarma** (registrace) | `nova-3` — nejvyšší přesnost, více než 30 jazyků | Žádný limit RPM pro kredity zdarma |
| 🔵 **AssemblyAI** | **50 dolarů zdarma** (registrace) | `universal-3-pro` — kapitoly, sentiment, osobní údaje | Žádný limit RPM pro kredity zdarma |
| 🔴 **Groq** | **Navždy zdarma** | `whisper-large-v3` — OpenAI Šepot | 30 ot./min (omezená rychlost) |
**Navrhovaná kombinace v `/dashboard/combos` :**
@@ -1041,118 +953,118 @@ OmniRoute v2.0 je navržen jako operační platforma, nikoli pouze jako proxy pr
### 🆕 Nové — Vylepšení inspirovaná ClawRouterem (březen 2026)
Funkce | Co to dělá
--- | ---
**Grok-4 Rychlá rodina** | Modely xAI za 0,20 USD/0,50 USD/M v benchmarku 1143 ms (o 30 % rychlejší než Gemini 2.5 Flash)
🧠 **GLM-5 přes Z.AI** | 128 tisíc výstupních dat, 0,5 USD/1 milion USD nejnovější vlajková loď rodiny GLM
🔮 **MiniMax M2.5** | Úvaha + agentní úkoly za 0,30 USD/1 milion významný upgrade oproti M2.1
🎯 **Příznak volání nástroje pro každý model** | `toolCalling: true/false` v registru — AutoCombo přeskakuje modely, které nepodporují nástroje.
🌍 **Detekce vícejazyčného záměru** | Klíčová slova PT/ZH/ES/AR v bodování AutoCombo lepší výběr modelu pro neanglický obsah
📊 **Záložní metody řízené benchmarkem** | Skutečná latence p95 z živých požadavků poskytuje kombinované skóre AutoCombo se učí ze skutečných dat
🔁 **Požádat o deduplikaci** | Okno pro deduplikaci na základě hashování obsahu bezpečné pro více agentů, zabraňuje duplicitním platbám
🔌 **Strategie pro zásuvné routery** | Rozšiřitelné rozhraní `RouterStrategy` přidejte si vlastní logiku směrování jako pluginy
| Funkce | Co to dělá |
| ---------------------------------------------- | ---------------------------------------------------------------------------------------------------------- |
| **Grok-4 Rychlá rodina** | Modely xAI za 0,20 USD/0,50 USD/M v benchmarku 1143 ms (o 30 % rychlejší než Gemini 2.5 Flash) |
| 🧠 **GLM-5 přes Z.AI** | 128 tisíc výstupních dat, 0,5 USD/1 milion USD nejnovější vlajková loď rodiny GLM |
| 🔮 **MiniMax M2.5** | Úvaha + agentní úkoly za 0,30 USD/1 milion významný upgrade oproti M2.1 |
| 🎯 **Příznak volání nástroje pro každý model** | `toolCalling: true/false` v registru — AutoCombo přeskakuje modely, které nepodporují nástroje. |
| 🌍 **Detekce vícejazyčného záměru** | Klíčová slova PT/ZH/ES/AR v bodování AutoCombo lepší výběr modelu pro neanglický obsah |
| 📊 **Záložní metody řízené benchmarkem** | Skutečná latence p95 z živých požadavků poskytuje kombinované skóre AutoCombo se učí ze skutečných dat |
| 🔁 **Požádat o deduplikaci** | Okno pro deduplikaci na základě hashování obsahu bezpečné pro více agentů, zabraňuje duplicitním platbám |
| 🔌 **Strategie pro zásuvné routery** | Rozšiřitelné rozhraní `RouterStrategy` přidejte si vlastní logiku směrování jako pluginy |
### 🚀 Předchozí verze v2.0.9+ — Hřiště, otisky prstů v CLI a ACP
Funkce | Co to dělá
--- | ---
🎮 **Modelové hřiště** | Stránka řídicího panelu pro přímé testování libovolného modelu selektory poskytovatele/modelu/koncového bodu, editor Monaco, streamování, přerušení, načasování
🔏 **Porovnávání otisků prstů v CLI** | Řazení hlaviček/těl serveru podle poskytovatele tak, aby odpovídalo nativním podpisům CLI přepínání pro jednotlivé poskytovatele v Nastavení &gt; Zabezpečení. **Vaše IP adresa proxy serveru je zachována.**
🤝 **Podpora ACP (Agent Client Protocol)** | Vyhledávání agentů CLI (Codex, Claude, Goose, Gemini CLI, OpenClaw + 9 dalších), generátor procesů, koncový bod `/api/acp/agents`
🤖 **Řídicí panel agentů ACP** | Ladění Stránka Agenti — mřížka 14 agentů se stavem instalace, verzí a formulářem pro vlastní agenta pro libovolný nástroj CLI. Uživatelé **OpenCode** získají tlačítko „Stáhnout opencode.json“, které automaticky vygeneruje konfiguraci připravenou k použití se všemi dostupnými modely.
🔧 **Směrování `apiFormat` pro vlastní model** | Vlastní modely s `apiFormat: "responses"` nyní správně směrují do překladače Responses API.
🏢 **Izolace pracovního prostoru Codexu** | Více pracovních prostorů Codexu na jeden e-mail OAuth správně odděluje připojení podle ID pracovního prostoru
🔄 **Automatická aktualizace elektronů** | Desktopová aplikace kontroluje aktualizace + automaticky se instaluje po restartu
| Funkce | Co to dělá |
| ---------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 🎮 **Modelové hřiště** | Stránka řídicího panelu pro přímé testování libovolného modelu selektory poskytovatele/modelu/koncového bodu, editor Monaco, streamování, přerušení, načasování |
| 🔏 **Porovnávání otisků prstů v CLI** | Řazení hlaviček/těl serveru podle poskytovatele tak, aby odpovídalo nativním podpisům CLI přepínání pro jednotlivé poskytovatele v Nastavení &gt; Zabezpečení. **Vaše IP adresa proxy serveru je zachována.** |
| 🤝 **Podpora ACP (Agent Client Protocol)** | Vyhledávání agentů CLI (Codex, Claude, Goose, Gemini CLI, OpenClaw + 9 dalších), generátor procesů, koncový bod `/api/acp/agents` |
| 🤖 **Řídicí panel agentů ACP** | Ladění Stránka Agenti — mřížka 14 agentů se stavem instalace, verzí a formulářem pro vlastní agenta pro libovolný nástroj CLI. Uživatelé **OpenCode** získají tlačítko „Stáhnout opencode.json“, které automaticky vygeneruje konfiguraci připravenou k použití se všemi dostupnými modely. |
| 🔧 **Směrování `apiFormat` pro vlastní model** | Vlastní modely s `apiFormat: "responses"` nyní správně směrují do překladače Responses API. |
| 🏢 **Izolace pracovního prostoru Codexu** | Více pracovních prostorů Codexu na jeden e-mail OAuth správně odděluje připojení podle ID pracovního prostoru |
| 🔄 **Automatická aktualizace elektronů** | Desktopová aplikace kontroluje aktualizace + automaticky se instaluje po restartu |
### 🤖 Operace s agenty a protokoly (v2.0)
Funkce | Co to dělá
--- | ---
🔧 **MCP Server (16 nástrojů)** | Nástroje IDE/agent prostřednictvím 3 transportů: stdio, SSE ( `/api/mcp/sse` ), Streamovatelný HTTP ( `/api/mcp/stream` )
🤝 **A2A server (JSON-RPC + SSE)** | Spouštění úloh mezi agenty se synchronizací a streamováním
🧭 **Konsolidovaná stránka koncových bodů** | Stránka pro správu s kartami Endpoint Proxy, MCP, A2A a API Endpoints
🎚️ **Přepínače pro povolení/zakázání služby** | Přepínače ZAP/VYP pro MCP a A2A s trvalým nastavením (výchozí: VYP)
🛰️ **Srdeční tep za běhu MCP** | Skutečný stav procesu (pid, doba provozuschopnosti, stáří heartbeatu, transport, režim rozsahu)
📋 **Auditní záznam MCP** | Filtrovatelné protokoly auditu s hodnocením úspěchu/neúspěchu a klíčovým přiřazením
🔐 **Vynucování rozsahu MCP** | 9 podrobných oprávnění pro řízený přístup k nástrojům
📡 **Správa životního cyklu úkolů A2A** | Seznam/filtrování úloh, kontrola událostí/artefaktů, zrušení spuštěných úloh
📋 **Objevení karty agenta** | `/.well-known/agent.json` pro automatické vyhledávání klientů
🧪 **Testovací postroj Protocol E2E** | Skutečné MCP SDK + toky klientů A2A v `test:protocols:e2e`
⚙️ **Provozní kontroly** | Kombinace přepínačů, použití profilů odolnosti, resetování jističů z jednoho ovládacího panelu
| Funkce | Co to dělá |
| --------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- |
| 🔧 **MCP Server (16 nástrojů)** | Nástroje IDE/agent prostřednictvím 3 transportů: stdio, SSE ( `/api/mcp/sse` ), Streamovatelný HTTP ( `/api/mcp/stream` ) |
| 🤝 **A2A server (JSON-RPC + SSE)** | Spouštění úloh mezi agenty se synchronizací a streamováním |
| 🧭 **Konsolidovaná stránka koncových bodů** | Stránka pro správu s kartami Endpoint Proxy, MCP, A2A a API Endpoints |
| 🎚️ **Přepínače pro povolení/zakázání služby** | Přepínače ZAP/VYP pro MCP a A2A s trvalým nastavením (výchozí: VYP) |
| 🛰️ **Srdeční tep za běhu MCP** | Skutečný stav procesu (pid, doba provozuschopnosti, stáří heartbeatu, transport, režim rozsahu) |
| 📋 **Auditní záznam MCP** | Filtrovatelné protokoly auditu s hodnocením úspěchu/neúspěchu a klíčovým přiřazením |
| 🔐 **Vynucování rozsahu MCP** | 9 podrobných oprávnění pro řízený přístup k nástrojům |
| 📡 **Správa životního cyklu úkolů A2A** | Seznam/filtrování úloh, kontrola událostí/artefaktů, zrušení spuštěných úloh |
| 📋 **Objevení karty agenta** | `/.well-known/agent.json` pro automatické vyhledávání klientů |
| 🧪 **Testovací postroj Protocol E2E** | Skutečné MCP SDK + toky klientů A2A v `test:protocols:e2e` |
| ⚙️ **Provozní kontroly** | Kombinace přepínačů, použití profilů odolnosti, resetování jističů z jednoho ovládacího panelu |
### 🧠 Směrování a inteligence
Funkce | Co to dělá
--- | ---
🎯 **Inteligentní čtyřúrovňový záložní systém** | Automatická trasa: Předplatné → API klíč → Levné → Zdarma
📊 **Sledování kvót v reálném čase** | Počet tokenů v reálném čase + odpočet resetování pro každého poskytovatele
🔄 **Překlad formátu** | OpenAI ↔ Claude ↔ Gemini ↔ Odpovědi s konverzemi bezpečnými pro schéma
👥 **Podpora více účtů** | Více účtů na poskytovatele s inteligentním výběrem
🔄 **Automatická aktualizace tokenů** | Tokeny OAuth se automaticky obnovují při opakovaném pokusu.
🎨 **Vlastní kombinace** | 6 vyvažovacích strategií + řízení záložního řetězce
🌐 **Směrovač se zástupnými znaky** | dynamické směrování `provider/*`
🧠 **Přemýšlení o rozpočtových kontrolách** | Limity pro průchozí, automatické, vlastní a adaptivní uvažování
🔀 **Aliasy modelů** | Vestavěné + vlastní aliasování modelů a bezpečnost migrace
**Degradace pozadí** | Směrujte úlohy na pozadí s nízkou prioritou na levnější modely
🧪 **Chytré směrování s ohledem na úkoly** | Automatický výběr modelu podle typu obsahu (kódování/vize/analýza/sumarizace)
💬 **Vstřikování do systému** | Globální kontroly chování uplatňované konzistentně
📄 **Kompatibilita API pro odpovědi** | Plná podpora `/v1/responses` pro Codex a pokročilé agentické pracovní postupy
| Funkce | Co to dělá |
| ----------------------------------------------- | ----------------------------------------------------------------------------- |
| 🎯 **Inteligentní čtyřúrovňový záložní systém** | Automatická trasa: Předplatné → API klíč → Levné → Zdarma |
| 📊 **Sledování kvót v reálném čase** | Počet tokenů v reálném čase + odpočet resetování pro každého poskytovatele |
| 🔄 **Překlad formátu** | OpenAI ↔ Claude ↔ Gemini ↔ Odpovědi s konverzemi bezpečnými pro schéma |
| 👥 **Podpora více účtů** | Více účtů na poskytovatele s inteligentním výběrem |
| 🔄 **Automatická aktualizace tokenů** | Tokeny OAuth se automaticky obnovují při opakovaném pokusu. |
| 🎨 **Vlastní kombinace** | 6 vyvažovacích strategií + řízení záložního řetězce |
| 🌐 **Směrovač se zástupnými znaky** | dynamické směrování `provider/*` |
| 🧠 **Přemýšlení o rozpočtových kontrolách** | Limity pro průchozí, automatické, vlastní a adaptivní uvažování |
| 🔀 **Aliasy modelů** | Vestavěné + vlastní aliasování modelů a bezpečnost migrace |
| **Degradace pozadí** | Směrujte úlohy na pozadí s nízkou prioritou na levnější modely |
| 🧪 **Chytré směrování s ohledem na úkoly** | Automatický výběr modelu podle typu obsahu (kódování/vize/analýza/sumarizace) |
| 💬 **Vstřikování do systému** | Globální kontroly chování uplatňované konzistentně |
| 📄 **Kompatibilita API pro odpovědi** | Plná podpora `/v1/responses` pro Codex a pokročilé agentické pracovní postupy |
### 🎵 Multimodální API
Funkce | Co to dělá
--- | ---
🖼️ **Generování obrázků** | `/v1/images/generations` s cloudovým a lokálním backendem
📐 **Vložení** | `/v1/embeddings` pro vyhledávání a RAG pipelines
🎤 **Přepis zvuku** | `/v1/audio/transcriptions` (Whisper a další poskytovatelé)
🔊 **Převod textu na řeč** | `/v1/audio/speech` (více enginů/poskytovatelů)
🎬 **Generování videa** | `/v1/videos/generations` (pracovní postupy ComfyUI + SD WebUI)
🎵 **Hudební generace** | `/v1/music/generations` (pracovní postupy ComfyUI)
🛡️ **Moderování** | Bezpečnostní kontroly `/v1/moderations`
🔀 **Změna pořadí** | `/v1/rerank` pro hodnocení relevance
🔍 **Vyhledávání na webu** 🆕 | `/v1/search` — 5 poskytovatelů (Serper, Brave, Perplexity, Exa, Tavily), více než 6 500 zdarma/měsíc, automatické přepnutí na záložní systém, mezipaměť
| Funkce | Co to dělá |
| ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 🖼️ **Generování obrázků** | `/v1/images/generations` s cloudovým a lokálním backendem |
| 📐 **Vložení** | `/v1/embeddings` pro vyhledávání a RAG pipelines |
| 🎤 **Přepis zvuku** | `/v1/audio/transcriptions` (Whisper a další poskytovatelé) |
| 🔊 **Převod textu na řeč** | `/v1/audio/speech` (více enginů/poskytovatelů) |
| 🎬 **Generování videa** | `/v1/videos/generations` (pracovní postupy ComfyUI + SD WebUI) |
| 🎵 **Hudební generace** | `/v1/music/generations` (pracovní postupy ComfyUI) |
| 🛡️ **Moderování** | Bezpečnostní kontroly `/v1/moderations` |
| 🔀 **Změna pořadí** | `/v1/rerank` pro hodnocení relevance |
| 🔍 **Vyhledávání na webu** 🆕 | `/v1/search` — 5 poskytovatelů (Serper, Brave, Perplexity, Exa, Tavily), více než 6 500 zdarma/měsíc, automatické přepnutí na záložní systém, mezipaměť |
### 🛡️ Odolnost, bezpečnost a správa věcí veřejných
Funkce | Co to dělá
--- | ---
🔌 **Jističe** | Vypnutí/obnovení pro každý model s ovládáním prahových hodnot
🎯 **Modely s ohledem na koncové body** | Vlastní modely deklarují podporované koncové body + formát API
🛡️ **Stádo proti hromům** | Ochrana mutexu a semaforu při událostech opakování/rychlosti
🧠 **Sémantická + podpisová mezipaměť** | Snížení nákladů/latence díky dvěma vrstvám mezipaměti
**Žádost o idempotenci** | Okno ochrany proti duplikacím
🔒 **Falšování otisků prstů pomocí TLS** | Otisk TLS podobný prohlížeči **snižuje detekci botů a nahlašování účtů**
🔏 **Porovnávání otisků prstů v CLI** | Shoduje se s nativními podpisy požadavků CLI **snižuje riziko zablokování a zároveň zachovává IP adresu proxy**
🌐 **Filtrování IP adres** | Ovládání seznamu povolených/blokovaných položek pro odhalená nasazení
📊 **Upravitelné limity rychlosti** | Konfigurovatelné globální/na úrovni poskytovatele limity s perzistencí
🔑 **Správa klíčů API a stanovení rozsahu** | Bezpečné vydávání/rotace klíčů a kontroly modelu/poskytovatele
🛡️ **Chráněné `/models`** | Volitelné ověřování a skrytí poskytovatele pro katalog modelů
| Funkce | Co to dělá |
| ------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- |
| 🔌 **Jističe** | Vypnutí/obnovení pro každý model s ovládáním prahových hodnot |
| 🎯 **Modely s ohledem na koncové body** | Vlastní modely deklarují podporované koncové body + formát API |
| 🛡️ **Stádo proti hromům** | Ochrana mutexu a semaforu při událostech opakování/rychlosti |
| 🧠 **Sémantická + podpisová mezipaměť** | Snížení nákladů/latence díky dvěma vrstvám mezipaměti |
| **Žádost o idempotenci** | Okno ochrany proti duplikacím |
| 🔒 **Falšování otisků prstů pomocí TLS** | Otisk TLS podobný prohlížeči **snižuje detekci botů a nahlašování účtů** |
| 🔏 **Porovnávání otisků prstů v CLI** | Shoduje se s nativními podpisy požadavků CLI **snižuje riziko zablokování a zároveň zachovává IP adresu proxy** |
| 🌐 **Filtrování IP adres** | Ovládání seznamu povolených/blokovaných položek pro odhalená nasazení |
| 📊 **Upravitelné limity rychlosti** | Konfigurovatelné globální/na úrovni poskytovatele limity s perzistencí |
| 🔑 **Správa klíčů API a stanovení rozsahu** | Bezpečné vydávání/rotace klíčů a kontroly modelu/poskytovatele |
| 🛡️ **Chráněné `/models`** | Volitelné ověřování a skrytí poskytovatele pro katalog modelů |
### 📊 Pozorovatelnost a analytika
Funkce | Co to dělá
--- | ---
📝 **Žádost + protokolování proxy** | Úplné protokolování požadavků/odpovědí a proxy
📋 **Sjednocený panel protokolů** | Zobrazení požadavků, proxy, auditu a konzole na jedné stránce
🔍 **Vyžádat si telemetrii** | Latence p50/p95/p99 a trasování požadavků
🏥 **Panel zdraví** | Doba provozuschopnosti, stavy jističů, uzamčení, statistiky mezipaměti
💰 **Sledování nákladů** | Kontrola rozpočtu a přehled o cenách pro jednotlivé modely
📈 **Analytické vizualizace** | Přehledy využití modelů/poskytovatelů a zobrazení trendů
🧪 **Rámec hodnocení** | Testování zlaté sady s konfigurovatelnými strategiemi shody
| Funkce | Co to dělá |
| ----------------------------------- | ---------------------------------------------------------------------- |
| 📝 **Žádost + protokolování proxy** | Úplné protokolování požadavků/odpovědí a proxy |
| 📋 **Sjednocený panel protokolů** | Zobrazení požadavků, proxy, auditu a konzole na jedné stránce |
| 🔍 **Vyžádat si telemetrii** | Latence p50/p95/p99 a trasování požadavků |
| 🏥 **Panel zdraví** | Doba provozuschopnosti, stavy jističů, uzamčení, statistiky mezipaměti |
| 💰 **Sledování nákladů** | Kontrola rozpočtu a přehled o cenách pro jednotlivé modely |
| 📈 **Analytické vizualizace** | Přehledy využití modelů/poskytovatelů a zobrazení trendů |
| 🧪 **Rámec hodnocení** | Testování zlaté sady s konfigurovatelnými strategiemi shody |
### ☁️ Nasazení a platforma
Funkce | Co to dělá
--- | ---
🌐 **Nasazení kdekoli** | Localhost, VPS, Docker, cloudová prostředí
💾 **Synchronizace s cloudem** | Synchronizace konfigurace přes cloud worker
🔄 **Zálohování/Obnovení** | Toky exportu/importu a obnovy po havárii
🧙 **Průvodce nástupem** | Průvodce prvním spuštěním
🔧 **Panel nástrojů CLI** | Nastavení oblíbených kódovacích nástrojů jedním kliknutím
🎮 **Modelové hřiště** | Otestujte libovolného poskytovatele/model/koncový bod z řídicího panelu
🔏 **Přepínač otisků prstů v příkazovém řádku** | Porovnávání otisků prstů podle poskytovatele v Nastavení &gt; Zabezpečení
🌐 **i18n (30 jazyků)** | Plná jazyková podpora dashboardu a dokumentace s psaním zprava doleva
📂 **Adresář vlastních dat** | Přepsání `DATA_DIR` pro umístění úložiště
| Funkce | Co to dělá |
| ----------------------------------------------- | ------------------------------------------------------------------------- |
| 🌐 **Nasazení kdekoli** | Localhost, VPS, Docker, cloudová prostředí |
| 💾 **Synchronizace s cloudem** | Synchronizace konfigurace přes cloud worker |
| 🔄 **Zálohování/Obnovení** | Toky exportu/importu a obnovy po havárii |
| 🧙 **Průvodce nástupem** | Průvodce prvním spuštěním |
| 🔧 **Panel nástrojů CLI** | Nastavení oblíbených kódovacích nástrojů jedním kliknutím |
| 🎮 **Modelové hřiště** | Otestujte libovolného poskytovatele/model/koncový bod z řídicího panelu |
| 🔏 **Přepínač otisků prstů v příkazovém řádku** | Porovnávání otisků prstů podle poskytovatele v Nastavení &gt; Zabezpečení |
| 🌐 **i18n (30 jazyků)** | Plná jazyková podpora dashboardu a dokumentace s psaním zprava doleva |
| 📂 **Adresář vlastních dat** | Přepsání `DATA_DIR` pro umístění úložiště |
### Hluboký pohled na funkce
@@ -1203,12 +1115,12 @@ Předinstalovaná sada „OmniRoute Golden Set“ obsahuje testovací případy
### Strategie hodnocení
Strategie | Popis | Příklad
--- | --- | ---
`exact` | Výstup se musí přesně shodovat | `"4"`
`contains` | Výstup musí obsahovat podřetězec (bez rozlišení velkých a malých písmen) | `"Paris"`
`regex` | Výstup musí odpovídat vzoru regulárních výrazů | `"1.*2.*3"`
`custom` | Vlastní JS funkce vrací true/false | `(output) => output.length > 10`
| Strategie | Popis | Příklad |
| ---------- | ------------------------------------------------------------------------ | -------------------------------- |
| `exact` | Výstup se musí přesně shodovat | `"4"` |
| `contains` | Výstup musí obsahovat podřetězec (bez rozlišení velkých a malých písmen) | `"Paris"` |
| `regex` | Výstup musí odpovídat vzoru regulárních výrazů | `"1.*2.*3"` |
| `custom` | Vlastní JS funkce vrací true/false | `(output) => output.length > 10` |
---
@@ -1240,9 +1152,6 @@ Užitečná API pro automatizaci:
- `GET /api/mcp/audit`
- `GET /api/mcp/audit/stats`
<details>
<summary><b>🤝 Nastavení A2A (Agent2Agent)</b></summary>
</details>
@@ -1272,9 +1181,6 @@ Provozní uživatelské rozhraní:
- `/dashboard/a2a` pro pozorovatelnost úloh/stavů/streamů a akce kouření
<details>
<summary><b>🧪 Komplexní validace protokolu</b></summary>
</details>
@@ -1291,9 +1197,6 @@ Tím se ověřuje:
- A2A objevování/odesílání/streamování/získávání/zrušení
- Křížová kontrola dat v auditu MCP a API pro správu úloh A2A
<details>
<summary><b>💳 Poskytovatelé předplatného</b></summary>
</details>
@@ -1369,9 +1272,6 @@ Models:
gh/gemini-3-pro
```
<details>
<summary><b>🔑 Poskytovatelé klíčů API</b></summary>
</details>
@@ -1381,7 +1281,7 @@ Models:
1. Registrace: [build.nvidia.com](https://build.nvidia.com)
2. Získejte zdarma klíč API (včetně 1000 inferenčních kreditů)
3. Ovládací panel → Přidat poskytovatele → NVIDIA NIM:
- Klíč API: `nvapi-your-key`
- Klíč API: `nvapi-your-key`
**Modely:** `nvidia/llama-3.3-70b-instruct` , `nvidia/mistral-7b-instruct` a více než 50 dalších
@@ -1413,9 +1313,6 @@ Models:
**Modely:** Získejte přístup k více než 100 modelům od všech hlavních poskytovatelů prostřednictvím jediného klíče API.
<details>
<summary><b>💰 Levní poskytovatelé (záložní)</b></summary>
</details>
@@ -1425,8 +1322,8 @@ Models:
1. Registrace: [Zhipu AI](https://open.bigmodel.cn/)
2. Získejte klíč API z kódovacího plánu
3. Nástěnka → Přidat klíč API:
- Poskytovatel: `glm`
- Klíč API: `your-key`
- Poskytovatel: `glm`
- Klíč API: `your-key`
**Použití:** `glm/glm-4.7`
@@ -1452,9 +1349,6 @@ Models:
**Tip pro profesionály:** Fixních 9 $/měsíc za 10 milionů tokenů = efektivní náklady 0,90 $/1 milion!
<details>
<summary><b>🆓 BEZPLATNÍ poskytovatelé (nouzové zálohování)</b></summary>
</details>
@@ -1498,9 +1392,6 @@ Models:
kr/claude-haiku-4.5
```
<details>
<summary><b>🎨 Vytvořte kombinace</b></summary>
</details>
@@ -1531,9 +1422,6 @@ Models:
Cost: $0 forever!
```
<details>
<summary><b>🔧 Integrace s rozhraním příkazového řádku</b></summary>
</details>
@@ -1637,9 +1525,6 @@ opencode
> **Tip:** Do sekce `models` přidejte jakýkoli model dostupný ve vašem koncovém bodu OmniRoute `/v1/models` . Použijte formát `provider/model-id` z vašeho dashboardu OmniRoute.
---
## 🐛 Řešení problémů
@@ -1880,14 +1765,8 @@ Chcete-li získat přístup k kriterii pověření, můžete použít adresu **U
> Toto řešení funguje na základě autorizačního kódu na adrese URL a nezávislého přesměrování přesměrování nebo jiného.
---
## 🛠️ Technologický stack
<details>
@@ -1909,28 +1788,25 @@ Chcete-li získat přístup k kriterii pověření, můžete použít adresu **U
- **Docker** : [hub.docker.com/r/diegosouzapw/omniroute](https://hub.docker.com/r/diegosouzapw/omniroute)
- **Odolnost** : Jistič, exponenciální odstavení, ochrana proti hromům, falešné TLS, automatické kombinované samoopravování
---
## 📖 Dokumentace
Dokument | Popis
--- | ---
[Uživatelská příručka](docs/USER_GUIDE.md) | Poskytovatelé, kombinace, integrace CLI, nasazení
[Referenční informace k API](docs/API_REFERENCE.md) | Všechny koncové body s příklady
[MCP server](open-sse/mcp-server/README.md) | 16 nástrojů MCP, konfigurace IDE, klienti Python/TS/Go
[Server A2A](src/lib/a2a/README.md) | Protokol JSON-RPC 2.0, dovednosti, streamování, správa úloh
[Auto-Combo Engine](docs/auto-combo.md) | 6faktorové bodování, balíčky režimů, samoléčba
[Odstraňování problémů](docs/TROUBLESHOOTING.md) | Běžné problémy a jejich řešení
[Architektura](docs/ARCHITECTURE.md) | Architektura a interní prvky systému
[Přispívání](CONTRIBUTING.md) | Nastavení a pokyny pro vývoj
[Specifikace OpenAPI](docs/openapi.yaml) | Specifikace OpenAPI 3.0
[Bezpečnostní zásady](SECURITY.md) | Hlášení zranitelností a bezpečnostní postupy
[Nasazení virtuálního počítače](docs/VM_DEPLOYMENT_GUIDE.md) | Kompletní průvodce: Nastavení virtuálního počítače + nginx + Cloudflare
[Galerie funkcí](docs/FEATURES.md) | Vizuální prohlídka řídicího panelu se snímky obrazovky
[Kontrolní seznam vydání](docs/RELEASE_CHECKLIST.md) | Kroky ověření před vydáním
| Dokument | Popis |
| ------------------------------------------------------------ | ----------------------------------------------------------------------- |
| [Uživatelská příručka](docs/USER_GUIDE.md) | Poskytovatelé, kombinace, integrace CLI, nasazení |
| [Referenční informace k API](docs/API_REFERENCE.md) | Všechny koncové body s příklady |
| [MCP server](open-sse/mcp-server/README.md) | 16 nástrojů MCP, konfigurace IDE, klienti Python/TS/Go |
| [Server A2A](src/lib/a2a/README.md) | Protokol JSON-RPC 2.0, dovednosti, streamování, správa úloh |
| [Auto-Combo Engine](docs/auto-combo.md) | 6faktorové bodování, balíčky režimů, samoléčba |
| [Odstraňování problémů](docs/TROUBLESHOOTING.md) | Běžné problémy a jejich řešení |
| [Architektura](docs/ARCHITECTURE.md) | Architektura a interní prvky systému |
| [Přispívání](CONTRIBUTING.md) | Nastavení a pokyny pro vývoj |
| [Specifikace OpenAPI](docs/openapi.yaml) | Specifikace OpenAPI 3.0 |
| [Bezpečnostní zásady](SECURITY.md) | Hlášení zranitelností a bezpečnostní postupy |
| [Nasazení virtuálního počítače](docs/VM_DEPLOYMENT_GUIDE.md) | Kompletní průvodce: Nastavení virtuálního počítače + nginx + Cloudflare |
| [Galerie funkcí](docs/FEATURES.md) | Vizuální prohlídka řídicího panelu se snímky obrazovky |
| [Kontrolní seznam vydání](docs/RELEASE_CHECKLIST.md) | Kroky ověření před vydáním |
---
@@ -1938,14 +1814,14 @@ Dokument | Popis
OmniRoute má **v plánu více než 210 funkcí** v několika fázích vývoje. Zde jsou klíčové oblasti:
Kategorie | Plánované funkce | Hlavní body
--- | --- | ---
🧠 **Směrování a inteligence** | 25+ | Směrování s nejnižší latencí, směrování založené na tagech, kontrola kvót před výstupem, výběr účtu P2C
🔒 **Zabezpečení a dodržování předpisů** | 20+ | Zpevnění SSRF, maskování přihlašovacích údajů, limit rychlosti pro každý koncový bod, stanovení rozsahu klíčů pro správu
📊 **Pozorovatelnost** | 15+ | Integrace OpenTelemetry, sledování kvót v reálném čase, sledování nákladů podle modelu
🔄 **Integrace poskytovatelů** | 20+ | Dynamický registr modelů, doba zchlazení poskytovatelů, Codex pro více účtů, analýza kvót Copilota
**Výkon** | 15+ | Dvojitá vrstva mezipaměti, mezipaměť výzev, mezipaměť odpovědí, udržování streamování, dávkové API
🌐 **Ekosystém** | 10+ | WebSocket API, horké opětovné načítání konfigurace, distribuované úložiště konfigurace, komerční režim
| Kategorie | Plánované funkce | Hlavní body |
| ---------------------------------------- | ---------------- | ------------------------------------------------------------------------------------------------------------------------ |
| 🧠 **Směrování a inteligence** | 25+ | Směrování s nejnižší latencí, směrování založené na tagech, kontrola kvót před výstupem, výběr účtu P2C |
| 🔒 **Zabezpečení a dodržování předpisů** | 20+ | Zpevnění SSRF, maskování přihlašovacích údajů, limit rychlosti pro každý koncový bod, stanovení rozsahu klíčů pro správu |
| 📊 **Pozorovatelnost** | 15+ | Integrace OpenTelemetry, sledování kvót v reálném čase, sledování nákladů podle modelu |
| 🔄 **Integrace poskytovatelů** | 20+ | Dynamický registr modelů, doba zchlazení poskytovatelů, Codex pro více účtů, analýza kvót Copilota |
| **Výkon** | 15+ | Dvojitá vrstva mezipaměti, mezipaměť výzev, mezipaměť odpovědí, udržování streamování, dávkové API |
| 🌐 **Ekosystém** | 10+ | WebSocket API, horké opětovné načítání konfigurace, distribuované úložiště konfigurace, komerční režim |
### 🔜 Již brzy
+2 -2
View File
@@ -934,8 +934,8 @@ OmniRoute v2.0 er bygget som en operationel platform, ikke kun en relæ-proxy.
| Funktion | Hvad det gør || -------------------------- | -------------------------------------------------------------------- |
| 🖼️ **Billedgenerering** | `/v1/images/generations` med cloud og lokale backends |
| 📐 **Indlejringer** | `/v1/embeddings` til søgning og RAG-rørledninger |
| 🎤 **Lydtransskription** | `/v1/audio/transcriptions` (Whisper og yderligere udbydere) |
| 🔊 **Tekst-til-tale** | `/v1/audio/speech` (flere motorer/udbydere) |
| 🎤 **Lydtransskription** | `/v1/audio/transcriptions` — 7 providers (Deepgram Nova 3, AssemblyAI, Groq Whisper, HuggingFace, ElevenLabs, OpenAI, Azure), auto-language detection, MP4/MP3/WAV support |
| 🔊 **Tekst-til-tale** | `/v1/audio/speech` — 10 providers (ElevenLabs, OpenAI, Deepgram, Cartesia, PlayHT, HuggingFace, Nvidia NIM, Inworld, Coqui, Tortoise) |
| 🎬 **Videogenerering** | `/v1/videos/generations` (ComfyUI + SD WebUI-arbejdsgange) |
| 🎵 **Music Generation** | `/v1/music/generations` (ComfyUI-arbejdsgange) |
| 🛡️ **Moderationer** | `/v1/moderations` sikkerhedstjek |
+2 -2
View File
@@ -939,8 +939,8 @@ OmniRoute v2.0 ist als Betriebsplattform konzipiert und nicht nur als Relay-Prox
| Funktion | Was es tut || -------------------------- | ------------------------------------------------------------- |
| 🖼️ **Bilderzeugung** | `/v1/images/generations` mit Cloud- und lokalen Backends |
| 📐 **Einbettungen** | `/v1/embeddings` für Such- und RAG-Pipelines |
| 🎤 **Audio-Transkription** | `/v1/audio/transcriptions` (Whisper und zusätzliche Anbieter) |
| 🔊 **Text-to-Speech** | `/v1/audio/speech` (mehrere Engines/Anbieter) |
| 🎤 **Audio-Transkription** | `/v1/audio/transcriptions` — 7 providers (Deepgram Nova 3, AssemblyAI, Groq Whisper, HuggingFace, ElevenLabs, OpenAI, Azure), auto-language detection, MP4/MP3/WAV support |
| 🔊 **Text-to-Speech** | `/v1/audio/speech` — 10 providers (ElevenLabs, OpenAI, Deepgram, Cartesia, PlayHT, HuggingFace, Nvidia NIM, Inworld, Coqui, Tortoise) |
| 🎬 **Videogenerierung** | `/v1/videos/generations` (ComfyUI + SD WebUI-Workflows) |
| 🎵 **Musikgeneration** | `/v1/music/generations` (ComfyUI-Workflows) |
| 🛡️ **Moderationen** | `/v1/moderations` Sicherheitsprüfungen |
+8 -8
View File
@@ -877,14 +877,14 @@ npm run electron:build:linux # Linux (.AppImage)
### 🎵 APIs Multi-Modal
| Característica | Qué Hace |
| ----------------------------- | ------------------------------------------------------ |
| 🖼️ **Generación de Imágenes** | `/v1/images/generations` — 4 proveedores, 9+ modelos |
| 📐 **Embeddings** | `/v1/embeddings` — 6 proveedores, 9+ modelos |
| 🎤 **Transcripción de Audio** | `/v1/audio/transcriptions`Compatible con Whisper |
| 🔊 **Texto a Voz** | `/v1/audio/speech`Síntesis de audio multi-proveedor |
| 🛡️ **Moderaciones** | `/v1/moderations` — Verificaciones de seguridad |
| 🔀 **Reranking** | `/v1/rerank` — Reranking de relevancia de documentos |
| Característica | Qué Hace |
| ----------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 🖼️ **Generación de Imágenes** | `/v1/images/generations` — 4 proveedores, 9+ modelos |
| 📐 **Embeddings** | `/v1/embeddings` — 6 proveedores, 9+ modelos |
| 🎤 **Transcripción de Audio** | `/v1/audio/transcriptions`7 providers (Deepgram Nova 3, AssemblyAI, Groq Whisper, HuggingFace, ElevenLabs, OpenAI, Azure), auto-language detection, MP4/MP3/WAV support |
| 🔊 **Texto a Voz** | `/v1/audio/speech`10 providers (ElevenLabs, OpenAI, Deepgram, Cartesia, PlayHT, HuggingFace, Nvidia NIM, Inworld, Coqui, Tortoise) |
| 🛡️ **Moderaciones** | `/v1/moderations` — Verificaciones de seguridad |
| 🔀 **Reranking** | `/v1/rerank` — Reranking de relevancia de documentos |
### 🛡️ Resiliencia y Seguridad
+8 -8
View File
@@ -874,14 +874,14 @@ npm run electron:build:linux # Linux (.AppImage)
### 🎵 Multimodaaliset sovellusliittymät
| Ominaisuus | Mitä se tekee |
| ------------------------- | --------------------------------------------------------- |
| 🖼️ **Kuvan luominen** | `/v1/images/generations` — 4 toimittajaa, 9+ mallia |
| 📐 **Upotukset** | `/v1/embeddings` — 6 toimittajaa, 9+ mallia |
| 🎤 **Äänitranskriptio** | `/v1/audio/transcriptions`Kuiskausyhteensopiva |
| 🔊 **Tekstistä puheeksi** | `/v1/audio/speech`Usean palveluntarjoajan äänisynteesi |
| 🛡️ **Moderaatiot** | `/v1/moderations` — Sisällön turvallisuustarkistukset |
| 🔀 **Uudelleenjärjestys** | `/v1/rerank` — Asiakirjan osuvuuden uudelleensijoitus |
| Ominaisuus | Mitä se tekee |
| ------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 🖼️ **Kuvan luominen** | `/v1/images/generations` — 4 toimittajaa, 9+ mallia |
| 📐 **Upotukset** | `/v1/embeddings` — 6 toimittajaa, 9+ mallia |
| 🎤 **Äänitranskriptio** | `/v1/audio/transcriptions`7 providers (Deepgram Nova 3, AssemblyAI, Groq Whisper, HuggingFace, ElevenLabs, OpenAI, Azure), auto-language detection, MP4/MP3/WAV support |
| 🔊 **Tekstistä puheeksi** | `/v1/audio/speech`10 providers (ElevenLabs, OpenAI, Deepgram, Cartesia, PlayHT, HuggingFace, Nvidia NIM, Inworld, Coqui, Tortoise) |
| 🛡️ **Moderaatiot** | `/v1/moderations` — Sisällön turvallisuustarkistukset |
| 🔀 **Uudelleenjärjestys** | `/v1/rerank` — Asiakirjan osuvuuden uudelleensijoitus |
### 🛡️ Joustavuus ja turvallisuus
+8 -8
View File
@@ -875,14 +875,14 @@ npm run electron:build:linux # Linux (.AppImage)
### 🎵 APIs multi-modales
| Fonctionnalité | Ce qu'elle fait |
| -------------------------- | ------------------------------------------------------- |
| 🖼️ **Génération d'images** | `/v1/images/generations` — 4 fournisseurs, 9+ modèles |
| 📐 **Embeddings** | `/v1/embeddings` — 6 fournisseurs, 9+ modèles |
| 🎤 **Transcription audio** | `/v1/audio/transcriptions`compatible Whisper |
| 🔊 **Texte vers parole** | `/v1/audio/speech`synthèse audio multi-fournisseur |
| 🛡️ **Modérations** | `/v1/moderations` — vérifications de sécurité |
| 🔀 **Reranking** | `/v1/rerank` — reclassement de pertinence des documents |
| Fonctionnalité | Ce qu'elle fait |
| -------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 🖼️ **Génération d'images** | `/v1/images/generations` — 4 fournisseurs, 9+ modèles |
| 📐 **Embeddings** | `/v1/embeddings` — 6 fournisseurs, 9+ modèles |
| 🎤 **Transcription audio** | `/v1/audio/transcriptions`7 providers (Deepgram Nova 3, AssemblyAI, Groq Whisper, HuggingFace, ElevenLabs, OpenAI, Azure), auto-language detection, MP4/MP3/WAV support |
| 🔊 **Texte vers parole** | `/v1/audio/speech`10 providers (ElevenLabs, OpenAI, Deepgram, Cartesia, PlayHT, HuggingFace, Nvidia NIM, Inworld, Coqui, Tortoise) |
| 🛡️ **Modérations** | `/v1/moderations` — vérifications de sécurité |
| 🔀 **Reranking** | `/v1/rerank` — reclassement de pertinence des documents |
### 🛡️ Résilience & Sécurité
+8 -8
View File
@@ -873,14 +873,14 @@ npm run electron:build:linux # Linux (.AppImage)
### 🎵 ממשקי API רב-מודאליים
| תכונה | מה זה עושה |
| ------------------- | --------------------------------------------- |
| 🖼️ **יצירת תמונות** | `/v1/images/generations` — 4 ספקים, 9+ דגמים |
| 📐 **הטבעות** | `/v1/embeddings` — 6 ספקים, 9+ דגמים |
| 🎤 **תמלול אודיו** | `/v1/audio/transcriptions`תואם לחישה |
| 🔊 **טקסט לדיבור** | `/v1/audio/speech`סינתזת אודיו מרובה ספקים |
| 🛡️ **מנחים** | `/v1/moderations` — בדיקות בטיחות תוכן |
| 🔀 **דירוג מחדש** | `/v1/rerank` — דירוג מחדש של רלוונטיות המסמך |
| תכונה | מה זה עושה |
| ------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 🖼️ **יצירת תמונות** | `/v1/images/generations` — 4 ספקים, 9+ דגמים |
| 📐 **הטבעות** | `/v1/embeddings` — 6 ספקים, 9+ דגמים |
| 🎤 **תמלול אודיו** | `/v1/audio/transcriptions`7 providers (Deepgram Nova 3, AssemblyAI, Groq Whisper, HuggingFace, ElevenLabs, OpenAI, Azure), auto-language detection, MP4/MP3/WAV support |
| 🔊 **טקסט לדיבור** | `/v1/audio/speech`10 providers (ElevenLabs, OpenAI, Deepgram, Cartesia, PlayHT, HuggingFace, Nvidia NIM, Inworld, Coqui, Tortoise) |
| 🛡️ **מנחים** | `/v1/moderations` — בדיקות בטיחות תוכן |
| 🔀 **דירוג מחדש** | `/v1/rerank` — דירוג מחדש של רלוונטיות המסמך |
### 🛡️ חוסן וביטחון
+8 -8
View File
@@ -874,14 +874,14 @@ npm run electron:build:linux # Linux (.AppImage)
### 🎵 Multimodális API-k
| Funkció | Mit csinál |
| ---------------------- | ------------------------------------------------------------ |
| 🖼️ **Képgenerálás** | `/v1/images/generations` — 4 szolgáltató, 9+ modell |
| 📐 **Beágyazás** | `/v1/embeddings` — 6 szolgáltató, 9+ modell |
| 🎤 **Audio átírás** | `/v1/audio/transcriptions`Suttogás-kompatibilis |
| 🔊 **Szövegfelolvasó** | `/v1/audio/speech`Hangszintézis több szolgáltatónál |
| 🛡️ **Moderálás** | `/v1/moderations` — Tartalombiztonsági ellenőrzések |
| 🔀 **Átsorolás** | `/v1/rerank` — A dokumentumok relevancia szerinti átsorolása |
| Funkció | Mit csinál |
| ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 🖼️ **Képgenerálás** | `/v1/images/generations` — 4 szolgáltató, 9+ modell |
| 📐 **Beágyazás** | `/v1/embeddings` — 6 szolgáltató, 9+ modell |
| 🎤 **Audio átírás** | `/v1/audio/transcriptions`7 providers (Deepgram Nova 3, AssemblyAI, Groq Whisper, HuggingFace, ElevenLabs, OpenAI, Azure), auto-language detection, MP4/MP3/WAV support |
| 🔊 **Szövegfelolvasó** | `/v1/audio/speech`10 providers (ElevenLabs, OpenAI, Deepgram, Cartesia, PlayHT, HuggingFace, Nvidia NIM, Inworld, Coqui, Tortoise) |
| 🛡️ **Moderálás** | `/v1/moderations` — Tartalombiztonsági ellenőrzések |
| 🔀 **Átsorolás** | `/v1/rerank` — A dokumentumok relevancia szerinti átsorolása |
### 🛡️ Rugalmasság és biztonság
+8 -8
View File
@@ -874,14 +874,14 @@ npm run electron:build:linux # Linux (.AppImage)
### 🎵 API Multi-Modal
| Fitur | Apa Fungsinya |
| -------------------------- | ------------------------------------------------------ |
| 🖼️ **Pembuatan Gambar** | `/v1/images/generations` — 4 penyedia, 9+ model |
| 📐 **Sematan** | `/v1/embeddings` — 6 penyedia, 9+ model |
| 🎤 **Transkripsi Audio** | `/v1/audio/transcriptions`Kompatibel dengan bisikan |
| 🔊 **Teks-ke-Ucapan** | `/v1/audio/speech`Sintesis audio multi-penyedia |
| 🛡️ **Moderasi** | `/v1/moderations` — Pemeriksaan keamanan konten |
| 🔀 **Pemeringkatan Ulang** | `/v1/rerank` — Pemeringkatan ulang relevansi dokumen |
| Fitur | Apa Fungsinya |
| -------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 🖼️ **Pembuatan Gambar** | `/v1/images/generations` — 4 penyedia, 9+ model |
| 📐 **Sematan** | `/v1/embeddings` — 6 penyedia, 9+ model |
| 🎤 **Transkripsi Audio** | `/v1/audio/transcriptions`7 providers (Deepgram Nova 3, AssemblyAI, Groq Whisper, HuggingFace, ElevenLabs, OpenAI, Azure), auto-language detection, MP4/MP3/WAV support |
| 🔊 **Teks-ke-Ucapan** | `/v1/audio/speech`10 providers (ElevenLabs, OpenAI, Deepgram, Cartesia, PlayHT, HuggingFace, Nvidia NIM, Inworld, Coqui, Tortoise) |
| 🛡️ **Moderasi** | `/v1/moderations` — Pemeriksaan keamanan konten |
| 🔀 **Pemeringkatan Ulang** | `/v1/rerank` — Pemeringkatan ulang relevansi dokumen |
### 🛡️ Ketahanan & Keamanan
+8 -8
View File
@@ -770,14 +770,14 @@ npm run electron:build:linux # Linux (.AppImage)
### 🎵 मल्टी-मॉडल एपीआई
| फ़ीचर | यह क्या करता है |
| ---------------------------- | ------------------------------------------------- |
| 🖼️ **छवि निर्माण** | `/v1/images/generations` - 4 प्रदाता, 9+ मॉडल |
| 📐 **एंबेडिंग** | `/v1/embeddings` — 6 प्रदाता, 9+ मॉडल |
| 🎤 **ऑडियो ट्रांस्क्रिप्शन** | `/v1/audio/transcriptions` - कानाफूसी-संगत |
| 🔊 **टेक्स्ट-टू-स्पीच** | `/v1/audio/speech` - बहु-प्रदाता ऑडियो संश्लेषण |
| 🛡️ **संयम** | `/v1/moderations` — सामग्री सुरक्षा जांच |
| 🔀 **पुनर्रैंकिंग** | `/v1/rerank` — दस्तावेज़ प्रासंगिकता पुनर्रैंकिंग |
| फ़ीचर | यह क्या करता है |
| ---------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 🖼️ **छवि निर्माण** | `/v1/images/generations` - 4 प्रदाता, 9+ मॉडल |
| 📐 **एंबेडिंग** | `/v1/embeddings` — 6 प्रदाता, 9+ मॉडल |
| 🎤 **ऑडियो ट्रांस्क्रिप्शन** | `/v1/audio/transcriptions` — 7 providers (Deepgram Nova 3, AssemblyAI, Groq Whisper, HuggingFace, ElevenLabs, OpenAI, Azure), auto-language detection, MP4/MP3/WAV support |
| 🔊 **टेक्स्ट-टू-स्पीच** | `/v1/audio/speech` — 10 providers (ElevenLabs, OpenAI, Deepgram, Cartesia, PlayHT, HuggingFace, Nvidia NIM, Inworld, Coqui, Tortoise) |
| 🛡️ **संयम** | `/v1/moderations` — सामग्री सुरक्षा जांच |
| 🔀 **पुनर्रैंकिंग** | `/v1/rerank` — दस्तावेज़ प्रासंगिकता पुनर्रैंकिंग |
### 🛡️ लचीलापन और सुरक्षा
+8 -8
View File
@@ -874,14 +874,14 @@ npm run electron:build:linux # Linux (.AppImage)
### 🎵 API Multi-modali
| Funzionalità | Cosa Fa |
| --------------------------- | ---------------------------------------------------- |
| 🖼️ **Generazione immagini** | `/v1/images/generations` — 4 provider, 9+ modelli |
| 📐 **Embeddings** | `/v1/embeddings` — 6 provider, 9+ modelli |
| 🎤 **Trascrizione audio** | `/v1/audio/transcriptions`Compatibile Whisper |
| 🔊 **Testo a voce** | `/v1/audio/speech`Sintesi audio multi-provider |
| 🛡️ **Moderazioni** | `/v1/moderations` — Controlli di sicurezza |
| 🔀 **Reranking** | `/v1/rerank` — Riclassificazione rilevanza documenti |
| Funzionalità | Cosa Fa |
| --------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 🖼️ **Generazione immagini** | `/v1/images/generations` — 4 provider, 9+ modelli |
| 📐 **Embeddings** | `/v1/embeddings` — 6 provider, 9+ modelli |
| 🎤 **Trascrizione audio** | `/v1/audio/transcriptions`7 providers (Deepgram Nova 3, AssemblyAI, Groq Whisper, HuggingFace, ElevenLabs, OpenAI, Azure), auto-language detection, MP4/MP3/WAV support |
| 🔊 **Testo a voce** | `/v1/audio/speech`10 providers (ElevenLabs, OpenAI, Deepgram, Cartesia, PlayHT, HuggingFace, Nvidia NIM, Inworld, Coqui, Tortoise) |
| 🛡️ **Moderazioni** | `/v1/moderations` — Controlli di sicurezza |
| 🔀 **Reranking** | `/v1/rerank` — Riclassificazione rilevanza documenti |
### 🛡️ Resilienza & Sicurezza
+8 -8
View File
@@ -874,14 +874,14 @@ npm run electron:build:linux # Linux (.AppImage)
### 🎵 マルチモーダル API
| 特集 | 何をするのか |
| ----------------------- | --------------------------------------------------------------- |
| 🖼️ **画像生成** | `/v1/images/generations` — 4 つのプロバイダー、9 つ以上のモデル |
| 📐 **埋め込み** | `/v1/embeddings` — 6 つのプロバイダー、9 つ以上のモデル |
| 🎤 **音声文字起こし** | `/v1/audio/transcriptions`ウィスパー互換 |
| 🔊 **テキスト読み上げ** | `/v1/audio/speech`マルチプロバイダーのオーディオ合成 |
| 🛡️ **モデレーション** | `/v1/moderations` — コンテンツの安全性チェック |
| 🔀 **再ランキング** | `/v1/rerank` — ドキュメントの関連性の再ランキング |
| 特集 | 何をするのか |
| ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 🖼️ **画像生成** | `/v1/images/generations` — 4 つのプロバイダー、9 つ以上のモデル |
| 📐 **埋め込み** | `/v1/embeddings` — 6 つのプロバイダー、9 つ以上のモデル |
| 🎤 **音声文字起こし** | `/v1/audio/transcriptions`7 providers (Deepgram Nova 3, AssemblyAI, Groq Whisper, HuggingFace, ElevenLabs, OpenAI, Azure), auto-language detection, MP4/MP3/WAV support |
| 🔊 **テキスト読み上げ** | `/v1/audio/speech`10 providers (ElevenLabs, OpenAI, Deepgram, Cartesia, PlayHT, HuggingFace, Nvidia NIM, Inworld, Coqui, Tortoise) |
| 🛡️ **モデレーション** | `/v1/moderations` — コンテンツの安全性チェック |
| 🔀 **再ランキング** | `/v1/rerank` — ドキュメントの関連性の再ランキング |
### 🛡️ 復元力とセキュリティ
+8 -8
View File
@@ -873,14 +873,14 @@ npm run electron:build:linux # Linux (.AppImage)
### 🎵 다중 모드 API
| 기능 | 그것이 하는 일 |
| ----------------------- | ------------------------------------------------------ |
| 🖼️ **이미지 생성** | `/v1/images/generations` — 4개 공급자, 9개 이상의 모델 |
| 📐 **임베딩** | `/v1/embeddings` — 6개 공급자, 9개 이상의 모델 |
| 🎤 **오디오 전사** | `/v1/audio/transcriptions`속삭임 호환 |
| 🔊 **텍스트 음성 변환** | `/v1/audio/speech`다중 제공자 오디오 합성 |
| 🛡️ **조정** | `/v1/moderations` — 콘텐츠 안전 확인 |
| 🔀 **재순위** | `/v1/rerank` — 문서 관련성 재순위 |
| 기능 | 그것이 하는 일 |
| ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 🖼️ **이미지 생성** | `/v1/images/generations` — 4개 공급자, 9개 이상의 모델 |
| 📐 **임베딩** | `/v1/embeddings` — 6개 공급자, 9개 이상의 모델 |
| 🎤 **오디오 전사** | `/v1/audio/transcriptions`7 providers (Deepgram Nova 3, AssemblyAI, Groq Whisper, HuggingFace, ElevenLabs, OpenAI, Azure), auto-language detection, MP4/MP3/WAV support |
| 🔊 **텍스트 음성 변환** | `/v1/audio/speech`10 providers (ElevenLabs, OpenAI, Deepgram, Cartesia, PlayHT, HuggingFace, Nvidia NIM, Inworld, Coqui, Tortoise) |
| 🛡️ **조정** | `/v1/moderations` — 콘텐츠 안전 확인 |
| 🔀 **재순위** | `/v1/rerank` — 문서 관련성 재순위 |
### 🛡️ 복원력 및 보안
+8 -8
View File
@@ -873,14 +873,14 @@ npm run electron:build:linux # Linux (.AppImage)
### 🎵 API Berbilang Modal
| Ciri | Apa yang Dilakukan |
| ------------------------ | ------------------------------------------------------ |
| 🖼️ **Penjanaan Imej** | `/v1/images/generations` — 4 pembekal, 9+ model |
| 📐 **Pembenaman** | `/v1/embeddings` — 6 pembekal, 9+ model |
| 🎤 **Transkripsi Audio** | `/v1/audio/transcriptions`Serasi dengan bisikan |
| 🔊 **Teks-ke-Ucapan** | `/v1/audio/speech`Sintesis audio berbilang pembekal |
| 🛡️ **Kesederhanaan** | `/v1/moderations` — Pemeriksaan keselamatan kandungan |
| 🔀 **Penyusunan semula** | `/v1/rerank` — Penarafan semula perkaitan dokumen |
| Ciri | Apa yang Dilakukan |
| ------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 🖼️ **Penjanaan Imej** | `/v1/images/generations` — 4 pembekal, 9+ model |
| 📐 **Pembenaman** | `/v1/embeddings` — 6 pembekal, 9+ model |
| 🎤 **Transkripsi Audio** | `/v1/audio/transcriptions`7 providers (Deepgram Nova 3, AssemblyAI, Groq Whisper, HuggingFace, ElevenLabs, OpenAI, Azure), auto-language detection, MP4/MP3/WAV support |
| 🔊 **Teks-ke-Ucapan** | `/v1/audio/speech`10 providers (ElevenLabs, OpenAI, Deepgram, Cartesia, PlayHT, HuggingFace, Nvidia NIM, Inworld, Coqui, Tortoise) |
| 🛡️ **Kesederhanaan** | `/v1/moderations` — Pemeriksaan keselamatan kandungan |
| 🔀 **Penyusunan semula** | `/v1/rerank` — Penarafan semula perkaitan dokumen |
### 🛡️ Ketahanan & Keselamatan
+8 -8
View File
@@ -873,14 +873,14 @@ npm run electron:build:linux # Linux (.AppImage)
### 🎵 Multimodale API's
| Kenmerk | Wat het doet |
| ------------------------ | --------------------------------------------------------- |
| 🖼️ **Beeldgeneratie** | `/v1/images/generations` — 4 providers, 9+ modellen |
| 📐 **Insluitingen** | `/v1/embeddings` — 6 providers, 9+ modellen |
| 🎤 **Audiotranscriptie** | `/v1/audio/transcriptions`Whisper-compatibel |
| 🔊 **Tekst-naar-spraak** | `/v1/audio/speech`Audiosynthese van meerdere providers |
| 🛡️ **Moderaties** | `/v1/moderations` — Veiligheidscontroles van inhoud |
| 🔀 **Herschikking** | `/v1/rerank` — Herschikking van documentrelevantie |
| Kenmerk | Wat het doet |
| ------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 🖼️ **Beeldgeneratie** | `/v1/images/generations` — 4 providers, 9+ modellen |
| 📐 **Insluitingen** | `/v1/embeddings` — 6 providers, 9+ modellen |
| 🎤 **Audiotranscriptie** | `/v1/audio/transcriptions`7 providers (Deepgram Nova 3, AssemblyAI, Groq Whisper, HuggingFace, ElevenLabs, OpenAI, Azure), auto-language detection, MP4/MP3/WAV support |
| 🔊 **Tekst-naar-spraak** | `/v1/audio/speech`10 providers (ElevenLabs, OpenAI, Deepgram, Cartesia, PlayHT, HuggingFace, Nvidia NIM, Inworld, Coqui, Tortoise) |
| 🛡️ **Moderaties** | `/v1/moderations` — Veiligheidscontroles van inhoud |
| 🔀 **Herschikking** | `/v1/rerank` — Herschikking van documentrelevantie |
### 🛡️ Veerkracht en veiligheid
+8 -8
View File
@@ -873,14 +873,14 @@ npm run electron:build:linux # Linux (.AppImage)
### 🎵 Multi-Modal APIer
| Funksjon | Hva det gjør |
| ----------------------- | ------------------------------------------------------ |
| 🖼️ **Bildegenerering** | `/v1/images/generations` — 4 leverandører, 9+ modeller |
| 📐 **Innbygging** | `/v1/embeddings` — 6 leverandører, 9+ modeller |
| 🎤 **Lydtranskripsjon** | `/v1/audio/transcriptions`Whisper-kompatibel |
| 🔊 **Tekst-til-tale** | `/v1/audio/speech`Multi-leverandør lydsyntese |
| 🛡️ **Moderasjoner** | `/v1/moderations` — Innholdssikkerhetssjekker |
| 🔀 **Omrangering** | `/v1/rerank` — Rerangering av dokumentrelevans |
| Funksjon | Hva det gjør |
| ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 🖼️ **Bildegenerering** | `/v1/images/generations` — 4 leverandører, 9+ modeller |
| 📐 **Innbygging** | `/v1/embeddings` — 6 leverandører, 9+ modeller |
| 🎤 **Lydtranskripsjon** | `/v1/audio/transcriptions`7 providers (Deepgram Nova 3, AssemblyAI, Groq Whisper, HuggingFace, ElevenLabs, OpenAI, Azure), auto-language detection, MP4/MP3/WAV support |
| 🔊 **Tekst-til-tale** | `/v1/audio/speech`10 providers (ElevenLabs, OpenAI, Deepgram, Cartesia, PlayHT, HuggingFace, Nvidia NIM, Inworld, Coqui, Tortoise) |
| 🛡️ **Moderasjoner** | `/v1/moderations` — Innholdssikkerhetssjekker |
| 🔀 **Omrangering** | `/v1/rerank` — Rerangering av dokumentrelevans |
### 🛡️ Spenst og sikkerhet
+8 -8
View File
@@ -873,14 +873,14 @@ npm run electron:build:linux # Linux (.AppImage)
### 🎵 Mga Multi-Modal na API
| Tampok | Ano ang Ginagawa Nito |
| -------------------------- | ------------------------------------------------------------ |
| 🖼️ **Pagbuo ng Larawan** | `/v1/images/generations` — 4 na provider, 9+ na modelo |
| 📐 **Mga Pag-embed** | `/v1/embeddings` — 6 na provider, 9+ na modelo |
| 🎤 **Audio Transcription** | `/v1/audio/transcriptions`Whisper-compatible |
| 🔊 **Text-to-Speech** | `/v1/audio/speech`Multi-provider audio synthesis |
| 🛡️ **Mga Pag-moderate** | `/v1/moderations` — Mga pagsusuri sa kaligtasan ng nilalaman |
| 🔀 **Reranking** | `/v1/rerank` — Muling pagraranggo ng kaugnayan ng dokumento |
| Tampok | Ano ang Ginagawa Nito |
| -------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 🖼️ **Pagbuo ng Larawan** | `/v1/images/generations` — 4 na provider, 9+ na modelo |
| 📐 **Mga Pag-embed** | `/v1/embeddings` — 6 na provider, 9+ na modelo |
| 🎤 **Audio Transcription** | `/v1/audio/transcriptions`7 providers (Deepgram Nova 3, AssemblyAI, Groq Whisper, HuggingFace, ElevenLabs, OpenAI, Azure), auto-language detection, MP4/MP3/WAV support |
| 🔊 **Text-to-Speech** | `/v1/audio/speech`10 providers (ElevenLabs, OpenAI, Deepgram, Cartesia, PlayHT, HuggingFace, Nvidia NIM, Inworld, Coqui, Tortoise) |
| 🛡️ **Mga Pag-moderate** | `/v1/moderations` — Mga pagsusuri sa kaligtasan ng nilalaman |
| 🔀 **Reranking** | `/v1/rerank` — Muling pagraranggo ng kaugnayan ng dokumento |
### 🛡️ Katatagan at Seguridad
+8 -8
View File
@@ -873,14 +873,14 @@ npm run electron:build:linux # Linux (.AppImage)
### 🎵 Wielomodalne interfejsy API
| Funkcja | Co to robi |
| ----------------------------- | ------------------------------------------------------ |
| 🖼️ **Generowanie obrazu** | `/v1/images/generations` — 4 dostawców, ponad 9 modeli |
| 📐 **Osadzenia** | `/v1/embeddings` — 6 dostawców, ponad 9 modeli |
| 🎤 **Transkrypcja audio** | `/v1/audio/transcriptions`Kompatybilny z szeptem |
| 🔊 **Zamiana tekstu na mowę** | `/v1/audio/speech`Synteza dźwięku wielu dostawców |
| 🛡️ **Moderacje** | `/v1/moderations` — Kontrola bezpieczeństwa treści |
| 🔀 **Ponowna pozycja** | `/v1/rerank` — Zmiana rankingu trafności dokumentu |
| Funkcja | Co to robi |
| ----------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 🖼️ **Generowanie obrazu** | `/v1/images/generations` — 4 dostawców, ponad 9 modeli |
| 📐 **Osadzenia** | `/v1/embeddings` — 6 dostawców, ponad 9 modeli |
| 🎤 **Transkrypcja audio** | `/v1/audio/transcriptions`7 providers (Deepgram Nova 3, AssemblyAI, Groq Whisper, HuggingFace, ElevenLabs, OpenAI, Azure), auto-language detection, MP4/MP3/WAV support |
| 🔊 **Zamiana tekstu na mowę** | `/v1/audio/speech`10 providers (ElevenLabs, OpenAI, Deepgram, Cartesia, PlayHT, HuggingFace, Nvidia NIM, Inworld, Coqui, Tortoise) |
| 🛡️ **Moderacje** | `/v1/moderations` — Kontrola bezpieczeństwa treści |
| 🔀 **Ponowna pozycja** | `/v1/rerank` — Zmiana rankingu trafności dokumentu |
### 🛡️ Odporność i bezpieczeństwo
+79 -28
View File
@@ -819,24 +819,28 @@ Quando minimizado, o OmniRoute fica na bandeja do sistema com ações rápidas:
## 💰 Preços Resumidos
| Tier | Provedor | Custo | Reset de Cota | Melhor Para |
| ----------------- | ----------------- | ---------------------------- | ----------------- | ----------------------- |
| **💳 ASSINATURA** | Claude Code (Pro) | $20/mês | 5h + semanal | Já é assinante |
| | Codex (Plus/Pro) | $20-200/mês | 5h + semanal | Usuários OpenAI |
| | Gemini CLI | **GRATUITO** | 180K/mês + 1K/dia | Todos! |
| | GitHub Copilot | $10-19/mês | Mensal | Usuários GitHub |
| **🔑 API KEY** | NVIDIA NIM | **GRATUITO** (1000 créditos) | Único | Testes gratuitos |
| | DeepSeek | Por uso | Nenhum | Melhor preço/qualidade |
| | Groq | Tier gratuito + pago | Limitado | Inferência ultra-rápida |
| | xAI (Grok) | Por uso | Nenhum | Modelos Grok |
| | Mistral | Tier gratuito + pago | Limitado | IA Europeia |
| | OpenRouter | Por uso | Nenhum | 100+ modelos |
| **💰 BARATO** | GLM-4.7 | $0.6/1M | Diário 10h | Backup econômico |
| | MiniMax M2.1 | $0.2/1M | Rotativo 5h | Opção mais barata |
| | Kimi K2 | $9/mês fixo | 10M tokens/mês | Custo previsível |
| **🆓 GRATUITO** | iFlow | $0 | Ilimitado | 8 modelos gratuitos |
| | Qwen | $0 | Ilimitado | 3 modelos gratuitos |
| | Kiro | $0 | Ilimitado | Claude gratuito |
| Tier | Provedor | Custo | Reset de Cota | Melhor Para |
| ----------------- | ----------------- | ---------------------------- | ----------------- | ------------------------------ |
| **💳 ASSINATURA** | Claude Code (Pro) | $20/mês | 5h + semanal | Já é assinante |
| | Codex (Plus/Pro) | $20-200/mês | 5h + semanal | Usuários OpenAI |
| | Gemini CLI | **GRATUITO** | 180K/mês + 1K/dia | Todos! |
| | GitHub Copilot | $10-19/mês | Mensal | Usuários GitHub |
| **🔑 API KEY** | NVIDIA NIM | **GRATUITO** (1000 créditos) | Único | Testes gratuitos |
| | DeepSeek | Por uso | Nenhum | Melhor preço/qualidade |
| | Groq | Tier gratuito + pago | Limitado | Inferência ultra-rápida |
| | xAI (Grok) | Por uso | Nenhum | Modelos Grok |
| | Mistral | Tier gratuito + pago | Limitado | IA Europeia |
| | OpenRouter | Por uso | Nenhum | 100+ modelos |
| **💰 BARATO** | GLM-4.7 | $0.6/1M | Diário 10h | Backup econômico |
| | MiniMax M2.1 | $0.2/1M | Rotativo 5h | Opção mais barata |
| | Kimi K2 | $9/mês fixo | 10M tokens/mês | Custo previsível |
| **🆓 GRATUITO** | iFlow | $0 | Ilimitado | 8 modelos gratuitos |
| | Qwen | $0 | Ilimitado | 3 modelos gratuitos |
| | Kiro | $0 | Ilimitado | Claude gratuito |
| | LongCat 🆕 | **$0** (50M tok/dia 🔥) | 1 req/s | Maior cota grátis do mundo |
| | Pollinations 🆕 | **$0** (sem chave API) | 1 req/15s | GPT-5, Claude, DeepSeek, Llama |
| | Cloudflare AI 🆕 | **$0** (10K Neurons/dia) | ~150 resp/dia | 50+ modelos, edge global |
| | Scaleway AI 🆕 | **$0** (1M tokens total) | Limitado por taxa | EU/GDPR, Qwen3 235B, Llama 70B |
**💡 Dica Pro:** Comece com Gemini CLI (180K grátis/mês) + iFlow (ilimitado grátis) = $0 de custo!
@@ -879,16 +883,16 @@ Por que isso é relevante:
### 🎵 APIs Multi-Modal
| 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, Inworld, Cartesia, PlayHT |
| 🎬 **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 |
| 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`7 providers (Deepgram Nova 3, AssemblyAI, Groq Whisper, HuggingFace, ElevenLabs, OpenAI, Azure), auto-language detection, MP4/MP3/WAV support |
| 🔊 **Texto para Fala** | `/v1/audio/speech`10 providers (ElevenLabs, OpenAI, Deepgram, Cartesia, PlayHT, HuggingFace, Nvidia NIM, Inworld, Coqui, Tortoise) |
| 🎬 **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
@@ -1223,6 +1227,53 @@ Modelos:
kr/claude-haiku-4.5
```
### LongCat AI (GRATUITO 50M tokens/dia!) 🆕
1. Cadastre-se: [longcat.chat](https://longcat.chat) com e-mail ou telefone
2. Gere uma chave de API gratuita
3. Dashboard → Adicionar Provedor → LongCat
**Modelos:**
- `lc/LongCat-Flash-Lite`**50M tokens/dia** 💥 (maior cota gratuita do mundo!)
- `lc/LongCat-Flash-Chat` — 500K tokens/dia
- `lc/LongCat-Flash-Thinking` — 500K tokens/dia (raciocínio)
> 100% gratuito durante o beta público. Reset diário à meia-noite UTC.
### Pollinations AI (SEM CHAVE NECESSÁRIA!) 🆕
1. Adicione o provedor Pollinations no Dashboard
2. Deixe o campo de chave API vazio (ou coloque qualquer string)
3. Comece a usar imediatamente!
**Modelos via `pol/`:** `openai` (GPT-5), `claude`, `gemini`, `deepseek`, `llama` (Llama 4)
> Sem cadastro, sem chave, sem cartão de crédito. 1 req/15s ilimitado.
### Cloudflare Workers AI (GRATUITO 10K Neurons/dia!) 🆕
1. Cadastre-se: [dash.cloudflare.com](https://dash.cloudflare.com)
2. Gere um API Token em Profile → API Tokens
3. Copie seu Account ID (coluna direita do dashboard)
4. Dashboard → Adicionar Provedor → Cloudflare AI
- API Key: seu token
- Account ID: seu account ID
**Modelos via `cf/`:** `@cf/meta/llama-3.3-70b-instruct`, `@cf/google/gemma-3-12b-it`, 50+ mais
> 10K Neurons/dia ≈ 150 respostas de LLM ou 500s de transcrição Whisper gratuita!
### Scaleway AI (1M tokens gratuitos!) 🆕
1. Cadastre-se: [console.scaleway.com](https://console.scaleway.com)
2. Gere uma chave de API IAM
3. Dashboard → Adicionar Provedor → Scaleway
**Modelos via `scw/`:** `qwen3-235b-a22b-instruct-2507` (Qwen3 235B!), `llama-3.1-70b-instruct`
> 1M tokens gratuitos para novas contas. Dados processados na 🇫🇷 França (EU/GDPR).
</details>
<details>
+8 -8
View File
@@ -874,14 +874,14 @@ npm run electron:build:linux # Linux (.AppImage)
### 🎵 APIs multimodais
| Recurso | O que faz |
| --------------------------------- | ----------------------------------------------------------- |
| 🖼️ **Geração de imagens** | `/v1/images/generations` — 4 provedores, mais de 9 modelos |
| 📐 **Incorporações** | `/v1/embeddings` — 6 provedores, mais de 9 modelos |
| 🎤 **Transcrição de áudio** | `/v1/audio/transcriptions`Compatível com sussurro |
| 🔊 **Conversão de texto em fala** | `/v1/audio/speech`Síntese de áudio multiprovedor |
| 🛡️ **Moderações** | `/v1/moderations` — Verificações de segurança de conteúdo |
| 🔀 **Reclassificação** | `/v1/rerank` — Reclassificação da relevância dos documentos |
| Recurso | O que faz |
| --------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 🖼️ **Geração de imagens** | `/v1/images/generations` — 4 provedores, mais de 9 modelos |
| 📐 **Incorporações** | `/v1/embeddings` — 6 provedores, mais de 9 modelos |
| 🎤 **Transcrição de áudio** | `/v1/audio/transcriptions`7 providers (Deepgram Nova 3, AssemblyAI, Groq Whisper, HuggingFace, ElevenLabs, OpenAI, Azure), auto-language detection, MP4/MP3/WAV support |
| 🔊 **Conversão de texto em fala** | `/v1/audio/speech`10 providers (ElevenLabs, OpenAI, Deepgram, Cartesia, PlayHT, HuggingFace, Nvidia NIM, Inworld, Coqui, Tortoise) |
| 🛡️ **Moderações** | `/v1/moderations` — Verificações de segurança de conteúdo |
| 🔀 **Reclassificação** | `/v1/rerank` — Reclassificação da relevância dos documentos |
### 🛡️ Resiliência e segurança
+8 -8
View File
@@ -875,14 +875,14 @@ npm run electron:build:linux # Linux (.AppImage)
### 🎵 API-uri multimodale
| Caracteristica | Ce face |
| ------------------------- | ---------------------------------------------------------- |
| 🖼️ **Generarea imaginii** | `/v1/images/generations` — 4 furnizori, peste 9 modele |
| 📐 **Inglobări** | `/v1/embeddings` — 6 furnizori, peste 9 modele |
| 🎤 **Transcriere audio** | `/v1/audio/transcriptions`Compatibil cu Whisper |
| 🔊 **Text-to-speech** | `/v1/audio/speech`Sinteză audio cu mai mulți furnizori |
| 🛡️ **Moderații** | `/v1/moderations` — Verificări de siguranță a conținutului |
| 🔀 **Reclasificare** | `/v1/rerank` — Reclasificarea relevanței documentului |
| Caracteristica | Ce face |
| ------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 🖼️ **Generarea imaginii** | `/v1/images/generations` — 4 furnizori, peste 9 modele |
| 📐 **Inglobări** | `/v1/embeddings` — 6 furnizori, peste 9 modele |
| 🎤 **Transcriere audio** | `/v1/audio/transcriptions`7 providers (Deepgram Nova 3, AssemblyAI, Groq Whisper, HuggingFace, ElevenLabs, OpenAI, Azure), auto-language detection, MP4/MP3/WAV support |
| 🔊 **Text-to-speech** | `/v1/audio/speech`10 providers (ElevenLabs, OpenAI, Deepgram, Cartesia, PlayHT, HuggingFace, Nvidia NIM, Inworld, Coqui, Tortoise) |
| 🛡️ **Moderații** | `/v1/moderations` — Verificări de siguranță a conținutului |
| 🔀 **Reclasificare** | `/v1/rerank` — Reclasificarea relevanței documentului |
### 🛡️ Reziliență și securitate
+8 -8
View File
@@ -873,14 +873,14 @@ npm run electron:build:linux # Linux (.AppImage)
### 🎵 Мультимодальные API
| Функция | Что делает |
| ---------------------------- | --------------------------------------------------- |
| 🖼️ **Генерация изображений** | `/v1/images/generations` — 4 провайдера, 9+ моделей |
| 📐 **Embeddings** | `/v1/embeddings` — 6 провайдеров, 9+ моделей |
| 🎤 **Транскрипция аудио** | `/v1/audio/transcriptions`Совместимо с Whisper |
| 🔊 **Текст в речь** | `/v1/audio/speech`Мульти-провайдерный синтез |
| 🛡️ **Модерация** | `/v1/moderations` — Проверки безопасности контента |
| 🔀 **Reranking** | `/v1/rerank` — Переранжирование релевантности |
| Функция | Что делает |
| ---------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 🖼️ **Генерация изображений** | `/v1/images/generations` — 4 провайдера, 9+ моделей |
| 📐 **Embeddings** | `/v1/embeddings` — 6 провайдеров, 9+ моделей |
| 🎤 **Транскрипция аудио** | `/v1/audio/transcriptions`7 providers (Deepgram Nova 3, AssemblyAI, Groq Whisper, HuggingFace, ElevenLabs, OpenAI, Azure), auto-language detection, MP4/MP3/WAV support |
| 🔊 **Текст в речь** | `/v1/audio/speech`10 providers (ElevenLabs, OpenAI, Deepgram, Cartesia, PlayHT, HuggingFace, Nvidia NIM, Inworld, Coqui, Tortoise) |
| 🛡️ **Модерация** | `/v1/moderations` — Проверки безопасности контента |
| 🔀 **Reranking** | `/v1/rerank` — Переранжирование релевантности |
### 🛡️ Устойчивость и безопасность
+8 -8
View File
@@ -877,14 +877,14 @@ npm run electron:build:linux # Linux (.AppImage)
### 🎵 Multimodálne API
| Funkcia | Čo to robí |
| --------------------------- | ---------------------------------------------------------------- |
| 🖼️ **Generovanie obrázkov** | `/v1/images/generations` — 4 poskytovatelia, 9+ modelov |
| 📐 **Vloženie** | `/v1/embeddings` — 6 poskytovateľov, 9+ modelov |
| 🎤 **Prepis zvuku** | `/v1/audio/transcriptions`Kompatibilné so šepotom |
| 🔊 **Prevod textu na reč** | `/v1/audio/speech`Zvuková syntéza od viacerých poskytovateľov |
| 🛡️ **Moderovania** | `/v1/moderations` — Kontroly bezpečnosti obsahu |
| 🔀 **Reranking** | `/v1/rerank` — Zmena poradia relevantnosti dokumentu |
| Funkcia | Čo to robí |
| --------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 🖼️ **Generovanie obrázkov** | `/v1/images/generations` — 4 poskytovatelia, 9+ modelov |
| 📐 **Vloženie** | `/v1/embeddings` — 6 poskytovateľov, 9+ modelov |
| 🎤 **Prepis zvuku** | `/v1/audio/transcriptions`7 providers (Deepgram Nova 3, AssemblyAI, Groq Whisper, HuggingFace, ElevenLabs, OpenAI, Azure), auto-language detection, MP4/MP3/WAV support |
| 🔊 **Prevod textu na reč** | `/v1/audio/speech`10 providers (ElevenLabs, OpenAI, Deepgram, Cartesia, PlayHT, HuggingFace, Nvidia NIM, Inworld, Coqui, Tortoise) |
| 🛡️ **Moderovania** | `/v1/moderations` — Kontroly bezpečnosti obsahu |
| 🔀 **Reranking** | `/v1/rerank` — Zmena poradia relevantnosti dokumentu |
### 🛡️ Odolnosť a bezpečnosť
+8 -8
View File
@@ -873,14 +873,14 @@ npm run electron:build:linux # Linux (.AppImage)
### 🎵 Multimodala API:er
| Funktion | Vad det gör |
| ------------------------ | ------------------------------------------------------ |
| 🖼️ **Bildgenerering** | `/v1/images/generations` — 4 leverantörer, 9+ modeller |
| 📐 **Inbäddningar** | `/v1/embeddings` — 6 leverantörer, 9+ modeller |
| 🎤 **Ljudtranskription** | `/v1/audio/transcriptions`Whisper-kompatibel |
| 🔊 **Text-till-tal** | `/v1/audio/speech`Ljudsyntes med flera leverantörer |
| 🛡️ **Moderationer** | `/v1/moderations` — Innehållssäkerhetskontroller |
| 🔀 **Omrankning** | `/v1/rerank` — Omrankning av dokumentrelevans |
| Funktion | Vad det gör |
| ------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 🖼️ **Bildgenerering** | `/v1/images/generations` — 4 leverantörer, 9+ modeller |
| 📐 **Inbäddningar** | `/v1/embeddings` — 6 leverantörer, 9+ modeller |
| 🎤 **Ljudtranskription** | `/v1/audio/transcriptions`7 providers (Deepgram Nova 3, AssemblyAI, Groq Whisper, HuggingFace, ElevenLabs, OpenAI, Azure), auto-language detection, MP4/MP3/WAV support |
| 🔊 **Text-till-tal** | `/v1/audio/speech`10 providers (ElevenLabs, OpenAI, Deepgram, Cartesia, PlayHT, HuggingFace, Nvidia NIM, Inworld, Coqui, Tortoise) |
| 🛡️ **Moderationer** | `/v1/moderations` — Innehållssäkerhetskontroller |
| 🔀 **Omrankning** | `/v1/rerank` — Omrankning av dokumentrelevans |
### 🛡️ Motståndskraft och säkerhet
+8 -8
View File
@@ -874,14 +874,14 @@ npm run electron:build:linux # Linux (.AppImage)
### 🎵 Multi-Modal API
| คุณสมบัติ | มันทำอะไร |
| ----------------------- | ------------------------------------------------------------- |
| 🖼️ **การสร้างภาพ** | `/v1/images/generations` — ผู้ให้บริการ 4 ราย รุ่น 9+ |
| 📐 **การฝัง** | `/v1/embeddings` — ผู้ให้บริการ 6 ราย รุ่น 9+ |
| 🎶 **การถอดเสียง** | `/v1/audio/transcriptions` — รองรับการกระซิบ |
| 🔊 **ข้อความเป็นคำพูด** | `/v1/audio/speech`การสังเคราะห์เสียงจากผู้ให้บริการหลายราย |
| 🛡️ **การกลั่นกรอง** | `/v1/moderations` — การตรวจสอบความปลอดภัยของเนื้อหา |
| 🔀 **จัดอันดับ** | `/v1/rerank` — การจัดอันดับความเกี่ยวข้องของเอกสาร |
| คุณสมบัติ | มันทำอะไร |
| ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
| 🖼️ **การสร้างภาพ** | `/v1/images/generations` — ผู้ให้บริการ 4 ราย รุ่น 9+ |
| 📐 **การฝัง** | `/v1/embeddings` — ผู้ให้บริการ 6 ราย รุ่น 9+ |
| 🎶 **การถอดเสียง** | `/v1/audio/transcriptions` — รองรับการกระซิบ |
| 🔊 **ข้อความเป็นคำพูด** | `/v1/audio/speech`10 providers (ElevenLabs, OpenAI, Deepgram, Cartesia, PlayHT, HuggingFace, Nvidia NIM, Inworld, Coqui, Tortoise) |
| 🛡️ **การกลั่นกรอง** | `/v1/moderations` — การตรวจสอบความปลอดภัยของเนื้อหา |
| 🔀 **จัดอันดับ** | `/v1/rerank` — การจัดอันดับความเกี่ยวข้องของเอกสาร |
### 🛡️ ความยืดหยุ่นและความปลอดภัย
+8 -8
View File
@@ -878,14 +878,14 @@ npm run electron:build:linux # Linux (.AppImage)
### 🎵 Мультимодальні API
| Особливість | Що він робить |
| ---------------------------------- | ----------------------------------------------------- |
| 🖼️ **Створення зображень** | `/v1/images/generations` — 4 провайдери, 9+ моделей |
| 📐 **Вбудовування** | `/v1/embeddings` — 6 провайдерів, 9+ моделей |
| 🎤 **Транскрипція аудіо** | `/v1/audio/transcriptions`сумісний із Whisper |
| 🔊 **Створення тексту в мовлення** | `/v1/audio/speech`Багатопровайдерний аудіосинтез |
| 🛡️ **Модерації** | `/v1/moderations` — Перевірка безпеки вмісту |
| 🔀 **Переранжування** | `/v1/rerank` — Переранжування релевантності документа |
| Особливість | Що він робить |
| ---------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 🖼️ **Створення зображень** | `/v1/images/generations` — 4 провайдери, 9+ моделей |
| 📐 **Вбудовування** | `/v1/embeddings` — 6 провайдерів, 9+ моделей |
| 🎤 **Транскрипція аудіо** | `/v1/audio/transcriptions`7 providers (Deepgram Nova 3, AssemblyAI, Groq Whisper, HuggingFace, ElevenLabs, OpenAI, Azure), auto-language detection, MP4/MP3/WAV support |
| 🔊 **Створення тексту в мовлення** | `/v1/audio/speech`10 providers (ElevenLabs, OpenAI, Deepgram, Cartesia, PlayHT, HuggingFace, Nvidia NIM, Inworld, Coqui, Tortoise) |
| 🛡️ **Модерації** | `/v1/moderations` — Перевірка безпеки вмісту |
| 🔀 **Переранжування** | `/v1/rerank` — Переранжування релевантності документа |
### 🛡️ Стійкість і безпека
+8 -8
View File
@@ -874,14 +874,14 @@ npm run electron:build:linux # Linux (.AppImage)
### 🎵 API đa phương thức
| Tính năng | Nó làm gì |
| ------------------------------------- | ------------------------------------------------------------ |
| 🖼️ **Tạo hình ảnh** | `/v1/images/generations` — 4 nhà cung cấp, hơn 9 mô hình |
| 📐 **Nhúng** | `/v1/embeddings` — 6 nhà cung cấp, hơn 9 mô hình |
| 🎤 **Phiên âm âm thanh** | `/v1/audio/transcriptions`Tương thích với lời thì thầm |
| 🔊 **Chuyển văn bản thành giọng nói** | `/v1/audio/speech`Tổng hợp âm thanh từ nhiều nhà cung cấp |
| 🛡️ **Kiểm duyệt** | `/v1/moderations` — Kiểm tra an toàn nội dung |
| 🔀 **Sắp xếp lại** | `/v1/rerank` — Sắp xếp lại mức độ liên quan của tài liệu |
| Tính năng | Nó làm gì |
| ------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 🖼️ **Tạo hình ảnh** | `/v1/images/generations` — 4 nhà cung cấp, hơn 9 mô hình |
| 📐 **Nhúng** | `/v1/embeddings` — 6 nhà cung cấp, hơn 9 mô hình |
| 🎤 **Phiên âm âm thanh** | `/v1/audio/transcriptions`7 providers (Deepgram Nova 3, AssemblyAI, Groq Whisper, HuggingFace, ElevenLabs, OpenAI, Azure), auto-language detection, MP4/MP3/WAV support |
| 🔊 **Chuyển văn bản thành giọng nói** | `/v1/audio/speech`10 providers (ElevenLabs, OpenAI, Deepgram, Cartesia, PlayHT, HuggingFace, Nvidia NIM, Inworld, Coqui, Tortoise) |
| 🛡️ **Kiểm duyệt** | `/v1/moderations` — Kiểm tra an toàn nội dung |
| 🔀 **Sắp xếp lại** | `/v1/rerank` — Sắp xếp lại mức độ liên quan của tài liệu |
### 🛡️ Khả năng phục hồi và bảo mật
+8 -8
View File
@@ -873,14 +873,14 @@ npm run electron:build:linux # Linux (.AppImage)
### 🎵 多模态 API
| 功能 | 功能描述 |
| ----------------- | ---------------------------------------------- |
| 🖼️ **图像生成** | `/v1/images/generations` — 4 个提供商,9+ 模型 |
| 📐 **Embeddings** | `/v1/embeddings` — 6 个提供商,9+ 模型 |
| 🎤 **音频转录** | `/v1/audio/transcriptions`Whisper 兼容 |
| 🔊 **文字转语音** | `/v1/audio/speech`多提供商音频合成 |
| 🛡️ **内容审核** | `/v1/moderations` — 内容安全检查 |
| 🔀 **重排序** | `/v1/rerank` — 文档相关性重排序 |
| 功能 | 功能描述 |
| ----------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 🖼️ **图像生成** | `/v1/images/generations` — 4 个提供商,9+ 模型 |
| 📐 **Embeddings** | `/v1/embeddings` — 6 个提供商,9+ 模型 |
| 🎤 **音频转录** | `/v1/audio/transcriptions`7 providers (Deepgram Nova 3, AssemblyAI, Groq Whisper, HuggingFace, ElevenLabs, OpenAI, Azure), auto-language detection, MP4/MP3/WAV support |
| 🔊 **文字转语音** | `/v1/audio/speech`10 providers (ElevenLabs, OpenAI, Deepgram, Cartesia, PlayHT, HuggingFace, Nvidia NIM, Inworld, Coqui, Tortoise) |
| 🛡️ **内容审核** | `/v1/moderations` — 内容安全检查 |
| 🔀 **重排序** | `/v1/rerank` — 文档相关性重排序 |
### 🛡️ 弹性与安全
+1 -1
View File
@@ -1,7 +1,7 @@
openapi: 3.1.0
info:
title: OmniRoute API
version: 2.8.9
version: 3.0.0-rc.2
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,
+1
View File
@@ -17,6 +17,7 @@ export interface EmbeddingProvider {
}
export interface EmbeddingProviderNodeRow {
id?: string;
prefix: string;
name: string;
baseUrl: string;
+174
View File
@@ -47,6 +47,8 @@ export interface RegistryEntry {
executor: string;
baseUrl?: string;
baseUrls?: string[];
/** Override base URL used only for API key validation (e.g., opencode-go validates on zen/v1) */
testKeyBaseUrl?: string;
responsesBaseUrl?: string;
urlSuffix?: string;
urlBuilder?: (base: string, model: string, stream: boolean) => string;
@@ -495,6 +497,41 @@ export const REGISTRY: Record<string, RegistryEntry> = {
],
},
"opencode-go": {
id: "opencode-go",
alias: "opencode-go",
format: "openai",
executor: "opencode",
baseUrl: "https://opencode.ai/zen/go/v1",
// (#532) Key validation must hit the main zen endpoint (same key works for both tiers)
testKeyBaseUrl: "https://opencode.ai/zen/v1",
authType: "apikey",
authHeader: "Authorization",
authPrefix: "Bearer",
models: [
{ id: "glm-5", name: "GLM-5" },
{ id: "kimi-k2.5", name: "Kimi K2.5" },
{ id: "minimax-m2.7", name: "MiniMax M2.7", targetFormat: "claude" },
{ id: "minimax-m2.5", name: "MiniMax M2.5", targetFormat: "claude" },
],
},
"opencode-zen": {
id: "opencode-zen",
alias: "opencode-zen",
format: "openai",
executor: "opencode",
baseUrl: "https://opencode.ai/zen/v1",
authType: "apikey",
authHeader: "Authorization",
authPrefix: "Bearer",
models: [
{ id: "minimax-m2.5-free", name: "MiniMax M2.5 Free" },
{ id: "big-pickle", name: "Big Pickle" },
{ id: "gpt-5-nano", name: "GPT 5 Nano" },
],
},
openrouter: {
id: "openrouter",
alias: "openrouter",
@@ -883,6 +920,12 @@ export const REGISTRY: Record<string, RegistryEntry> = {
authType: "apikey",
authHeader: "bearer",
models: [
{ id: "meta-llama/Llama-3.3-70B-Instruct-Turbo-Free", name: "Llama 3.3 70B Turbo (🆓 Free)" },
{ id: "meta-llama/Llama-Vision-Free", name: "Llama Vision (🆓 Free)" },
{
id: "deepseek-ai/DeepSeek-R1-Distill-Llama-70B-Free",
name: "DeepSeek R1 Distill 70B (🆓 Free)",
},
{ id: "meta-llama/Llama-3.3-70B-Instruct-Turbo", name: "Llama 3.3 70B Turbo" },
{ id: "deepseek-ai/DeepSeek-R1", name: "DeepSeek R1" },
{ id: "Qwen/Qwen3-235B-A22B", name: "Qwen3 235B" },
@@ -1125,6 +1168,137 @@ export const REGISTRY: Record<string, RegistryEntry> = {
{ id: "claude-sonnet-4-5@20251101", name: "Claude Sonnet 4.5 (Vertex)" },
],
},
alibaba: {
id: "alibaba",
alias: "ali",
format: "openai",
executor: "default",
// DashScope international OpenAI-compatible endpoint.
// China users should set providerSpecificData.baseUrl to:
// https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions
baseUrl: "https://dashscope-intl.aliyuncs.com/compatible-mode/v1/chat/completions",
modelsUrl: "https://dashscope-intl.aliyuncs.com/compatible-mode/v1/models",
authType: "apikey",
authHeader: "bearer",
models: [
{ id: "qwen-max", name: "Qwen Max" },
{ id: "qwen-max-2025-01-25", name: "Qwen Max (2025-01-25)" },
{ id: "qwen-plus", name: "Qwen Plus" },
{ id: "qwen-plus-2025-07-14", name: "Qwen Plus (2025-07-14)" },
{ id: "qwen-turbo", name: "Qwen Turbo" },
{ id: "qwen-turbo-2025-11-01", name: "Qwen Turbo (2025-11-01)" },
{ id: "qwen3-coder-plus", name: "Qwen3 Coder Plus" },
{ id: "qwen3-coder-flash", name: "Qwen3 Coder Flash" },
{ id: "qwq-plus", name: "QwQ Plus (Reasoning)" },
{ id: "qwq-32b", name: "QwQ 32B" },
{ id: "qwen3-32b", name: "Qwen3 32B" },
{ id: "qwen3-235b-a22b", name: "Qwen3 235B A22B" },
],
passthroughModels: true,
},
// ── New Free Providers (2026) ─────────────────────────────────────────────
longcat: {
id: "longcat",
alias: "lc",
format: "openai",
executor: "default",
// (#536) Correct OpenAI-compatible base URL — was longcat.chat/api/v1/chat/completions
// which is the chat endpoint directly, not the base. Key validation and routing must
// use https://api.longcat.chat/openai which resolves /v1/models and /v1/chat/completions
baseUrl: "https://api.longcat.chat/openai",
authType: "apikey",
authHeader: "Authorization",
authPrefix: "Bearer",
// Free tier: 50M tokens/day (Flash-Lite) + 500K/day (Chat/Thinking) — 100% free while public beta
models: [
{ id: "LongCat-Flash-Lite", name: "LongCat Flash-Lite (50M tok/day 🆓)" },
{ id: "LongCat-Flash-Chat", name: "LongCat Flash-Chat (500K tok/day 🆓)" },
{ id: "LongCat-Flash-Thinking", name: "LongCat Flash-Thinking (500K tok/day 🆓)" },
{ id: "LongCat-Flash-Thinking-2601", name: "LongCat Flash-Thinking-2601 (🆓)" },
{ id: "LongCat-Flash-Omni-2603", name: "LongCat Flash-Omni-2603 (🆓)" },
],
},
pollinations: {
id: "pollinations",
alias: "pol",
format: "openai",
executor: "pollinations",
// No API key required for basic use. Proxy to GPT-5, Claude, Gemini, DeepSeek, Llama 4.
baseUrl: "https://text.pollinations.ai/openai/chat/completions",
authType: "apikey", // Optional — works without one too
authHeader: "bearer",
models: [
{ id: "openai", name: "GPT-5 via Pollinations (🆓)" },
{ id: "claude", name: "Claude via Pollinations (🆓)" },
{ id: "gemini", name: "Gemini via Pollinations (🆓)" },
{ id: "deepseek", name: "DeepSeek V3 via Pollinations (🆓)" },
{ id: "llama", name: "Llama 4 via Pollinations (🆓)" },
{ id: "mistral", name: "Mistral via Pollinations (🆓)" },
],
},
"cloudflare-ai": {
id: "cloudflare-ai",
alias: "cf",
format: "openai",
executor: "cloudflare-ai",
// URL is dynamic: uses accountId from credentials. The executor builds it.
baseUrl: "https://api.cloudflare.com/client/v4/accounts",
authType: "apikey",
authHeader: "bearer",
// 10K Neurons/day free: ~150 LLM responses or 500s Whisper audio — global edge
models: [
{ id: "@cf/meta/llama-3.3-70b-instruct", name: "Llama 3.3 70B (🆓 ~150 resp/day)" },
{ id: "@cf/meta/llama-3.1-8b-instruct", name: "Llama 3.1 8B (🆓)" },
{ id: "@cf/google/gemma-3-12b-it", name: "Gemma 3 12B (🆓)" },
{ id: "@cf/mistral/mistral-7b-instruct-v0.2-lora", name: "Mistral 7B (🆓)" },
{ id: "@cf/qwen/qwen2.5-coder-15b-instruct", name: "Qwen 2.5 Coder 15B (🆓)" },
{ id: "@cf/deepseek-ai/deepseek-r1-distill-qwen-32b", name: "DeepSeek R1 Distill 32B (🆓)" },
],
},
scaleway: {
id: "scaleway",
alias: "scw",
format: "openai",
executor: "default",
baseUrl: "https://api.scaleway.ai/v1/chat/completions",
authType: "apikey",
authHeader: "bearer",
// 1M tokens free for new accounts — EU/GDPR (Paris), no credit card needed under limit
models: [
{ id: "qwen3-235b-a22b-instruct-2507", name: "Qwen3 235B A22B (1M free tok 🆓)" },
{ id: "llama-3.1-70b-instruct", name: "Llama 3.1 70B (🆓 EU)" },
{ id: "llama-3.1-8b-instruct", name: "Llama 3.1 8B (🆓 EU)" },
{ id: "mistral-small-3.2-24b-instruct-2506", name: "Mistral Small 3.2 (🆓 EU)" },
{ id: "deepseek-v3-0324", name: "DeepSeek V3 (🆓 EU)" },
{ id: "gpt-oss-120b", name: "GPT-OSS 120B (🆓 EU)" },
],
},
aimlapi: {
id: "aimlapi",
alias: "aiml",
format: "openai",
executor: "default",
baseUrl: "https://api.aimlapi.com/v1/chat/completions",
authType: "apikey",
authHeader: "bearer",
// $0.025/day free credits — 200+ models via single aggregator endpoint
models: [
{ id: "gpt-4o", name: "GPT-4o (via AI/ML API)" },
{ id: "claude-3-5-sonnet-20241022", name: "Claude 3.5 Sonnet (via AI/ML API)" },
{ id: "gemini-1.5-pro", name: "Gemini 1.5 Pro (via AI/ML API)" },
{ id: "meta-llama/Meta-Llama-3.1-70B-Instruct-Turbo", name: "Llama 3.1 70B (via AI/ML API)" },
{ id: "deepseek-chat", name: "DeepSeek Chat (via AI/ML API)" },
{ id: "mistral-large-latest", name: "Mistral Large (via AI/ML API)" },
],
passthroughModels: true,
},
};
// ── Generator Functions ───────────────────────────────────────────────────
+20 -4
View File
@@ -44,12 +44,28 @@ export class AntigravityExecutor extends BaseExecutor {
// stale/wrong client-side values causing 404/403 from Cloud Code endpoints.
// Opt-in escape hatch: set OMNIROUTE_ALLOW_BODY_PROJECT_OVERRIDE=1.
const projectId =
allowBodyProjectOverride && bodyProjectId ? bodyProjectId : credentialsProjectId || bodyProjectId;
allowBodyProjectOverride && bodyProjectId
? bodyProjectId
: credentialsProjectId || bodyProjectId;
if (!projectId) {
throw new Error(
"Missing Google projectId for Antigravity account. Please reconnect OAuth so OmniRoute can fetch your real Cloud Code project (loadCodeAssist)."
);
// (#489) Return a structured error instead of throwing — gives the client a clear signal
// to show a "Reconnect OAuth" prompt rather than an opaque "Internal Server Error".
const errorMsg =
"Missing Google projectId for Antigravity account. Please reconnect OAuth in Providers → Antigravity so OmniRoute can fetch your Cloud Code project.";
const errorBody = {
error: {
message: errorMsg,
type: "oauth_missing_project_id",
code: "missing_project_id",
},
};
const resp = new Response(JSON.stringify(errorBody), {
status: 422,
headers: { "Content-Type": "application/json" },
});
// Returning a Response object signals the executor to stop and forward it
return resp as unknown as never;
}
// Fix contents for Claude models via Antigravity
+17 -1
View File
@@ -2,6 +2,20 @@ import { HTTP_STATUS, FETCH_TIMEOUT_MS } from "../config/constants.ts";
import { applyFingerprint, isCliCompatEnabled } from "../config/cliFingerprints.ts";
import { getRotatingApiKey } from "../services/apiKeyRotator.ts";
/**
* Sanitizes a custom API path to prevent path traversal attacks.
* Valid paths must start with '/', contain no '..' segments,
* no null bytes, and be reasonable in length.
*/
function sanitizePath(path: string): boolean {
if (typeof path !== "string") return false;
if (!path.startsWith("/")) return false;
if (path.includes("\0")) return false; // null byte
if (path.includes("..")) return false; // path traversal
if (path.length > 512) return false; // sanity limit
return true;
}
type JsonRecord = Record<string, unknown>;
export type ProviderConfig = {
@@ -103,7 +117,9 @@ export class BaseExecutor {
const psd = credentials?.providerSpecificData;
const baseUrl = typeof psd?.baseUrl === "string" ? psd.baseUrl : "https://api.openai.com/v1";
const normalized = baseUrl.replace(/\/$/, "");
const customPath = typeof psd?.chatPath === "string" && psd.chatPath ? psd.chatPath : null;
// Sanitize custom path: must start with '/', no path traversal, no null bytes
const rawPath = typeof psd?.chatPath === "string" && psd.chatPath ? psd.chatPath : null;
const customPath = rawPath && sanitizePath(rawPath) ? rawPath : null;
if (customPath) return `${normalized}${customPath}`;
const path = this.provider.includes("responses") ? "/responses" : "/chat/completions";
return `${normalized}${path}`;
+59
View File
@@ -0,0 +1,59 @@
import { BaseExecutor } from "./base.ts";
import { PROVIDERS } from "../config/constants.ts";
/**
* CloudflareAIExecutor handles dynamic URL construction with accountId.
* Cloudflare Workers AI uses the authenticated user's account ID in the URL.
*
* URL pattern: https://api.cloudflare.com/client/v4/accounts/{accountId}/ai/v1/chat/completions
* Auth: Bearer <API Token>
* Docs: https://developers.cloudflare.com/workers-ai/
*
* Free tier: 10,000 Neurons/day = ~150 LLM responses or 500s Whisper audio
* API Token: dash.cloudflare.com/profile/api-tokens
* Account ID: right sidebar of dash.cloudflare.com
*/
export class CloudflareAIExecutor extends BaseExecutor {
constructor() {
super("cloudflare-ai", PROVIDERS["cloudflare-ai"] || { format: "openai" });
}
buildUrl(_model: string, _stream: boolean, _urlIndex = 0, credentials: any = null): string {
// Account ID can be stored in providerSpecificData or at top level credentials
const accountId =
credentials?.providerSpecificData?.accountId ||
credentials?.accountId ||
process.env.CLOUDFLARE_ACCOUNT_ID;
if (!accountId) {
throw new Error(
"Cloudflare Workers AI requires an Account ID. " +
"Add it in provider settings under 'Account ID'. " +
"Find it at: https://dash.cloudflare.com (right sidebar)."
);
}
return `https://api.cloudflare.com/client/v4/accounts/${accountId}/ai/v1/chat/completions`;
}
buildHeaders(credentials: any, stream = true): Record<string, string> {
const headers: Record<string, string> = {
"Content-Type": "application/json",
Authorization: `Bearer ${credentials.apiKey || credentials.accessToken}`,
};
if (stream) {
headers["Accept"] = "text/event-stream";
}
return headers;
}
transformRequest(_model: string, body: any, _stream: boolean, _credentials: any): any {
// Cloudflare uses full model paths like @cf/meta/llama-3.3-70b-instruct
// No transformation needed — user sends the full Cloudflare model path.
return body;
}
}
export default CloudflareAIExecutor;
+6 -10
View File
@@ -80,18 +80,14 @@ export class DefaultExecutor extends BaseExecutor {
}
/**
* For compatible providers, ensure the model name sent upstream
* is the clean model name without internal routing prefixes.
* e.g. "openapi-chat-anti/claude-opus-4-6-thinking" "claude-opus-4-6-thinking"
* For compatible providers, the model name is already clean by the time
* it reaches the executor (chatCore sets body.model = modelInfo.model,
* which is the parsed model ID without internal routing prefixes).
*
* Models may legitimately contain "/" as part of their ID (e.g. "zai-org/GLM-5-FP8",
* "org/model-name") we must NOT strip path segments. (Fix #493)
*/
transformRequest(model, body, stream, credentials) {
if (
this.provider?.startsWith?.("openai-compatible-") ||
this.provider?.startsWith?.("anthropic-compatible-")
) {
const cleanModel = model.includes("/") ? model.split("/").slice(1).join("/") : model;
return { ...body, model: cleanModel };
}
return body;
}
+12
View File
@@ -6,6 +6,9 @@ import { KiroExecutor } from "./kiro.ts";
import { CodexExecutor } from "./codex.ts";
import { CursorExecutor } from "./cursor.ts";
import { DefaultExecutor } from "./default.ts";
import { PollinationsExecutor } from "./pollinations.ts";
import { CloudflareAIExecutor } from "./cloudflare-ai.ts";
import { OpencodeExecutor } from "./opencode.ts";
const executors = {
antigravity: new AntigravityExecutor(),
@@ -16,6 +19,12 @@ const executors = {
codex: new CodexExecutor(),
cursor: new CursorExecutor(),
cu: new CursorExecutor(), // Alias for cursor
pollinations: new PollinationsExecutor(),
pol: new PollinationsExecutor(), // Alias
"cloudflare-ai": new CloudflareAIExecutor(),
cf: new CloudflareAIExecutor(), // Alias
"opencode-zen": new OpencodeExecutor("opencode-zen"),
"opencode-go": new OpencodeExecutor("opencode-go"),
};
const defaultCache = new Map();
@@ -39,3 +48,6 @@ export { KiroExecutor } from "./kiro.ts";
export { CodexExecutor } from "./codex.ts";
export { CursorExecutor } from "./cursor.ts";
export { DefaultExecutor } from "./default.ts";
export { PollinationsExecutor } from "./pollinations.ts";
export { CloudflareAIExecutor } from "./cloudflare-ai.ts";
export { OpencodeExecutor } from "./opencode.ts";
+61
View File
@@ -0,0 +1,61 @@
import { BaseExecutor, type ExecuteInput, type ProviderCredentials } from "./base.ts";
import { PROVIDERS } from "../config/constants.ts";
import { getModelTargetFormat } from "../config/providerModels.ts";
export class OpencodeExecutor extends BaseExecutor {
_requestFormat: string | null = null;
constructor(provider: string) {
super(provider, PROVIDERS[provider] || PROVIDERS.openai);
}
async execute(input: ExecuteInput) {
this._requestFormat = getModelTargetFormat(this.provider, input.model) || "openai";
try {
return await super.execute(input);
} finally {
this._requestFormat = null;
}
}
buildUrl(
model: string,
stream: boolean,
urlIndex = 0,
credentials: ProviderCredentials | null = null
) {
void urlIndex;
void credentials;
const base = this.config.baseUrl;
switch (this._requestFormat) {
case "claude":
return `${base}/messages`;
case "openai-responses":
return `${base}/responses`;
case "gemini":
return `${base}/models/${model}:${stream ? "streamGenerateContent?alt=sse" : "generateContent"}`;
default:
return `${base}/chat/completions`;
}
}
buildHeaders(credentials: ProviderCredentials | null, stream = true) {
const headers: Record<string, string> = { "Content-Type": "application/json" };
const key = credentials?.apiKey || credentials?.accessToken;
if (key) {
headers["Authorization"] = `Bearer ${key}`;
}
if (this._requestFormat === "claude") {
headers["anthropic-version"] = "2023-06-01";
}
if (stream) {
headers["Accept"] = "text/event-stream";
}
return headers;
}
}
+46
View File
@@ -0,0 +1,46 @@
import { BaseExecutor } from "./base.ts";
import { PROVIDERS } from "../config/constants.ts";
/**
* PollinationsExecutor handles optional API key auth.
* Pollinations AI works WITHOUT any API key for basic use (1 req/15s).
* If an API key is provided, higher rate limits apply.
*
* Endpoint: https://text.pollinations.ai/openai/chat/completions
* Docs: https://pollinations.ai/docs
*/
export class PollinationsExecutor extends BaseExecutor {
constructor() {
super("pollinations", PROVIDERS["pollinations"] || { format: "openai" });
}
buildUrl(_model: string, _stream: boolean, _urlIndex = 0, _credentials = null): string {
return "https://text.pollinations.ai/openai/chat/completions";
}
buildHeaders(credentials: any, stream = true): Record<string, string> {
const headers: Record<string, string> = {
"Content-Type": "application/json",
};
// API key is OPTIONAL — skip Authorization header if no key provided
const key = credentials?.apiKey || credentials?.accessToken;
if (key) {
headers["Authorization"] = `Bearer ${key}`;
}
if (stream) {
headers["Accept"] = "text/event-stream";
}
return headers;
}
transformRequest(model: string, body: any, _stream: boolean, _credentials: any): any {
// Pollinations uses model names directly like "openai", "claude", "deepseek", etc.
// No transformation needed — the model name is already the Pollinations alias.
return body;
}
}
export default PollinationsExecutor;
+8 -4
View File
@@ -28,13 +28,17 @@ function upstreamErrorResponse(res, errText) {
let errorMessage: string;
try {
const parsed = JSON.parse(errText);
errorMessage =
// Extract a human-readable message from various error response shapes.
// Guard against `parsed.error` being an object (e.g. ElevenLabs returns
// { error: { message: "...", status_code: 401 } } or { detail: { ... } })
const raw =
parsed?.err_msg ||
parsed?.error?.message ||
parsed?.error ||
(typeof parsed?.error === "string" ? parsed.error : null) ||
parsed?.message ||
parsed?.detail ||
errText;
(typeof parsed?.detail === "string" ? parsed.detail : parsed?.detail?.message) ||
null;
errorMessage = raw ? String(raw) : errText || `Upstream error (${res.status})`;
} catch {
errorMessage = errText || `Upstream error (${res.status})`;
}
+64 -8
View File
@@ -34,13 +34,15 @@ function upstreamErrorResponse(res, errText) {
let errorMessage: string;
try {
const parsed = JSON.parse(errText);
errorMessage =
// Guard against `parsed.error` or `parsed.detail` being objects
const raw =
parsed?.err_msg ||
parsed?.error?.message ||
parsed?.error ||
(typeof parsed?.error === "string" ? parsed.error : null) ||
parsed?.message ||
parsed?.detail ||
errText;
(typeof parsed?.detail === "string" ? parsed.detail : parsed?.detail?.message) ||
null;
errorMessage = raw ? String(raw) : errText || `Upstream error (${res.status})`;
} catch {
errorMessage = errText || `Upstream error (${res.status})`;
}
@@ -65,13 +67,67 @@ function getUploadedFileName(file: Blob & { name?: unknown }): string {
return typeof file.name === "string" && file.name.length > 0 ? file.name : "audio.wav";
}
/**
* Infer a suitable Content-Type for Deepgram from the browser-provided MIME
* type and the original filename. Deepgram accepts `audio/*` and many raw
* formats, but `video/*` causes it to silently fail with "no speech detected".
*
* Strategy:
* 1. If the browser says `audio/*`, keep it as-is.
* 2. If it's `video/*` (e.g. `.mp4`), remap to the audio equivalent so
* Deepgram extracts the audio track. `.mp4` `audio/mp4`, etc.
* 3. Fall back to `application/octet-stream` which tells Deepgram to
* auto-detect from the raw bytes (most reliable for unknown formats).
*/
function resolveAudioContentType(file: Blob & { name?: unknown }): string {
const browserType = (file.type || "").toLowerCase();
const fileName = typeof file.name === "string" ? file.name.toLowerCase() : "";
// 1) Browser already says it's audio — trust it
if (browserType.startsWith("audio/")) return browserType;
// 2) Derive from file extension (covers video/* and empty MIME)
const ext = fileName.includes(".") ? fileName.split(".").pop() : "";
const EXT_TO_MIME: Record<string, string> = {
mp3: "audio/mpeg",
mp4: "audio/mp4",
m4a: "audio/mp4",
wav: "audio/wav",
ogg: "audio/ogg",
flac: "audio/flac",
webm: "audio/webm",
aac: "audio/aac",
wma: "audio/x-ms-wma",
opus: "audio/opus",
};
if (ext && EXT_TO_MIME[ext]) return EXT_TO_MIME[ext];
// 3) Fallback — let Deepgram auto-detect from raw bytes
return "application/octet-stream";
}
/**
* Handle Deepgram transcription (raw binary audio, model via query param)
*/
async function handleDeepgramTranscription(providerConfig, file, modelId, token) {
async function handleDeepgramTranscription(
providerConfig,
file,
modelId,
token,
formData?: FormData
) {
const url = new URL(providerConfig.baseUrl);
url.searchParams.set("model", modelId);
url.searchParams.set("smart_format", "true");
url.searchParams.set("punctuate", "true");
// Language: if caller specified one, use it; otherwise let Deepgram auto-detect
const langParam = formData?.get("language");
if (typeof langParam === "string" && langParam.trim()) {
url.searchParams.set("language", langParam.trim());
} else {
url.searchParams.set("detect_language", "true");
}
const arrayBuffer = await file.arrayBuffer();
@@ -79,7 +135,7 @@ async function handleDeepgramTranscription(providerConfig, file, modelId, token)
method: "POST",
headers: {
...buildAuthHeaders(providerConfig, token),
"Content-Type": file.type || "audio/wav",
"Content-Type": resolveAudioContentType(file),
},
body: arrayBuffer,
});
@@ -212,7 +268,7 @@ async function handleHuggingFaceTranscription(providerConfig, file, modelId, tok
method: "POST",
headers: {
...buildAuthHeaders(providerConfig, token),
"Content-Type": file.type || "audio/wav",
"Content-Type": resolveAudioContentType(file),
},
body: arrayBuffer,
});
@@ -283,7 +339,7 @@ export async function handleAudioTranscription({
// Route to provider-specific handler
if (providerConfig.format === "deepgram") {
return handleDeepgramTranscription(providerConfig, file, modelId, token);
return handleDeepgramTranscription(providerConfig, file, modelId, token, formData);
}
if (providerConfig.format === "assemblyai") {
+28 -2
View File
@@ -308,6 +308,27 @@ export async function handleChatCore({
}
return [];
}
// (#527) tool_result → convert to text instead of dropping.
// When Claude Code + superpowers routes through Codex, it sends tool_result
// blocks in user messages. Silently dropping them causes Codex to loop
// because it never receives the tool response and keeps re-requesting it.
if (block.type === "tool_result") {
const toolId = block.tool_use_id ?? block.id ?? "unknown";
const resultContent = block.content ?? block.text ?? block.output ?? "";
const resultText =
typeof resultContent === "string"
? resultContent
: Array.isArray(resultContent)
? resultContent
.filter((c: Record<string, unknown>) => c.type === "text")
.map((c: Record<string, unknown>) => c.text)
.join("\n")
: JSON.stringify(resultContent);
if (resultText.length > 0) {
return [{ type: "text", text: `[Tool Result: ${toolId}]\n${resultText}` }];
}
return [];
}
// Unknown types: drop silently
log?.debug?.("CONTENT", `Dropped unsupported content part type="${block.type}"`);
return [];
@@ -317,10 +338,15 @@ export async function handleChatCore({
}
}
const normalizeToolCallId = getModelNormalizeToolCallId(provider || "", model || "");
const normalizeToolCallId = getModelNormalizeToolCallId(
provider || "",
model || "",
sourceFormat
);
const preserveDeveloperRole = getModelPreserveOpenAIDeveloperRole(
provider || "",
model || ""
model || "",
sourceFormat
);
translatedBody = translateRequest(
sourceFormat,
+67 -4
View File
@@ -447,8 +447,10 @@ export async function handleComboChat({
const handleSingleModelWrapped = combo.context_cache_protection
? async (b, modelStr) => {
const res = await handleSingleModel(b, modelStr);
// Inject tag only on success and only for non-streaming non-binary responses
if (res.ok && !b.stream) {
if (!res.ok) return res;
// Non-streaming: inject tag into JSON response (existing logic)
if (!b.stream) {
try {
const json = await res.clone().json();
const msgs = Array.isArray(json?.messages) ? json.messages : [];
@@ -460,10 +462,71 @@ export async function handleComboChat({
});
}
} catch {
/* non-JSON or stream — skip tagging */
/* non-JSON — skip tagging */
}
return res;
}
return res;
// Streaming (Fix #490 + #511): prepend omniModel tag into the first
// non-empty content chunk so it arrives BEFORE finish_reason:stop.
// SDKs close the connection on finish_reason, so anything sent after
// that marker is silently dropped.
if (!res.body) return res;
const tagContent = `\\n<omniModel>${modelStr}</omniModel>\\n`;
const encoder = new TextEncoder();
const decoder = new TextDecoder();
let tagInjected = false;
const transform = new TransformStream({
transform(chunk, controller) {
if (tagInjected) {
// Already injected — passthrough
controller.enqueue(chunk);
return;
}
const text = decoder.decode(chunk, { stream: true });
// Look for the first SSE data line with non-empty content
// Pattern: "content":"<non-empty>" — we inject tag at the start
const contentMatch = text.match(/"content":"([^"]+)/);
if (contentMatch) {
// Inject tag at the beginning of the first content value
const injected = text.replace(
/"content":"([^"]+)/,
`"content":"${tagContent.replace(/"/g, '\\"')}$1`
);
tagInjected = true;
controller.enqueue(encoder.encode(injected));
return;
}
// No content yet — passthrough
controller.enqueue(chunk);
},
flush(controller) {
// If stream ends without ever finding content (edge case),
// inject tag as a standalone chunk before the stream closes
if (!tagInjected) {
const tagChunk = `data: ${JSON.stringify({
choices: [
{
delta: { content: tagContent },
index: 0,
finish_reason: null,
},
],
})}\n\n`;
controller.enqueue(encoder.encode(tagChunk));
}
},
});
const transformedStream = res.body.pipeThrough(transform);
return new Response(transformedStream, {
status: res.status,
headers: res.headers,
});
}
: handleSingleModel;
// ─────────────────────────────────────────────────────────────────────────
+10 -2
View File
@@ -34,7 +34,11 @@ interface Message {
// ── Context Caching Tag ─────────────────────────────────────────────────────
const CACHE_TAG_PATTERN = /<omniModel>([^<]+)<\/omniModel>/;
// Handles both actual newlines (U+000A) and literal \n sequences injected
// by combo.ts streaming around the <omniModel> tag (#531). Non-global so that
// .exec() and .test() stay stateless; callers that need full replacement use
// String.prototype.replace() which replaces all non-overlapping matches.
const CACHE_TAG_PATTERN = /(?:\\n|\n)?<omniModel>([^<]+)<\/omniModel>(?:\\n|\n)?/;
/**
* Inject the model tag into the last assistant message (or append a new one).
@@ -165,7 +169,11 @@ export function applyComboAgentMiddleware(
if (comboConfig.context_cache_protection) {
pinnedModel = extractPinnedModel(messages);
if (pinnedModel) {
// Model is pinned — caller should override model selection
// (#535) Model is pinned via <omniModel> tag — override body.model so the combo
// router uses exactly this model instead of picking a different one. Without this,
// the extracted pinnedModel is returned but body.model is unchanged, breaking
// context cache sessions by sending subsequent turns to a different model.
body = { ...body, model: pinnedModel };
}
}
@@ -93,10 +93,11 @@ export function convertResponsesApiFormat(body) {
}
// Cleanup Responses API specific fields
// Note: prompt_cache_key is intentionally preserved — it is used by Codex and other
// providers as a cache-affinity signal. Stripping it breaks prompt caching (#517).
delete result.input;
delete result.instructions;
delete result.include;
delete result.prompt_cache_key;
delete result.store;
delete result.reasoning;
@@ -227,10 +227,11 @@ export function openaiResponsesToOpenAIRequest(
});
// Cleanup Responses API specific fields
// Note: prompt_cache_key is intentionally preserved — it is used by Codex and other
// providers as a cache-affinity signal. Stripping it breaks prompt caching (#517).
delete result.input;
delete result.instructions;
delete result.include;
delete result.prompt_cache_key;
delete result.store;
delete result.reasoning;
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "omniroute",
"version": "2.8.9",
"version": "3.0.0-rc.2",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "omniroute",
"version": "2.8.9",
"version": "3.0.0-rc.2",
"hasInstallScript": true,
"license": "MIT",
"workspaces": [
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "omniroute",
"version": "2.8.9",
"version": "3.0.0-rc.2",
"description": "Smart AI Router with auto fallback — route to FREE & cheap models, zero downtime. Works with Cursor, Cline, Claude Desktop, Codex, and any OpenAI-compatible tool.",
"type": "module",
"bin": {
Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

+1
View File
@@ -0,0 +1 @@
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Cloudflare</title><path d="M16.5088 16.8447c.1475-.5068.0908-.9707-.1553-1.3154-.2246-.3164-.6045-.499-1.0615-.5205l-8.6592-.1123a.1559.1559 0 0 1-.1333-.0713c-.0283-.042-.0351-.0986-.021-.1553.0278-.084.1123-.1484.2036-.1562l8.7359-.1123c1.0351-.0489 2.1601-.8868 2.5537-1.9136l.499-1.3013c.0215-.0561.0293-.1128.0147-.168-.5625-2.5463-2.835-4.4453-5.5499-4.4453-2.5039 0-4.6284 1.6177-5.3876 3.8614-.4927-.3658-1.1187-.5625-1.794-.499-1.2026.119-2.1665 1.083-2.2861 2.2856-.0283.31-.0069.6128.0635.894C1.5683 13.171 0 14.7754 0 16.752c0 .1748.0142.3515.0352.5273.0141.083.0844.1475.1689.1475h15.9814c.0909 0 .1758-.0645.2032-.1553l.12-.4268zm2.7568-5.5634c-.0771 0-.1611 0-.2383.0112-.0566 0-.1054.0415-.127.0976l-.3378 1.1744c-.1475.5068-.0918.9707.1543 1.3164.2256.3164.6055.498 1.0625.5195l1.8437.1133c.0557 0 .1055.0263.1329.0703.0283.043.0351.1074.0214.1562-.0283.084-.1132.1485-.204.1553l-1.921.1123c-1.041.0488-2.1582.8867-2.5527 1.914l-.1406.3585c-.0283.0713.0215.1416.0986.1416h6.5977c.0771 0 .1474-.0489.169-.126.1122-.4082.1757-.837.1757-1.2803 0-2.6025-2.125-4.727-4.7344-4.727"/></svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

+1
View File
@@ -0,0 +1 @@
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Scaleway</title><path d="M16.605 11.11v5.72a1.77 1.77 0 01-1.54 1.69h-4a1.43 1.43 0 01-1.31-1.22 1.09 1.09 0 010-.18 1.37 1.37 0 011.37-1.36h1.74a1 1 0 001-1v-3.62a1.4 1.4 0 011.18-1.39h.17a1.37 1.37 0 011.39 1.36zm-6.46 1.74V9.26a1 1 0 011-1h1.85a1.37 1.37 0 001.37-1.37 1 1 0 000-.17 1.45 1.45 0 00-1.41-1.2h-3.96a1.81 1.81 0 00-1.58 1.66v5.7a1.37 1.37 0 001.37 1.37h.21a1.4 1.4 0 001.15-1.4zm12-4.29V20a4.53 4.53 0 01-4.15 4h-7.58a8.57 8.57 0 01-8.56-8.57V4.54A4.54 4.54 0 016.395 0h7.18a8.56 8.56 0 018.56 8.56zm-2.74 0a5.83 5.83 0 00-5.82-5.82h-7.19a1.79 1.79 0 00-1.8 1.8v10.89a5.83 5.83 0 005.82 5.8h7.44a1.79 1.79 0 001.54-1.48z"/></svg>

After

Width:  |  Height:  |  Size: 723 B

+2 -1
View File
@@ -48,7 +48,8 @@ function extractChangelogSections(content) {
}
function isSemver(value) {
return /^\d+\.\d+\.\d+$/.test(value);
// Accept X.Y.Z and X.Y.Z-prerelease.N (e.g. 3.0.0-rc.1, 3.0.0-beta.2)
return /^\d+\.\d+\.\d+(-[a-zA-Z0-9.]+)?$/.test(value);
}
let hasFailure = false;
@@ -0,0 +1,196 @@
/**
* Search Analytics Tab
*
* Shows search request stats from call_logs (request_type = 'search'),
* provider breakdown, cache hit rate, and cost summary.
*/
"use client";
import { useEffect, useState } from "react";
interface SearchStats {
total: number;
today: number;
cached: number;
errors: number;
totalCostUsd: number;
byProvider: Record<string, { count: number; costUsd: number }>;
last24h: Array<{ hour: string; count: number }>;
cacheHitRate: number;
avgDurationMs: number;
}
function StatCard({
icon,
label,
value,
sub,
}: {
icon: string;
label: string;
value: string | number;
sub?: string;
}) {
return (
<div className="card p-4 flex flex-col gap-1">
<div className="flex items-center gap-2 text-text-muted text-sm">
<span className="material-symbols-outlined text-[18px]">{icon}</span>
{label}
</div>
<div className="text-2xl font-bold text-text">{value}</div>
{sub && <div className="text-xs text-text-muted">{sub}</div>}
</div>
);
}
function ProviderBar({
provider,
count,
total,
costUsd,
}: {
provider: string;
count: number;
total: number;
costUsd: number;
}) {
const pct = total > 0 ? Math.round((count / total) * 100) : 0;
return (
<div className="flex flex-col gap-1">
<div className="flex justify-between text-sm">
<span className="font-medium text-text">{provider}</span>
<span className="text-text-muted">
{count} queries · ${costUsd.toFixed(4)}
</span>
</div>
<div className="h-2 rounded-full bg-bg-muted overflow-hidden">
<div
className="h-full rounded-full bg-primary transition-all"
style={{ width: `${pct}%` }}
/>
</div>
<div className="text-xs text-text-muted text-right">{pct}%</div>
</div>
);
}
export default function SearchAnalyticsTab() {
const [stats, setStats] = useState<SearchStats | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
fetch("/api/v1/search/analytics")
.then((r) => r.json())
.then((d) => {
setStats(d);
setLoading(false);
})
.catch((e) => {
setError(e.message);
setLoading(false);
});
}, []);
if (loading) {
return (
<div className="flex items-center justify-center py-16 text-text-muted">
<span className="material-symbols-outlined animate-spin mr-2">progress_activity</span>
Loading search analytics
</div>
);
}
if (error || !stats) {
return (
<div className="card p-6 text-center text-text-muted">
<span className="material-symbols-outlined text-[32px] mb-2 block">search_off</span>
{error || "No search data available yet."}
<p className="text-xs mt-2">
Search requests will appear here after the first search via /v1/search.
</p>
</div>
);
}
const providers = Object.entries(stats.byProvider).sort(([, a], [, b]) => b.count - a.count);
return (
<div className="flex flex-col gap-6">
{/* KPI Cards */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<StatCard
icon="manage_search"
label="Total Searches"
value={stats.total.toLocaleString()}
sub={`${stats.today} today`}
/>
<StatCard
icon="cached"
label="Cache Hit Rate"
value={`${stats.cacheHitRate}%`}
sub={`${stats.cached} cached requests`}
/>
<StatCard
icon="attach_money"
label="Total Cost"
value={`$${stats.totalCostUsd.toFixed(4)}`}
sub="search API costs"
/>
<StatCard
icon="timer"
label="Avg Response"
value={`${stats.avgDurationMs}ms`}
sub={stats.errors > 0 ? `${stats.errors} errors` : "No errors"}
/>
</div>
{/* Provider Breakdown */}
{providers.length > 0 && (
<div className="card p-5">
<h3 className="font-semibold text-text mb-4 flex items-center gap-2">
<span className="material-symbols-outlined text-primary text-[20px]">hub</span>
Provider Breakdown
</h3>
<div className="flex flex-col gap-4">
{providers.map(([prov, data]) => (
<ProviderBar
key={prov}
provider={prov}
count={data.count}
total={stats.total}
costUsd={data.costUsd}
/>
))}
</div>
</div>
)}
{/* Empty state */}
{stats.total === 0 && (
<div className="card p-8 text-center text-text-muted">
<span className="material-symbols-outlined text-[48px] mb-3 block text-primary opacity-50">
travel_explore
</span>
<p className="font-medium text-text">No searches yet</p>
<p className="text-sm mt-1">
Use <code className="bg-bg-muted px-1 rounded">POST /v1/search</code> to start routing
web searches.
</p>
</div>
)}
{/* Free tier note */}
<div className="text-xs text-text-muted border border-border rounded-lg p-3 flex items-start gap-2">
<span className="material-symbols-outlined text-[16px] text-green-500 mt-0.5">
check_circle
</span>
<span>
<strong>Free tier available:</strong> Serper (2,500/mo), Brave (2,000/mo), Exa (1,000/mo),
Tavily (1,000/mo) total 6,500+ free searches/month with automatic failover.
</span>
</div>
</div>
);
}
@@ -3,15 +3,17 @@
import { useState, Suspense } from "react";
import { UsageAnalytics, CardSkeleton, SegmentedControl } from "@/shared/components";
import EvalsTab from "../usage/components/EvalsTab";
import SearchAnalyticsTab from "./SearchAnalyticsTab";
import { useTranslations } from "next-intl";
export default function AnalyticsPage() {
const [activeTab, setActiveTab] = useState("overview");
const t = useTranslations("analytics");
const tabDescriptions = {
const tabDescriptions: Record<string, string> = {
overview: t("overviewDescription"),
evals: t("evalsDescription"),
search: "Search request analytics — provider breakdown, cache hit rate, and cost tracking.",
};
return (
@@ -29,6 +31,7 @@ export default function AnalyticsPage() {
options={[
{ value: "overview", label: t("overview") },
{ value: "evals", label: t("evals") },
{ value: "search", label: "Search" },
]}
value={activeTab}
onChange={setActiveTab}
@@ -40,6 +43,7 @@ export default function AnalyticsPage() {
</Suspense>
)}
{activeTab === "evals" && <EvalsTab />}
{activeTab === "search" && <SearchAnalyticsTab />}
</div>
);
}
@@ -523,15 +523,12 @@ export default function ApiManagerPageClient() {
</div>
<div className="col-span-3 flex items-center gap-1.5">
<code className="text-sm text-text-muted font-mono truncate">{key.key}</code>
<button
onClick={() => copy(key.key, key.id)}
className="p-1 hover:bg-black/5 dark:hover:bg-white/5 rounded text-text-muted hover:text-primary opacity-0 group-hover:opacity-100 transition-all shrink-0"
title={t("copyMaskedKey")}
<span
className="p-1 text-text-muted/40 opacity-0 group-hover:opacity-100 transition-all shrink-0 cursor-help"
title={t("keyOnlyAvailableAtCreation")}
>
<span className="material-symbols-outlined text-[14px]">
{copied === key.id ? "check" : "content_copy"}
</span>
</button>
<span className="material-symbols-outlined text-[14px]">lock</span>
</span>
</div>
<div className="col-span-2 flex items-center">
<div className="flex flex-col items-start gap-1">
@@ -58,8 +58,10 @@ export default function ClaudeToolCard({
const effectiveConfigStatus = configStatus || batchStatus?.configStatus || null;
useEffect(() => {
// (#523) Store the key *id* (not the masked string) so the backend can
// resolve the real secret from DB before writing to settings.json.
if (apiKeys?.length > 0 && !selectedApiKey) {
setSelectedApiKey(apiKeys[0].key);
setSelectedApiKey(apiKeys[0].id);
}
}, [apiKeys, selectedApiKey]);
@@ -95,10 +97,11 @@ export default function ClaudeToolCard({
}
}
});
// Only set selectedApiKey if it exists in apiKeys list
// Restore selected key from file: match token stored in file against known keys
const tokenFromFile = env.ANTHROPIC_AUTH_TOKEN;
if (tokenFromFile && apiKeys?.some((k) => k.key === tokenFromFile)) {
setSelectedApiKey(tokenFromFile);
if (tokenFromFile) {
const matchedKey = apiKeys?.find((k) => k.key === tokenFromFile);
if (matchedKey) setSelectedApiKey(matchedKey.id);
}
}
}, [claudeStatus, apiKeys, tool.defaultModels, onModelMappingChange]);
@@ -132,24 +135,27 @@ export default function ClaudeToolCard({
try {
const env: any = { ANTHROPIC_BASE_URL: getEffectiveBaseUrl() };
// Get key from dropdown, fallback to first key or sk_omniroute for localhost
const keyToUse =
selectedApiKey?.trim() ||
(apiKeys?.length > 0 ? apiKeys[0].key : null) ||
(!cloudEnabled ? "sk_omniroute" : null);
// (#523) Prefer keyId lookup so the backend writes the real key to disk.
// Fall back to sk_omniroute for localhost-only setups without a key.
const selectedKeyId = selectedApiKey?.trim() || (apiKeys?.length > 0 ? apiKeys[0].id : null);
const skOmnirouteFallback = !cloudEnabled ? "sk_omniroute" : null;
if (keyToUse) {
env.ANTHROPIC_AUTH_TOKEN = keyToUse;
if (!selectedKeyId && skOmnirouteFallback) {
env.ANTHROPIC_AUTH_TOKEN = skOmnirouteFallback;
}
tool.defaultModels.forEach((model) => {
const targetModel = modelMappings[model.alias];
if (targetModel && model.envKey) env[model.envKey] = targetModel;
});
const postBody: Record<string, unknown> = { env };
if (selectedKeyId) postBody.keyId = selectedKeyId;
const res = await fetch("/api/cli-tools/claude-settings", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ env }),
body: JSON.stringify(postBody),
});
const data = await res.json();
if (res.ok) {
@@ -412,7 +418,7 @@ export default function ClaudeToolCard({
className="flex-1 px-2 py-1.5 bg-surface rounded text-xs border border-border focus:outline-none focus:ring-1 focus:ring-primary/50"
>
{apiKeys.map((key) => (
<option key={key.id} value={key.key}>
<option key={key.id} value={key.id}>
{key.key}
</option>
))}
@@ -1,6 +1,6 @@
"use client";
import { useState, useEffect, useCallback, useRef } from "react";
import { useState, useEffect, useCallback, useRef, useMemo } from "react";
import { useNotificationStore } from "@/store/notificationStore";
import PropTypes from "prop-types";
import { useParams, useRouter } from "next/navigation";
@@ -31,6 +31,129 @@ import {
} from "@/shared/constants/providers";
import { getModelsByProviderId } from "@/shared/constants/models";
import { useCopyToClipboard } from "@/shared/hooks/useCopyToClipboard";
import {
MODEL_COMPAT_PROTOCOL_KEYS,
type ModelCompatProtocolKey,
} from "@/shared/constants/modelCompat";
type CompatByProtocolMap = Partial<
Record<
ModelCompatProtocolKey,
{ normalizeToolCallId?: boolean; preserveOpenAIDeveloperRole?: boolean }
>
>;
type CompatModelRow = {
id?: string;
name?: string;
source?: string;
apiFormat?: string;
supportedEndpoints?: string[];
normalizeToolCallId?: boolean;
preserveOpenAIDeveloperRole?: boolean;
compatByProtocol?: CompatByProtocolMap;
};
type CompatModelMap = Map<string, CompatModelRow>;
function buildCompatMap(rows: CompatModelRow[]): CompatModelMap {
const m = new Map<string, CompatModelRow>();
for (const r of rows) if (r.id) m.set(r.id, r);
return m;
}
function getProtoSlice(
c: CompatModelRow | undefined,
o: CompatModelRow | undefined,
protocol: string
) {
return c?.compatByProtocol?.[protocol] ?? o?.compatByProtocol?.[protocol];
}
function effectiveNormalizeForProtocol(
modelId: string,
protocol: string,
customMap: CompatModelMap,
overrideMap: CompatModelMap
): boolean {
const c = customMap.get(modelId);
const o = overrideMap.get(modelId);
const pc = getProtoSlice(c, o, protocol);
if (pc && Object.prototype.hasOwnProperty.call(pc, "normalizeToolCallId")) {
return Boolean(pc.normalizeToolCallId);
}
if (c?.normalizeToolCallId) return true;
return Boolean(o?.normalizeToolCallId);
}
function effectivePreserveForProtocol(
modelId: string,
protocol: string,
customMap: CompatModelMap,
overrideMap: CompatModelMap
): boolean {
const c = customMap.get(modelId);
const o = overrideMap.get(modelId);
const pc = getProtoSlice(c, o, protocol);
if (pc && Object.prototype.hasOwnProperty.call(pc, "preserveOpenAIDeveloperRole")) {
return Boolean(pc.preserveOpenAIDeveloperRole);
}
if (c && Object.prototype.hasOwnProperty.call(c, "preserveOpenAIDeveloperRole")) {
return Boolean(c.preserveOpenAIDeveloperRole);
}
if (o && Object.prototype.hasOwnProperty.call(o, "preserveOpenAIDeveloperRole")) {
return Boolean(o.preserveOpenAIDeveloperRole);
}
return true;
}
function anyNormalizeCompatBadge(
modelId: string,
customMap: CompatModelMap,
overrideMap: CompatModelMap
): boolean {
const c = customMap.get(modelId);
const o = overrideMap.get(modelId);
if (c?.normalizeToolCallId || o?.normalizeToolCallId) return true;
for (const p of MODEL_COMPAT_PROTOCOL_KEYS) {
const pc = getProtoSlice(c, o, p);
if (pc?.normalizeToolCallId) return true;
}
return false;
}
function anyNoPreserveCompatBadge(
modelId: string,
customMap: CompatModelMap,
overrideMap: CompatModelMap
): boolean {
const c = customMap.get(modelId);
const o = overrideMap.get(modelId);
if (
c &&
Object.prototype.hasOwnProperty.call(c, "preserveOpenAIDeveloperRole") &&
c.preserveOpenAIDeveloperRole === false
) {
return true;
}
if (
o &&
Object.prototype.hasOwnProperty.call(o, "preserveOpenAIDeveloperRole") &&
o.preserveOpenAIDeveloperRole === false
) {
return true;
}
for (const p of MODEL_COMPAT_PROTOCOL_KEYS) {
const pc = getProtoSlice(c, o, p);
if (
pc &&
Object.prototype.hasOwnProperty.call(pc, "preserveOpenAIDeveloperRole") &&
pc.preserveOpenAIDeveloperRole === false
) {
return true;
}
}
return false;
}
interface ModelRowProps {
model: { id: string };
@@ -40,10 +163,16 @@ interface ModelRowProps {
onCopy: (text: string, key: string) => void;
t: (key: string, values?: Record<string, unknown>) => string;
showDeveloperToggle?: boolean;
normalizeToolCallId?: boolean;
preserveDeveloperRole?: boolean;
onNormalizeChange?: (v: boolean) => void;
onPreserveChange?: (v: boolean) => void;
effectiveModelNormalize: (modelId: string, protocol?: string) => boolean;
effectiveModelPreserveDeveloper: (modelId: string, protocol?: string) => boolean;
saveModelCompatFlags: (
modelId: string,
patch: {
normalizeToolCallId?: boolean;
preserveOpenAIDeveloperRole?: boolean;
compatByProtocol?: CompatByProtocolMap;
}
) => void;
compatDisabled?: boolean;
}
@@ -55,10 +184,16 @@ interface PassthroughModelRowProps {
onDeleteAlias: () => void;
t: (key: string, values?: Record<string, unknown>) => string;
showDeveloperToggle?: boolean;
normalizeToolCallId?: boolean;
preserveDeveloperRole?: boolean;
onNormalizeChange?: (v: boolean) => void;
onPreserveChange?: (v: boolean) => void;
effectiveModelNormalize: (modelId: string, protocol?: string) => boolean;
effectiveModelPreserveDeveloper: (modelId: string, protocol?: string) => boolean;
saveModelCompatFlags: (
modelId: string,
patch: {
normalizeToolCallId?: boolean;
preserveOpenAIDeveloperRole?: boolean;
compatByProtocol?: CompatByProtocolMap;
}
) => void;
compatDisabled?: boolean;
}
@@ -230,27 +365,43 @@ function normalizeCodexLimitPolicy(policy: unknown): { use5h: boolean; useWeekly
};
}
function compatProtocolLabelKey(protocol: string): string {
if (protocol === "openai") return "compatProtocolOpenAI";
if (protocol === "openai-responses") return "compatProtocolOpenAIResponses";
if (protocol === "claude") return "compatProtocolClaude";
return "compatProtocolOpenAI";
}
function ModelCompatPopover({
t,
normalizeToolCallId,
preserveDeveloperRole,
effectiveModelNormalize,
effectiveModelPreserveDeveloper,
onCompatPatch,
showDeveloperToggle = true,
onNormalizeChange,
onPreserveChange,
disabled,
}: {
t: (key: string) => string;
normalizeToolCallId: boolean;
preserveDeveloperRole?: boolean;
effectiveModelNormalize: (protocol: string) => boolean;
effectiveModelPreserveDeveloper: (protocol: string) => boolean;
onCompatPatch: (
protocol: string,
payload: {
normalizeToolCallId?: boolean;
preserveOpenAIDeveloperRole?: boolean;
}
) => void;
showDeveloperToggle?: boolean;
onNormalizeChange: (v: boolean) => void;
onPreserveChange: (v: boolean) => void;
disabled?: boolean;
}) {
const [open, setOpen] = useState(false);
const [protocol, setProtocol] = useState<string>(MODEL_COMPAT_PROTOCOL_KEYS[0]);
const ref = useRef<HTMLDivElement>(null);
const panelRef = useRef<HTMLDivElement | null>(null);
const normalizeToolCallId = effectiveModelNormalize(protocol);
const preserveDeveloperRole = effectiveModelPreserveDeveloper(protocol);
const devToggle = showDeveloperToggle && protocol !== "claude";
// Click-outside: check both trigger and panel so that if the panel is ever rendered
// in a portal (outside this subtree), clicks inside the panel still do not close it.
useEffect(() => {
@@ -280,32 +431,47 @@ function ModelCompatPopover({
{open && (
<div
ref={panelRef}
className="absolute left-0 top-full mt-1 z-50 min-w-[200px] p-3 rounded-lg border border-border bg-white dark:bg-zinc-900 shadow-xl ring-1 ring-black/5 dark:ring-white/10"
className="absolute left-0 top-full mt-1 z-50 min-w-[220px] max-w-[92vw] p-3 rounded-lg border border-border bg-white dark:bg-zinc-900 shadow-xl ring-1 ring-black/5 dark:ring-white/10"
>
<p className="text-[10px] font-semibold uppercase tracking-wide text-text-muted mb-2">
<p className="text-[10px] font-semibold uppercase tracking-wide text-text-muted mb-1">
{t("compatAdjustmentsTitle")}
</p>
<p className="text-[10px] text-text-muted mb-2 leading-snug">{t("compatProtocolHint")}</p>
<label className="block text-[10px] font-medium text-text-muted mb-1">
{t("compatProtocolLabel")}
</label>
<select
value={protocol}
onChange={(e) => setProtocol(e.target.value)}
disabled={disabled}
className="w-full mb-3 px-2 py-1.5 text-xs rounded-md border border-border bg-white dark:bg-zinc-800 text-zinc-900 dark:text-zinc-100 focus:outline-none focus:ring-1 focus:ring-primary/50"
>
{MODEL_COMPAT_PROTOCOL_KEYS.map((p) => (
<option key={p} value={p}>
{t(compatProtocolLabelKey(p))}
</option>
))}
</select>
<div className="flex flex-col gap-3">
<Toggle
size="sm"
label={t("compatToolIdShort")}
title={t("normalizeToolCallIdLabel")}
checked={normalizeToolCallId}
onChange={onNormalizeChange}
onChange={(v) => onCompatPatch(protocol, { normalizeToolCallId: v })}
disabled={disabled}
/>
{showDeveloperToggle && (
<>
{/* Inversion: Toggle checked = "do not preserve" (UI). onPreserveChange(val) = value to store (true = preserve, false = do not preserve), so we pass !checked. */}
<Toggle
size="sm"
label={t("compatDoNotPreserveDeveloper")}
title={t("preserveDeveloperRoleLabel")}
checked={preserveDeveloperRole === false}
onChange={(checked) => onPreserveChange(!checked)}
disabled={disabled}
/>
</>
{devToggle && (
<Toggle
size="sm"
label={t("compatDoNotPreserveDeveloper")}
title={t("preserveDeveloperRoleLabel")}
checked={preserveDeveloperRole === false}
onChange={(checked) =>
onCompatPatch(protocol, { preserveOpenAIDeveloperRole: !checked })
}
disabled={disabled}
/>
)}
</div>
</div>
@@ -350,12 +516,8 @@ export default function ProviderDetailPage() {
importedCount: 0,
});
const [modelMeta, setModelMeta] = useState<{
customModels: Record<string, unknown>[];
modelCompatOverrides: {
id: string;
normalizeToolCallId?: boolean;
preserveOpenAIDeveloperRole?: boolean;
}[];
customModels: CompatModelRow[];
modelCompatOverrides: Array<CompatModelRow & { id: string }>;
}>({ customModels: [], modelCompatOverrides: [] });
const [compatSavingModelId, setCompatSavingModelId] = useState<string | null>(null);
@@ -1020,48 +1182,91 @@ export default function ProviderDetailPage() {
const canImportModels = connections.some((conn) => conn.isActive !== false);
const effectiveModelNormalize = (modelId: string) => {
const c = modelMeta.customModels.find((m: { id?: string }) => m.id === modelId) as
| { normalizeToolCallId?: boolean }
| undefined;
if (c) return Boolean(c.normalizeToolCallId);
const o = modelMeta.modelCompatOverrides.find((e) => e.id === modelId);
return Boolean(o?.normalizeToolCallId);
};
const customMap = useMemo(() => buildCompatMap(modelMeta.customModels), [modelMeta.customModels]);
const overrideMap = useMemo(
() => buildCompatMap(modelMeta.modelCompatOverrides),
[modelMeta.modelCompatOverrides]
);
const effectiveModelPreserveDeveloper = (modelId: string) => {
const c = modelMeta.customModels.find((m: { id?: string }) => m.id === modelId) as
| Record<string, unknown>
| undefined;
if (c && Object.prototype.hasOwnProperty.call(c, "preserveOpenAIDeveloperRole")) {
return Boolean(c.preserveOpenAIDeveloperRole);
}
const o = modelMeta.modelCompatOverrides.find((e) => e.id === modelId);
if (o && Object.prototype.hasOwnProperty.call(o, "preserveOpenAIDeveloperRole")) {
return Boolean(o.preserveOpenAIDeveloperRole);
}
return true;
};
const effectiveModelNormalize = (modelId: string, protocol = MODEL_COMPAT_PROTOCOL_KEYS[0]) =>
effectiveNormalizeForProtocol(modelId, protocol, customMap, overrideMap);
const effectiveModelPreserveDeveloper = (
modelId: string,
protocol = MODEL_COMPAT_PROTOCOL_KEYS[0]
) => effectivePreserveForProtocol(modelId, protocol, customMap, overrideMap);
const saveModelCompatFlags = async (
modelId: string,
patch: { normalizeToolCallId?: boolean; preserveOpenAIDeveloperRole?: boolean }
patch: {
normalizeToolCallId?: boolean;
preserveOpenAIDeveloperRole?: boolean;
compatByProtocol?: CompatByProtocolMap;
}
) => {
setCompatSavingModelId(modelId);
try {
const body: Record<string, unknown> = { provider: providerId, modelId, ...patch };
const c = customMap.get(modelId) as Record<string, unknown> | undefined;
let body: Record<string, unknown>;
const onlyCompatByProtocol =
patch.compatByProtocol &&
patch.normalizeToolCallId === undefined &&
patch.preserveOpenAIDeveloperRole === undefined;
if (c) {
if (onlyCompatByProtocol) {
body = {
provider: providerId,
modelId,
compatByProtocol: patch.compatByProtocol,
};
} else {
body = {
provider: providerId,
modelId,
modelName: (c.name as string) || modelId,
source: (c.source as string) || "manual",
apiFormat: (c.apiFormat as string) || "chat-completions",
supportedEndpoints:
Array.isArray(c.supportedEndpoints) && (c.supportedEndpoints as unknown[]).length
? c.supportedEndpoints
: ["chat"],
normalizeToolCallId:
patch.normalizeToolCallId !== undefined
? patch.normalizeToolCallId
: Boolean(c.normalizeToolCallId),
preserveOpenAIDeveloperRole:
patch.preserveOpenAIDeveloperRole !== undefined
? patch.preserveOpenAIDeveloperRole
: Object.prototype.hasOwnProperty.call(c, "preserveOpenAIDeveloperRole")
? Boolean(c.preserveOpenAIDeveloperRole)
: true,
};
if (patch.compatByProtocol) body.compatByProtocol = patch.compatByProtocol;
}
} else {
body = { provider: providerId, modelId, ...patch };
}
const res = await fetch("/api/provider-models", {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
});
if (res.ok) await fetchProviderModelMeta();
else notify.error(t("failedSaveCustomModel"));
if (!res.ok) {
notify.error(t("failedSaveCustomModel"));
return;
}
} catch {
notify.error(t("failedSaveCustomModel"));
return;
} finally {
setCompatSavingModelId(null);
}
try {
await fetchProviderModelMeta();
} catch {
/* refresh failure is non-critical — data was already saved */
}
};
const renderModelsSection = () => {
@@ -1166,14 +1371,9 @@ export default function ProviderDetailPage() {
onCopy={copy}
t={t}
showDeveloperToggle
normalizeToolCallId={effectiveModelNormalize(model.id)}
preserveDeveloperRole={effectiveModelPreserveDeveloper(model.id)}
onNormalizeChange={(v) =>
saveModelCompatFlags(model.id, { normalizeToolCallId: v })
}
onPreserveChange={(v) =>
saveModelCompatFlags(model.id, { preserveOpenAIDeveloperRole: v })
}
effectiveModelNormalize={effectiveModelNormalize}
effectiveModelPreserveDeveloper={effectiveModelPreserveDeveloper}
saveModelCompatFlags={saveModelCompatFlags}
compatDisabled={compatSavingModelId === model.id}
/>
);
@@ -1813,10 +2013,9 @@ function ModelRow({
onCopy,
t,
showDeveloperToggle = true,
normalizeToolCallId,
preserveDeveloperRole,
onNormalizeChange,
onPreserveChange,
effectiveModelNormalize,
effectiveModelPreserveDeveloper,
saveModelCompatFlags,
compatDisabled,
}: ModelRowProps) {
return (
@@ -1840,11 +2039,12 @@ function ModelRow({
</div>
<ModelCompatPopover
t={t}
normalizeToolCallId={Boolean(normalizeToolCallId)}
preserveDeveloperRole={preserveDeveloperRole}
effectiveModelNormalize={(p) => effectiveModelNormalize(model.id, p)}
effectiveModelPreserveDeveloper={(p) => effectiveModelPreserveDeveloper(model.id, p)}
onCompatPatch={(protocol, payload) =>
saveModelCompatFlags(model.id, { compatByProtocol: { [protocol]: payload } })
}
showDeveloperToggle={showDeveloperToggle}
onNormalizeChange={onNormalizeChange}
onPreserveChange={onPreserveChange}
disabled={compatDisabled}
/>
</div>
@@ -1861,10 +2061,9 @@ ModelRow.propTypes = {
onCopy: PropTypes.func.isRequired,
t: PropTypes.func,
showDeveloperToggle: PropTypes.bool,
normalizeToolCallId: PropTypes.bool,
preserveDeveloperRole: PropTypes.bool,
onNormalizeChange: PropTypes.func,
onPreserveChange: PropTypes.func,
effectiveModelNormalize: PropTypes.func.isRequired,
effectiveModelPreserveDeveloper: PropTypes.func.isRequired,
saveModelCompatFlags: PropTypes.func.isRequired,
compatDisabled: PropTypes.bool,
};
@@ -1960,12 +2159,9 @@ function PassthroughModelsSection({
onDeleteAlias={() => onDeleteAlias(alias)}
t={t}
showDeveloperToggle
normalizeToolCallId={effectiveModelNormalize(modelId)}
preserveDeveloperRole={effectiveModelPreserveDeveloper(modelId)}
onNormalizeChange={(v) => saveModelCompatFlags(modelId, { normalizeToolCallId: v })}
onPreserveChange={(v) =>
saveModelCompatFlags(modelId, { preserveOpenAIDeveloperRole: v })
}
effectiveModelNormalize={effectiveModelNormalize}
effectiveModelPreserveDeveloper={effectiveModelPreserveDeveloper}
saveModelCompatFlags={saveModelCompatFlags}
compatDisabled={compatSavingModelId === modelId}
/>
))}
@@ -1997,10 +2193,9 @@ function PassthroughModelRow({
onDeleteAlias,
t,
showDeveloperToggle = true,
normalizeToolCallId,
preserveDeveloperRole,
onNormalizeChange,
onPreserveChange,
effectiveModelNormalize,
effectiveModelPreserveDeveloper,
saveModelCompatFlags,
compatDisabled,
}: PassthroughModelRowProps) {
return (
@@ -2037,11 +2232,12 @@ function PassthroughModelRow({
<div className="pl-9">
<ModelCompatPopover
t={t}
normalizeToolCallId={Boolean(normalizeToolCallId)}
preserveDeveloperRole={preserveDeveloperRole}
effectiveModelNormalize={(p) => effectiveModelNormalize(modelId, p)}
effectiveModelPreserveDeveloper={(p) => effectiveModelPreserveDeveloper(modelId, p)}
onCompatPatch={(protocol, payload) =>
saveModelCompatFlags(modelId, { compatByProtocol: { [protocol]: payload } })
}
showDeveloperToggle={showDeveloperToggle}
onNormalizeChange={onNormalizeChange}
onPreserveChange={onPreserveChange}
disabled={compatDisabled}
/>
</div>
@@ -2057,10 +2253,9 @@ PassthroughModelRow.propTypes = {
onDeleteAlias: PropTypes.func.isRequired,
t: PropTypes.func,
showDeveloperToggle: PropTypes.bool,
normalizeToolCallId: PropTypes.bool,
preserveDeveloperRole: PropTypes.bool,
onNormalizeChange: PropTypes.func,
onPreserveChange: PropTypes.func,
effectiveModelNormalize: PropTypes.func.isRequired,
effectiveModelPreserveDeveloper: PropTypes.func.isRequired,
saveModelCompatFlags: PropTypes.func.isRequired,
compatDisabled: PropTypes.bool,
};
@@ -2075,7 +2270,10 @@ function CustomModelsSection({
}: CustomModelsSectionProps) {
const t = useTranslations("providers");
const notify = useNotificationStore();
const [customModels, setCustomModels] = useState([]);
const [customModels, setCustomModels] = useState<CompatModelRow[]>([]);
const [modelCompatOverrides, setModelCompatOverrides] = useState<
Array<CompatModelRow & { id: string }>
>([]);
const [newModelId, setNewModelId] = useState("");
const [newModelName, setNewModelName] = useState("");
const [newApiFormat, setNewApiFormat] = useState("chat-completions");
@@ -2085,16 +2283,18 @@ function CustomModelsSection({
const [editingModelId, setEditingModelId] = useState<string | null>(null);
const [editingApiFormat, setEditingApiFormat] = useState("chat-completions");
const [editingEndpoints, setEditingEndpoints] = useState<string[]>(["chat"]);
const [editingNormalizeToolCallId, setEditingNormalizeToolCallId] = useState(false);
const [editingPreserveDeveloperRole, setEditingPreserveDeveloperRole] = useState(false);
const [savingModelId, setSavingModelId] = useState<string | null>(null);
const customMap = useMemo(() => buildCompatMap(customModels), [customModels]);
const overrideMap = useMemo(() => buildCompatMap(modelCompatOverrides), [modelCompatOverrides]);
const fetchCustomModels = useCallback(async () => {
try {
const res = await fetch(`/api/provider-models?provider=${encodeURIComponent(providerId)}`);
if (res.ok) {
const data = await res.json();
setCustomModels(data.models || []);
setModelCompatOverrides(data.modelCompatOverrides || []);
}
} catch (e) {
console.error("Failed to fetch custom models:", e);
@@ -2160,23 +2360,44 @@ function CustomModelsSection({
? model.supportedEndpoints
: ["chat"]
);
setEditingNormalizeToolCallId(Boolean(model.normalizeToolCallId));
setEditingPreserveDeveloperRole(
Object.prototype.hasOwnProperty.call(model, "preserveOpenAIDeveloperRole")
? Boolean(model.preserveOpenAIDeveloperRole)
: true
);
};
const cancelEdit = () => {
setEditingModelId(null);
setEditingApiFormat("chat-completions");
setEditingEndpoints(["chat"]);
setEditingNormalizeToolCallId(false);
setEditingPreserveDeveloperRole(true);
setSavingModelId(null);
};
const saveCustomCompat = async (
modelId: string,
patch: { compatByProtocol?: CompatByProtocolMap }
) => {
setSavingModelId(modelId);
try {
const res = await fetch("/api/provider-models", {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ provider: providerId, modelId, ...patch }),
});
if (!res.ok) {
notify.error(t("failedSaveCustomModel"));
return;
}
} catch {
notify.error(t("failedSaveCustomModel"));
return;
} finally {
setSavingModelId(null);
}
try {
await fetchCustomModels();
onModelsChanged?.();
} catch {
/* refresh failure is non-critical — data was already saved */
}
};
const saveEdit = async (modelId) => {
if (!editingModelId || editingModelId !== modelId) return;
if (!editingEndpoints.length) {
@@ -2197,8 +2418,6 @@ function CustomModelsSection({
source: model?.source || "manual",
apiFormat: editingApiFormat,
supportedEndpoints: editingEndpoints,
normalizeToolCallId: editingNormalizeToolCallId,
preserveOpenAIDeveloperRole: editingPreserveDeveloperRole,
}),
});
@@ -2361,7 +2580,7 @@ function CustomModelsSection({
🔊 Audio
</span>
)}
{model.normalizeToolCallId && (
{anyNormalizeCompatBadge(model.id, customMap, overrideMap) && (
<span
className="text-[10px] px-1.5 py-0.5 rounded-full bg-slate-500/15 text-slate-400 font-medium"
title={t("normalizeToolCallIdLabel")}
@@ -2369,7 +2588,7 @@ function CustomModelsSection({
ID×9
</span>
)}
{model.preserveOpenAIDeveloperRole === false && (
{anyNoPreserveCompatBadge(model.id, customMap, overrideMap) && (
<span
className="text-[10px] px-1.5 py-0.5 rounded-full bg-cyan-500/15 text-cyan-400 font-medium"
title={t("compatDoNotPreserveDeveloper")}
@@ -2433,11 +2652,18 @@ function CustomModelsSection({
<div className="mt-3 pt-3 border-t border-border/80 w-full">
<ModelCompatPopover
t={t}
normalizeToolCallId={editingNormalizeToolCallId}
preserveDeveloperRole={editingPreserveDeveloperRole}
effectiveModelNormalize={(p) =>
effectiveNormalizeForProtocol(model.id, p, customMap, overrideMap)
}
effectiveModelPreserveDeveloper={(p) =>
effectivePreserveForProtocol(model.id, p, customMap, overrideMap)
}
onCompatPatch={(protocol, payload) =>
saveCustomCompat(model.id, {
compatByProtocol: { [protocol]: payload },
})
}
showDeveloperToggle
onNormalizeChange={setEditingNormalizeToolCallId}
onPreserveChange={setEditingPreserveDeveloperRole}
disabled={savingModelId === model.id}
/>
</div>
@@ -2716,12 +2942,9 @@ function CompatibleModelsSection({
onDeleteAlias={() => handleDeleteModel(modelId, alias)}
t={t}
showDeveloperToggle={!isAnthropic}
normalizeToolCallId={effectiveModelNormalize(modelId)}
preserveDeveloperRole={effectiveModelPreserveDeveloper(modelId)}
onNormalizeChange={(v) => saveModelCompatFlags(modelId, { normalizeToolCallId: v })}
onPreserveChange={(v) =>
saveModelCompatFlags(modelId, { preserveOpenAIDeveloperRole: v })
}
effectiveModelNormalize={effectiveModelNormalize}
effectiveModelPreserveDeveloper={effectiveModelPreserveDeveloper}
saveModelCompatFlags={saveModelCompatFlags}
compatDisabled={compatSavingModelId === modelId}
/>
))}
@@ -12,6 +12,7 @@ import { createBackup } from "@/shared/services/backupService";
import { saveCliToolLastConfigured, deleteCliToolLastConfigured } from "@/lib/db/cliToolState";
import { cliSettingsEnvSchema } from "@/shared/validation/schemas";
import { isValidationFailure, validateBody } from "@/shared/validation/helpers";
import { getApiKeyById } from "@/lib/localDb";
// Get claude settings path based on OS
const getClaudeSettingsPath = () => getCliPrimaryConfigPath("claude");
@@ -100,6 +101,22 @@ export async function POST(request: Request) {
}
const { env } = validation.data;
// (#523/#526) If a keyId was provided, resolve the real API key from DB.
// The /api/keys list endpoint returns masked key strings — sending those to
// disk would save an unusable half-hidden token. Resolving by ID guarantees
// we always write the full key value to the config file.
const keyId = typeof rawBody?.keyId === "string" ? rawBody.keyId.trim() : null;
if (keyId) {
try {
const keyRecord = await getApiKeyById(keyId);
if (keyRecord?.key) {
env.ANTHROPIC_AUTH_TOKEN = keyRecord.key as string;
}
} catch {
// Non-critical: fall back to whatever value was in env (e.g. sk_omniroute)
}
}
const settingsPath = getClaudeSettingsPath();
const claudeDir = path.dirname(settingsPath);
+13 -1
View File
@@ -9,6 +9,7 @@ import { createBackup } from "@/shared/services/backupService";
import { saveCliToolLastConfigured, deleteCliToolLastConfigured } from "@/lib/db/cliToolState";
import { cliModelConfigSchema } from "@/shared/validation/schemas";
import { isValidationFailure, validateBody } from "@/shared/validation/helpers";
import { getApiKeyById } from "@/lib/localDb";
const CLINE_DATA_DIR = path.join(os.homedir(), ".cline", "data");
const GLOBAL_STATE_PATH = path.join(CLINE_DATA_DIR, "globalState.json");
@@ -125,7 +126,18 @@ export async function POST(request: Request) {
if (isValidationFailure(validation)) {
return NextResponse.json({ error: validation.error }, { status: 400 });
}
const { baseUrl, apiKey, model } = validation.data;
let { baseUrl, apiKey, model } = validation.data;
// (#526) Resolve real key from DB if keyId was provided
const keyId = typeof rawBody?.keyId === "string" ? rawBody.keyId.trim() : null;
if (keyId) {
try {
const keyRecord = await getApiKeyById(keyId);
if (keyRecord?.key) apiKey = keyRecord.key as string;
} catch {
/* non-critical */
}
}
// Ensure directory exists
await fs.mkdir(CLINE_DATA_DIR, { recursive: true });
@@ -39,6 +39,10 @@ export async function POST(request, { params }) {
switch (toolId) {
case "continue":
return await saveContinueConfig({ baseUrl, apiKey, model });
case "opencode":
// (#524) OpenCode config was never saved because only 'continue' was handled here.
// opencode reads ~/.config/opencode/config.toml — write the OmniRoute settings there.
return await saveOpenCodeConfig({ baseUrl, apiKey, model });
default:
return NextResponse.json(
{ error: `Direct config save not supported for: ${toolId}` },
@@ -125,3 +129,56 @@ async function saveContinueConfig({ baseUrl, apiKey, model }) {
configPath,
});
}
/**
* Save OpenCode config to ~/.config/opencode/config.toml (XDG_CONFIG_HOME aware).
* (#524) OpenCode was silently failing because this handler was missing.
*/
async function saveOpenCodeConfig({ baseUrl, apiKey, model }) {
const { apiPort } = getRuntimePorts();
// Honour $XDG_CONFIG_HOME if set, otherwise use ~/.config per the XDG Base Directory spec
const xdgConfigHome = process.env.XDG_CONFIG_HOME || path.join(os.homedir(), ".config");
const configPath = path.join(xdgConfigHome, "opencode", "config.toml");
const configDir = path.dirname(configPath);
// Ensure ~/.config/opencode/ exists
await fs.mkdir(configDir, { recursive: true });
const normalizedBaseUrl = String(baseUrl || "")
.trim()
.replace(/\/+$/, "");
// Read existing TOML to preserve any user settings outside our block
let existingContent = "";
try {
existingContent = await fs.readFile(configPath, "utf-8");
} catch {
// File doesn't exist yet — start fresh
}
// Build the OmniRoute TOML block.
// opencode config.toml uses the [provider.X] table format.
void apiPort; // available for future port-based detection
const omniBlock = `
# OmniRoute managed updated automatically by OmniRoute CLI Tools
[provider.omniroute]
api_key = "${apiKey || "sk_omniroute"}"
base_url = "${normalizedBaseUrl}"
model = "${model}"
`;
// Remove old OmniRoute-managed block (if any) then append fresh one
const cleanedContent = existingContent
.replace(/\n?# OmniRoute managed[\s\S]*?(?=\n\[|$)/, "")
.trimEnd();
const newContent = (cleanedContent ? cleanedContent + "\n" : "") + omniBlock;
await fs.writeFile(configPath, newContent, "utf-8");
return NextResponse.json({
success: true,
message: `OpenCode config saved to ${configPath}`,
configPath,
});
}
@@ -12,6 +12,7 @@ import { createBackup } from "@/shared/services/backupService";
import { saveCliToolLastConfigured, deleteCliToolLastConfigured } from "@/lib/db/cliToolState";
import { cliModelConfigSchema } from "@/shared/validation/schemas";
import { isValidationFailure, validateBody } from "@/shared/validation/helpers";
import { getApiKeyById } from "@/lib/localDb";
const getOpenClawSettingsPath = () => getCliPrimaryConfigPath("openclaw");
const getOpenClawDir = () => path.dirname(getOpenClawSettingsPath());
@@ -101,7 +102,18 @@ export async function POST(request: Request) {
if (isValidationFailure(validation)) {
return NextResponse.json({ error: validation.error }, { status: 400 });
}
const { baseUrl, apiKey, model } = validation.data;
let { baseUrl, apiKey, model } = validation.data;
// (#526) Resolve real key from DB if keyId was provided
const keyId = typeof rawBody?.keyId === "string" ? rawBody.keyId.trim() : null;
if (keyId) {
try {
const keyRecord = await getApiKeyById(keyId);
if (keyRecord?.key) apiKey = keyRecord.key as string;
} catch {
/* non-critical */
}
}
const openclawDir = getOpenClawDir();
const settingsPath = getOpenClawSettingsPath();
+19 -6
View File
@@ -6,6 +6,7 @@ import {
updateCustomModel,
getModelCompatOverrides,
mergeModelCompatOverride,
type ModelCompatPatch,
} from "@/lib/localDb";
import {
AI_PROVIDERS,
@@ -129,6 +130,7 @@ export async function PUT(request) {
supportedEndpoints,
normalizeToolCallId,
preserveOpenAIDeveloperRole,
compatByProtocol,
} = validation.data;
const raw = rawBody as Record<string, unknown>;
@@ -139,6 +141,9 @@ export async function PUT(request) {
if ("normalizeToolCallId" in raw) updates.normalizeToolCallId = normalizeToolCallId;
if ("preserveOpenAIDeveloperRole" in raw)
updates.preserveOpenAIDeveloperRole = preserveOpenAIDeveloperRole;
if ("compatByProtocol" in raw && compatByProtocol !== undefined) {
updates.compatByProtocol = compatByProtocol;
}
const model = await updateCustomModel(provider, modelId, updates);
@@ -147,9 +152,17 @@ export async function PUT(request) {
const compatOnly =
rawKeys.length > 0 &&
rawKeys.every((k) =>
["provider", "modelId", "normalizeToolCallId", "preserveOpenAIDeveloperRole"].includes(k)
[
"provider",
"modelId",
"normalizeToolCallId",
"preserveOpenAIDeveloperRole",
"compatByProtocol",
].includes(k)
) &&
("normalizeToolCallId" in raw || "preserveOpenAIDeveloperRole" in raw);
("normalizeToolCallId" in raw ||
"preserveOpenAIDeveloperRole" in raw ||
"compatByProtocol" in raw);
if (compatOnly) {
const knownProvider =
!!provider &&
@@ -165,10 +178,7 @@ export async function PUT(request) {
{ status: 400 }
);
}
const patch: {
normalizeToolCallId?: boolean;
preserveOpenAIDeveloperRole?: boolean | null;
} = {};
const patch: ModelCompatPatch = {};
if ("normalizeToolCallId" in raw && typeof normalizeToolCallId === "boolean") {
patch.normalizeToolCallId = normalizeToolCallId;
}
@@ -178,6 +188,9 @@ export async function PUT(request) {
? preserveOpenAIDeveloperRole
: undefined;
}
if ("compatByProtocol" in raw && compatByProtocol && typeof compatByProtocol === "object") {
patch.compatByProtocol = compatByProtocol;
}
if (Object.keys(patch).length > 0) {
mergeModelCompatOverride(provider, modelId, patch);
}
+37 -3
View File
@@ -1,4 +1,8 @@
import { getProviderConnectionById, updateProviderConnection, resolveProxyForConnection } from "@/lib/localDb";
import {
getProviderConnectionById,
updateProviderConnection,
resolveProxyForConnection,
} from "@/lib/localDb";
import { getMachineId } from "@/shared/utils/machine";
import { getUsageForProvider } from "@omniroute/open-sse/services/usage.ts";
import { getExecutor } from "@omniroute/open-sse/executors/index.ts";
@@ -109,7 +113,10 @@ async function refreshAndUpdateCredentials(connection: any) {
/**
* GET /api/usage/[connectionId] - Get usage data for a specific connection
*/
export async function GET(request: Request, { params }: { params: Promise<{ connectionId: string }> }) {
export async function GET(
request: Request,
{ params }: { params: Promise<{ connectionId: string }> }
) {
try {
const { connectionId } = await params;
@@ -155,7 +162,34 @@ export async function GET(request: Request, { params }: { params: Promise<{ conn
// Populate quota cache for quota-aware account selection
if (isRecord(usage?.quotas)) {
setQuotaCache(connectionId, connection.provider, usage.quotas);
setQuotaCache(
connectionId,
connection.provider as string,
usage.quotas as Record<string, unknown>
);
}
// (#491) If the live usage check returned an auth error, sync the expired status
// back to the DB so the Providers page reflects the same degraded state as
// Limits & Quotas (which performs the live check).
const errorMessage = typeof usage?.message === "string" ? usage.message.toLowerCase() : "";
const isAuthError =
errorMessage.includes("token expired") ||
errorMessage.includes("access denied") ||
errorMessage.includes("re-authenticate") ||
errorMessage.includes("unauthorized");
if (isAuthError && connection.testStatus !== "expired") {
try {
await updateProviderConnection(connection.id as string, {
testStatus: "expired",
lastErrorType: "token_expired",
lastErrorAt: new Date().toISOString(),
});
} catch (dbErr) {
// Non-critical: log but don't block the response
console.error("[Usage API] Failed to sync expired status to DB:", dbErr);
}
}
return Response.json(usage);
+3 -1
View File
@@ -160,6 +160,7 @@ export async function POST(request) {
// Resolve provider config — dynamic first (local override), then hardcoded
let providerConfig: EmbeddingProvider | null =
dynamicProviders.find((dp) => dp.id === provider) || getEmbeddingProvider(provider) || null;
let credentialsProviderId = provider;
// #496: Fallback — resolve from ALL provider_nodes (not just localhost)
// This enables custom embedding models (e.g. google/gemini-embedding-001) whose
@@ -180,6 +181,7 @@ export async function POST(request) {
authHeader: "bearer",
models: [],
};
credentialsProviderId = matchingNode.id || provider;
log.info(
"EMBED",
`Resolved custom embedding provider: ${provider}${providerConfig.baseUrl}`
@@ -200,7 +202,7 @@ export async function POST(request) {
// Get credentials — skip for local providers (authType: "none")
let credentials = null;
if (providerConfig && providerConfig.authType !== "none") {
credentials = await getProviderCredentials(provider);
credentials = await getProviderCredentials(credentialsProviderId);
if (!credentials) {
return errorResponse(
HTTP_STATUS.BAD_REQUEST,
+103
View File
@@ -0,0 +1,103 @@
/**
* GET /api/v1/search/analytics
*
* Returns search request statistics from call_logs (request_type = 'search').
* Includes provider breakdown, cache hit rate, cost summary, and error count.
*/
import { NextResponse } from "next/server";
import { getDbInstance } from "@/lib/db/core";
import { enforceApiKeyPolicy } from "@/shared/utils/apiKeyPolicy";
export async function GET(req: Request) {
const policy = await enforceApiKeyPolicy(req, "analytics");
if (policy.rejection) return policy.rejection;
try {
const db = getDbInstance();
// Total search requests
const totalRow = db
.prepare(`SELECT COUNT(*) as cnt FROM call_logs WHERE request_type = 'search'`)
.get() as { cnt: number };
const total = totalRow?.cnt ?? 0;
// Today's searches (UTC date)
const todayStart = new Date();
todayStart.setUTCHours(0, 0, 0, 0);
const todayRow = db
.prepare(
`SELECT COUNT(*) as cnt FROM call_logs WHERE request_type = 'search' AND timestamp >= ?`
)
.get(todayStart.toISOString()) as { cnt: number };
const today = todayRow?.cnt ?? 0;
// Errors
const errRow = db
.prepare(
`SELECT COUNT(*) as cnt FROM call_logs WHERE request_type = 'search' AND (status >= 400 OR error IS NOT NULL)`
)
.get() as { cnt: number };
const errors = errRow?.cnt ?? 0;
// Avg duration
const durRow = db
.prepare(
`SELECT AVG(duration) as avg FROM call_logs WHERE request_type = 'search' AND duration > 0`
)
.get() as { avg: number | null };
const avgDurationMs = Math.round(durRow?.avg ?? 0);
// Per-provider breakdown (provider column stores search provider id)
const provRows = db
.prepare(
`SELECT provider, COUNT(*) as cnt
FROM call_logs WHERE request_type = 'search'
GROUP BY provider ORDER BY cnt DESC`
)
.all() as Array<{ provider: string; cnt: number }>;
// Cost per search provider (matching searchRegistry.ts rates)
const COST_PER_QUERY: Record<string, number> = {
"serper-search": 0.001,
"brave-search": 0.003,
"perplexity-search": 0.005,
"exa-search": 0.01,
"tavily-search": 0.004,
};
const byProvider: Record<string, { count: number; costUsd: number }> = {};
let totalCostUsd = 0;
for (const row of provRows) {
const cost = (COST_PER_QUERY[row.provider] ?? 0.001) * row.cnt;
byProvider[row.provider] = { count: row.cnt, costUsd: cost };
totalCostUsd += cost;
}
// Cached: very fast responses (< 5ms) indicate cache hits
const cachedRow = db
.prepare(
`SELECT COUNT(*) as cnt FROM call_logs
WHERE request_type = 'search' AND duration > 0 AND duration < 5`
)
.get() as { cnt: number };
const cached = cachedRow?.cnt ?? 0;
const cacheHitRate = total > 0 ? Math.round((cached / total) * 100) : 0;
return NextResponse.json({
total,
today,
cached,
errors,
totalCostUsd,
byProvider,
cacheHitRate,
avgDurationMs,
last24h: [],
});
} catch (err: unknown) {
const msg = err instanceof Error ? err.message : String(err);
console.error("[/api/v1/search/analytics]", msg);
return NextResponse.json({ error: "Internal server error" }, { status: 500 });
}
}
+5
View File
@@ -69,6 +69,11 @@ export default function LoginPage() {
router.refresh();
} else {
const data = await res.json();
// (#521) If no password is set, redirect to onboarding instead of showing an error
if (data.needsSetup) {
router.push("/dashboard/onboarding");
return;
}
setError(data.error || t("invalidPassword"));
}
} catch (err) {
+6
View File
@@ -281,6 +281,7 @@
"failedUpdatePermissionsRetry": "Failed to update permissions. Please try again.",
"unknownProvider": "unknown",
"copyMaskedKey": "Copy masked key",
"keyOnlyAvailableAtCreation": "Full key available only at creation time — copy it when you first create the key",
"modelsCount": "{count, plural, one {# model} other {# models}}",
"lastUsedOn": "Last: {date}",
"editPermissions": "Edit permissions",
@@ -1437,6 +1438,11 @@
"compatDeveloperShort": "Developer role",
"compatDoNotPreserveDeveloper": "Do not preserve developer role",
"compatBadgeNoPreserve": "No preserve",
"compatProtocolLabel": "Client request protocol",
"compatProtocolHint": "These options apply when OmniRoute detects this request shape (OpenAI Chat, Responses API, or Anthropic Messages).",
"compatProtocolOpenAI": "OpenAI Chat Completions",
"compatProtocolOpenAIResponses": "OpenAI Responses API",
"compatProtocolClaude": "Anthropic Messages",
"modelId": "Model ID",
"customModelPlaceholder": "e.g. gpt-4.5-turbo",
"loading": "Loading...",
+5
View File
@@ -1432,6 +1432,11 @@
"compatDeveloperShort": "Developer 角色",
"compatDoNotPreserveDeveloper": "不保留 developer 角色",
"compatBadgeNoPreserve": "不保留",
"compatProtocolLabel": "客户端请求协议",
"compatProtocolHint": "以下选项在 OmniRoute 识别到该请求形态(OpenAI Chat、Responses API 或 Anthropic Messages)时生效。",
"compatProtocolOpenAI": "OpenAI Chat Completions",
"compatProtocolOpenAIResponses": "OpenAI Responses API",
"compatProtocolClaude": "Anthropic Messages",
"modelId": "模型 ID",
"customModelPlaceholder": "例如:gpt-4.5-turbo",
"loading": "正在加载...",
+35 -2
View File
@@ -38,6 +38,8 @@ interface ApiKeyMetadata {
autoResolve: boolean;
isActive: boolean;
accessSchedule: AccessSchedule | null;
maxRequestsPerDay: number | null;
maxRequestsPerMinute: number | null;
}
interface ApiKeyRow extends JsonRecord {
@@ -187,6 +189,14 @@ function ensureApiKeysColumns(db: ApiKeysDbLike) {
db.exec("ALTER TABLE api_keys ADD COLUMN access_schedule TEXT");
console.log("[DB] Added api_keys.access_schedule column");
}
if (!columnNames.has("max_requests_per_day")) {
db.exec("ALTER TABLE api_keys ADD COLUMN max_requests_per_day INTEGER");
console.log("[DB] Added api_keys.max_requests_per_day column");
}
if (!columnNames.has("max_requests_per_minute")) {
db.exec("ALTER TABLE api_keys ADD COLUMN max_requests_per_minute INTEGER");
console.log("[DB] Added api_keys.max_requests_per_minute column");
}
_schemaChecked = true;
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
@@ -212,7 +222,7 @@ function getPreparedStatements(db: ApiKeysDbLike): ApiKeysStatements {
_stmtGetKeyById = db.prepare<ApiKeyRow>("SELECT * FROM api_keys WHERE id = ?");
_stmtValidateKey = db.prepare<JsonRecord>("SELECT 1 FROM api_keys WHERE key = ?");
_stmtGetKeyMetadata = db.prepare<ApiKeyRow>(
"SELECT id, name, machine_id, allowed_models, allowed_connections, no_log, auto_resolve, is_active, access_schedule FROM api_keys WHERE key = ?"
"SELECT id, name, machine_id, allowed_models, allowed_connections, no_log, auto_resolve, is_active, access_schedule, max_requests_per_day, max_requests_per_minute FROM api_keys WHERE key = ?"
);
_stmtInsertKey = db.prepare(
"INSERT INTO api_keys (id, name, key, machine_id, allowed_models, no_log, created_at) VALUES (?, ?, ?, ?, ?, ?, ?)"
@@ -406,6 +416,8 @@ export async function updateApiKeyPermissions(
autoResolve?: boolean;
isActive?: boolean;
accessSchedule?: AccessSchedule | null;
maxRequestsPerDay?: number | null;
maxRequestsPerMinute?: number | null;
}
) {
const db = getDbInstance() as ApiKeysDbLike;
@@ -422,6 +434,8 @@ export async function updateApiKeyPermissions(
autoResolve: update.autoResolve,
isActive: update.isActive,
accessSchedule: update.accessSchedule,
maxRequestsPerDay: update.maxRequestsPerDay,
maxRequestsPerMinute: update.maxRequestsPerMinute,
};
if (
@@ -431,7 +445,9 @@ export async function updateApiKeyPermissions(
normalized.noLog === undefined &&
normalized.autoResolve === undefined &&
normalized.isActive === undefined &&
normalized.accessSchedule === undefined
normalized.accessSchedule === undefined &&
normalized.maxRequestsPerDay === undefined &&
normalized.maxRequestsPerMinute === undefined
) {
return false;
}
@@ -446,6 +462,8 @@ export async function updateApiKeyPermissions(
autoResolve?: number;
isActive?: number;
accessSchedule?: string | null;
maxRequestsPerDay?: number | null;
maxRequestsPerMinute?: number | null;
} = { id };
if (normalized.name !== undefined) {
@@ -486,6 +504,16 @@ export async function updateApiKeyPermissions(
normalized.accessSchedule !== null ? JSON.stringify(normalized.accessSchedule) : null;
}
if (normalized.maxRequestsPerDay !== undefined) {
updates.push("max_requests_per_day = @maxRequestsPerDay");
params.maxRequestsPerDay = normalized.maxRequestsPerDay;
}
if (normalized.maxRequestsPerMinute !== undefined) {
updates.push("max_requests_per_minute = @maxRequestsPerMinute");
params.maxRequestsPerMinute = normalized.maxRequestsPerMinute;
}
const result = db.prepare(`UPDATE api_keys SET ${updates.join(", ")} WHERE id = @id`).run(params);
if (result.changes === 0) return false;
@@ -574,6 +602,9 @@ export async function getApiKeyMetadata(
const machineIdRaw = record.machine_id ?? record.machineId;
const metadataMachineId = typeof machineIdRaw === "string" ? machineIdRaw : null;
const rawMaxRPD = record.max_requests_per_day ?? record.maxRequestsPerDay;
const rawMaxRPM = record.max_requests_per_minute ?? record.maxRequestsPerMinute;
const metadata: ApiKeyMetadata = {
id: metadataId,
name: metadataName,
@@ -586,6 +617,8 @@ export async function getApiKeyMetadata(
autoResolve: parseAutoResolve(record.auto_resolve ?? record.autoResolve),
isActive: parseIsActive(record.is_active ?? record.isActive),
accessSchedule: parseAccessSchedule(record.access_schedule ?? record.accessSchedule),
maxRequestsPerDay: typeof rawMaxRPD === "number" && rawMaxRPD > 0 ? rawMaxRPD : null,
maxRequestsPerMinute: typeof rawMaxRPM === "number" && rawMaxRPM > 0 ? rawMaxRPM : null,
};
if (!metadata.id) {
+134 -8
View File
@@ -4,16 +4,60 @@
import { getDbInstance } from "./core";
import { backupDbFile } from "./backup";
import {
MODEL_COMPAT_PROTOCOL_KEYS,
type ModelCompatProtocolKey,
} from "@/shared/constants/modelCompat";
type JsonRecord = Record<string, unknown>;
/** Built-in / alias models: tool-call + developer-role flags without a full custom row */
const MODEL_COMPAT_NAMESPACE = "modelCompatOverrides";
export { MODEL_COMPAT_PROTOCOL_KEYS, type ModelCompatProtocolKey };
export type ModelCompatPerProtocol = {
normalizeToolCallId?: boolean;
preserveOpenAIDeveloperRole?: boolean;
};
type CompatByProtocolMap = Partial<Record<ModelCompatProtocolKey, ModelCompatPerProtocol>>;
function isCompatProtocolKey(p: string): p is ModelCompatProtocolKey {
return (MODEL_COMPAT_PROTOCOL_KEYS as readonly string[]).includes(p);
}
function deepMergeCompatByProtocol(
prev: CompatByProtocolMap | undefined,
patch: Partial<Record<ModelCompatProtocolKey, Partial<ModelCompatPerProtocol>>>
): CompatByProtocolMap {
const out: CompatByProtocolMap = { ...(prev || {}) };
for (const key of Object.keys(patch) as ModelCompatProtocolKey[]) {
if (!isCompatProtocolKey(key)) continue;
const deltas = patch[key];
if (!deltas || typeof deltas !== "object") continue;
const hasDelta =
Object.prototype.hasOwnProperty.call(deltas, "normalizeToolCallId") ||
Object.prototype.hasOwnProperty.call(deltas, "preserveOpenAIDeveloperRole");
if (!hasDelta) continue;
const cur: ModelCompatPerProtocol = { ...(out[key] || {}) };
if ("normalizeToolCallId" in deltas) {
cur.normalizeToolCallId = Boolean(deltas.normalizeToolCallId);
}
if ("preserveOpenAIDeveloperRole" in deltas) {
cur.preserveOpenAIDeveloperRole = Boolean(deltas.preserveOpenAIDeveloperRole);
}
if (Object.keys(cur).length === 0) delete out[key];
else out[key] = cur;
}
return out;
}
export type ModelCompatOverride = {
id: string;
normalizeToolCallId?: boolean;
preserveOpenAIDeveloperRole?: boolean;
compatByProtocol?: CompatByProtocolMap;
};
function readCompatList(providerId: string): ModelCompatOverride[] {
@@ -52,13 +96,24 @@ export function getModelCompatOverrides(providerId: string): ModelCompatOverride
return readCompatList(providerId);
}
export type ModelCompatPatch = {
normalizeToolCallId?: boolean;
preserveOpenAIDeveloperRole?: boolean | null;
compatByProtocol?: CompatByProtocolMap;
};
function compatByProtocolHasEntries(map: CompatByProtocolMap | undefined): boolean {
if (!map || typeof map !== "object") return false;
return Object.keys(map).some((k) => {
const v = map[k as ModelCompatProtocolKey];
return v && typeof v === "object" && Object.keys(v).length > 0;
});
}
export function mergeModelCompatOverride(
providerId: string,
modelId: string,
patch: Partial<{
normalizeToolCallId: boolean;
preserveOpenAIDeveloperRole: boolean | null;
}>
patch: ModelCompatPatch
) {
const list = readCompatList(providerId);
const idx = list.findIndex((e) => e.id === modelId);
@@ -75,9 +130,18 @@ export function mergeModelCompatOverride(
next.preserveOpenAIDeveloperRole = Boolean(patch.preserveOpenAIDeveloperRole);
}
}
if (patch.compatByProtocol && Object.keys(patch.compatByProtocol).length > 0) {
const merged = deepMergeCompatByProtocol(next.compatByProtocol, patch.compatByProtocol);
if (compatByProtocolHasEntries(merged)) next.compatByProtocol = merged;
else delete next.compatByProtocol;
}
const filtered = list.filter((e) => e.id !== modelId);
const hasPreserveFlag = Object.prototype.hasOwnProperty.call(next, "preserveOpenAIDeveloperRole");
if (next.normalizeToolCallId || hasPreserveFlag) {
if (
next.normalizeToolCallId ||
hasPreserveFlag ||
compatByProtocolHasEntries(next.compatByProtocol)
) {
filtered.push(next);
}
writeCompatList(providerId, filtered);
@@ -281,6 +345,23 @@ export async function updateCustomModel(
if (index === -1) return null;
const current = models[index];
const currentCompat = (current as JsonRecord).compatByProtocol as CompatByProtocolMap | undefined;
let mergedCompat: CompatByProtocolMap | undefined = currentCompat;
if (
updates.compatByProtocol !== undefined &&
typeof updates.compatByProtocol === "object" &&
updates.compatByProtocol !== null &&
!Array.isArray(updates.compatByProtocol)
) {
mergedCompat = deepMergeCompatByProtocol(
currentCompat,
updates.compatByProtocol as Partial<
Record<ModelCompatProtocolKey, Partial<ModelCompatPerProtocol>>
>
);
if (!compatByProtocolHasEntries(mergedCompat)) mergedCompat = undefined;
}
const next: JsonRecord = {
...current,
...(updates.modelName !== undefined ? { name: updates.modelName || current.name } : {}),
@@ -299,6 +380,13 @@ export async function updateCustomModel(
next.preserveOpenAIDeveloperRole = Boolean(updates.preserveOpenAIDeveloperRole);
}
}
if (updates.compatByProtocol !== undefined) {
if (mergedCompat && compatByProtocolHasEntries(mergedCompat)) {
next.compatByProtocol = mergedCompat;
} else {
delete next.compatByProtocol;
}
}
models[index] = next;
@@ -335,11 +423,33 @@ function getCustomModelRow(providerId: string, modelId: string): JsonRecord | nu
/**
* Whether the given provider/model has "normalize tool call id" (9-char Mistral-style) enabled.
* Custom model row wins; otherwise {@link getModelCompatOverrides}.
* When `sourceFormat` is one of `openai` | `openai-responses` | `claude`, per-protocol
* `compatByProtocol[sourceFormat].normalizeToolCallId` overrides the legacy top-level flag.
*/
export function getModelNormalizeToolCallId(providerId: string, modelId: string): boolean {
export function getModelNormalizeToolCallId(
providerId: string,
modelId: string,
sourceFormat?: string | null
): boolean {
const m = getCustomModelRow(providerId, modelId);
if (m) return Boolean(m.normalizeToolCallId);
const protocol = sourceFormat && isCompatProtocolKey(sourceFormat) ? sourceFormat : null;
if (m) {
if (protocol) {
const pc = (m.compatByProtocol as CompatByProtocolMap | undefined)?.[protocol];
if (pc && Object.prototype.hasOwnProperty.call(pc, "normalizeToolCallId")) {
return Boolean(pc.normalizeToolCallId);
}
}
return Boolean(m.normalizeToolCallId);
}
const co = readCompatList(providerId).find((e) => e.id === modelId);
if (protocol && co?.compatByProtocol?.[protocol]) {
const pc = co.compatByProtocol[protocol]!;
if (Object.prototype.hasOwnProperty.call(pc, "normalizeToolCallId")) {
return Boolean(pc.normalizeToolCallId);
}
}
return Boolean(co?.normalizeToolCallId);
}
@@ -347,19 +457,35 @@ export function getModelNormalizeToolCallId(providerId: string, modelId: string)
* Explicit preserve-openai-developer preference for this provider/model.
* `undefined` = unset routing keeps legacy default (preserve developer for OpenAI format).
* `false` = map developer system (e.g. MiniMax). `true` = keep developer.
* Per-protocol overrides live under `compatByProtocol[sourceFormat]` when `sourceFormat` matches.
*/
export function getModelPreserveOpenAIDeveloperRole(
providerId: string,
modelId: string
modelId: string,
sourceFormat?: string | null
): boolean | undefined {
const m = getCustomModelRow(providerId, modelId);
const protocol = sourceFormat && isCompatProtocolKey(sourceFormat) ? sourceFormat : null;
if (m) {
if (protocol) {
const pc = (m.compatByProtocol as CompatByProtocolMap | undefined)?.[protocol];
if (pc && Object.prototype.hasOwnProperty.call(pc, "preserveOpenAIDeveloperRole")) {
return Boolean(pc.preserveOpenAIDeveloperRole);
}
}
if (Object.prototype.hasOwnProperty.call(m, "preserveOpenAIDeveloperRole")) {
return Boolean(m.preserveOpenAIDeveloperRole);
}
return undefined;
}
const co = readCompatList(providerId).find((e) => e.id === modelId);
if (protocol && co?.compatByProtocol?.[protocol]) {
const pc = co.compatByProtocol[protocol]!;
if (Object.prototype.hasOwnProperty.call(pc, "preserveOpenAIDeveloperRole")) {
return Boolean(pc.preserveOpenAIDeveloperRole);
}
}
if (co && Object.prototype.hasOwnProperty.call(co, "preserveOpenAIDeveloperRole")) {
return Boolean(co.preserveOpenAIDeveloperRole);
}
+2
View File
@@ -48,6 +48,8 @@ export {
getModelPreserveOpenAIDeveloperRole,
} from "./db/models";
export type { ModelCompatPerProtocol, ModelCompatPatch } from "./db/models";
export {
// Combos
getCombos,
+6 -1
View File
@@ -610,7 +610,12 @@ export async function validateProviderApiKey({ provider, apiKey, providerSpecifi
}
const modelId = entry.models?.[0]?.id || null;
const baseUrl = resolveBaseUrl(entry, providerSpecificData);
// (#532) Use testKeyBaseUrl if defined — some providers validate keys on a different endpoint
// than where requests are sent (e.g. opencode-go validates on zen/v1, not zen/go/v1)
const validationEntry = entry.testKeyBaseUrl
? { ...entry, baseUrl: entry.testKeyBaseUrl }
: entry;
const baseUrl = resolveBaseUrl(validationEntry, providerSpecificData);
try {
if (OPENAI_LIKE_FORMATS.has(entry.format)) {
+9
View File
@@ -0,0 +1,9 @@
/**
* Model compatibility protocol keys shared between client UI and server.
* Must not import Node or DB code so client components can import safely.
*/
/** Client request shapes from detectFormat — compat options apply when the client uses this protocol */
export const MODEL_COMPAT_PROTOCOL_KEYS = ["openai", "openai-responses", "claude"] as const;
export type ModelCompatProtocolKey = (typeof MODEL_COMPAT_PROTOCOL_KEYS)[number];
+94 -1
View File
@@ -161,11 +161,14 @@ export const APIKEY_PROVIDERS = {
gemini: {
id: "gemini",
alias: "gemini",
name: "Gemini",
name: "Gemini (Google AI Studio)",
icon: "diamond",
color: "#4285F4",
textIcon: "GE",
website: "https://ai.google.dev",
hasFree: true,
freeNote:
"Free forever: 1,500 req/day for Gemini 2.5 Flash — no credit card, get key at aistudio.google.com",
},
deepseek: {
id: "deepseek",
@@ -231,6 +234,9 @@ export const APIKEY_PROVIDERS = {
color: "#0F6FFF",
textIcon: "TG",
website: "https://www.together.ai",
hasFree: true,
freeNote:
"$25 signup credits + 3 permanently free models: Llama 3.3 70B, Vision, DeepSeek-R1 distill",
},
fireworks: {
id: "fireworks",
@@ -490,6 +496,93 @@ export const APIKEY_PROVIDERS = {
website: "https://tavily.com",
authHint: "API key from app.tavily.com (format: tvly-...)",
},
"opencode-zen": {
id: "opencode-zen",
alias: "opencode-zen",
name: "OpenCode Zen",
icon: "opencode",
color: "#6366f1",
website: "https://opencode.ai/zen",
},
"opencode-go": {
id: "opencode-go",
alias: "opencode-go",
name: "OpenCode Go",
icon: "opencode",
color: "#6366f1",
website: "https://opencode.ai/zen/go",
},
alibaba: {
id: "alibaba",
alias: "ali",
name: "Alibaba Cloud (DashScope)",
icon: "cloud_queue",
color: "#FF6600",
textIcon: "AL",
website: "https://dashscope-intl.aliyuncs.com",
hasFree: false,
},
longcat: {
id: "longcat",
alias: "lc",
name: "LongCat AI",
icon: "auto_awesome",
color: "#FF6B9D",
textIcon: "LC",
website: "https://longcat.chat",
hasFree: true,
freeNote:
"50M tokens/day (Flash-Lite) + 500K/day (Chat/Thinking) — 100% free while public beta",
},
pollinations: {
id: "pollinations",
alias: "pol",
name: "Pollinations AI",
icon: "local_florist",
color: "#4CAF50",
textIcon: "PO",
website: "https://pollinations.ai",
hasFree: true,
freeNote:
"No API key needed — access GPT-5, Claude, Gemini, DeepSeek V3, Llama 4 free (1 req/15s)",
},
"cloudflare-ai": {
id: "cloudflare-ai",
alias: "cf",
name: "Cloudflare Workers AI",
icon: "cloud",
color: "#F48120",
textIcon: "CF",
website: "https://developers.cloudflare.com/workers-ai/",
hasFree: true,
freeNote:
"Free 10K Neurons/day: ~150 LLM responses or 500s Whisper audio — edge inference globally",
authHint: "Requires API Token AND Account ID (found at dash.cloudflare.com)",
},
scaleway: {
id: "scaleway",
alias: "scw",
name: "Scaleway AI",
icon: "cloud",
color: "#4F0599",
textIcon: "SCW",
website: "https://www.scaleway.com/en/ai/generative-apis/",
hasFree: true,
freeNote: "1M free tokens for new accounts — EU/GDPR compliant (Paris), Qwen3 235B & Llama 70B",
},
aimlapi: {
id: "aimlapi",
alias: "aiml",
name: "AI/ML API",
icon: "hub",
color: "#6366F1",
textIcon: "AI",
website: "https://aimlapi.com",
hasFree: true,
freeNote:
"$0.025/day free credits — 200+ models (GPT-4o, Claude, Gemini, Llama) via single endpoint",
passthroughModels: true,
},
};
export const OPENAI_COMPATIBLE_PREFIX = "openai-compatible-";
+20 -2
View File
@@ -105,6 +105,24 @@ const CLI_TOOLS: Record<string, any> = {
const isWindows = () => process.platform === "win32";
/**
* (#510) Normalize MSYS2/Git-Bash style paths to Windows-native paths.
* On Windows with Git Bash, 'where claude' may return '/c/Program Files/...'
* instead of 'C:\\Program Files\\...'. Convert these so the path is usable
* by Node's fs and child_process modules.
*/
const normalizeMsys2Path = (p: string): string => {
if (!p || !isWindows()) return p;
// Match /letter/rest-of-path — MSYS2 POSIX-style drive mount
const msys2Match = p.match(/^\/([a-zA-Z])\/(.+)$/);
if (msys2Match) {
const drive = msys2Match[1].toUpperCase();
const rest = msys2Match[2].replace(/\//g, "\\");
return `${drive}:\\${rest}`;
}
return p;
};
const parseBoolean = (value: unknown, defaultValue = true) => {
if (value == null || value === "") return defaultValue;
return !FALSE_VALUES.has(String(value).trim().toLowerCase());
@@ -256,7 +274,7 @@ const locateCommand = async (command: string, env: Record<string, string | undef
const first =
located.stdout
.split(/\r?\n/)
.map((line) => line.trim())
.map((line) => normalizeMsys2Path(line.trim()))
.find(Boolean) || null;
return { installed: !!first, commandPath: first, reason: first ? null : "not_found" };
}
@@ -271,7 +289,7 @@ const locateCommand = async (command: string, env: Record<string, string | undef
const first =
located.stdout
.split(/\r?\n/)
.map((line) => line.trim())
.map((line) => normalizeMsys2Path(line.trim()))
.find(Boolean) || null;
return { installed: !!first, commandPath: first, reason: first ? null : "not_found" };
};
+77
View File
@@ -35,6 +35,8 @@ export interface ApiKeyMetadata {
usedBudget?: number;
isActive?: boolean;
accessSchedule?: AccessSchedule | null;
maxRequestsPerDay?: number | null;
maxRequestsPerMinute?: number | null;
}
/**
@@ -103,6 +105,65 @@ function isWithinSchedule(schedule: AccessSchedule): boolean {
return localMinutes >= fromMinutes && localMinutes < untilMinutes;
}
// ── In-memory request counter for per-key rate limits (#452) ──
/** Sliding-window request timestamps per API key */
const _requestTimestamps = new Map<string, number[]>();
const REQUEST_COUNTER_MAX_KEYS = 5000;
const REQUEST_DAY_MS = 24 * 60 * 60 * 1000;
const REQUEST_MINUTE_MS = 60 * 1000;
/** Record a request and check per-key limits. Returns null if OK, or an error message. */
function checkRequestCountLimits(
apiKeyId: string,
maxPerDay: number | null | undefined,
maxPerMinute: number | null | undefined
): string | null {
if (!maxPerDay && !maxPerMinute) return null;
const now = Date.now();
// Get or create timestamp array for this key
let timestamps = _requestTimestamps.get(apiKeyId);
if (!timestamps) {
timestamps = [];
_requestTimestamps.set(apiKeyId, timestamps);
// Prevent unbounded growth
if (_requestTimestamps.size > REQUEST_COUNTER_MAX_KEYS) {
const firstKey = _requestTimestamps.keys().next().value;
if (firstKey) _requestTimestamps.delete(firstKey);
}
}
// Prune timestamps older than 24h
const dayAgo = now - REQUEST_DAY_MS;
while (timestamps.length > 0 && timestamps[0] < dayAgo) {
timestamps.shift();
}
// Check per-minute limit (before recording this request)
if (maxPerMinute && maxPerMinute > 0) {
const minuteAgo = now - REQUEST_MINUTE_MS;
const recentCount = timestamps.filter((t) => t >= minuteAgo).length;
if (recentCount >= maxPerMinute) {
return `Per-minute request limit exceeded (${maxPerMinute} RPM). Try again in a few seconds.`;
}
}
// Check per-day limit
if (maxPerDay && maxPerDay > 0) {
if (timestamps.length >= maxPerDay) {
return `Daily request limit exceeded (${maxPerDay} RPD). Resets in ${Math.ceil(
(timestamps[0] + REQUEST_DAY_MS - now) / 60000
)} minutes.`;
}
}
// All checks passed — record this request
timestamps.push(now);
return null;
}
export interface ApiKeyPolicyResult {
/** API key string (null if no key provided) */
apiKey: string | null;
@@ -224,5 +285,21 @@ export async function enforceApiKeyPolicy(
}
}
// ── Check 5: Request-count limits (#452) ──
if (apiKeyInfo.id && (apiKeyInfo.maxRequestsPerDay || apiKeyInfo.maxRequestsPerMinute)) {
const limitError = checkRequestCountLimits(
apiKeyInfo.id,
apiKeyInfo.maxRequestsPerDay,
apiKeyInfo.maxRequestsPerMinute
);
if (limitError) {
return {
apiKey,
apiKeyInfo,
rejection: errorResponse(HTTP_STATUS.RATE_LIMITED, limitError),
};
}
}
return { apiKey, apiKeyInfo, rejection: null };
}
+73 -26
View File
@@ -1,39 +1,86 @@
import { execSync, execFileSync } from "child_process";
import { existsSync, readFileSync } from "fs";
/**
* Get raw machine ID using OS-specific methods.
*
* IMPORTANT: We do NOT use `if (process.platform === ...)` branching here.
* Next.js SWC bundler evaluates `process.platform` at BUILD time, so when the
* project is built on Linux, the win32/darwin branches get dead-code-eliminated
* and the Linux fallback (which uses `head`) runs on Windows at runtime.
*
* Instead, we use a try/catch waterfall: try each OS method and fall through
* to the next on failure. The correct method always succeeds on the target OS.
*/
function getMachineIdRaw(): string {
if (process.platform === "win32") {
// Strategy 1: Windows — REG.exe query for MachineGuid
try {
const sysRoot = process.env.SystemRoot || process.env.windir || "C:\\Windows";
const output = execFileSync(
`${sysRoot}\\System32\\REG.exe`,
["QUERY", "HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Cryptography", "/v", "MachineGuid"],
{ encoding: "utf8" }
);
return (
output
.toString()
const regPath = `${sysRoot}\\System32\\REG.exe`;
if (existsSync(regPath)) {
const output = execFileSync(
regPath,
["QUERY", "HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Cryptography", "/v", "MachineGuid"],
{ encoding: "utf8", timeout: 5000 }
);
const id = output
.split("REG_SZ")[1]
?.replace(/\r+|\n+|\s+/gi, "")
?.toLowerCase() ?? ""
);
?.toLowerCase();
if (id && id.length > 8) return id;
}
} catch {
// Not Windows or REG.exe failed — continue
}
if (process.platform === "darwin") {
const output = execSync("ioreg -rd1 -c IOPlatformExpertDevice", { encoding: "utf8" });
return (
output
// Strategy 2: macOS — ioreg IOPlatformUUID
try {
const output = execSync("ioreg -rd1 -c IOPlatformExpertDevice", {
encoding: "utf8",
timeout: 5000,
});
if (output.includes("IOPlatformUUID")) {
const id = output
.split("IOPlatformUUID")[1]
?.split("\n")[0]
?.replace(/\=|\s+|\"/gi, "")
?.toLowerCase() ?? ""
);
?.replace(/=|\s+|"/gi, "")
?.toLowerCase();
if (id && id.length > 8) return id;
}
} catch {
// Not macOS or ioreg not available — continue
}
const output = execSync(
"( cat /var/lib/dbus/machine-id /etc/machine-id 2> /dev/null || hostname ) | head -n 1 || :",
{ encoding: "utf8" }
);
return output
.toString()
.replace(/\r+|\n+|\s+/gi, "")
.toLowerCase();
// Strategy 3: Linux — read machine-id files directly (no `head` or pipe)
try {
for (const filePath of ["/etc/machine-id", "/var/lib/dbus/machine-id"]) {
if (existsSync(filePath)) {
const content = readFileSync(filePath, "utf8").trim().toLowerCase();
if (content.length > 8) return content;
}
}
} catch {
// Files not readable — continue
}
// Strategy 4: Hostname fallback (works on all platforms)
try {
const hostname = execSync("hostname", { encoding: "utf8", timeout: 5000 });
const id = hostname.trim().toLowerCase();
if (id) return id;
} catch {
// hostname failed — continue
}
// Strategy 5: Node.js os.hostname() (no exec needed)
try {
const os = require("os");
return os.hostname().toLowerCase();
} catch {
// Final fallback
}
return "unknown-machine";
}
/**
+10
View File
@@ -340,6 +340,13 @@ export const clearModelAvailabilitySchema = z.object({
model: modelIdSchema,
});
const modelCompatPerProtocolSchema = z
.object({
normalizeToolCallId: z.boolean().optional(),
preserveOpenAIDeveloperRole: z.boolean().optional(),
})
.strict();
export const providerModelMutationSchema = z.object({
provider: z.string().trim().min(1, "provider is required").max(120),
modelId: z.string().trim().min(1, "modelId is required").max(240),
@@ -349,6 +356,9 @@ export const providerModelMutationSchema = z.object({
supportedEndpoints: z.array(z.enum(["chat", "embeddings", "images", "audio"])).default(["chat"]),
normalizeToolCallId: z.boolean().optional(),
preserveOpenAIDeveloperRole: z.boolean().nullable().optional(),
compatByProtocol: z
.record(z.enum(["openai", "openai-responses", "claude"]), modelCompatPerProtocolSchema)
.optional(),
});
const pricingFieldsSchema = z
@@ -106,3 +106,60 @@ test("embeddings route clears stale provider error metadata on success", async (
globalThis.fetch = originalFetch;
}
});
test("embeddings route uses provider node id for compatible provider credentials", async () => {
await resetStorage();
const providerNode = await providersDb.createProviderNode({
id: "openai-compatible-responses-google-embeddings",
type: "openai-compatible",
name: "Gemini Embeddings",
prefix: "google",
apiType: "responses",
baseUrl: "https://generativelanguage.googleapis.com/v1beta/openai",
});
const created = await providersDb.createProviderConnection({
provider: providerNode.id,
authType: "apikey",
email: null,
name: "google-compatible-key",
apiKey: "google-compatible-test-key",
testStatus: "active",
lastError: null,
lastErrorType: "token_refresh_failed",
lastErrorSource: "oauth",
errorCode: "refresh_failed",
rateLimitedUntil: null,
backoffLevel: 2,
});
const originalFetch = globalThis.fetch;
globalThis.fetch = async (url, init) => {
assert.equal(url, "https://generativelanguage.googleapis.com/v1beta/openai/embeddings");
assert.equal(init?.headers?.Authorization, "Bearer google-compatible-test-key");
return Response.json({
data: [{ object: "embedding", index: 0, embedding: [0.1, 0.2] }],
usage: { prompt_tokens: 3, total_tokens: 3 },
});
};
try {
const request = new Request("http://localhost/v1/embeddings", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ model: "google/gemini-embedding-001", input: "hello" }),
});
const response = await embeddingsRoute.POST(request);
assert.equal(response.status, 200);
const updated = await readConnection(created.id);
assert.equal(updated.testStatus, "active");
assert.equal(updated.errorCode, undefined);
assert.equal(updated.lastErrorType, undefined);
assert.equal(updated.lastErrorSource, undefined);
} finally {
globalThis.fetch = originalFetch;
}
});
+189
View File
@@ -0,0 +1,189 @@
import { afterEach, beforeEach, describe, it } from "node:test";
import assert from "node:assert/strict";
const { OpencodeExecutor } = await import("../../open-sse/executors/opencode.ts");
const { PROVIDER_MODELS } = await import("../../open-sse/config/providerModels.ts");
function createMockResponse() {
return new Response(JSON.stringify({ ok: true }), {
status: 200,
headers: { "Content-Type": "application/json" },
});
}
function createInput(model, stream = true, credentials = { apiKey: "test-key" }) {
return {
model,
stream,
credentials,
body: {
model,
stream,
messages: [{ role: "user", content: "hello" }],
},
};
}
function registerModel(provider, model) {
PROVIDER_MODELS[provider] = [...(PROVIDER_MODELS[provider] || []), model];
}
describe("OpencodeExecutor", () => {
let zenExecutor;
let goExecutor;
let fetchCalls;
let originalFetch;
let originalZenModels;
let originalGoModels;
beforeEach(() => {
zenExecutor = new OpencodeExecutor("opencode-zen");
goExecutor = new OpencodeExecutor("opencode-go");
fetchCalls = [];
originalFetch = globalThis.fetch;
originalZenModels = [...(PROVIDER_MODELS["opencode-zen"] || [])];
originalGoModels = [...(PROVIDER_MODELS["opencode-go"] || [])];
globalThis.fetch = async (url, options) => {
fetchCalls.push({ url, options });
return createMockResponse();
};
});
afterEach(() => {
globalThis.fetch = originalFetch;
PROVIDER_MODELS["opencode-zen"] = originalZenModels;
PROVIDER_MODELS["opencode-go"] = originalGoModels;
});
describe("execute", () => {
it("routes opencode zen default models to chat completions", async () => {
const minimaxResult = await zenExecutor.execute(createInput("minimax-m2.5-free"));
assert.equal(minimaxResult.url, "https://opencode.ai/zen/v1/chat/completions");
assert.equal(fetchCalls[0].url, "https://opencode.ai/zen/v1/chat/completions");
const pickleResult = await zenExecutor.execute(createInput("big-pickle"));
assert.equal(pickleResult.url, "https://opencode.ai/zen/v1/chat/completions");
assert.equal(fetchCalls[1].url, "https://opencode.ai/zen/v1/chat/completions");
const nanoResult = await zenExecutor.execute(createInput("gpt-5-nano"));
assert.equal(nanoResult.url, "https://opencode.ai/zen/v1/chat/completions");
assert.equal(fetchCalls[2].url, "https://opencode.ai/zen/v1/chat/completions");
});
it("routes claude target format models to messages endpoint", async () => {
const m27Result = await goExecutor.execute(
createInput("minimax-m2.7", true, { apiKey: "claude-key" })
);
assert.equal(m27Result.url, "https://opencode.ai/zen/go/v1/messages");
assert.equal(fetchCalls[0].url, "https://opencode.ai/zen/go/v1/messages");
assert.equal(m27Result.headers["anthropic-version"], "2023-06-01");
const m25Result = await goExecutor.execute(
createInput("minimax-m2.5", true, { apiKey: "claude-key" })
);
assert.equal(m25Result.url, "https://opencode.ai/zen/go/v1/messages");
assert.equal(fetchCalls[1].url, "https://opencode.ai/zen/go/v1/messages");
assert.equal(m25Result.headers["anthropic-version"], "2023-06-01");
});
it("routes openai responses target format models to responses endpoint", async () => {
registerModel("opencode-zen", {
id: "gpt-5-responses",
name: "GPT 5 Responses",
targetFormat: "openai-responses",
});
const result = await zenExecutor.execute(createInput("gpt-5-responses"));
assert.equal(result.url, "https://opencode.ai/zen/v1/responses");
assert.equal(fetchCalls[0].url, "https://opencode.ai/zen/v1/responses");
});
it("routes gemini streaming requests to streamGenerateContent", async () => {
registerModel("opencode-zen", {
id: "gemini-2.5-pro",
name: "Gemini 2.5 Pro",
targetFormat: "gemini",
});
const result = await zenExecutor.execute(createInput("gemini-2.5-pro"));
assert.equal(
result.url,
"https://opencode.ai/zen/v1/models/gemini-2.5-pro:streamGenerateContent?alt=sse"
);
assert.equal(
fetchCalls[0].url,
"https://opencode.ai/zen/v1/models/gemini-2.5-pro:streamGenerateContent?alt=sse"
);
});
it("routes gemini non streaming requests to generateContent", async () => {
registerModel("opencode-zen", {
id: "gemini-2.5-pro",
name: "Gemini 2.5 Pro",
targetFormat: "gemini",
});
const result = await zenExecutor.execute(createInput("gemini-2.5-pro", false));
assert.equal(result.url, "https://opencode.ai/zen/v1/models/gemini-2.5-pro:generateContent");
assert.equal(
fetchCalls[0].url,
"https://opencode.ai/zen/v1/models/gemini-2.5-pro:generateContent"
);
});
it("falls back to chat completions for unknown models", async () => {
const result = await zenExecutor.execute(createInput("unknown-model"));
assert.equal(result.url, "https://opencode.ai/zen/v1/chat/completions");
assert.equal(fetchCalls[0].url, "https://opencode.ai/zen/v1/chat/completions");
});
it("builds default headers for standard models", async () => {
const result = await zenExecutor.execute(createInput("gpt-5-nano"));
assert.deepEqual(result.headers, {
Authorization: "Bearer test-key",
"Content-Type": "application/json",
Accept: "text/event-stream",
});
assert.deepEqual(fetchCalls[0].options.headers, result.headers);
});
it("adds anthropic version for claude target format", async () => {
const result = await goExecutor.execute(
createInput("minimax-m2.7", true, { apiKey: "claude-key" })
);
assert.deepEqual(result.headers, {
Authorization: "Bearer claude-key",
"Content-Type": "application/json",
"anthropic-version": "2023-06-01",
Accept: "text/event-stream",
});
assert.deepEqual(fetchCalls[0].options.headers, result.headers);
});
it("omits accept header when stream is false", async () => {
const result = await zenExecutor.execute(createInput("big-pickle", false));
assert.deepEqual(result.headers, {
Authorization: "Bearer test-key",
"Content-Type": "application/json",
});
assert.deepEqual(fetchCalls[0].options.headers, result.headers);
});
it("omits authorization when credentials are missing", async () => {
const result = await zenExecutor.execute(createInput("minimax-m2.5-free", true, null));
assert.deepEqual(result.headers, {
"Content-Type": "application/json",
Accept: "text/event-stream",
});
assert.deepEqual(fetchCalls[0].options.headers, result.headers);
});
});
});
@@ -0,0 +1,93 @@
/**
* Unit tests for PR #397 Anthropic-format tools filter fix (#346)
*
* Verifies that tools arriving in Anthropic format (`tool.name` without `.function`)
* are NOT dropped by the empty-name filter in chatCore.ts.
* Before the fix, ALL anthropic-format tools were silently dropped, causing
* `400: tool_choice.any may only be specified while providing tools` from Anthropic.
*/
import { describe, it } from "node:test";
import assert from "node:assert/strict";
// Inline the filter logic from chatCore.ts (lines 225-231 after #397 fix)
function filterEmptyNameTools(tools) {
return tools.filter((tool) => {
const fn = tool.function;
const name = fn?.name ?? tool.name;
return name && String(name).trim().length > 0;
});
}
describe("tools empty-name filter — #346 / PR #397", () => {
it("should keep tools with valid OpenAI format name (tool.function.name)", () => {
const tools = [
{ type: "function", function: { name: "get_weather", description: "Get weather" } },
];
assert.equal(filterEmptyNameTools(tools).length, 1);
});
it("should keep tools with valid Anthropic format name (tool.name)", () => {
const tools = [
{ name: "get_weather", description: "Get weather", input_schema: { type: "object" } },
];
assert.equal(filterEmptyNameTools(tools).length, 1);
});
it("should drop tools with empty OpenAI format name (tool.function.name = '')", () => {
const tools = [{ type: "function", function: { name: "" } }];
assert.equal(filterEmptyNameTools(tools).length, 0);
});
it("should drop tools with empty Anthropic format name (tool.name = '')", () => {
const tools = [{ name: "", description: "Ghost tool", input_schema: { type: "object" } }];
assert.equal(filterEmptyNameTools(tools).length, 0);
});
it("should NOT drop Anthropic-format tools when function wrapper is absent (regression for PR #397)", () => {
// Before fix: fn was undefined, fn?.name was undefined, filter returned false → ALL tools dropped
// After fix: fn?.name ?? tool.name → falls back to tool.name → keeps valid tools
const tools = [
{
name: "search",
description: "Search the web",
input_schema: { type: "object", properties: {} },
},
{ name: "code_exec", description: "Execute code", input_schema: { type: "object" } },
];
const result = filterEmptyNameTools(tools);
assert.equal(result.length, 2, "Both anthropic-format tools should be preserved");
});
it("should handle mixed format tools in the same array", () => {
const tools = [
{ type: "function", function: { name: "openai_tool" } }, // OpenAI format
{ name: "anthropic_tool", input_schema: { type: "object" } }, // Anthropic format
{ type: "function", function: { name: "" } }, // Empty OpenAI — should be dropped
{ name: "", input_schema: { type: "object" } }, // Empty Anthropic — should be dropped
];
const result = filterEmptyNameTools(tools);
assert.equal(result.length, 2, "Should keep 2 valid tools (one of each format)");
assert.ok(
result.some((t) => t.function?.name === "openai_tool"),
"OpenAI tool preserved"
);
assert.ok(
result.some((t) => t.name === "anthropic_tool"),
"Anthropic tool preserved"
);
});
it("should handle tools with whitespace-only names", () => {
const tools = [{ name: " ", input_schema: { type: "object" } }];
assert.equal(filterEmptyNameTools(tools).length, 0);
});
it("should handle null/undefined tool.name gracefully", () => {
const tools = [
{ input_schema: { type: "object" } }, // Neither name nor function
{ name: null, input_schema: { type: "object" } },
];
assert.equal(filterEmptyNameTools(tools).length, 0);
});
});
+2 -1
View File
@@ -34,7 +34,8 @@
"**/*.js",
"**/*.jsx",
".next/types/**/*.ts",
".next/dev/types/**/*.ts"
".next/dev/types/**/*.ts",
".next/dev/dev/types/**/*.ts"
],
"exclude": [
"node_modules",