Compare commits

...

70 Commits

Author SHA1 Message Date
diegosouzapw 69e9bd81e9 chore: release v2.3.7
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
- Cline OAuth base64 decodeURIComponent fix
- OAuth account name normalization (name=email fallback)
- Remove sequential Account N naming
2026-03-12 12:25:17 -03:00
diegosouzapw 26f927f798 fix: replace sequential Account N with stable ID-based fallback for OAuth accounts
Remove Account cntValue+1 sequential naming (confusing when accounts deleted)
Leave name=null when no email → getAccountDisplayName returns Account ID-based label
2026-03-12 12:23:51 -03:00
diegosouzapw 2042dcf991 fix: Cline OAuth base64 parsing + name=email fallback for all OAuth accounts
- cline.ts: add decodeURIComponent before base64 decode to handle URL-encoded codes
- cline.ts: populate name = firstName+lastName || email in mapTokens
- oauth/exchange route: normalize name=email for all providers on exchange/poll/poll-callback
- Fixes: accounts showing Account #ID instead of email in providers dashboard
2026-03-12 12:22:20 -03:00
diegosouzapw 87ffe41d8c fix: i18n sync 29 langs + provider test [object Object] fix
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
- Add cliTools.toolDescriptions.opencode, .kiro, guides.opencode, guides.kiro to en.json
- Sync 1111 missing keys across 29 language files (English fallbacks)
- Fix [object Object] in provider batch test modal:
  normalize data.error object to string before setTestResults()
  and in ProviderTestResultsView rendering
- Bump version to 2.3.6
2026-03-12 11:11:15 -03:00
diegosouzapw 943a9374b4 fix: permanent @swc/helpers MODULE_NOT_FOUND fix (#crash)
Build Electron Desktop App / Validate version (push) Failing after 28s
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
prepublish.mjs: explicitly copy @swc/helpers into standalone app/node_modules
before packaging. npm tarball will always include it.

postinstall.mjs: fallback copy of @swc/helpers from root node_modules into
app/node_modules/@swc/ when missing after npm install -g.

Fixes server crash after npm install -g omniroute.
2026-03-12 10:42:59 -03:00
diegosouzapw 8956ffef73 chore: release v2.3.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
2026-03-12 10:27:45 -03:00
diegosouzapw 4383e7d807 feat(ui): endpoint page music section, fixed action buttons, provider logos
Endpoints page:
- Add Music Generation section (/v1/music/generations) in Media & Multi-Modal category
- Include music models (type=music) in endpointData and total model count
- Transcription section already shows Deepgram/AssemblyAI via allModels filter

Provider action buttons:
- Remove hover-only behavior from connection action buttons (edit/delete/reauth/proxy)
- Remove hover-only behavior from combo action buttons (test/duplicate/proxy/edit/delete)
- Buttons now always visible for better UX

Provider logos (SVG fallback):
- ProviderCard now tries .svg before showing text initials when .png not found
- Add SVG logos: ElevenLabs, Hyperbolic, AssemblyAI, PlayHT, Inworld, NanoBanana
- Add ollama-cloud.png (official Ollama icon)
2026-03-12 10:21:05 -03:00
diegosouzapw 863055768e fix(docker): copy native-binary-compat.mjs into build image
postinstall.mjs imports native-binary-compat.mjs but the Dockerfile
only copied postinstall.mjs, causing ERR_MODULE_NOT_FOUND during npm ci:

  Cannot find module '/app/scripts/native-binary-compat.mjs'
  imported from /app/scripts/postinstall.mjs
2026-03-12 10:11:50 -03:00
diegosouzapw 2c1da9e146 fix(ci): resolve 3 GitHub Actions workflow failures
- docs/openapi.yaml: bump version 2.3.1 → 2.3.3 (fixes check:docs-sync CI step)
- tests/unit/model-parse.test.mjs: add missing 'import {test}' from node:test (fixes ReferenceError in unit tests)
- electron/package.json: convert author to object with email (fixes fpm .deb build: 'Please specify author email')
2026-03-12 10:10:45 -03:00
diegosouzapw 845787ab7f chore(release): v2.3.3
Build Electron Desktop App / Validate version (push) Failing after 37s
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): prevent error boundary crash when Test All fails or times out (PR #330)
2026-03-12 09:56:51 -03:00
Diego Rodrigues de Sa e Souza 1db948e9bb Merge pull request #330 from diegosouzapw/fix/providers-test-all-crash
fix(providers): prevent error boundary crash when Test All fails or times out
2026-03-12 09:56:25 -03:00
diegosouzapw f0d00bcee5 fix(providers): prevent error boundary when 'Test All' times out or returns bad JSON
- Add AbortController (90s timeout) to handleBatchTest fetch
- Add inner try/catch for res.json() — handles truncated/non-JSON responses
- Guard ProviderTestResultsView against null/undefined results (was crashing → error boundary)
- Improve error check: error path now also guards results.results.length === 0
- Add 'providerTestTimeout' i18n key for friendly timeout message
2026-03-12 09:38:40 -03:00
diegosouzapw 1e9a9adbad chore(release): v2.3.2
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
feat(claude): [1m] suffix for 1M extended context (PR #311 @DavyMassoneto)
feat(registry): new models for iFlow, Qwen, Kimi (PR #326 @nyatoru)
fix(cli): postinstall binary copy instead of rebuild (PR #327 @ardaaltinors, fixes #321)
docs: English Remote OAuth guide in README (PR #329, fixes #318)
test: 3 unit tests for parseModel [1m] suffix
2026-03-12 07:00:10 -03:00
Diego Rodrigues de Sa e Souza d87c7c3b8c Merge pull request #311 from DavyMassoneto/fix/merge-duplicates-and-lint-warnings
feat(claude): support [1m] suffix for 1M extended context window
2026-03-12 06:58:57 -03:00
Diego Rodrigues de Sa e Souza eb3c834609 Merge pull request #326 from nyatoru/update/sync-qwen-iflow-model
feat(registry): add new models to the provider registry
2026-03-12 06:58:12 -03:00
Diego Rodrigues de Sa e Souza e53c76081f Merge pull request #327 from ardaaltinors/fix/postinstall-copy-native-binary
fix(cli): fix postinstall native binary rebuild regression (#321)
2026-03-12 06:58:10 -03:00
Diego Rodrigues de Sa e Souza 134316328c Merge pull request #329 from diegosouzapw/fix/issue-318-readme-oauth-en
docs: add English Remote OAuth guide to README (#318)
2026-03-12 06:58:07 -03:00
diegosouzapw 4767561f02 docs: add English translation for Remote OAuth section in README (#318)
The '🔐 OAuth on a Remote Server' guide existed only in Portuguese (#oauth-em-servidor-remoto).
Multiple users (@hijak, @ldsgroups225, @vipinpg) couldn't find it in English.

Changes:
- Full English step-by-step guide added above the existing PT content
- Added 'oauth-on-a-remote-server' anchor (EN) alongside 'oauth-em-servidor-remoto' (PT)
- Portuguese version moved into a collapsible <details> section
- OAuthModal.tsx already updated in v2.3.1 to link to #oauth-on-a-remote-server
2026-03-12 06:56:05 -03:00
Nyaru Toru 2d6b31b606 Update open-sse/config/providerRegistry.ts
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
2026-03-12 15:08:05 +07:00
ardaaltinors a22f0a4e7b fix(cli): address review feedback on native binary detection and postinstall
- Read only first 4096 bytes of binary header instead of entire file
- Add error logging to all catch blocks with specific failure messages
- Separate copy vs dlopen catch blocks in postinstall Strategy 1
- Add archCount sanity cap (max 30) for fat Mach-O parsing
- Distinguish timeout vs rebuild failure in Strategy 2
2026-03-12 10:34:56 +03:00
ardaaltinors 5a244aa12a fix(cli): include native-binary-compat.mjs in published package files
The module is imported by bin/omniroute.mjs but was missing from the
files array in package.json, causing ERR_MODULE_NOT_FOUND on global
installs.
2026-03-12 10:26:16 +03:00
ardaaltinors 69d28bec4d feat(cli): detect native binary platform from file header instead of dlopen
Add native-binary-compat module that reads ELF/Mach-O/PE headers to
determine the actual target platform/arch of the .node binary. This
eliminates the macOS false-positive where dlopen loads a linux-x64
binary without throwing.

- Parse ELF (linux), Mach-O (darwin), and PE (win32) binary formats
- Use header-based check as primary signal, dlopen as secondary
- Update pre-flight check in CLI to use the new module
- Add unit tests for all binary formats and cross-platform scenarios
2026-03-12 10:20:08 +03:00
ardaaltinors c859665c6b fix(cli): copy native binary from root node_modules instead of rebuilding (#321)
The standalone app/ directory created by Next.js only contains runtime
files for better-sqlite3 (no binding.gyp, no source, no prebuild-install),
so `npm rebuild` inside app/ is a no-op. The previous fix (#312) added
exit(1) on rebuild failure, which caused npm to rollback the entire
package installation — leaving users with nothing to fix manually.

New approach:
1. Check if existing binary is already compatible (dlopen)
2. Copy the correctly-built binary from root node_modules/ (npm already
   compiles it for the correct platform during install)
3. Fall back to npm rebuild if root binary is unavailable
4. Warn but don't fail the install if nothing works — the package stays
   installed and the CLI pre-flight check gives a clear error at startup
2026-03-12 10:07:43 +03:00
nyatoru e7b19758f3 feat(registry): add new models to the provider registry 2026-03-12 11:18:16 +08:00
DavyMassoneto 623c63baf6 feat(claude): support [1m] suffix for 1M context window
Parse [1m] suffix from model name (e.g. claude-sonnet-4-6[1m]) and
propagate extendedContext flag through the request pipeline to append
context-1m-2025-08-07 to the Anthropic-Beta header.
2026-03-11 23:53:09 -03:00
diegosouzapw a3ad7c6c2e chore(release): v2.3.1
Build Electron Desktop App / Validate version (push) Failing after 39s
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(ui): translate hardcoded PT-BR text in OAuthModal to English (#314, PR #325)
fix(ts): wrap unknown dataObj fields with toRecord() in usage.ts (Kimi parser)
fix(instrumentation): await getSettings() — property access on Promise (#316 follow-up)
2026-03-11 20:49:37 -03:00
Diego Rodrigues de Sa e Souza afc9362ca5 Merge pull request #325 from diegosouzapw/fix/issue-314-oauth-modal-pt-text
fix(ui): translate hardcoded PT-BR text in OAuthModal to English (#314)
2026-03-11 20:48:31 -03:00
diegosouzapw f6b125e8c2 fix(ui): translate hardcoded PT-BR text in OAuthModal to English (#314)
Two strings were hardcoded in Portuguese regardless of the user's language setting:
1. The redirect_uri_mismatch error message (line ~101)
2. The remote access info banner for Google OAuth providers (line ~515)

Both are now in English. The anchor href is updated from
'#oauth-em-servidor-remoto' to '#oauth-on-a-remote-server' to match
the EN README anchor.
2026-03-11 20:45:45 -03:00
diegosouzapw 5df3c22be8 fix(ts): wrap unknown dataObj fields with toRecord() in usage.ts (Kimi usage parser)
Six TypeScript errors on lines 921/922/925/926/939/948:
- dataObj.five_hour / seven_day are 'unknown', can't be passed directly to
  hasUtilization/createQuotaObject which expect JsonRecord — wrap with toRecord()
- dataObj.user is 'unknown', can't chain .membership?.level — use toRecord() first
2026-03-11 20:45:39 -03:00
diegosouzapw 11a0df5443 fix(instrumentation): await getSettings() — property access on Promise (#316 follow-up)
getSettings() is declared async so calling it without await left
settings as a Promise<Record<string, unknown>>, causing 4 TS errors
when accessing settings.modelAliases in the alias restore block.
2026-03-11 13:07:39 -03:00
diegosouzapw e27a2a0d55 chore(release): v2.3.0
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
fix(aliases): custom model aliases applied to routing + restored on startup (#315 #316, PR #317)
fix(cli): better-sqlite3 postinstall rebuild cross-platform macOS ARM (#312, PR #313 @ardaaltinors)
2026-03-11 12:43:50 -03:00
Diego Rodrigues de Sa e Souza dc8abe60ee Merge pull request #317 from diegosouzapw/fix/issue-315-316-alias-bugs
fix(aliases): resolve custom model aliases before routing + restore on startup (#315, #316)
2026-03-11 12:43:02 -03:00
diegosouzapw afe2ab37e4 fix(aliases): resolve custom model aliases before routing + restore on startup (#315, #316)
#315: Import and call resolveModelAlias() in chatCore.ts before the
getModelTargetFormat() lookup so that custom aliases configured in
Settings → Model Aliases → Pattern→Target are actually applied during
routing instead of being silently ignored.

#316: Load persisted custom model aliases from settings DB at server
startup (instrumentation.ts). Previously _customAliases started as an
empty object after every restart since setCustomAliases() was only
called by the PUT /api/settings/model-aliases handler — never at init.
Now aliases are restored from settings.modelAliases JSON field on boot.
2026-03-11 12:42:18 -03:00
Diego Rodrigues de Sa e Souza f7bd99f965 Merge pull request #313 from ardaaltinors/fix/better-sqlite3-postinstall-rebuild
fix(cli): improve better-sqlite3 postinstall rebuild for cross-platform installs
2026-03-11 12:39:03 -03:00
ardaaltinors f5238944b4 fix(cli): improve better-sqlite3 postinstall rebuild for cross-platform installs (#312)
Replace unreliable process.dlopen() platform detection with explicit
platform/arch comparison against the build target (linux-x64). On macOS,
dlopen can load an incompatible binary without throwing, causing the
postinstall script to skip the rebuild entirely.

- Detect platform mismatch via process.platform/arch instead of dlopen
- Fail the install (exit 1) if rebuild fails, instead of warning silently
- Verify rebuilt binary loads correctly after rebuild
- Add pre-flight binary check in CLI entry point as a safety net
2026-03-11 17:11:00 +03:00
diegosouzapw c7ae9c30c2 chore(release): v2.2.9
Build Electron Desktop App / Validate version (push) Failing after 36s
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): persist custom model endpoint edits (#307, PR #307 by @hijak)
fix(deps): add @swc/helpers as explicit dep to fix MODULE_NOT_FOUND (#306, PR #308)
fix(usage): correct Claude quota display — utilization = % used (#299, PR #309)
2026-03-11 08:46:16 -03:00
Diego Rodrigues de Sa e Souza 82f7a12a46 Merge pull request #309 from diegosouzapw/fix/issue-299-claude-quota-inversion
fix(usage): correct Claude quota display — utilization = % used (#299)
2026-03-11 08:45:05 -03:00
Diego Rodrigues de Sa e Souza f494a8531b Merge pull request #308 from diegosouzapw/fix/issue-306-swc-helpers-missing
fix(deps): add @swc/helpers as explicit dependency (#306)
2026-03-11 08:45:01 -03:00
Diego Rodrigues de Sa e Souza 36ed0499db Merge pull request #307 from hijak/fix/provider-model-endpoints-save
fix(providers): persist supported endpoints with explicit save
2026-03-11 08:44:58 -03:00
diegosouzapw 46cff2200d fix(usage): correct Claude quota display — utilization = % used, not % remaining (#299)
The Claude Code OAuth API returns 'utilization' as percent USED,
not percent remaining. The createQuotaObject function had them swapped:
it set remainingPercentage = utilization, which inverted the quota bar.

Confirmed by reporter: Claude.ai shows 87% used → OmniRoute was showing
87% remaining (green bar), should show 13% remaining (yellow/red bar).

Fix: used = utilization; remaining = 100 - utilization.
2026-03-11 08:42:44 -03:00
diegosouzapw 5ea6ad4a9e fix(deps): add @swc/helpers as explicit dependency (#306)
next@16 lists @swc/helpers@0.5.15 in its own dependencies but npm's
deduplication during global install fails to place it in the omniroute
app's node_modules when hoisted. This causes MODULE_NOT_FOUND for
@swc/helpers/esm/_interop_require_default.js on startup.

Fix: add @swc/helpers@0.5.19 to omniroute's top-level dependencies and
overrides so npm guarantees its presence regardless of hoisting strategy.
Reproducible on Windows (Node 22) and Linux.
2026-03-11 08:40:31 -03:00
jack 6cad4fae8e fix(providers): persist supported endpoints with explicit save for custom models 2026-03-11 11:20:25 +00:00
diegosouzapw 8df24c855b chore(release): v2.2.8
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
fix(docker): healthcheck now uses /api/monitoring/health (#296, PR #301)
fix(rate-limit): maxWait=120s on Bottleneck prevents endless queue (#297, PR #302)
2026-03-11 00:20:57 -03:00
Diego Rodrigues de Sa e Souza f25882c0e9 Merge pull request #302 from diegosouzapw/fix/issue-296-healthcheck-endpoint
fix(docker): use /api/monitoring/health for Docker healthcheck (#296)
2026-03-11 00:20:17 -03:00
Diego Rodrigues de Sa e Souza be6c769192 Merge pull request #301 from diegosouzapw/fix/issue-297-rate-limit-maxwait
fix(rate-limit): prevent endless queue with maxWait (#297)
2026-03-11 00:20:14 -03:00
diegosouzapw a4276444b5 fix(rate-limit): add maxWait to Bottleneck to prevent endless queuing (#297)
When all provider quotas are exhausted (reservoir=0 after repeated 429s),
Bottleneck's schedule() would queue requests indefinitely since no maxWait
was configured. Clients (Cursor, Claude Code, VS Code) would hang forever.

Fix: add maxWait=120000 (2min, configurable via RATE_LIMIT_MAX_WAIT_MS env)
to DEFAULT_SETTINGS and all three Bottleneck constructors. When a job waits
longer than maxWait, Bottleneck rejects with a BottleneckError which
propagates as a 502/503 error to the client — a clean fail-fast instead
of infinite hang.
2026-03-10 23:58:36 -03:00
diegosouzapw 0af27b8d8a fix(docker): use /api/monitoring/health for healthcheck (#296)
The healthcheck script was querying /api/settings which returns config
data rather than system health. Updated to /api/monitoring/health which
is the canonical health endpoint used across tests, SystemMonitor.tsx,
MaintenanceBanner.tsx, playwright config, and MCP tools.
2026-03-10 23:57:17 -03:00
diegosouzapw 542eb0e719 chore(release): v2.2.7
Build Electron Desktop App / Validate version (push) Failing after 31s
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(docker): bootstrap-env.mjs missing in runtime image (#292, PR #293)
fix(google-cli): prefer OAuth projectId over stale body.project (PR #294)
fix(chat): strip empty name from messages/input before upstream (#291, PR #300)
deps: bump hono 4.12.4 → 4.12.7 (PR #298)
2026-03-10 23:34:19 -03:00
Diego Rodrigues de Sa e Souza c658b39270 Merge pull request #300 from diegosouzapw/fix/issue-291-strip-empty-name
fix(chat): strip empty name from messages/input before upstream (#291)
2026-03-10 23:33:04 -03:00
Diego Rodrigues de Sa e Souza 52ef3dfc7e Merge pull request #298 from diegosouzapw/dependabot/npm_and_yarn/hono-4.12.7
deps: bump hono from 4.12.4 to 4.12.7
2026-03-10 23:33:01 -03:00
Diego Rodrigues de Sa e Souza 57da407693 Merge pull request #294 from hijak/fix/google-cli-prefer-oauth-projectid
fix(google-cli): prefer OAuth projectId over request body project
2026-03-10 23:32:59 -03:00
Diego Rodrigues de Sa e Souza d2d6fc5883 Merge pull request #293 from hijak/fix/docker-bootstrap-env-missing
fix(docker): include bootstrap-env.mjs in runtime image
2026-03-10 23:32:57 -03:00
diegosouzapw 6a7a6022d4 fix(chat): strip empty name fields from messages/input before upstream (#291)
OpenAI-compatible providers (OpenAI, Codex) reject name:'' with 400 errors:
  - 'Unknown parameter: input[1].name'
  - 'Invalid tools[0].name: empty string'

Some clients (e.g. PocketPaw) forward assistant turns with name:'' in
the OpenAI Responses API input[] and chat completions messages[].

Fix: filter out name:'' from messages[] and input[] before translateRequest.
Non-empty non-null name values are preserved per OpenAI spec.
2026-03-10 23:31:31 -03:00
dependabot[bot] b53eafa615 deps: bump hono from 4.12.4 to 4.12.7
Bumps [hono](https://github.com/honojs/hono) from 4.12.4 to 4.12.7.
- [Release notes](https://github.com/honojs/hono/releases)
- [Commits](https://github.com/honojs/hono/compare/v4.12.4...v4.12.7)

---
updated-dependencies:
- dependency-name: hono
  dependency-version: 4.12.7
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-11 02:07:19 +00:00
jack c949214e99 feat(google-cli): add env escape hatch for body.project override 2026-03-10 22:15:26 +00:00
jack 887cf25b65 fix(google-cli): prefer OAuth projectId over client body project 2026-03-10 22:12:39 +00:00
jack dd6142196f fix(docker): copy bootstrap-env.mjs into runtime image 2026-03-10 21:55:21 +00:00
diegosouzapw 902c7244d1 chore(release): v2.2.6
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
fix(translator): map Claude thinking_delta to reasoning_content (#289)
- Close #289: thinking tokens now visible in Claude Code, Cursor, Windsurf
2026-03-10 16:21:20 -03:00
Diego Rodrigues de Sa e Souza 4f11762c68 Merge pull request #290 from diegosouzapw/fix/issue-289-thinking-tokens
fix(translator): map Claude thinking_delta to reasoning_content (#289)
2026-03-10 16:20:22 -03:00
diegosouzapw 8a7f7c1ba0 fix(translator): map Claude thinking_delta to reasoning_content not content (#289)
When proxying Claude responses through OmniRoute, thinking blocks were being
emitted as regular content (delta.content) with <think>...</think> XML tags.
Clients like Claude Code, Cursor, and Windsurf look for delta.reasoning_content
to render the thinking panel — not <think> tags inside content.

Root cause (claude-to-openai.ts):
  - content_block_start type:thinking → emitted { content: '<think>' }
  - content_block_delta thinking_delta → emitted { content: delta.thinking }
  - content_block_stop thinking block → emitted { content: '</think>' }

Fix:
  - content_block_start → emits { reasoning_content: '' } (signals block start)
  - thinking_delta → emits { reasoning_content: delta.thinking }
  - content_block_stop → no extra chunk needed (thinking streamed via reasoning_content)

This fix applies when sourceFormat=CLAUDE targetFormat=OPENAI (Antigravity OAuth,
direct Claude API providers). The user reported 'Thinking Budget: passthrough'
was enabled but thinking was invisible — this is the root cause.

Fixes #289
2026-03-10 15:25:31 -03:00
diegosouzapw af46f87eed feat(bootstrap): zero-config auto-generated secrets on first run
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
Resolves root cause of #252 (Electron black screen) and #249 (OAuth fail)
for users running with zero configuration (no .env needed).

New: scripts/bootstrap-env.mjs
- Auto-generates JWT_SECRET (64 bytes), STORAGE_ENCRYPTION_KEY (32 bytes),
  API_KEY_SECRET (32 bytes) if missing or empty
- Persists to {DATA_DIR}/server.env — survives restarts, Docker volume
  remounts, and upgrades without changing secrets
- Reads .env from CWD (user overrides), then merges process.env (highest prio)
- Logs friendly warnings for missing optional OAuth secrets

Updated: run-standalone.mjs + run-next.mjs
- Call bootstrapEnv() before spawning server — covers npm + Docker paths

Updated: electron/main.js (synchronous inline — CJS cannot await import ESM)
- Reads userData/server.env, generates missing secrets with crypto.randomBytes()
- Persists back to server.env, sets OMNIROUTE_BOOTSTRAPPED=true

New: BootstrapBanner.tsx + page.tsx update
- Dismissable amber banner on dashboard home when running in zero-config mode
- Shows where server.env is located and how to customize secrets
2026-03-10 15:15:07 -03:00
diegosouzapw fd749d1e0b fix(electron): auto-generate JWT_SECRET and STORAGE_ENCRYPTION_KEY if missing
In packaged Electron on macOS/Windows/Linux, there is no .env file.
The Next.js server needs JWT_SECRET and STORAGE_ENCRYPTION_KEY to start —
without them it crashes silently, causing ERR_CONNECTION_REFUSED
and a black screen in the Electron window.

Fix: Generate cryptographically random values with crypto.randomBytes()
on first launch, persist them in userData/electron-env.json, and pass
them to the spawned server.js process via the env option.

Root cause: macOS users reported 'app black screen' (#252) and
ERR_CONNECTION_REFUSED — this was the Next.js server crashing at startup
because these env vars don't exist in the desktop OS environment.
2026-03-10 15:06:57 -03:00
diegosouzapw 5046f90dfa docs(workflow): make openapi.yaml sync mandatory in generate-release
- Step 4 now marked ⚠️ MANDATORY with CI will fail warning
- Command is now auto-extracting version from package.json (no manual substitution)
- Step 4 has // turbo annotation for auto-execution
- Added 'Known CI Pitfalls' table: docs-sync failures, Electron fpm, Docker 502
2026-03-10 15:02:08 -03:00
diegosouzapw cf13e95610 fix(ci): bump openapi.yaml version to 2.2.4
check:docs-sync fails when openapi.yaml version != package.json version.
Updating to match after v2.2.4 release.

Systematic fix: openapi.yaml version must always be updated alongside
package.json during releases (see generate-release workflow step 4).
2026-03-10 14:43:17 -03:00
diegosouzapw 5763609008 feat(release): v2.2.4 — CI fixes (docs-sync, electron fpm, docker)
Build Electron Desktop App / Validate version (push) Failing after 26s
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-10 14:37:04 -03:00
diegosouzapw 6d672ab09a fix(ci): docs-sync, electron linux fpm, docker cache env
CI Lint fixes:
- docs/openapi.yaml: bump version 2.2.0 → 2.2.3 (was out of sync with package.json)
- CHANGELOG.md: add '## [Unreleased]' as first section (required by check:docs-sync)

Electron Linux fix:
- electron-release.yml: add 'gem install fpm' step for Linux builds
  fpm is required by electron-builder to package .deb installers;
  ubuntu-latest runners don't have it pre-installed

Docker publish:
- docker-publish.yml: add DOCKER_BUILDKIT_INLINE_CACHE env; prev 502 was
  a transient Docker Hub network error, no code change needed
2026-03-10 14:31:48 -03:00
diegosouzapw ac68022233 feat(release): v2.2.3 — bug fixes from community PRs
Build Electron Desktop App / Validate version (push) Failing after 41s
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(google-cli): remove fake projectId fallback causing permission/verification errors (#285)
- antigravity.ts, openai-to-gemini.ts, geminiHelper.ts
- Throws clear error instead of silently sending with random project IDs

fix(claude): extend empty tool name filter to all message roles (#288)
- Pass 1.4 now covers all roles, not just assistant
- Filters tool_result with missing tool_use_id
- Filters top-level body.tools with empty names
- Explicit @swc/helpers COPY in Dockerfile runner-base stage
2026-03-10 12:53:47 -03:00
Nina Gleichner c2b31f6b20 Fix empty tool name 400 errors from Claude API and missing @swc/helpers in Docker (#288)
* Initial plan

* fix: filter empty tool names and missing tool_use_id; add @swc/helpers to Docker

Co-authored-by: ngleichner1 <263653359+ngleichner1@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: ngleichner1 <263653359+ngleichner1@users.noreply.github.com>
2026-03-10 12:46:17 -03:00
Jack 54b1d8c8de fix(google-cli): stop random project fallback and require real OAuth projectId (#285)
Co-authored-by: jack <jack@plutus-32g.local>
2026-03-10 12:46:15 -03:00
diegosouzapw cd1ab696b2 docs: add npm run system-info to Support and troubleshooting sections
Users are now directed to run 'npm run system-info' when reporting bugs.
Added to:
- ## Support → '🐛 Reporting a Bug?' subsection
- Pain point #10 '🐛 I can't diagnose errors' bullet list
2026-03-10 12:45:20 -03:00
87 changed files with 4000 additions and 2170 deletions
+15 -2
View File
@@ -53,10 +53,14 @@ Keep an empty `## [Unreleased]` section above it.
## [2.x.y] — YYYY-MM-DD
```
### 4. Update openapi.yaml version
### 4. Update openapi.yaml version ⚠️ MANDATORY
> **CI will fail** if `docs/openapi.yaml` version ≠ `package.json` version (`check:docs-sync` enforces this).
// turbo
```bash
sed -i 's/version: OLD/version: NEW/' docs/openapi.yaml
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
@@ -95,3 +99,12 @@ ssh root@<VPS_IP> "npm install -g omniroute@2.x.y && pm2 restart omniroute"
- 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
## 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 - 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 |
+3
View File
@@ -49,6 +49,9 @@ jobs:
${{ env.IMAGE_NAME }}:latest
cache-from: type=gha
cache-to: type=gha,mode=max
no-cache: false
env:
DOCKER_BUILDKIT_INLINE_CACHE: 1
- name: Inspect image
run: |
+4
View File
@@ -107,6 +107,10 @@ jobs:
"
echo "✓ electron/package.json version set to $VERSION_NO_V"
- name: Install fpm (Linux .deb packaging tool)
if: matrix.platform == 'linux'
run: sudo gem install fpm --no-document
- name: Install Electron dependencies
working-directory: electron
run: npm install --no-audit --no-fund
+18 -1867
View File
File diff suppressed because it is too large Load Diff
+4
View File
@@ -3,6 +3,7 @@ WORKDIR /app
COPY package*.json ./
COPY scripts/postinstall.mjs ./scripts/postinstall.mjs
COPY scripts/native-binary-compat.mjs ./scripts/native-binary-compat.mjs
RUN if [ -f package-lock.json ]; then npm ci --no-audit --no-fund; else npm install --no-audit --no-fund; fi
COPY . ./
@@ -29,8 +30,11 @@ RUN mkdir -p /app/data
COPY --from=builder /app/public ./public
COPY --from=builder /app/.next/static ./.next/static
COPY --from=builder /app/.next/standalone ./
# Explicitly copy @swc/helpers — not always traced by standalone output but needed at runtime
COPY --from=builder /app/node_modules/@swc/helpers ./node_modules/@swc/helpers
COPY --from=builder /app/scripts/run-standalone.mjs ./run-standalone.mjs
COPY --from=builder /app/scripts/runtime-env.mjs ./runtime-env.mjs
COPY --from=builder /app/scripts/bootstrap-env.mjs ./bootstrap-env.mjs
COPY --from=builder /app/scripts/healthcheck.mjs ./healthcheck.mjs
EXPOSE 20128
+104 -2
View File
@@ -167,6 +167,16 @@ _Connect any AI-powered IDE or CLI tool through OmniRoute — free API gateway f
- **Contributing**: See [CONTRIBUTING.md](CONTRIBUTING.md), open a PR, or pick a `good first issue`
- **Original Project**: [9router by decolua](https://github.com/decolua/9router)
### 🐛 Reporting a Bug?
When opening an issue, please run the system-info command and attach the generated file:
```bash
npm run system-info
```
This generates a `system-info.txt` with your Node.js version, OmniRoute version, OS details, installed CLI tools (iflow, gemini, claude, codex, antigravity, droid, etc.), Docker/PM2 status, and system packages — everything we need to reproduce your issue quickly. Attach the file directly to your GitHub issue.
---
## 🔄 How It Works
@@ -358,6 +368,7 @@ When a call fails, the dev doesn't know if it was a rate limit, expired token, w
- **Translator Playground** — 4 debugging modes: Playground (format translation), Chat Tester (round-trip), Test Bench (batch), Live Monitor (real-time)
- **Request Telemetry** — p50/p95/p99 latency + X-Request-Id tracing
- **File-Based Logging with Rotation** — Console interceptor captures everything to JSON log with size-based rotation
- **System Info Report** — `npm run system-info` generates `system-info.txt` with your full environment (Node version, OmniRoute version, OS, CLI tools, Docker/PM2 status). Attach it when reporting issues for instant triage.
</details>
@@ -1497,11 +1508,102 @@ opencode
- OmniRoute v1.0.6+ includes fallback validation via chat completions
- Ensure base URL includes `/v1` suffix
### 🔐 OAuth em Servidor Remoto (Remote OAuth Setup)
### 🔐 OAuth on a Remote Server
<a name="oauth-on-a-remote-server"></a>
<a name="oauth-em-servidor-remoto"></a>
> **⚠️ IMPORTANTE para usuários com OmniRoute em VPS/Docker/servidor remoto**
> **⚠️ Important for users running OmniRoute on a VPS, Docker, or any remote server**
#### Why does Antigravity / Gemini CLI OAuth fail on remote servers?
The **Antigravity** and **Gemini CLI** providers use **Google OAuth 2.0**. Google requires the `redirect_uri` in the OAuth flow to exactly match one of the pre-registered URIs in the app's Google Cloud Console.
The OAuth credentials bundled in OmniRoute are registered **for `localhost` only**. When you access OmniRoute on a remote server (e.g. `https://omniroute.myserver.com`), Google rejects the authentication with:
```
Error 400: redirect_uri_mismatch
```
#### Solution: Configure your own OAuth credentials
You need to create an **OAuth 2.0 Client ID** in Google Cloud Console with your server's URI.
#### Step-by-step
**1. Open Google Cloud Console**
Go to: [https://console.cloud.google.com/apis/credentials](https://console.cloud.google.com/apis/credentials)
**2. Create a new OAuth 2.0 Client ID**
- Click **"+ Create Credentials"** → **"OAuth client ID"**
- Application type: **"Web application"**
- Name: anything you like (e.g. `OmniRoute Remote`)
**3. Add Authorized Redirect URIs**
In the **"Authorized redirect URIs"** field, add:
```
https://your-server.com/callback
```
> Replace `your-server.com` with your server's domain or IP (include the port if needed, e.g. `http://45.33.32.156:20128/callback`).
**4. Save and copy the credentials**
After creating, Google will show the **Client ID** and **Client Secret**.
**5. Set environment variables**
In your `.env` (or Docker environment variables):
```bash
# For Antigravity:
ANTIGRAVITY_OAUTH_CLIENT_ID=your-client-id.apps.googleusercontent.com
ANTIGRAVITY_OAUTH_CLIENT_SECRET=GOCSPX-your-secret
# For Gemini CLI:
GEMINI_OAUTH_CLIENT_ID=your-client-id.apps.googleusercontent.com
GEMINI_OAUTH_CLIENT_SECRET=GOCSPX-your-secret
GEMINI_CLI_OAUTH_CLIENT_SECRET=GOCSPX-your-secret
```
**6. Restart OmniRoute**
```bash
# npm:
npm run dev
# Docker:
docker restart omniroute
```
**7. Try connecting again**
Dashboard → Providers → Antigravity (or Gemini CLI) → OAuth
Google will now redirect correctly to `https://your-server.com/callback`.
---
#### Temporary workaround (without custom credentials)
If you don't want to set up your own credentials right now, you can still use the **manual URL flow**:
1. OmniRoute opens the Google authorization URL
2. After authorizing, Google tries to redirect to `localhost` (which fails on the remote server)
3. **Copy the full URL** from your browser's address bar (even if the page doesn't load)
4. Paste that URL into the field shown in the OmniRoute connection modal
5. Click **"Connect"**
> This works because the authorization code in the URL is valid regardless of whether the redirect page loaded.
---
<details>
<summary><b>🇧🇷 Versão em Português</b></summary>
#### Por que o OAuth do Antigravity / Gemini CLI falha em servidores remotos?
+24
View File
@@ -17,6 +17,7 @@ import { existsSync, readFileSync } from "node:fs";
import { join, dirname } from "node:path";
import { fileURLToPath } from "node:url";
import { homedir, platform } from "node:os";
import { isNativeBinaryCompatible } from "../scripts/native-binary-compat.mjs";
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
@@ -193,6 +194,29 @@ if (!existsSync(serverJs)) {
process.exit(1);
}
// ── Pre-flight: verify better-sqlite3 native binary ───────
// Verify the binary's actual target platform/arch before trusting dlopen.
// This avoids the macOS false positive where a bundled linux-x64 addon can
// appear to load even though the runtime will fail when better-sqlite3 starts.
const sqliteBinary = join(
APP_DIR,
"node_modules",
"better-sqlite3",
"build",
"Release",
"better_sqlite3.node"
);
if (existsSync(sqliteBinary) && !isNativeBinaryCompatible(sqliteBinary)) {
console.error(
"\x1b[31m✖ better-sqlite3 native module is incompatible with this platform.\x1b[0m"
);
console.error(` Run: cd ${APP_DIR} && npm rebuild better-sqlite3`);
if (platform() === "darwin") {
console.error(" If build tools are missing: xcode-select --install");
}
process.exit(1);
}
// ── Start server ───────────────────────────────────────────
console.log(` \x1b[2m⏳ Starting server...\x1b[0m\n`);
+1 -1
View File
@@ -1,7 +1,7 @@
openapi: 3.1.0
info:
title: OmniRoute API
version: 2.2.0
version: 2.3.6
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,
+64 -1
View File
@@ -383,6 +383,69 @@ function startNextServer() {
return;
}
// ── Zero-config bootstrap: auto-generate required secrets ─────────────────
// Electron uses CJS — cannot dynamically import ESM bootstrap-env.mjs.
// This mirrors bootstrap-env.mjs logic synchronously:
// 1. Read persisted secrets from userData/server.env
// 2. Generate missing secrets with crypto.randomBytes()
// 3. Persist back to userData/server.env for future restarts
const crypto = require("crypto");
const userDataDir = app.getPath("userData");
const serverEnvPath = path.join(userDataDir, "server.env");
// Parse a simple KEY=VALUE file
function parseEnvFile(filePath) {
if (!fs.existsSync(filePath)) return {};
const env = {};
for (const line of fs.readFileSync(filePath, "utf8").split(/\r?\n/)) {
const t = line.trim();
if (!t || t.startsWith("#")) continue;
const eq = t.indexOf("=");
if (eq < 1) continue;
env[t.slice(0, eq).trim()] = t.slice(eq + 1).trim();
}
return env;
}
const persisted = parseEnvFile(serverEnvPath);
const serverEnv = { ...process.env, ...persisted };
let changed = false;
if (!serverEnv.JWT_SECRET) {
serverEnv.JWT_SECRET = persisted.JWT_SECRET = crypto.randomBytes(64).toString("hex");
changed = true;
console.log("[Electron] ✨ JWT_SECRET auto-generated");
}
if (!serverEnv.STORAGE_ENCRYPTION_KEY) {
serverEnv.STORAGE_ENCRYPTION_KEY = persisted.STORAGE_ENCRYPTION_KEY = crypto
.randomBytes(32)
.toString("hex");
serverEnv.STORAGE_ENCRYPTION_KEY_VERSION = persisted.STORAGE_ENCRYPTION_KEY_VERSION = "v1";
changed = true;
console.log("[Electron] ✨ STORAGE_ENCRYPTION_KEY auto-generated");
}
if (!serverEnv.API_KEY_SECRET) {
serverEnv.API_KEY_SECRET = persisted.API_KEY_SECRET = crypto.randomBytes(32).toString("hex");
changed = true;
console.log("[Electron] ✨ API_KEY_SECRET auto-generated");
}
if (changed) {
serverEnv.OMNIROUTE_BOOTSTRAPPED = "true";
try {
fs.mkdirSync(userDataDir, { recursive: true });
const lines = [
"# Auto-generated by OmniRoute bootstrap",
"",
...Object.entries(persisted).map(([k, v]) => `${k}=${v}`),
"",
];
fs.writeFileSync(serverEnvPath, lines.join("\n"), "utf8");
console.log("[Electron] 📁 Secrets persisted to:", serverEnvPath);
} catch (e) {
console.warn("[Electron] Could not persist secrets:", e.message);
}
}
console.log("[Electron] Starting Next.js server on port", serverPort);
sendToRenderer("server-status", { status: "starting", port: serverPort });
@@ -390,7 +453,7 @@ function startNextServer() {
nextServer = spawn("node", [serverScript], {
cwd: NEXT_SERVER_PATH,
env: {
...process.env,
...serverEnv,
PORT: String(serverPort),
NODE_ENV: "production",
},
+4 -1
View File
@@ -3,7 +3,10 @@
"version": "2.0.13",
"description": "OmniRoute Desktop Application",
"main": "main.js",
"author": "OmniRoute Team",
"author": {
"name": "OmniRoute Team",
"email": "support@omniroute.online"
},
"license": "MIT",
"homepage": "https://omniroute.online",
"scripts": {
+13 -6
View File
@@ -225,6 +225,7 @@ export const REGISTRY: Record<string, RegistryEntry> = {
{ id: "qwen3-coder-plus", name: "Qwen3 Coder Plus" },
{ id: "qwen3-coder-flash", name: "Qwen3 Coder Flash" },
{ id: "vision-model", name: "Qwen3 Vision Model" },
{ id: "coder-model", name: "Qwen3.5 (Coder Model)" },
],
},
@@ -248,15 +249,20 @@ export const REGISTRY: Record<string, RegistryEntry> = {
authUrl: "https://iflow.cn/oauth",
},
models: [
{ id: "iflow-rome-30ba3b", name: "iFlow ROME" },
{ id: "qwen3-coder-plus", name: "Qwen3 Coder Plus" },
{ id: "qwen3-max", name: "Qwen3 Max" },
{ id: "qwen3-vl-plus", name: "Qwen3 Vision Plus" },
{ id: "kimi-k2-0905", name: "Kimi K2 0905" },
{ id: "qwen3-max-preview", name: "Qwen3 Max Preview" },
{ id: "kimi-k2", name: "Kimi K2" },
{ id: "kimi-k2-thinking", name: "Kimi K2 Thinking" },
{ id: "kimi-k2.5", name: "Kimi K2.5" },
{ id: "deepseek-v3.2", name: "DeepSeek-V3.2-Exp" },
{ id: "deepseek-r1", name: "DeepSeek R1" },
{ id: "deepseek-v3.2-chat", name: "DeepSeek V3.2 Chat" },
{ id: "deepseek-v3.2-reasoner", name: "DeepSeek V3.2 Reasoner" },
{ id: "minimax-m2.1", name: "MiniMax M2.1" },
{ id: "glm-4.7", name: "GLM 4.7" },
{ id: "deepseek-v3", name: "DeepSeek V3" },
{ id: "qwen3-32b", name: "Qwen3 32B" },
{ id: "qwen3-235b-a22b-thinking-2507", name: "Qwen3 235B A22B Thinking 2507" },
{ id: "qwen3-235b-a22b-instruct", name: "Qwen3 235B A22B Instruct" },
{ id: "qwen3-235b", name: "Qwen3 235B" },
],
},
@@ -486,6 +492,7 @@ export const REGISTRY: Record<string, RegistryEntry> = {
{ id: "kimi-k2.5", name: "Kimi K2.5" },
{ id: "kimi-k2.5-thinking", name: "Kimi K2.5 Thinking" },
{ id: "kimi-latest", name: "Kimi Latest" },
{ id: "kimi-for-coding", name: "Kimi For Coding" },
],
},
+10 -13
View File
@@ -38,14 +38,17 @@ export class AntigravityExecutor extends BaseExecutor {
transformRequest(model, body, stream, credentials) {
const bodyProjectId = body?.project;
const credentialsProjectId = credentials?.projectId;
const hasExplicitProject = !!(bodyProjectId || credentialsProjectId);
const projectId = bodyProjectId || credentialsProjectId || this.generateProjectId();
const allowBodyProjectOverride = process.env.OMNIROUTE_ALLOW_BODY_PROJECT_OVERRIDE === "1";
if (!hasExplicitProject) {
console.warn(
`[Antigravity] ⚠️ No projectId provided via body or credentials — using generated fallback "${projectId}". ` +
`This may cause 404 errors if the account has no active GCP project. ` +
`Ensure the OAuth token includes a valid project or the request includes a project field.`
// Default: prefer OAuth-stored projectId over incoming body.project to avoid
// 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;
if (!projectId) {
throw new Error(
"Missing Google projectId for Antigravity account. Please reconnect OAuth so OmniRoute can fetch your real Cloud Code project (loadCodeAssist)."
);
}
@@ -128,12 +131,6 @@ export class AntigravityExecutor extends BaseExecutor {
}
}
generateProjectId() {
const adj = ["useful", "bright", "swift", "calm", "bold"][Math.floor(Math.random() * 5)];
const noun = ["fuze", "wave", "spark", "flow", "core"][Math.floor(Math.random() * 5)];
return `${adj}-${noun}-${crypto.randomUUID().slice(0, 5)}`;
}
generateSessionId() {
return `-${Math.floor(Math.random() * 9_000_000_000_000_000_000)}`;
}
+25 -1
View File
@@ -40,6 +40,7 @@ export type ExecuteInput = {
credentials: ProviderCredentials;
signal?: AbortSignal | null;
log?: ExecutorLog | null;
extendedContext?: boolean;
};
function mergeAbortSignals(primary: AbortSignal, secondary: AbortSignal): AbortSignal {
@@ -174,7 +175,7 @@ export class BaseExecutor {
return { status: response.status, message: bodyText || `HTTP ${response.status}` };
}
async execute({ model, body, stream, credentials, signal, log }: ExecuteInput) {
async execute({ model, body, stream, credentials, signal, log, extendedContext }: ExecuteInput) {
const fallbackCount = this.getFallbackCount();
let lastError: unknown = null;
let lastStatus = 0;
@@ -182,6 +183,29 @@ export class BaseExecutor {
for (let urlIndex = 0; urlIndex < fallbackCount; urlIndex++) {
const url = this.buildUrl(model, stream, urlIndex, credentials);
const headers = this.buildHeaders(credentials, stream);
// Append 1M context beta header when [1m] suffix was used
// Only supported for specific Claude models per Anthropic docs
if (extendedContext) {
const EXTENDED_CONTEXT_MODELS = [
"claude-opus-4-6",
"claude-sonnet-4-6",
"claude-sonnet-4-5",
"claude-sonnet-4",
];
const baseModel = model.replace(/-\d{8}$/, "");
if (
EXTENDED_CONTEXT_MODELS.some((m) => baseModel === m || model === m || model.startsWith(m))
) {
const existing = headers["Anthropic-Beta"];
if (existing) {
headers["Anthropic-Beta"] = existing + ",context-1m-2025-08-07";
} else {
headers["Anthropic-Beta"] = "context-1m-2025-08-07";
}
}
}
const transformedBody = this.transformRequest(model, body, stream, credentials);
try {
+10 -1
View File
@@ -20,7 +20,16 @@ export class GeminiCLIExecutor extends BaseExecutor {
}
transformRequest(model, body, stream, credentials) {
if (!body.project && credentials?.projectId) {
const allowBodyProjectOverride = process.env.OMNIROUTE_ALLOW_BODY_PROJECT_OVERRIDE === "1";
// Default: prefer OAuth-stored projectId. Incoming body.project can be stale
// when clients cache older Cloud Code project values.
// Opt-in escape hatch: set OMNIROUTE_ALLOW_BODY_PROJECT_OVERRIDE=1.
if (allowBodyProjectOverride && body?.project) {
return body;
}
if (credentials?.projectId) {
body.project = credentials.projectId;
}
return body;
+32 -2
View File
@@ -12,6 +12,7 @@ import { addBufferToUsage, filterUsageForFormat, estimateUsage } from "../utils/
import { refreshWithRetry } from "../services/tokenRefresh.ts";
import { createRequestLogger } from "../utils/requestLogger.ts";
import { getModelTargetFormat, PROVIDER_ID_TO_ALIAS } from "../config/providerModels.ts";
import { resolveModelAlias } from "../services/modelDeprecation.ts";
import { createErrorResult, parseUpstreamError, formatProviderError } from "../utils/error.ts";
import { HTTP_STATUS } from "../config/constants.ts";
import { handleBypassRequest } from "../utils/bypassHandler.ts";
@@ -68,7 +69,7 @@ export async function handleChatCore({
userAgent,
comboName,
}) {
const { provider, model } = modelInfo;
const { provider, model, extendedContext } = modelInfo;
const startTime = Date.now();
// ── Phase 9.2: Idempotency check ──
@@ -105,8 +106,13 @@ export async function handleChatCore({
// Detect source format and get target format
// Model-specific targetFormat takes priority over provider default
// Apply custom model aliases (Settings → Model Aliases → Pattern→Target) before routing (#315)
// Custom aliases take priority over built-in and must be resolved here so the
// downstream getModelTargetFormat() lookup uses the correct, aliased model ID.
const resolvedModel = resolveModelAlias(model);
const alias = PROVIDER_ID_TO_ALIAS[provider] || provider;
const modelTargetFormat = getModelTargetFormat(alias, model);
const modelTargetFormat = getModelTargetFormat(alias, resolvedModel);
const targetFormat = modelTargetFormat || getTargetFormat(provider);
// Default to false unless client explicitly sets stream: true (OpenAI spec compliant)
@@ -158,6 +164,28 @@ export async function handleChatCore({
translatedBody = { ...translatedBody, _disableToolPrefix: true };
}
// ── #291: Strip empty name fields from messages/input items ──
// Upstream providers (OpenAI, Codex) reject name:"" with 400 errors.
// Clients like PocketPaw may forward empty name fields from assistant turns.
if (Array.isArray(body.messages)) {
body.messages = body.messages.map((msg: Record<string, unknown>) => {
if (msg.name === "") {
const { name: _n, ...rest } = msg;
return rest;
}
return msg;
});
}
if (Array.isArray(body.input)) {
body.input = body.input.map((item: Record<string, unknown>) => {
if (item.name === "") {
const { name: _n, ...rest } = item;
return rest;
}
return item;
});
}
translatedBody = translateRequest(
sourceFormat,
targetFormat,
@@ -248,6 +276,7 @@ export async function handleChatCore({
credentials,
signal: streamController.signal,
log,
extendedContext,
})
);
@@ -335,6 +364,7 @@ export async function handleChatCore({
credentials,
signal: streamController.signal,
log,
extendedContext,
});
if (retryResult.response.ok) {
+35 -9
View File
@@ -59,29 +59,50 @@ function resolveProviderModelAlias(providerOrAlias, modelId) {
/**
* Parse model string: "alias/model" or "provider/model" or just alias
* Supports [1m] suffix for extended 1M context window (e.g. "claude-sonnet-4-6[1m]")
*/
export function parseModel(modelStr) {
if (!modelStr) {
return { provider: null, model: null, isAlias: false, providerAlias: null };
return {
provider: null,
model: null,
isAlias: false,
providerAlias: null,
extendedContext: false,
};
}
// Sanitize: reject strings with path traversal or control characters
if (/\.\.[\/\\]/.test(modelStr) || /[\x00-\x1f]/.test(modelStr)) {
console.log(`[MODEL] Warning: rejected malformed model string: "${modelStr.substring(0, 50)}"`);
return { provider: null, model: null, isAlias: false, providerAlias: null };
return {
provider: null,
model: null,
isAlias: false,
providerAlias: null,
extendedContext: false,
};
}
// Extract [1m] suffix before parsing provider/model
let extendedContext = false;
let cleanStr = modelStr;
if (cleanStr.endsWith("[1m]")) {
extendedContext = true;
cleanStr = cleanStr.slice(0, -4);
}
// Check if standard format: provider/model or alias/model
if (modelStr.includes("/")) {
const firstSlash = modelStr.indexOf("/");
const providerOrAlias = modelStr.slice(0, firstSlash);
const model = modelStr.slice(firstSlash + 1);
if (cleanStr.includes("/")) {
const firstSlash = cleanStr.indexOf("/");
const providerOrAlias = cleanStr.slice(0, firstSlash);
const model = cleanStr.slice(firstSlash + 1);
const provider = resolveProviderAlias(providerOrAlias);
return { provider, model, isAlias: false, providerAlias: providerOrAlias };
return { provider, model, isAlias: false, providerAlias: providerOrAlias, extendedContext };
}
// Alias format (model alias, not provider alias)
return { provider: null, model: modelStr, isAlias: true, providerAlias: null };
return { provider: null, model: cleanStr, isAlias: true, providerAlias: null, extendedContext };
}
/**
@@ -123,12 +144,14 @@ export function resolveModelAliasFromMap(alias, aliases) {
*/
export async function getModelInfoCore(modelStr, aliasesOrGetter) {
const parsed = parseModel(modelStr);
const { extendedContext } = parsed;
if (!parsed.isAlias) {
const canonicalModel = resolveProviderModelAlias(parsed.provider, parsed.model);
return {
provider: parsed.provider,
model: canonicalModel,
extendedContext,
};
}
@@ -142,6 +165,7 @@ export async function getModelInfoCore(modelStr, aliasesOrGetter) {
return {
provider: resolved.provider,
model: canonicalModel,
extendedContext,
};
}
@@ -153,6 +177,7 @@ export async function getModelInfoCore(modelStr, aliasesOrGetter) {
return {
provider: "openai",
model: modelId,
extendedContext,
};
}
@@ -160,7 +185,7 @@ export async function getModelInfoCore(modelStr, aliasesOrGetter) {
if (nonOpenAIProviders.length === 1) {
const provider = nonOpenAIProviders[0];
const canonicalModel = resolveProviderModelAlias(provider, modelId);
return { provider, model: canonicalModel };
return { provider, model: canonicalModel, extendedContext };
}
if (nonOpenAIProviders.length > 1) {
@@ -182,5 +207,6 @@ export async function getModelInfoCore(modelStr, aliasesOrGetter) {
return {
provider: "openai",
model: modelId,
extendedContext,
};
}
+8
View File
@@ -59,6 +59,11 @@ const PERSIST_DEBOUNCE_MS = 60_000; // Debounce persistence to every 60s max
// Track initialization
let initialized = false;
// Max time (ms) a job can wait in queue before failing with a timeout error.
// Prevents infinite queuing when all providers are exhausted after a 429.
// Configurable via RATE_LIMIT_MAX_WAIT_MS env var (default: 2 minutes).
const MAX_WAIT_MS = parseInt(process.env.RATE_LIMIT_MAX_WAIT_MS || "120000", 10);
// Default conservative settings (before we learn from headers)
const DEFAULT_SETTINGS = {
maxConcurrent: 10,
@@ -66,6 +71,7 @@ const DEFAULT_SETTINGS = {
reservoir: null, // No initial reservoir — unlimited until we learn
reservoirRefreshAmount: null,
reservoirRefreshInterval: null,
maxWait: MAX_WAIT_MS, // Fail-fast: don't queue forever on 429 exhaustion
};
/**
@@ -111,6 +117,7 @@ export async function initializeRateLimits() {
reservoir: rpm,
reservoirRefreshAmount: rpm,
reservoirRefreshInterval: 60 * 1000,
maxWait: MAX_WAIT_MS,
id: key,
})
);
@@ -135,6 +142,7 @@ export async function initializeRateLimits() {
reservoir: DEFAULT_API_LIMITS.requestsPerMinute,
reservoirRefreshAmount: DEFAULT_API_LIMITS.requestsPerMinute,
reservoirRefreshInterval: 60 * 1000, // Refresh every minute
maxWait: MAX_WAIT_MS,
id: key,
})
);
+12 -9
View File
@@ -488,13 +488,14 @@ async function getClaudeUsage(accessToken) {
const data = await oauthResponse.json();
const quotas: Record<string, UsageQuota> = {};
// utilization = percentage REMAINING (e.g., 90 means 90% remaining, 10% used)
// utilization = percentage USED (e.g., 90 means 90% used, 10% remaining)
// Confirmed via user report #299: Claude.ai shows 87% used = OmniRoute must show 13% remaining.
const hasUtilization = (window: JsonRecord) =>
window && typeof window === "object" && safePercentage(window.utilization) !== undefined;
const createQuotaObject = (window: JsonRecord) => {
const remaining = safePercentage(window.utilization) as number;
const used = 100 - remaining;
const used = safePercentage(window.utilization) as number; // utilization = % used
const remaining = Math.max(0, 100 - used);
return {
used,
total: 100,
@@ -917,12 +918,12 @@ async function getKimiUsage(accessToken) {
};
};
if (hasUtilization(dataObj.five_hour)) {
quotas["session (5h)"] = createQuotaObject(dataObj.five_hour);
if (hasUtilization(toRecord(dataObj.five_hour))) {
quotas["session (5h)"] = createQuotaObject(toRecord(dataObj.five_hour));
}
if (hasUtilization(dataObj.seven_day)) {
quotas["weekly (7d)"] = createQuotaObject(dataObj.seven_day);
if (hasUtilization(toRecord(dataObj.seven_day))) {
quotas["weekly (7d)"] = createQuotaObject(toRecord(dataObj.seven_day));
}
// Check for model-specific quotas
@@ -935,7 +936,8 @@ async function getKimiUsage(accessToken) {
}
if (Object.keys(quotas).length > 0) {
const membershipLevel = dataObj.user?.membership?.level;
const userRecord = toRecord(dataObj.user);
const membershipLevel = toRecord(userRecord.membership).level;
const planName = getKimiPlanName(membershipLevel);
return {
plan: planName || "Kimi Coding",
@@ -944,7 +946,8 @@ async function getKimiUsage(accessToken) {
}
// No quota data in response
const membershipLevel = dataObj.user?.membership?.level;
const userRecord = toRecord(dataObj.user);
const membershipLevel = toRecord(userRecord.membership).level;
const planName = getKimiPlanName(membershipLevel);
return {
plan: planName || "Kimi Coding",
+12 -2
View File
@@ -127,14 +127,24 @@ export function prepareClaudeRequest(body, provider = null) {
}
// Pass 1.4: Filter out tool_use blocks with empty names (causes Claude 400 error)
// Apply to ALL roles (assistant tool_use + any user messages that may carry tool_use)
// Also filter tool_result blocks with missing tool_use_id
for (const msg of filtered) {
if (msg.role === "assistant" && Array.isArray(msg.content)) {
if (Array.isArray(msg.content)) {
msg.content = msg.content.filter(
(block) => block.type !== "tool_use" || (block.name && block.name.trim())
(block) => block.type !== "tool_use" || (block.name && block.name?.trim())
);
msg.content = msg.content.filter(
(block) => block.type !== "tool_result" || block.tool_use_id
);
}
}
// Also filter top-level tool declarations with empty names
if (body.tools && Array.isArray(body.tools)) {
body.tools = body.tools.filter((tool) => tool.name && tool.name?.trim());
}
// Pass 1.5: Fix tool_use/tool_result ordering
// Each tool_use must have tool_result in the NEXT message (not same message with other content)
filtered = fixToolUseOrdering(filtered);
@@ -126,15 +126,6 @@ export function generateSessionId() {
return `-${Math.floor(Math.random() * 9000000000000000000)}`;
}
// Generate project ID
export function generateProjectId() {
const adjectives = ["useful", "bright", "swift", "calm", "bold"];
const nouns = ["fuze", "wave", "spark", "flow", "core"];
const adj = adjectives[Math.floor(Math.random() * adjectives.length)];
const noun = nouns[Math.floor(Math.random() * nouns.length)];
return `${adj}-${noun}-${crypto.randomUUID().slice(0, 5)}`;
}
// Helper: Remove unsupported keywords recursively from object/array
function removeUnsupportedKeywords(obj, keywords) {
if (!obj || typeof obj !== "object") return;
@@ -175,6 +175,9 @@ export function openaiToClaudeRequest(model, body, stream) {
};
});
// Filter out tools with empty names (would cause Claude 400 error)
result.tools = result.tools.filter((tool) => tool.name && tool.name?.trim());
// Add cache_control to last tool that doesn't have defer_loading
// Tools with defer_loading=true cannot have cache_control (API rejects it)
for (let i = result.tools.length - 1; i >= 0; i--) {
@@ -227,6 +230,8 @@ function getContentBlocksFromMessage(msg, toolNameMap = new Map(), disableToolPr
if (part.type === "text" && part.text) {
blocks.push({ type: "text", text: part.text });
} else if (part.type === "tool_result") {
// Skip tool_result with no tool_use_id (would be useless and may cause errors)
if (!part.tool_use_id) continue;
blocks.push({
type: "tool_result",
tool_use_id: part.tool_use_id,
@@ -15,7 +15,6 @@ import {
tryParseJSON,
generateRequestId,
generateSessionId,
generateProjectId,
cleanJSONSchemaForAntigravity,
} from "../helpers/geminiHelper.ts";
@@ -321,13 +320,11 @@ export function openaiToGeminiCLIRequest(model, body, stream) {
// Wrap Gemini CLI format in Cloud Code wrapper
function wrapInCloudCodeEnvelope(model, geminiCLI, credentials = null, isAntigravity = false) {
const hasRealProject = !!credentials?.projectId;
const projectId = credentials?.projectId || generateProjectId();
const projectId = credentials?.projectId;
if (!hasRealProject) {
console.warn(
`[${isAntigravity ? "Antigravity" : "GeminiCLI"}] ⚠️ No projectId in credentials — using generated fallback "${projectId}". ` +
`This may cause 404 errors. Ensure the OAuth token includes a valid GCP project.`
if (!projectId) {
throw new Error(
`${isAntigravity ? "Antigravity" : "GeminiCLI"} account is missing projectId. Reconnect OAuth to load your real Cloud Code project before sending requests.`
);
}
@@ -374,13 +371,11 @@ function wrapInCloudCodeEnvelope(model, geminiCLI, credentials = null, isAntigra
}
function wrapInCloudCodeEnvelopeForClaude(model, claudeRequest, credentials = null) {
const hasRealProject = !!credentials?.projectId;
const projectId = credentials?.projectId || generateProjectId();
const projectId = credentials?.projectId;
if (!hasRealProject) {
console.warn(
`[Antigravity/Claude] ⚠️ No projectId in credentials — using generated fallback "${projectId}". ` +
`This may cause 404 errors. Ensure the OAuth token includes a valid GCP project.`
if (!projectId) {
throw new Error(
"Antigravity/Claude account is missing projectId. Reconnect OAuth to load your real Cloud Code project before sending requests."
);
}
@@ -51,7 +51,9 @@ export function claudeToOpenAIResponse(chunk, state) {
} else if (block?.type === "thinking") {
state.inThinkingBlock = true;
state.currentBlockIndex = chunk.index;
results.push(createChunk(state, { content: "<think>" }));
// Emit empty reasoning_content to signal thinking block start
// (clients like Claude Code look for reasoning_content, not <think> tags)
results.push(createChunk(state, { reasoning_content: "" }));
} else if (block?.type === "tool_use") {
const toolCallIndex = state.toolCallIndex++;
// Restore original tool name from mapping (Claude OAuth)
@@ -76,7 +78,9 @@ export function claudeToOpenAIResponse(chunk, state) {
if (delta?.type === "text_delta" && delta.text) {
results.push(createChunk(state, { content: delta.text }));
} else if (delta?.type === "thinking_delta" && delta.thinking) {
results.push(createChunk(state, { content: delta.thinking }));
// Map Claude thinking_delta → OpenAI reasoning_content
// Clients (Claude Code, Cursor, etc.) display reasoning_content as the thinking panel
results.push(createChunk(state, { reasoning_content: delta.thinking }));
} else if (delta?.type === "input_json_delta" && delta.partial_json) {
const toolCall = state.toolCalls.get(chunk.index);
if (toolCall) {
@@ -99,7 +103,8 @@ export function claudeToOpenAIResponse(chunk, state) {
case "content_block_stop": {
if (state.inThinkingBlock && chunk.index === state.currentBlockIndex) {
results.push(createChunk(state, { content: "</think>" }));
// Thinking block closed — no additional content needed;
// reasoning_content chunks have already been streamed
state.inThinkingBlock = false;
}
state.textBlockStarted = false;
+6 -16
View File
@@ -1,12 +1,12 @@
{
"name": "omniroute",
"version": "2.2.2",
"version": "2.3.3",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "omniroute",
"version": "2.2.2",
"version": "2.3.3",
"hasInstallScript": true,
"license": "MIT",
"workspaces": [
@@ -15,6 +15,7 @@
"dependencies": {
"@modelcontextprotocol/sdk": "^1.27.1",
"@monaco-editor/react": "^4.7.0",
"@swc/helpers": "0.5.19",
"bcryptjs": "^3.0.3",
"better-sqlite3": "^12.6.2",
"bottleneck": "^2.19.5",
@@ -7212,9 +7213,9 @@
}
},
"node_modules/hono": {
"version": "4.12.4",
"resolved": "https://registry.npmjs.org/hono/-/hono-4.12.4.tgz",
"integrity": "sha512-ooiZW1Xy8rQ4oELQ++otI2T9DsKpV0M6c6cO6JGx4RTfav9poFFLlet9UMXHZnoM1yG0HWGlQLswBGX3RZmHtg==",
"version": "4.12.7",
"resolved": "https://registry.npmjs.org/hono/-/hono-4.12.7.tgz",
"integrity": "sha512-jq9l1DM0zVIvsm3lv9Nw9nlJnMNPOcAtsbsgiUhWcFzPE99Gvo6yRTlszSLLYacMeQ6quHD6hMfId8crVHvexw==",
"license": "MIT",
"engines": {
"node": ">=16.9.0"
@@ -8978,17 +8979,6 @@
}
}
},
"node_modules/next-intl/node_modules/@swc/helpers": {
"version": "0.5.19",
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.19.tgz",
"integrity": "sha512-QamiFeIK3txNjgUTNppE6MiG3p7TdninpZu0E0PbqVh1a9FNLT2FRhisaa4NcaX52XVhA5l7Pk58Ft7Sqi/2sA==",
"license": "Apache-2.0",
"optional": true,
"peer": true,
"dependencies": {
"tslib": "^2.8.0"
}
},
"node_modules/next/node_modules/postcss": {
"version": "8.4.31",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz",
+5 -3
View File
@@ -1,6 +1,6 @@
{
"name": "omniroute",
"version": "2.2.2",
"version": "2.3.7",
"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": {
@@ -13,6 +13,7 @@
"open-sse/mcp-server/",
"src/shared/contracts/",
"scripts/postinstall.mjs",
"scripts/native-binary-compat.mjs",
"README.md",
"LICENSE"
],
@@ -109,7 +110,8 @@
"uuid": "^13.0.0",
"wreq-js": "^2.0.1",
"zod": "^4.3.6",
"zustand": "^5.0.10"
"zustand": "^5.0.10",
"@swc/helpers": "0.5.19"
},
"devDependencies": {
"@playwright/test": "^1.58.2",
@@ -142,6 +144,6 @@
]
},
"overrides": {
"@swc/helpers": "^0.5.19"
"@swc/helpers": "0.5.19"
}
}
+12
View File
@@ -0,0 +1,12 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<rect width="100" height="100" rx="18" fill="#0062FF"/>
<!-- AssemblyAI — waveform/microphone mark -->
<rect x="47" y="18" width="6" height="30" rx="3" fill="white"/>
<rect x="35" y="26" width="6" height="22" rx="3" fill="white" opacity="0.8"/>
<rect x="59" y="26" width="6" height="22" rx="3" fill="white" opacity="0.8"/>
<rect x="23" y="34" width="6" height="14" rx="3" fill="white" opacity="0.5"/>
<rect x="71" y="34" width="6" height="14" rx="3" fill="white" opacity="0.5"/>
<!-- Bottom line -->
<rect x="30" y="62" width="40" height="4" rx="2" fill="white" opacity="0.7"/>
<rect x="45" y="66" width="10" height="14" rx="2" fill="white" opacity="0.7"/>
</svg>

After

Width:  |  Height:  |  Size: 749 B

+6
View File
@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<rect width="100" height="100" rx="18" fill="#6C47FF"/>
<!-- ElevenLabs "11" logo mark — two vertical bars -->
<rect x="24" y="20" width="20" height="60" rx="4" fill="white"/>
<rect x="56" y="20" width="20" height="60" rx="4" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 321 B

+13
View File
@@ -0,0 +1,13 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<rect width="100" height="100" rx="18" fill="#141414"/>
<!-- Hyperbolic — stylized "H" with gradient accent -->
<defs>
<linearGradient id="hg" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#00D4FF"/>
<stop offset="100%" stop-color="#7B2FFF"/>
</linearGradient>
</defs>
<rect x="22" y="20" width="14" height="60" rx="3" fill="url(#hg)"/>
<rect x="22" y="41" width="56" height="14" rx="3" fill="url(#hg)"/>
<rect x="64" y="20" width="14" height="60" rx="3" fill="url(#hg)"/>
</svg>

After

Width:  |  Height:  |  Size: 600 B

+12
View File
@@ -0,0 +1,12 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<rect width="100" height="100" rx="18" fill="#0A0A1A"/>
<defs>
<linearGradient id="ig" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#5B4FFF"/>
<stop offset="100%" stop-color="#00E5FF"/>
</linearGradient>
</defs>
<!-- Inworld "i" with dot - futuristic -->
<circle cx="50" cy="28" r="8" fill="url(#ig)"/>
<rect x="42" y="42" width="16" height="38" rx="5" fill="url(#ig)"/>
</svg>

After

Width:  |  Height:  |  Size: 495 B

+12
View File
@@ -0,0 +1,12 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<rect width="100" height="100" rx="18" fill="#1C1A00"/>
<!-- NanoBanana - banana icon stylized -->
<defs>
<linearGradient id="bg" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#FFE000"/>
<stop offset="100%" stop-color="#FF9500"/>
</linearGradient>
</defs>
<path d="M 35 75 Q 20 40 40 20 Q 55 10 70 18 Q 60 22 52 30 Q 38 45 42 65 Z" fill="url(#bg)"/>
<path d="M 42 65 Q 38 45 52 30 Q 60 22 70 18 Q 75 28 72 38 Q 68 55 55 65 Z" fill="#FFD700"/>
</svg>

After

Width:  |  Height:  |  Size: 566 B

+375
View File
@@ -0,0 +1,375 @@
<!doctype html>
<html class="h-full overflow-y-scroll">
<head>
<title>Ollama</title>
<meta charset="utf-8" />
<meta name="description" content="Get up and running with large language models."/>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta property="og:title" content="Ollama" />
<meta property="og:description" content="Get up and running with large language models." />
<meta property="og:url" content="https://ollama.com" />
<meta property="og:image" content="https://ollama.com/public/og.png" />
<meta property="og:image:type" content="image/png" />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="628" />
<meta property="og:type" content="website" />
<meta name="robots" content="index, follow" />
<meta property="twitter:card" content="summary" />
<meta property="twitter:title" content="Ollama" />
<meta property="twitter:description" content="Get up and running with large language models." />
<meta property="twitter:site" content="ollama" />
<meta property="twitter:image:src" content="https://ollama.com/public/og-twitter.png" />
<meta property="twitter:image:width" content="1200" />
<meta property="twitter:image:height" content="628" />
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<link rel="icon" type="image/png" sizes="16x16" href="/public/icon-16x16.png" />
<link rel="icon" type="image/png" sizes="32x32" href="/public/icon-32x32.png" />
<link rel="icon" type="image/png" sizes="48x48" href="/public/icon-48x48.png" />
<link rel="icon" type="image/png" sizes="64x64" href="/public/icon-64x64.png" />
<link rel="apple-touch-icon" sizes="180x180" href="/public/apple-touch-icon.png" />
<link rel="icon" type="image/png" sizes="192x192" href="/public/android-chrome-icon-192x192.png" />
<link rel="icon" type="image/png" sizes="512x512" href="/public/android-chrome-icon-512x512.png" />
<link href="/public/tailwind.css?v=9f0babb28a8cef23daf033b8840da7f9" rel="stylesheet" />
<link href="/public/vendor/prism/prism.css?v=9f0babb28a8cef23daf033b8840da7f9" rel="stylesheet" />
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "WebSite",
"name": "Ollama",
"url": "https://ollama.com"
}
</script>
<script type="text/javascript">
function copyToClipboard(element) {
let commandElement = null;
const preElement = element.closest('pre');
const languageNoneElement = element.closest('.language-none');
if (preElement) {
commandElement = preElement.querySelector('code');
} else if (languageNoneElement) {
commandElement = languageNoneElement.querySelector('.command');
} else {
const parent = element.parentElement;
if (parent) {
commandElement = parent.querySelector('.command');
}
}
if (!commandElement) {
console.error('No code or command element found');
return;
}
const code = commandElement.textContent ? commandElement.textContent.trim() : commandElement.value;
navigator.clipboard
.writeText(code)
.then(() => {
const copyIcon = element.querySelector('.copy-icon')
const checkIcon = element.querySelector('.check-icon')
copyIcon.classList.add('hidden')
checkIcon.classList.remove('hidden')
setTimeout(() => {
copyIcon.classList.remove('hidden')
checkIcon.classList.add('hidden')
}, 2000)
})
}
</script>
<script>
function getIcon(url) {
url = url.toLowerCase();
if (url.includes('x.com') || url.includes('twitter.com')) return 'x';
if (url.includes('github.com')) return 'github';
if (url.includes('linkedin.com')) return 'linkedin';
if (url.includes('youtube.com')) return 'youtube';
if (url.includes('hf.co') || url.includes('huggingface.co') || url.includes('huggingface.com')) return 'hugging-face';
return 'default';
}
function setInputIcon(input) {
const icon = getIcon(input.value);
const img = input.previousElementSibling.querySelector('img');
img.src = `/public/social/${icon}.svg`;
img.alt = `${icon} icon`;
}
function setDisplayIcon(imgElement, url) {
const icon = getIcon(url);
imgElement.src = `/public/social/${icon}.svg`;
imgElement.alt = `${icon} icon`;
}
</script>
<script src="/public/vendor/htmx/bundle.js"></script>
</head>
<body
class="
antialiased
min-h-screen
w-full
m-0
flex
flex-col
"
hx-on:keydown="
if (event.target.tagName === 'INPUT' || event.target.tagName === 'TEXTAREA') {
// Ignore key events in input fields.
return;
}
if ((event.metaKey && event.key === 'k') || event.key === '/') {
event.preventDefault();
const sp = htmx.find('#search') || htmx.find('#navbar-input');
sp.focus();
}
"
>
<header class="sticky top-0 z-40 bg-white underline-offset-4 lg:static">
<nav class="flex w-full items-center justify-between px-6 py-[9px]">
<a href="/" class="z-50">
<img src="/public/ollama.png" class="w-8" alt="Ollama" />
</a>
<div class="hidden lg:flex xl:flex-1 items-center space-x-6 ml-6 mr-6 xl:mr-0 text-lg">
<a class="hover:underline focus:underline focus:outline-none focus:ring-0" href="/search">Models</a>
<a class="hover:underline focus:underline focus:outline-none focus:ring-0" href="/docs">Docs</a>
<a class="hover:underline focus:underline focus:outline-none focus:ring-0" href="/pricing">Pricing</a>
</div>
<div class="flex-grow justify-center items-center hidden lg:flex">
<div class="relative w-full xl:max-w-[28rem]">
<form action="/search" autocomplete="off">
<div
class="relative flex w-full appearance-none bg-black/5 border border-neutral-100 items-center rounded-full"
hx-on:focusout="
if (!this.contains(event.relatedTarget)) {
const searchPreview = document.querySelector('#searchpreview');
if (searchPreview) {
htmx.addClass('#searchpreview', 'hidden');
}
}
"
>
<span id="searchIcon" class="pl-2 text-2xl text-neutral-500">
<svg class="mt-0.25 ml-1.5 h-5 w-5 fill-current" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
<path d="m8.5 3c3.0375661 0 5.5 2.46243388 5.5 5.5 0 1.24832096-.4158777 2.3995085-1.1166416 3.3225711l4.1469717 4.1470988c.2928932.2928932.2928932.767767 0 1.0606602-.2662666.2662665-.6829303.2904726-.9765418.0726181l-.0841184-.0726181-4.1470988-4.1469717c-.9230626.7007639-2.07425014 1.1166416-3.3225711 1.1166416-3.03756612 0-5.5-2.4624339-5.5-5.5 0-3.03756612 2.46243388-5.5 5.5-5.5zm0 1.5c-2.209139 0-4 1.790861-4 4s1.790861 4 4 4 4-1.790861 4-4-1.790861-4-4-4z" />
</svg>
</span>
<input
id="search"
hx-get="/search"
hx-trigger="keyup changed delay:100ms, focus"
hx-target="#searchpreview"
hx-swap="innerHTML"
name="q"
class="resize-none rounded-full border-0 py-2.5 bg-transparent text-sm w-full placeholder:text-neutral-500 focus:outline-none focus:ring-0"
placeholder="Search models"
autocomplete="off"
hx-on:keydown="
if (event.key === 'Enter') {
event.preventDefault();
window.location.href = '/search?q=' + encodeURIComponent(this.value);
return;
}
if (event.key === 'Escape') {
event.preventDefault();
this.value = '';
this.blur();
htmx.addClass('#searchpreview', 'hidden');
return;
}
if (event.key === 'Tab') {
htmx.addClass('#searchpreview', 'hidden');
return;
}
if (event.key === 'ArrowDown') {
let first = document.querySelector('#search-preview-list a:first-of-type');
first?.focus();
event.preventDefault();
}
if (event.key === 'ArrowUp') {
let last = document.querySelector('#view-all-link');
last?.focus();
event.preventDefault();
}
htmx.removeClass('#searchpreview', 'hidden');
"
hx-on:focus="
htmx.removeClass('#searchpreview', 'hidden')
"
/>
</form>
<div id="searchpreview" class="hidden absolute left-0 right-0 top-12 z-50" style="width: calc(100% + 2px); margin-left: -1px;"></div>
</div>
</div>
</div>
<div class="hidden lg:flex xl:flex-1 items-center space-x-2 justify-end ml-6 xl:ml-0">
<a class="flex cursor-pointer items-center rounded-full bg-black/5 hover:bg-black/10 text-lg px-4 py-1.5 text-black whitespace-nowrap" href="/signin">Sign in</a>
<a class="flex cursor-pointer items-center rounded-full bg-neutral-800 text-lg px-4 py-1.5 text-white hover:bg-black whitespace-nowrap focus:bg-black" href="/download">Download</a>
</div>
<div class="lg:hidden flex items-center">
<input type="checkbox" id="menu" class="peer hidden" />
<label for="menu" class="z-50 cursor-pointer peer-checked:hidden block">
<svg
class="h-8 w-8"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
aria-hidden="true"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5"
/>
</svg>
</label>
<label for="menu" class="z-50 cursor-pointer hidden peer-checked:block fixed top-4 right-6">
<svg
class="h-8 w-8"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
aria-hidden="true"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M6 18L18 6M6 6l12 12"
/>
</svg>
</label>
<div class="fixed inset-0 bg-white z-40 hidden peer-checked:block overflow-y-auto">
<div class="flex flex-col space-y-5 pt-[5.5rem] text-3xl">
<a class="px-6" href="/search">Models</a>
<a class="px-6" href="/download">Download</a>
<a class="px-6" href="/docs">Docs</a>
<a class="px-6" href="/pricing">Pricing</a>
<a href="/signin" class="block px-6">Sign in</a>
</div>
</div>
</div>
</nav>
</header>
<main class="mx-auto flex max-w-4xl flex-1 flex-col-reverse items-center justify-center p-32 md:flex-row md:items-start md:justify-between">
<div class="space-y-2 text-center md:pt-6 md:text-left">
<h2 class="text-3xl font-normal tracking-tight md:text-4xl">
404.
<span class="text-neutral-400"> That's an error. </span>
</h2>
<p class="text-center text-lg md:text-left md:text-xl">
The page was not found.
</p>
</div>
<div class="pb-4 md:pb-0">
<img src="/public/400s.svg" class="w-40 md:w-48" alt="400s ollama" />
</div>
</main>
<footer class="mt-auto">
<div class="underline-offset-4 hidden md:block">
<div class="flex items-center justify-between px-6 py-3.5">
<div class="text-xs text-neutral-500">© 2026 Ollama</div>
<div class="flex space-x-6 text-xs text-neutral-500">
<a href="/download" class="hover:underline">Download</a>
<a href="/blog" class="hover:underline">Blog</a>
<a href="https://docs.ollama.com" class="hover:underline">Docs</a>
<a href="https://github.com/ollama/ollama" class="hover:underline">GitHub</a>
<a href="https://discord.com/invite/ollama" class="hover:underline">Discord</a>
<a href="https://twitter.com/ollama" class="hover:underline">X (Twitter)</a>
<a href="mailto:hello@ollama.com" class="hover:underline">Contact</a>
</div>
</div>
</div>
<div class="py-4 md:hidden">
<div class="flex flex-col items-center justify-center">
<ul class="flex flex-wrap items-center justify-center text-sm text-neutral-500">
<li class="mx-2 my-1">
<a href="/blog" class="hover:underline">Blog</a>
</li>
<li class="mx-2 my-1">
<a href="/download" class="hover:underline">Download</a>
</li>
<li class="mx-2 my-1">
<a href="https://docs.ollama.com" class="hover:underline">Docs</a>
</li>
</ul>
<ul class="flex flex-wrap items-center justify-center text-sm text-neutral-500">
<li class="mx-2 my-1">
<a href="https://github.com/ollama/ollama" class="hover:underline">GitHub</a>
</li>
<li class="mx-2 my-1">
<a href="https://discord.com/invite/ollama" class="hover:underline">Discord</a>
</li>
<li class="mx-2 my-1">
<a href="https://twitter.com/ollama" class="hover:underline">X (Twitter)</a>
</li>
<li class="mx-2 my-1">
<a href="https://lu.ma/ollama" class="hover:underline">Meetups</a>
</li>
</ul>
<div class="mt-2 flex items-center justify-center text-sm text-neutral-500">
© 2026 Ollama Inc.
</div>
</div>
</div>
</footer>
<span class="hidden" id="end_of_template"></span>
</body>
</html>
+11
View File
@@ -0,0 +1,11 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<rect width="100" height="100" rx="18" fill="#1A1A2E"/>
<defs>
<linearGradient id="pg" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#7B2FFF"/>
<stop offset="100%" stop-color="#FF6B6B"/>
</linearGradient>
</defs>
<!-- Play triangle -->
<polygon points="28,22 28,78 78,50" fill="url(#pg)" rx="4"/>
</svg>

After

Width:  |  Height:  |  Size: 418 B

+174
View File
@@ -0,0 +1,174 @@
#!/usr/bin/env node
/**
* OmniRoute — Zero-Config Bootstrap
*
* Auto-generates required secrets (JWT_SECRET, STORAGE_ENCRYPTION_KEY) if
* missing or empty, persists them to {DATA_DIR}/server.env so they survive
* restarts, Docker volume remounts, and upgrades.
*
* Works across all deployment modes:
* - npm / CLI: called from run-standalone.mjs and run-next.mjs
* - Docker: same, secrets persisted in mounted volume
* - Electron: called from main.js startup, persisted in userData
*
* Priority (lowest → highest):
* 1. Auto-generated defaults
* 2. {DATA_DIR}/server.env (persisted on first boot)
* 3. .env in CWD (user overrides)
* 4. process.env (shell / Docker -e flags, highest priority)
*/
import { createHash, randomBytes } from "node:crypto";
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
import { homedir } from "node:os";
import { join, resolve } from "node:path";
// ── OAuth secrets that are optional but warn if missing ─────────────────────
const OPTIONAL_OAUTH_SECRETS = [
{ key: "ANTIGRAVITY_OAUTH_CLIENT_SECRET", label: "Antigravity OAuth" },
{ key: "IFLOW_OAUTH_CLIENT_SECRET", label: "iFlow OAuth" },
{ key: "GEMINI_OAUTH_CLIENT_SECRET", label: "Gemini OAuth" },
];
// ── Resolve DATA_DIR (mirrors dataPaths.ts logic) ───────────────────────────
function resolveDataDir(overridePath) {
if (overridePath) return resolve(overridePath);
const configured = process.env.DATA_DIR?.trim();
if (configured) return resolve(configured);
if (process.platform === "win32") {
const appData = process.env.APPDATA || join(homedir(), "AppData", "Roaming");
return join(appData, "omniroute");
}
const xdg = process.env.XDG_CONFIG_HOME?.trim();
if (xdg) return join(resolve(xdg), "omniroute");
return join(homedir(), ".omniroute");
}
// ── Parse a simple KEY=VALUE env file ───────────────────────────────────────
function parseEnvFile(filePath) {
if (!existsSync(filePath)) return {};
const env = {};
const lines = readFileSync(filePath, "utf8").split(/\r?\n/);
for (const line of lines) {
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith("#")) continue;
const eqIdx = trimmed.indexOf("=");
if (eqIdx < 1) continue;
const key = trimmed.slice(0, eqIdx).trim();
const val = trimmed.slice(eqIdx + 1).trim();
env[key] = val;
}
return env;
}
// ── Write a simple KEY=VALUE env file ───────────────────────────────────────
function writeEnvFile(filePath, env) {
const lines = [
"# Auto-generated by OmniRoute bootstrap — do not delete",
`# Created: ${new Date().toISOString()}`,
"",
...Object.entries(env).map(([k, v]) => `${k}=${v}`),
"",
];
writeFileSync(filePath, lines.join("\n"), "utf8");
}
// ── Main bootstrap function ──────────────────────────────────────────────────
/**
* @param {{ dataDirOverride?: string; quiet?: boolean }} options
* @returns {Record<string, string>} merged env to pass to child process
*/
export function bootstrapEnv({ dataDirOverride, quiet = false } = {}) {
const log = quiet ? () => {} : (msg) => process.stderr.write(`[bootstrap] ${msg}\n`);
const dataDir = resolveDataDir(dataDirOverride);
const serverEnvPath = join(dataDir, "server.env");
const dotEnvPath = join(process.cwd(), ".env");
// ── Layer 1: Load persisted server.env ────────────────────────────────────
let persisted = parseEnvFile(serverEnvPath);
// ── Layer 2: Load .env from CWD (user overrides, higher priority) ─────────
const dotEnv = parseEnvFile(dotEnvPath);
// ── Merge: persisted < .env < process.env ─────────────────────────────────
const merged = { ...persisted, ...dotEnv, ...process.env };
// ── Auto-generate required secrets ────────────────────────────────────────
let needsPersist = false;
if (!merged.JWT_SECRET?.trim()) {
persisted.JWT_SECRET = randomBytes(64).toString("hex");
merged.JWT_SECRET = persisted.JWT_SECRET;
needsPersist = true;
log("✨ JWT_SECRET auto-generated (first run)");
}
if (!merged.STORAGE_ENCRYPTION_KEY?.trim()) {
persisted.STORAGE_ENCRYPTION_KEY = randomBytes(32).toString("hex");
merged.STORAGE_ENCRYPTION_KEY = persisted.STORAGE_ENCRYPTION_KEY;
needsPersist = true;
log("✨ STORAGE_ENCRYPTION_KEY auto-generated (first run)");
}
if (!merged.STORAGE_ENCRYPTION_KEY_VERSION?.trim()) {
persisted.STORAGE_ENCRYPTION_KEY_VERSION = "v1";
merged.STORAGE_ENCRYPTION_KEY_VERSION = persisted.STORAGE_ENCRYPTION_KEY_VERSION;
needsPersist = true;
}
if (!merged.API_KEY_SECRET?.trim()) {
persisted.API_KEY_SECRET = randomBytes(32).toString("hex");
merged.API_KEY_SECRET = persisted.API_KEY_SECRET;
needsPersist = true;
log("✨ API_KEY_SECRET auto-generated (first run)");
}
// ── Persist new secrets ────────────────────────────────────────────────────
if (needsPersist) {
try {
mkdirSync(dataDir, { recursive: true });
// Only persist keys that we auto-generated (not .env or process.env vals)
writeEnvFile(serverEnvPath, persisted);
log(`📁 Secrets persisted to: ${serverEnvPath}`);
} catch (e) {
log(`⚠️ Could not persist secrets to ${serverEnvPath}: ${e.message}`);
}
}
// ── Mark as bootstrapped ───────────────────────────────────────────────────
if (needsPersist) {
merged.OMNIROUTE_BOOTSTRAPPED = "true";
}
// ── Warn about missing optional OAuth secrets ──────────────────────────────
const missingOauth = OPTIONAL_OAUTH_SECRETS.filter(({ key }) => !merged[key]?.trim());
if (missingOauth.length > 0) {
log("️ The following OAuth integrations are not configured:");
for (const { key, label } of missingOauth) {
log(`${label} (${key}) — set in .env or ${serverEnvPath}`);
}
log(" These providers will not work until configured.");
}
// ── Warn about default password ────────────────────────────────────────────
if (merged.INITIAL_PASSWORD === "CHANGEME" || !merged.INITIAL_PASSWORD?.trim()) {
log("⚠️ INITIAL_PASSWORD is not set — using default 'CHANGEME'. Change it in Settings!");
}
return merged;
}
// ── CLI usage: node scripts/bootstrap-env.mjs ──────────────────────────────
if (process.argv[1] && process.argv[1].endsWith("bootstrap-env.mjs")) {
const env = bootstrapEnv();
process.stderr.write(`[bootstrap] Done. DATA_DIR resolved to: ${resolveDataDir()}\n`);
process.stderr.write(`[bootstrap] JWT_SECRET length: ${env.JWT_SECRET?.length ?? 0}\n`);
process.stderr.write(
`[bootstrap] STORAGE_ENCRYPTION_KEY length: ${env.STORAGE_ENCRYPTION_KEY?.length ?? 0}\n`
);
}
+2 -2
View File
@@ -2,12 +2,12 @@
/**
* Docker healthcheck script for OmniRoute.
* Checks the /api/settings endpoint on the dashboard port.
* Checks the /api/monitoring/health endpoint on the dashboard port.
* Used by Dockerfile and docker-compose files.
*/
const port = process.env.DASHBOARD_PORT || process.env.PORT || "20128";
fetch(`http://127.0.0.1:${port}/api/settings`)
fetch(`http://127.0.0.1:${port}/api/monitoring/health`)
.then((r) => {
if (!r.ok) throw new Error(`HTTP ${r.status}`);
})
+163
View File
@@ -0,0 +1,163 @@
import { existsSync, openSync, readSync, closeSync } from "node:fs";
export const PUBLISHED_BUILD_PLATFORM = "linux";
export const PUBLISHED_BUILD_ARCH = "x64";
const HEADER_SIZE = 4096;
const MAX_FAT_ARCH_COUNT = 30;
function mapElfMachine(machine) {
switch (machine) {
case 62:
return "x64";
case 183:
return "arm64";
default:
return null;
}
}
function mapMachCpuType(cpuType) {
switch (cpuType) {
case 0x01000007:
return "x64";
case 0x0100000c:
return "arm64";
default:
return null;
}
}
function mapPeMachine(machine) {
switch (machine) {
case 0x8664:
return "x64";
case 0xaa64:
return "arm64";
default:
return null;
}
}
function readUInt16(buffer, offset, littleEndian) {
return littleEndian ? buffer.readUInt16LE(offset) : buffer.readUInt16BE(offset);
}
function readUInt32(buffer, offset, littleEndian) {
return littleEndian ? buffer.readUInt32LE(offset) : buffer.readUInt32BE(offset);
}
const ELF_MAGIC = 0x7f454c46;
function detectElfTarget(buffer) {
if (buffer.length < 20) return null;
if (buffer.readUInt32BE(0) !== ELF_MAGIC) return null;
const littleEndian = buffer[5] !== 2;
const arch = mapElfMachine(readUInt16(buffer, 18, littleEndian));
if (!arch) return null;
return { platform: "linux", architectures: [arch] };
}
const THIN_MACH_MAGIC = new Map([
[0xfeedface, false],
[0xfeedfacf, false],
[0xcefaedfe, true],
[0xcffaedfe, true],
]);
const FAT_MACH_MAGIC = new Map([
[0xcafebabe, false],
[0xcafebabf, false],
[0xbebafeca, true],
[0xbfbafeca, true],
]);
function detectMachTarget(buffer) {
if (buffer.length < 8) return null;
const magic = buffer.readUInt32BE(0);
if (THIN_MACH_MAGIC.has(magic)) {
const littleEndian = THIN_MACH_MAGIC.get(magic);
const arch = mapMachCpuType(readUInt32(buffer, 4, littleEndian));
if (!arch) return null;
return { platform: "darwin", architectures: [arch] };
}
if (!FAT_MACH_MAGIC.has(magic)) return null;
const littleEndian = FAT_MACH_MAGIC.get(magic);
const isFat64 = magic === 0xcafebabf || magic === 0xbfbafeca;
const archCount = readUInt32(buffer, 4, littleEndian);
if (archCount > MAX_FAT_ARCH_COUNT) return null;
const entrySize = isFat64 ? 32 : 20;
const architectures = new Set();
for (let index = 0; index < archCount; index += 1) {
const offset = 8 + index * entrySize;
if (offset + 4 > buffer.length) break;
const arch = mapMachCpuType(readUInt32(buffer, offset, littleEndian));
if (arch) architectures.add(arch);
}
if (architectures.size === 0) return null;
return { platform: "darwin", architectures: [...architectures] };
}
function detectPeTarget(buffer) {
if (buffer.length < 0x40) return null;
if (buffer.readUInt16LE(0) !== 0x5a4d) return null;
const peHeaderOffset = buffer.readUInt32LE(0x3c);
if (peHeaderOffset + 6 > buffer.length) return null;
if (buffer.readUInt32LE(peHeaderOffset) !== 0x00004550) return null;
const arch = mapPeMachine(buffer.readUInt16LE(peHeaderOffset + 4));
if (!arch) return null;
return { platform: "win32", architectures: [arch] };
}
export function detectNativeBinaryTarget(buffer) {
return detectElfTarget(buffer) ?? detectMachTarget(buffer) ?? detectPeTarget(buffer);
}
export function readNativeBinaryTarget(binaryPath) {
if (!existsSync(binaryPath)) return null;
let fd;
try {
fd = openSync(binaryPath, "r");
const buffer = Buffer.alloc(HEADER_SIZE);
const bytesRead = readSync(fd, buffer, 0, HEADER_SIZE, 0);
return detectNativeBinaryTarget(buffer.subarray(0, bytesRead));
} catch (err) {
console.warn(` ⚠️ Could not read native binary at ${binaryPath}: ${err.message}`);
return null;
} finally {
if (fd !== undefined) closeSync(fd);
}
}
export function isNativeBinaryCompatible(
binaryPath,
{ runtimePlatform = process.platform, runtimeArch = process.arch, dlopen = process.dlopen } = {}
) {
const target = readNativeBinaryTarget(binaryPath);
if (target) {
if (target.platform !== runtimePlatform || !target.architectures.includes(runtimeArch)) {
return false;
}
} else if (runtimePlatform !== PUBLISHED_BUILD_PLATFORM || runtimeArch !== PUBLISHED_BUILD_ARCH) {
return false;
}
try {
dlopen({ exports: {} }, binaryPath);
return true;
} catch (err) {
console.warn(` ⚠️ Native binary dlopen failed: ${err.message}`);
return false;
}
}
+108 -25
View File
@@ -1,57 +1,140 @@
#!/usr/bin/env node
/**
* OmniRoute — Postinstall Native Module Rebuild
* OmniRoute — Postinstall Native Module Fix
*
* The npm package ships with a Next.js standalone build that includes
* better-sqlite3 compiled for the build platform (Linux x64).
* This script detects platform mismatches and rebuilds the native
* module for the user's actual OS/architecture.
* better-sqlite3 compiled for the build platform (Linux x64) inside
* app/node_modules/. However, npm also installs better-sqlite3 as a
* top-level dependency (in the root node_modules/), correctly compiled
* for the user's platform.
*
* This script copies the correctly-built native binary from the root
* into the standalone app directory — no rebuild or build tools needed.
*
* Fixes: https://github.com/diegosouzapw/OmniRoute/issues/129
* Fixes: https://github.com/diegosouzapw/OmniRoute/issues/321
*/
import { execSync } from "node:child_process";
import { existsSync } from "node:fs";
import { existsSync, copyFileSync, mkdirSync } from "node:fs";
import { join, dirname } from "node:path";
import { fileURLToPath } from "node:url";
import { PUBLISHED_BUILD_PLATFORM, PUBLISHED_BUILD_ARCH } from "./native-binary-compat.mjs";
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const ROOT = join(__dirname, "..");
// The standalone build bundles better-sqlite3 inside app/node_modules
const appNodeModules = join(ROOT, "app", "node_modules", "better-sqlite3");
const appBinary = join(
ROOT,
"app",
"node_modules",
"better-sqlite3",
"build",
"Release",
"better_sqlite3.node"
);
const rootBinary = join(
ROOT,
"node_modules",
"better-sqlite3",
"build",
"Release",
"better_sqlite3.node"
);
if (!existsSync(appNodeModules)) {
// No bundled better-sqlite3 — nothing to do (dev install, not npm global)
if (!existsSync(join(ROOT, "app", "node_modules", "better-sqlite3"))) {
process.exit(0);
}
const buildInfoPath = join(appNodeModules, "build", "Release", "better_sqlite3.node");
const platformMatch =
process.platform === PUBLISHED_BUILD_PLATFORM && process.arch === PUBLISHED_BUILD_ARCH;
// Quick check: try to load the native module
try {
// Use a dynamic import-like approach — try to dlopen the .node file
process.dlopen({ exports: {} }, buildInfoPath);
// If it loaded, the binary is compatible — nothing to do
process.exit(0);
} catch {
// Binary is incompatible — rebuild
if (platformMatch) {
try {
process.dlopen({ exports: {} }, appBinary);
process.exit(0);
} catch (err) {
console.warn(` ⚠️ Bundled binary incompatible despite platform match: ${err.message}`);
}
}
console.log(`\n 🔧 Rebuilding better-sqlite3 for ${process.platform}-${process.arch}...`);
console.log(`\n 🔧 Fixing better-sqlite3 binary for ${process.platform}-${process.arch}...`);
// Strategy 1: Copy the correctly-built binary from root node_modules
if (existsSync(rootBinary)) {
try {
mkdirSync(dirname(appBinary), { recursive: true });
copyFileSync(rootBinary, appBinary);
} catch (err) {
console.warn(` ⚠️ Failed to copy binary: ${err.message}`);
}
try {
process.dlopen({ exports: {} }, appBinary);
console.log(" ✅ Native module fixed successfully!\n");
process.exit(0);
} catch (err) {
console.warn(` ⚠️ Copied binary failed to load: ${err.message}`);
}
}
// Strategy 2: Fall back to npm rebuild (may work if build tools are available)
console.log(" ⚠️ Root binary not available or incompatible, attempting npm rebuild...");
try {
const { execSync } = await import("node:child_process");
execSync("npm rebuild better-sqlite3", {
cwd: join(ROOT, "app"),
stdio: "inherit",
timeout: 120_000,
});
process.dlopen({ exports: {} }, appBinary);
console.log(" ✅ Native module rebuilt successfully!\n");
} catch (error) {
console.warn(" ⚠️ Failed to rebuild better-sqlite3 automatically.");
console.warn(" You can fix this manually by running:");
console.warn(` cd ${join(ROOT, "app")} && npm rebuild better-sqlite3\n`);
// Don't fail the install — the user can fix manually
process.exit(0);
} catch (err) {
const isTimeout = err.killed || err.signal === "SIGTERM";
if (isTimeout) {
console.warn(" ⚠️ npm rebuild timed out after 120s.");
} else {
console.warn(` ⚠️ npm rebuild failed: ${err.message}`);
}
}
// If nothing worked, warn but don't fail the install — let the package stay
// installed so users can fix manually or use the pre-flight check in the CLI
console.warn(" ⚠️ Could not fix better-sqlite3 native module automatically.");
console.warn(" The server may not start correctly.");
console.warn(" Try manually:");
console.warn(` cd ${join(ROOT, "app")} && npm rebuild better-sqlite3`);
if (process.platform === "darwin") {
console.warn(" If build tools are missing: xcode-select --install");
}
console.warn("");
// ── @swc/helpers fix ────────────────────────────────────────────────────────
// Next.js standalone tracer doesn't always include @swc/helpers in app/node_modules/,
// causing a MODULE_NOT_FOUND crash at runtime. Copy it from root node_modules if needed.
const swcHelpersApp = join(ROOT, "app", "node_modules", "@swc", "helpers");
const swcHelpersRoot = join(ROOT, "node_modules", "@swc", "helpers");
if (!existsSync(swcHelpersApp)) {
if (existsSync(swcHelpersRoot)) {
try {
const { cpSync } = await import("node:fs");
mkdirSync(join(ROOT, "app", "node_modules", "@swc"), { recursive: true });
cpSync(swcHelpersRoot, swcHelpersApp, { recursive: true });
console.log(" ✅ @swc/helpers copied to standalone app/node_modules.\n");
} catch (err) {
console.warn(` ⚠️ Could not copy @swc/helpers: ${err.message}`);
console.warn(
" Try manually: cp -r node_modules/@swc/helpers app/node_modules/@swc/helpers\n"
);
}
} else {
console.warn(" ⚠️ @swc/helpers not found in root node_modules either.");
console.warn(" Try: npm install --save-exact @swc/helpers@0.5.19\n");
}
}
+12
View File
@@ -122,6 +122,18 @@ if (existsSync(sharedApiKey)) {
// ── Step 10: Ensure data/ directory exists ──────────────────
mkdirSync(join(APP_DIR, "data"), { recursive: true });
// ── Step 10.5: Copy @swc/helpers into standalone ───────────
// Next.js standalone tracer sometimes omits @swc/helpers from app/node_modules/,
// causing MODULE_NOT_FOUND at runtime. Always copy it explicitly.
const swcHelpersSrc = join(ROOT, "node_modules", "@swc", "helpers");
const swcHelpersDst = join(APP_DIR, "node_modules", "@swc", "helpers");
if (existsSync(swcHelpersSrc) && !existsSync(swcHelpersDst)) {
console.log(" 📋 Copying @swc/helpers to standalone app/node_modules...");
mkdirSync(join(APP_DIR, "node_modules", "@swc"), { recursive: true });
cpSync(swcHelpersSrc, swcHelpersDst, { recursive: true });
console.log(" ✅ @swc/helpers included in standalone build.");
}
// ── Done ───────────────────────────────────────────────────
const appPkg = join(APP_DIR, "package.json");
if (existsSync(appPkg)) {
+5 -1
View File
@@ -5,12 +5,16 @@ import {
withRuntimePortEnv,
spawnWithForwardedSignals,
} from "./runtime-env.mjs";
import { bootstrapEnv } from "./bootstrap-env.mjs";
const mode = process.argv[2] === "start" ? "start" : "dev";
const runtimePorts = resolveRuntimePorts();
const { dashboardPort } = runtimePorts;
// Auto-generate secrets on first run, merge .env + process.env
const env = bootstrapEnv();
const args = ["./node_modules/next/dist/bin/next", mode, "--port", String(dashboardPort)];
if (mode === "dev") {
args.splice(2, 0, "--webpack");
@@ -18,5 +22,5 @@ if (mode === "dev") {
spawnWithForwardedSignals(process.execPath, args, {
stdio: "inherit",
env: withRuntimePortEnv(process.env, runtimePorts),
env: withRuntimePortEnv(env, runtimePorts),
});
+5 -1
View File
@@ -5,10 +5,14 @@ import {
withRuntimePortEnv,
spawnWithForwardedSignals,
} from "./runtime-env.mjs";
import { bootstrapEnv } from "./bootstrap-env.mjs";
const runtimePorts = resolveRuntimePorts();
// Auto-generate secrets on first run, merge .env + process.env
const env = bootstrapEnv();
spawnWithForwardedSignals("node", ["server.js"], {
stdio: "inherit",
env: withRuntimePortEnv(process.env, runtimePorts),
env: withRuntimePortEnv(env, runtimePorts),
});
@@ -0,0 +1,48 @@
"use client";
import { useState } from "react";
/**
* Shown when OmniRoute was started with auto-generated secrets (zero-config mode).
* The banner is dismissable and persists only for the current session.
*/
export default function BootstrapBanner() {
const [dismissed, setDismissed] = useState(false);
if (dismissed) return null;
// Determine default data dir hint based on platform hint from user-agent
const dataDir =
typeof navigator !== "undefined" && navigator.platform?.startsWith("Win")
? "%APPDATA%\\omniroute\\server.env"
: "~/.omniroute/server.env";
return (
<div
role="alert"
className="flex items-start gap-3 rounded-lg border border-amber-500/30 bg-amber-500/10 px-4 py-3 text-sm text-amber-200 mb-4"
>
<span className="text-amber-400 text-base shrink-0 mt-0.5"></span>
<div className="flex-1 min-w-0">
<p className="font-semibold text-amber-300">Running in zero-config mode</p>
<p className="mt-0.5 text-amber-200/80">
OmniRoute auto-generated secure encryption keys on first launch. They are persisted to{" "}
<code className="font-mono bg-amber-500/20 px-1 rounded text-xs">{dataDir}</code>. No
action is required your data is encrypted and safe. To use custom keys, add{" "}
<code className="font-mono bg-amber-500/20 px-1 rounded text-xs">JWT_SECRET</code> and{" "}
<code className="font-mono bg-amber-500/20 px-1 rounded text-xs">
STORAGE_ENCRYPTION_KEY
</code>{" "}
to that file.
</p>
</div>
<button
onClick={() => setDismissed(true)}
className="shrink-0 text-amber-400/60 hover:text-amber-300 transition-colors ml-1"
aria-label="Dismiss"
>
</button>
</div>
);
}
@@ -976,7 +976,7 @@ function ComboCard({
onChange={onToggle}
title={isDisabled ? t("enableCombo") : t("disableCombo")}
/>
<div className="flex items-center gap-1 opacity-100 md:opacity-0 md:group-hover:opacity-100 transition-opacity">
<div className="flex items-center gap-1 transition-opacity">
<button
onClick={onTest}
disabled={testing}
@@ -86,7 +86,8 @@ export default function APIPageClient({ machineId }) {
(m) => m.type === "audio" && m.subtype === "speech" && !m.parent
);
const moderation = allModels.filter((m) => m.type === "moderation" && !m.parent);
return { chat, embeddings, images, rerank, audioTranscription, audioSpeech, moderation };
const music = allModels.filter((m) => m.type === "music" && !m.parent);
return { chat, embeddings, images, rerank, audioTranscription, audioSpeech, moderation, music };
}, [allModels]);
const postCloudAction = async (action, timeoutMs = CLOUD_ACTION_TIMEOUT_MS) => {
@@ -392,6 +393,7 @@ export default function APIPageClient({ machineId }) {
endpointData.audioTranscription,
endpointData.audioSpeech,
endpointData.moderation,
endpointData.music,
].filter((a) => a.length > 0).length + 2,
})}
</p>
@@ -530,6 +532,25 @@ export default function APIPageClient({ machineId }) {
copied={copied}
baseUrl={currentEndpoint}
/>
{/* Music Generation */}
<EndpointSection
icon="music_note"
iconColor="text-fuchsia-500"
iconBg="bg-fuchsia-500/10"
title={t("musicGeneration") || "Music Generation"}
path="/v1/music/generations"
description={
t("musicDesc") ||
"Generate music and audio tracks via ComfyUI (Stable Audio, MusicGen)"
}
models={endpointData.music}
expanded={expandedEndpoint === "music"}
onToggle={() => setExpandedEndpoint(expandedEndpoint === "music" ? null : "music")}
copy={copy}
copied={copied}
baseUrl={currentEndpoint}
/>
</div>
</div>
+8 -1
View File
@@ -2,6 +2,7 @@ import { redirect } from "next/navigation";
import { getMachineId } from "@/shared/utils/machine";
import { getSettings } from "@/lib/localDb";
import HomePageClient from "./HomePageClient";
import BootstrapBanner from "./BootstrapBanner";
// Must be dynamic — depends on DB state (setupComplete) that changes at runtime
export const dynamic = "force-dynamic";
@@ -12,5 +13,11 @@ export default async function DashboardPage() {
redirect("/dashboard/onboarding");
}
const machineId = await getMachineId();
return <HomePageClient machineId={machineId} />;
const isBootstrapped = process.env.OMNIROUTE_BOOTSTRAPPED === "true";
return (
<>
{isBootstrapped && <BootstrapBanner />}
<HomePageClient machineId={machineId} />
</>
);
}
@@ -1341,6 +1341,7 @@ PassthroughModelRow.propTypes = {
function CustomModelsSection({ providerId, providerAlias, copied, onCopy }) {
const t = useTranslations("providers");
const notify = useNotificationStore();
const [customModels, setCustomModels] = useState([]);
const [newModelId, setNewModelId] = useState("");
const [newModelName, setNewModelName] = useState("");
@@ -1348,6 +1349,10 @@ function CustomModelsSection({ providerId, providerAlias, copied, onCopy }) {
const [newEndpoints, setNewEndpoints] = useState(["chat"]);
const [adding, setAdding] = useState(false);
const [loading, setLoading] = useState(true);
const [editingModelId, setEditingModelId] = useState<string | null>(null);
const [editingApiFormat, setEditingApiFormat] = useState("chat-completions");
const [editingEndpoints, setEditingEndpoints] = useState<string[]>(["chat"]);
const [savingModelId, setSavingModelId] = useState<string | null>(null);
const fetchCustomModels = useCallback(async () => {
try {
@@ -1410,6 +1415,61 @@ function CustomModelsSection({ providerId, providerAlias, copied, onCopy }) {
}
};
const beginEdit = (model) => {
setEditingModelId(model.id);
setEditingApiFormat(model.apiFormat || "chat-completions");
setEditingEndpoints(
Array.isArray(model.supportedEndpoints) && model.supportedEndpoints.length
? model.supportedEndpoints
: ["chat"]
);
};
const cancelEdit = () => {
setEditingModelId(null);
setEditingApiFormat("chat-completions");
setEditingEndpoints(["chat"]);
setSavingModelId(null);
};
const saveEdit = async (modelId) => {
if (!editingModelId || editingModelId !== modelId) return;
if (!editingEndpoints.length) {
notify.error("Select at least one supported endpoint");
return;
}
setSavingModelId(modelId);
try {
const model = customModels.find((m) => m.id === modelId);
const res = await fetch("/api/provider-models", {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
provider: providerId,
modelId,
modelName: model?.name || modelId,
source: model?.source || "manual",
apiFormat: editingApiFormat,
supportedEndpoints: editingEndpoints,
}),
});
if (!res.ok) {
throw new Error("Failed to save model endpoint settings");
}
await fetchCustomModels();
notify.success("Saved model endpoint settings");
cancelEdit();
} catch (e) {
console.error("Failed to save custom model:", e);
notify.error("Failed to save model endpoint settings");
} finally {
setSavingModelId(null);
}
};
return (
<div className="mt-6 pt-6 border-t border-border">
<h3 className="text-sm font-semibold mb-3 flex items-center gap-2">
@@ -1554,14 +1614,89 @@ function CustomModelsSection({ providerId, providerAlias, copied, onCopy }) {
</span>
)}
</div>
{editingModelId === model.id && (
<div className="mt-3 p-3 rounded-lg border border-border bg-sidebar/40">
<div className="flex items-end gap-3 flex-wrap">
<div className="w-44">
<label className="text-xs text-text-muted mb-1 block">API Format</label>
<select
value={editingApiFormat}
onChange={(e) => setEditingApiFormat(e.target.value)}
className="w-full px-2.5 py-2 text-xs border border-border rounded-lg bg-background focus:outline-none focus:border-primary"
>
<option value="chat-completions">Chat Completions</option>
<option value="responses">Responses API</option>
</select>
</div>
<div className="flex-1 min-w-[240px]">
<span className="text-xs text-text-muted mb-1 block">
Supported Endpoints
</span>
<div className="flex items-center gap-3 flex-wrap">
{["chat", "embeddings", "images", "audio"].map((ep) => (
<label
key={ep}
className="flex items-center gap-1.5 text-xs text-text-main cursor-pointer"
>
<input
type="checkbox"
checked={editingEndpoints.includes(ep)}
onChange={(e) => {
if (e.target.checked) {
setEditingEndpoints((prev) =>
prev.includes(ep) ? prev : [...prev, ep]
);
} else {
setEditingEndpoints((prev) => prev.filter((x) => x !== ep));
}
}}
className="rounded border-border"
/>
{ep === "chat"
? "💬 Chat"
: ep === "embeddings"
? "📐 Embeddings"
: ep === "images"
? "🖼️ Images"
: "🔊 Audio"}
</label>
))}
</div>
</div>
</div>
<div className="mt-3 flex items-center gap-2">
<Button
size="sm"
onClick={() => saveEdit(model.id)}
disabled={savingModelId === model.id}
>
{savingModelId === model.id ? t("saving") : t("save")}
</Button>
<Button size="sm" variant="ghost" onClick={cancelEdit}>
{t("cancel")}
</Button>
</div>
</div>
)}
</div>
<div className="flex items-center gap-1">
<button
onClick={() => beginEdit(model)}
className="p-1 hover:bg-sidebar rounded text-text-muted hover:text-primary"
title={t("edit")}
>
<span className="material-symbols-outlined text-sm">edit</span>
</button>
<button
onClick={() => handleRemove(model.id)}
className="p-1 hover:bg-red-50 rounded text-red-500"
title={t("removeCustomModel")}
>
<span className="material-symbols-outlined text-sm">delete</span>
</button>
</div>
<button
onClick={() => handleRemove(model.id)}
className="p-1 hover:bg-red-50 rounded text-red-500"
title={t("removeCustomModel")}
>
<span className="material-symbols-outlined text-sm">delete</span>
</button>
</div>
);
})}
@@ -2184,7 +2319,7 @@ function ConnectionRow({
onChange={onToggleActive}
title={(connection.isActive ?? true) ? t("disableConnection") : t("enableConnection")}
/>
<div className="flex gap-1 ml-1 opacity-0 group-hover:opacity-100 transition-opacity">
<div className="flex gap-1 ml-1 transition-opacity">
{onReauth && (
<button
onClick={onReauth}
@@ -189,23 +189,43 @@ export default function ProvidersPage() {
if (testingMode) return;
setTestingMode(mode === "provider" ? providerId : mode);
setTestResults(null);
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 90_000); // 90s max
try {
const res = await fetch("/api/providers/test-batch", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ mode, providerId }),
signal: controller.signal,
});
const data = await res.json();
setTestResults(data);
if (data.summary) {
let data: any;
try {
data = await res.json();
} catch {
// Response body is not valid JSON (e.g. truncated due to timeout)
data = { error: t("providerTestFailed"), results: [], summary: null };
}
setTestResults({
...data,
// Normalize error: if API returns an error object { message, details }, extract the string
error: data.error
? typeof data.error === "object"
? data.error.message || data.error.error || JSON.stringify(data.error)
: String(data.error)
: null,
});
if (data?.summary) {
const { passed, failed, total } = data.summary;
if (failed === 0) notify.success(t("allTestsPassed", { total }));
else notify.warning(t("testSummary", { passed, failed, total }));
}
} catch (error) {
setTestResults({ error: t("providerTestFailed") });
notify.error(t("providerTestFailed"));
} catch (error: any) {
const isAbort = error?.name === "AbortError";
const msg = isAbort ? t("providerTestTimeout") : t("providerTestFailed");
setTestResults({ error: msg, results: [], summary: null });
notify.error(msg);
} finally {
clearTimeout(timeoutId);
setTestingMode(null);
}
};
@@ -470,8 +490,17 @@ function ProviderCard({ providerId, provider, stats, authType, onToggle }) {
const t = useTranslations("providers");
const tc = useTranslations("common");
const { connected, error, errorCode, errorTime, allDisabled } = stats;
const [imgSrc, setImgSrc] = useState(`/providers/${provider.id}.png`);
const [imgError, setImgError] = useState(false);
const handleImgError = () => {
if (imgSrc.endsWith(".png")) {
setImgSrc(`/providers/${provider.id}.svg`);
} else {
setImgError(true);
}
};
const dotColors = {
free: "bg-green-500",
oauth: "bg-blue-500",
@@ -503,13 +532,13 @@ function ProviderCard({ providerId, provider, stats, authType, onToggle }) {
</span>
) : (
<Image
src={`/providers/${provider.id}.png`}
src={imgSrc}
alt={provider.name}
width={30}
height={30}
className="object-contain rounded-lg max-w-[32px] max-h-[32px]"
sizes="32px"
onError={() => setImgError(true)}
onError={handleImgError}
/>
)}
</div>
@@ -590,7 +619,6 @@ function ApiKeyProviderCard({ providerId, provider, stats, authType, onToggle })
const { connected, error, errorCode, errorTime, allDisabled } = stats;
const isCompatible = providerId.startsWith(OPENAI_COMPATIBLE_PREFIX);
const isAnthropicCompatible = providerId.startsWith(ANTHROPIC_COMPATIBLE_PREFIX);
const [imgError, setImgError] = useState(false);
const dotColors = {
free: "bg-green-500",
@@ -616,6 +644,18 @@ function ApiKeyProviderCard({ providerId, provider, stats, authType, onToggle })
return `/providers/${provider.id}.png`;
};
const [imgSrc, setImgSrc] = useState<string>(() => getIconPath());
const [imgError, setImgError] = useState(false);
const handleImgError = () => {
const basePath = getIconPath();
if (imgSrc.endsWith(".png") && !isCompatible && !isAnthropicCompatible) {
setImgSrc(`/providers/${provider.id}.svg`);
} else {
setImgError(true);
}
};
return (
<Link href={`/dashboard/providers/${providerId}`} className="group">
<Card
@@ -634,13 +674,13 @@ function ApiKeyProviderCard({ providerId, provider, stats, authType, onToggle })
</span>
) : (
<Image
src={getIconPath()}
src={imgSrc || getIconPath()}
alt={provider.name}
width={30}
height={30}
className="object-contain rounded-lg max-w-[30px] max-h-[30px]"
sizes="30px"
onError={() => setImgError(true)}
onError={handleImgError}
/>
)}
</div>
@@ -1041,17 +1081,27 @@ function ProviderTestResultsView({ results }) {
const t = useTranslations("providers");
const tc = useTranslations("common");
if (results.error && !results.results) {
// Guard: never crash on malformed/null results (would trigger error boundary)
if (!results || typeof results !== "object") {
return null;
}
if (results.error && (!results.results || results.results.length === 0)) {
return (
<div className="text-center py-6">
<span className="material-symbols-outlined text-red-500 text-[32px] mb-2 block">error</span>
<p className="text-sm text-red-400">{results.error}</p>
<p className="text-sm text-red-400">
{typeof results.error === "object"
? results.error?.message || JSON.stringify(results.error)
: String(results.error)}
</p>
</div>
);
}
const { summary, mode } = results;
const items = results.results || [];
const summary = results.summary ?? null;
const mode = results.mode ?? "";
const items = Array.isArray(results.results) ? results.results : [];
const modeLabel =
{
@@ -226,6 +226,12 @@ export async function POST(
exchangeTokens(provider, code, redirectUri, codeVerifier, state)
);
// Normalize: if name is missing, use email or displayName as fallback so accounts
// always show a real label (e.g. user@gmail.com) instead of "Account #abc123"
if (!tokenData.name && (tokenData.email || tokenData.displayName)) {
tokenData.name = tokenData.email || tokenData.displayName;
}
// Upsert: update existing connection if same provider+email, else create new
const expiresAt = tokenData.expiresIn
? new Date(Date.now() + tokenData.expiresIn * 1000).toISOString()
@@ -297,6 +303,11 @@ export async function POST(
}
if (result.success) {
// Normalize: if name is missing, use email as fallback display label
if (!result.tokens.name && (result.tokens.email || result.tokens.displayName)) {
result.tokens.name = result.tokens.email || result.tokens.displayName;
}
// Upsert: update existing connection if same provider+email, else create new
const expiresAt = result.tokens.expiresIn
? new Date(Date.now() + result.tokens.expiresIn * 1000).toISOString()
@@ -418,6 +429,11 @@ export async function POST(
exchangeTokens(provider, params.code, redirectUri, codeVerifier, params.state)
);
// Normalize: if name is missing, use email as fallback display label
if (!tokenData.name && (tokenData.email || tokenData.displayName)) {
tokenData.name = tokenData.email || tokenData.displayName;
}
// Upsert: update existing connection if same provider+email, else create new
const expiresAt = tokenData.expiresIn
? new Date(Date.now() + tokenData.expiresIn * 1000).toISOString()
+54
View File
@@ -3,6 +3,7 @@ import {
getAllCustomModels,
addCustomModel,
removeCustomModel,
updateCustomModel,
} from "@/lib/localDb";
import { isAuthenticated } from "@/shared/utils/apiAuth";
import { providerModelMutationSchema } from "@/shared/validation/schemas";
@@ -84,6 +85,59 @@ export async function POST(request) {
}
}
/**
* PUT /api/provider-models
* Body: { provider, modelId, modelName?, apiFormat?, supportedEndpoints? }
*/
export async function PUT(request) {
let rawBody;
try {
rawBody = await request.json();
} catch {
return Response.json(
{ error: { message: "Invalid JSON body", type: "validation_error" } },
{ status: 400 }
);
}
try {
if (!(await isAuthenticated(request))) {
return Response.json(
{ error: { message: "Authentication required", type: "invalid_api_key" } },
{ status: 401 }
);
}
const validation = validateBody(providerModelMutationSchema, rawBody);
if (isValidationFailure(validation)) {
return Response.json({ error: validation.error }, { status: 400 });
}
const { provider, modelId, modelName, apiFormat, supportedEndpoints } = validation.data;
const model = await updateCustomModel(provider, modelId, {
modelName,
apiFormat,
supportedEndpoints,
});
if (!model) {
return Response.json(
{ error: { message: "Model not found", type: "not_found" } },
{ status: 404 }
);
}
return Response.json({ model });
} catch (error) {
console.error("Error updating provider model:", error);
return Response.json(
{ error: { message: "Failed to update provider model", type: "server_error" } },
{ status: 500 }
);
}
}
/**
* DELETE /api/provider-models?provider=<id>&model=<modelId>
*/
+69 -4
View File
@@ -138,7 +138,11 @@
"media": "الإعلام",
"mediaDescription": "إنشاء الصور ومقاطع الفيديو والموسيقى",
"themes": "المواضيع",
"themesDescription": "اختر سمة لون للوحة المعلومات بأكملها"
"themesDescription": "اختر سمة لون للوحة المعلومات بأكملها",
"mcp": "MCP",
"mcpDescription": "Model Context Protocol server management and tools",
"a2a": "A2A",
"a2aDescription": "Agent-to-Agent protocol tasks and observability"
},
"home": {
"quickStart": "بداية سريعة",
@@ -456,7 +460,9 @@
"cline": "Cline AI Coding Assistant CLI",
"kilo": "كيلو كود AI مساعد CLI",
"cursor": "محرر كود المؤشر AI",
"continue": "تابع مساعد الذكاء الاصطناعي"
"continue": "تابع مساعد الذكاء الاصطناعي",
"opencode": "OpenCode AI coding agent (Terminal)",
"kiro": "Amazon Kiro — AI-powered IDE"
},
"guides": {
"cursor": {
@@ -505,6 +511,42 @@
"desc": "أضف التكوين التالي إلى مجموعة النماذج الخاصة بك:"
}
}
},
"opencode": {
"steps": {
"1": {
"title": "Install OpenCode",
"desc": "Install via npm: npm install -g opencode-ai"
},
"2": {
"title": "API Key"
},
"3": {
"title": "Set Base URL",
"desc": "opencode config set baseUrl {{baseUrl}}"
},
"4": {
"title": "Select Model"
}
}
},
"kiro": {
"steps": {
"1": {
"title": "Open Kiro Settings",
"desc": "Go to Settings → AI Provider"
},
"2": {
"title": "Base URL",
"desc": "Paste your OmniRoute endpoint URL"
},
"3": {
"title": "API Key"
},
"4": {
"title": "Select Model"
}
}
}
}
},
@@ -1338,7 +1380,8 @@
"editCompatibleTitle": "تحرير {type} متوافق",
"compatibleBaseUrlHint": "استخدم عنوان URL الأساسي (الذي ينتهي بـ /v1) لواجهة برمجة التطبيقات المتوافقة مع {type}.",
"apiKeyForCheck": "مفتاح API (للفحص)",
"compatibleProdPlaceholder": "{type} متوافق (المنتج)"
"compatibleProdPlaceholder": "{type} متوافق (المنتج)",
"providerTestTimeout": "Provider test timed out — too many connections to test at once"
},
"settings": {
"title": "الإعدادات",
@@ -1734,7 +1777,12 @@
"customPricingNote": "يمكنك تجاوز التسعير الافتراضي لنماذج محددة. تحظى التجاوزات المخصصة بالأولوية على الأسعار التي يتم اكتشافها تلقائيًا.",
"editPricing": "تحرير التسعير",
"viewFullDetails": "عرض التفاصيل الكاملة",
"themeCoral": "مرجاني"
"themeCoral": "مرجاني",
"cliFingerprint": "CLI Fingerprint Matching",
"cliFingerprintDesc": "Match native CLI binary signatures when proxying requests. Reorders headers and body fields to look identical to the official CLI tools. Your proxy IP is preserved.",
"cliFingerprintEnabled": "{count} provider(s) with CLI fingerprint active",
"enableFingerprintTitle": "Enable fingerprint for {provider}",
"disableFingerprintTitle": "Disable fingerprint for {provider}"
},
"translator": {
"title": "مترجم",
@@ -2419,5 +2467,22 @@
"featureWebhooks": "تكوين Webhooks واشتراكات الأحداث",
"featureSwagger": "إنشاء مواصفات OpenAPI / Swagger تلقائياً",
"featureAuth": "إدارة مفاتيح API ونطاقات OAuth لكل نقطة نهاية"
},
"agents": {
"title": "CLI Agents",
"description": "Discover installed CLI agents on your system. Add custom agents for auto-detection.",
"refresh": "Refresh",
"installed": "Installed",
"notFound": "Not Found",
"builtIn": "Built-in",
"custom": "Custom",
"remove": "Remove",
"addCustomAgent": "Add Custom Agent",
"addCustomAgentDesc": "Register any CLI tool for detection. It will be scanned automatically on refresh.",
"agentName": "Agent Name",
"binaryName": "Binary Name",
"versionCommand": "Version Command",
"spawnArgs": "Spawn Args",
"addAgent": "Add Agent"
}
}
+69 -4
View File
@@ -138,7 +138,11 @@
"media": "Медия",
"mediaDescription": "Генериране на изображения, видеоклипове и музика",
"themes": "Теми",
"themesDescription": "Изберете цветова тема за целия панел на таблото"
"themesDescription": "Изберете цветова тема за целия панел на таблото",
"mcp": "MCP",
"mcpDescription": "Model Context Protocol server management and tools",
"a2a": "A2A",
"a2aDescription": "Agent-to-Agent protocol tasks and observability"
},
"home": {
"quickStart": "Бърз старт",
@@ -456,7 +460,9 @@
"cline": "Cline AI Coding Assistant CLI",
"kilo": "Kilo Code AI Assistant CLI",
"cursor": "Cursor AI Code Editor",
"continue": "Продължете AI Assistant"
"continue": "Продължете AI Assistant",
"opencode": "OpenCode AI coding agent (Terminal)",
"kiro": "Amazon Kiro — AI-powered IDE"
},
"guides": {
"cursor": {
@@ -505,6 +511,42 @@
"desc": "Добавете следната конфигурация към вашия масив от модели:"
}
}
},
"opencode": {
"steps": {
"1": {
"title": "Install OpenCode",
"desc": "Install via npm: npm install -g opencode-ai"
},
"2": {
"title": "API Key"
},
"3": {
"title": "Set Base URL",
"desc": "opencode config set baseUrl {{baseUrl}}"
},
"4": {
"title": "Select Model"
}
}
},
"kiro": {
"steps": {
"1": {
"title": "Open Kiro Settings",
"desc": "Go to Settings → AI Provider"
},
"2": {
"title": "Base URL",
"desc": "Paste your OmniRoute endpoint URL"
},
"3": {
"title": "API Key"
},
"4": {
"title": "Select Model"
}
}
}
}
},
@@ -1338,7 +1380,8 @@
"editCompatibleTitle": "Редактиране {type} Съвместим",
"compatibleBaseUrlHint": "Използвайте основния URL (завършващ на /v1) за вашия {type}-съвместим API.",
"apiKeyForCheck": "API ключ (за проверка)",
"compatibleProdPlaceholder": "{type} Съвместим (Prod)"
"compatibleProdPlaceholder": "{type} Съвместим (Prod)",
"providerTestTimeout": "Provider test timed out — too many connections to test at once"
},
"settings": {
"title": "Настройки",
@@ -1734,7 +1777,12 @@
"customPricingNote": "Можете да замените цените по подразбиране за конкретни модели. Персонализираните замени имат приоритет пред автоматично разпознатото ценообразуване.",
"editPricing": "Редактиране на цените",
"viewFullDetails": "Вижте пълните подробности",
"themeCoral": "Корал"
"themeCoral": "Корал",
"cliFingerprint": "CLI Fingerprint Matching",
"cliFingerprintDesc": "Match native CLI binary signatures when proxying requests. Reorders headers and body fields to look identical to the official CLI tools. Your proxy IP is preserved.",
"cliFingerprintEnabled": "{count} provider(s) with CLI fingerprint active",
"enableFingerprintTitle": "Enable fingerprint for {provider}",
"disableFingerprintTitle": "Disable fingerprint for {provider}"
},
"translator": {
"title": "Преводач",
@@ -2419,5 +2467,22 @@
"featureWebhooks": "Webhook configuration and event subscriptions",
"featureSwagger": "OpenAPI / Swagger spec auto-generation",
"featureAuth": "API key and OAuth scope management per endpoint"
},
"agents": {
"title": "CLI Agents",
"description": "Discover installed CLI agents on your system. Add custom agents for auto-detection.",
"refresh": "Refresh",
"installed": "Installed",
"notFound": "Not Found",
"builtIn": "Built-in",
"custom": "Custom",
"remove": "Remove",
"addCustomAgent": "Add Custom Agent",
"addCustomAgentDesc": "Register any CLI tool for detection. It will be scanned automatically on refresh.",
"agentName": "Agent Name",
"binaryName": "Binary Name",
"versionCommand": "Version Command",
"spawnArgs": "Spawn Args",
"addAgent": "Add Agent"
}
}
+69 -4
View File
@@ -138,7 +138,11 @@
"media": "Medie",
"mediaDescription": "Generer billeder, videoer og musik",
"themes": "Temaer",
"themesDescription": "Vælg et farvetema til hele dashboardpanelet"
"themesDescription": "Vælg et farvetema til hele dashboardpanelet",
"mcp": "MCP",
"mcpDescription": "Model Context Protocol server management and tools",
"a2a": "A2A",
"a2aDescription": "Agent-to-Agent protocol tasks and observability"
},
"home": {
"quickStart": "Hurtig start",
@@ -456,7 +460,9 @@
"cline": "Cline AI Coding Assistant CLI",
"kilo": "Kilokode AI Assistant CLI",
"cursor": "Cursor AI Code Editor",
"continue": "Fortsæt AI Assistant"
"continue": "Fortsæt AI Assistant",
"opencode": "OpenCode AI coding agent (Terminal)",
"kiro": "Amazon Kiro — AI-powered IDE"
},
"guides": {
"cursor": {
@@ -505,6 +511,42 @@
"desc": "Tilføj følgende konfiguration til dit modelarray:"
}
}
},
"opencode": {
"steps": {
"1": {
"title": "Install OpenCode",
"desc": "Install via npm: npm install -g opencode-ai"
},
"2": {
"title": "API Key"
},
"3": {
"title": "Set Base URL",
"desc": "opencode config set baseUrl {{baseUrl}}"
},
"4": {
"title": "Select Model"
}
}
},
"kiro": {
"steps": {
"1": {
"title": "Open Kiro Settings",
"desc": "Go to Settings → AI Provider"
},
"2": {
"title": "Base URL",
"desc": "Paste your OmniRoute endpoint URL"
},
"3": {
"title": "API Key"
},
"4": {
"title": "Select Model"
}
}
}
}
},
@@ -1338,7 +1380,8 @@
"editCompatibleTitle": "Rediger {type} Kompatibel",
"compatibleBaseUrlHint": "Brug basis-URL'en (der slutter på /v1) til din {type}-kompatible API.",
"apiKeyForCheck": "API-nøgle (til check)",
"compatibleProdPlaceholder": "{type} Kompatibel (Prod)"
"compatibleProdPlaceholder": "{type} Kompatibel (Prod)",
"providerTestTimeout": "Provider test timed out — too many connections to test at once"
},
"settings": {
"title": "Indstillinger",
@@ -1734,7 +1777,12 @@
"customPricingNote": "Du kan tilsidesætte standardpriser for specifikke modeller. Tilpassede tilsidesættelser har prioritet frem for automatisk registrerede priser.",
"editPricing": "Rediger prissætning",
"viewFullDetails": "Se alle detaljer",
"themeCoral": "Koral"
"themeCoral": "Koral",
"cliFingerprint": "CLI Fingerprint Matching",
"cliFingerprintDesc": "Match native CLI binary signatures when proxying requests. Reorders headers and body fields to look identical to the official CLI tools. Your proxy IP is preserved.",
"cliFingerprintEnabled": "{count} provider(s) with CLI fingerprint active",
"enableFingerprintTitle": "Enable fingerprint for {provider}",
"disableFingerprintTitle": "Disable fingerprint for {provider}"
},
"translator": {
"title": "Oversætter",
@@ -2419,5 +2467,22 @@
"featureWebhooks": "Webhook configuration and event subscriptions",
"featureSwagger": "OpenAPI / Swagger spec auto-generation",
"featureAuth": "API key and OAuth scope management per endpoint"
},
"agents": {
"title": "CLI Agents",
"description": "Discover installed CLI agents on your system. Add custom agents for auto-detection.",
"refresh": "Refresh",
"installed": "Installed",
"notFound": "Not Found",
"builtIn": "Built-in",
"custom": "Custom",
"remove": "Remove",
"addCustomAgent": "Add Custom Agent",
"addCustomAgentDesc": "Register any CLI tool for detection. It will be scanned automatically on refresh.",
"agentName": "Agent Name",
"binaryName": "Binary Name",
"versionCommand": "Version Command",
"spawnArgs": "Spawn Args",
"addAgent": "Add Agent"
}
}
+69 -4
View File
@@ -138,7 +138,11 @@
"media": "Medien",
"mediaDescription": "Generieren Sie Bilder, Videos und Musik",
"themes": "Themen",
"themesDescription": "Wählen Sie ein Farbthema für das gesamte Dashboard-Panel"
"themesDescription": "Wählen Sie ein Farbthema für das gesamte Dashboard-Panel",
"mcp": "MCP",
"mcpDescription": "Model Context Protocol server management and tools",
"a2a": "A2A",
"a2aDescription": "Agent-to-Agent protocol tasks and observability"
},
"home": {
"quickStart": "Schnellstart",
@@ -456,7 +460,9 @@
"cline": "Cline AI Coding Assistant CLI",
"kilo": "Kilo Code AI Assistant CLI",
"cursor": "Cursor-KI-Code-Editor",
"continue": "Weiter AI Assistant"
"continue": "Weiter AI Assistant",
"opencode": "OpenCode AI coding agent (Terminal)",
"kiro": "Amazon Kiro — AI-powered IDE"
},
"guides": {
"cursor": {
@@ -505,6 +511,42 @@
"desc": "Fügen Sie Ihrem Modellarray die folgende Konfiguration hinzu:"
}
}
},
"opencode": {
"steps": {
"1": {
"title": "Install OpenCode",
"desc": "Install via npm: npm install -g opencode-ai"
},
"2": {
"title": "API Key"
},
"3": {
"title": "Set Base URL",
"desc": "opencode config set baseUrl {{baseUrl}}"
},
"4": {
"title": "Select Model"
}
}
},
"kiro": {
"steps": {
"1": {
"title": "Open Kiro Settings",
"desc": "Go to Settings → AI Provider"
},
"2": {
"title": "Base URL",
"desc": "Paste your OmniRoute endpoint URL"
},
"3": {
"title": "API Key"
},
"4": {
"title": "Select Model"
}
}
}
}
},
@@ -1338,7 +1380,8 @@
"editCompatibleTitle": "Bearbeiten Sie {type} kompatibel",
"compatibleBaseUrlHint": "Verwenden Sie die Basis-URL (die auf /v1 endet) für Ihre {type}-kompatible API.",
"apiKeyForCheck": "API-Schlüssel (zur Überprüfung)",
"compatibleProdPlaceholder": "{type} Kompatibel (Prod)"
"compatibleProdPlaceholder": "{type} Kompatibel (Prod)",
"providerTestTimeout": "Provider test timed out — too many connections to test at once"
},
"settings": {
"title": "Einstellungen",
@@ -1734,7 +1777,12 @@
"customPricingNote": "Sie können die Standardpreise für bestimmte Modelle überschreiben. Benutzerdefinierte Überschreibungen haben Vorrang vor automatisch erkannten Preisen.",
"editPricing": "Preise bearbeiten",
"viewFullDetails": "Vollständige Details anzeigen",
"themeCoral": "Koralle"
"themeCoral": "Koralle",
"cliFingerprint": "CLI Fingerprint Matching",
"cliFingerprintDesc": "Match native CLI binary signatures when proxying requests. Reorders headers and body fields to look identical to the official CLI tools. Your proxy IP is preserved.",
"cliFingerprintEnabled": "{count} provider(s) with CLI fingerprint active",
"enableFingerprintTitle": "Enable fingerprint for {provider}",
"disableFingerprintTitle": "Disable fingerprint for {provider}"
},
"translator": {
"title": "Übersetzer",
@@ -2419,5 +2467,22 @@
"featureWebhooks": "Webhook-Konfiguration und Event-Abonnements",
"featureSwagger": "Automatische OpenAPI / Swagger-Spezifikation",
"featureAuth": "API-Schlüssel- und OAuth-Scope-Verwaltung pro Endpunkt"
},
"agents": {
"title": "CLI Agents",
"description": "Discover installed CLI agents on your system. Add custom agents for auto-detection.",
"refresh": "Refresh",
"installed": "Installed",
"notFound": "Not Found",
"builtIn": "Built-in",
"custom": "Custom",
"remove": "Remove",
"addCustomAgent": "Add Custom Agent",
"addCustomAgentDesc": "Register any CLI tool for detection. It will be scanned automatically on refresh.",
"agentName": "Agent Name",
"binaryName": "Binary Name",
"versionCommand": "Version Command",
"spawnArgs": "Spawn Args",
"addAgent": "Add Agent"
}
}
+40 -1
View File
@@ -460,7 +460,9 @@
"cline": "Cline AI Coding Assistant CLI",
"kilo": "Kilo Code AI Assistant CLI",
"cursor": "Cursor AI Code Editor",
"continue": "Continue AI Assistant"
"continue": "Continue AI Assistant",
"opencode": "OpenCode AI coding agent (Terminal)",
"kiro": "Amazon Kiro — AI-powered IDE"
},
"guides": {
"cursor": {
@@ -509,6 +511,42 @@
"desc": "Add the following configuration to your models array:"
}
}
},
"opencode": {
"steps": {
"1": {
"title": "Install OpenCode",
"desc": "Install via npm: npm install -g opencode-ai"
},
"2": {
"title": "API Key"
},
"3": {
"title": "Set Base URL",
"desc": "opencode config set baseUrl {{baseUrl}}"
},
"4": {
"title": "Select Model"
}
}
},
"kiro": {
"steps": {
"1": {
"title": "Open Kiro Settings",
"desc": "Go to Settings → AI Provider"
},
"2": {
"title": "Base URL",
"desc": "Paste your OmniRoute endpoint URL"
},
"3": {
"title": "API Key"
},
"4": {
"title": "Select Model"
}
}
}
}
},
@@ -1184,6 +1222,7 @@
"clearing": "Clearing...",
"until": "Until {time}",
"providerTestFailed": "Provider test failed",
"providerTestTimeout": "Provider test timed out — too many connections to test at once",
"modeTest": "{mode} Test",
"passedCount": "{count} passed",
"failedCount": "{count} failed",
+69 -4
View File
@@ -138,7 +138,11 @@
"media": "Medios de comunicación",
"mediaDescription": "Genera imágenes, vídeos y música.",
"themes": "Temas",
"themesDescription": "Elija un tema de color para todo el panel del tablero"
"themesDescription": "Elija un tema de color para todo el panel del tablero",
"mcp": "MCP",
"mcpDescription": "Model Context Protocol server management and tools",
"a2a": "A2A",
"a2aDescription": "Agent-to-Agent protocol tasks and observability"
},
"home": {
"quickStart": "Inicio rápido",
@@ -456,7 +460,9 @@
"cline": "CLI del asistente de codificación AI de Cline",
"kilo": "CLI del Asistente de Inteligencia Artificial de Kilo Code",
"cursor": "Editor de código AI del cursor",
"continue": "Continuar Asistente de IA"
"continue": "Continuar Asistente de IA",
"opencode": "OpenCode AI coding agent (Terminal)",
"kiro": "Amazon Kiro — AI-powered IDE"
},
"guides": {
"cursor": {
@@ -505,6 +511,42 @@
"desc": "Agregue la siguiente configuración a su matriz de modelos:"
}
}
},
"opencode": {
"steps": {
"1": {
"title": "Install OpenCode",
"desc": "Install via npm: npm install -g opencode-ai"
},
"2": {
"title": "API Key"
},
"3": {
"title": "Set Base URL",
"desc": "opencode config set baseUrl {{baseUrl}}"
},
"4": {
"title": "Select Model"
}
}
},
"kiro": {
"steps": {
"1": {
"title": "Open Kiro Settings",
"desc": "Go to Settings → AI Provider"
},
"2": {
"title": "Base URL",
"desc": "Paste your OmniRoute endpoint URL"
},
"3": {
"title": "API Key"
},
"4": {
"title": "Select Model"
}
}
}
}
},
@@ -1338,7 +1380,8 @@
"editCompatibleTitle": "Editar {type} Compatible",
"compatibleBaseUrlHint": "Utilice la URL base (que termina en /v1) para su API compatible con {type}.",
"apiKeyForCheck": "Clave API (para verificación)",
"compatibleProdPlaceholder": "{type} Compatible (Prod.)"
"compatibleProdPlaceholder": "{type} Compatible (Prod.)",
"providerTestTimeout": "Provider test timed out — too many connections to test at once"
},
"settings": {
"title": "Configuración",
@@ -1734,7 +1777,12 @@
"customPricingNote": "Puede anular los precios predeterminados para modelos específicos. Las anulaciones personalizadas tienen prioridad sobre los precios detectados automáticamente.",
"editPricing": "Editar precios",
"viewFullDetails": "Ver todos los detalles",
"themeCoral": "Coral"
"themeCoral": "Coral",
"cliFingerprint": "CLI Fingerprint Matching",
"cliFingerprintDesc": "Match native CLI binary signatures when proxying requests. Reorders headers and body fields to look identical to the official CLI tools. Your proxy IP is preserved.",
"cliFingerprintEnabled": "{count} provider(s) with CLI fingerprint active",
"enableFingerprintTitle": "Enable fingerprint for {provider}",
"disableFingerprintTitle": "Disable fingerprint for {provider}"
},
"translator": {
"title": "Traductor",
@@ -2419,5 +2467,22 @@
"featureWebhooks": "Configuración de webhooks y suscripciones de eventos",
"featureSwagger": "Generación automática de especificaciones OpenAPI / Swagger",
"featureAuth": "Gestión de claves API y alcances OAuth por endpoint"
},
"agents": {
"title": "CLI Agents",
"description": "Discover installed CLI agents on your system. Add custom agents for auto-detection.",
"refresh": "Refresh",
"installed": "Installed",
"notFound": "Not Found",
"builtIn": "Built-in",
"custom": "Custom",
"remove": "Remove",
"addCustomAgent": "Add Custom Agent",
"addCustomAgentDesc": "Register any CLI tool for detection. It will be scanned automatically on refresh.",
"agentName": "Agent Name",
"binaryName": "Binary Name",
"versionCommand": "Version Command",
"spawnArgs": "Spawn Args",
"addAgent": "Add Agent"
}
}
+69 -4
View File
@@ -138,7 +138,11 @@
"media": "Media",
"mediaDescription": "Luo kuvia, videoita ja musiikkia",
"themes": "Teemat",
"themesDescription": "Valitse väriteema koko kojelautapaneelille"
"themesDescription": "Valitse väriteema koko kojelautapaneelille",
"mcp": "MCP",
"mcpDescription": "Model Context Protocol server management and tools",
"a2a": "A2A",
"a2aDescription": "Agent-to-Agent protocol tasks and observability"
},
"home": {
"quickStart": "Pika-aloitus",
@@ -456,7 +460,9 @@
"cline": "Cline AI Coding Assistant CLI",
"kilo": "Kilo Code AI Assistant CLI",
"cursor": "Cursor AI Code Editor",
"continue": "Jatka AI Assistantia"
"continue": "Jatka AI Assistantia",
"opencode": "OpenCode AI coding agent (Terminal)",
"kiro": "Amazon Kiro — AI-powered IDE"
},
"guides": {
"cursor": {
@@ -505,6 +511,42 @@
"desc": "Lisää seuraavat kokoonpanot mallien matriisiisi:"
}
}
},
"opencode": {
"steps": {
"1": {
"title": "Install OpenCode",
"desc": "Install via npm: npm install -g opencode-ai"
},
"2": {
"title": "API Key"
},
"3": {
"title": "Set Base URL",
"desc": "opencode config set baseUrl {{baseUrl}}"
},
"4": {
"title": "Select Model"
}
}
},
"kiro": {
"steps": {
"1": {
"title": "Open Kiro Settings",
"desc": "Go to Settings → AI Provider"
},
"2": {
"title": "Base URL",
"desc": "Paste your OmniRoute endpoint URL"
},
"3": {
"title": "API Key"
},
"4": {
"title": "Select Model"
}
}
}
}
},
@@ -1338,7 +1380,8 @@
"editCompatibleTitle": "Muokkaa {type} Yhteensopiva",
"compatibleBaseUrlHint": "Käytä perus-URL-osoitetta (päättyy /v1) {type}-yhteensopivalle API:lle.",
"apiKeyForCheck": "API-avain (tarkistusta varten)",
"compatibleProdPlaceholder": "{type} Yhteensopiva (tuote)"
"compatibleProdPlaceholder": "{type} Yhteensopiva (tuote)",
"providerTestTimeout": "Provider test timed out — too many connections to test at once"
},
"settings": {
"title": "Asetukset",
@@ -1734,7 +1777,12 @@
"customPricingNote": "Voit ohittaa tiettyjen mallien oletushinnoittelun. Mukautetut ohitukset ovat etusijalla automaattisesti tunnistettuihin hinnoitteluun nähden.",
"editPricing": "Muokkaa hinnoittelua",
"viewFullDetails": "Näytä täydelliset tiedot",
"themeCoral": "Koralli"
"themeCoral": "Koralli",
"cliFingerprint": "CLI Fingerprint Matching",
"cliFingerprintDesc": "Match native CLI binary signatures when proxying requests. Reorders headers and body fields to look identical to the official CLI tools. Your proxy IP is preserved.",
"cliFingerprintEnabled": "{count} provider(s) with CLI fingerprint active",
"enableFingerprintTitle": "Enable fingerprint for {provider}",
"disableFingerprintTitle": "Disable fingerprint for {provider}"
},
"translator": {
"title": "Kääntäjä",
@@ -2419,5 +2467,22 @@
"featureWebhooks": "Webhook configuration and event subscriptions",
"featureSwagger": "OpenAPI / Swagger spec auto-generation",
"featureAuth": "API key and OAuth scope management per endpoint"
},
"agents": {
"title": "CLI Agents",
"description": "Discover installed CLI agents on your system. Add custom agents for auto-detection.",
"refresh": "Refresh",
"installed": "Installed",
"notFound": "Not Found",
"builtIn": "Built-in",
"custom": "Custom",
"remove": "Remove",
"addCustomAgent": "Add Custom Agent",
"addCustomAgentDesc": "Register any CLI tool for detection. It will be scanned automatically on refresh.",
"agentName": "Agent Name",
"binaryName": "Binary Name",
"versionCommand": "Version Command",
"spawnArgs": "Spawn Args",
"addAgent": "Add Agent"
}
}
+69 -4
View File
@@ -138,7 +138,11 @@
"media": "Médias",
"mediaDescription": "Générez des images, des vidéos et de la musique",
"themes": "Thèmes",
"themesDescription": "Choisissez un thème de couleur pour l'ensemble du panneau du tableau de bord"
"themesDescription": "Choisissez un thème de couleur pour l'ensemble du panneau du tableau de bord",
"mcp": "MCP",
"mcpDescription": "Model Context Protocol server management and tools",
"a2a": "A2A",
"a2aDescription": "Agent-to-Agent protocol tasks and observability"
},
"home": {
"quickStart": "Démarrage rapide",
@@ -456,7 +460,9 @@
"cline": "CLI de l'assistant de codage Cline AI",
"kilo": "CLI de l'assistant IA Kilo Code",
"cursor": "Éditeur de code AI du curseur",
"continue": "Continuer l'Assistant IA"
"continue": "Continuer l'Assistant IA",
"opencode": "OpenCode AI coding agent (Terminal)",
"kiro": "Amazon Kiro — AI-powered IDE"
},
"guides": {
"cursor": {
@@ -505,6 +511,42 @@
"desc": "Ajoutez la configuration suivante à votre tableau models :"
}
}
},
"opencode": {
"steps": {
"1": {
"title": "Install OpenCode",
"desc": "Install via npm: npm install -g opencode-ai"
},
"2": {
"title": "API Key"
},
"3": {
"title": "Set Base URL",
"desc": "opencode config set baseUrl {{baseUrl}}"
},
"4": {
"title": "Select Model"
}
}
},
"kiro": {
"steps": {
"1": {
"title": "Open Kiro Settings",
"desc": "Go to Settings → AI Provider"
},
"2": {
"title": "Base URL",
"desc": "Paste your OmniRoute endpoint URL"
},
"3": {
"title": "API Key"
},
"4": {
"title": "Select Model"
}
}
}
}
},
@@ -1338,7 +1380,8 @@
"editCompatibleTitle": "Modifier {type} Compatible",
"compatibleBaseUrlHint": "Utilisez l'URL de base (se terminant par /v1) pour votre API compatible {type}.",
"apiKeyForCheck": "Clé API (pour vérification)",
"compatibleProdPlaceholder": "{type} Compatible (Prod)"
"compatibleProdPlaceholder": "{type} Compatible (Prod)",
"providerTestTimeout": "Provider test timed out — too many connections to test at once"
},
"settings": {
"title": "Paramètres",
@@ -1734,7 +1777,12 @@
"customPricingNote": "Vous pouvez remplacer le prix par défaut pour des modèles spécifiques. Les remplacements personnalisés ont la priorité sur les prix détectés automatiquement.",
"editPricing": "Modifier le prix",
"viewFullDetails": "Afficher tous les détails",
"themeCoral": "Corail"
"themeCoral": "Corail",
"cliFingerprint": "CLI Fingerprint Matching",
"cliFingerprintDesc": "Match native CLI binary signatures when proxying requests. Reorders headers and body fields to look identical to the official CLI tools. Your proxy IP is preserved.",
"cliFingerprintEnabled": "{count} provider(s) with CLI fingerprint active",
"enableFingerprintTitle": "Enable fingerprint for {provider}",
"disableFingerprintTitle": "Disable fingerprint for {provider}"
},
"translator": {
"title": "Traducteur",
@@ -2419,5 +2467,22 @@
"featureWebhooks": "Configuration de webhooks et abonnements aux événements",
"featureSwagger": "Génération automatique de spécifications OpenAPI / Swagger",
"featureAuth": "Gestion des clés API et des portées OAuth par point d'accès"
},
"agents": {
"title": "CLI Agents",
"description": "Discover installed CLI agents on your system. Add custom agents for auto-detection.",
"refresh": "Refresh",
"installed": "Installed",
"notFound": "Not Found",
"builtIn": "Built-in",
"custom": "Custom",
"remove": "Remove",
"addCustomAgent": "Add Custom Agent",
"addCustomAgentDesc": "Register any CLI tool for detection. It will be scanned automatically on refresh.",
"agentName": "Agent Name",
"binaryName": "Binary Name",
"versionCommand": "Version Command",
"spawnArgs": "Spawn Args",
"addAgent": "Add Agent"
}
}
+69 -4
View File
@@ -138,7 +138,11 @@
"media": "Media",
"mediaDescription": "Generate images, videos, and music",
"themes": "Themes",
"themesDescription": "Choose a color theme for the whole dashboard panel"
"themesDescription": "Choose a color theme for the whole dashboard panel",
"mcp": "MCP",
"mcpDescription": "Model Context Protocol server management and tools",
"a2a": "A2A",
"a2aDescription": "Agent-to-Agent protocol tasks and observability"
},
"home": {
"quickStart": "התחלה מהירה",
@@ -456,7 +460,9 @@
"cline": "Cline AI Coding Assistant CLI",
"kilo": "קילו קוד AI עוזר CLI",
"cursor": "עורך קוד AI של הסמן",
"continue": "המשך עוזר AI"
"continue": "המשך עוזר AI",
"opencode": "OpenCode AI coding agent (Terminal)",
"kiro": "Amazon Kiro — AI-powered IDE"
},
"guides": {
"cursor": {
@@ -505,6 +511,42 @@
"desc": "הוסף את התצורה הבאה למערך הדגמים שלך:"
}
}
},
"opencode": {
"steps": {
"1": {
"title": "Install OpenCode",
"desc": "Install via npm: npm install -g opencode-ai"
},
"2": {
"title": "API Key"
},
"3": {
"title": "Set Base URL",
"desc": "opencode config set baseUrl {{baseUrl}}"
},
"4": {
"title": "Select Model"
}
}
},
"kiro": {
"steps": {
"1": {
"title": "Open Kiro Settings",
"desc": "Go to Settings → AI Provider"
},
"2": {
"title": "Base URL",
"desc": "Paste your OmniRoute endpoint URL"
},
"3": {
"title": "API Key"
},
"4": {
"title": "Select Model"
}
}
}
}
},
@@ -1338,7 +1380,8 @@
"editCompatibleTitle": "ערוך {type} תואם",
"compatibleBaseUrlHint": "השתמש בכתובת ה-URL הבסיסית (המסתיימת ב-/v1) עבור ה-API התואם {type} שלך.",
"apiKeyForCheck": "מפתח API (לבדיקה)",
"compatibleProdPlaceholder": "{type} תואם (פרוד)"
"compatibleProdPlaceholder": "{type} תואם (פרוד)",
"providerTestTimeout": "Provider test timed out — too many connections to test at once"
},
"settings": {
"title": "הגדרות",
@@ -1734,7 +1777,12 @@
"customPricingNote": "אתה יכול לעקוף את תמחור ברירת המחדל עבור דגמים ספציפיים. עקיפות מותאמות אישית מקבלות עדיפות על פני תמחור שזוהה אוטומטית.",
"editPricing": "ערוך תמחור",
"viewFullDetails": "צפה בפרטים המלאים",
"themeCoral": "אלמוג"
"themeCoral": "אלמוג",
"cliFingerprint": "CLI Fingerprint Matching",
"cliFingerprintDesc": "Match native CLI binary signatures when proxying requests. Reorders headers and body fields to look identical to the official CLI tools. Your proxy IP is preserved.",
"cliFingerprintEnabled": "{count} provider(s) with CLI fingerprint active",
"enableFingerprintTitle": "Enable fingerprint for {provider}",
"disableFingerprintTitle": "Disable fingerprint for {provider}"
},
"translator": {
"title": "מתרגם",
@@ -2419,5 +2467,22 @@
"featureWebhooks": "Webhook configuration and event subscriptions",
"featureSwagger": "OpenAPI / Swagger spec auto-generation",
"featureAuth": "API key and OAuth scope management per endpoint"
},
"agents": {
"title": "CLI Agents",
"description": "Discover installed CLI agents on your system. Add custom agents for auto-detection.",
"refresh": "Refresh",
"installed": "Installed",
"notFound": "Not Found",
"builtIn": "Built-in",
"custom": "Custom",
"remove": "Remove",
"addCustomAgent": "Add Custom Agent",
"addCustomAgentDesc": "Register any CLI tool for detection. It will be scanned automatically on refresh.",
"agentName": "Agent Name",
"binaryName": "Binary Name",
"versionCommand": "Version Command",
"spawnArgs": "Spawn Args",
"addAgent": "Add Agent"
}
}
+69 -4
View File
@@ -138,7 +138,11 @@
"media": "Média",
"mediaDescription": "Készítsen képeket, videókat és zenét",
"themes": "Témák",
"themesDescription": "Válasszon színtémát az egész irányítópult panelhez"
"themesDescription": "Válasszon színtémát az egész irányítópult panelhez",
"mcp": "MCP",
"mcpDescription": "Model Context Protocol server management and tools",
"a2a": "A2A",
"a2aDescription": "Agent-to-Agent protocol tasks and observability"
},
"home": {
"quickStart": "Gyors kezdés",
@@ -456,7 +460,9 @@
"cline": "Cline AI Coding Assistant CLI",
"kilo": "Kilo Code AI Assistant CLI",
"cursor": "Kurzor AI kódszerkesztő",
"continue": "Az AI-asszisztens folytatása"
"continue": "Az AI-asszisztens folytatása",
"opencode": "OpenCode AI coding agent (Terminal)",
"kiro": "Amazon Kiro — AI-powered IDE"
},
"guides": {
"cursor": {
@@ -505,6 +511,42 @@
"desc": "Adja hozzá a következő konfigurációt a modellek tömbjéhez:"
}
}
},
"opencode": {
"steps": {
"1": {
"title": "Install OpenCode",
"desc": "Install via npm: npm install -g opencode-ai"
},
"2": {
"title": "API Key"
},
"3": {
"title": "Set Base URL",
"desc": "opencode config set baseUrl {{baseUrl}}"
},
"4": {
"title": "Select Model"
}
}
},
"kiro": {
"steps": {
"1": {
"title": "Open Kiro Settings",
"desc": "Go to Settings → AI Provider"
},
"2": {
"title": "Base URL",
"desc": "Paste your OmniRoute endpoint URL"
},
"3": {
"title": "API Key"
},
"4": {
"title": "Select Model"
}
}
}
}
},
@@ -1338,7 +1380,8 @@
"editCompatibleTitle": "Szerkesztés {type} Kompatibilis",
"compatibleBaseUrlHint": "Használja a {type}-kompatibilis API alap URL-jét (a /v1 végződésű).",
"apiKeyForCheck": "API-kulcs (ellenőrzéshez)",
"compatibleProdPlaceholder": "{type} Kompatibilis (termék)"
"compatibleProdPlaceholder": "{type} Kompatibilis (termék)",
"providerTestTimeout": "Provider test timed out — too many connections to test at once"
},
"settings": {
"title": "Beállítások elemre",
@@ -1734,7 +1777,12 @@
"customPricingNote": "Egyes modelleknél felülbírálhatja az alapértelmezett árazást. Az egyéni felülbírálások elsőbbséget élveznek az automatikusan észlelt árképzéssel szemben.",
"editPricing": "Árak szerkesztése",
"viewFullDetails": "Teljes részletek megtekintése",
"themeCoral": "Korall"
"themeCoral": "Korall",
"cliFingerprint": "CLI Fingerprint Matching",
"cliFingerprintDesc": "Match native CLI binary signatures when proxying requests. Reorders headers and body fields to look identical to the official CLI tools. Your proxy IP is preserved.",
"cliFingerprintEnabled": "{count} provider(s) with CLI fingerprint active",
"enableFingerprintTitle": "Enable fingerprint for {provider}",
"disableFingerprintTitle": "Disable fingerprint for {provider}"
},
"translator": {
"title": "Fordító",
@@ -2419,5 +2467,22 @@
"featureWebhooks": "Webhook configuration and event subscriptions",
"featureSwagger": "OpenAPI / Swagger spec auto-generation",
"featureAuth": "API key and OAuth scope management per endpoint"
},
"agents": {
"title": "CLI Agents",
"description": "Discover installed CLI agents on your system. Add custom agents for auto-detection.",
"refresh": "Refresh",
"installed": "Installed",
"notFound": "Not Found",
"builtIn": "Built-in",
"custom": "Custom",
"remove": "Remove",
"addCustomAgent": "Add Custom Agent",
"addCustomAgentDesc": "Register any CLI tool for detection. It will be scanned automatically on refresh.",
"agentName": "Agent Name",
"binaryName": "Binary Name",
"versionCommand": "Version Command",
"spawnArgs": "Spawn Args",
"addAgent": "Add Agent"
}
}
+69 -4
View File
@@ -138,7 +138,11 @@
"media": "Media",
"mediaDescription": "Generate images, videos, and music",
"themes": "Themes",
"themesDescription": "Choose a color theme for the whole dashboard panel"
"themesDescription": "Choose a color theme for the whole dashboard panel",
"mcp": "MCP",
"mcpDescription": "Model Context Protocol server management and tools",
"a2a": "A2A",
"a2aDescription": "Agent-to-Agent protocol tasks and observability"
},
"home": {
"quickStart": "Mulai Cepat",
@@ -456,7 +460,9 @@
"cline": "CLI Asisten Pengkodean AI Cline",
"kilo": "CLI Asisten AI Kode Kilo",
"cursor": "Editor Kode AI Kursor",
"continue": "Lanjutkan Asisten AI"
"continue": "Lanjutkan Asisten AI",
"opencode": "OpenCode AI coding agent (Terminal)",
"kiro": "Amazon Kiro — AI-powered IDE"
},
"guides": {
"cursor": {
@@ -505,6 +511,42 @@
"desc": "Tambahkan konfigurasi berikut ke array model Anda:"
}
}
},
"opencode": {
"steps": {
"1": {
"title": "Install OpenCode",
"desc": "Install via npm: npm install -g opencode-ai"
},
"2": {
"title": "API Key"
},
"3": {
"title": "Set Base URL",
"desc": "opencode config set baseUrl {{baseUrl}}"
},
"4": {
"title": "Select Model"
}
}
},
"kiro": {
"steps": {
"1": {
"title": "Open Kiro Settings",
"desc": "Go to Settings → AI Provider"
},
"2": {
"title": "Base URL",
"desc": "Paste your OmniRoute endpoint URL"
},
"3": {
"title": "API Key"
},
"4": {
"title": "Select Model"
}
}
}
}
},
@@ -1338,7 +1380,8 @@
"editCompatibleTitle": "Sunting {type} Kompatibel",
"compatibleBaseUrlHint": "Gunakan URL dasar (berakhiran /v1) untuk API Anda yang kompatibel dengan {type}.",
"apiKeyForCheck": "Kunci API (untuk Pemeriksaan)",
"compatibleProdPlaceholder": "{type} Kompatibel (Prod)"
"compatibleProdPlaceholder": "{type} Kompatibel (Prod)",
"providerTestTimeout": "Provider test timed out — too many connections to test at once"
},
"settings": {
"title": "Pengaturan",
@@ -1734,7 +1777,12 @@
"customPricingNote": "Anda dapat mengganti harga default untuk model tertentu. Penggantian khusus lebih diprioritaskan dibandingkan harga yang terdeteksi otomatis.",
"editPricing": "Sunting Harga",
"viewFullDetails": "Lihat Detail Lengkap",
"themeCoral": "Koral"
"themeCoral": "Koral",
"cliFingerprint": "CLI Fingerprint Matching",
"cliFingerprintDesc": "Match native CLI binary signatures when proxying requests. Reorders headers and body fields to look identical to the official CLI tools. Your proxy IP is preserved.",
"cliFingerprintEnabled": "{count} provider(s) with CLI fingerprint active",
"enableFingerprintTitle": "Enable fingerprint for {provider}",
"disableFingerprintTitle": "Disable fingerprint for {provider}"
},
"translator": {
"title": "Penerjemah",
@@ -2419,5 +2467,22 @@
"featureWebhooks": "Webhook configuration and event subscriptions",
"featureSwagger": "OpenAPI / Swagger spec auto-generation",
"featureAuth": "API key and OAuth scope management per endpoint"
},
"agents": {
"title": "CLI Agents",
"description": "Discover installed CLI agents on your system. Add custom agents for auto-detection.",
"refresh": "Refresh",
"installed": "Installed",
"notFound": "Not Found",
"builtIn": "Built-in",
"custom": "Custom",
"remove": "Remove",
"addCustomAgent": "Add Custom Agent",
"addCustomAgentDesc": "Register any CLI tool for detection. It will be scanned automatically on refresh.",
"agentName": "Agent Name",
"binaryName": "Binary Name",
"versionCommand": "Version Command",
"spawnArgs": "Spawn Args",
"addAgent": "Add Agent"
}
}
+69 -4
View File
@@ -138,7 +138,11 @@
"media": "Media",
"mediaDescription": "Generate images, videos, and music",
"themes": "Themes",
"themesDescription": "Choose a color theme for the whole dashboard panel"
"themesDescription": "Choose a color theme for the whole dashboard panel",
"mcp": "MCP",
"mcpDescription": "Model Context Protocol server management and tools",
"a2a": "A2A",
"a2aDescription": "Agent-to-Agent protocol tasks and observability"
},
"home": {
"quickStart": "त्वरित शुरुआत",
@@ -456,7 +460,9 @@
"cline": "क्लाइन एआई कोडिंग सहायक सीएलआई",
"kilo": "किलो कोड एआई असिस्टेंट सीएलआई",
"cursor": "कर्सर एआई कोड संपादक",
"continue": "एआई असिस्टेंट जारी रखें"
"continue": "एआई असिस्टेंट जारी रखें",
"opencode": "OpenCode AI coding agent (Terminal)",
"kiro": "Amazon Kiro — AI-powered IDE"
},
"guides": {
"cursor": {
@@ -505,6 +511,42 @@
"desc": "अपने मॉडल सरणी में निम्नलिखित कॉन्फ़िगरेशन जोड़ें:"
}
}
},
"opencode": {
"steps": {
"1": {
"title": "Install OpenCode",
"desc": "Install via npm: npm install -g opencode-ai"
},
"2": {
"title": "API Key"
},
"3": {
"title": "Set Base URL",
"desc": "opencode config set baseUrl {{baseUrl}}"
},
"4": {
"title": "Select Model"
}
}
},
"kiro": {
"steps": {
"1": {
"title": "Open Kiro Settings",
"desc": "Go to Settings → AI Provider"
},
"2": {
"title": "Base URL",
"desc": "Paste your OmniRoute endpoint URL"
},
"3": {
"title": "API Key"
},
"4": {
"title": "Select Model"
}
}
}
}
},
@@ -1338,7 +1380,8 @@
"editCompatibleTitle": "संपादित करें {type} संगत",
"compatibleBaseUrlHint": "अपने {type}-संगत API के लिए आधार URL (/v1 पर समाप्त) का उपयोग करें।",
"apiKeyForCheck": "एपीआई कुंजी (चेक के लिए)",
"compatibleProdPlaceholder": "{type} संगत (उत्पाद)"
"compatibleProdPlaceholder": "{type} संगत (उत्पाद)",
"providerTestTimeout": "Provider test timed out — too many connections to test at once"
},
"settings": {
"title": "सेटिंग्स",
@@ -1734,7 +1777,12 @@
"customPricingNote": "आप विशिष्ट मॉडलों के लिए डिफ़ॉल्ट मूल्य निर्धारण को ओवरराइड कर सकते हैं। कस्टम ओवरराइड्स को स्वतः-पता लगाए गए मूल्य-निर्धारण पर प्राथमिकता दी जाती है।",
"editPricing": "मूल्य निर्धारण संपादित करें",
"viewFullDetails": "पूर्ण विवरण देखें",
"themeCoral": "कोरल"
"themeCoral": "कोरल",
"cliFingerprint": "CLI Fingerprint Matching",
"cliFingerprintDesc": "Match native CLI binary signatures when proxying requests. Reorders headers and body fields to look identical to the official CLI tools. Your proxy IP is preserved.",
"cliFingerprintEnabled": "{count} provider(s) with CLI fingerprint active",
"enableFingerprintTitle": "Enable fingerprint for {provider}",
"disableFingerprintTitle": "Disable fingerprint for {provider}"
},
"translator": {
"title": "अनुवादक",
@@ -2419,5 +2467,22 @@
"featureWebhooks": "Webhook configuration and event subscriptions",
"featureSwagger": "OpenAPI / Swagger spec auto-generation",
"featureAuth": "API key and OAuth scope management per endpoint"
},
"agents": {
"title": "CLI Agents",
"description": "Discover installed CLI agents on your system. Add custom agents for auto-detection.",
"refresh": "Refresh",
"installed": "Installed",
"notFound": "Not Found",
"builtIn": "Built-in",
"custom": "Custom",
"remove": "Remove",
"addCustomAgent": "Add Custom Agent",
"addCustomAgentDesc": "Register any CLI tool for detection. It will be scanned automatically on refresh.",
"agentName": "Agent Name",
"binaryName": "Binary Name",
"versionCommand": "Version Command",
"spawnArgs": "Spawn Args",
"addAgent": "Add Agent"
}
}
+69 -4
View File
@@ -138,7 +138,11 @@
"media": "Media",
"mediaDescription": "Generate images, videos, and music",
"themes": "Themes",
"themesDescription": "Choose a color theme for the whole dashboard panel"
"themesDescription": "Choose a color theme for the whole dashboard panel",
"mcp": "MCP",
"mcpDescription": "Model Context Protocol server management and tools",
"a2a": "A2A",
"a2aDescription": "Agent-to-Agent protocol tasks and observability"
},
"home": {
"quickStart": "Avvio rapido",
@@ -456,7 +460,9 @@
"cline": "Cline AI Coding Assistant CLI",
"kilo": "CLI dell'Assistente AI Kilo Code",
"cursor": "Editor del codice AI del cursore",
"continue": "Continua Assistente AI"
"continue": "Continua Assistente AI",
"opencode": "OpenCode AI coding agent (Terminal)",
"kiro": "Amazon Kiro — AI-powered IDE"
},
"guides": {
"cursor": {
@@ -505,6 +511,42 @@
"desc": "Aggiungi la seguente configurazione all'array dei modelli:"
}
}
},
"opencode": {
"steps": {
"1": {
"title": "Install OpenCode",
"desc": "Install via npm: npm install -g opencode-ai"
},
"2": {
"title": "API Key"
},
"3": {
"title": "Set Base URL",
"desc": "opencode config set baseUrl {{baseUrl}}"
},
"4": {
"title": "Select Model"
}
}
},
"kiro": {
"steps": {
"1": {
"title": "Open Kiro Settings",
"desc": "Go to Settings → AI Provider"
},
"2": {
"title": "Base URL",
"desc": "Paste your OmniRoute endpoint URL"
},
"3": {
"title": "API Key"
},
"4": {
"title": "Select Model"
}
}
}
}
},
@@ -1338,7 +1380,8 @@
"editCompatibleTitle": "Modifica {type} Compatibile",
"compatibleBaseUrlHint": "Utilizza l'URL di base (che termina con /v1) per la tua API compatibile con {type}.",
"apiKeyForCheck": "Chiave API (per controllo)",
"compatibleProdPlaceholder": "{type} Compatibile (prodotto)"
"compatibleProdPlaceholder": "{type} Compatibile (prodotto)",
"providerTestTimeout": "Provider test timed out — too many connections to test at once"
},
"settings": {
"title": "Impostazioni",
@@ -1734,7 +1777,12 @@
"customPricingNote": "Puoi sostituire i prezzi predefiniti per modelli specifici. Le sostituzioni personalizzate hanno la priorità sui prezzi rilevati automaticamente.",
"editPricing": "Modifica prezzi",
"viewFullDetails": "Visualizza i dettagli completi",
"themeCoral": "Corallo"
"themeCoral": "Corallo",
"cliFingerprint": "CLI Fingerprint Matching",
"cliFingerprintDesc": "Match native CLI binary signatures when proxying requests. Reorders headers and body fields to look identical to the official CLI tools. Your proxy IP is preserved.",
"cliFingerprintEnabled": "{count} provider(s) with CLI fingerprint active",
"enableFingerprintTitle": "Enable fingerprint for {provider}",
"disableFingerprintTitle": "Disable fingerprint for {provider}"
},
"translator": {
"title": "Traduttore",
@@ -2419,5 +2467,22 @@
"featureWebhooks": "Configurazione webhook e sottoscrizioni eventi",
"featureSwagger": "Generazione automatica specifiche OpenAPI / Swagger",
"featureAuth": "Gestione chiavi API e ambiti OAuth per endpoint"
},
"agents": {
"title": "CLI Agents",
"description": "Discover installed CLI agents on your system. Add custom agents for auto-detection.",
"refresh": "Refresh",
"installed": "Installed",
"notFound": "Not Found",
"builtIn": "Built-in",
"custom": "Custom",
"remove": "Remove",
"addCustomAgent": "Add Custom Agent",
"addCustomAgentDesc": "Register any CLI tool for detection. It will be scanned automatically on refresh.",
"agentName": "Agent Name",
"binaryName": "Binary Name",
"versionCommand": "Version Command",
"spawnArgs": "Spawn Args",
"addAgent": "Add Agent"
}
}
+69 -4
View File
@@ -138,7 +138,11 @@
"media": "Media",
"mediaDescription": "Generate images, videos, and music",
"themes": "Themes",
"themesDescription": "Choose a color theme for the whole dashboard panel"
"themesDescription": "Choose a color theme for the whole dashboard panel",
"mcp": "MCP",
"mcpDescription": "Model Context Protocol server management and tools",
"a2a": "A2A",
"a2aDescription": "Agent-to-Agent protocol tasks and observability"
},
"home": {
"quickStart": "クイックスタート",
@@ -456,7 +460,9 @@
"cline": "Cline AI コーディング アシスタント CLI",
"kilo": "Kilo Code AI アシスタント CLI",
"cursor": "カーソルAIコードエディター",
"continue": "AIアシスタントを続ける"
"continue": "AIアシスタントを続ける",
"opencode": "OpenCode AI coding agent (Terminal)",
"kiro": "Amazon Kiro — AI-powered IDE"
},
"guides": {
"cursor": {
@@ -505,6 +511,42 @@
"desc": "次の構成をモデル配列に追加します。"
}
}
},
"opencode": {
"steps": {
"1": {
"title": "Install OpenCode",
"desc": "Install via npm: npm install -g opencode-ai"
},
"2": {
"title": "API Key"
},
"3": {
"title": "Set Base URL",
"desc": "opencode config set baseUrl {{baseUrl}}"
},
"4": {
"title": "Select Model"
}
}
},
"kiro": {
"steps": {
"1": {
"title": "Open Kiro Settings",
"desc": "Go to Settings → AI Provider"
},
"2": {
"title": "Base URL",
"desc": "Paste your OmniRoute endpoint URL"
},
"3": {
"title": "API Key"
},
"4": {
"title": "Select Model"
}
}
}
}
},
@@ -1338,7 +1380,8 @@
"editCompatibleTitle": "編集 {type} 互換",
"compatibleBaseUrlHint": "{type} 互換 API のベース URL (/v1 で終わる) を使用します。",
"apiKeyForCheck": "APIキー(チェック用)",
"compatibleProdPlaceholder": "{type} 互換性あり (製品)"
"compatibleProdPlaceholder": "{type} 互換性あり (製品)",
"providerTestTimeout": "Provider test timed out — too many connections to test at once"
},
"settings": {
"title": "設定",
@@ -1734,7 +1777,12 @@
"customPricingNote": "特定のモデルのデフォルトの価格をオーバーライドできます。カスタム オーバーライドは、自動検出された価格設定よりも優先されます。",
"editPricing": "価格の編集",
"viewFullDetails": "詳細を表示",
"themeCoral": "コーラル"
"themeCoral": "コーラル",
"cliFingerprint": "CLI Fingerprint Matching",
"cliFingerprintDesc": "Match native CLI binary signatures when proxying requests. Reorders headers and body fields to look identical to the official CLI tools. Your proxy IP is preserved.",
"cliFingerprintEnabled": "{count} provider(s) with CLI fingerprint active",
"enableFingerprintTitle": "Enable fingerprint for {provider}",
"disableFingerprintTitle": "Disable fingerprint for {provider}"
},
"translator": {
"title": "翻訳者",
@@ -2419,5 +2467,22 @@
"featureWebhooks": "Webhook設定とイベントサブスクリプション",
"featureSwagger": "OpenAPI / Swagger仕様の自動生成",
"featureAuth": "エンドポイントごとのAPIキーとOAuthスコープ管理"
},
"agents": {
"title": "CLI Agents",
"description": "Discover installed CLI agents on your system. Add custom agents for auto-detection.",
"refresh": "Refresh",
"installed": "Installed",
"notFound": "Not Found",
"builtIn": "Built-in",
"custom": "Custom",
"remove": "Remove",
"addCustomAgent": "Add Custom Agent",
"addCustomAgentDesc": "Register any CLI tool for detection. It will be scanned automatically on refresh.",
"agentName": "Agent Name",
"binaryName": "Binary Name",
"versionCommand": "Version Command",
"spawnArgs": "Spawn Args",
"addAgent": "Add Agent"
}
}
+69 -4
View File
@@ -138,7 +138,11 @@
"media": "Media",
"mediaDescription": "Generate images, videos, and music",
"themes": "Themes",
"themesDescription": "Choose a color theme for the whole dashboard panel"
"themesDescription": "Choose a color theme for the whole dashboard panel",
"mcp": "MCP",
"mcpDescription": "Model Context Protocol server management and tools",
"a2a": "A2A",
"a2aDescription": "Agent-to-Agent protocol tasks and observability"
},
"home": {
"quickStart": "빠른 시작",
@@ -456,7 +460,9 @@
"cline": "Cline AI 코딩 어시스턴트 CLI",
"kilo": "킬로코드 AI 어시스턴트 CLI",
"cursor": "커서 AI 코드 편집기",
"continue": "AI 어시스턴트 계속하기"
"continue": "AI 어시스턴트 계속하기",
"opencode": "OpenCode AI coding agent (Terminal)",
"kiro": "Amazon Kiro — AI-powered IDE"
},
"guides": {
"cursor": {
@@ -505,6 +511,42 @@
"desc": "모델 배열에 다음 구성을 추가합니다."
}
}
},
"opencode": {
"steps": {
"1": {
"title": "Install OpenCode",
"desc": "Install via npm: npm install -g opencode-ai"
},
"2": {
"title": "API Key"
},
"3": {
"title": "Set Base URL",
"desc": "opencode config set baseUrl {{baseUrl}}"
},
"4": {
"title": "Select Model"
}
}
},
"kiro": {
"steps": {
"1": {
"title": "Open Kiro Settings",
"desc": "Go to Settings → AI Provider"
},
"2": {
"title": "Base URL",
"desc": "Paste your OmniRoute endpoint URL"
},
"3": {
"title": "API Key"
},
"4": {
"title": "Select Model"
}
}
}
}
},
@@ -1338,7 +1380,8 @@
"editCompatibleTitle": "{type} 호환 가능 편집",
"compatibleBaseUrlHint": "{type} 호환 API에는 기본 URL(/v1로 끝남)을 사용하세요.",
"apiKeyForCheck": "API Key(확인용)",
"compatibleProdPlaceholder": "{type} 호환 가능(프로덕션)"
"compatibleProdPlaceholder": "{type} 호환 가능(프로덕션)",
"providerTestTimeout": "Provider test timed out — too many connections to test at once"
},
"settings": {
"title": "설정",
@@ -1734,7 +1777,12 @@
"customPricingNote": "특정 모델의 기본 가격을 재정의할 수 있습니다. 맞춤 재정의는 자동 감지된 가격보다 우선 적용됩니다.",
"editPricing": "가격 편집",
"viewFullDetails": "전체 세부정보 보기",
"themeCoral": "코랄"
"themeCoral": "코랄",
"cliFingerprint": "CLI Fingerprint Matching",
"cliFingerprintDesc": "Match native CLI binary signatures when proxying requests. Reorders headers and body fields to look identical to the official CLI tools. Your proxy IP is preserved.",
"cliFingerprintEnabled": "{count} provider(s) with CLI fingerprint active",
"enableFingerprintTitle": "Enable fingerprint for {provider}",
"disableFingerprintTitle": "Disable fingerprint for {provider}"
},
"translator": {
"title": "번역기",
@@ -2419,5 +2467,22 @@
"featureWebhooks": "웹훅 구성 및 이벤트 구독",
"featureSwagger": "OpenAPI / Swagger 사양 자동 생성",
"featureAuth": "엔드포인트별 API 키 및 OAuth 범위 관리"
},
"agents": {
"title": "CLI Agents",
"description": "Discover installed CLI agents on your system. Add custom agents for auto-detection.",
"refresh": "Refresh",
"installed": "Installed",
"notFound": "Not Found",
"builtIn": "Built-in",
"custom": "Custom",
"remove": "Remove",
"addCustomAgent": "Add Custom Agent",
"addCustomAgentDesc": "Register any CLI tool for detection. It will be scanned automatically on refresh.",
"agentName": "Agent Name",
"binaryName": "Binary Name",
"versionCommand": "Version Command",
"spawnArgs": "Spawn Args",
"addAgent": "Add Agent"
}
}
+69 -4
View File
@@ -138,7 +138,11 @@
"media": "Media",
"mediaDescription": "Generate images, videos, and music",
"themes": "Themes",
"themesDescription": "Choose a color theme for the whole dashboard panel"
"themesDescription": "Choose a color theme for the whole dashboard panel",
"mcp": "MCP",
"mcpDescription": "Model Context Protocol server management and tools",
"a2a": "A2A",
"a2aDescription": "Agent-to-Agent protocol tasks and observability"
},
"home": {
"quickStart": "Mula Pantas",
@@ -456,7 +460,9 @@
"cline": "Pembantu Pengekodan AI Cline CLI",
"kilo": "Kilo Code AI Assistant CLI",
"cursor": "Editor Kod AI Kursor",
"continue": "Teruskan AI Assistant"
"continue": "Teruskan AI Assistant",
"opencode": "OpenCode AI coding agent (Terminal)",
"kiro": "Amazon Kiro — AI-powered IDE"
},
"guides": {
"cursor": {
@@ -505,6 +511,42 @@
"desc": "Tambahkan konfigurasi berikut pada tatasusunan model anda:"
}
}
},
"opencode": {
"steps": {
"1": {
"title": "Install OpenCode",
"desc": "Install via npm: npm install -g opencode-ai"
},
"2": {
"title": "API Key"
},
"3": {
"title": "Set Base URL",
"desc": "opencode config set baseUrl {{baseUrl}}"
},
"4": {
"title": "Select Model"
}
}
},
"kiro": {
"steps": {
"1": {
"title": "Open Kiro Settings",
"desc": "Go to Settings → AI Provider"
},
"2": {
"title": "Base URL",
"desc": "Paste your OmniRoute endpoint URL"
},
"3": {
"title": "API Key"
},
"4": {
"title": "Select Model"
}
}
}
}
},
@@ -1338,7 +1380,8 @@
"editCompatibleTitle": "Edit {type} Serasi",
"compatibleBaseUrlHint": "Gunakan URL asas (berakhir dengan /v1) untuk API serasi {type} anda.",
"apiKeyForCheck": "Kunci API (untuk Semakan)",
"compatibleProdPlaceholder": "{type} Serasi (Prod)"
"compatibleProdPlaceholder": "{type} Serasi (Prod)",
"providerTestTimeout": "Provider test timed out — too many connections to test at once"
},
"settings": {
"title": "tetapan",
@@ -1734,7 +1777,12 @@
"customPricingNote": "Anda boleh mengatasi harga lalai untuk model tertentu. Penggantian tersuai diutamakan berbanding harga yang dikesan secara automatik.",
"editPricing": "Edit Harga",
"viewFullDetails": "Lihat Butiran Penuh",
"themeCoral": "Koral"
"themeCoral": "Koral",
"cliFingerprint": "CLI Fingerprint Matching",
"cliFingerprintDesc": "Match native CLI binary signatures when proxying requests. Reorders headers and body fields to look identical to the official CLI tools. Your proxy IP is preserved.",
"cliFingerprintEnabled": "{count} provider(s) with CLI fingerprint active",
"enableFingerprintTitle": "Enable fingerprint for {provider}",
"disableFingerprintTitle": "Disable fingerprint for {provider}"
},
"translator": {
"title": "Penterjemah",
@@ -2419,5 +2467,22 @@
"featureWebhooks": "Webhook configuration and event subscriptions",
"featureSwagger": "OpenAPI / Swagger spec auto-generation",
"featureAuth": "API key and OAuth scope management per endpoint"
},
"agents": {
"title": "CLI Agents",
"description": "Discover installed CLI agents on your system. Add custom agents for auto-detection.",
"refresh": "Refresh",
"installed": "Installed",
"notFound": "Not Found",
"builtIn": "Built-in",
"custom": "Custom",
"remove": "Remove",
"addCustomAgent": "Add Custom Agent",
"addCustomAgentDesc": "Register any CLI tool for detection. It will be scanned automatically on refresh.",
"agentName": "Agent Name",
"binaryName": "Binary Name",
"versionCommand": "Version Command",
"spawnArgs": "Spawn Args",
"addAgent": "Add Agent"
}
}
+69 -4
View File
@@ -138,7 +138,11 @@
"media": "Media",
"mediaDescription": "Genereer afbeeldingen, video's en muziek",
"themes": "Themes",
"themesDescription": "Choose a color theme for the whole dashboard panel"
"themesDescription": "Choose a color theme for the whole dashboard panel",
"mcp": "MCP",
"mcpDescription": "Model Context Protocol server management and tools",
"a2a": "A2A",
"a2aDescription": "Agent-to-Agent protocol tasks and observability"
},
"home": {
"quickStart": "Snel beginnen",
@@ -456,7 +460,9 @@
"cline": "Cline AI Coderingsassistent CLI",
"kilo": "Kilocode AI Assistent CLI",
"cursor": "Cursor AI-code-editor",
"continue": "Ga door met AI-assistent"
"continue": "Ga door met AI-assistent",
"opencode": "OpenCode AI coding agent (Terminal)",
"kiro": "Amazon Kiro — AI-powered IDE"
},
"guides": {
"cursor": {
@@ -505,6 +511,42 @@
"desc": "Voeg de volgende configuratie toe aan uw modellenarray:"
}
}
},
"opencode": {
"steps": {
"1": {
"title": "Install OpenCode",
"desc": "Install via npm: npm install -g opencode-ai"
},
"2": {
"title": "API Key"
},
"3": {
"title": "Set Base URL",
"desc": "opencode config set baseUrl {{baseUrl}}"
},
"4": {
"title": "Select Model"
}
}
},
"kiro": {
"steps": {
"1": {
"title": "Open Kiro Settings",
"desc": "Go to Settings → AI Provider"
},
"2": {
"title": "Base URL",
"desc": "Paste your OmniRoute endpoint URL"
},
"3": {
"title": "API Key"
},
"4": {
"title": "Select Model"
}
}
}
}
},
@@ -1338,7 +1380,8 @@
"editCompatibleTitle": "Bewerk {type} Compatibel",
"compatibleBaseUrlHint": "Gebruik de basis-URL (eindigend op /v1) voor uw {type}-compatibele API.",
"apiKeyForCheck": "API-sleutel (ter controle)",
"compatibleProdPlaceholder": "{type} Compatibel (product)"
"compatibleProdPlaceholder": "{type} Compatibel (product)",
"providerTestTimeout": "Provider test timed out — too many connections to test at once"
},
"settings": {
"title": "Instellingen",
@@ -1734,7 +1777,12 @@
"customPricingNote": "U kunt de standaardprijzen voor specifieke modellen overschrijven. Aangepaste overschrijvingen hebben voorrang op automatisch gedetecteerde prijzen.",
"editPricing": "Prijzen bewerken",
"viewFullDetails": "Bekijk volledige details",
"themeCoral": "Koraal"
"themeCoral": "Koraal",
"cliFingerprint": "CLI Fingerprint Matching",
"cliFingerprintDesc": "Match native CLI binary signatures when proxying requests. Reorders headers and body fields to look identical to the official CLI tools. Your proxy IP is preserved.",
"cliFingerprintEnabled": "{count} provider(s) with CLI fingerprint active",
"enableFingerprintTitle": "Enable fingerprint for {provider}",
"disableFingerprintTitle": "Disable fingerprint for {provider}"
},
"translator": {
"title": "Vertaler",
@@ -2419,5 +2467,22 @@
"featureWebhooks": "Webhook configuration and event subscriptions",
"featureSwagger": "OpenAPI / Swagger spec auto-generation",
"featureAuth": "API key and OAuth scope management per endpoint"
},
"agents": {
"title": "CLI Agents",
"description": "Discover installed CLI agents on your system. Add custom agents for auto-detection.",
"refresh": "Refresh",
"installed": "Installed",
"notFound": "Not Found",
"builtIn": "Built-in",
"custom": "Custom",
"remove": "Remove",
"addCustomAgent": "Add Custom Agent",
"addCustomAgentDesc": "Register any CLI tool for detection. It will be scanned automatically on refresh.",
"agentName": "Agent Name",
"binaryName": "Binary Name",
"versionCommand": "Version Command",
"spawnArgs": "Spawn Args",
"addAgent": "Add Agent"
}
}
+69 -4
View File
@@ -138,7 +138,11 @@
"media": "Media",
"mediaDescription": "Generate images, videos, and music",
"themes": "Themes",
"themesDescription": "Choose a color theme for the whole dashboard panel"
"themesDescription": "Choose a color theme for the whole dashboard panel",
"mcp": "MCP",
"mcpDescription": "Model Context Protocol server management and tools",
"a2a": "A2A",
"a2aDescription": "Agent-to-Agent protocol tasks and observability"
},
"home": {
"quickStart": "Rask start",
@@ -456,7 +460,9 @@
"cline": "Cline AI-kodingsassistent CLI",
"kilo": "Kilokode AI Assistant CLI",
"cursor": "Cursor AI Code Editor",
"continue": "Fortsett AI Assistant"
"continue": "Fortsett AI Assistant",
"opencode": "OpenCode AI coding agent (Terminal)",
"kiro": "Amazon Kiro — AI-powered IDE"
},
"guides": {
"cursor": {
@@ -505,6 +511,42 @@
"desc": "Legg til følgende konfigurasjon til modellarrayet ditt:"
}
}
},
"opencode": {
"steps": {
"1": {
"title": "Install OpenCode",
"desc": "Install via npm: npm install -g opencode-ai"
},
"2": {
"title": "API Key"
},
"3": {
"title": "Set Base URL",
"desc": "opencode config set baseUrl {{baseUrl}}"
},
"4": {
"title": "Select Model"
}
}
},
"kiro": {
"steps": {
"1": {
"title": "Open Kiro Settings",
"desc": "Go to Settings → AI Provider"
},
"2": {
"title": "Base URL",
"desc": "Paste your OmniRoute endpoint URL"
},
"3": {
"title": "API Key"
},
"4": {
"title": "Select Model"
}
}
}
}
},
@@ -1338,7 +1380,8 @@
"editCompatibleTitle": "Rediger {type} Kompatibel",
"compatibleBaseUrlHint": "Bruk basis-URLen (som slutter på /v1) for din {type}-kompatible API.",
"apiKeyForCheck": "API-nøkkel (for sjekk)",
"compatibleProdPlaceholder": "{type} Kompatibel (Prod)"
"compatibleProdPlaceholder": "{type} Kompatibel (Prod)",
"providerTestTimeout": "Provider test timed out — too many connections to test at once"
},
"settings": {
"title": "Innstillinger",
@@ -1734,7 +1777,12 @@
"customPricingNote": "Du kan overstyre standardpriser for spesifikke modeller. Egendefinerte overstyringer prioriteres fremfor automatisk oppdagede priser.",
"editPricing": "Rediger priser",
"viewFullDetails": "Se alle detaljer",
"themeCoral": "Korall"
"themeCoral": "Korall",
"cliFingerprint": "CLI Fingerprint Matching",
"cliFingerprintDesc": "Match native CLI binary signatures when proxying requests. Reorders headers and body fields to look identical to the official CLI tools. Your proxy IP is preserved.",
"cliFingerprintEnabled": "{count} provider(s) with CLI fingerprint active",
"enableFingerprintTitle": "Enable fingerprint for {provider}",
"disableFingerprintTitle": "Disable fingerprint for {provider}"
},
"translator": {
"title": "Oversetter",
@@ -2419,5 +2467,22 @@
"featureWebhooks": "Webhook configuration and event subscriptions",
"featureSwagger": "OpenAPI / Swagger spec auto-generation",
"featureAuth": "API key and OAuth scope management per endpoint"
},
"agents": {
"title": "CLI Agents",
"description": "Discover installed CLI agents on your system. Add custom agents for auto-detection.",
"refresh": "Refresh",
"installed": "Installed",
"notFound": "Not Found",
"builtIn": "Built-in",
"custom": "Custom",
"remove": "Remove",
"addCustomAgent": "Add Custom Agent",
"addCustomAgentDesc": "Register any CLI tool for detection. It will be scanned automatically on refresh.",
"agentName": "Agent Name",
"binaryName": "Binary Name",
"versionCommand": "Version Command",
"spawnArgs": "Spawn Args",
"addAgent": "Add Agent"
}
}
+69 -4
View File
@@ -138,7 +138,11 @@
"media": "Media",
"mediaDescription": "Generate images, videos, and music",
"themes": "Themes",
"themesDescription": "Choose a color theme for the whole dashboard panel"
"themesDescription": "Choose a color theme for the whole dashboard panel",
"mcp": "MCP",
"mcpDescription": "Model Context Protocol server management and tools",
"a2a": "A2A",
"a2aDescription": "Agent-to-Agent protocol tasks and observability"
},
"home": {
"quickStart": "Mabilis na Pagsisimula",
@@ -456,7 +460,9 @@
"cline": "Cline AI Coding Assistant CLI",
"kilo": "Kilo Code AI Assistant CLI",
"cursor": "Cursor AI Code Editor",
"continue": "Ipagpatuloy ang AI Assistant"
"continue": "Ipagpatuloy ang AI Assistant",
"opencode": "OpenCode AI coding agent (Terminal)",
"kiro": "Amazon Kiro — AI-powered IDE"
},
"guides": {
"cursor": {
@@ -505,6 +511,42 @@
"desc": "Idagdag ang sumusunod na configuration sa iyong array ng mga modelo:"
}
}
},
"opencode": {
"steps": {
"1": {
"title": "Install OpenCode",
"desc": "Install via npm: npm install -g opencode-ai"
},
"2": {
"title": "API Key"
},
"3": {
"title": "Set Base URL",
"desc": "opencode config set baseUrl {{baseUrl}}"
},
"4": {
"title": "Select Model"
}
}
},
"kiro": {
"steps": {
"1": {
"title": "Open Kiro Settings",
"desc": "Go to Settings → AI Provider"
},
"2": {
"title": "Base URL",
"desc": "Paste your OmniRoute endpoint URL"
},
"3": {
"title": "API Key"
},
"4": {
"title": "Select Model"
}
}
}
}
},
@@ -1338,7 +1380,8 @@
"editCompatibleTitle": "I-edit ang {type} Compatible",
"compatibleBaseUrlHint": "Gamitin ang base URL (nagtatapos sa /v1) para sa iyong {type}-compatible na API.",
"apiKeyForCheck": "API Key (para sa Pagsusuri)",
"compatibleProdPlaceholder": "{type} Compatible (Prod)"
"compatibleProdPlaceholder": "{type} Compatible (Prod)",
"providerTestTimeout": "Provider test timed out — too many connections to test at once"
},
"settings": {
"title": "Mga setting",
@@ -1734,7 +1777,12 @@
"customPricingNote": "Maaari mong i-override ang default na pagpepresyo para sa mga partikular na modelo. Mas inuuna ang mga custom na override kaysa sa awtomatikong natukoy na pagpepresyo.",
"editPricing": "I-edit ang Pagpepresyo",
"viewFullDetails": "Tingnan ang Buong Detalye",
"themeCoral": "Coral"
"themeCoral": "Coral",
"cliFingerprint": "CLI Fingerprint Matching",
"cliFingerprintDesc": "Match native CLI binary signatures when proxying requests. Reorders headers and body fields to look identical to the official CLI tools. Your proxy IP is preserved.",
"cliFingerprintEnabled": "{count} provider(s) with CLI fingerprint active",
"enableFingerprintTitle": "Enable fingerprint for {provider}",
"disableFingerprintTitle": "Disable fingerprint for {provider}"
},
"translator": {
"title": "Tagasalin",
@@ -2419,5 +2467,22 @@
"featureWebhooks": "Webhook configuration and event subscriptions",
"featureSwagger": "OpenAPI / Swagger spec auto-generation",
"featureAuth": "API key and OAuth scope management per endpoint"
},
"agents": {
"title": "CLI Agents",
"description": "Discover installed CLI agents on your system. Add custom agents for auto-detection.",
"refresh": "Refresh",
"installed": "Installed",
"notFound": "Not Found",
"builtIn": "Built-in",
"custom": "Custom",
"remove": "Remove",
"addCustomAgent": "Add Custom Agent",
"addCustomAgentDesc": "Register any CLI tool for detection. It will be scanned automatically on refresh.",
"agentName": "Agent Name",
"binaryName": "Binary Name",
"versionCommand": "Version Command",
"spawnArgs": "Spawn Args",
"addAgent": "Add Agent"
}
}
+69 -4
View File
@@ -138,7 +138,11 @@
"media": "Media",
"mediaDescription": "Generate images, videos, and music",
"themes": "Themes",
"themesDescription": "Choose a color theme for the whole dashboard panel"
"themesDescription": "Choose a color theme for the whole dashboard panel",
"mcp": "MCP",
"mcpDescription": "Model Context Protocol server management and tools",
"a2a": "A2A",
"a2aDescription": "Agent-to-Agent protocol tasks and observability"
},
"home": {
"quickStart": "Szybki start",
@@ -456,7 +460,9 @@
"cline": "Cline AI Asystent kodowania CLI",
"kilo": "Kilo Code AI Assistant CLI",
"cursor": "Edytor kodu AI kursora",
"continue": "Kontynuuj Asystenta AI"
"continue": "Kontynuuj Asystenta AI",
"opencode": "OpenCode AI coding agent (Terminal)",
"kiro": "Amazon Kiro — AI-powered IDE"
},
"guides": {
"cursor": {
@@ -505,6 +511,42 @@
"desc": "Dodaj następującą konfigurację do tablicy modeli:"
}
}
},
"opencode": {
"steps": {
"1": {
"title": "Install OpenCode",
"desc": "Install via npm: npm install -g opencode-ai"
},
"2": {
"title": "API Key"
},
"3": {
"title": "Set Base URL",
"desc": "opencode config set baseUrl {{baseUrl}}"
},
"4": {
"title": "Select Model"
}
}
},
"kiro": {
"steps": {
"1": {
"title": "Open Kiro Settings",
"desc": "Go to Settings → AI Provider"
},
"2": {
"title": "Base URL",
"desc": "Paste your OmniRoute endpoint URL"
},
"3": {
"title": "API Key"
},
"4": {
"title": "Select Model"
}
}
}
}
},
@@ -1338,7 +1380,8 @@
"editCompatibleTitle": "Edytuj {type} Kompatybilny",
"compatibleBaseUrlHint": "Użyj podstawowego adresu URL (kończącego się na /v1) dla interfejsu API zgodnego z {type}.",
"apiKeyForCheck": "Klucz API (do sprawdzenia)",
"compatibleProdPlaceholder": "{type} Kompatybilny (Prod)"
"compatibleProdPlaceholder": "{type} Kompatybilny (Prod)",
"providerTestTimeout": "Provider test timed out — too many connections to test at once"
},
"settings": {
"title": "Ustawienia",
@@ -1734,7 +1777,12 @@
"customPricingNote": "Możesz zastąpić domyślne ceny dla określonych modeli. Zastąpienia niestandardowe mają pierwszeństwo przed automatycznie wykrytymi cenami.",
"editPricing": "Edytuj ceny",
"viewFullDetails": "Zobacz pełne szczegóły",
"themeCoral": "Koral"
"themeCoral": "Koral",
"cliFingerprint": "CLI Fingerprint Matching",
"cliFingerprintDesc": "Match native CLI binary signatures when proxying requests. Reorders headers and body fields to look identical to the official CLI tools. Your proxy IP is preserved.",
"cliFingerprintEnabled": "{count} provider(s) with CLI fingerprint active",
"enableFingerprintTitle": "Enable fingerprint for {provider}",
"disableFingerprintTitle": "Disable fingerprint for {provider}"
},
"translator": {
"title": "Tłumacz",
@@ -2419,5 +2467,22 @@
"featureWebhooks": "Webhook configuration and event subscriptions",
"featureSwagger": "OpenAPI / Swagger spec auto-generation",
"featureAuth": "API key and OAuth scope management per endpoint"
},
"agents": {
"title": "CLI Agents",
"description": "Discover installed CLI agents on your system. Add custom agents for auto-detection.",
"refresh": "Refresh",
"installed": "Installed",
"notFound": "Not Found",
"builtIn": "Built-in",
"custom": "Custom",
"remove": "Remove",
"addCustomAgent": "Add Custom Agent",
"addCustomAgentDesc": "Register any CLI tool for detection. It will be scanned automatically on refresh.",
"agentName": "Agent Name",
"binaryName": "Binary Name",
"versionCommand": "Version Command",
"spawnArgs": "Spawn Args",
"addAgent": "Add Agent"
}
}
+46 -3
View File
@@ -138,7 +138,11 @@
"media": "Mídia",
"mediaDescription": "Gerar imagens, vídeos e músicas",
"themes": "Themes",
"themesDescription": "Choose a color theme for the whole dashboard panel"
"themesDescription": "Choose a color theme for the whole dashboard panel",
"mcp": "MCP",
"mcpDescription": "Model Context Protocol server management and tools",
"a2a": "A2A",
"a2aDescription": "Agent-to-Agent protocol tasks and observability"
},
"home": {
"quickStart": "Início Rápido",
@@ -456,7 +460,9 @@
"cline": "CLI assistente de codificação Cline",
"kilo": "CLI assistente de IA Kilo Code",
"cursor": "Editor de código com IA Cursor",
"continue": "Assistente de IA Continue"
"continue": "Assistente de IA Continue",
"opencode": "OpenCode AI coding agent (Terminal)",
"kiro": "Amazon Kiro — AI-powered IDE"
},
"guides": {
"cursor": {
@@ -505,6 +511,42 @@
"desc": "Adicione a configuração abaixo ao array de modelos:"
}
}
},
"opencode": {
"steps": {
"1": {
"title": "Install OpenCode",
"desc": "Install via npm: npm install -g opencode-ai"
},
"2": {
"title": "API Key"
},
"3": {
"title": "Set Base URL",
"desc": "opencode config set baseUrl {{baseUrl}}"
},
"4": {
"title": "Select Model"
}
}
},
"kiro": {
"steps": {
"1": {
"title": "Open Kiro Settings",
"desc": "Go to Settings → AI Provider"
},
"2": {
"title": "Base URL",
"desc": "Paste your OmniRoute endpoint URL"
},
"3": {
"title": "API Key"
},
"4": {
"title": "Select Model"
}
}
}
}
},
@@ -1338,7 +1380,8 @@
"editCompatibleTitle": "Editar Compatível {type}",
"compatibleBaseUrlHint": "Use a URL base (terminando em /v1) para sua API compatível com {type}.",
"apiKeyForCheck": "Chave de API (para verificação)",
"compatibleProdPlaceholder": "{type} Compatível (Prod)"
"compatibleProdPlaceholder": "{type} Compatível (Prod)",
"providerTestTimeout": "Provider test timed out — too many connections to test at once"
},
"settings": {
"title": "Configurações",
+69 -4
View File
@@ -138,7 +138,11 @@
"media": "Media",
"mediaDescription": "Generate images, videos, and music",
"themes": "Themes",
"themesDescription": "Choose a color theme for the whole dashboard panel"
"themesDescription": "Choose a color theme for the whole dashboard panel",
"mcp": "MCP",
"mcpDescription": "Model Context Protocol server management and tools",
"a2a": "A2A",
"a2aDescription": "Agent-to-Agent protocol tasks and observability"
},
"home": {
"quickStart": "Início rápido",
@@ -456,7 +460,9 @@
"cline": "CLI do assistente de codificação Cline AI",
"kilo": "CLI do assistente de IA do Kilo Code",
"cursor": "Editor de código do cursor AI",
"continue": "Continuar Assistente de IA"
"continue": "Continuar Assistente de IA",
"opencode": "OpenCode AI coding agent (Terminal)",
"kiro": "Amazon Kiro — AI-powered IDE"
},
"guides": {
"cursor": {
@@ -505,6 +511,42 @@
"desc": "Adicione a seguinte configuração ao seu array de modelos:"
}
}
},
"opencode": {
"steps": {
"1": {
"title": "Install OpenCode",
"desc": "Install via npm: npm install -g opencode-ai"
},
"2": {
"title": "API Key"
},
"3": {
"title": "Set Base URL",
"desc": "opencode config set baseUrl {{baseUrl}}"
},
"4": {
"title": "Select Model"
}
}
},
"kiro": {
"steps": {
"1": {
"title": "Open Kiro Settings",
"desc": "Go to Settings → AI Provider"
},
"2": {
"title": "Base URL",
"desc": "Paste your OmniRoute endpoint URL"
},
"3": {
"title": "API Key"
},
"4": {
"title": "Select Model"
}
}
}
}
},
@@ -1350,7 +1392,8 @@
"editCompatibleTitle": "Editar {type} Compatível",
"compatibleBaseUrlHint": "Use o URL base (terminando em /v1) para sua API compatível com {type}.",
"apiKeyForCheck": "Chave API (para verificação)",
"compatibleProdPlaceholder": "{type} Compatível (Produção)"
"compatibleProdPlaceholder": "{type} Compatível (Produção)",
"providerTestTimeout": "Provider test timed out — too many connections to test at once"
},
"settings": {
"title": "Configurações",
@@ -1746,7 +1789,12 @@
"customPricingNote": "Você pode substituir o preço padrão de modelos específicos. As substituições personalizadas têm prioridade sobre os preços detectados automaticamente.",
"editPricing": "Editar preços",
"viewFullDetails": "Ver detalhes completos",
"themeCoral": "Coral"
"themeCoral": "Coral",
"cliFingerprint": "CLI Fingerprint Matching",
"cliFingerprintDesc": "Match native CLI binary signatures when proxying requests. Reorders headers and body fields to look identical to the official CLI tools. Your proxy IP is preserved.",
"cliFingerprintEnabled": "{count} provider(s) with CLI fingerprint active",
"enableFingerprintTitle": "Enable fingerprint for {provider}",
"disableFingerprintTitle": "Disable fingerprint for {provider}"
},
"translator": {
"title": "Tradutor",
@@ -2419,5 +2467,22 @@
"termsSection5Text": "OmniRoute é fornecido \"como está\" sem qualquer tipo de garantia. Não somos responsáveis por quaisquer custos incorridos através do uso da API, interrupções de serviço ou perda de dados. Sempre mantenha backups de sua configuração.",
"termsSection6Title": "6. Código aberto",
"termsSection6Text": "OmniRoute é um software de código aberto. Você é livre para inspecioná-lo, modificá-lo e distribuí-lo sob os termos de sua licença."
},
"agents": {
"title": "CLI Agents",
"description": "Discover installed CLI agents on your system. Add custom agents for auto-detection.",
"refresh": "Refresh",
"installed": "Installed",
"notFound": "Not Found",
"builtIn": "Built-in",
"custom": "Custom",
"remove": "Remove",
"addCustomAgent": "Add Custom Agent",
"addCustomAgentDesc": "Register any CLI tool for detection. It will be scanned automatically on refresh.",
"agentName": "Agent Name",
"binaryName": "Binary Name",
"versionCommand": "Version Command",
"spawnArgs": "Spawn Args",
"addAgent": "Add Agent"
}
}
+69 -4
View File
@@ -138,7 +138,11 @@
"media": "Media",
"mediaDescription": "Generate images, videos, and music",
"themes": "Themes",
"themesDescription": "Choose a color theme for the whole dashboard panel"
"themesDescription": "Choose a color theme for the whole dashboard panel",
"mcp": "MCP",
"mcpDescription": "Model Context Protocol server management and tools",
"a2a": "A2A",
"a2aDescription": "Agent-to-Agent protocol tasks and observability"
},
"home": {
"quickStart": "Pornire rapidă",
@@ -456,7 +460,9 @@
"cline": "CLI Cline AI Coding Assistant",
"kilo": "Kilo Code AI Assistant CLI",
"cursor": "Cursor AI Code Editor",
"continue": "Continuați Asistentul AI"
"continue": "Continuați Asistentul AI",
"opencode": "OpenCode AI coding agent (Terminal)",
"kiro": "Amazon Kiro — AI-powered IDE"
},
"guides": {
"cursor": {
@@ -505,6 +511,42 @@
"desc": "Adăugați următoarea configurație la matricea dvs. de modele:"
}
}
},
"opencode": {
"steps": {
"1": {
"title": "Install OpenCode",
"desc": "Install via npm: npm install -g opencode-ai"
},
"2": {
"title": "API Key"
},
"3": {
"title": "Set Base URL",
"desc": "opencode config set baseUrl {{baseUrl}}"
},
"4": {
"title": "Select Model"
}
}
},
"kiro": {
"steps": {
"1": {
"title": "Open Kiro Settings",
"desc": "Go to Settings → AI Provider"
},
"2": {
"title": "Base URL",
"desc": "Paste your OmniRoute endpoint URL"
},
"3": {
"title": "API Key"
},
"4": {
"title": "Select Model"
}
}
}
}
},
@@ -1338,7 +1380,8 @@
"editCompatibleTitle": "Editați {type} Compatibil",
"compatibleBaseUrlHint": "Utilizați adresa URL de bază (se termină în /v1) pentru API-ul dvs. compatibil {type}.",
"apiKeyForCheck": "Cheie API (pentru verificare)",
"compatibleProdPlaceholder": "{type} Compatibil (Prod)"
"compatibleProdPlaceholder": "{type} Compatibil (Prod)",
"providerTestTimeout": "Provider test timed out — too many connections to test at once"
},
"settings": {
"title": "Setări",
@@ -1734,7 +1777,12 @@
"customPricingNote": "Puteți suprascrie prețurile implicite pentru anumite modele. Anulările personalizate au prioritate față de prețurile detectate automat.",
"editPricing": "Editați prețul",
"viewFullDetails": "Vezi detalii complete",
"themeCoral": "Coral"
"themeCoral": "Coral",
"cliFingerprint": "CLI Fingerprint Matching",
"cliFingerprintDesc": "Match native CLI binary signatures when proxying requests. Reorders headers and body fields to look identical to the official CLI tools. Your proxy IP is preserved.",
"cliFingerprintEnabled": "{count} provider(s) with CLI fingerprint active",
"enableFingerprintTitle": "Enable fingerprint for {provider}",
"disableFingerprintTitle": "Disable fingerprint for {provider}"
},
"translator": {
"title": "Traducător",
@@ -2419,5 +2467,22 @@
"featureWebhooks": "Webhook configuration and event subscriptions",
"featureSwagger": "OpenAPI / Swagger spec auto-generation",
"featureAuth": "API key and OAuth scope management per endpoint"
},
"agents": {
"title": "CLI Agents",
"description": "Discover installed CLI agents on your system. Add custom agents for auto-detection.",
"refresh": "Refresh",
"installed": "Installed",
"notFound": "Not Found",
"builtIn": "Built-in",
"custom": "Custom",
"remove": "Remove",
"addCustomAgent": "Add Custom Agent",
"addCustomAgentDesc": "Register any CLI tool for detection. It will be scanned automatically on refresh.",
"agentName": "Agent Name",
"binaryName": "Binary Name",
"versionCommand": "Version Command",
"spawnArgs": "Spawn Args",
"addAgent": "Add Agent"
}
}
+69 -4
View File
@@ -138,7 +138,11 @@
"media": "Media",
"mediaDescription": "Generate images, videos, and music",
"themes": "Темы",
"themesDescription": "Выберите цветовую тему для всей панели"
"themesDescription": "Выберите цветовую тему для всей панели",
"mcp": "MCP",
"mcpDescription": "Model Context Protocol server management and tools",
"a2a": "A2A",
"a2aDescription": "Agent-to-Agent protocol tasks and observability"
},
"home": {
"quickStart": "Быстрый старт",
@@ -456,7 +460,9 @@
"cline": "Cline AI Coding Assistant CLI",
"kilo": "Интерфейс командной строки Kilo Code AI Assistant",
"cursor": "Редактор кода курсора AI",
"continue": "Продолжить AI-помощник"
"continue": "Продолжить AI-помощник",
"opencode": "OpenCode AI coding agent (Terminal)",
"kiro": "Amazon Kiro — AI-powered IDE"
},
"guides": {
"cursor": {
@@ -505,6 +511,42 @@
"desc": "Добавьте следующую конфигурацию в массив моделей:"
}
}
},
"opencode": {
"steps": {
"1": {
"title": "Install OpenCode",
"desc": "Install via npm: npm install -g opencode-ai"
},
"2": {
"title": "API Key"
},
"3": {
"title": "Set Base URL",
"desc": "opencode config set baseUrl {{baseUrl}}"
},
"4": {
"title": "Select Model"
}
}
},
"kiro": {
"steps": {
"1": {
"title": "Open Kiro Settings",
"desc": "Go to Settings → AI Provider"
},
"2": {
"title": "Base URL",
"desc": "Paste your OmniRoute endpoint URL"
},
"3": {
"title": "API Key"
},
"4": {
"title": "Select Model"
}
}
}
}
},
@@ -1338,7 +1380,8 @@
"editCompatibleTitle": "Изменить совместимость {type}",
"compatibleBaseUrlHint": "Используйте базовый URL-адрес (оканчивающийся на /v1) для вашего {type}-совместимого API.",
"apiKeyForCheck": "API-ключ (для проверки)",
"compatibleProdPlaceholder": "{type} Совместимость (Прод.)"
"compatibleProdPlaceholder": "{type} Совместимость (Прод.)",
"providerTestTimeout": "Provider test timed out — too many connections to test at once"
},
"settings": {
"title": "Настройки",
@@ -1734,7 +1777,12 @@
"customPricingNote": "Вы можете переопределить цены по умолчанию для определенных моделей. Пользовательские переопределения имеют приоритет над ценами, определяемыми автоматически.",
"editPricing": "Изменить цену",
"viewFullDetails": "Посмотреть полную информацию",
"themeCoral": "Коралл"
"themeCoral": "Коралл",
"cliFingerprint": "CLI Fingerprint Matching",
"cliFingerprintDesc": "Match native CLI binary signatures when proxying requests. Reorders headers and body fields to look identical to the official CLI tools. Your proxy IP is preserved.",
"cliFingerprintEnabled": "{count} provider(s) with CLI fingerprint active",
"enableFingerprintTitle": "Enable fingerprint for {provider}",
"disableFingerprintTitle": "Disable fingerprint for {provider}"
},
"translator": {
"title": "Переводчик",
@@ -2419,5 +2467,22 @@
"featureWebhooks": "Настройка вебхуков и подписки на события",
"featureSwagger": "Автоматическая генерация спецификаций OpenAPI / Swagger",
"featureAuth": "Управление API-ключами и OAuth-областями для каждого эндпоинта"
},
"agents": {
"title": "CLI Agents",
"description": "Discover installed CLI agents on your system. Add custom agents for auto-detection.",
"refresh": "Refresh",
"installed": "Installed",
"notFound": "Not Found",
"builtIn": "Built-in",
"custom": "Custom",
"remove": "Remove",
"addCustomAgent": "Add Custom Agent",
"addCustomAgentDesc": "Register any CLI tool for detection. It will be scanned automatically on refresh.",
"agentName": "Agent Name",
"binaryName": "Binary Name",
"versionCommand": "Version Command",
"spawnArgs": "Spawn Args",
"addAgent": "Add Agent"
}
}
+69 -4
View File
@@ -138,7 +138,11 @@
"media": "Media",
"mediaDescription": "Generate images, videos, and music",
"themes": "Themes",
"themesDescription": "Choose a color theme for the whole dashboard panel"
"themesDescription": "Choose a color theme for the whole dashboard panel",
"mcp": "MCP",
"mcpDescription": "Model Context Protocol server management and tools",
"a2a": "A2A",
"a2aDescription": "Agent-to-Agent protocol tasks and observability"
},
"home": {
"quickStart": "Rýchly štart",
@@ -456,7 +460,9 @@
"cline": "Cline AI Coding Assistant CLI",
"kilo": "Kilo Code AI Assistant CLI",
"cursor": "Editor kódu AI kurzora",
"continue": "Pokračovať v Asistentovi AI"
"continue": "Pokračovať v Asistentovi AI",
"opencode": "OpenCode AI coding agent (Terminal)",
"kiro": "Amazon Kiro — AI-powered IDE"
},
"guides": {
"cursor": {
@@ -505,6 +511,42 @@
"desc": "Pridajte do poľa modelov nasledujúcu konfiguráciu:"
}
}
},
"opencode": {
"steps": {
"1": {
"title": "Install OpenCode",
"desc": "Install via npm: npm install -g opencode-ai"
},
"2": {
"title": "API Key"
},
"3": {
"title": "Set Base URL",
"desc": "opencode config set baseUrl {{baseUrl}}"
},
"4": {
"title": "Select Model"
}
}
},
"kiro": {
"steps": {
"1": {
"title": "Open Kiro Settings",
"desc": "Go to Settings → AI Provider"
},
"2": {
"title": "Base URL",
"desc": "Paste your OmniRoute endpoint URL"
},
"3": {
"title": "API Key"
},
"4": {
"title": "Select Model"
}
}
}
}
},
@@ -1338,7 +1380,8 @@
"editCompatibleTitle": "Upraviť {type} kompatibilné",
"compatibleBaseUrlHint": "Pre svoje {type}-kompatibilné API použite základnú webovú adresu (končiacu na /v1).",
"apiKeyForCheck": "API kľúč (na kontrolu)",
"compatibleProdPlaceholder": "{type} Kompatibilné (produkt)"
"compatibleProdPlaceholder": "{type} Kompatibilné (produkt)",
"providerTestTimeout": "Provider test timed out — too many connections to test at once"
},
"settings": {
"title": "Nastavenia",
@@ -1734,7 +1777,12 @@
"customPricingNote": "Predvolené ceny pre konkrétne modely môžete prepísať. Vlastné prepísania majú prednosť pred automaticky zistenými cenami.",
"editPricing": "Upraviť ceny",
"viewFullDetails": "Zobraziť úplné podrobnosti",
"themeCoral": "Korál"
"themeCoral": "Korál",
"cliFingerprint": "CLI Fingerprint Matching",
"cliFingerprintDesc": "Match native CLI binary signatures when proxying requests. Reorders headers and body fields to look identical to the official CLI tools. Your proxy IP is preserved.",
"cliFingerprintEnabled": "{count} provider(s) with CLI fingerprint active",
"enableFingerprintTitle": "Enable fingerprint for {provider}",
"disableFingerprintTitle": "Disable fingerprint for {provider}"
},
"translator": {
"title": "Prekladateľ",
@@ -2419,5 +2467,22 @@
"featureWebhooks": "Webhook configuration and event subscriptions",
"featureSwagger": "OpenAPI / Swagger spec auto-generation",
"featureAuth": "API key and OAuth scope management per endpoint"
},
"agents": {
"title": "CLI Agents",
"description": "Discover installed CLI agents on your system. Add custom agents for auto-detection.",
"refresh": "Refresh",
"installed": "Installed",
"notFound": "Not Found",
"builtIn": "Built-in",
"custom": "Custom",
"remove": "Remove",
"addCustomAgent": "Add Custom Agent",
"addCustomAgentDesc": "Register any CLI tool for detection. It will be scanned automatically on refresh.",
"agentName": "Agent Name",
"binaryName": "Binary Name",
"versionCommand": "Version Command",
"spawnArgs": "Spawn Args",
"addAgent": "Add Agent"
}
}
+69 -4
View File
@@ -138,7 +138,11 @@
"media": "Media",
"mediaDescription": "Generate images, videos, and music",
"themes": "Themes",
"themesDescription": "Choose a color theme for the whole dashboard panel"
"themesDescription": "Choose a color theme for the whole dashboard panel",
"mcp": "MCP",
"mcpDescription": "Model Context Protocol server management and tools",
"a2a": "A2A",
"a2aDescription": "Agent-to-Agent protocol tasks and observability"
},
"home": {
"quickStart": "Snabbstart",
@@ -456,7 +460,9 @@
"cline": "Cline AI Coding Assistant CLI",
"kilo": "Kilokod AI Assistant CLI",
"cursor": "Cursor AI Code Editor",
"continue": "Fortsätt AI Assistant"
"continue": "Fortsätt AI Assistant",
"opencode": "OpenCode AI coding agent (Terminal)",
"kiro": "Amazon Kiro — AI-powered IDE"
},
"guides": {
"cursor": {
@@ -505,6 +511,42 @@
"desc": "Lägg till följande konfiguration till din modellarray:"
}
}
},
"opencode": {
"steps": {
"1": {
"title": "Install OpenCode",
"desc": "Install via npm: npm install -g opencode-ai"
},
"2": {
"title": "API Key"
},
"3": {
"title": "Set Base URL",
"desc": "opencode config set baseUrl {{baseUrl}}"
},
"4": {
"title": "Select Model"
}
}
},
"kiro": {
"steps": {
"1": {
"title": "Open Kiro Settings",
"desc": "Go to Settings → AI Provider"
},
"2": {
"title": "Base URL",
"desc": "Paste your OmniRoute endpoint URL"
},
"3": {
"title": "API Key"
},
"4": {
"title": "Select Model"
}
}
}
}
},
@@ -1338,7 +1380,8 @@
"editCompatibleTitle": "Redigera {type} Kompatibel",
"compatibleBaseUrlHint": "Använd basadressen (som slutar på /v1) för ditt {type}-kompatibla API.",
"apiKeyForCheck": "API-nyckel (för kontroll)",
"compatibleProdPlaceholder": "{type} Kompatibel (Prod)"
"compatibleProdPlaceholder": "{type} Kompatibel (Prod)",
"providerTestTimeout": "Provider test timed out — too many connections to test at once"
},
"settings": {
"title": "Inställningar",
@@ -1734,7 +1777,12 @@
"customPricingNote": "Du kan åsidosätta standardpriser för specifika modeller. Anpassade åsidosättningar har prioritet framför automatiskt identifierade priser.",
"editPricing": "Redigera prissättning",
"viewFullDetails": "Visa fullständiga detaljer",
"themeCoral": "Korall"
"themeCoral": "Korall",
"cliFingerprint": "CLI Fingerprint Matching",
"cliFingerprintDesc": "Match native CLI binary signatures when proxying requests. Reorders headers and body fields to look identical to the official CLI tools. Your proxy IP is preserved.",
"cliFingerprintEnabled": "{count} provider(s) with CLI fingerprint active",
"enableFingerprintTitle": "Enable fingerprint for {provider}",
"disableFingerprintTitle": "Disable fingerprint for {provider}"
},
"translator": {
"title": "Översättare",
@@ -2419,5 +2467,22 @@
"featureWebhooks": "Webhook configuration and event subscriptions",
"featureSwagger": "OpenAPI / Swagger spec auto-generation",
"featureAuth": "API key and OAuth scope management per endpoint"
},
"agents": {
"title": "CLI Agents",
"description": "Discover installed CLI agents on your system. Add custom agents for auto-detection.",
"refresh": "Refresh",
"installed": "Installed",
"notFound": "Not Found",
"builtIn": "Built-in",
"custom": "Custom",
"remove": "Remove",
"addCustomAgent": "Add Custom Agent",
"addCustomAgentDesc": "Register any CLI tool for detection. It will be scanned automatically on refresh.",
"agentName": "Agent Name",
"binaryName": "Binary Name",
"versionCommand": "Version Command",
"spawnArgs": "Spawn Args",
"addAgent": "Add Agent"
}
}
+69 -4
View File
@@ -138,7 +138,11 @@
"media": "Media",
"mediaDescription": "Generate images, videos, and music",
"themes": "ธีมส์",
"themesDescription": "Choose a color theme for the whole dashboard panel"
"themesDescription": "Choose a color theme for the whole dashboard panel",
"mcp": "MCP",
"mcpDescription": "Model Context Protocol server management and tools",
"a2a": "A2A",
"a2aDescription": "Agent-to-Agent protocol tasks and observability"
},
"home": {
"quickStart": "เริ่มต้นอย่างรวดเร็ว",
@@ -456,7 +460,9 @@
"cline": "Cline AI ผู้ช่วยเข้ารหัส CLI",
"kilo": "กิโลโค้ด AI Assistant CLI",
"cursor": "ตัวแก้ไขรหัสเคอร์เซอร์ AI",
"continue": "ดำเนินการต่อผู้ช่วย AI"
"continue": "ดำเนินการต่อผู้ช่วย AI",
"opencode": "OpenCode AI coding agent (Terminal)",
"kiro": "Amazon Kiro — AI-powered IDE"
},
"guides": {
"cursor": {
@@ -505,6 +511,42 @@
"desc": "เพิ่มการกำหนดค่าต่อไปนี้ให้กับอาร์เรย์โมเดลของคุณ:"
}
}
},
"opencode": {
"steps": {
"1": {
"title": "Install OpenCode",
"desc": "Install via npm: npm install -g opencode-ai"
},
"2": {
"title": "API Key"
},
"3": {
"title": "Set Base URL",
"desc": "opencode config set baseUrl {{baseUrl}}"
},
"4": {
"title": "Select Model"
}
}
},
"kiro": {
"steps": {
"1": {
"title": "Open Kiro Settings",
"desc": "Go to Settings → AI Provider"
},
"2": {
"title": "Base URL",
"desc": "Paste your OmniRoute endpoint URL"
},
"3": {
"title": "API Key"
},
"4": {
"title": "Select Model"
}
}
}
}
},
@@ -1338,7 +1380,8 @@
"editCompatibleTitle": "แก้ไข {type} เข้ากันได้",
"compatibleBaseUrlHint": "ใช้ URL พื้นฐาน (ลงท้ายด้วย /v1) สำหรับ {type}- API ที่เข้ากันได้กับของคุณ",
"apiKeyForCheck": "คีย์ API (สำหรับการตรวจสอบ)",
"compatibleProdPlaceholder": "{type} เข้ากันได้ (ผลิตภัณฑ์)"
"compatibleProdPlaceholder": "{type} เข้ากันได้ (ผลิตภัณฑ์)",
"providerTestTimeout": "Provider test timed out — too many connections to test at once"
},
"settings": {
"title": "การตั้งค่า",
@@ -1734,7 +1777,12 @@
"customPricingNote": "คุณสามารถแทนที่ราคาเริ่มต้นสำหรับรุ่นเฉพาะได้ การแทนที่แบบกำหนดเองจะมีลำดับความสำคัญมากกว่าการกำหนดราคาที่ตรวจพบอัตโนมัติ",
"editPricing": "แก้ไขราคา",
"viewFullDetails": "ดูรายละเอียดทั้งหมด",
"themeCoral": "คอรัล"
"themeCoral": "คอรัล",
"cliFingerprint": "CLI Fingerprint Matching",
"cliFingerprintDesc": "Match native CLI binary signatures when proxying requests. Reorders headers and body fields to look identical to the official CLI tools. Your proxy IP is preserved.",
"cliFingerprintEnabled": "{count} provider(s) with CLI fingerprint active",
"enableFingerprintTitle": "Enable fingerprint for {provider}",
"disableFingerprintTitle": "Disable fingerprint for {provider}"
},
"translator": {
"title": "นักแปล",
@@ -2419,5 +2467,22 @@
"featureWebhooks": "Webhook configuration and event subscriptions",
"featureSwagger": "OpenAPI / Swagger spec auto-generation",
"featureAuth": "API key and OAuth scope management per endpoint"
},
"agents": {
"title": "CLI Agents",
"description": "Discover installed CLI agents on your system. Add custom agents for auto-detection.",
"refresh": "Refresh",
"installed": "Installed",
"notFound": "Not Found",
"builtIn": "Built-in",
"custom": "Custom",
"remove": "Remove",
"addCustomAgent": "Add Custom Agent",
"addCustomAgentDesc": "Register any CLI tool for detection. It will be scanned automatically on refresh.",
"agentName": "Agent Name",
"binaryName": "Binary Name",
"versionCommand": "Version Command",
"spawnArgs": "Spawn Args",
"addAgent": "Add Agent"
}
}
+69 -4
View File
@@ -138,7 +138,11 @@
"media": "Media",
"mediaDescription": "Generate images, videos, and music",
"themes": "Themes",
"themesDescription": "Choose a color theme for the whole dashboard panel"
"themesDescription": "Choose a color theme for the whole dashboard panel",
"mcp": "MCP",
"mcpDescription": "Model Context Protocol server management and tools",
"a2a": "A2A",
"a2aDescription": "Agent-to-Agent protocol tasks and observability"
},
"home": {
"quickStart": "Швидкий старт",
@@ -456,7 +460,9 @@
"cline": "Cline AI Coding Assistant CLI",
"kilo": "Кіло Код AI Assistant CLI",
"cursor": "Редактор коду Cursor AI",
"continue": "Продовжити AI Assistant"
"continue": "Продовжити AI Assistant",
"opencode": "OpenCode AI coding agent (Terminal)",
"kiro": "Amazon Kiro — AI-powered IDE"
},
"guides": {
"cursor": {
@@ -505,6 +511,42 @@
"desc": "Додайте таку конфігурацію до свого масиву моделей:"
}
}
},
"opencode": {
"steps": {
"1": {
"title": "Install OpenCode",
"desc": "Install via npm: npm install -g opencode-ai"
},
"2": {
"title": "API Key"
},
"3": {
"title": "Set Base URL",
"desc": "opencode config set baseUrl {{baseUrl}}"
},
"4": {
"title": "Select Model"
}
}
},
"kiro": {
"steps": {
"1": {
"title": "Open Kiro Settings",
"desc": "Go to Settings → AI Provider"
},
"2": {
"title": "Base URL",
"desc": "Paste your OmniRoute endpoint URL"
},
"3": {
"title": "API Key"
},
"4": {
"title": "Select Model"
}
}
}
}
},
@@ -1338,7 +1380,8 @@
"editCompatibleTitle": "Редагувати {type} Сумісний",
"compatibleBaseUrlHint": "Використовуйте базову URL-адресу (закінчується на /v1) для свого {type}-сумісного API.",
"apiKeyForCheck": "Ключ API (для перевірки)",
"compatibleProdPlaceholder": "{type} Сумісність (Prod)"
"compatibleProdPlaceholder": "{type} Сумісність (Prod)",
"providerTestTimeout": "Provider test timed out — too many connections to test at once"
},
"settings": {
"title": "Налаштування",
@@ -1734,7 +1777,12 @@
"customPricingNote": "Ви можете змінити ціни за умовчанням для певних моделей. Спеціальні зміни мають пріоритет над автоматично визначеними цінами.",
"editPricing": "Редагувати ціни",
"viewFullDetails": "Переглянути повну інформацію",
"themeCoral": "Корал"
"themeCoral": "Корал",
"cliFingerprint": "CLI Fingerprint Matching",
"cliFingerprintDesc": "Match native CLI binary signatures when proxying requests. Reorders headers and body fields to look identical to the official CLI tools. Your proxy IP is preserved.",
"cliFingerprintEnabled": "{count} provider(s) with CLI fingerprint active",
"enableFingerprintTitle": "Enable fingerprint for {provider}",
"disableFingerprintTitle": "Disable fingerprint for {provider}"
},
"translator": {
"title": "Перекладач",
@@ -2419,5 +2467,22 @@
"featureWebhooks": "Webhook configuration and event subscriptions",
"featureSwagger": "OpenAPI / Swagger spec auto-generation",
"featureAuth": "API key and OAuth scope management per endpoint"
},
"agents": {
"title": "CLI Agents",
"description": "Discover installed CLI agents on your system. Add custom agents for auto-detection.",
"refresh": "Refresh",
"installed": "Installed",
"notFound": "Not Found",
"builtIn": "Built-in",
"custom": "Custom",
"remove": "Remove",
"addCustomAgent": "Add Custom Agent",
"addCustomAgentDesc": "Register any CLI tool for detection. It will be scanned automatically on refresh.",
"agentName": "Agent Name",
"binaryName": "Binary Name",
"versionCommand": "Version Command",
"spawnArgs": "Spawn Args",
"addAgent": "Add Agent"
}
}
+69 -4
View File
@@ -138,7 +138,11 @@
"media": "Media",
"mediaDescription": "Generate images, videos, and music",
"themes": "Themes",
"themesDescription": "Choose a color theme for the whole dashboard panel"
"themesDescription": "Choose a color theme for the whole dashboard panel",
"mcp": "MCP",
"mcpDescription": "Model Context Protocol server management and tools",
"a2a": "A2A",
"a2aDescription": "Agent-to-Agent protocol tasks and observability"
},
"home": {
"quickStart": "Bắt đầu nhanh",
@@ -456,7 +460,9 @@
"cline": "Trợ lý mã hóa Cline AI CLI",
"kilo": "Trợ lý AI Kilo Code CLI",
"cursor": "Trình chỉnh sửa mã AI con trỏ",
"continue": "Tiếp tục Trợ lý AI"
"continue": "Tiếp tục Trợ lý AI",
"opencode": "OpenCode AI coding agent (Terminal)",
"kiro": "Amazon Kiro — AI-powered IDE"
},
"guides": {
"cursor": {
@@ -505,6 +511,42 @@
"desc": "Thêm cấu hình sau vào mảng mô hình của bạn:"
}
}
},
"opencode": {
"steps": {
"1": {
"title": "Install OpenCode",
"desc": "Install via npm: npm install -g opencode-ai"
},
"2": {
"title": "API Key"
},
"3": {
"title": "Set Base URL",
"desc": "opencode config set baseUrl {{baseUrl}}"
},
"4": {
"title": "Select Model"
}
}
},
"kiro": {
"steps": {
"1": {
"title": "Open Kiro Settings",
"desc": "Go to Settings → AI Provider"
},
"2": {
"title": "Base URL",
"desc": "Paste your OmniRoute endpoint URL"
},
"3": {
"title": "API Key"
},
"4": {
"title": "Select Model"
}
}
}
}
},
@@ -1338,7 +1380,8 @@
"editCompatibleTitle": "Chỉnh sửa {type} Tương thích",
"compatibleBaseUrlHint": "Sử dụng URL cơ sở (kết thúc bằng /v1) cho API tương thích {type} của bạn.",
"apiKeyForCheck": "Khóa API (để kiểm tra)",
"compatibleProdPlaceholder": "{type} Tương thích (Sản phẩm)"
"compatibleProdPlaceholder": "{type} Tương thích (Sản phẩm)",
"providerTestTimeout": "Provider test timed out — too many connections to test at once"
},
"settings": {
"title": "Cài đặt",
@@ -1734,7 +1777,12 @@
"customPricingNote": "Bạn có thể ghi đè giá mặc định cho các kiểu máy cụ thể. Ghi đè tùy chỉnh được ưu tiên hơn giá được tự động phát hiện.",
"editPricing": "Chỉnh sửa giá",
"viewFullDetails": "Xem chi tiết đầy đủ",
"themeCoral": "San hô"
"themeCoral": "San hô",
"cliFingerprint": "CLI Fingerprint Matching",
"cliFingerprintDesc": "Match native CLI binary signatures when proxying requests. Reorders headers and body fields to look identical to the official CLI tools. Your proxy IP is preserved.",
"cliFingerprintEnabled": "{count} provider(s) with CLI fingerprint active",
"enableFingerprintTitle": "Enable fingerprint for {provider}",
"disableFingerprintTitle": "Disable fingerprint for {provider}"
},
"translator": {
"title": "Người phiên dịch",
@@ -2419,5 +2467,22 @@
"featureWebhooks": "Webhook configuration and event subscriptions",
"featureSwagger": "OpenAPI / Swagger spec auto-generation",
"featureAuth": "API key and OAuth scope management per endpoint"
},
"agents": {
"title": "CLI Agents",
"description": "Discover installed CLI agents on your system. Add custom agents for auto-detection.",
"refresh": "Refresh",
"installed": "Installed",
"notFound": "Not Found",
"builtIn": "Built-in",
"custom": "Custom",
"remove": "Remove",
"addCustomAgent": "Add Custom Agent",
"addCustomAgentDesc": "Register any CLI tool for detection. It will be scanned automatically on refresh.",
"agentName": "Agent Name",
"binaryName": "Binary Name",
"versionCommand": "Version Command",
"spawnArgs": "Spawn Args",
"addAgent": "Add Agent"
}
}
+69 -4
View File
@@ -138,7 +138,11 @@
"media": "Media",
"mediaDescription": "Generate images, videos, and music",
"themes": "Themes",
"themesDescription": "Choose a color theme for the whole dashboard panel"
"themesDescription": "Choose a color theme for the whole dashboard panel",
"mcp": "MCP",
"mcpDescription": "Model Context Protocol server management and tools",
"a2a": "A2A",
"a2aDescription": "Agent-to-Agent protocol tasks and observability"
},
"home": {
"quickStart": "快速入门",
@@ -456,7 +460,9 @@
"cline": "Cline AI 编码助手 CLI",
"kilo": "Kilo Code AI 助手 CLI",
"cursor": "光标AI代码编辑器",
"continue": "继续AI助手"
"continue": "继续AI助手",
"opencode": "OpenCode AI coding agent (Terminal)",
"kiro": "Amazon Kiro — AI-powered IDE"
},
"guides": {
"cursor": {
@@ -505,6 +511,42 @@
"desc": "将以下配置添加到您的模型数组中:"
}
}
},
"opencode": {
"steps": {
"1": {
"title": "Install OpenCode",
"desc": "Install via npm: npm install -g opencode-ai"
},
"2": {
"title": "API Key"
},
"3": {
"title": "Set Base URL",
"desc": "opencode config set baseUrl {{baseUrl}}"
},
"4": {
"title": "Select Model"
}
}
},
"kiro": {
"steps": {
"1": {
"title": "Open Kiro Settings",
"desc": "Go to Settings → AI Provider"
},
"2": {
"title": "Base URL",
"desc": "Paste your OmniRoute endpoint URL"
},
"3": {
"title": "API Key"
},
"4": {
"title": "Select Model"
}
}
}
}
},
@@ -1338,7 +1380,8 @@
"editCompatibleTitle": "编辑 {type} 兼容",
"compatibleBaseUrlHint": "使用 {type} 兼容 API 的基本 URL(以 /v1 结尾)。",
"apiKeyForCheck": "API 密钥(用于检查)",
"compatibleProdPlaceholder": "{type} 兼容(产品)"
"compatibleProdPlaceholder": "{type} 兼容(产品)",
"providerTestTimeout": "Provider test timed out — too many connections to test at once"
},
"settings": {
"title": "设置",
@@ -1734,7 +1777,12 @@
"customPricingNote": "您可以覆盖特定型号的默认定价。自定义覆盖优先于自动检测的定价。",
"editPricing": "编辑定价",
"viewFullDetails": "查看完整详情",
"themeCoral": "珊瑚色"
"themeCoral": "珊瑚色",
"cliFingerprint": "CLI Fingerprint Matching",
"cliFingerprintDesc": "Match native CLI binary signatures when proxying requests. Reorders headers and body fields to look identical to the official CLI tools. Your proxy IP is preserved.",
"cliFingerprintEnabled": "{count} provider(s) with CLI fingerprint active",
"enableFingerprintTitle": "Enable fingerprint for {provider}",
"disableFingerprintTitle": "Disable fingerprint for {provider}"
},
"translator": {
"title": "翻译者",
@@ -2419,5 +2467,22 @@
"featureWebhooks": "Webhook配置和事件订阅",
"featureSwagger": "OpenAPI / Swagger规范自动生成",
"featureAuth": "每个端点的API密钥和OAuth范围管理"
},
"agents": {
"title": "CLI Agents",
"description": "Discover installed CLI agents on your system. Add custom agents for auto-detection.",
"refresh": "Refresh",
"installed": "Installed",
"notFound": "Not Found",
"builtIn": "Built-in",
"custom": "Custom",
"remove": "Remove",
"addCustomAgent": "Add Custom Agent",
"addCustomAgentDesc": "Register any CLI tool for detection. It will be scanned automatically on refresh.",
"agentName": "Agent Name",
"binaryName": "Binary Name",
"versionCommand": "Version Command",
"spawnArgs": "Spawn Args",
"addAgent": "Add Agent"
}
}
+24
View File
@@ -46,6 +46,30 @@ export async function register() {
startBackgroundRefresh();
console.log("[STARTUP] Quota cache background refresh started");
// Model aliases: restore persisted custom aliases into in-memory state (#316)
// Custom aliases are saved to settings.modelAliases on PUT /api/settings/model-aliases
// but the in-memory _customAliases resets to {} on every restart — load them here.
try {
const { getSettings } = await import("@/lib/db/settings");
const { setCustomAliases } = await import("@omniroute/open-sse/services/modelDeprecation.ts");
const settings = await getSettings();
if (settings.modelAliases) {
const aliases =
typeof settings.modelAliases === "string"
? JSON.parse(settings.modelAliases)
: settings.modelAliases;
if (aliases && typeof aliases === "object") {
setCustomAliases(aliases);
console.log(
`[STARTUP] Restored ${Object.keys(aliases).length} custom model alias(es) from settings`
);
}
}
} catch (err: unknown) {
const msg = err instanceof Error ? err.message : String(err);
console.warn("[STARTUP] Could not restore model aliases:", msg);
}
// Compliance: Initialize audit_log table + cleanup expired logs
try {
const { initAuditLog, cleanupExpiredLogs } = await import("@/lib/compliance/index");
+35
View File
@@ -177,3 +177,38 @@ export async function removeCustomModel(providerId, modelId) {
backupDbFile("pre-write");
return true;
}
export async function updateCustomModel(providerId, modelId, updates = {}) {
const db = getDbInstance();
const row = db
.prepare("SELECT value FROM key_value WHERE namespace = 'customModels' AND key = ?")
.get(providerId);
if (!row) return null;
const value = getKeyValue(row).value;
if (!value) return null;
const models = JSON.parse(value);
const index = models.findIndex((m) => m.id === modelId);
if (index === -1) return null;
const current = models[index];
const next = {
...current,
...(updates.modelName !== undefined ? { name: updates.modelName || current.name } : {}),
...(updates.apiFormat !== undefined ? { apiFormat: updates.apiFormat } : {}),
...(updates.supportedEndpoints !== undefined
? { supportedEndpoints: updates.supportedEndpoints }
: {}),
};
models[index] = next;
db.prepare("UPDATE key_value SET value = ? WHERE namespace = 'customModels' AND key = ?").run(
JSON.stringify(models),
providerId
);
backupDbFile("pre-write");
return next;
}
+6 -8
View File
@@ -126,18 +126,16 @@ export async function createProviderConnection(data: JsonRecord) {
return cleanNulls(merged);
}
// Generate name
// Generate name: prefer explicit name, then email, then a stable short-ID label.
// Avoid sequential "Account N" — it reassigns when accounts are deleted/reordered.
let connectionName = data.name || null;
if (!connectionName && data.authType === "oauth") {
if (data.email) {
connectionName = data.email;
} else {
const count = db
.prepare("SELECT COUNT(*) as cnt FROM provider_connections WHERE provider = ?")
.get(data.provider) as JsonRecord | undefined;
const cntValue = toNumberOrZero(toRecord(count).cnt);
connectionName = `Account ${cntValue + 1}`;
connectionName = data.email as string;
} else if (data.displayName) {
connectionName = data.displayName as string;
}
// Otherwise leave null — UI will fall back to getAccountDisplayName() → "Account #<id>"
}
// Auto-increment priority
+1
View File
@@ -40,6 +40,7 @@ export {
getAllCustomModels,
addCustomModel,
removeCustomModel,
updateCustomModel,
} from "./db/models";
export {
+26 -12
View File
@@ -13,7 +13,14 @@ export const cline = {
},
exchangeToken: async (config, code, redirectUri) => {
try {
// Cline embeds tokens as base64-encoded JSON in the auth code.
// The code may be URL-encoded when pasted from the callback URL.
let base64 = code;
try {
base64 = decodeURIComponent(base64);
} catch {
/* already decoded */
}
const padding = 4 - (base64.length % 4);
if (padding !== 4) {
base64 += "=".repeat(padding);
@@ -62,16 +69,23 @@ export const cline = {
};
}
},
mapTokens: (tokens) => ({
accessToken: tokens.access_token,
refreshToken: tokens.refresh_token,
expiresIn: tokens.expires_at
? Math.floor((new Date(tokens.expires_at).getTime() - Date.now()) / 1000)
: 3600,
email: tokens.email,
providerSpecificData: {
firstName: tokens.firstName,
lastName: tokens.lastName,
},
}),
mapTokens: (tokens) => {
const firstName = tokens.firstName || "";
const lastName = tokens.lastName || "";
const fullName = [firstName, lastName].filter(Boolean).join(" ").trim();
return {
accessToken: tokens.access_token,
refreshToken: tokens.refresh_token,
expiresIn: tokens.expires_at
? Math.floor((new Date(tokens.expires_at).getTime() - Date.now()) / 1000)
: 3600,
// Use full name if available, fallback to email so UI shows a real label
name: fullName || tokens.email || null,
email: tokens.email,
providerSpecificData: {
firstName: tokens.firstName,
lastName: tokens.lastName,
},
};
},
};
+11 -11
View File
@@ -98,12 +98,12 @@ export default function OAuthModal({
GOOGLE_OAUTH_PROVIDERS.has(provider)
) {
setError(
"redirect_uri_mismatch: As credenciais padrão do Google OAuth só funcionam em localhost. " +
"Para uso remoto, configure suas próprias credenciais OAuth nas variáveis de ambiente: " +
"redirect_uri_mismatch: The default Google OAuth credentials only work on localhost. " +
"For remote use, configure your own OAuth credentials via environment variables: " +
(provider === "antigravity"
? "ANTIGRAVITY_OAUTH_CLIENT_ID e ANTIGRAVITY_OAUTH_CLIENT_SECRET"
: "GEMINI_OAUTH_CLIENT_ID e GEMINI_OAUTH_CLIENT_SECRET") +
". Veja o README, seção 'OAuth em Servidor Remoto'."
? "ANTIGRAVITY_OAUTH_CLIENT_ID and ANTIGRAVITY_OAUTH_CLIENT_SECRET"
: "GEMINI_OAUTH_CLIENT_ID and GEMINI_OAUTH_CLIENT_SECRET") +
". See the README section 'OAuth on a Remote Server'."
);
} else {
setError(err.message);
@@ -512,17 +512,17 @@ export default function OAuthModal({
<span className="material-symbols-outlined text-sm align-middle mr-1">
warning
</span>
<strong>Acesso remoto + Google OAuth:</strong> As credenciais padrão aceitam
redirect para <code>localhost</code>. Após autorizar, o browser tentará abrir
<code>localhost</code> copie essa URL completa e cole abaixo. Para uso
totalmente remoto sem esse passo manual,{" "}
<strong>Remote access + Google OAuth:</strong> The default credentials only accept
redirects to <code>localhost</code>. After authorizing, your browser will try to
open <code>localhost</code> copy that full URL and paste it below. For fully
remote use without this manual step,{" "}
<a
href="https://github.com/diegosouzapw/OmniRoute#oauth-em-servidor-remoto"
href="https://github.com/diegosouzapw/OmniRoute#oauth-on-a-remote-server"
target="_blank"
rel="noreferrer"
className="underline"
>
configure suas próprias credenciais OAuth
configure your own OAuth credentials
</a>
.
</div>
+9 -6
View File
@@ -227,7 +227,7 @@ async function handleSingleModelChat(
const resolved = await resolveModelOrError(modelStr, body);
if (resolved.error) return resolved.error;
const { provider, model, sourceFormat, targetFormat } = resolved;
const { provider, model, sourceFormat, targetFormat, extendedContext } = resolved;
// 2. Pipeline gates (availability + circuit breaker)
const gate = checkPipelineGates(provider, model);
@@ -290,6 +290,7 @@ async function handleSingleModelChat(
apiKeyInfo,
userAgent,
comboName,
extendedContext,
});
if (telemetry) telemetry.endPhase();
@@ -366,7 +367,7 @@ async function resolveModelOrError(modelStr: string, body: any) {
return { error: errorResponse(HTTP_STATUS.BAD_REQUEST, "Invalid model format") };
}
const { provider, model } = modelInfo;
const { provider, model, extendedContext } = modelInfo;
const sourceFormat = detectFormat(body);
const providerAlias = PROVIDER_ID_TO_ALIAS[provider] || provider;
@@ -378,13 +379,14 @@ async function resolveModelOrError(modelStr: string, body: any) {
log.info("ROUTING", `Custom model apiFormat=responses → targetFormat=openai-responses`);
}
const ctxTag = extendedContext && providerAlias === "claude" ? " [1m]" : "";
if (modelStr !== `${provider}/${model}`) {
log.info("ROUTING", `${modelStr}${provider}/${model}`);
log.info("ROUTING", `${modelStr}${provider}/${model}${ctxTag}`);
} else {
log.info("ROUTING", `Provider: ${provider}, Model: ${model}`);
log.info("ROUTING", `Provider: ${provider}, Model: ${model}${ctxTag}`);
}
return { provider, model, sourceFormat, targetFormat };
return { provider, model, sourceFormat, targetFormat, extendedContext };
}
/**
@@ -437,6 +439,7 @@ async function executeChatWithBreaker({
apiKeyInfo,
userAgent,
comboName,
extendedContext,
}: any): Promise<{ result: any; tlsFingerprintUsed: boolean }> {
let tlsFingerprintUsed = false;
@@ -445,7 +448,7 @@ async function executeChatWithBreaker({
runWithProxyContext(proxyInfo?.proxy || null, () =>
(handleChatCore as any)({
body: { ...body, model: `${provider}/${model}` },
modelInfo: { provider, model },
modelInfo: { provider, model, extendedContext },
credentials: refreshedCredentials,
log: logger,
clientRawRequest,
+8 -1
View File
@@ -39,6 +39,7 @@ async function lookupCustomModelApiFormat(
*/
export async function getModelInfo(modelStr) {
const parsed = parseModel(modelStr);
const { extendedContext } = parsed;
// Check custom provider nodes first (for both alias and non-alias formats)
if (parsed.providerAlias || parsed.provider) {
@@ -53,7 +54,12 @@ export async function getModelInfo(modelStr) {
matchedOpenAI.id as string,
parsed.model as string
);
return { provider: matchedOpenAI.id, model: parsed.model, ...(apiFormat && { apiFormat }) };
return {
provider: matchedOpenAI.id,
model: parsed.model,
extendedContext,
...(apiFormat && { apiFormat }),
};
}
// Check Anthropic Compatible nodes
@@ -67,6 +73,7 @@ export async function getModelInfo(modelStr) {
return {
provider: matchedAnthropic.id,
model: parsed.model,
extendedContext,
...(apiFormat && { apiFormat }),
};
}
+22
View File
@@ -0,0 +1,22 @@
import { test } from "node:test";
import assert from "node:assert/strict";
import { parseModel } from "../../open-sse/services/model.ts";
// [1m] extended context suffix — PR #311 (DavyMassoneto)
test("[1m] suffix: strips suffix and sets extendedContext=true", () => {
const result = parseModel("claude-sonnet-4-6[1m]");
assert.strictEqual(result.model, "claude-sonnet-4-6");
assert.strictEqual(result.extendedContext, true);
});
test("[1m] suffix: normal model has extendedContext=false", () => {
const result = parseModel("claude-sonnet-4-6");
assert.strictEqual(result.model, "claude-sonnet-4-6");
assert.strictEqual(result.extendedContext, false);
});
test("[1m] suffix: works with provider prefix", () => {
const result = parseModel("claude/claude-sonnet-4-6[1m]");
assert.strictEqual(result.model, "claude-sonnet-4-6");
assert.strictEqual(result.extendedContext, true);
});
+143
View File
@@ -0,0 +1,143 @@
import { describe, it } from "node:test";
import assert from "node:assert/strict";
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import { tmpdir } from "node:os";
import {
detectNativeBinaryTarget,
isNativeBinaryCompatible,
} from "../../scripts/native-binary-compat.mjs";
function makeElfBinary(machine) {
const buffer = Buffer.alloc(64);
buffer[0] = 0x7f;
buffer[1] = 0x45;
buffer[2] = 0x4c;
buffer[3] = 0x46;
buffer[4] = 2;
buffer[5] = 1;
buffer.writeUInt16LE(machine, 18);
return buffer;
}
function makeMachBinary(cpuType) {
const buffer = Buffer.alloc(32);
buffer.writeUInt32BE(0xcffaedfe, 0);
buffer.writeUInt32LE(cpuType, 4);
return buffer;
}
function makePeBinary(machine) {
const buffer = Buffer.alloc(160);
buffer[0] = 0x4d;
buffer[1] = 0x5a;
buffer.writeUInt32LE(0x80, 0x3c);
buffer.write("PE\0\0", 0x80, "ascii");
buffer.writeUInt16LE(machine, 0x84);
return buffer;
}
describe("detectNativeBinaryTarget", () => {
it("detects linux x64 ELF binaries", () => {
assert.deepEqual(detectNativeBinaryTarget(makeElfBinary(62)), {
platform: "linux",
architectures: ["x64"],
});
});
it("detects darwin arm64 Mach-O binaries", () => {
assert.deepEqual(detectNativeBinaryTarget(makeMachBinary(0x0100000c)), {
platform: "darwin",
architectures: ["arm64"],
});
});
it("detects win32 x64 PE binaries", () => {
assert.deepEqual(detectNativeBinaryTarget(makePeBinary(0x8664)), {
platform: "win32",
architectures: ["x64"],
});
});
});
describe("isNativeBinaryCompatible", () => {
function withTempBinary(buffer, callback) {
const dir = mkdtempSync(join(tmpdir(), "omniroute-native-"));
const file = join(dir, "better_sqlite3.node");
writeFileSync(file, buffer);
try {
callback(file);
} finally {
rmSync(dir, { recursive: true, force: true });
}
}
it("accepts linux-x64 binaries when the target matches and dlopen succeeds", () => {
withTempBinary(makeElfBinary(62), (binaryPath) => {
assert.equal(
isNativeBinaryCompatible(binaryPath, {
runtimePlatform: "linux",
runtimeArch: "x64",
dlopen() {},
}),
true
);
});
});
it("rejects linux-x64 binaries when dlopen fails on the same platform", () => {
withTempBinary(makeElfBinary(62), (binaryPath) => {
assert.equal(
isNativeBinaryCompatible(binaryPath, {
runtimePlatform: "linux",
runtimeArch: "x64",
dlopen() {
throw new Error("abi mismatch");
},
}),
false
);
});
});
it("rejects macOS false positives for bundled linux binaries", () => {
withTempBinary(makeElfBinary(62), (binaryPath) => {
assert.equal(
isNativeBinaryCompatible(binaryPath, {
runtimePlatform: "darwin",
runtimeArch: "arm64",
dlopen() {},
}),
false
);
});
});
it("rejects Windows false positives for bundled linux binaries", () => {
withTempBinary(makeElfBinary(62), (binaryPath) => {
assert.equal(
isNativeBinaryCompatible(binaryPath, {
runtimePlatform: "win32",
runtimeArch: "x64",
dlopen() {},
}),
false
);
});
});
it("accepts copied darwin binaries after postinstall replacement", () => {
withTempBinary(makeMachBinary(0x0100000c), (binaryPath) => {
assert.equal(
isNativeBinaryCompatible(binaryPath, {
runtimePlatform: "darwin",
runtimeArch: "arm64",
dlopen() {},
}),
true
);
});
});
});