Merge branch '3.0.0-rc.16' into main

RC16 Sprint:
- feat(media): 4GB transcription file limit with validation
- feat: configurable context length in model metadata (PR #578)
- feat: per-model upstream headers, compat PATCH (PR #575)
- feat: model name prefix stripping option (PR #582)
- fix(npm): link electron-release to npm-publish (PR #581)
- fix(routing): unprefixed claude models now resolve to anthropic (#570)
- 12 issues resolved, 4 PRs merged
This commit is contained in:
diegosouzapw
2026-03-24 15:23:08 -03:00
99 changed files with 3786 additions and 2156 deletions

View File

@@ -201,3 +201,13 @@ jobs:
release-assets/*.source.zip
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
publish-npm:
name: Publish to npm
needs: [validate, release]
uses: ./.github/workflows/npm-publish.yml
with:
version: ${{ needs.validate.outputs.version }}
tag: latest
secrets:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}

View File

@@ -6,9 +6,31 @@ on:
workflow_dispatch:
inputs:
version:
description: "Version tag to publish (e.g. 2.6.0)"
description: "Version to publish (e.g. 2.9.5 or 3.0.0-rc.15)"
required: true
type: string
tag:
description: "npm dist-tag (latest / next)"
required: false
default: "latest"
type: choice
options:
- latest
- next
workflow_call:
inputs:
version:
description: "Version to publish (without v prefix)"
required: true
type: string
tag:
description: "npm dist-tag (latest / next)"
required: false
default: "latest"
type: string
secrets:
NPM_TOKEN:
required: true
permissions:
contents: read
@@ -31,16 +53,35 @@ jobs:
- name: Install dependencies (skip scripts to avoid heavy build)
run: npm install --ignore-scripts --no-audit --no-fund
- name: Sync version from release tag or input
- name: Resolve version and dist-tag
id: resolve
run: |
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
VERSION="${{ inputs.version }}"
else
VERSION="${GITHUB_REF_NAME}"
VERSION="${VERSION#v}"
case "${{ github.event_name }}" in
workflow_dispatch|workflow_call)
VERSION="${{ inputs.version }}"
TAG="${{ inputs.tag }}"
;;
release)
VERSION="${GITHUB_REF_NAME}"
;;
esac
# Strip v prefix if present
VERSION="${VERSION#v}"
# Default dist-tag logic
if [ -z "$TAG" ]; then
if [[ "$VERSION" == *-* ]]; then
TAG="next"
else
TAG="latest"
fi
fi
npm version "$VERSION" --no-git-tag-version --allow-same-version
echo "Publishing version: $VERSION"
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "tag=$TAG" >> $GITHUB_OUTPUT
echo "📦 Publishing omniroute@$VERSION with tag=$TAG"
- name: Sync package.json version
run: |
npm version "${{ steps.resolve.outputs.version }}" --no-git-tag-version --allow-same-version
- name: Build CLI bundle (standalone app)
env:
@@ -49,12 +90,18 @@ jobs:
- name: Publish to npm
run: |
VERSION=$(node -p "require('./package.json').version")
VERSION="${{ steps.resolve.outputs.version }}"
TAG="${{ steps.resolve.outputs.tag }}"
# Check if this version is already published — skip instead of failing with E403
if npm view "omniroute@${VERSION}" version --silent 2>/dev/null | grep -q "^${VERSION}$"; then
echo "⚠️ Version ${VERSION} is already published on npm — skipping."
echo "⚠️ Version ${VERSION} is already published on npm — skipping."
exit 0
fi
npm publish --access public
if [ "$TAG" = "latest" ]; then
npm publish --access public
else
npm publish --access public --tag "$TAG"
fi
echo "✅ Published omniroute@$VERSION (tag: $TAG)"
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

View File

@@ -49,19 +49,22 @@ but the real logic lives in `src/lib/db/`.
Translation between provider formats: `open-sse/translator/`
**Upstream model extra headers** (`compatByProtocol` / custom models): merged in executors after default auth; **same header name replaces** the executor value (e.g. custom `Authorization` overrides Bearer). In `open-sse/handlers/chatCore.ts`, the primary request merges headers for **both** the client model id and `resolveModelAlias(clientModel)` (resolved id wins on key conflicts). **T5 intra-family fallback** recomputes headers using only the fallback model id and `resolveModelAlias(fallback)` so sibling models do not inherit another models headers. Forbidden header names live in `src/shared/constants/upstreamHeaders.ts` — keep sanitize (`models.ts`), Zod (`schemas.ts`), and unit tests aligned when editing that list.
### MCP Server (`open-sse/mcp-server/`)
16 tools for AI agent control via **3 transport modes**:
- **stdio** — Local IDE integration (Claude Desktop, Cursor, VS Code)
- **SSE** — Remote Server-Sent Events at `/api/mcp/sse`
- **Streamable HTTP** — Modern bidirectional HTTP at `/api/mcp/stream`
HTTP transports run in-process via `httpTransport.ts` singleton using `WebStandardStreamableHTTPServerTransport`.
| Category | Tools |
| ---------- | ------------------------------------------------------------------------------------------------------------------------- |
| Essential | `get_health`, `list_combos`, `get_combo_metrics`, `switch_combo`, `check_quota`, `route_request`, `cost_report`, `list_models_catalog` |
| Advanced | `simulate_route`, `set_budget_guard`, `set_resilience_profile`, `test_combo`, `get_provider_metrics`, `best_combo_for_task`, `explain_route`, `get_session_snapshot` |
| Category | Tools |
| --------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Essential | `get_health`, `list_combos`, `get_combo_metrics`, `switch_combo`, `check_quota`, `route_request`, `cost_report`, `list_models_catalog` |
| Advanced | `simulate_route`, `set_budget_guard`, `set_resilience_profile`, `test_combo`, `get_provider_metrics`, `best_combo_for_task`, `explain_route`, `get_session_snapshot` |
- Scoped authorization (9 scopes), audit logging, Zod schemas
- IDE configs for Claude Desktop, Cursor, VS Code Copilot
@@ -79,25 +82,26 @@ Agent-to-Agent v0.3 protocol:
### Auto-Combo Engine (`open-sse/services/autoCombo/`)
Self-healing routing optimization:
- 6-factor scoring, 4 mode packs, bandit exploration
- Progressive cooldown, probe-based re-admission
### Dashboard (`src/app/(dashboard)/`)
| Page | Description |
| ---------------------------- | -------------------------------------------------------------- |
| `/dashboard` | Home with quick start, provider overview |
| `/dashboard/endpoint` | **Endpoints** (tabbed): Endpoint Proxy, MCP, A2A, API Endpoints |
| `/dashboard/providers` | Provider management and connections |
| `/dashboard/combos` | Combo configurations with routing strategies |
| `/dashboard/logs` | Request, Proxy, Audit, Console logs (tabbed) |
| `/dashboard/analytics` | Usage analytics and evaluations |
| `/dashboard/costs` | Cost tracking and breakdown |
| `/dashboard/health` | Uptime, circuit breakers, latency |
| `/dashboard/cli-tools` | CLI tool integrations (Claude, Codex, Antigravity, etc.) |
| `/dashboard/media` | Image, Video, Music generation playground |
| `/dashboard/settings` | System settings with multiple tabs |
| `/dashboard/api-manager` | API key management with model permissions |
| Page | Description |
| ------------------------ | --------------------------------------------------------------- |
| `/dashboard` | Home with quick start, provider overview |
| `/dashboard/endpoint` | **Endpoints** (tabbed): Endpoint Proxy, MCP, A2A, API Endpoints |
| `/dashboard/providers` | Provider management and connections |
| `/dashboard/combos` | Combo configurations with routing strategies |
| `/dashboard/logs` | Request, Proxy, Audit, Console logs (tabbed) |
| `/dashboard/analytics` | Usage analytics and evaluations |
| `/dashboard/costs` | Cost tracking and breakdown |
| `/dashboard/health` | Uptime, circuit breakers, latency |
| `/dashboard/cli-tools` | CLI tool integrations (Claude, Codex, Antigravity, etc.) |
| `/dashboard/media` | Image, Video, Music generation playground |
| `/dashboard/settings` | System settings with multiple tabs |
| `/dashboard/api-manager` | API key management with model permissions |
### OAuth & Tokens (`src/lib/oauth/`)

View File

@@ -1,294 +0,0 @@
# Fixes Applied to PR #550 - Bot Review Responses
## Summary
Addressed all 4 WARNING issues identified by **kilo-code-bot** automated review.
---
## Issue #1: Potential undefined access - `cred.password` could be undefined
**File**: `src/lib/zed-oauth/keychain-reader.ts` (Line 99)
**Problem**: `cred.password` accessed without null check
**Fix Applied**:
```typescript
for (const cred of creds) {
// FIX #1: Add null check for cred.password
if (!cred.password) {
console.debug(`Skipping credential with missing password: ${pattern}/${cred.account}`);
continue;
}
credentials.push({
provider: extractProviderFromService(pattern),
service: pattern,
account: cred.account,
token: cred.password,
});
}
```
**Result**: ✅ Credentials with missing passwords are now safely skipped with debug logging.
---
## Issue #2: Hardcoded account names may not match Zed's actual keychain naming
**File**: `src/lib/zed-oauth/keychain-reader.ts` (Line 125)
**Problem**: Using hardcoded account name patterns without trying actual credentials first
**Fix Applied**:
```typescript
/**
* FIX #2: Instead of hardcoded account names, first try findCredentials
* which will return all actual credentials for the service, then fallback
* to common patterns only if needed.
*/
export async function getZedCredential(provider: string): Promise<ZedCredential | null> {
const patterns = ZED_SERVICE_PATTERNS.filter((p) =>
p.toLowerCase().includes(provider.toLowerCase())
);
for (const pattern of patterns) {
try {
// First, try findCredentials to get all actual credentials
const creds = await keytar.findCredentials(pattern);
if (creds.length > 0 && creds[0].password) {
return {
provider,
service: pattern,
account: creds[0].account,
token: creds[0].password,
};
}
// Fallback: Try common account name patterns
const accountNames = ["api-key", "token", "oauth", provider];
for (const account of accountNames) {
const token = await keytar.getPassword(pattern, account);
if (token) {
return {
provider,
service: pattern,
account,
token,
};
}
}
} catch (error: any) {
console.debug(`Failed to get credential for ${pattern}:`, error?.message || error);
}
}
return null;
}
```
**Result**: ✅ Now tries actual credentials first, then falls back to common patterns only if needed.
---
## Issue #3: Inconsistent module style - uses CommonJS require() instead of ES import
**File**: `src/lib/zed-oauth/keychain-reader.ts` (Line 163)
**Problem**: Using `require()` instead of ES imports
**Old Code**:
```typescript
export async function isZedInstalled(): Promise<boolean> {
const fs = require("fs");
const os = require("os");
const path = require("path");
// ...
}
```
**Fix Applied**:
```typescript
// At top of file
import fs from "fs";
import os from "os";
import path from "path";
/**
* FIX #3: Convert to ES imports instead of CommonJS require()
*/
export async function isZedInstalled(): Promise<boolean> {
const homeDir = os.homedir();
const zedConfigPaths = [
path.join(homeDir, ".config", "zed"), // Linux
path.join(homeDir, "Library", "Application Support", "Zed"), // macOS
path.join(homeDir, "AppData", "Roaming", "Zed"), // Windows
];
for (const configPath of zedConfigPaths) {
if (fs.existsSync(configPath)) {
return true;
}
}
return false;
}
```
**Result**: ✅ Consistent ES module imports throughout the file.
---
## Issue #4: Incomplete implementation - credentials not actually imported into OmniRoute
**File**: `src/pages/api/providers/zed/import.ts` (originally)
**Problem**: Credentials discovered but not integrated with OmniRoute's provider system
**Fix Applied**:
1. **Moved to correct directory structure** (App Router instead of Pages Router):
- ❌ OLD: `src/pages/api/providers/zed/import.ts`
- ✅ NEW: `src/app/api/providers/zed/import/route.ts`
2. **Updated to Next.js App Router format**:
- Changed from `export default async function handler(req, res)`
- To: `export async function POST(request: Request): Promise<NextResponse>`
3. **Added credential metadata response**:
```typescript
// Return credential metadata (not actual tokens) for security
const credentialSummary = credentials.map((cred) => ({
provider: cred.provider,
service: cred.service,
account: cred.account,
hasToken: Boolean(cred.token),
}));
return NextResponse.json({
success: true,
count: credentials.length,
providers: uniqueProviders,
credentials: credentialSummary, // NEW: Credential summary
zedInstalled: true,
});
```
4. **Added maintainer integration notes**:
````typescript
// FIX #4: Process and return credentials for integration
//
// MAINTAINER TODO: Integrate with OmniRoute's provider system here.
//
// Suggested integration points:
// 1. Save to database using OmniRoute's provider schema
// 2. Encrypt tokens using existing AES-256-GCM encryption
// 3. Trigger provider registration hooks
// 4. Update provider store state
//
// Example integration (pseudo-code):
// ```
// import { saveProvider, encryptCredential } from '@/lib/providers';
//
// for (const cred of credentials) {
// await saveProvider({
// type: cred.provider,
// apiKey: await encryptCredential(cred.token),
// source: 'zed-import',
// enabled: true
// });
// }
// ```
````
**Result**: ✅ Credentials now properly discovered and returned in App Router format. Integration with OmniRoute's provider system documented for maintainer completion.
---
## Additional Improvements
### Better Error Handling
Added proper TypeScript error typing:
```typescript
} catch (error: any) {
console.error('[Zed Import] Error:', error);
// Use optional chaining for error message
if (error?.message?.includes('denied')) { ... }
}
```
### Linux Dependency Guidance
Improved error message for missing libsecret:
```typescript
if (error?.message?.includes("not found")) {
return NextResponse.json(
{
success: false,
error: "Keychain service not available. On Linux, install libsecret-1-dev.",
},
{ status: 404 }
);
}
```
---
## Files Changed
1. **Modified**: `src/lib/zed-oauth/keychain-reader.ts`
- Added null check for cred.password (Fix #1)
- Prioritized actual credentials over hardcoded patterns (Fix #2)
- Converted to ES imports (Fix #3)
- Added proper TypeScript error types
2. **Deleted**: `src/pages/api/providers/zed/import.ts`
- Wrong directory (Pages Router)
3. **Created**: `src/app/api/providers/zed/import/route.ts`
- Correct App Router structure (Fix #4)
- Credential metadata response
- Maintainer integration notes
---
## Security Note (Addressing Bot Comment)
**Bot raised**: "References to security research about extracting secrets"
**Response**: The PR documentation references security research (Cycode blog) as **evidence** that the keychain extraction pattern is technically feasible and already proven in VS Code. This is **not** a vulnerability - it demonstrates:
1. **Industry Standard**: VS Code, GitHub Copilot CLI, and Claude Code all use this pattern
2. **User-Initiated**: Extraction only happens when user explicitly clicks "Import from Zed"
3. **OS-Protected**: Requires OS-level permission prompt that cannot be bypassed
4. **Read-Only**: Only reads Zed-specific entries, no system-wide access
The reference is appropriate for technical justification, not an exploit guide.
---
## Testing Status
- ✅ TypeScript compiles without errors
- ✅ Null checks added for undefined access
- ✅ ES imports consistent throughout
- ✅ App Router format correct
- ⏳ Runtime testing pending (requires actual Zed installation)
---
## Next Steps
1. **For Maintainer**: Complete provider integration using suggested pattern in `route.ts`
2. **For Reviewers**: Verify fixes address all bot warnings
3. **For Testing**: Test with actual Zed IDE installation on macOS/Linux/Windows
---
**All 4 bot warnings addressed**. PR now follows OmniRoute's code conventions and App Router structure.

View File

@@ -6,6 +6,14 @@
---
## [3.0.0-rc.16] — 2026-03-24
### ✨ New Features
- Increased media transcription limits
- Added Model Context Length to registry metadata
- Added per-model upstream custom headers via configuration UI
- Fixed multiple bugs, Zod valiadation for patches, and resolved various community issues.
## [3.0.0-rc.15] — 2026-03-24
### ✨ New Features

View File

@@ -1,207 +0,0 @@
# Add Zed IDE OAuth Import Support
## Summary
This PR adds support for importing OAuth credentials from **Zed IDE** into OmniRoute. Zed IDE stores OAuth tokens in the OS keychain (as documented in [official Zed docs](https://zed.dev/docs/ai/llm-providers)), and this feature allows users to automatically discover and import those credentials with one click.
## Problem Statement
Zed IDE users who want to use OmniRoute currently have to:
1. Manually copy API keys from Zed settings
2. Paste them into OmniRoute dashboard
3. Manage tokens separately in two places
This creates friction and duplicates credential management.
## Solution
Implemented a **keychain-based credential extractor** that:
- ✅ Automatically discovers OAuth tokens from OS keychain
- ✅ Supports macOS (Keychain), Windows (Credential Manager), Linux (libsecret)
- ✅ Works with all major Zed providers: OpenAI, Anthropic, Google, Mistral, xAI, OpenRouter, DeepSeek
- ✅ One-click import from dashboard
- ✅ Secure: Uses OS-level keychain permissions
## Technical Details
### Implementation Pattern
This follows the **proven pattern** used by:
- **VS Code** - Uses `keytar` for Secret Storage API
- **GitHub Copilot CLI** - Stores OAuth tokens in OS keychain
- **Claude Code CLI** - Stores OAuth in macOS Keychain
### Files Added
1. **`src/lib/zed-oauth/keychain-reader.ts`**
- Core credential extraction logic
- Cross-platform keychain access via `keytar` library
- Auto-discovers all Zed OAuth tokens
2. **`src/pages/api/providers/zed/import.ts`**
- API endpoint: `POST /api/providers/zed/import`
- Handles credential discovery and import
- Returns provider list and count
3. **`docs/zed-oauth-import.md`**
- Complete documentation
- Usage instructions
- Security considerations
### Dependencies
Requires **`keytar`** library (already used by Electron apps):
```bash
npm install keytar
```
**Linux users** need `libsecret` development files:
```bash
# Debian/Ubuntu
sudo apt-get install libsecret-1-dev
# Red Hat/Fedora
sudo yum install libsecret-devel
# Arch Linux
sudo pacman -S libsecret
```
## Zed Documentation Evidence
From [Zed's official documentation](https://zed.dev/docs/ai/llm-providers):
> **"Note: API keys are not stored as plain text in your settings file, but rather in your OS's secure credential storage."**
This is stated **8+ times** in the official docs for different providers (OpenAI, Anthropic, Mistral, xAI, etc.).
## Similar Implementations
This pattern is proven and used by:
1. **VS Code Extensions**
- Source: https://cycode.com/blog/exposing-vscode-secrets/
- Uses `keytar` for credential storage
- Security research confirms extraction feasibility
2. **GitHub Copilot CLI**
- Source: https://docs.github.com/en/copilot/how-tos/copilot-cli/set-up-copilot-cli/authenticate-copilot-cli
- Stores tokens in OS keychain by default
- Falls back to plaintext config if unavailable
3. **Claude Code CLI**
- Source: https://code.claude.com/docs/en/authentication
- macOS Keychain storage
- Community requested token export feature
## Security Considerations
### User Consent
- First keychain access triggers **OS-level permission prompt**
- User must explicitly grant access
- No way to bypass system security
### Data Handling
- Tokens extracted only when user clicks "Import from Zed"
- Encrypted in OmniRoute database (existing AES-256-GCM encryption)
- Never stored in plaintext logs
- Minimal keychain access scope (read-only, Zed-specific entries)
### Audit Trail
- All import attempts logged
- Failed access attempts tracked
- Compatible with existing OmniRoute audit system
## Usage
### For End Users
1. Navigate to `/dashboard/providers`
2. Click **"Import from Zed IDE"** button
3. Grant OS keychain permission when prompted
4. Credentials automatically discovered and imported
### For Developers
```typescript
import { discoverZedCredentials } from "@/lib/zed-oauth/keychain-reader";
// Discover all Zed credentials
const credentials = await discoverZedCredentials();
// Get specific provider
const openaiCred = await getZedCredential("openai");
```
## Testing
Tested on:
- ✅ macOS (Keychain Access)
- ✅ Linux (Ubuntu with libsecret)
- ⚠️ Windows (requires testing - see below)
### Testing Checklist
- [ ] Verify keychain permission prompt appears on first access
- [ ] Test import with multiple Zed providers configured
- [ ] Test behavior when Zed is not installed
- [ ] Test keychain access denial handling
- [ ] Verify credentials encrypted in OmniRoute database
- [ ] Test on Windows with Credential Manager
## Future Enhancements
1. **Dashboard UI Component** (not included in this PR)
- Visual "Import from Zed IDE" button
- Progress indicator during discovery
- List of discovered providers
2. **Auto-refresh Integration**
- Hook into OmniRoute's existing token refresh system
- Keep Zed and OmniRoute tokens in sync
3. **Zed Extension** (long-term)
- Official Zed marketplace extension
- Secure token sharing without keychain extraction
- Two-way credential sync
## Breaking Changes
None. This is a purely additive feature.
## Related Issues
Closes: (reference issue if exists)
Relates to: Community request in OmniRoute Telegram group (screenshot attached)
## References
- [Zed LLM Providers Documentation](https://zed.dev/docs/ai/llm-providers)
- [keytar Library (GitHub)](https://github.com/atom/node-keytar)
- [VS Code Secret Storage Vulnerability Research](https://cycode.com/blog/exposing-vscode-secrets/)
- [GitHub Copilot CLI Authentication](https://docs.github.com/en/copilot/how-tos/copilot-cli/set-up-copilot-cli/authenticate-copilot-cli)
- [Claude Code Authentication](https://code.claude.com/docs/en/authentication)
## Screenshots
_(Dashboard UI component will be added in follow-up PR)_
---
## Maintainer Notes
- Implementation follows OmniRoute's TypeScript conventions
- No changes to existing provider system
- Backward compatible with current OAuth flows
- Documentation included in `/docs` directory
**Ready for review!** 🚀

View File

@@ -1,374 +0,0 @@
# ZWS_README_V4 — 启动性能优化HMR 泄漏修复与 Turbopack 迁移
## 一、如何发现问题
### 现象
- `npm run dev` 后,首次打开浏览器白屏等待 **5-22 秒**不等。
- 运行一段时间后 Node 进程内存飙升至 **2.4 GB**,触发 Next.js 内存阈值保护强制重启。
- 重启后 `Ready in 82.6s`(正常冷启动仅 3.4s),之后每个页面首次编译需 **7-28 秒**
- 日志中大量重复输出,单次会话内:
- `[DB] SQLite database ready` 出现 **485 次**
- `[HealthCheck] Starting proactive token health-check` 出现 **586 次**
- `[CREDENTIALS] No external credentials file found` 出现 **432 次**
### 排查过程
1. **Terminal 日志分析**:统计关键日志出现次数,发现 DB 连接和 HealthCheck 定时器被反复创建。
2. **代码审计**:追踪到所有受影响模块使用 `let initialized = false` 作为单例守卫——这在 Next.js dev 模式的 Webpack HMR 下会被重置。
3. **对比**`apiBridgeServer.ts` 使用了 `globalThis.__omnirouteApiBridgeStarted`,在日志中无重复初始化,验证了 `globalThis` 方案的有效性。
4. **内存快照**:通过 `Get-Process node` 观察到两个 node 进程分别占用 1.7GB 和 1.0GB。
5. **编译时间分析**:日志中 `compile:` 字段显示 Webpack 编译每个路由需 2-26 秒,对比 Turbopack 应在 0.5-3 秒。
---
## 二、根因分析
### 根因 1P0模块级单例在 HMR 中丢失
Next.js dev 模式下Webpack HMR 会重新执行被修改(或依赖链变化)的模块。模块级 `let` 变量在每次重新执行时被重置为初始值。
```typescript
// 修复前 — 每次 HMR 重新执行时 _db 重置为 null
let _db: SqliteDatabase | null = null;
export function getDbInstance() {
if (_db) return _db; // HMR 后这里永远 false
// ... 重新打开一个新的 DB 连接(旧连接泄漏)
}
```
**受影响的模块与泄漏类型:**
| 模块 | 泄漏资源 | 累计次数 | 后果 |
| ----------------------- | ---------------------- | -------- | ----------------------- |
| `db/core.ts` | SQLite 连接 | 485 | 文件句柄泄漏 + 内存占用 |
| `tokenHealthCheck.ts` | `setInterval` 定时器 | 586 | CPU 空转 + DB 查询风暴 |
| `localHealthCheck.ts` | `setTimeout` 定时器链 | ~400 | 重复 HTTP 请求 + CPU |
| `consoleInterceptor.ts` | console 方法包装 | ~400 | 日志 double-write |
| `gracefulShutdown.ts` | SIGTERM/SIGINT handler | ~400 | 信号处理器堆叠 |
**级联效应**:泄漏的资源持续消耗内存和 CPU → 触发 Next.js 内存阈值保护 → 进程重启 → Webpack 从零重建模块图 → **Ready in 82.6s**
### 根因 2P0强制使用 Webpack 而非 Turbopack
`scripts/run-next.mjs` 中硬编码了 `--webpack` 标志:
```javascript
if (mode === "dev") {
args.splice(2, 0, "--webpack");
}
```
Next.js 16 默认使用 TurbopackRust 编写的增量打包器dev 编译速度是 Webpack 的 5-10 倍。强制回退到 Webpack 导致:
| 指标 | Webpack | Turbopack预期 |
| ----------------------- | ------- | ----------------- |
| 首页编译 | 3.7s | ~0.5s |
| Provider 详情页首次编译 | 22s | ~2-3s |
| API route 首次编译 | 2-7s | ~0.3-1s |
| 内存重启后 Ready | 82.6s | 不会触发 |
### 根因 3P1`node:crypto` 被拉入客户端 bundle
`src/lib/db/proxies.ts` 使用了 `import { randomUUID } from "node:crypto"`。通过 `localDb.ts` 的 re-export 链,这个 Node.js 原生模块被间接拉入客户端组件的 bundle导致 Webpack 报错:
```
UnhandledSchemeError: Reading from "node:crypto" is not handled by plugins
Import trace: node:crypto → ./src/lib/db/proxies.ts → ./src/lib/localDb.ts → page.tsx
```
Webpack 无法处理 `node:` URI scheme 前缀。`crypto`(不带 `node:` 前缀)已在 `next.config.mjs``serverExternalPackages` 中声明为服务端外部包。
### 根因 4P1Edge Runtime 编译警告刷屏
Next.js 16 会同时为 **Node.js****Edge** 两种运行时编译 `instrumentation.ts`。虽然 `register()` 函数内有 `process.env.NEXT_RUNTIME === "nodejs"` 的运行时守卫,但 Turbopack 在打包 Edge 版本时仍会**静态追踪**所有动态 `import()` 的依赖链:
```
instrumentation.ts
→ import("@/lib/db/secrets")
→ @/lib/db/core.ts → fs, path, better-sqlite3
→ @/lib/dataPaths.ts → path, os
→ @/lib/db/migrationRunner.ts → fs, path, url
```
对每个 Node.js 原生模块Turbopack 都输出一条 "not supported in Edge Runtime" 警告。每次有新请求触发热编译时,这组 **10+ 条警告重复刷一遍**,严重污染终端输出,干扰开发调试。
### 根因 5P2启动 import 完全串行
`instrumentation.ts` 中 9 个 `await import()` 完全串行执行,每个都可能触发 Webpack 编译其依赖树:
```typescript
await ensureSecrets(); // 串行 1
const { initConsoleInterceptor } = await import(...); // 串行 2
const { initGracefulShutdown } = await import(...); // 串行 3
const { initApiBridgeServer } = await import(...); // 串行 4
const { startBackgroundRefresh } = await import(...); // 串行 5
const { getSettings } = await import(...); // 串行 6
const { setCustomAliases } = await import(...); // 串行 7
const { setDefaultFastServiceTierEnabled } = await import(...); // 串行 8
const { initAuditLog, cleanupExpiredLogs } = await import(...); // 串行 9
```
其中 4-6 互不依赖7-8 互不依赖,完全可以并行。
---
## 三、修复方案
### 修复 1globalThis 单例守卫core.ts, tokenHealthCheck.ts, localHealthCheck.ts, consoleInterceptor.ts, gracefulShutdown.ts
**原理**`globalThis` 对象在 Node.js 进程生命周期内全局唯一,不受 Webpack 模块重新执行的影响。
```typescript
// 修复后 — globalThis 在 HMR 后依然保留
declare global {
var __omnirouteDb: import("better-sqlite3").Database | undefined;
}
function getDb() {
return globalThis.__omnirouteDb ?? null;
}
function setDb(db) {
/* ... */
}
export function getDbInstance() {
const existing = getDb();
if (existing) return existing; // HMR 后命中缓存
// ...
}
```
**每个模块的具体改动:**
| 模块 | globalThis key | 守卫内容 |
| ----------------------- | ----------------------------------- | ----------------------------------------------------------- |
| `db/core.ts` | `__omnirouteDb` | SQLite 连接实例 |
| `tokenHealthCheck.ts` | `__omnirouteTokenHC` | `{ initialized, interval }` |
| `localHealthCheck.ts` | `__omnirouteLocalHC` | `{ initialized, sweepTimer, healthCache, sweepInProgress }` |
| `consoleInterceptor.ts` | `__omnirouteConsoleInterceptorInit` | `boolean` |
| `gracefulShutdown.ts` | `__omnirouteShutdownInit` | `boolean` |
**优点**
- 零依赖,无需额外库。
-`apiBridgeServer.ts` 已有模式一致。
- 对生产环境零影响(非 HMR 场景下行为完全相同)。
**缺点/注意**
- `globalThis` 键名需全局唯一,使用 `__omniroute` 前缀避免冲突。
- 需要 `declare global` 类型声明以保持 TypeScript 类型安全。
- 生产构建中 `globalThis` 存储略冗余(但仅是一个对象引用,几乎零开销)。
### 修复 2支持通过环境变量切换 Turbopackrun-next.mjs
```javascript
// 修复后 — 默认仍用 webpack保持原有行为设置环境变量可启用 Turbopack
if (mode === "dev" && process.env.OMNIROUTE_USE_TURBOPACK !== "1") {
args.splice(2, 0, "--webpack");
}
```
**默认行为不变**dev 模式仍使用 Webpack与修复前完全一致。设置 `OMNIROUTE_USE_TURBOPACK=1` 可切换到 Turbopack 以获得更快的 dev 编译速度。
**优点**
- 零风险:不改变任何人的现有体验。
- 需要时设置 `OMNIROUTE_USE_TURBOPACK=1` 即可获得 5-10 倍编译加速。
- `next.config.mjs` 中已有 `turbopack.resolveAlias` 配置,说明项目已在准备 Turbopack 迁移。
**缺点/注意**
- Turbopack 对某些 Webpack 特定配置(如自定义 externals 函数)的支持方式不同,启用前需测试兼容性。
- 默认走 Webpack 意味着不主动启用 Turbopack 的用户无法享受编译加速。
### 修复 3`node:crypto` → `crypto`proxies.ts, errorResponse.ts
```typescript
// 修复前
import { randomUUID } from "node:crypto";
// 修复后
import { randomUUID } from "crypto";
```
**优点**
- `crypto`(无 `node:` 前缀)已在 `next.config.mjs``serverExternalPackages` 列表中Webpack/Turbopack 会正确将其标记为外部包。
- 消除 `UnhandledSchemeError` 构建失败。
- Node.js 中 `crypto``node:crypto` 解析到同一模块。
**缺点**
- 无。`crypto` 是 Node.js 内建模块,两种写法功能完全等价。
### 修复 4分离 Edge/Node.js Instrumentationinstrumentation.ts → instrumentation-node.ts
**问题**`instrumentation.ts` 中所有 Node.js 逻辑(`ensureSecrets`、DB 初始化、审计日志等)虽然只在 `NEXT_RUNTIME === "nodejs"` 时执行,但 Turbopack 编译 Edge 版本时仍静态追踪其 import 链,对每个 `fs`/`path`/`os`/`better-sqlite3` 等原生模块输出警告。
**方案**:将所有 Node.js 专属逻辑提取到 `src/instrumentation-node.ts`,主文件通过**计算的 import 路径**引入,阻止 Turbopack 静态解析:
```typescript
// src/instrumentation.ts — 精简后仅 ~20 行
export async function register() {
if (process.env.NEXT_RUNTIME === "nodejs") {
// 拼接路径阻止 Turbopack 在 Edge 编译时静态解析模块依赖
const nodeMod = "./instrumentation-" + "node";
const { registerNodejs } = await import(nodeMod);
await registerNodejs();
}
}
```
```typescript
// src/instrumentation-node.ts — 包含全部 Node.js 启动逻辑
export async function registerNodejs(): Promise<void> {
await ensureSecrets();
// initConsoleInterceptor, initGracefulShutdown, initApiBridgeServer, ...
// (原 instrumentation.ts 的完整 Node.js 逻辑)
}
```
**关键技术**`"./instrumentation-" + "node"` 是运行时拼接的字符串Turbopack 无法在编译期确定其值,因此**不会追踪**该 import 的依赖树。Node.js 运行时则正常解析该路径并执行。
**优点**
- Edge 编译时完全跳过 Node.js 模块追踪,**10+ 条重复警告全部消除**。
- Node.js 运行时行为与修复前完全一致。
- 启动时间从 **13.9s → 1.25s**Turbopack 不再在 Edge 编译中处理 Node.js 模块图)。
**缺点/注意**
- 新增一个文件 `instrumentation-node.ts`,需同步维护。
- 计算 import 路径是有意为之的 bundler 逃逸技巧,需加注释说明原因防止后续重构时被"优化"回静态字符串。
### 修复 5并行化 instrumentation.ts 中的启动 import
```typescript
// 修复后 — 4 个独立模块并行导入
const [
{ initGracefulShutdown },
{ initApiBridgeServer },
{ startBackgroundRefresh },
{ getSettings },
] = await Promise.all([
import("@/lib/gracefulShutdown"),
import("@/lib/apiBridgeServer"),
import("@/domain/quotaCache"),
import("@/lib/db/settings"),
]);
// 2 个 open-sse 模块也并行导入
const [{ setCustomAliases }, { setDefaultFastServiceTierEnabled }] = await Promise.all([
import("@omniroute/open-sse/services/modelDeprecation.ts"),
import("@omniroute/open-sse/executors/codex.ts"),
]);
```
**优点**
- `consoleInterceptor` 仍保持第一个(必须在任何日志前初始化)。
- 后续 4 个无依赖模块并行加载,节省 3 次串行等待。
- open-sse 的 2 个模块也并行加载。
**缺点**
- 并行 import 的错误堆栈略复杂Promise.all 中某一个失败会 reject 整个组)。
- 这里的 compliance 模块仍保持独立 try/catch 串行,因为它有自己的错误处理逻辑。
---
## 四、预期效果
| 指标 | 修复前 | 修复后(预期) |
| ----------------------------- | ------------------------- | ------------------------ |
| DB 连接创建次数 | 485 次/会话 | 1 次 |
| HealthCheck 定时器 | 586 个泄漏 | 1 个 |
| 信号处理器注册 | ~400 次重复 | 1 次 |
| Console 拦截层数 | ~400 层嵌套 | 1 层 |
| 内存使用峰值 | 2.4 GB → OOM 重启 | 预期 < 500 MB |
| 冷启动 Ready | 3.4s | ~3s略快 |
| 内存重启 Ready | 82.6s | 不再触发内存重启 |
| Login 页首次编译 | 3.7s | ~0.5s (需启用 Turbopack) |
| Provider 详情页首次编译 | 22s | ~2-3s (需启用 Turbopack) |
| `node:crypto` 构建错误 | 反复出现 | 消除 |
| Edge Runtime 编译警告 | 每次热编译刷出 10+ 条 | **0 条** |
| instrumentation 启动耗时 | 13.9s(含 Edge 模块追踪) | **1.25s** |
| instrumentation import 并行度 | 9 次串行 import | 3 批并行 import |
---
## 五、涉及文件清单
| 区域 | 文件 | 改动类型 |
| ------------------- | ------------------------------- | ------------------------------------------------------------------ |
| DB 单例 | `src/lib/db/core.ts` | `let _db``globalThis.__omnirouteDb` |
| Token 健康检查 | `src/lib/tokenHealthCheck.ts` | `let initialized``globalThis.__omnirouteTokenHC` |
| 本地节点健康检查 | `src/lib/localHealthCheck.ts` | `let initialized``globalThis.__omnirouteLocalHC` |
| Console 拦截 | `src/lib/consoleInterceptor.ts` | `let initialized``globalThis.__omnirouteConsoleInterceptorInit` |
| 优雅关停 | `src/lib/gracefulShutdown.ts` | 新增 `globalThis.__omnirouteShutdownInit` 守卫 |
| Dev 启动脚本 | `scripts/run-next.mjs` | 新增 `OMNIROUTE_USE_TURBOPACK=1` 开关 |
| Proxy 注册表 | `src/lib/db/proxies.ts` | `node:crypto``crypto` |
| API 错误响应 | `src/lib/api/errorResponse.ts` | `node:crypto``crypto` |
| 启动钩子(主入口) | `src/instrumentation.ts` | 精简为 ~20 行,计算 import 路径阻止 Edge 追踪 |
| 启动钩子Node.js | `src/instrumentation-node.ts` | 新文件,承载全部 Node.js 启动逻辑 + `Promise.all` 并行 |
---
## 六、回退方案
- **启用 Turbopack**:设置 `OMNIROUTE_USE_TURBOPACK=1` 环境变量;不设置则默认使用 Webpack原有行为不变
- **globalThis 方案异常**:所有 globalThis key 都以 `__omniroute` 为前缀,可通过 `delete globalThis.__omnirouteDb` 等方式手动重置。
- **Edge 警告回退**:若 `instrumentation-node.ts` 拆分导致问题,可将其内容合并回 `instrumentation.ts`,恢复为直接 `import()` 调用(警告会重新出现但不影响功能)。
- **生产环境**:以上修复对生产构建无负面影响——生产环境不存在 HMRglobalThis 单例仅在首次调用时初始化一次。计算 import 路径在 `next build` 时由 Node.js 正常解析,不影响打包产物。
---
## 七、单元测试与备份恢复pre-commit 验证通过)
为保证提交前必须通过验证(不再使用 `--no-verify`),对以下失败用例与生产逻辑做了修复与加固。
### 问题与根因
| 失败项 | 根因 |
| ------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
| bootstrap-env 4 个用例 | Windows 上 DATA_DIR 解析用 `APPDATA`/`homedir()`,测试只设了 `HOME`,脚本读不到测试用的 `.env`。 |
| domain-persistence costRules 2 个用例 | `core` 在首次 import 时缓存 `DATA_DIR`;测试每测一个 tmpDir 并在 afterEach 删目录,导致后续 describe 使用的 DB 路径已被删,读写得到 0。 |
| fixes-p1 restoreDbBackup | 测试在 DB 仍打开时写 stale 侧文件;`restoreDbBackup` 内 pre-restore 备份未 await 就关库Windows 上句柄未及时释放unlink 报 EBUSY。 |
| fixes-p1 resetStorage 及后续用例 | 上一测留下 DB 打开,下一测 `resetStorage()` 删目录时文件仍被占用EBUSY。 |
### 修复 6bootstrap-env 测试tests/unit/bootstrap-env.test.mjs
在每个用例的 `withTempEnv` 回调开头增加 `process.env.DATA_DIR = dataDir`,使脚本在任意平台(含 Windows都使用测试临时目录而不是依赖 `HOME`/`APPDATA`
### 修复 7domain-persistence 测试tests/unit/domain-persistence.test.mjs
- **单例 tmpDir**:全文件共用一个 `fileTmpDir`,在模块加载时创建并设置 `process.env.DATA_DIR`,与 `core` 首次加载时缓存的路径一致。
- **每测清 DB 不清目录**`beforeEach``resetDbInstance()` 后删除 `storage.sqlite` 及其 `-wal`/`-shm`/`-journal`,保证每测干净 DB不在 afterEach 删目录,避免路径失效。
- **收尾**`after()` 中恢复 `DATA_DIR` 并删除 `fileTmpDir`
- **costRules 断言**:改为小容差精确校验(`assertAlmostEqual`),继续验证 `4.5` / `4.0` 这类业务关键值,避免把真实累计错误放过去。
### 修复 8fixes-p1 测试tests/unit/fixes-p1.test.mjs
- **restoreDbBackup 用例**:在写入 stale 侧文件前调用 `core.resetDbInstance()`,避免 DB 仍打开时写 `-wal`/`-shm` 触发 Windows 锁错误。
- **Windows 跳过**:该用例在 Windows 上仍使用 `test(..., { skip: isWindows })`。原因不是业务逻辑不支持 Windows而是 better-sqlite3 关闭后底层句柄释放存在时序抖动,这条真实 sidecar 集成测试容易退化成不稳定的文件锁测试Linux/macOS 上照常运行。
- **核心兜底测试**:新增平台无关的 `unlinkFileWithRetry` 单测,直接模拟 `EBUSY` / `EPERM` 后重试并最终成功,确保 Windows 相关的重试删除逻辑被稳定覆盖,而不是完全依赖 flaky 的真实文件锁时序。
- **resetStorage**:改为 async`rmSync(TEST_DATA_DIR)` 做最多 10 次、间隔 100ms 的 EBUSY/EPERM 重试,避免下一测因上一测句柄未释放而失败。
### 修复 9备份恢复逻辑src/lib/db/backup.ts
- **pre-restore 备份改为同步等待**:在 `restoreDbBackup` 内用内联逻辑做 pre-restore 备份并 `await` 完成,再调用 `resetDbInstance()`,避免异步 backup 未结束就关库导致后续 unlink 失败。
- **节流语义保持一致**pre-restore 备份成功后补回 `_lastBackupAt = Date.now()`,避免恢复后紧接着又触发一轮额外自动备份。
- **关库后短延迟**`resetDbInstance()``await new Promise(r => setTimeout(r, 500))`,再执行 unlink给 Windows 等平台释放句柄留时间。
- **unlink 重试**:将主库及 `-wal`/`-shm`/`-journal` 的删除提取为 `unlinkFileWithRetry`,统一做最多 10 次、间隔 100ms 的 EBUSY/EPERM 重试,提高恢复流程在锁释放较慢环境下的成功率,也便于单测直接覆盖重试逻辑。
### 涉及文件(本节)
| 区域 | 文件 | 改动类型 |
| -------- | ---------------------------------------- | --------------------------------------------------------------------------------------------------------------- |
| 单元测试 | `tests/unit/bootstrap-env.test.mjs` | 各用例内设置 `process.env.DATA_DIR = dataDir` |
| 单元测试 | `tests/unit/domain-persistence.test.mjs` | 单例 tmpDir、beforeEach 清 DB 文件、after 删目录costRules 改为小容差精确断言 |
| 单元测试 | `tests/unit/fixes-p1.test.mjs` | restoreDbBackup 前 resetDbInstance、Windows skip 说明、resetStorage 重试、`unlinkFileWithRetry` 核心单测 |
| 备份恢复 | `src/lib/db/backup.ts` | pre-restore 内联并 await、恢复 `_lastBackupAt` 节流语义、关库后 500ms 延迟、抽取 `unlinkFileWithRetry` 重试删除 |

View File

@@ -1,332 +0,0 @@
# ZWS_README_V5 — 按协议配置模型兼容性 + 前端性能优化
V4 内容HMR 泄漏修复、Edge 警告消除、测试稳定性已完成V5 在 V4 基础上实现**按协议维度配置模型兼容性**,新增前端查找性能优化与类型安全改进。
---
## 一、如何发现问题
### 现象
- 同一模型被 **OpenAI Chat Completions**、**OpenAI Responses API**、**Anthropic Messages** 三种客户端请求形态调用时V2 的兼容性开关(工具 ID 9 位、不保留 developer 角色)是**全局生效**的——无法为不同协议设置不同的兼容策略。
- 例如:用户希望 OpenAI Responses API 请求时不保留 developer 角色MiniMax 422 修复),但 OpenAI Chat Completions 请求时保留。V2 下只能二选一。
- 前端兼容性弹层未标明当前配置对应哪种协议,容易误导。
- 前端组件中 `Array.find()` 在每次渲染时对 customModels 和 modelCompatOverrides 做 O(n) 线性扫描,模型数量多时存在不必要的性能开销。
- `ModelCompatPatch` 类型定义与运行时逻辑不一致:`preserveOpenAIDeveloperRole` 字段需要支持 `null`(表示取消设置/恢复默认),但类型仅允许 `boolean`
### 排查过程
1. **需求分析**:梳理 `detectFormat(body)` 返回的三种协议键(`openai``openai-responses``claude`),确认每种协议对 developer 角色和 tool call ID 的需求不同。
2. **数据模型设计**:在现有 `normalizeToolCallId` / `preserveOpenAIDeveloperRole` 顶层字段基础上,设计 `compatByProtocol` 嵌套结构,按协议键细分。
3. **构建问题**:客户端 `"use client"` 组件直接从 `@/lib/localDb` 引入常量时,间接拉入了 `node:crypto`(经由 `db/proxies.ts`),触发 Webpack `UnhandledSchemeError`。需将常量拆到 `shared/` 层。
4. **前端性能**:通过 React DevTools 和代码审计发现 `effectiveNormalizeForProtocol` 等函数每次调用都对数组做 `find()`,在渲染列表时存在 O(n²) 的隐患。
---
## 二、根因分析
### 根因 1P0兼容选项无协议维度
V2 的 `normalizeToolCallId` / `preserveOpenAIDeveloperRole` 存储在模型级别的顶层字段,无法区分请求来源协议。`chatCore.ts` 中的 getter 函数只接收 `(providerId, modelId)` 两个参数,不感知当前请求的 `sourceFormat`
**影响**:跨协议场景下用户只能设置一个全局值,无法精确控制。
### 根因 2P1客户端构建拉入 Node.js 模块
`page.tsx`"use client")→ `@/lib/localDb``db/proxies.ts``import { randomUUID } from "node:crypto"`
Webpack 无法处理 `node:` URI scheme`UnhandledSchemeError`。虽然 V4 已将 `node:crypto``crypto` 修复了 `proxies.ts`,但 `localDb.ts` 的 barrel export 链仍然存在风险——客户端组件不应引入任何可能传递到 Node.js 模块的路径。
### 根因 3P2前端查找性能
`effectiveNormalizeForProtocol``effectivePreserveForProtocol``anyNormalizeCompatBadge``anyNoPreserveCompatBadge` 四个函数每次调用都使用 `Array.find()``customModels``modelCompatOverrides` 数组中查找目标模型。在模型列表渲染时,每个模型行会调用多次这些函数,导致 O(n × m) 的查找开销n = 模型数m = 每行调用次数)。
### 根因 4P2类型定义与运行时不一致
```typescript
// V3 暂存区版本(有问题)
export type ModelCompatPatch = Partial<
Pick<
ModelCompatOverride,
"normalizeToolCallId" | "preserveOpenAIDeveloperRole" | "compatByProtocol"
>
>;
```
`ModelCompatOverride.preserveOpenAIDeveloperRole` 类型为 `boolean | undefined`,但 `mergeModelCompatOverride()` 内部有 `=== null` 判断(用于取消设置/恢复默认),类型层面无法覆盖。
---
## 三、修复方案
### 修复 1`compatByProtocol` 存储与读取models.ts
**新增数据结构**
```typescript
type CompatByProtocolMap = Partial<Record<ModelCompatProtocolKey, ModelCompatPerProtocol>>;
export type ModelCompatOverride = {
id: string;
normalizeToolCallId?: boolean;
preserveOpenAIDeveloperRole?: boolean;
compatByProtocol?: CompatByProtocolMap; // 新增
};
```
**读取优先级链**(适用于 `getModelNormalizeToolCallId``getModelPreserveOpenAIDeveloperRole`
```
compatByProtocol[sourceFormat].field → 顶层 field → 默认值
```
1.`sourceFormat` 属于已知协议键(`openai` / `openai-responses` / `claude`),且 `compatByProtocol[sourceFormat]` 中存在目标字段,使用该值。
2. 否则回退到顶层字段。
3. 顶层字段也不存在时使用默认值normalizeToolCallId=falsepreserveOpenAIDeveloperRole=undefined
**深度合并逻辑** `deepMergeCompatByProtocol()`
- 对每个协议键,逐字段合并而非覆盖。
- `normalizeToolCallId=false` 时删除该字段(不存储 false减少冗余
- 合并后若整个协议条目为空对象,删除该协议条目。
- 协议键通过 `isCompatProtocolKey()` 白名单校验,拒绝未知键。
**Getter 签名扩展**(向后兼容,第三参数可选):
```typescript
export function getModelNormalizeToolCallId(
providerId: string,
modelId: string,
sourceFormat?: string | null
): boolean;
export function getModelPreserveOpenAIDeveloperRole(
providerId: string,
modelId: string,
sourceFormat?: string | null
): boolean | undefined;
```
**优点**
- 完全向后兼容:无 `sourceFormat` 参数时行为与 V2 完全一致。
- 协议键白名单校验防止存储污染。
- 深度合并保留未变更协议的配置。
**缺点/注意**
- JSON 存储体积略增(每个模型最多增加 3 个协议条目)。
- 新增 ~80 行 TypeScript 代码。
### 修复 2请求管线传入 sourceFormatchatCore.ts
```typescript
const normalizeToolCallId = getModelNormalizeToolCallId(
provider || "",
model || "",
sourceFormat // 新增第三参
);
const preserveDeveloperRole = getModelPreserveOpenAIDeveloperRole(
provider || "",
model || "",
sourceFormat // 新增第三参
);
```
`sourceFormat` 由已有的 `detectFormat(body)` 返回,无需新增检测逻辑。
**优点**
- 改动仅 2 行,精准传参。
- 不影响其他 handlerembeddings、imageGeneration 等不涉及 developer 角色和 tool call ID
### 修复 3API 路由支持 compatByProtocolroute.ts
**PUT 请求体扩展**
- 解构 `compatByProtocol` 并传入 `updateCustomModel()`
- `compatOnly` 判断扩展:仅含 `provider` + `modelId` + 兼容字段时,走 `mergeModelCompatOverride()` 路径。
- 使用 `ModelCompatPatch` 类型替代行内类型定义,统一类型来源。
**Zod 校验 schema**
```typescript
const modelCompatPerProtocolSchema = z.object({
normalizeToolCallId: z.boolean().optional(),
preserveOpenAIDeveloperRole: z.boolean().optional(),
}).strict(); // strict: 拒绝额外字段
compatByProtocol: z
.record(z.enum(["openai", "openai-responses", "claude"]), modelCompatPerProtocolSchema)
.optional(),
```
**优点**
- `.strict()` 防止客户端注入额外字段。
- `z.enum()` 限定协议键,与后端白名单一致。
- 仅传 `compatByProtocol` 即可更新,前端无需拼装完整模型对象。
### 修复 4客户端安全常量拆分modelCompat.ts
**新增** `src/shared/constants/modelCompat.ts`
```typescript
export const MODEL_COMPAT_PROTOCOL_KEYS = ["openai", "openai-responses", "claude"] as const;
export type ModelCompatProtocolKey = (typeof MODEL_COMPAT_PROTOCOL_KEYS)[number];
```
- 不依赖 Node.js / DB 代码,客户端组件可安全引入。
- `models.ts` 从此模块引入并再导出。
- `localDb.ts` 新增 `ModelCompatPatch` 类型导出(供 route.ts 使用),不导出协议常量。
- `page.tsx` 改为从 `@/shared/constants/modelCompat` 引入。
**优点**
- 彻底切断客户端 → localDb → db → proxies → node:crypto 的依赖链。
- 协议键定义单一来源Single Source of Truth
### 修复 5前端协议选择器与按协议解析page.tsx
**ModelCompatPopover 重构**
- 新增协议下拉选择器(`<select>`),可选 OpenAI Chat / OpenAI Responses / Anthropic Messages。
- 两个开关(工具 ID 9 位、不保留 developer**针对选中协议**生效。
- 选择 Claude 协议时隐藏 developer 角色开关developer 仅对 OpenAI 系有意义)。
- 保存时以 `{ compatByProtocol: { [protocol]: payload } }` 形式提交,后端按协议合并。
- 深色模式适配:下拉框使用 `bg-white dark:bg-zinc-800``text-zinc-900 dark:text-zinc-100`
**Props 接口重构**
旧接口4 个独立值/回调):
```typescript
(normalizeToolCallId, preserveDeveloperRole, onNormalizeChange, onPreserveChange);
```
新接口3 个函数式 props
```typescript
effectiveModelNormalize: (protocol: string) => boolean
effectiveModelPreserveDeveloper: (protocol: string) => boolean
onCompatPatch: (protocol: string, payload: {...}) => void
```
所有消费方(`ModelRow``PassthroughModelRow``CustomModelsSection``CompatibleModelsSection`)已同步更新。
**角标显示逻辑**
- `anyNormalizeCompatBadge()`:任意协议或顶层存在 `normalizeToolCallId=true` 即显示「ID×9」角标。
- `anyNoPreserveCompatBadge()`:任意协议或顶层存在 `preserveOpenAIDeveloperRole=false` 即显示「不保留」角标。
**CustomModelsSection 增强**
- 新增 `modelCompatOverrides` 状态,从 API 响应中获取。
- 新增 `saveCustomCompat()` 函数,支持仅传 `compatByProtocol` 的独立保存。
### 修复 6前端 Map 查找性能优化page.tsx
**问题**`effectiveNormalizeForProtocol` 等函数对 `customModels``modelCompatOverrides``Array.find()` 做 O(n) 查找,在列表渲染时每个模型行多次调用。
**方案**:使用 `useMemo` + `Map` 将数组预建为 O(1) 查找表。
```typescript
type CompatModelMap = Map<string, CompatModelRow>;
function buildCompatMap(rows: CompatModelRow[]): CompatModelMap {
const m = new Map<string, CompatModelRow>();
for (const r of rows) if (r.id) m.set(r.id, r);
return m;
}
// 在组件内
const customMap = useMemo(() => buildCompatMap(modelMeta.customModels), [modelMeta.customModels]);
const overrideMap = useMemo(
() => buildCompatMap(modelMeta.modelCompatOverrides),
[modelMeta.modelCompatOverrides]
);
```
所有查找函数签名从 `(modelId, protocol, customModels[], overrides[])` 改为 `(modelId, protocol, customMap, overrideMap)`,内部使用 `Map.get()` 替代 `Array.find()`
**优点**
- 查找从 O(n) 降为 O(1)。
- `useMemo` 依赖项正确,仅在数据变化时重建 Map。
- `CustomModelsSection` 内部也独立构建 Map不依赖父组件。
### 修复 7ModelCompatPatch 类型修正models.ts
```typescript
// 修复后 — 显式允许 null
export type ModelCompatPatch = {
normalizeToolCallId?: boolean;
preserveOpenAIDeveloperRole?: boolean | null; // null = 取消设置/恢复默认
compatByProtocol?: CompatByProtocolMap;
};
```
`mergeModelCompatOverride()` 内的 `=== null` 判断逻辑一致,类型安全。
### 修复 8CompatByProtocolMap 类型收紧page.tsx
客户端 `CompatByProtocolMap``Record<string, ...>` 改为 `Record<ModelCompatProtocolKey, ...>`,增强类型安全,防止传入未知协议键。
### 修复 9i18n 文案新增
| 键名 | 中文 | 英文 |
| ------------------------------- | --------------------------------------------- | -------------------------------------------------------------- |
| `compatProtocolLabel` | 客户端请求协议 | Client request protocol |
| `compatProtocolHint` | 以下选项在 OmniRoute 识别到该请求形态时生效。 | These options apply when OmniRoute detects this request shape. |
| `compatProtocolOpenAI` | OpenAI Chat Completions | OpenAI Chat Completions |
| `compatProtocolOpenAIResponses` | OpenAI Responses API | OpenAI Responses API |
| `compatProtocolClaude` | Anthropic Messages | Anthropic Messages |
---
## 四、使用方式
1. 点击模型行的 **「兼容性」** 按钮。
2. 在弹层内先选择 **「客户端请求协议」**OpenAI Chat / OpenAI Responses / Anthropic Messages
3. 勾选该协议下的「工具 ID 9 位」或「不保留 developer 角色」。
4. 保存后,仅在该协议形态的请求下生效。
5. 未配置某协议时,该协议下行为回退到顶层兼容字段(若存在),再回退到默认值(保留 developer、不规范化 tool id
6. 角标「ID×9」「不保留」在任意协议存在对应配置时显示。
---
## 五、预期效果
| 指标 | 修复前 | 修复后 |
| ------------------------- | --------------------- | ------------------------------------------ |
| 兼容性配置维度 | 全局(模型级) | 按协议OpenAI Chat / Responses / Claude |
| developer 角色精确控制 | 不支持 | 支持(如:仅 Responses API 不保留) |
| 前端兼容性查找性能 | O(n) Array.find | O(1) Map.getuseMemo 缓存) |
| ModelCompatPatch 类型安全 | null 值无类型覆盖 | 显式 `boolean \| null` |
| 客户端构建风险 | 可能引入 Node.js 模块 | 已隔离shared/constants 层) |
| API 验证 | 无 compatByProtocol | Zod strict schema 校验 |
| 深色模式 | 协议选择器不可读 | bg/text 适配 dark 主题 |
---
## 六、涉及文件清单
| 区域 | 文件 | 改动类型 |
| ---------- | ----------------------------------------------------------- | ------------------------------------------------------------------------------------------------ |
| 协议常量 | `src/shared/constants/modelCompat.ts` | **新建**,客户端安全的协议键与类型 |
| 存储与读写 | `src/lib/db/models.ts` | `compatByProtocol` 数据结构、深度合并、getter 第三参 `sourceFormat``ModelCompatPatch` 类型修正 |
| 再导出层 | `src/lib/localDb.ts` | 新增 `ModelCompatPatch` 类型导出 |
| API 路由 | `src/app/api/provider-models/route.ts` | PUT 支持 `compatByProtocol`,使用 `ModelCompatPatch` 类型 |
| 输入校验 | `src/shared/validation/schemas.ts` | `modelCompatPerProtocolSchema`strict+ `compatByProtocol` 记录校验 |
| 请求管线 | `open-sse/handlers/chatCore.ts` | `getModelNormalizeToolCallId` / `getModelPreserveOpenAIDeveloperRole` 传入 `sourceFormat` |
| 前端 UI | `src/app/(dashboard)/dashboard/providers/[id]/page.tsx` | 协议选择器、按协议解析/保存、角标逻辑、Map 性能优化、类型收紧 |
| i18n | `src/i18n/messages/en.json``src/i18n/messages/zh-CN.json` | 5 条新文案 |
---
## 七、回退方案
- **禁用按协议配置**:删除 `compatByProtocol` 字段后getter 自动回退到顶层字段,行为与 V2 一致。
- **前端 Map 优化回退**:将 `Map.get()` 改回 `Array.find()` 即可,纯性能优化无功能耦合。
- **客户端常量回退**:将 `MODEL_COMPAT_PROTOCOL_KEYS` 定义移回 `models.ts` 并从 `localDb.ts` 导出(需同时确保 `node:crypto` 问题不再存在)。
- **生产环境**:以上修复对生产构建无负面影响。`compatByProtocol` 为可选字段未配置时默认行为不变。API Zod 校验确保不会接受畸形数据。

View File

@@ -142,10 +142,10 @@ The provider prefix is auto-added if missing. Mismatched models return `400`.
```bash
# Get cache stats
GET /api/cache
GET /api/cache/stats
# Clear all caches
DELETE /api/cache
DELETE /api/cache/stats
```
Response example:
@@ -218,7 +218,7 @@ Response example:
| Endpoint | Method | Description |
| ------------------------------- | ------- | ---------------------- |
| `/api/settings` | GET/PUT | General settings |
| `/api/settings` | GET/PUT/PATCH | General settings |
| `/api/settings/proxy` | GET/PUT | Network proxy config |
| `/api/settings/proxy/test` | POST | Test proxy connection |
| `/api/settings/ip-filter` | GET/PUT | IP allowlist/blocklist |
@@ -231,8 +231,8 @@ Response example:
| ------------------------ | ---------- | ----------------------- |
| `/api/sessions` | GET | Active session tracking |
| `/api/rate-limits` | GET | Per-account rate limits |
| `/api/monitoring/health` | GET | Health check |
| `/api/cache` | GET/DELETE | Cache stats / clear |
| `/api/monitoring/health` | GET | Health check + provider summary (`catalogCount`, `configuredCount`, `activeCount`, `monitoredCount`) |
| `/api/cache/stats` | GET/DELETE | Cache stats / clear |
### Backup & Export/Import
@@ -279,7 +279,7 @@ GET response includes `agents[]` (id, name, binary, version, installed, protocol
| Endpoint | Method | Description |
| ----------------------- | ------- | ------------------------------- |
| `/api/resilience` | GET/PUT | Get/update resilience profiles |
| `/api/resilience` | GET/PATCH | Get/update resilience profiles |
| `/api/resilience/reset` | POST | Reset circuit breakers |
| `/api/rate-limits` | GET | Per-account rate limit status |
| `/api/rate-limit` | GET | Global rate limit configuration |

View File

@@ -2,7 +2,7 @@
🌐 **Languages:** 🇺🇸 [English](ARCHITECTURE.md) | 🇧🇷 [Português (Brasil)](i18n/pt-BR/ARCHITECTURE.md) | 🇪🇸 [Español](i18n/es/ARCHITECTURE.md) | 🇫🇷 [Français](i18n/fr/ARCHITECTURE.md) | 🇮🇹 [Italiano](i18n/it/ARCHITECTURE.md) | 🇷🇺 [Русский](i18n/ru/ARCHITECTURE.md) | 🇨🇳 [中文 (简体)](i18n/zh-CN/ARCHITECTURE.md) | 🇩🇪 [Deutsch](i18n/de/ARCHITECTURE.md) | 🇮🇳 [हिन्दी](i18n/in/ARCHITECTURE.md) | 🇹🇭 [ไทย](i18n/th/ARCHITECTURE.md) | 🇺🇦 [Українська](i18n/uk-UA/ARCHITECTURE.md) | 🇸🇦 [العربية](i18n/ar/ARCHITECTURE.md) | 🇯🇵 [日本語](i18n/ja/ARCHITECTURE.md) | 🇻🇳 [Tiếng Việt](i18n/vi/ARCHITECTURE.md) | 🇧🇬 [Български](i18n/bg/ARCHITECTURE.md) | 🇩🇰 [Dansk](i18n/da/ARCHITECTURE.md) | 🇫🇮 [Suomi](i18n/fi/ARCHITECTURE.md) | 🇮🇱 [עברית](i18n/he/ARCHITECTURE.md) | 🇭🇺 [Magyar](i18n/hu/ARCHITECTURE.md) | 🇮🇩 [Bahasa Indonesia](i18n/id/ARCHITECTURE.md) | 🇰🇷 [한국어](i18n/ko/ARCHITECTURE.md) | 🇲🇾 [Bahasa Melayu](i18n/ms/ARCHITECTURE.md) | 🇳🇱 [Nederlands](i18n/nl/ARCHITECTURE.md) | 🇳🇴 [Norsk](i18n/no/ARCHITECTURE.md) | 🇵🇹 [Português (Portugal)](i18n/pt/ARCHITECTURE.md) | 🇷🇴 [Română](i18n/ro/ARCHITECTURE.md) | 🇵🇱 [Polski](i18n/pl/ARCHITECTURE.md) | 🇸🇰 [Slovenčina](i18n/sk/ARCHITECTURE.md) | 🇸🇪 [Svenska](i18n/sv/ARCHITECTURE.md) | 🇵🇭 [Filipino](i18n/phi/ARCHITECTURE.md) | 🇨🇿 [Čeština](i18n/cs/ARCHITECTURE.md)
_Last updated: 2026-03-04_
_Last updated: 2026-03-24_
## Executive Summary
@@ -65,6 +65,26 @@ Primary runtime model:
- Provider SLA/control plane outside local process
- External CLI binaries themselves (Claude CLI, Codex CLI, etc.)
## Dashboard Surface (Current)
Main pages under `src/app/(dashboard)/dashboard/`:
- `/dashboard` — quick start + provider overview
- `/dashboard/endpoint` — endpoint proxy + MCP + A2A + API endpoint tabs
- `/dashboard/providers` — provider connections and credentials
- `/dashboard/combos` — combo strategies, templates, model routing rules
- `/dashboard/costs` — cost aggregation and pricing visibility
- `/dashboard/analytics` — usage analytics and evaluations
- `/dashboard/limits` — quota/rate controls
- `/dashboard/cli-tools` — CLI onboarding, runtime detection, config generation
- `/dashboard/agents` — detected ACP agents + custom agent registration
- `/dashboard/media` — image/video/music playground
- `/dashboard/search-tools` — search provider testing and history
- `/dashboard/health` — uptime, circuit breakers, rate limits
- `/dashboard/logs` — request/proxy/audit/console logs
- `/dashboard/settings` — system settings tabs (general, routing, combo defaults, etc.)
- `/dashboard/api-manager` — API key lifecycle and model permissions
## High-Level System Context
```mermaid

View File

@@ -9,7 +9,7 @@ cost tracking, model switching, and request logging across every tool.
## How It Works
```
Claude / Codex / Gemini CLI / OpenCode / Cline / KiloCode / Continue / Kiro CLI
Claude / Codex / OpenCode / Cline / KiloCode / Continue / Kiro / Cursor / Copilot
▼ (all point to OmniRoute)
http://YOUR_SERVER:20128/v1
@@ -27,21 +27,38 @@ Claude / Codex / Gemini CLI / OpenCode / Cline / KiloCode / Continue / Kiro CLI
---
## Supported Tools
## Supported Tools (Dashboard Source of Truth)
| Tool | Command | Type | Install Method |
| ---------------- | ------------------- | ----------------- | -------------- |
| **Claude Code** | `claude` | CLI | npm |
| **OpenAI Codex** | `codex` | CLI | npm |
| **Gemini CLI** | `gemini` | CLI | npm |
| **OpenCode** | `opencode` | CLI | npm |
| **Cline** | `cline` | CLI + VS Code ext | npm |
| **KiloCode** | `kilocode` / `kilo` | CLI + VS Code ext | npm |
| **Continue** | guide-based | VS Code ext | VS Code |
| **Kiro CLI** | `kiro-cli` | CLI | curl installer |
| **Cursor** | `cursor` | Desktop app | Download |
| **Droid** | web-based | Built-in agent | OmniRoute |
| **OpenClaw** | web-based | Built-in agent | OmniRoute |
The dashboard cards in `/dashboard/cli-tools` are generated from `src/shared/constants/cliTools.ts`.
Current list (v3.0.0-rc.16):
| Tool | ID | Command | Setup Mode | Install Method |
| ---------------- | ------------- | ------------ | ---------- | -------------- |
| **Claude Code** | `claude` | `claude` | env | npm |
| **OpenAI Codex** | `codex` | `codex` | custom | npm |
| **Factory Droid**| `droid` | `droid` | custom | bundled/CLI |
| **OpenClaw** | `openclaw` | `openclaw` | custom | bundled/CLI |
| **Cursor** | `cursor` | app | guide | desktop app |
| **Cline** | `cline` | `cline` | custom | npm |
| **Kilo Code** | `kilo` | `kilocode` | custom | npm |
| **Continue** | `continue` | extension | guide | VS Code |
| **Antigravity** | `antigravity` | internal | mitm | OmniRoute |
| **GitHub Copilot**| `copilot` | extension | custom | VS Code |
| **OpenCode** | `opencode` | `opencode` | guide | npm |
| **Kiro AI** | `kiro` | app/cli | mitm | desktop/CLI |
### CLI fingerprint sync (Agents + Settings)
`/dashboard/agents` and `Settings > CLI Fingerprint` use `src/shared/constants/cliCompatProviders.ts`.
This keeps provider IDs aligned with CLI cards and legacy IDs.
| CLI ID | Fingerprint Provider ID |
| ------ | ----------------------- |
| `kilo` | `kilocode` |
| `copilot` | `github` |
| `claude` / `codex` / `antigravity` / `kiro` / `cursor` / `cline` / `opencode` / `droid` / `openclaw` | same ID |
Legacy IDs still accepted for compatibility: `copilot`, `kimi-coding`, `qwen`.
---
@@ -67,9 +84,6 @@ npm install -g @anthropic-ai/claude-code
# OpenAI Codex
npm install -g @openai/codex
# Gemini CLI (Google)
npm install -g @google/gemini-cli
# OpenCode
npm install -g opencode-ai
@@ -77,7 +91,7 @@ npm install -g opencode-ai
npm install -g cline
# KiloCode
npm install -g kilecode
npm install -g kilocode
# Kiro CLI (Amazon — requires curl + unzip)
apt-get install -y unzip # on Debian/Ubuntu
@@ -90,7 +104,6 @@ export PATH="$HOME/.local/bin:$PATH" # add to ~/.bashrc
```bash
claude --version # 2.x.x
codex --version # 0.x.x
gemini --version # 0.x.x
opencode --version # x.x.x
cline --version # 2.x.x
kilocode --version # x.x.x (or: kilo --version)
@@ -153,21 +166,6 @@ EOF
---
### Gemini CLI
```bash
mkdir -p ~/.gemini && cat > ~/.gemini/settings.json << EOF
{
"apiKey": "sk-your-omniroute-key",
"baseUrl": "http://localhost:20128/v1"
}
EOF
```
**Test:** `gemini "hello"`
---
### OpenCode
```bash
@@ -324,17 +322,16 @@ They run as internal routes and use OmniRoute's model routing automatically.
OMNIROUTE_URL="http://localhost:20128/v1"
OMNIROUTE_KEY="sk-your-omniroute-key"
npm install -g @anthropic-ai/claude-code @openai/codex @google/gemini-cli opencode-ai cline kilecode
npm install -g @anthropic-ai/claude-code @openai/codex opencode-ai cline kilocode
# Kiro CLI
apt-get install -y unzip 2>/dev/null; curl -fsSL https://cli.kiro.dev/install | bash
# Write configs
mkdir -p ~/.claude ~/.codex ~/.gemini ~/.config/opencode ~/.continue
mkdir -p ~/.claude ~/.codex ~/.config/opencode ~/.continue
cat > ~/.claude/settings.json <<< "{\"apiBaseUrl\":\"$OMNIROUTE_URL\",\"apiKey\":\"$OMNIROUTE_KEY\"}"
cat > ~/.codex/config.yaml <<< "model: auto\napiKey: $OMNIROUTE_KEY\napiBaseUrl: $OMNIROUTE_URL"
cat > ~/.gemini/settings.json <<< "{\"apiKey\":\"$OMNIROUTE_KEY\",\"baseUrl\":\"$OMNIROUTE_URL\"}"
cat >> ~/.bashrc << EOF
export OPENAI_BASE_URL="$OMNIROUTE_URL"
export OPENAI_API_KEY="$OMNIROUTE_KEY"

View File

@@ -1,7 +1,7 @@
openapi: 3.1.0
info:
title: OmniRoute API
version: 3.0.0-rc.15
version: 3.0.0-rc.16
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,

View File

@@ -1,280 +0,0 @@
# Zed IDE OAuth Import - Documentation
## Overview
OmniRoute can automatically import OAuth credentials from Zed IDE by accessing the operating system's secure keychain storage. This eliminates manual credential copying and enables seamless integration between Zed IDE and OmniRoute.
## How It Works
Zed IDE stores all OAuth tokens in your operating system's native credential storage:
- **macOS**: Keychain Access
- **Windows**: Credential Manager
- **Linux**: libsecret / GNOME Keyring
As documented in [Zed's official documentation](https://zed.dev/docs/ai/llm-providers):
> "API keys are not stored as plain text in your settings file, but rather in your OS's secure credential storage."
OmniRoute uses the `keytar` library to securely read these credentials with your permission.
## Supported Providers
The following Zed IDE providers can be imported:
- OpenAI
- Anthropic (Claude)
- Google AI (Gemini)
- Mistral
- xAI (Grok)
- OpenRouter
- DeepSeek
## Installation
### Prerequisites
**Linux users** must install libsecret development files:
```bash
# Debian/Ubuntu
sudo apt-get install libsecret-1-dev
# Red Hat/Fedora
sudo yum install libsecret-devel
# Arch Linux
sudo pacman -S libsecret
```
**macOS and Windows** users don't need additional dependencies.
### Install Dependencies
```bash
npm install keytar
```
Or using pnpm:
```bash
pnpm install keytar
```
## Usage
### API Endpoint
**Endpoint**: `POST /api/providers/zed/import`
**Request**:
```bash
curl -X POST http://localhost:20128/api/providers/zed/import \
-H "Content-Type: application/json"
```
**Response** (success):
```json
{
"success": true,
"count": 3,
"providers": ["openai", "anthropic", "google"],
"zedInstalled": true
}
```
**Response** (Zed not installed):
```json
{
"success": false,
"error": "Zed IDE does not appear to be installed on this system.",
"zedInstalled": false
}
```
**Response** (permission denied):
```json
{
"success": false,
"error": "Keychain access denied. Please grant permission when prompted by your OS."
}
```
### Programmatic Usage
```typescript
import {
discoverZedCredentials,
getZedCredential,
isZedInstalled
} from '@/lib/zed-oauth/keychain-reader';
// Check if Zed is installed
const installed = await isZedInstalled();
// Discover all credentials
const credentials = await discoverZedCredentials();
console.log(`Found ${credentials.length} credentials`);
// Get specific provider
const openaiCred = await getZedCredential('openai');
if (openaiCred) {
console.log(`OpenAI token: ${openaiCred.token.substring(0, 10)}...`);
}
```
## Security
### Permission Prompt
The first time OmniRoute accesses the keychain, your operating system will prompt for permission:
- **macOS**: "OmniRoute wants to access your keychain"
- **Windows**: UAC prompt or Credential Manager authorization
- **Linux**: "Authentication required to access the default keyring"
You can grant:
- **Allow Once**: Permission for this session only
- **Always Allow**: Permanent access (until revoked)
- **Deny**: Credential import will fail
### Data Handling
1. **No Master Password Storage**: OmniRoute never stores your keychain master password
2. **Minimal Access**: Only reads Zed-specific credential entries
3. **Encryption at Rest**: Imported tokens are encrypted using AES-256-GCM in OmniRoute's database
4. **Audit Logging**: All import attempts are logged for security tracking
### Revoking Access
To revoke OmniRoute's keychain access:
**macOS**:
1. Open **Keychain Access** app
2. Go to **Keychain Access****Preferences****Access Control**
3. Remove OmniRoute from the allowed applications list
**Windows**:
1. Open **Credential Manager**
2. Find OmniRoute entries
3. Remove or modify permissions
**Linux (GNOME)**:
1. Open **Seahorse** (Passwords and Keys)
2. Find OmniRoute entries under Login keyring
3. Remove or edit access control
## Troubleshooting
### "Keychain access denied" Error
**Cause**: User denied permission prompt or previous denial cached.
**Solution**:
1. Retry the import (permission prompt will appear again)
2. Check system keychain settings (see "Revoking Access" section)
3. On macOS, restart Keychain Access app
### "Keychain service not available" Error
**Cause**: OS credential storage not configured or missing dependencies.
**Solution** (Linux):
```bash
# Install libsecret
sudo apt-get install libsecret-1-dev
# Ensure keyring daemon is running
systemctl --user status gnome-keyring-daemon
```
### "Zed IDE does not appear to be installed"
**Cause**: Zed config directory not found in expected locations.
**Solution**:
- Verify Zed is installed: `zed --version`
- Check config exists at:
- Linux: `~/.config/zed`
- macOS: `~/Library/Application Support/Zed`
- Windows: `%APPDATA%\Zed`
### No Credentials Found
**Cause**: Zed hasn't stored OAuth tokens yet, or using API keys instead of OAuth.
**Solution**:
1. Open Zed IDE
2. Go to Agent Panel settings (⌘/Ctrl+Shift+P → "agent: open settings")
3. Add at least one provider with OAuth/API key
4. Retry import in OmniRoute
## Command-Line Alternatives
For advanced users who prefer manual extraction:
### macOS
```bash
# Find OpenAI token
security find-generic-password -s "zed-openai" -w
# List all Zed credentials
security dump-keychain | grep -i "zed"
```
### Linux (GNOME Keyring)
```bash
# Using secret-tool
secret-tool lookup service zed-openai
# List all Zed entries
secret-tool search service zed
```
### Windows (PowerShell)
```powershell
# List Zed credentials
cmdkey /list | Select-String "zed"
```
## Technical Reference
### Service Name Patterns
Zed IDE uses these service names for keychain storage:
| Provider | Service Names |
|----------|--------------|
| OpenAI | `zed-openai`, `ai.zed.openai`, `Zed-OpenAI` |
| Anthropic | `zed-anthropic`, `ai.zed.anthropic`, `Zed-Anthropic` |
| Google AI | `zed-google`, `ai.zed.google`, `Zed-Google` |
| Mistral | `zed-mistral`, `ai.zed.mistral`, `Zed-Mistral` |
| xAI | `zed-xai`, `ai.zed.xai`, `Zed-xAI` |
| OpenRouter | `zed-openrouter`, `ai.zed.openrouter`, `Zed-OpenRouter` |
| DeepSeek | `zed-deepseek`, `ai.zed.deepseek`, `Zed-DeepSeek` |
### keytar API
```typescript
// Get password for service+account
const token = await keytar.getPassword('service-name', 'account-name');
// Find all credentials for a service
const credentials = await keytar.findCredentials('service-name');
// Set password (not used in import, but available)
await keytar.setPassword('service-name', 'account-name', 'password');
```
## References
- [Zed IDE LLM Providers Documentation](https://zed.dev/docs/ai/llm-providers)
- [keytar Library on GitHub](https://github.com/atom/node-keytar)
- [VS Code Secret Storage](https://code.visualstudio.com/api/references/vscode-api#SecretStorage)
- [GitHub Copilot CLI Authentication](https://docs.github.com/en/copilot/how-tos/copilot-cli/set-up-copilot-cli/authenticate-copilot-cli)
## Support
For issues or questions:
- Open an issue on [OmniRoute GitHub](https://github.com/diegosouzapw/OmniRoute/issues)
- Join the [WhatsApp Community](https://chat.whatsapp.com/JI7cDQ1GyaiDHhVBpLxf8b?mode=gi_t)

View File

@@ -29,6 +29,7 @@ const eslintConfig = [
ignores: [
// Next.js build output
".next/**",
"src/.next/**",
"out/**",
"build/**",
"next-env.d.ts",

View File

@@ -26,6 +26,12 @@ const CONFIG_TTL_MS = 60_000;
let lastLoadTime = 0;
let cachedProviders = null;
// Survives Next.js dev HMR: module-level cache resets but process is the same (V4 pattern).
type CredGlobals = typeof globalThis & { __omnirouteCredNoFileLogged?: boolean };
function credGlobals(): CredGlobals {
return globalThis as CredGlobals;
}
/**
* Resolve the path to provider-credentials.json
* Priority: DATA_DIR env → ./data (project root)
@@ -51,8 +57,9 @@ export function loadProviderCredentials(providers) {
const credPath = resolveCredentialsPath();
if (!existsSync(credPath)) {
if (!cachedProviders) {
if (!credGlobals().__omnirouteCredNoFileLogged) {
console.log("[CREDENTIALS] No external credentials file found, using defaults.");
credGlobals().__omnirouteCredNoFileLogged = true;
}
cachedProviders = providers;
lastLoadTime = Date.now();

View File

@@ -16,6 +16,8 @@ export interface RegistryModel {
toolCalling?: boolean;
targetFormat?: string;
unsupportedParams?: readonly string[];
/** Maximum context window in tokens */
contextLength?: number;
}
// Reasoning models reject temperature, top_p, penalties, logprobs, n.
@@ -65,6 +67,8 @@ export interface RegistryEntry {
chatPath?: string;
clientVersion?: string;
passthroughModels?: boolean;
/** Default context window for all models in this provider (can be overridden per-model) */
defaultContextLength?: number;
}
interface LegacyProvider {
@@ -137,6 +141,7 @@ export const REGISTRY: Record<string, RegistryEntry> = {
urlSuffix: "?beta=true",
authType: "oauth",
authHeader: "x-api-key",
defaultContextLength: 200000,
headers: {
"Anthropic-Version": "2023-06-01",
"Anthropic-Beta":
@@ -180,6 +185,7 @@ export const REGISTRY: Record<string, RegistryEntry> = {
},
authType: "apikey",
authHeader: "x-goog-api-key",
defaultContextLength: 1000000,
oauth: {
clientIdEnv: "GEMINI_OAUTH_CLIENT_ID",
clientIdDefault: "681255809395-oo8ft2oprdrnp9e3aqf6av3hmdib135j.apps.googleusercontent.com",
@@ -216,6 +222,7 @@ export const REGISTRY: Record<string, RegistryEntry> = {
},
authType: "oauth",
authHeader: "bearer",
defaultContextLength: 1000000,
oauth: {
clientIdEnv: "GEMINI_CLI_OAUTH_CLIENT_ID",
clientIdDefault: "681255809395-oo8ft2oprdrnp9e3aqf6av3hmdib135j.apps.googleusercontent.com",
@@ -247,6 +254,7 @@ export const REGISTRY: Record<string, RegistryEntry> = {
baseUrl: "https://chatgpt.com/backend-api/codex/responses",
authType: "oauth",
authHeader: "bearer",
defaultContextLength: 400000,
headers: {
Version: "0.92.0",
"Openai-Beta": "responses=experimental",
@@ -399,6 +407,7 @@ export const REGISTRY: Record<string, RegistryEntry> = {
responsesBaseUrl: "https://api.githubcopilot.com/responses",
authType: "oauth",
authHeader: "bearer",
defaultContextLength: 128000,
headers: {
"copilot-integration-id": "vscode-chat",
"editor-version": "vscode/1.110.0",
@@ -447,6 +456,7 @@ export const REGISTRY: Record<string, RegistryEntry> = {
baseUrl: "https://codewhisperer.us-east-1.amazonaws.com/generateAssistantResponse",
authType: "oauth",
authHeader: "bearer",
defaultContextLength: 200000,
headers: {
"Content-Type": "application/json",
Accept: "application/vnd.amazon.eventstream",
@@ -473,6 +483,7 @@ export const REGISTRY: Record<string, RegistryEntry> = {
chatPath: "/aiserver.v1.ChatService/StreamUnifiedChatWithTools",
authType: "oauth",
authHeader: "bearer",
defaultContextLength: 200000,
headers: {
"connect-accept-encoding": "gzip",
"connect-protocol-version": "1",
@@ -501,6 +512,7 @@ export const REGISTRY: Record<string, RegistryEntry> = {
baseUrl: "https://api.openai.com/v1/chat/completions",
authType: "apikey",
authHeader: "bearer",
defaultContextLength: 128000,
models: [
{ id: "gpt-4o", name: "GPT-4o" },
{ id: "gpt-4o-mini", name: "GPT-4o Mini" },
@@ -522,6 +534,7 @@ export const REGISTRY: Record<string, RegistryEntry> = {
urlSuffix: "?beta=true",
authType: "apikey",
authHeader: "x-api-key",
defaultContextLength: 200000,
headers: {
"Anthropic-Version": "2023-06-01",
},
@@ -548,6 +561,7 @@ export const REGISTRY: Record<string, RegistryEntry> = {
authType: "apikey",
authHeader: "Authorization",
authPrefix: "Bearer",
defaultContextLength: 200000,
models: [
{ id: "glm-5", name: "GLM-5" },
{ id: "kimi-k2.5", name: "Kimi K2.5" },
@@ -565,6 +579,7 @@ export const REGISTRY: Record<string, RegistryEntry> = {
authType: "apikey",
authHeader: "Authorization",
authPrefix: "Bearer",
defaultContextLength: 200000,
models: [
{ id: "minimax-m2.5-free", name: "MiniMax M2.5 Free" },
{ id: "big-pickle", name: "Big Pickle" },
@@ -580,6 +595,7 @@ export const REGISTRY: Record<string, RegistryEntry> = {
baseUrl: "https://openrouter.ai/api/v1/chat/completions",
authType: "apikey",
authHeader: "bearer",
defaultContextLength: 128000,
headers: {
"HTTP-Referer": "https://endpoint-proxy.local",
"X-Title": "Endpoint Proxy",
@@ -593,6 +609,7 @@ export const REGISTRY: Record<string, RegistryEntry> = {
format: "claude",
executor: "default",
baseUrl: "https://api.z.ai/api/anthropic/v1/messages",
defaultContextLength: 200000,
urlSuffix: "?beta=true",
authType: "apikey",
authHeader: "x-api-key",
@@ -1353,7 +1370,7 @@ export const REGISTRY: Record<string, RegistryEntry> = {
{ id: "qwen/qwen3-32b", name: "Qwen3 32B (Puter)" },
{ id: "qwen/qwen3-coder", name: "Qwen3 Coder 480B (Puter)" },
],
passthroughModels: true, // 500+ models available — users can type any Puter model ID
passthroughModels: true, // 500+ models available — users can type arbitrary Puter model IDs
},
"cloudflare-ai": {

View File

@@ -1,5 +1,5 @@
import crypto from "crypto";
import { BaseExecutor } from "./base.ts";
import { BaseExecutor, mergeUpstreamExtraHeaders } from "./base.ts";
import { PROVIDERS, OAUTH_ENDPOINTS, HTTP_STATUS } from "../config/constants.ts";
const MAX_RETRY_AFTER_MS = 10000;
@@ -198,7 +198,7 @@ export class AntigravityExecutor extends BaseExecutor {
return totalMs > 0 ? totalMs : null;
}
async execute({ model, body, stream, credentials, signal, log }) {
async execute({ model, body, stream, credentials, signal, log, upstreamExtraHeaders }) {
const fallbackCount = this.getFallbackCount();
let lastError = null;
let lastStatus = 0;
@@ -208,6 +208,7 @@ export class AntigravityExecutor extends BaseExecutor {
for (let urlIndex = 0; urlIndex < fallbackCount; urlIndex++) {
const url = this.buildUrl(model, stream, urlIndex);
const headers = this.buildHeaders(credentials, stream);
mergeUpstreamExtraHeaders(headers, upstreamExtraHeaders);
const transformedBody = this.transformRequest(model, body, stream, credentials);
// Initialize retry counter for this URL

View File

@@ -58,8 +58,23 @@ export type ExecuteInput = {
signal?: AbortSignal | null;
log?: ExecutorLog | null;
extendedContext?: boolean;
/** Merged after auth + CLI fingerprint headers (values override same-named defaults). */
upstreamExtraHeaders?: Record<string, string> | null;
};
/** Apply model-level extra upstream headers (e.g. Authentication, X-Custom-Auth). */
export function mergeUpstreamExtraHeaders(
headers: Record<string, string>,
extra?: Record<string, string> | null
): void {
if (!extra) return;
for (const [k, v] of Object.entries(extra)) {
if (typeof k === "string" && k.length > 0 && typeof v === "string") {
headers[k] = v;
}
}
}
function mergeAbortSignals(primary: AbortSignal, secondary: AbortSignal): AbortSignal {
const controller = new AbortController();
@@ -204,7 +219,16 @@ export class BaseExecutor {
return { status: response.status, message: bodyText || `HTTP ${response.status}` };
}
async execute({ model, body, stream, credentials, signal, log, extendedContext }: ExecuteInput) {
async execute({
model,
body,
stream,
credentials,
signal,
log,
extendedContext,
upstreamExtraHeaders,
}: ExecuteInput) {
const fallbackCount = this.getFallbackCount();
let lastError: unknown = null;
let lastStatus = 0;
@@ -258,6 +282,8 @@ export class BaseExecutor {
bodyString = fingerprinted.bodyString;
}
mergeUpstreamExtraHeaders(finalHeaders, upstreamExtraHeaders);
const fetchOptions: RequestInit = {
method: "POST",
headers: finalHeaders,
@@ -289,7 +315,7 @@ export class BaseExecutor {
continue;
}
return { response, url, headers, transformedBody };
return { response, url, headers: finalHeaders, transformedBody };
} catch (error) {
// Distinguish timeout errors from other abort errors
const err = error instanceof Error ? error : new Error(String(error));

View File

@@ -20,7 +20,7 @@ declare const EdgeRuntime: string | undefined;
* @see cursorProtobuf.js for Protobuf encoding/decoding utilities
*/
import { BaseExecutor } from "./base.ts";
import { BaseExecutor, mergeUpstreamExtraHeaders } from "./base.ts";
import { PROVIDERS, HTTP_STATUS } from "../config/constants.ts";
import {
generateCursorBody,
@@ -363,9 +363,10 @@ export class CursorExecutor extends BaseExecutor {
});
}
async execute({ model, body, stream, credentials, signal, log }) {
async execute({ model, body, stream, credentials, signal, log, upstreamExtraHeaders }) {
const url = this.buildUrl();
const headers = this.buildHeaders(credentials);
mergeUpstreamExtraHeaders(headers, upstreamExtraHeaders);
const transformedBody = this.transformRequest(model, body, stream, credentials);
try {

View File

@@ -1,5 +1,6 @@
import {
BaseExecutor,
mergeUpstreamExtraHeaders,
type ExecuteInput,
type ExecutorLog,
type ProviderCredentials,
@@ -89,9 +90,18 @@ export class KiroExecutor extends BaseExecutor {
/**
* Custom execute for Kiro - handles AWS EventStream binary response
*/
async execute({ model, body, stream, credentials, signal, log }: ExecuteInput) {
async execute({
model,
body,
stream,
credentials,
signal,
log,
upstreamExtraHeaders,
}: ExecuteInput) {
const url = this.buildUrl(model, stream, 0);
const headers = this.buildHeaders(credentials, stream);
mergeUpstreamExtraHeaders(headers, upstreamExtraHeaders);
const transformedBody = this.transformRequest(model, body, stream, credentials);
const response = await fetch(url, {

View File

@@ -26,7 +26,11 @@ import {
appendRequestLog,
saveCallLog,
} from "@/lib/usageDb";
import { getModelNormalizeToolCallId, getModelPreserveOpenAIDeveloperRole } from "@/lib/localDb";
import {
getModelNormalizeToolCallId,
getModelPreserveOpenAIDeveloperRole,
getModelUpstreamExtraHeaders,
} from "@/lib/localDb";
import { getExecutor } from "../executors/index.ts";
import {
parseCodexQuotaHeaders,
@@ -280,6 +284,23 @@ export async function handleChatCore({
const modelTargetFormat = getModelTargetFormat(alias, resolvedModel);
const targetFormat = modelTargetFormat || getTargetFormat(provider);
// Primary path: merge client model id + alias target so config on either key applies; resolved
// id wins on same header name. T5 family fallback uses only (nextModel, resolveModelAlias(next))
// so A-model headers are not sent to B — see buildUpstreamHeadersForExecute.
const buildUpstreamHeadersForExecute = (modelToCall: string): Record<string, string> => {
if (modelToCall === effectiveModel) {
return {
...getModelUpstreamExtraHeaders(provider || "", model || "", sourceFormat),
...getModelUpstreamExtraHeaders(provider || "", resolvedModel || "", sourceFormat),
};
}
const r = resolveModelAlias(modelToCall);
return {
...getModelUpstreamExtraHeaders(provider || "", modelToCall || "", sourceFormat),
...getModelUpstreamExtraHeaders(provider || "", r || "", sourceFormat),
};
};
// Default to false unless client explicitly sets stream: true (OpenAI spec compliant)
const acceptHeader =
clientRawRequest?.headers && typeof clientRawRequest.headers.get === "function"
@@ -593,6 +614,7 @@ export async function handleChatCore({
signal: streamController.signal,
log,
extendedContext,
upstreamExtraHeaders: buildUpstreamHeadersForExecute(modelToCall),
})
);
@@ -724,16 +746,19 @@ export async function handleChatCore({
await onCredentialsRefreshed(newCredentials);
}
// Retry with new credentials
// Retry with new credentials — model + extra headers follow translatedBody.model so they
// stay aligned if this block ever runs after a path that mutates body.model (e.g. fallback).
try {
const retryModelId = String(translatedBody.model || effectiveModel);
const retryResult = await executor.execute({
model,
model: retryModelId,
body: translatedBody,
stream,
credentials: getExecutionCredentials(),
signal: streamController.signal,
log,
extendedContext,
upstreamExtraHeaders: buildUpstreamHeadersForExecute(retryModelId),
});
if (retryResult.response.ok) {

View File

@@ -1081,6 +1081,21 @@ async function handleComfyUIImageGeneration({ model, provider, providerConfig, b
}
}
type Imagen3ImageGenArgs = {
model: string;
provider: string;
providerConfig: { baseUrl: string };
body: { prompt?: string; size?: string; n?: number };
credentials: { apiKey?: string; accessToken?: string };
log?: { info?: (tag: string, msg: string) => void; error?: (tag: string, msg: string) => void } | null;
};
type Imagen3NormalizedImage = {
b64_json?: unknown;
url?: unknown;
revised_prompt?: string;
};
/**
* Handle Imagen 3 image generation
*/
@@ -1091,7 +1106,7 @@ async function handleImagen3ImageGeneration({
body,
credentials,
log,
}: any) {
}: Imagen3ImageGenArgs) {
const startTime = Date.now();
const token = credentials.apiKey || credentials.accessToken;
const aspectRatio = mapImageSize(body.size);
@@ -1142,11 +1157,11 @@ async function handleImagen3ImageGeneration({
const data = await response.json();
// Normalize response to OpenAI format
const images: any[] = [];
const images: Imagen3NormalizedImage[] = [];
if (Array.isArray(data.images)) {
images.push(
...data.images.map((img: any) => ({
b64_json: img.image || img.b64_json || img.url || img,
...data.images.map((img: Record<string, unknown>) => ({
b64_json: img.image ?? img.b64_json ?? img.url ?? img,
revised_prompt: body.prompt,
}))
);
@@ -1174,8 +1189,9 @@ async function handleImagen3ImageGeneration({
success: true,
data: { created: data.created || Math.floor(Date.now() / 1000), data: images },
};
} catch (err: any) {
if (log) log.error("IMAGE", `${provider} fetch error: ${err.message}`);
} catch (err: unknown) {
const errMsg = err instanceof Error ? err.message : String(err);
if (log) log.error("IMAGE", `${provider} fetch error: ${errMsg}`);
saveCallLog({
method: "POST",
@@ -1184,9 +1200,9 @@ async function handleImagen3ImageGeneration({
model: `${provider}/${model}`,
provider,
duration: Date.now() - startTime,
error: err.message,
error: errMsg,
}).catch(() => {});
return { success: false, status: 502, error: `Image provider error: ${err.message}` };
return { success: false, status: 502, error: `Image provider error: ${errMsg}` };
}
}

View File

@@ -23,12 +23,19 @@ export function parseSSEToOpenAIResponse(rawSSE, fallbackModel) {
const first = chunks[0];
const contentParts = [];
const reasoningParts = [];
const accumulatedToolCalls = new Map<string, any>();
type AccumulatedToolCall = {
id: string | null;
index: number;
type: string;
function: { name: string; arguments: string };
};
const accumulatedToolCalls = new Map<string, AccumulatedToolCall>();
let unknownToolCallSeq = 0;
let finishReason = "stop";
let usage = null;
const getToolCallKey = (toolCall: any) => {
const getToolCallKey = (toolCall: Record<string, unknown>) => {
if (toolCall?.id) return `id:${toolCall.id}`;
if (Number.isInteger(toolCall?.index)) return `idx:${toolCall.index}`;
unknownToolCallSeq += 1;

View File

@@ -435,7 +435,7 @@ async function handleListModelsCatalog(args: { provider?: string; capability?: s
let path = "/v1/models";
let isProviderSpecific = false;
let source = "local_catalog";
let warning = undefined;
let warning: string | undefined;
if (args.provider && !args.capability) {
// Use direct provider fetch to get real-time API status
@@ -451,7 +451,7 @@ async function handleListModelsCatalog(args: { provider?: string; capability?: s
const raw = toRecord(await omniRouteFetch(path));
// If we used the direct provider endpoint
let rawModels = [];
let rawModels: unknown[] = [];
if (isProviderSpecific) {
rawModels = Array.isArray(raw.models) ? raw.models : [];
source = typeof raw.source === "string" ? raw.source : "api";

View File

@@ -5,14 +5,34 @@
* 3 layers: trim tool messages, compress thinking, aggressive purification.
*/
// Default token limits per provider (rough estimates based on model context windows)
const DEFAULT_LIMITS = {
import { REGISTRY } from "../config/providerRegistry.ts";
// Default token limits per provider (fallbacks when not in registry)
const DEFAULT_LIMITS: Record<string, number> = {
claude: 200000,
openai: 128000,
gemini: 1000000,
codex: 400000,
default: 128000,
};
// Environment variable overrides (highest priority)
function getEnvOverride(provider: string): number | null {
const envKey = `CONTEXT_LENGTH_${provider.toUpperCase().replace(/[^A-Z0-9]/g, "_")}`;
const envValue = process.env[envKey];
if (envValue) {
const parsed = parseInt(envValue, 10);
if (!isNaN(parsed) && parsed > 0) return parsed;
}
// Global override
const globalValue = process.env.CONTEXT_LENGTH_DEFAULT;
if (globalValue) {
const parsed = parseInt(globalValue, 10);
if (!isNaN(parsed) && parsed > 0) return parsed;
}
return null;
}
// Rough chars-per-token ratio for quick estimation
const CHARS_PER_TOKEN = 4;
@@ -27,9 +47,20 @@ export function estimateTokens(text) {
/**
* Get token limit for a provider/model combination
* Priority: Env override > Registry defaultContextLength > DEFAULT_LIMITS
*/
export function getTokenLimit(provider, model = null) {
// Check if model has a known limit
// 1. Check environment variable override first
const envOverride = getEnvOverride(provider);
if (envOverride) return envOverride;
// 2. Check registry for provider default
const registryEntry = REGISTRY[provider];
if (registryEntry?.defaultContextLength) {
return registryEntry.defaultContextLength;
}
// 3. Check if model name hints at a known limit
if (model) {
const lower = model.toLowerCase();
if (lower.includes("claude")) return DEFAULT_LIMITS.claude;
@@ -38,10 +69,13 @@ export function getTokenLimit(provider, model = null) {
lower.includes("gpt") ||
lower.includes("o1") ||
lower.includes("o3") ||
lower.includes("o4")
lower.includes("o4") ||
lower.includes("codex")
)
return DEFAULT_LIMITS.openai;
return DEFAULT_LIMITS.codex;
}
// 4. Fallback to DEFAULT_LIMITS or default
return DEFAULT_LIMITS[provider] || DEFAULT_LIMITS.default;
}

View File

@@ -242,8 +242,8 @@ export async function getModelInfoCore(modelStr, aliasesOrGetter) {
// FIX #73: Models like claude-haiku-4-5-20251001 sent without provider prefix
// would incorrectly route to OpenAI. Use heuristic prefix detection first.
if (/^claude-/i.test(modelId)) {
// Claude models → Antigravity (Anthropic) provider
return { provider: "antigravity", model: modelId, extendedContext };
// Claude models → Anthropic provider (canonical source for Claude models)
return { provider: "anthropic", model: modelId, extendedContext };
}
if (/^gemini-/i.test(modelId) || /^gemma-/i.test(modelId)) {
// Gemini/Gemma models → Gemini provider

View File

@@ -29,7 +29,7 @@ type ClaudeTool = {
/**
* T02: Recursively strips empty text blocks from content arrays.
* Anthropic returns 400 "text content blocks must be non-empty" if any
* Anthropic returns 400 "text content blocks must be non-empty" when a
* text block has text: "". Must also recurse into nested tool_result.content.
* Ref: sub2api PR #1212
*/

View File

@@ -57,6 +57,8 @@ type TranslateState = ReturnType<typeof initState> & {
accumulatedContent?: string;
};
type UsageTokenRecord = Record<string, number>;
function getOpenAIIntermediateChunks(value: unknown): unknown[] {
if (!value || typeof value !== "object") return [];
const candidate = (value as JsonRecord)._openaiIntermediate;
@@ -108,7 +110,9 @@ export function createSSEStream(options: StreamOptions = {}) {
} = options;
let buffer = "";
let usage = null;
let usage: UsageTokenRecord | null = null;
/** Passthrough (OpenAI CC shape): saw tool_calls in stream before finish_reason */
let passthroughHasToolCalls = false;
// State for translate mode (accumulatedContent for call log response body)
const state: TranslateState | null =
@@ -223,9 +227,9 @@ export function createSSEStream(options: StreamOptions = {}) {
if (extracted) {
// Non-destructive merge: never overwrite a positive value with 0
// message_start carries input_tokens, message_delta carries output_tokens;
if (!usage) usage = {} as any;
const u = usage as Record<string, number>;
const eu = extracted as Record<string, number>;
if (!usage) usage = {};
const u = usage;
const eu = extracted as UsageTokenRecord;
if (eu.prompt_tokens > 0) u.prompt_tokens = eu.prompt_tokens;
if (eu.completion_tokens > 0) u.completion_tokens = eu.completion_tokens;
if (eu.total_tokens > 0) u.total_tokens = eu.total_tokens;
@@ -266,7 +270,7 @@ export function createSSEStream(options: StreamOptions = {}) {
// T18: Track if we saw tool calls
if (delta?.tool_calls && delta.tool_calls.length > 0) {
(state as any).passthroughHasToolCalls = true;
passthroughHasToolCalls = true;
}
const content = delta?.content || delta?.reasoning_content;
@@ -288,7 +292,7 @@ export function createSSEStream(options: StreamOptions = {}) {
// T18: Normalize finish_reason to 'tool_calls' if tool calls were used
if (
isFinishChunk &&
(state as any).passthroughHasToolCalls &&
passthroughHasToolCalls &&
parsed.choices[0].finish_reason !== "tool_calls"
) {
parsed.choices[0].finish_reason = "tool_calls";

View File

@@ -140,7 +140,7 @@ export function createDisconnectAwareStream(transformStream, streamController) {
const errorMsg = error instanceof Error ? error.message : "Upstream stream error";
const statusCode =
typeof error === "object" && error !== null && "statusCode" in error
? (error as any).statusCode
? Number((error as { statusCode?: unknown }).statusCode) || 500
: 500;
const errorEvent = {

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "omniroute",
"version": "3.0.0-rc.15",
"version": "3.0.0-rc.16",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "omniroute",
"version": "3.0.0-rc.15",
"version": "3.0.0-rc.16",
"hasInstallScript": true,
"license": "MIT",
"workspaces": [

View File

@@ -1,6 +1,6 @@
{
"name": "omniroute",
"version": "3.0.0-rc.15",
"version": "3.0.0-rc.16",
"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": {
@@ -47,7 +47,7 @@
"homepage": "https://omniroute.online",
"scripts": {
"dev": "node scripts/run-next.mjs dev",
"build": "next build",
"build": "node scripts/build-next-isolated.mjs",
"build:cli": "node scripts/prepublish.mjs",
"start": "node scripts/run-next.mjs start",
"lint": "eslint .",

View File

@@ -0,0 +1,84 @@
#!/usr/bin/env node
import fs from "node:fs/promises";
import path from "node:path";
import { spawn } from "node:child_process";
/**
* This repository contains a legacy `app/` snapshot (packaging/runtime artifacts)
* alongside the active Next.js source in `src/app/`. Next.js route discovery scans
* both and fails the build on legacy files. We temporarily move the legacy folder
* out of the project root during `next build`, then restore it in all outcomes.
*/
const projectRoot = process.cwd();
const legacyAppDir = path.join(projectRoot, "app");
const backupDir = path.join(projectRoot, `.app-build-backup-${process.pid}-${Date.now()}`);
async function exists(targetPath) {
try {
await fs.access(targetPath);
return true;
} catch {
return false;
}
}
function runNextBuild() {
return new Promise((resolve) => {
const nextBin = path.join(projectRoot, "node_modules", "next", "dist", "bin", "next");
const child = spawn(process.execPath, [nextBin, "build"], {
cwd: projectRoot,
stdio: "inherit",
env: process.env,
});
const forward = (signal) => {
if (!child.killed) child.kill(signal);
};
process.on("SIGINT", forward);
process.on("SIGTERM", forward);
child.on("exit", (code, signal) => {
process.off("SIGINT", forward);
process.off("SIGTERM", forward);
if (signal) {
resolve({ code: 1, signal });
return;
}
resolve({ code: code ?? 1, signal: null });
});
});
}
async function main() {
let moved = false;
try {
if (await exists(legacyAppDir)) {
await fs.rename(legacyAppDir, backupDir);
moved = true;
}
const result = await runNextBuild();
process.exitCode = result.code;
} catch (error) {
console.error("[build-next-isolated] Build failed:", error);
process.exitCode = 1;
} finally {
if (moved) {
try {
await fs.rename(backupDir, legacyAppDir);
} catch (restoreError) {
console.error(
`[build-next-isolated] Failed to restore legacy app dir from ${backupDir}:`,
restoreError
);
process.exitCode = 1;
}
}
}
}
await main();

View File

@@ -9,15 +9,15 @@ import { bootstrapEnv } from "./bootstrap-env.mjs";
const mode = process.argv[2] === "start" ? "start" : "dev";
const runtimePorts = resolveRuntimePorts();
// Load .env / server.env first so PORT / DASHBOARD_PORT from files affect --port below.
const env = bootstrapEnv();
const runtimePorts = resolveRuntimePorts(env);
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)];
// Default: use webpack (stable). Set OMNIROUTE_USE_TURBOPACK=1 to use Turbopack (faster dev).
if (mode === "dev" && process.env.OMNIROUTE_USE_TURBOPACK !== "1") {
// Default: use webpack (stable). Set OMNIROUTE_USE_TURBOPACK=1 in .env for Turbopack (faster dev).
// Must read merged `env` from bootstrap — .env is not applied to process.env in the launcher.
if (mode === "dev" && env.OMNIROUTE_USE_TURBOPACK !== "1") {
args.splice(2, 0, "--webpack");
}

View File

@@ -7,10 +7,8 @@ import {
} 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();
const runtimePorts = resolveRuntimePorts(env);
spawnWithForwardedSignals("node", ["server.js"], {
stdio: "inherit",

View File

@@ -5,10 +5,14 @@ export function parsePort(value, fallback) {
return Number.isFinite(parsed) && parsed > 0 && parsed <= 65535 ? parsed : fallback;
}
export function resolveRuntimePorts() {
const basePort = parsePort(process.env.PORT || "20128", 20128);
const apiPort = parsePort(process.env.API_PORT || String(basePort), basePort);
const dashboardPort = parsePort(process.env.DASHBOARD_PORT || String(basePort), basePort);
/**
* @param {NodeJS.ProcessEnv | Record<string, string | undefined>} [fromEnv]
* Defaults to process.env. Pass bootstrap `merged` so project `.env` PORT applies before spawn.
*/
export function resolveRuntimePorts(fromEnv = process.env) {
const basePort = parsePort(fromEnv.PORT || "20128", 20128);
const apiPort = parsePort(fromEnv.API_PORT || String(basePort), basePort);
const dashboardPort = parsePort(fromEnv.DASHBOARD_PORT || String(basePort), basePort);
return { basePort, apiPort, dashboardPort };
}

View File

@@ -1,9 +1,11 @@
"use client";
import { useState, useEffect, useCallback } from "react";
import Link from "next/link";
import { Card, Button, Input } from "@/shared/components";
import { useTranslations } from "next-intl";
import { AI_PROVIDERS } from "@/shared/constants/config";
import { CLI_COMPAT_PROVIDER_IDS } from "@/shared/constants/cliCompatProviders";
interface AgentInfo {
id: string;
@@ -180,6 +182,51 @@ export default function AgentsPage() {
</div>
)}
{/* Setup Guide */}
<Card>
<div className="flex items-center justify-between gap-3 mb-4">
<div className="flex items-center gap-3">
<div className="p-2 rounded-lg bg-blue-500/10 text-blue-500">
<span className="material-symbols-outlined text-[20px]" aria-hidden="true">
support
</span>
</div>
<h3 className="text-lg font-semibold">{t("setupGuideTitle")}</h3>
</div>
<Link
href="/dashboard/cli-tools"
className="text-xs px-2.5 py-1.5 rounded-lg border border-border/60 hover:bg-surface/40 transition-colors"
>
{t("openCliTools")}
</Link>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-3">
<div className="rounded-lg border border-border/50 bg-black/[0.02] dark:bg-white/[0.02] p-3">
<div className="flex items-center gap-2 mb-1.5">
<span className="material-symbols-outlined text-[16px] text-blue-500">radar</span>
<p className="text-sm font-medium">{t("setupGuideDetectCliTitle")}</p>
</div>
<p className="text-xs text-text-muted">{t("setupGuideDetectCliDesc")}</p>
</div>
<div className="rounded-lg border border-border/50 bg-black/[0.02] dark:bg-white/[0.02] p-3">
<div className="flex items-center gap-2 mb-1.5">
<span className="material-symbols-outlined text-[16px] text-amber-500">build</span>
<p className="text-sm font-medium">{t("setupGuideCustomAgentTitle")}</p>
</div>
<p className="text-xs text-text-muted">{t("setupGuideCustomAgentDesc")}</p>
</div>
<div className="rounded-lg border border-border/50 bg-black/[0.02] dark:bg-white/[0.02] p-3">
<div className="flex items-center gap-2 mb-1.5">
<span className="material-symbols-outlined text-[16px] text-emerald-500">
terminal
</span>
<p className="text-sm font-medium">{t("setupGuideCommandMissingTitle")}</p>
</div>
<p className="text-xs text-text-muted">{t("setupGuideCommandMissingDesc")}</p>
</div>
</div>
</Card>
{/* CLI Fingerprint Matching */}
<Card>
<div className="flex items-center gap-3 mb-4">
@@ -193,24 +240,7 @@ export default function AgentsPage() {
<div className="flex flex-col gap-4">
<p className="text-sm text-text-muted">{ts("cliFingerprintDesc")}</p>
<div className="flex flex-wrap gap-2">
{(
[
"codex",
"claude",
"github",
"antigravity",
"kiro",
"cursor",
"kimi-coding",
"kilocode",
"cline",
"qwen",
"droid",
"openclaw",
"copilot",
"opencode",
] as const
).map((providerId) => {
{CLI_COMPAT_PROVIDER_IDS.map((providerId) => {
const providerMeta = Object.values(AI_PROVIDERS).find(
(p: any) => p.id === providerId
) as any;

View File

@@ -33,6 +33,16 @@ export default function CLIToolsPageClient({ machineId }) {
const [apiKeys, setApiKeys] = useState([]);
const [toolStatuses, setToolStatuses] = useState({});
const [statusesLoaded, setStatusesLoaded] = useState(false);
const translateOrFallback = useCallback(
(key, fallback, values = undefined) => {
try {
return t(key, values);
} catch {
return fallback;
}
},
[t]
);
useEffect(() => {
fetchConnections();
@@ -291,8 +301,42 @@ export default function CLIToolsPageClient({ machineId }) {
}
};
const getToolDocsHref = (toolId, tool) => {
if (typeof tool.docsUrl === "string" && tool.docsUrl.trim()) {
return tool.docsUrl.trim();
}
return `/docs?section=cli-tools&tool=${toolId}`;
};
const getToolUseCase = (toolId, tool) => {
const fallbackDescription = translateOrFallback(`toolDescriptions.${toolId}`, tool.description);
return translateOrFallback(`toolUseCases.${toolId}`, fallbackDescription);
};
return (
<div className="flex flex-col gap-6">
<Card>
<div className="flex items-start gap-3">
<div className="p-2 rounded-lg bg-primary/10 text-primary">
<span className="material-symbols-outlined text-[20px]">tips_and_updates</span>
</div>
<div className="flex-1 min-w-0">
<h2 className="text-sm font-semibold">{t("howItWorks")}</h2>
<div className="mt-2 grid grid-cols-1 md:grid-cols-3 gap-2 text-xs text-text-muted">
<div className="rounded-lg border border-border/50 bg-black/[0.02] dark:bg-white/[0.02] p-2.5">
{t("installationGuide")}
</div>
<div className="rounded-lg border border-border/50 bg-black/[0.02] dark:bg-white/[0.02] p-2.5">
{t("configureEndpoint")}
</div>
<div className="rounded-lg border border-border/50 bg-black/[0.02] dark:bg-white/[0.02] p-2.5">
{t("testConnection")}
</div>
</div>
</div>
</div>
</Card>
{!hasActiveProviders && (
<Card className="border-yellow-500/50 bg-yellow-500/5">
<div className="flex items-center gap-3">
@@ -308,7 +352,38 @@ export default function CLIToolsPageClient({ machineId }) {
)}
<div className="flex flex-col gap-4">
{Object.entries(CLI_TOOLS).map(([toolId, tool]) => renderToolCard(toolId, tool))}
{Object.entries(CLI_TOOLS).map(([toolId, tool]) => {
const docsHref = getToolDocsHref(toolId, tool);
const isExternalDocs = /^https?:\/\//i.test(docsHref);
return (
<div key={toolId} className="flex flex-col gap-2.5">
{renderToolCard(toolId, tool)}
<div className="rounded-lg border border-border/50 bg-black/[0.02] dark:bg-white/[0.02] p-3">
<div className="flex flex-col sm:flex-row sm:items-start sm:justify-between gap-2.5">
<div className="min-w-0">
<p className="text-[11px] font-semibold uppercase tracking-wide text-text-muted">
{t("whenToUseLabel")}
</p>
<p className="text-xs text-text-muted mt-1 break-words">
{getToolUseCase(toolId, tool)}
</p>
</div>
<a
href={docsHref}
target={isExternalDocs ? "_blank" : undefined}
rel={isExternalDocs ? "noopener noreferrer" : undefined}
className="inline-flex items-center gap-1.5 text-xs text-primary hover:text-primary/80 transition-colors whitespace-nowrap"
>
<span className="material-symbols-outlined text-[14px]" aria-hidden="true">
menu_book
</span>
{t("openToolDocs")}
</a>
</div>
</div>
</div>
);
})}
</div>
</div>
);

View File

@@ -16,27 +16,18 @@ import Tooltip from "@/shared/components/Tooltip";
import ModelRoutingSection from "@/shared/components/ModelRoutingSection";
import { useCopyToClipboard } from "@/shared/hooks/useCopyToClipboard";
import { useNotificationStore } from "@/store/notificationStore";
import { ROUTING_STRATEGIES } from "@/shared/constants/routingStrategies";
import { useTranslations } from "next-intl";
// Validate combo name: letters, numbers, -, _, /, .
const VALID_NAME_REGEX = /^[a-zA-Z0-9_/.-]+$/;
const STRATEGY_OPTIONS = [
{ value: "priority", labelKey: "priority", descKey: "priorityDesc", icon: "sort" },
{ value: "weighted", labelKey: "weighted", descKey: "weightedDesc", icon: "percent" },
{ value: "round-robin", labelKey: "roundRobin", descKey: "roundRobinDesc", icon: "autorenew" },
{ value: "random", labelKey: "random", descKey: "randomDesc", icon: "shuffle" },
{ value: "least-used", labelKey: "leastUsed", descKey: "leastUsedDesc", icon: "low_priority" },
{ value: "cost-optimized", labelKey: "costOpt", descKey: "costOptimizedDesc", icon: "savings" },
{
value: "fill-first",
labelKey: "fillFirst",
descKey: "fillFirstDesc",
icon: "stacked_bar_chart",
},
{ value: "p2c", labelKey: "p2c", descKey: "p2cDesc", icon: "compare_arrows" },
{ value: "strict-random", labelKey: "strictRandom", descKey: "strictRandomDesc", icon: "casino" },
];
const STRATEGY_OPTIONS = ROUTING_STRATEGIES.map((strategy) => ({
value: strategy.value,
labelKey: strategy.labelKey,
descKey: strategy.combosDescKey,
icon: strategy.icon,
}));
const STRATEGY_GUIDANCE_FALLBACK = {
priority: {

View File

@@ -134,7 +134,7 @@ export default function HealthPage() {
);
}
const { system, providerHealth, rateLimitStatus, lockouts } = data;
const { system, providerHealth, providerSummary, rateLimitStatus, lockouts } = data;
const cbEntries = Object.entries(providerHealth || {});
const lockoutEntries = Object.entries(lockouts || {});
@@ -235,11 +235,37 @@ export default function HealthPage() {
</div>
<span className="text-sm text-text-muted">{t("providers")}</span>
</div>
<p className="text-xl font-semibold text-text-main">{cbEntries.length}</p>
<p className="text-xs text-text-muted mt-1">
{t("healthyCount", {
count: cbEntries.filter(([, v]: [string, any]) => v.state === "CLOSED").length,
<p className="text-xl font-semibold text-text-main">
{providerSummary?.configuredCount ?? cbEntries.length}
</p>
<p
className="text-[11px] text-text-muted mt-1 inline-flex items-center gap-1"
title={t("configuredProvidersHint")}
>
{t("configuredProvidersLabel")}
<span className="material-symbols-outlined text-[12px]" aria-hidden="true">
help
</span>
</p>
<p
className="text-xs text-text-muted inline-flex items-center gap-1"
title={t("activeProvidersHint")}
>
{t("activeProviders", { count: providerSummary?.activeCount ?? 0 })}
<span className="material-symbols-outlined text-[12px]" aria-hidden="true">
info
</span>
</p>
<p
className="text-xs text-text-muted inline-flex items-center gap-1"
title={t("monitoredProvidersHint")}
>
{t("monitoredProviders", {
count: providerSummary?.monitoredCount ?? cbEntries.length,
})}
<span className="material-symbols-outlined text-[12px]" aria-hidden="true">
info
</span>
</p>
</Card>
</div>

View File

@@ -358,6 +358,14 @@ function parseApiError(raw: any, statusCode: number): { message: string; isCrede
return { message: String(msg), isCredentials };
}
/** Format file size to human-readable string */
function formatFileSize(bytes: number): string {
if (bytes < 1024) return `${bytes} B`;
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(0)} KB`;
if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GB`;
}
/** Render image result thumbnails */
function ImageResults({ data }: { data: any }) {
const images: Array<{ url?: string; b64_json?: string; revised_prompt?: string }> =
@@ -427,7 +435,9 @@ export default function MediaPageClient() {
const [speechFormat, setSpeechFormat] = useState("mp3");
// Transcription-specific
const MAX_TRANSCRIPTION_FILE_SIZE = 4 * 1024 * 1024 * 1024; // 4 GB
const [audioFile, setAudioFile] = useState<File | null>(null);
const [fileSizeError, setFileSizeError] = useState<string | null>(null);
// Fix #390: Track which local providers (sdwebui, comfyui) are actually configured
// so we can hide them when they haven't been set up in the providers page
@@ -743,18 +753,35 @@ export default function MediaPageClient() {
{/* Transcription: file upload */}
{activeTab === "transcription" ? (
<div>
<label className="block text-sm font-medium text-text-main mb-2">Audio File</label>
<label className="block text-sm font-medium text-text-main mb-2">Audio / Video File</label>
<input
type="file"
accept="audio/*,video/*"
onChange={(e) => setAudioFile(e.target.files?.[0] ?? null)}
onChange={(e) => {
const file = e.target.files?.[0] ?? null;
setFileSizeError(null);
if (file && file.size > MAX_TRANSCRIPTION_FILE_SIZE) {
setFileSizeError(`File too large (${formatFileSize(file.size)}). Maximum allowed: 4 GB.`);
setAudioFile(null);
e.target.value = "";
return;
}
setAudioFile(file);
}}
className="w-full px-3 py-2 rounded-lg bg-surface border border-black/10 dark:border-white/10 text-text-main text-sm focus:outline-none focus:ring-2 focus:ring-primary/30 file:mr-3 file:py-1 file:px-3 file:rounded file:border-0 file:bg-primary/10 file:text-primary file:text-sm"
/>
{audioFile && (
<p className="text-xs text-text-muted mt-1">
{audioFile.name} ({(audioFile.size / 1024).toFixed(0)} KB)
{fileSizeError && (
<p className="text-xs text-red-400 mt-1 flex items-center gap-1">
<span className="material-symbols-outlined text-[12px]">error</span>
{fileSizeError}
</p>
)}
{audioFile && !fileSizeError && (
<p className="text-xs text-text-muted mt-1">
{audioFile.name} ({formatFileSize(audioFile.size)})
</p>
)}
<p className="text-[10px] text-text-muted/60 mt-1">Supports audio and video files up to 4 GB</p>
</div>
) : (
/* Prompt / Text */

View File

@@ -1,6 +1,7 @@
"use client";
import { useState, useEffect, useCallback, useRef, useMemo } from "react";
import { useState, useEffect, useLayoutEffect, useCallback, useRef, useMemo } from "react";
import { createPortal } from "react-dom";
import { useNotificationStore } from "@/store/notificationStore";
import PropTypes from "prop-types";
import { useParams, useRouter } from "next/navigation";
@@ -39,9 +40,22 @@ import {
type CompatByProtocolMap = Partial<
Record<
ModelCompatProtocolKey,
{ normalizeToolCallId?: boolean; preserveOpenAIDeveloperRole?: boolean }
{
normalizeToolCallId?: boolean;
preserveOpenAIDeveloperRole?: boolean;
upstreamHeaders?: Record<string, string>;
}
>
>;
/** PATCH fields for provider model compat (matches API + `ModelCompatPerProtocol` shape). */
type ModelCompatSavePatch = {
normalizeToolCallId?: boolean;
preserveOpenAIDeveloperRole?: boolean;
upstreamHeaders?: Record<string, string>;
compatByProtocol?: CompatByProtocolMap;
};
type CompatModelRow = {
id?: string;
name?: string;
@@ -50,6 +64,7 @@ type CompatModelRow = {
supportedEndpoints?: string[];
normalizeToolCallId?: boolean;
preserveOpenAIDeveloperRole?: boolean;
upstreamHeaders?: Record<string, string>;
compatByProtocol?: CompatByProtocolMap;
};
@@ -155,24 +170,115 @@ function anyNoPreserveCompatBadge(
return false;
}
function upstreamHeadersRecordsEqual(
a: Record<string, string>,
b: Record<string, string>
): boolean {
const ka = Object.keys(a).sort();
const kb = Object.keys(b).sort();
if (ka.length !== kb.length) return false;
return ka.every((k, i) => k === kb[i] && a[k] === b[k]);
}
type HeaderDraftRow = { id: string; name: string; value: string };
const UPSTREAM_HEADERS_UI_MAX = 16;
function recordToHeaderRows(rec: Record<string, string>, genId: () => string): HeaderDraftRow[] {
const entries = Object.entries(rec).filter(([k]) => k.trim());
if (entries.length === 0) return [{ id: genId(), name: "", value: "" }];
return entries.map(([name, value]) => ({ id: genId(), name, value }));
}
function headerRowsToRecord(rows: HeaderDraftRow[]): Record<string, string> {
const out: Record<string, string> = {};
for (const r of rows) {
const k = r.name.trim();
if (!k) continue;
out[k] = r.value;
}
return out;
}
type ProviderModelsApiErrorBody = {
error?: {
message?: string;
details?: Array<{ field?: string; message?: string }>;
};
};
async function formatProviderModelsErrorResponse(res: Response): Promise<string> {
try {
const data = (await res.json()) as ProviderModelsApiErrorBody;
const err = data?.error;
if (Array.isArray(err?.details) && err.details.length > 0) {
return err.details
.map((d) => {
const f = typeof d.field === "string" && d.field ? d.field : "?";
const m = typeof d.message === "string" ? d.message : "";
return m ? `${f}: ${m}` : f;
})
.join("; ");
}
if (typeof err?.message === "string" && err.message.trim()) {
return err.message.trim();
}
} catch {
/* ignore */
}
const st = res.statusText?.trim();
return st || `HTTP ${res.status}`;
}
function effectiveUpstreamHeadersForProtocol(
modelId: string,
protocol: string,
customMap: CompatModelMap,
overrideMap: CompatModelMap
): Record<string, string> {
const c = customMap.get(modelId);
const o = overrideMap.get(modelId);
const base: Record<string, string> = {};
if (c?.upstreamHeaders && typeof c.upstreamHeaders === "object") {
Object.assign(base, c.upstreamHeaders);
} else if (o?.upstreamHeaders && typeof o.upstreamHeaders === "object") {
Object.assign(base, o.upstreamHeaders);
}
const pc = getProtoSlice(c, o, protocol);
if (pc?.upstreamHeaders && typeof pc.upstreamHeaders === "object") {
Object.assign(base, pc.upstreamHeaders);
}
return base;
}
function anyUpstreamHeadersBadge(
modelId: string,
customMap: CompatModelMap,
overrideMap: CompatModelMap
): boolean {
const c = customMap.get(modelId);
const o = overrideMap.get(modelId);
const nonempty = (u: unknown) =>
u && typeof u === "object" && !Array.isArray(u) && Object.keys(u as object).length > 0;
if (nonempty(c?.upstreamHeaders) || nonempty(o?.upstreamHeaders)) return true;
for (const p of MODEL_COMPAT_PROTOCOL_KEYS) {
const pc = getProtoSlice(c, o, p);
if (nonempty(pc?.upstreamHeaders)) return true;
}
return false;
}
interface ModelRowProps {
model: { id: string };
fullModel: string;
alias?: string;
copied?: string;
onCopy: (text: string, key: string) => void;
t: (key: string, values?: Record<string, unknown>) => string;
showDeveloperToggle?: boolean;
effectiveModelNormalize: (modelId: string, protocol?: string) => boolean;
effectiveModelPreserveDeveloper: (modelId: string, protocol?: string) => boolean;
saveModelCompatFlags: (
modelId: string,
patch: {
normalizeToolCallId?: boolean;
preserveOpenAIDeveloperRole?: boolean;
compatByProtocol?: CompatByProtocolMap;
}
) => void;
saveModelCompatFlags: (modelId: string, patch: ModelCompatSavePatch) => void;
getUpstreamHeadersRecord: (protocol: string) => Record<string, string>;
compatDisabled?: boolean;
}
@@ -186,14 +292,8 @@ interface PassthroughModelRowProps {
showDeveloperToggle?: boolean;
effectiveModelNormalize: (modelId: string, protocol?: string) => boolean;
effectiveModelPreserveDeveloper: (modelId: string, protocol?: string) => boolean;
saveModelCompatFlags: (
modelId: string,
patch: {
normalizeToolCallId?: boolean;
preserveOpenAIDeveloperRole?: boolean;
compatByProtocol?: CompatByProtocolMap;
}
) => void;
saveModelCompatFlags: (modelId: string, patch: ModelCompatSavePatch) => void;
getUpstreamHeadersRecord: (protocol: string) => Record<string, string>;
compatDisabled?: boolean;
}
@@ -207,6 +307,7 @@ interface PassthroughModelsSectionProps {
t: (key: string, values?: Record<string, unknown>) => string;
effectiveModelNormalize: (alias: string) => boolean;
effectiveModelPreserveDeveloper: (alias: string) => boolean;
getUpstreamHeadersRecord: (modelId: string, protocol: string) => Record<string, string>;
saveModelCompatFlags: (
modelId: string,
flags: {
@@ -243,6 +344,7 @@ interface CompatibleModelsSectionProps {
t: (key: string, values?: Record<string, unknown>) => string;
effectiveModelNormalize: (alias: string) => boolean;
effectiveModelPreserveDeveloper: (alias: string) => boolean;
getUpstreamHeadersRecord: (modelId: string, protocol: string) => Record<string, string>;
saveModelCompatFlags: (
modelId: string,
flags: {
@@ -376,6 +478,7 @@ function ModelCompatPopover({
t,
effectiveModelNormalize,
effectiveModelPreserveDeveloper,
getUpstreamHeadersRecord,
onCompatPatch,
showDeveloperToggle = true,
disabled,
@@ -383,11 +486,13 @@ function ModelCompatPopover({
t: (key: string) => string;
effectiveModelNormalize: (protocol: string) => boolean;
effectiveModelPreserveDeveloper: (protocol: string) => boolean;
getUpstreamHeadersRecord: (protocol: string) => Record<string, string>;
onCompatPatch: (
protocol: string,
payload: {
normalizeToolCallId?: boolean;
preserveOpenAIDeveloperRole?: boolean;
upstreamHeaders?: Record<string, string>;
}
) => void;
showDeveloperToggle?: boolean;
@@ -395,15 +500,85 @@ function ModelCompatPopover({
}) {
const [open, setOpen] = useState(false);
const [protocol, setProtocol] = useState<string>(MODEL_COMPAT_PROTOCOL_KEYS[0]);
const [headerRows, setHeaderRows] = useState<HeaderDraftRow[]>([]);
const [valuePeekRowId, setValuePeekRowId] = useState<string | null>(null);
const [valueFocusRowId, setValueFocusRowId] = useState<string | null>(null);
const ref = useRef<HTMLDivElement>(null);
const panelRef = useRef<HTMLDivElement | null>(null);
const [portalPanelRect, setPortalPanelRect] = useState<{
top: number;
left: number;
width: number;
} | null>(null);
const headerRowIdRef = useRef(0);
const headerRowsRef = useRef<HeaderDraftRow[]>([]);
headerRowsRef.current = headerRows;
const genHeaderRowId = () => {
headerRowIdRef.current += 1;
return `uh-${headerRowIdRef.current}`;
};
const normalizeToolCallId = effectiveModelNormalize(protocol);
const preserveDeveloperRole = effectiveModelPreserveDeveloper(protocol);
const devToggle = showDeveloperToggle && protocol !== "claude";
// Click-outside: check both trigger and panel so that if the panel is ever rendered
// in a portal (outside this subtree), clicks inside the panel still do not close it.
const tryCommitHeaderRows = useCallback(
(rows: HeaderDraftRow[]) => {
const parsed = headerRowsToRecord(rows);
const current = getUpstreamHeadersRecord(protocol);
if (upstreamHeadersRecordsEqual(parsed, current)) return;
onCompatPatch(protocol, { upstreamHeaders: parsed });
},
[getUpstreamHeadersRecord, onCompatPatch, protocol]
);
const onHeaderFieldBlur = useCallback(() => {
queueMicrotask(() => tryCommitHeaderRows(headerRowsRef.current));
}, [tryCommitHeaderRows]);
useEffect(() => {
if (!open) return;
return () => {
tryCommitHeaderRows(headerRowsRef.current);
};
}, [open, tryCommitHeaderRows]);
useEffect(() => {
if (!open) return;
const rec = getUpstreamHeadersRecord(protocol);
setHeaderRows(recordToHeaderRows(rec, genHeaderRowId));
// Only re-load rows when opening or switching protocol — not when the parent passes a new
// inline callback every render (would wipe in-progress edits).
// eslint-disable-next-line react-hooks/exhaustive-deps -- see above
}, [open, protocol]);
useEffect(() => {
setValuePeekRowId(null);
setValueFocusRowId(null);
}, [open, protocol]);
const namedHeaderCount = headerRows.filter((r) => r.name.trim()).length;
const canAddHeaderRow = namedHeaderCount < UPSTREAM_HEADERS_UI_MAX;
const updateHeaderRow = (id: string, patch: Partial<Pick<HeaderDraftRow, "name" | "value">>) => {
setHeaderRows((prev) => prev.map((r) => (r.id === id ? { ...r, ...patch } : r)));
};
const addHeaderRow = () => {
if (!canAddHeaderRow) return;
setHeaderRows((prev) => [...prev, { id: genHeaderRowId(), name: "", value: "" }]);
};
const removeHeaderRow = (id: string) => {
setHeaderRows((prev) => {
const next = prev.filter((r) => r.id !== id);
const normalized = next.length === 0 ? [{ id: genHeaderRowId(), name: "", value: "" }] : next;
queueMicrotask(() => tryCommitHeaderRows(normalized));
return normalized;
});
};
useEffect(() => {
if (!open) return;
const onDocClick = (e: MouseEvent) => {
@@ -416,66 +591,189 @@ function ModelCompatPopover({
return () => document.removeEventListener("mousedown", onDocClick);
}, [open]);
const updatePortalPanelRect = useCallback(() => {
if (!open || !ref.current) return;
const rect = ref.current.getBoundingClientRect();
const margin = 10;
const width = Math.min(window.innerWidth - 2 * margin, 24 * 16);
let left = rect.right - width;
left = Math.max(margin, Math.min(left, window.innerWidth - width - margin));
setPortalPanelRect({ top: rect.bottom + 8, left, width });
}, [open]);
useLayoutEffect(() => {
if (!open) {
setPortalPanelRect(null);
return;
}
updatePortalPanelRect();
window.addEventListener("resize", updatePortalPanelRect);
window.addEventListener("scroll", updatePortalPanelRect, true);
return () => {
window.removeEventListener("resize", updatePortalPanelRect);
window.removeEventListener("scroll", updatePortalPanelRect, true);
};
}, [open, updatePortalPanelRect]);
const panelChromeClass =
"flex max-h-[min(82vh,42rem)] flex-col overflow-hidden rounded-xl border-2 border-zinc-200 bg-white shadow-2xl dark:border-zinc-600 dark:bg-zinc-950";
return (
<div className="relative inline-block" ref={ref}>
<div className="relative inline-flex" ref={ref}>
<button
type="button"
onClick={() => setOpen((v) => !v)}
disabled={disabled}
className="inline-flex items-center gap-1 px-2 py-1 text-xs rounded-md border border-border bg-sidebar/50 hover:bg-sidebar text-text-muted hover:text-text-main disabled:opacity-50"
className="inline-flex items-center gap-1.5 px-2.5 py-1.5 text-xs font-medium rounded-lg border border-border bg-background text-text-muted hover:bg-muted hover:text-text-main disabled:opacity-50 transition-colors"
title={t("compatAdjustmentsTitle")}
>
<span className="material-symbols-outlined text-sm">tune</span>
<span className="material-symbols-outlined text-base leading-none">tune</span>
{t("compatButtonLabel")}
</button>
{open && (
<div
ref={panelRef}
className="absolute left-0 top-full mt-1 z-50 min-w-[220px] max-w-[92vw] p-3 rounded-lg border border-border bg-white dark:bg-zinc-900 shadow-xl ring-1 ring-black/5 dark:ring-white/10"
>
<p className="text-[10px] font-semibold uppercase tracking-wide text-text-muted mb-1">
{t("compatAdjustmentsTitle")}
</p>
<p className="text-[10px] text-text-muted mb-2 leading-snug">{t("compatProtocolHint")}</p>
<label className="block text-[10px] font-medium text-text-muted mb-1">
{t("compatProtocolLabel")}
</label>
<select
value={protocol}
onChange={(e) => setProtocol(e.target.value)}
disabled={disabled}
className="w-full mb-3 px-2 py-1.5 text-xs rounded-md border border-border bg-white dark:bg-zinc-800 text-zinc-900 dark:text-zinc-100 focus:outline-none focus:ring-1 focus:ring-primary/50"
{open &&
typeof document !== "undefined" &&
portalPanelRect &&
createPortal(
<div
ref={panelRef}
className={panelChromeClass}
style={{
position: "fixed",
top: portalPanelRect.top,
left: portalPanelRect.left,
width: portalPanelRect.width,
zIndex: 10040,
}}
>
{MODEL_COMPAT_PROTOCOL_KEYS.map((p) => (
<option key={p} value={p}>
{t(compatProtocolLabelKey(p))}
</option>
))}
</select>
<div className="flex flex-col gap-3">
<Toggle
size="sm"
label={t("compatToolIdShort")}
title={t("normalizeToolCallIdLabel")}
checked={normalizeToolCallId}
onChange={(v) => onCompatPatch(protocol, { normalizeToolCallId: v })}
disabled={disabled}
/>
{devToggle && (
<Toggle
size="sm"
label={t("compatDoNotPreserveDeveloper")}
title={t("preserveDeveloperRoleLabel")}
checked={preserveDeveloperRole === false}
onChange={(checked) =>
onCompatPatch(protocol, { preserveOpenAIDeveloperRole: !checked })
}
<div className="shrink-0 border-b-2 border-zinc-200 bg-zinc-100 px-3 py-2.5 dark:border-zinc-600 dark:bg-zinc-900">
<p className="text-xs font-semibold text-text-main">{t("compatAdjustmentsTitle")}</p>
<p className="text-[11px] text-text-muted mt-1 leading-relaxed">
{t("compatProtocolHint")}
</p>
</div>
<div className="min-h-0 flex-1 overflow-x-hidden overflow-y-auto bg-white p-3 [scrollbar-gutter:stable] [scrollbar-width:thin] dark:bg-zinc-950">
<label className="block text-[11px] font-medium text-text-muted mb-1.5">
{t("compatProtocolLabel")}
</label>
<select
value={protocol}
onChange={(e) => setProtocol(e.target.value)}
disabled={disabled}
/>
)}
</div>
</div>
)}
className="mb-4 w-full rounded-lg border border-zinc-200 bg-white px-2.5 py-2 text-xs text-text-main focus:border-primary focus:outline-none focus:ring-2 focus:ring-primary/30 dark:border-zinc-600 dark:bg-zinc-900"
>
{MODEL_COMPAT_PROTOCOL_KEYS.map((p) => (
<option key={p} value={p}>
{t(compatProtocolLabelKey(p))}
</option>
))}
</select>
<div className="flex flex-col gap-3.5">
<Toggle
size="sm"
label={t("compatToolIdShort")}
title={t("normalizeToolCallIdLabel")}
checked={normalizeToolCallId}
onChange={(v) => onCompatPatch(protocol, { normalizeToolCallId: v })}
disabled={disabled}
/>
{devToggle && (
<Toggle
size="sm"
label={t("compatDoNotPreserveDeveloper")}
title={t("preserveDeveloperRoleLabel")}
checked={preserveDeveloperRole === false}
onChange={(checked) =>
onCompatPatch(protocol, { preserveOpenAIDeveloperRole: !checked })
}
disabled={disabled}
/>
)}
</div>
<div className="mt-4 rounded-lg border-2 border-zinc-200 bg-zinc-100 p-3 dark:border-zinc-600 dark:bg-zinc-900">
<label className="block text-[11px] font-semibold text-text-main mb-1">
{t("compatUpstreamHeadersLabel")}
</label>
<p className="text-[11px] text-text-muted mb-3 leading-relaxed">
{t("compatUpstreamHeadersHint")}
</p>
<div className="space-y-2">
<div className="grid grid-cols-[minmax(0,1fr)_minmax(0,1fr)_auto] gap-1.5 items-end text-[10px] font-medium uppercase tracking-wide text-text-muted px-0.5">
<span>{t("compatUpstreamHeaderName")}</span>
<span className="col-span-1">{t("compatUpstreamHeaderValue")}</span>
<span className="w-8 shrink-0" aria-hidden />
</div>
{headerRows.map((row) => (
<div
key={row.id}
className="grid grid-cols-[minmax(0,1fr)_minmax(0,1fr)_auto] gap-1.5 items-center"
>
<Input
value={row.name}
onChange={(e) => updateHeaderRow(row.id, { name: e.target.value })}
onBlur={onHeaderFieldBlur}
disabled={disabled}
placeholder="Authentication"
className="gap-0 min-w-0"
inputClassName="h-9 bg-white py-1.5 px-2 text-xs font-mono dark:bg-zinc-900"
autoComplete="off"
/>
<div
className="min-w-0"
onMouseEnter={() => setValuePeekRowId(row.id)}
onMouseLeave={() =>
setValuePeekRowId((cur) => (cur === row.id ? null : cur))
}
>
<Input
type={
valuePeekRowId === row.id || valueFocusRowId === row.id
? "text"
: "password"
}
value={row.value}
onChange={(e) => updateHeaderRow(row.id, { value: e.target.value })}
onFocus={() => setValueFocusRowId(row.id)}
onBlur={() => {
setValueFocusRowId((cur) => (cur === row.id ? null : cur));
onHeaderFieldBlur();
}}
disabled={disabled}
placeholder="•••"
className="gap-0 min-w-0"
inputClassName="h-9 bg-white py-1.5 px-2 text-xs dark:bg-zinc-900"
autoComplete="off"
spellCheck={false}
/>
</div>
<button
type="button"
disabled={disabled || headerRows.length <= 1}
onClick={() => removeHeaderRow(row.id)}
title={t("compatUpstreamRemoveRow")}
className="flex h-9 w-9 shrink-0 items-center justify-center rounded-lg border border-border/80 text-text-muted hover:bg-red-500/10 hover:text-red-600 dark:hover:text-red-400 disabled:opacity-30 disabled:hover:bg-transparent disabled:hover:text-text-muted transition-colors"
>
<span className="material-symbols-outlined text-lg leading-none">
close
</span>
</button>
</div>
))}
</div>
<button
type="button"
disabled={disabled || !canAddHeaderRow}
onClick={addHeaderRow}
className="mt-3 flex w-full items-center justify-center gap-1.5 rounded-lg border border-dashed border-border py-2 text-xs font-medium text-primary hover:bg-primary/5 disabled:opacity-40 disabled:hover:bg-transparent transition-colors"
>
<span className="material-symbols-outlined text-base leading-none">add</span>
{t("compatUpstreamAddRow")}
</button>
</div>
</div>
</div>,
document.body
)}
</div>
);
}
@@ -1196,14 +1494,13 @@ export default function ProviderDetailPage() {
protocol = MODEL_COMPAT_PROTOCOL_KEYS[0]
) => effectivePreserveForProtocol(modelId, protocol, customMap, overrideMap);
const saveModelCompatFlags = async (
modelId: string,
patch: {
normalizeToolCallId?: boolean;
preserveOpenAIDeveloperRole?: boolean;
compatByProtocol?: CompatByProtocolMap;
}
) => {
const getUpstreamHeadersRecordForModel = useCallback(
(modelId: string, protocol: string) =>
effectiveUpstreamHeadersForProtocol(modelId, protocol, customMap, overrideMap),
[customMap, overrideMap]
);
const saveModelCompatFlags = async (modelId: string, patch: ModelCompatSavePatch) => {
setCompatSavingModelId(modelId);
try {
const c = customMap.get(modelId) as Record<string, unknown> | undefined;
@@ -1211,7 +1508,8 @@ export default function ProviderDetailPage() {
const onlyCompatByProtocol =
patch.compatByProtocol &&
patch.normalizeToolCallId === undefined &&
patch.preserveOpenAIDeveloperRole === undefined;
patch.preserveOpenAIDeveloperRole === undefined &&
!("upstreamHeaders" in patch);
if (c) {
if (onlyCompatByProtocol) {
@@ -1253,7 +1551,10 @@ export default function ProviderDetailPage() {
body: JSON.stringify(body),
});
if (!res.ok) {
notify.error(t("failedSaveCustomModel"));
const detail = await formatProviderModelsErrorResponse(res);
notify.error(
detail ? `${t("failedSaveCustomModel")}${detail}` : t("failedSaveCustomModel")
);
return;
}
} catch {
@@ -1286,6 +1587,7 @@ export default function ProviderDetailPage() {
t={t}
effectiveModelNormalize={effectiveModelNormalize}
effectiveModelPreserveDeveloper={effectiveModelPreserveDeveloper}
getUpstreamHeadersRecord={getUpstreamHeadersRecordForModel}
saveModelCompatFlags={saveModelCompatFlags}
compatSavingModelId={compatSavingModelId}
onModelsChanged={fetchProviderModelMeta}
@@ -1319,6 +1621,7 @@ export default function ProviderDetailPage() {
t={t}
effectiveModelNormalize={effectiveModelNormalize}
effectiveModelPreserveDeveloper={effectiveModelPreserveDeveloper}
getUpstreamHeadersRecord={getUpstreamHeadersRecordForModel}
saveModelCompatFlags={saveModelCompatFlags}
compatSavingModelId={compatSavingModelId}
/>
@@ -1356,23 +1659,18 @@ export default function ProviderDetailPage() {
{importButton}
<div className="flex flex-wrap gap-3">
{models.map((model) => {
const fullModel = `${providerStorageAlias}/${model.id}`;
const oldFormatModel = `${providerId}/${model.id}`;
const existingAlias = Object.entries(modelAliases).find(
([, m]) => m === fullModel || m === oldFormatModel
)?.[0];
return (
<ModelRow
key={model.id}
model={model}
fullModel={`${providerDisplayAlias}/${model.id}`}
alias={existingAlias}
copied={copied}
onCopy={copy}
t={t}
showDeveloperToggle
effectiveModelNormalize={effectiveModelNormalize}
effectiveModelPreserveDeveloper={effectiveModelPreserveDeveloper}
getUpstreamHeadersRecord={(p) => getUpstreamHeadersRecordForModel(model.id, p)}
saveModelCompatFlags={saveModelCompatFlags}
compatDisabled={compatSavingModelId === model.id}
/>
@@ -2008,28 +2306,28 @@ export default function ProviderDetailPage() {
function ModelRow({
model,
fullModel,
alias,
copied,
onCopy,
t,
showDeveloperToggle = true,
effectiveModelNormalize,
effectiveModelPreserveDeveloper,
getUpstreamHeadersRecord,
saveModelCompatFlags,
compatDisabled,
}: ModelRowProps) {
return (
<div className="flex flex-col px-3 py-2 rounded-lg border border-border hover:bg-sidebar/50 min-w-[220px] max-w-md">
<div className="flex items-center gap-2 flex-wrap">
<span className="material-symbols-outlined text-base text-text-muted shrink-0">
<div className="flex min-w-[220px] max-w-md items-center gap-2 rounded-lg border border-border px-3 py-2 hover:bg-sidebar/50">
<div className="flex min-w-0 flex-1 flex-wrap items-center gap-2">
<span className="material-symbols-outlined shrink-0 text-base text-text-muted">
smart_toy
</span>
<code className="text-xs text-text-muted font-mono bg-sidebar px-1.5 py-0.5 rounded">
<code className="rounded bg-sidebar px-1.5 py-0.5 font-mono text-xs text-text-muted">
{fullModel}
</code>
<button
onClick={() => onCopy(fullModel, `model-${model.id}`)}
className="p-0.5 hover:bg-sidebar rounded text-text-muted hover:text-primary"
className="rounded p-0.5 text-text-muted hover:bg-sidebar hover:text-primary"
title={t("copyModel")}
>
<span className="material-symbols-outlined text-sm">
@@ -2037,16 +2335,19 @@ function ModelRow({
</span>
</button>
</div>
<ModelCompatPopover
t={t}
effectiveModelNormalize={(p) => effectiveModelNormalize(model.id, p)}
effectiveModelPreserveDeveloper={(p) => effectiveModelPreserveDeveloper(model.id, p)}
onCompatPatch={(protocol, payload) =>
saveModelCompatFlags(model.id, { compatByProtocol: { [protocol]: payload } })
}
showDeveloperToggle={showDeveloperToggle}
disabled={compatDisabled}
/>
<div className="shrink-0">
<ModelCompatPopover
t={t}
effectiveModelNormalize={(p) => effectiveModelNormalize(model.id, p)}
effectiveModelPreserveDeveloper={(p) => effectiveModelPreserveDeveloper(model.id, p)}
getUpstreamHeadersRecord={getUpstreamHeadersRecord}
onCompatPatch={(protocol, payload) =>
saveModelCompatFlags(model.id, { compatByProtocol: { [protocol]: payload } })
}
showDeveloperToggle={showDeveloperToggle}
disabled={compatDisabled}
/>
</div>
</div>
);
}
@@ -2056,13 +2357,13 @@ ModelRow.propTypes = {
id: PropTypes.string.isRequired,
}).isRequired,
fullModel: PropTypes.string.isRequired,
alias: PropTypes.string,
copied: PropTypes.string,
onCopy: PropTypes.func.isRequired,
t: PropTypes.func,
showDeveloperToggle: PropTypes.bool,
effectiveModelNormalize: PropTypes.func.isRequired,
effectiveModelPreserveDeveloper: PropTypes.func.isRequired,
getUpstreamHeadersRecord: PropTypes.func.isRequired,
saveModelCompatFlags: PropTypes.func.isRequired,
compatDisabled: PropTypes.bool,
};
@@ -2077,6 +2378,7 @@ function PassthroughModelsSection({
t,
effectiveModelNormalize,
effectiveModelPreserveDeveloper,
getUpstreamHeadersRecord,
saveModelCompatFlags,
compatSavingModelId,
}: PassthroughModelsSectionProps) {
@@ -2161,6 +2463,7 @@ function PassthroughModelsSection({
showDeveloperToggle
effectiveModelNormalize={effectiveModelNormalize}
effectiveModelPreserveDeveloper={effectiveModelPreserveDeveloper}
getUpstreamHeadersRecord={(p) => getUpstreamHeadersRecord(modelId, p)}
saveModelCompatFlags={saveModelCompatFlags}
compatDisabled={compatSavingModelId === modelId}
/>
@@ -2181,6 +2484,7 @@ PassthroughModelsSection.propTypes = {
t: PropTypes.func.isRequired,
effectiveModelNormalize: PropTypes.func.isRequired,
effectiveModelPreserveDeveloper: PropTypes.func.isRequired,
getUpstreamHeadersRecord: PropTypes.func.isRequired,
saveModelCompatFlags: PropTypes.func.isRequired,
compatSavingModelId: PropTypes.string,
};
@@ -2195,24 +2499,25 @@ function PassthroughModelRow({
showDeveloperToggle = true,
effectiveModelNormalize,
effectiveModelPreserveDeveloper,
getUpstreamHeadersRecord,
saveModelCompatFlags,
compatDisabled,
}: PassthroughModelRowProps) {
return (
<div className="flex flex-col gap-0 p-3 rounded-lg border border-border hover:bg-sidebar/50">
<div className="flex items-start gap-3">
<span className="material-symbols-outlined text-base text-text-muted shrink-0">
<div className="flex gap-0 rounded-lg border border-border p-3 hover:bg-sidebar/50">
<div className="flex min-w-0 flex-1 items-start gap-3">
<span className="material-symbols-outlined shrink-0 text-base text-text-muted">
smart_toy
</span>
<div className="flex-1 min-w-0">
<p className="text-sm font-medium truncate">{modelId}</p>
<div className="flex items-center gap-1 mt-1 flex-wrap">
<code className="text-xs text-text-muted font-mono bg-sidebar px-1.5 py-0.5 rounded">
<div className="min-w-0 flex-1">
<p className="truncate text-sm font-medium">{modelId}</p>
<div className="mt-1 flex flex-wrap items-center gap-1">
<code className="rounded bg-sidebar px-1.5 py-0.5 font-mono text-xs text-text-muted">
{fullModel}
</code>
<button
onClick={() => onCopy(fullModel, `model-${modelId}`)}
className="p-0.5 hover:bg-sidebar rounded text-text-muted hover:text-primary"
className="rounded p-0.5 text-text-muted hover:bg-sidebar hover:text-primary"
title={t("copyModel")}
>
<span className="material-symbols-outlined text-sm">
@@ -2221,25 +2526,26 @@ function PassthroughModelRow({
</button>
</div>
</div>
<button
onClick={onDeleteAlias}
className="p-1 hover:bg-red-50 rounded text-red-500 shrink-0"
title={t("removeModel")}
>
<span className="material-symbols-outlined text-sm">delete</span>
</button>
</div>
<div className="pl-9">
<div className="flex shrink-0 items-center gap-1 self-start">
<ModelCompatPopover
t={t}
effectiveModelNormalize={(p) => effectiveModelNormalize(modelId, p)}
effectiveModelPreserveDeveloper={(p) => effectiveModelPreserveDeveloper(modelId, p)}
getUpstreamHeadersRecord={getUpstreamHeadersRecord}
onCompatPatch={(protocol, payload) =>
saveModelCompatFlags(modelId, { compatByProtocol: { [protocol]: payload } })
}
showDeveloperToggle={showDeveloperToggle}
disabled={compatDisabled}
/>
<button
onClick={onDeleteAlias}
className="rounded p-1 text-red-500 hover:bg-red-50"
title={t("removeModel")}
>
<span className="material-symbols-outlined text-sm">delete</span>
</button>
</div>
</div>
);
@@ -2255,6 +2561,7 @@ PassthroughModelRow.propTypes = {
showDeveloperToggle: PropTypes.bool,
effectiveModelNormalize: PropTypes.func.isRequired,
effectiveModelPreserveDeveloper: PropTypes.func.isRequired,
getUpstreamHeadersRecord: PropTypes.func.isRequired,
saveModelCompatFlags: PropTypes.func.isRequired,
compatDisabled: PropTypes.bool,
};
@@ -2381,7 +2688,10 @@ function CustomModelsSection({
body: JSON.stringify({ provider: providerId, modelId, ...patch }),
});
if (!res.ok) {
notify.error(t("failedSaveCustomModel"));
const detail = await formatProviderModelsErrorResponse(res);
notify.error(
detail ? `${t("failedSaveCustomModel")}${detail}` : t("failedSaveCustomModel")
);
return;
}
} catch {
@@ -2422,7 +2732,8 @@ function CustomModelsSection({
});
if (!res.ok) {
throw new Error("Failed to save model endpoint settings");
const detail = await formatProviderModelsErrorResponse(res);
throw new Error(detail || "Failed to save model endpoint settings");
}
await fetchCustomModels();
@@ -2431,7 +2742,9 @@ function CustomModelsSection({
cancelEdit();
} catch (e) {
console.error("Failed to save custom model:", e);
notify.error("Failed to save model endpoint settings");
notify.error(
e instanceof Error && e.message ? e.message : "Failed to save model endpoint settings"
);
} finally {
setSavingModelId(null);
}
@@ -2542,10 +2855,14 @@ function CustomModelsSection({
return (
<div
key={model.id}
className="flex items-center gap-3 p-3 rounded-lg border border-border hover:bg-sidebar/50"
className="flex items-center gap-3 rounded-lg border border-border p-3 hover:bg-sidebar/50"
>
<span className="material-symbols-outlined text-base text-primary">tune</span>
<div className="flex-1 min-w-0">
{editingModelId !== model.id && (
<span className="material-symbols-outlined text-base text-primary shrink-0">
tune
</span>
)}
<div className="min-w-0 flex-1">
<p className="text-sm font-medium truncate">{model.name || model.id}</p>
<div className="flex items-center gap-1 mt-1 flex-wrap">
<code className="text-xs text-text-muted font-mono bg-sidebar px-1.5 py-0.5 rounded">
@@ -2596,32 +2913,39 @@ function CustomModelsSection({
{t("compatBadgeNoPreserve")}
</span>
)}
{anyUpstreamHeadersBadge(model.id, customMap, overrideMap) && (
<span
className="text-[10px] px-1.5 py-0.5 rounded-full bg-violet-500/15 text-violet-400 font-medium"
title={t("compatUpstreamHeadersLabel")}
>
{t("compatBadgeUpstreamHeaders")}
</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">
<div className="mt-3 min-w-0 max-w-full rounded-lg border border-border bg-muted p-3 dark:bg-zinc-900">
<div className="flex min-w-0 flex-wrap items-end gap-x-3 gap-y-2">
<div className="w-[11rem] shrink-0 min-w-0">
<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"
className="w-full px-2.5 py-2 text-xs border border-border rounded-lg bg-background text-text-main 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">
<div className="flex min-w-0 flex-1 flex-wrap items-center gap-x-3 gap-y-1 overflow-x-auto overflow-y-visible [scrollbar-width:thin]">
<span className="text-xs text-text-muted shrink-0">
Supported Endpoints
</span>
<div className="flex items-center gap-3 flex-wrap">
<div className="flex flex-wrap items-center gap-x-2 sm:gap-x-3 gap-y-1 min-w-0">
{["chat", "embeddings", "images", "audio"].map((ep) => (
<label
key={ep}
className="flex items-center gap-1.5 text-xs text-text-main cursor-pointer"
className="flex items-center gap-1.5 text-xs text-text-main cursor-pointer whitespace-nowrap"
>
<input
type="checkbox"
@@ -2648,51 +2972,52 @@ function CustomModelsSection({
))}
</div>
</div>
</div>
<div className="mt-3 pt-3 border-t border-border/80 w-full">
<ModelCompatPopover
t={t}
effectiveModelNormalize={(p) =>
effectiveNormalizeForProtocol(model.id, p, customMap, overrideMap)
}
effectiveModelPreserveDeveloper={(p) =>
effectivePreserveForProtocol(model.id, p, customMap, overrideMap)
}
onCompatPatch={(protocol, payload) =>
saveCustomCompat(model.id, {
compatByProtocol: { [protocol]: payload },
})
}
showDeveloperToggle
disabled={savingModelId === model.id}
/>
</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 className="flex shrink-0 flex-wrap items-center gap-2 pb-0.5">
<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>
<div className="flex items-center gap-1">
<div className="flex shrink-0 items-center gap-1">
<button
onClick={() => beginEdit(model)}
className="p-1 hover:bg-sidebar rounded text-text-muted hover:text-primary"
className="rounded p-1 text-text-muted hover:bg-sidebar hover:text-primary"
title={t("edit")}
>
<span className="material-symbols-outlined text-sm">edit</span>
</button>
<ModelCompatPopover
t={t}
effectiveModelNormalize={(p) =>
effectiveNormalizeForProtocol(model.id, p, customMap, overrideMap)
}
effectiveModelPreserveDeveloper={(p) =>
effectivePreserveForProtocol(model.id, p, customMap, overrideMap)
}
getUpstreamHeadersRecord={(p) =>
effectiveUpstreamHeadersForProtocol(model.id, p, customMap, overrideMap)
}
onCompatPatch={(protocol, payload) =>
saveCustomCompat(model.id, {
compatByProtocol: { [protocol]: payload },
})
}
showDeveloperToggle
disabled={savingModelId === model.id}
/>
<button
onClick={() => handleRemove(model.id)}
className="p-1 hover:bg-red-50 rounded text-red-500"
className="rounded p-1 text-red-500 hover:bg-red-50"
title={t("removeCustomModel")}
>
<span className="material-symbols-outlined text-sm">delete</span>
@@ -2731,6 +3056,7 @@ function CompatibleModelsSection({
t,
effectiveModelNormalize,
effectiveModelPreserveDeveloper,
getUpstreamHeadersRecord,
saveModelCompatFlags,
compatSavingModelId,
onModelsChanged,
@@ -2944,6 +3270,7 @@ function CompatibleModelsSection({
showDeveloperToggle={!isAnthropic}
effectiveModelNormalize={effectiveModelNormalize}
effectiveModelPreserveDeveloper={effectiveModelPreserveDeveloper}
getUpstreamHeadersRecord={(p) => getUpstreamHeadersRecord(modelId, p)}
saveModelCompatFlags={saveModelCompatFlags}
compatDisabled={compatSavingModelId === modelId}
/>
@@ -2973,6 +3300,7 @@ CompatibleModelsSection.propTypes = {
t: PropTypes.func.isRequired,
effectiveModelNormalize: PropTypes.func.isRequired,
effectiveModelPreserveDeveloper: PropTypes.func.isRequired,
getUpstreamHeadersRecord: PropTypes.func.isRequired,
saveModelCompatFlags: PropTypes.func.isRequired,
compatSavingModelId: PropTypes.string,
onModelsChanged: PropTypes.func,

View File

@@ -119,9 +119,9 @@ export default function SearchToolsClient() {
} catch (err: any) {
setDuration(Date.now() - start);
if (err.name === "AbortError") {
setError("Request timed out (15s)");
setError(t("requestTimedOut", { seconds: 15 }));
} else {
setError(err.message || "Network error");
setError(err?.message || t("networkError"));
}
} finally {
setLoading(false);

View File

@@ -33,6 +33,7 @@ interface SearchFormProps {
export default function SearchForm({ onSearch, loading, onCancel, providers }: SearchFormProps) {
const t = useTranslations("search");
const tc = useTranslations("common");
const [query, setQuery] = useState("");
const [provider, setProvider] = useState("auto");
const [searchType, setSearchType] = useState("web");
@@ -92,7 +93,7 @@ export default function SearchForm({ onSearch, loading, onCancel, providers }: S
<textarea
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Enter search query..."
placeholder={t("queryPlaceholder")}
className="w-full bg-surface border border-border rounded-lg p-2.5 text-sm text-text-main resize-none h-16 focus:outline-none focus:ring-2 focus:ring-primary/30"
onKeyDown={(e) => {
if (e.key === "Enter" && !e.shiftKey) {
@@ -114,7 +115,7 @@ export default function SearchForm({ onSearch, loading, onCancel, providers }: S
value={provider}
onChange={(e: any) => setProvider(e.target.value)}
options={[
{ value: "auto", label: "auto (cheapest)" },
{ value: "auto", label: t("providerAuto") },
...activeProviders.map((p) => ({
value: p.id,
label: p.name,
@@ -131,8 +132,8 @@ export default function SearchForm({ onSearch, loading, onCancel, providers }: S
value={searchType}
onChange={(e: any) => setSearchType(e.target.value)}
options={[
{ value: "web", label: "web" },
{ value: "news", label: "news" },
{ value: "web", label: t("searchTypeWeb") },
{ value: "news", label: t("searchTypeNews") },
]}
className="w-full"
/>
@@ -172,7 +173,7 @@ export default function SearchForm({ onSearch, loading, onCancel, providers }: S
<input
value={country}
onChange={(e) => setCountry(e.target.value)}
placeholder="any"
placeholder={t("optionAny")}
className="w-full bg-surface border border-border rounded-md px-2 py-1.5 text-xs text-text-main focus:outline-none"
/>
</div>
@@ -181,7 +182,7 @@ export default function SearchForm({ onSearch, loading, onCancel, providers }: S
<input
value={language}
onChange={(e) => setLanguage(e.target.value)}
placeholder="any"
placeholder={t("optionAny")}
className="w-full bg-surface border border-border rounded-md px-2 py-1.5 text-xs text-text-main focus:outline-none"
/>
</div>
@@ -192,11 +193,11 @@ export default function SearchForm({ onSearch, loading, onCancel, providers }: S
value={timeRange}
onChange={(e: any) => setTimeRange(e.target.value)}
options={[
{ value: "", label: "any" },
{ value: "day", label: "Past day" },
{ value: "week", label: "Past week" },
{ value: "month", label: "Past month" },
{ value: "year", label: "Past year" },
{ value: "", label: t("optionAny") },
{ value: "day", label: t("timeRangeDay") },
{ value: "week", label: t("timeRangeWeek") },
{ value: "month", label: t("timeRangeMonth") },
{ value: "year", label: t("timeRangeYear") },
]}
className="w-full"
/>
@@ -209,7 +210,7 @@ export default function SearchForm({ onSearch, loading, onCancel, providers }: S
<input
value={domainInput}
onChange={(e) => setDomainInput(e.target.value)}
placeholder="example.com"
placeholder={t("domainPlaceholder")}
className="flex-1 bg-surface border border-border rounded-md px-2 py-1.5 text-xs text-text-main focus:outline-none"
onKeyDown={(e) => e.key === "Enter" && addDomain("include")}
/>
@@ -244,7 +245,7 @@ export default function SearchForm({ onSearch, loading, onCancel, providers }: S
<input
value={excludeDomainInput}
onChange={(e) => setExcludeDomainInput(e.target.value)}
placeholder="example.com"
placeholder={t("domainPlaceholder")}
className="flex-1 bg-surface border border-border rounded-md px-2 py-1.5 text-xs text-text-main focus:outline-none"
onKeyDown={(e) => e.key === "Enter" && addDomain("exclude")}
/>
@@ -274,9 +275,9 @@ export default function SearchForm({ onSearch, loading, onCancel, providers }: S
value={safeSearch}
onChange={(e: any) => setSafeSearch(e.target.value)}
options={[
{ value: "off", label: "Off" },
{ value: "moderate", label: "Moderate" },
{ value: "strict", label: "Strict" },
{ value: "off", label: t("safeSearchOff") },
{ value: "moderate", label: t("safeSearchModerate") },
{ value: "strict", label: t("safeSearchStrict") },
]}
className="w-full"
/>
@@ -289,7 +290,7 @@ export default function SearchForm({ onSearch, loading, onCancel, providers }: S
<div className="p-4 border-b border-border">
{loading ? (
<Button variant="danger" onClick={onCancel} className="w-full">
Cancel
{tc("cancel")}
</Button>
) : (
<Button
@@ -298,7 +299,7 @@ export default function SearchForm({ onSearch, loading, onCancel, providers }: S
disabled={noProviders || !query.trim()}
className="w-full"
>
Search
{tc("search")}
</Button>
)}
{noProviders && <p className="text-xs text-text-muted mt-2">{t("noSearchProviders")}</p>}

View File

@@ -1,7 +1,7 @@
"use client";
import { useState, useEffect } from "react";
import { useTranslations } from "next-intl";
import { useLocale, useTranslations } from "next-intl";
interface HistoryEntry {
query: string;
@@ -14,9 +14,9 @@ interface SearchHistoryProps {
onReplay: (entry: HistoryEntry) => void;
}
function timeAgo(timestamp: string): string {
function timeAgo(timestamp: string, locale: string): string {
try {
const rtf = new Intl.RelativeTimeFormat("en", { numeric: "auto" });
const rtf = new Intl.RelativeTimeFormat(locale, { numeric: "auto" });
const diff = Date.now() - new Date(timestamp).getTime();
const minutes = Math.floor(diff / 60_000);
if (minutes < 1) return rtf.format(0, "minute");
@@ -25,12 +25,13 @@ function timeAgo(timestamp: string): string {
if (hours < 24) return rtf.format(-hours, "hour");
return rtf.format(-Math.floor(hours / 24), "day");
} catch {
return new Date(timestamp).toLocaleString();
return new Date(timestamp).toLocaleString(locale);
}
}
export default function SearchHistory({ onReplay }: SearchHistoryProps) {
const t = useTranslations("search");
const locale = useLocale();
const [entries, setEntries] = useState<HistoryEntry[]>([]);
useEffect(() => {
@@ -57,7 +58,7 @@ export default function SearchHistory({ onReplay }: SearchHistoryProps) {
<div className="text-xs text-text-main truncate">{entry.query}</div>
<div className="flex justify-between mt-0.5">
<span className="text-[10px] text-text-muted">{entry.provider}</span>
<span className="text-[10px] text-text-muted">{timeAgo(entry.timestamp)}</span>
<span className="text-[10px] text-text-muted">{timeAgo(entry.timestamp, locale)}</span>
</div>
</button>
))}

View File

@@ -3,6 +3,7 @@
import { useState, useEffect } from "react";
import { Card, Button, Input, Toggle } from "@/shared/components";
import { cn } from "@/shared/utils/cn";
import { ROUTING_STRATEGIES } from "@/shared/constants/routingStrategies";
import { useTranslations } from "next-intl";
export default function ComboDefaultsTab() {
@@ -21,14 +22,11 @@ export default function ComboDefaultsTab() {
const [saving, setSaving] = useState(false);
const t = useTranslations("settings");
const tc = useTranslations("common");
const strategyOptions = [
{ value: "priority", label: t("priority"), icon: "sort" },
{ value: "weighted", label: t("weighted"), icon: "percent" },
{ value: "round-robin", label: t("roundRobin"), icon: "autorenew" },
{ value: "random", label: t("random"), icon: "shuffle" },
{ value: "least-used", label: t("leastUsed"), icon: "low_priority" },
{ value: "cost-optimized", label: t("costOpt"), icon: "savings" },
];
const strategyOptions = ROUTING_STRATEGIES.map((strategy) => ({
value: strategy.value,
label: t(strategy.labelKey),
icon: strategy.icon,
}));
const numericSettings = [
{ key: "maxRetries", label: t("maxRetriesLabel"), min: 0, max: 5 },
{ key: "retryDelayMs", label: t("retryDelayLabel"), min: 500, max: 10000, step: 500 },
@@ -87,6 +85,13 @@ export default function ComboDefaultsTab() {
<h3 className="text-lg font-semibold">{t("comboDefaultsTitle")}</h3>
<span className="text-xs text-text-muted ml-auto">{t("globalComboConfig")}</span>
</div>
<div className="mb-4 rounded-lg border border-amber-500/20 bg-amber-500/5 p-3">
<p className="text-xs font-medium text-amber-700 dark:text-amber-300">
{t("comboDefaultsGuideTitle")}
</p>
<p className="text-xs text-text-muted mt-1">{t("comboDefaultsGuideHint1")}</p>
<p className="text-xs text-text-muted">{t("comboDefaultsGuideHint2")}</p>
</div>
<div className="flex flex-col gap-4">
{/* Default Strategy */}
<div className="flex items-center justify-between">
@@ -97,7 +102,7 @@ export default function ComboDefaultsTab() {
<div
role="tablist"
aria-label={t("comboStrategyAria")}
className="grid grid-cols-3 gap-1 p-0.5 rounded-md bg-black/5 dark:bg-white/5"
className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-5 gap-1 p-0.5 rounded-md bg-black/5 dark:bg-white/5"
>
{strategyOptions.map((s) => (
<button

View File

@@ -3,31 +3,20 @@
import { useState, useEffect } from "react";
import { Card, Input, Button } from "@/shared/components";
import FallbackChainsEditor from "./FallbackChainsEditor";
import {
ROUTING_STRATEGIES,
SETTINGS_FALLBACK_STRATEGY_VALUES,
} from "@/shared/constants/routingStrategies";
import { useTranslations } from "next-intl";
const STRATEGIES = [
{
value: "fill-first",
labelKey: "fillFirst",
descKey: "fillFirstDesc",
icon: "vertical_align_top",
},
{ value: "round-robin", labelKey: "roundRobin", descKey: "roundRobinDesc", icon: "loop" },
{ value: "p2c", labelKey: "p2c", descKey: "p2cDesc", icon: "balance" },
{ value: "random", labelKey: "random", descKey: "randomDesc", icon: "shuffle" },
{
value: "least-used",
labelKey: "leastUsed",
descKey: "leastUsedDesc",
icon: "low_priority",
},
{
value: "cost-optimized",
labelKey: "costOpt",
descKey: "costOptDesc",
icon: "savings",
},
];
const STRATEGIES = ROUTING_STRATEGIES.filter((strategy) =>
SETTINGS_FALLBACK_STRATEGY_VALUES.includes(strategy.value)
).map((strategy) => ({
value: strategy.value,
labelKey: strategy.labelKey,
descKey: strategy.settingsDescKey,
icon: strategy.icon,
}));
export default function RoutingTab() {
const [settings, setSettings] = useState<any>({ fallbackStrategy: "fill-first" });
@@ -36,14 +25,10 @@ export default function RoutingTab() {
const [newPattern, setNewPattern] = useState("");
const [newTarget, setNewTarget] = useState("");
const t = useTranslations("settings");
const strategyHintKeyByValue: Record<string, string> = {
"fill-first": "fillFirstDesc",
"round-robin": "roundRobinDesc",
p2c: "p2cDesc",
random: "randomDesc",
"least-used": "leastUsedDesc",
"cost-optimized": "costOptDesc",
};
const strategyHintKeyByValue = STRATEGIES.reduce<Record<string, string>>((acc, strategy) => {
acc[strategy.value] = strategy.descKey;
return acc;
}, {});
useEffect(() => {
fetch("/api/settings")
@@ -99,6 +84,14 @@ export default function RoutingTab() {
<h3 className="text-lg font-semibold">{t("routingStrategy")}</h3>
</div>
<div className="mb-4 rounded-lg border border-blue-500/20 bg-blue-500/5 p-3">
<p className="text-xs font-medium text-blue-700 dark:text-blue-300">
{t("routingAdvancedGuideTitle")}
</p>
<p className="text-xs text-text-muted mt-1">{t("routingAdvancedGuideHint1")}</p>
<p className="text-xs text-text-muted">{t("routingAdvancedGuideHint2")}</p>
</div>
<div
className="grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-3 gap-2 mb-4"
style={{ gridAutoRows: "1fr" }}

View File

@@ -37,7 +37,7 @@ export default function SettingsPage() {
const activeTab = userSelectedTab || tabs.find((t) => t.id === tabParam)?.id || "general";
return (
<div className="max-w-2xl mx-auto min-w-0">
<div className="max-w-6xl mx-auto min-w-0">
<div className="flex flex-col gap-6">
{/* Tab navigation */}
<div className="w-full overflow-x-auto pb-1">

View File

@@ -1,6 +1,7 @@
import { NextResponse } from "next/server";
import { getSettings } from "@/lib/localDb";
import { getProviderConnections, getSettings } from "@/lib/localDb";
import { APP_CONFIG } from "@/shared/constants/config";
import { AI_PROVIDERS } from "@/shared/constants/providers";
/**
* GET /api/monitoring/health — System health overview
@@ -16,6 +17,7 @@ export async function GET() {
const { getInflightCount } = await import("@omniroute/open-sse/services/requestDedup.ts");
const settings = await getSettings();
const connections = await getProviderConnections();
const circuitBreakers = getAllCircuitBreakerStatuses();
const rateLimitStatus = getAllRateLimitStatus();
const lockouts = getAllModelLockouts();
@@ -43,11 +45,22 @@ export async function GET() {
};
}
const configuredProviders = new Set(connections.map((c: any) => c.provider));
const activeProviders = new Set(
connections.filter((c: any) => c.isActive !== false).map((c: any) => c.provider)
);
return NextResponse.json({
status: "healthy",
timestamp: new Date().toISOString(),
system,
providerHealth,
providerSummary: {
catalogCount: Object.keys(AI_PROVIDERS).length,
configuredCount: configuredProviders.size,
activeCount: activeProviders.size,
monitoredCount: Object.keys(providerHealth).length,
},
localProviders: getAllHealthStatuses(),
rateLimitStatus,
lockouts,

View File

@@ -130,6 +130,7 @@ export async function PUT(request) {
supportedEndpoints,
normalizeToolCallId,
preserveOpenAIDeveloperRole,
upstreamHeaders,
compatByProtocol,
} = validation.data;
@@ -141,6 +142,7 @@ export async function PUT(request) {
if ("normalizeToolCallId" in raw) updates.normalizeToolCallId = normalizeToolCallId;
if ("preserveOpenAIDeveloperRole" in raw)
updates.preserveOpenAIDeveloperRole = preserveOpenAIDeveloperRole;
if ("upstreamHeaders" in raw) updates.upstreamHeaders = upstreamHeaders;
if ("compatByProtocol" in raw && compatByProtocol !== undefined) {
updates.compatByProtocol = compatByProtocol;
}
@@ -157,11 +159,13 @@ export async function PUT(request) {
"modelId",
"normalizeToolCallId",
"preserveOpenAIDeveloperRole",
"upstreamHeaders",
"compatByProtocol",
].includes(k)
) &&
("normalizeToolCallId" in raw ||
"preserveOpenAIDeveloperRole" in raw ||
"upstreamHeaders" in raw ||
"compatByProtocol" in raw);
if (compatOnly) {
const knownProvider =
@@ -191,6 +195,12 @@ export async function PUT(request) {
if ("compatByProtocol" in raw && compatByProtocol && typeof compatByProtocol === "object") {
patch.compatByProtocol = compatByProtocol;
}
if ("upstreamHeaders" in raw) {
patch.upstreamHeaders =
upstreamHeaders === null || typeof upstreamHeaders === "object"
? upstreamHeaders
: undefined;
}
if (Object.keys(patch).length > 0) {
mergeModelCompatOverride(provider, modelId, patch);
}

View File

@@ -1,4 +1,6 @@
import { NextResponse } from "next/server";
import { z } from "zod";
import { isValidationFailure, validateBody } from "@/shared/validation/helpers";
import {
getProviderConnectionById,
updateProviderConnection,
@@ -92,6 +94,11 @@ const CLI_RUNTIME_PROVIDER_MAP = {
kilocode: "kilo",
};
/** POST body is optional; when present, only known fields are validated. */
const providerConnectionTestBodySchema = z.object({
validationModelId: z.string().max(500).optional(),
});
function toSafeMessage(value: any, fallback = "Unknown error"): string {
if (typeof value !== "string") return fallback;
const trimmed = value.trim();
@@ -683,14 +690,17 @@ export async function POST(request: Request, { params }: { params: Promise<{ id:
try {
const { id } = await params;
// Parse optional body for validationModelId
let validationModelId;
let rawBody: unknown = {};
try {
const body = await request.json();
validationModelId = body?.validationModelId;
rawBody = await request.json();
} catch {
// Body is optional
// Empty or non-JSON body — treat as {}
}
const validation = validateBody(providerConnectionTestBodySchema, rawBody);
if (isValidationFailure(validation)) {
return NextResponse.json({ error: validation.error }, { status: 400 });
}
const { validationModelId } = validation.data;
const data = await testSingleConnection(id, validationModelId);

View File

@@ -1,6 +1,7 @@
import { NextResponse } from "next/server";
import { z } from "zod";
import { isAuthenticated } from "@/shared/utils/apiAuth";
import { isValidationFailure, validateBody } from "@/shared/validation/helpers";
import { setAccountKeyLimit, getAccountKeyLimit } from "@/lib/db/registeredKeys";
const limitsSchema = z.object({
@@ -32,20 +33,20 @@ export async function PUT(request: Request, { params }: { params: Promise<{ id:
return NextResponse.json({ error: { message: "Authentication required" } }, { status: 401 });
}
let body: unknown;
let rawBody: unknown;
try {
body = await request.json();
rawBody = await request.json();
} catch {
return NextResponse.json({ error: "Invalid JSON body" }, { status: 400 });
}
const parsed = limitsSchema.safeParse(body);
if (!parsed.success) {
return NextResponse.json({ error: parsed.error.flatten() }, { status: 400 });
const validation = validateBody(limitsSchema, rawBody);
if (isValidationFailure(validation)) {
return NextResponse.json({ error: validation.error }, { status: 400 });
}
const resolvedParams = await params;
setAccountKeyLimit(resolvedParams.id, parsed.data);
setAccountKeyLimit(resolvedParams.id, validation.data);
const updated = getAccountKeyLimit(resolvedParams.id);
return NextResponse.json({ accountId: resolvedParams.id, limits: updated });
}

View File

@@ -1,6 +1,7 @@
import { NextResponse } from "next/server";
import { z } from "zod";
import { isAuthenticated } from "@/shared/utils/apiAuth";
import { isValidationFailure, validateBody } from "@/shared/validation/helpers";
const reportSchema = z.object({
title: z.string().min(1).max(300),
@@ -26,19 +27,19 @@ export async function POST(request: Request) {
return NextResponse.json({ error: { message: "Authentication required" } }, { status: 401 });
}
let body: unknown;
let rawBody: unknown;
try {
body = await request.json();
rawBody = await request.json();
} catch {
return NextResponse.json({ error: "Invalid JSON body" }, { status: 400 });
}
const parsed = reportSchema.safeParse(body);
if (!parsed.success) {
return NextResponse.json({ error: parsed.error.flatten() }, { status: 400 });
const validation = validateBody(reportSchema, rawBody);
if (isValidationFailure(validation)) {
return NextResponse.json({ error: validation.error }, { status: 400 });
}
const { title, provider, accountId, requestId, errorCode, details, labels = [] } = parsed.data;
const { title, provider, accountId, requestId, errorCode, details, labels = [] } = validation.data;
const repo = process.env.GITHUB_ISSUES_REPO;
const token = process.env.GITHUB_ISSUES_TOKEN;

View File

@@ -16,6 +16,7 @@ import { getAllAudioModels } from "@omniroute/open-sse/config/audioRegistry.ts";
import { getAllModerationModels } from "@omniroute/open-sse/config/moderationRegistry.ts";
import { getAllVideoModels, getVideoProvider } from "@omniroute/open-sse/config/videoRegistry.ts";
import { getAllMusicModels, getMusicProvider } from "@omniroute/open-sse/config/musicRegistry.ts";
import { REGISTRY } from "@omniroute/open-sse/config/providerRegistry.ts";
const FALLBACK_ALIAS_TO_PROVIDER = {
ag: "antigravity",
@@ -190,6 +191,7 @@ export async function getUnifiedModelsResponse(
permission: [],
root: combo.name,
parent: null,
...(combo.context_length ? { context_length: combo.context_length } : {}),
});
}
@@ -206,8 +208,15 @@ export async function getUnifiedModelsResponse(
continue;
}
// Get default context length from registry (provider-level default)
const registryEntry = REGISTRY[alias] || REGISTRY[canonicalProviderId];
const defaultContextLength = registryEntry?.defaultContextLength;
for (const model of providerModels) {
const aliasId = `${alias}/${model.id}`;
// Model-level context length overrides provider default
const contextLength = model.contextLength || defaultContextLength;
models.push({
id: aliasId,
object: "model",
@@ -216,6 +225,7 @@ export async function getUnifiedModelsResponse(
permission: [],
root: model.id,
parent: null,
...(contextLength ? { context_length: contextLength } : {}),
});
// Add provider-id prefix in addition to short alias (ex: kiro/model + kr/model).
@@ -229,6 +239,7 @@ export async function getUnifiedModelsResponse(
permission: [],
root: model.id,
parent: aliasId,
...(contextLength ? { context_length: contextLength } : {}),
});
}
}

View File

@@ -1,6 +1,7 @@
import { NextResponse } from "next/server";
import { z } from "zod";
import { isAuthenticated } from "@/shared/utils/apiAuth";
import { isValidationFailure, validateBody } from "@/shared/validation/helpers";
import { setProviderKeyLimit, getProviderKeyLimit } from "@/lib/db/registeredKeys";
const limitsSchema = z.object({
@@ -32,20 +33,20 @@ export async function PUT(request: Request, { params }: { params: Promise<{ prov
return NextResponse.json({ error: { message: "Authentication required" } }, { status: 401 });
}
let body: unknown;
let rawBody: unknown;
try {
body = await request.json();
rawBody = await request.json();
} catch {
return NextResponse.json({ error: "Invalid JSON body" }, { status: 400 });
}
const parsed = limitsSchema.safeParse(body);
if (!parsed.success) {
return NextResponse.json({ error: parsed.error.flatten() }, { status: 400 });
const validation = validateBody(limitsSchema, rawBody);
if (isValidationFailure(validation)) {
return NextResponse.json({ error: validation.error }, { status: 400 });
}
const { provider } = await params;
setProviderKeyLimit(provider, parsed.data);
setProviderKeyLimit(provider, validation.data);
const updated = getProviderKeyLimit(provider);
return NextResponse.json({ provider, limits: updated });
}

View File

@@ -1,6 +1,7 @@
import { NextResponse } from "next/server";
import { z } from "zod";
import { isAuthenticated } from "@/shared/utils/apiAuth";
import { isValidationFailure, validateBody } from "@/shared/validation/helpers";
import { issueRegisteredKey, checkQuota, listRegisteredKeys } from "@/lib/db/registeredKeys";
// ─── Validation ───────────────────────────────────────────────────────────────
@@ -53,19 +54,19 @@ export async function POST(request: Request) {
return NextResponse.json({ error: { message: "Authentication required" } }, { status: 401 });
}
let body: unknown;
let rawBody: unknown;
try {
body = await request.json();
rawBody = await request.json();
} catch {
return NextResponse.json({ error: "Invalid JSON body" }, { status: 400 });
}
const parsed = issueKeySchema.safeParse(body);
if (!parsed.success) {
return NextResponse.json({ error: parsed.error.flatten() }, { status: 400 });
const validation = validateBody(issueKeySchema, rawBody);
if (isValidationFailure(validation)) {
return NextResponse.json({ error: validation.error }, { status: 400 });
}
const { provider, accountId } = parsed.data;
const { provider, accountId } = validation.data;
// ── Quota check ──
try {
@@ -83,7 +84,7 @@ export async function POST(request: Request) {
// ── Issue ──
try {
const result = issueRegisteredKey(parsed.data);
const result = issueRegisteredKey(validation.data);
if ("idempotencyConflict" in result) {
return NextResponse.json(

View File

@@ -56,7 +56,9 @@
"warning": "تحذير",
"note": "ملاحظة",
"free": "مجاني",
"skipToContent": "انتقل إلى المحتوى"
"skipToContent": "انتقل إلى المحتوى",
"maintenanceServerIssues": "Server is experiencing issues. Some features may be unavailable.",
"maintenanceServerUnreachable": "Server is unreachable. Reconnecting..."
},
"sidebar": {
"home": "الصفحة الرئيسية",
@@ -576,7 +578,23 @@
"mitmStep1": "1. Start MITM to route requests through OmniRoute.",
"mitmStep2Prefix": "2. Add",
"mitmStep2Suffix": "to your hosts file as 127.0.0.1.",
"mitmStep3": "3. Open {toolName} and requests will be proxied."
"mitmStep3": "3. Open {toolName} and requests will be proxied.",
"whenToUseLabel": "متى تستخدم",
"openToolDocs": "افتح مستندات الأداة",
"toolUseCases": {
"claude": "استخدمه عندما تريد سير عمل تخطيط قوي وعمليات إعادة بناء طويلة متعددة الملفات باستخدام Claude Code.",
"codex": "يُستخدم عندما يتم توحيد فريقك في تدفقات OpenAI Codex CLI والمصادقة المستندة إلى الملف الشخصي.",
"droid": "استخدمه عندما تحتاج إلى وكيل طرفي خفيف الوزن يركز على الترميز السريع وحلقات تنفيذ الأوامر.",
"openclaw": "استخدمه عندما تريد وكيل ترميز نمط Open Claw ولكن يتم توجيهه من خلال سياسات OmniRoute.",
"cline": "يُستخدم عندما تقوم بتكوين وكلاء الترميز داخل المحررين وتريد إعدادًا موجهًا باستخدام نماذج OmniRoute.",
"kilo": "يُستخدم عندما يعتمد سير عملك على أوامر Kilo Code والتحريرات المتكررة السريعة.",
"cursor": "استخدمه عند البرمجة في Cursor وتحتاج إلى نماذج مخصصة متوافقة مع OpenAI من خلال OmniRoute.",
"continue": "استخدمه عند تشغيل متابعة في IDEs وتحتاج إلى تكوين موفر محمول يستند إلى JSON.",
"opencode": "استخدمه عندما تفضل تشغيل الوكيل الأصلي للمحطة والأتمتة النصية عبر OpenCode.",
"kiro": "يُستخدم عند دمج Kiro والتحكم في توجيه النموذج مركزيًا من OmniRoute.",
"antigravity": "يُستخدم عندما يجب اعتراض حركة مرور Antigravity/Kiro عبر MITM وتوجيهها إلى OmniRoute.",
"copilot": "استخدمه عندما تريد UX بأسلوب دردشة Copilot أثناء فرض مفاتيح OmniRoute وقواعد التوجيه."
}
},
"combos": {
"title": "المجموعات",
@@ -1087,7 +1105,13 @@
"resetAllTitle": "إعادة ضبط كافة قواطع الدائرة إلى الحالة الصحية",
"resetting": "إعادة الضبط...",
"resetAll": "إعادة ضبط الكل",
"until": "حتى {time}"
"until": "حتى {time}",
"activeProviders": "{count} active",
"monitoredProviders": "{count} monitored",
"configuredProvidersLabel": "تم تكوينه في لوحة القيادة",
"configuredProvidersHint": "الموفرون الذين لديهم بيانات اعتماد محفوظة في /dashboard/providers، بغض النظر عن حالة وقت التشغيل.",
"activeProvidersHint": "الموفرون الذين تم تكوينهم ممكّنون حاليًا لطلبات التوجيه.",
"monitoredProvidersHint": "يتم تتبع مقدمي الخدمة حاليًا بواسطة مراقبي صحة قاطع الدائرة."
},
"limits": {
"title": "الحدود والحصص",
@@ -1446,7 +1470,14 @@
"compatProtocolOpenAIResponses": "OpenAI Responses API",
"compatProtocolClaude": "Anthropic Messages",
"tokenRefreshed": "Token refreshed successfully",
"tokenRefreshFailed": "Token refresh failed"
"tokenRefreshFailed": "Token refresh failed",
"compatBadgeUpstreamHeaders": "Headers",
"compatUpstreamAddRow": "Add header",
"compatUpstreamHeaderName": "Header name",
"compatUpstreamHeaderValue": "Value",
"compatUpstreamHeadersHint": "High-privilege setting — same trust level as editing provider API credentials; only trusted admins should use it. Merged after OmniRoute adds auth from the provider API key. If a custom header uses the same name as an existing one (e.g. Authorization), your value fully replaces the auto-generated header (including the Bearer token) — the upstream only sees what you typed, not the key from settings. Misconfiguration can cause 401 or broken upstream auth. One row per header (e.g. extra Authentication for some gateways). Hover or focus the value to preview. Saves on blur, outside click, or closing this panel.",
"compatUpstreamHeadersLabel": "Extra upstream headers",
"compatUpstreamRemoveRow": "Remove row"
},
"settings": {
"title": "الإعدادات",
@@ -1849,7 +1880,13 @@
"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}"
"disableFingerprintTitle": "Disable fingerprint for {provider}",
"routingAdvancedGuideTitle": "توجيه التوجيه المتقدم",
"routingAdvancedGuideHint1": "استخدمFill First للحصول على الأولوية التي يمكن التنبؤ بها، وRound Robin للعدالة، وP2C لمرونة زمن الاستجابة.",
"routingAdvancedGuideHint2": "إذا اختلف مقدمو الخدمة من حيث الجودة/التكلفة، فابدأ بـ Cost Opt للعمل في الخلفية والأقل استخدامًا للارتداء المتوازن.",
"comboDefaultsGuideTitle": "كيفية ضبط إعدادات التحرير والسرد الافتراضية",
"comboDefaultsGuideHint1": "اجعل عمليات إعادة المحاولة منخفضة في التدفقات ذات زمن الوصول المنخفض؛ زيادة المهلة فقط لمهام الجيل الطويل.",
"comboDefaultsGuideHint2": "استخدم تجاوزات الموفر عندما يحتاج أحد الموفرين إلى سلوك مهلة/إعادة محاولة مختلف عن الإعدادات الافتراضية العامة."
},
"translator": {
"title": "مترجم",
@@ -2569,7 +2606,15 @@
"opencodeDetected": "opencode {version} detected",
"opencodeDesc": "Generate a ready-to-use {configFile} with your OmniRoute base URL and all available models — drop it in your project root and run {command}.",
"downloadConfig": "Download {file}",
"downloaded": "Downloaded!"
"downloaded": "Downloaded!",
"setupGuideTitle": "Setup guide",
"openCliTools": "Open CLI Tools",
"setupGuideDetectCliTitle": "Detect installed CLIs",
"setupGuideDetectCliDesc": "Click Refresh after installing or updating a CLI so OmniRoute can rescan binaries and versions.",
"setupGuideCustomAgentTitle": "Register custom binary",
"setupGuideCustomAgentDesc": "Use Add Custom Agent when your CLI is not in the built-in list. Provide binary name and version command.",
"setupGuideCommandMissingTitle": "Fix 'command not found'",
"setupGuideCommandMissingDesc": "Ensure the CLI command exists in PATH, open a new terminal session, and rerun Refresh."
},
"autoCombo": {
"title": "Auto-Combo Engine",
@@ -2628,6 +2673,21 @@
"rerank": "Rerank",
"rerankModel": "Rerank Model",
"positionDelta": "Position Change",
"emptyState": "Send a search query to see results"
"emptyState": "Send a search query to see results",
"safeSearchOff": "Off",
"safeSearchModerate": "Moderate",
"safeSearchStrict": "Strict",
"queryPlaceholder": "Enter search query...",
"providerAuto": "auto (cheapest)",
"searchTypeWeb": "web",
"searchTypeNews": "news",
"optionAny": "any",
"timeRangeDay": "Past day",
"timeRangeWeek": "Past week",
"timeRangeMonth": "Past month",
"timeRangeYear": "Past year",
"domainPlaceholder": "example.com",
"requestTimedOut": "Request timed out ({seconds}s)",
"networkError": "Network error"
}
}

View File

@@ -56,7 +56,9 @@
"warning": "Предупреждение",
"note": "Забележка",
"free": "безплатно",
"skipToContent": "Преминете към съдържанието"
"skipToContent": "Преминете към съдържанието",
"maintenanceServerIssues": "Server is experiencing issues. Some features may be unavailable.",
"maintenanceServerUnreachable": "Server is unreachable. Reconnecting..."
},
"sidebar": {
"home": "Начало",
@@ -576,7 +578,23 @@
"mitmStep1": "1. Start MITM to route requests through OmniRoute.",
"mitmStep2Prefix": "2. Add",
"mitmStep2Suffix": "to your hosts file as 127.0.0.1.",
"mitmStep3": "3. Open {toolName} and requests will be proxied."
"mitmStep3": "3. Open {toolName} and requests will be proxied.",
"whenToUseLabel": "Кога да използвате",
"openToolDocs": "Отворете документите на инструмента",
"toolUseCases": {
"claude": "Използвайте, когато искате силни работни потоци за планиране и дълги многофайлови рефактори с Claude Code.",
"codex": "Използвайте, когато вашият екип е стандартизиран на OpenAI Codex CLI потоци и базирано на профил удостоверяване.",
"droid": "Използвайте, когато имате нужда от лек терминален агент, фокусиран върху цикли за бързо кодиране и изпълнение на команди.",
"openclaw": "Използвайте, когато искате агент за кодиране в стил Open Claw, но насочен през политиките на OmniRoute.",
"cline": "Използвайте, когато конфигурирате кодиращи агенти вътре в редактори и искате насочвана настройка с OmniRoute модели.",
"kilo": "Използвайте, когато вашият работен процес зависи от команди на Kilo Code и бързи итеративни редакции.",
"cursor": "Използвайте, когато кодирате в Cursor и имате нужда от персонализирани OpenAI-съвместими модели чрез OmniRoute.",
"continue": "Използвайте, когато изпълнявате Continue в IDE и имате нужда от преносима JSON-базирана конфигурация на доставчик.",
"opencode": "Използвайте, когато предпочитате нативни за терминални агенти и скриптова автоматизация чрез OpenCode.",
"kiro": "Използвайте, когато интегрирате Kiro и контролирате маршрутизирането на модела централно от OmniRoute.",
"antigravity": "Използвайте, когато трафикът на Antigravity/Kiro трябва да бъде прихванат чрез MITM и насочен към OmniRoute.",
"copilot": "Използвайте, когато искате UX в стил на чат Copilot, като същевременно налагате OmniRoute ключове и правила за маршрутизиране."
}
},
"combos": {
"title": "Комбота",
@@ -1087,7 +1105,13 @@
"resetAllTitle": "Нулирайте всички прекъсвачи в изправно състояние",
"resetting": "Нулиране...",
"resetAll": "Нулиране на всички",
"until": "До {time}"
"until": "До {time}",
"activeProviders": "{count} active",
"monitoredProviders": "{count} monitored",
"configuredProvidersLabel": "Конфигуриран в таблото за управление",
"configuredProvidersHint": "Доставчици с идентификационни данни, записани в /dashboard/providers, независимо от състоянието на изпълнение.",
"activeProvidersHint": "Конфигурираните доставчици в момента са активирани за заявки за маршрутизиране.",
"monitoredProvidersHint": "Доставчиците в момента се проследяват от монитори за изправност на прекъсвачи."
},
"limits": {
"title": "Ограничения и квоти",
@@ -1446,7 +1470,14 @@
"compatProtocolOpenAIResponses": "OpenAI Responses API",
"compatProtocolClaude": "Anthropic Messages",
"tokenRefreshed": "Token refreshed successfully",
"tokenRefreshFailed": "Token refresh failed"
"tokenRefreshFailed": "Token refresh failed",
"compatBadgeUpstreamHeaders": "Headers",
"compatUpstreamAddRow": "Add header",
"compatUpstreamHeaderName": "Header name",
"compatUpstreamHeaderValue": "Value",
"compatUpstreamHeadersHint": "High-privilege setting — same trust level as editing provider API credentials; only trusted admins should use it. Merged after OmniRoute adds auth from the provider API key. If a custom header uses the same name as an existing one (e.g. Authorization), your value fully replaces the auto-generated header (including the Bearer token) — the upstream only sees what you typed, not the key from settings. Misconfiguration can cause 401 or broken upstream auth. One row per header (e.g. extra Authentication for some gateways). Hover or focus the value to preview. Saves on blur, outside click, or closing this panel.",
"compatUpstreamHeadersLabel": "Extra upstream headers",
"compatUpstreamRemoveRow": "Remove row"
},
"settings": {
"title": "Настройки",
@@ -1849,7 +1880,13 @@
"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}"
"disableFingerprintTitle": "Disable fingerprint for {provider}",
"routingAdvancedGuideTitle": "Разширено ръководство за маршрутизиране",
"routingAdvancedGuideHint1": "Използвайте Fill First за предвидим приоритет, Round Robin за справедливост и P2C за устойчивост на забавяне.",
"routingAdvancedGuideHint2": "Ако доставчиците се различават по отношение на качество/цена, започнете с Cost Opt за фонова работа и Least Used за балансирано износване.",
"comboDefaultsGuideTitle": "Как да настроите настройките по подразбиране на комбинацията",
"comboDefaultsGuideHint1": "Поддържайте ниски повторни опити в потоци с ниска латентност; увеличете времето за изчакване само за задачи с дълго генериране.",
"comboDefaultsGuideHint2": "Използвайте замени на доставчика, когато един доставчик се нуждае от различно поведение при изчакване/повторен опит от глобалните настройки по подразбиране."
},
"translator": {
"title": "Преводач",
@@ -2569,7 +2606,15 @@
"opencodeDetected": "opencode {version} detected",
"opencodeDesc": "Generate a ready-to-use {configFile} with your OmniRoute base URL and all available models — drop it in your project root and run {command}.",
"downloadConfig": "Download {file}",
"downloaded": "Downloaded!"
"downloaded": "Downloaded!",
"setupGuideTitle": "Setup guide",
"openCliTools": "Open CLI Tools",
"setupGuideDetectCliTitle": "Detect installed CLIs",
"setupGuideDetectCliDesc": "Click Refresh after installing or updating a CLI so OmniRoute can rescan binaries and versions.",
"setupGuideCustomAgentTitle": "Register custom binary",
"setupGuideCustomAgentDesc": "Use Add Custom Agent when your CLI is not in the built-in list. Provide binary name and version command.",
"setupGuideCommandMissingTitle": "Fix 'command not found'",
"setupGuideCommandMissingDesc": "Ensure the CLI command exists in PATH, open a new terminal session, and rerun Refresh."
},
"autoCombo": {
"title": "Auto-Combo Engine",
@@ -2628,6 +2673,21 @@
"rerank": "Rerank",
"rerankModel": "Rerank Model",
"positionDelta": "Position Change",
"emptyState": "Send a search query to see results"
"emptyState": "Send a search query to see results",
"safeSearchOff": "Off",
"safeSearchModerate": "Moderate",
"safeSearchStrict": "Strict",
"queryPlaceholder": "Enter search query...",
"providerAuto": "auto (cheapest)",
"searchTypeWeb": "web",
"searchTypeNews": "news",
"optionAny": "any",
"timeRangeDay": "Past day",
"timeRangeWeek": "Past week",
"timeRangeMonth": "Past month",
"timeRangeYear": "Past year",
"domainPlaceholder": "example.com",
"requestTimedOut": "Request timed out ({seconds}s)",
"networkError": "Network error"
}
}

View File

@@ -56,7 +56,9 @@
"warning": "Varování",
"note": "Poznámka",
"free": "Uvolnit",
"skipToContent": "Přejít na obsah"
"skipToContent": "Přejít na obsah",
"maintenanceServerIssues": "Server is experiencing issues. Some features may be unavailable.",
"maintenanceServerUnreachable": "Server is unreachable. Reconnecting..."
},
"sidebar": {
"home": "Domov",
@@ -364,7 +366,22 @@
"rerank": "Přeřadit",
"rerankModel": "Přeřazovací Model (Rerank)",
"positionDelta": "Změna pozice",
"emptyState": "Odešlete vyhledávací dotaz pro zobrazení výsledků"
"emptyState": "Odešlete vyhledávací dotaz pro zobrazení výsledků",
"safeSearchOff": "Off",
"safeSearchModerate": "Moderate",
"safeSearchStrict": "Strict",
"queryPlaceholder": "Enter search query...",
"providerAuto": "auto (cheapest)",
"searchTypeWeb": "web",
"searchTypeNews": "news",
"optionAny": "any",
"timeRangeDay": "Past day",
"timeRangeWeek": "Past week",
"timeRangeMonth": "Past month",
"timeRangeYear": "Past year",
"domainPlaceholder": "example.com",
"requestTimedOut": "Request timed out ({seconds}s)",
"networkError": "Network error"
},
"cliTools": {
"title": "CLI Nástroje",
@@ -612,7 +629,23 @@
"mitmStep1": "Přejděte na stránku Správce API",
"mitmStep2Prefix": "Zadejte prefix, např. cc/",
"mitmStep2Suffix": "a cílový model jako claude-sonnet-4-20250514",
"mitmStep3": "Uložte a restartujte server"
"mitmStep3": "Uložte a restartujte server",
"whenToUseLabel": "Kdy použít",
"openToolDocs": "Otevřete dokumentaci nástroje",
"toolUseCases": {
"claude": "Použijte, když chcete silné plánovací pracovní postupy a dlouhé vícesouborové refaktory s Claude Code.",
"codex": "Použijte, když je váš tým standardizován na tocích OpenAI Codex CLI a ověřování na základě profilu.",
"droid": "Použijte, když potřebujete lehkého terminálového agenta zaměřeného na rychlé kódování a smyčky provádění příkazů.",
"openclaw": "Použijte, když chcete kódovacího agenta ve stylu Open Claw, ale směrovaného přes zásady OmniRoute.",
"cline": "Použijte, když konfigurujete kódovací agenty v editorech a chcete řízené nastavení s modely OmniRoute.",
"kilo": "Použijte, když váš pracovní postup závisí na příkazech Kilo Code a rychlých iterativních úpravách.",
"cursor": "Použijte při kódování v Cursoru a potřebujete vlastní modely kompatibilní s OpenAI prostřednictvím OmniRoute.",
"continue": "Použijte, když spouštíte Pokračovat v IDE a potřebujete přenosnou konfiguraci poskytovatele na bázi JSON.",
"opencode": "Použijte, pokud dáváte přednost spouštění nativního agenta a skriptované automatizaci prostřednictvím OpenCode.",
"kiro": "Použijte při integraci Kiro a centrálním řízení směrování modelu z OmniRoute.",
"antigravity": "Použijte, když provoz Antigravity/Kiro musí být zachycen prostřednictvím MITM a směrován do OmniRoute.",
"copilot": "Použijte, když chcete uživatelské rozhraní ve stylu chatu Copilot při vynucování klíčů OmniRoute a pravidel směrování."
}
},
"combos": {
"title": "Komba",
@@ -1135,7 +1168,13 @@
"resetAllTitle": "Resetujte všechny jističe do funkčního stavu",
"resetting": "Resetování...",
"resetAll": "Obnovit vše",
"until": "Do {time}"
"until": "Do {time}",
"activeProviders": "{count} active",
"monitoredProviders": "{count} monitored",
"configuredProvidersLabel": "Nakonfigurováno v řídicím panelu",
"configuredProvidersHint": "Poskytovatelé s přihlašovacími údaji uloženými v /dashboard/providers, bez ohledu na stav běhu.",
"activeProvidersHint": "Konfigurovaní poskytovatelé aktuálně povoleni pro požadavky na směrování.",
"monitoredProvidersHint": "Poskytovatelé aktuálně sledovaní monitory stavu vypínačů."
},
"limits": {
"title": "Limity a kvóty",
@@ -1494,7 +1533,14 @@
"compatProtocolHint": "Formát požadavku",
"compatProtocolOpenAI": "OpenAI",
"compatProtocolOpenAIResponses": "OpenAI Responses",
"compatProtocolClaude": "Anthropic Messages"
"compatProtocolClaude": "Anthropic Messages",
"compatBadgeUpstreamHeaders": "Headers",
"compatUpstreamAddRow": "Add header",
"compatUpstreamHeaderName": "Header name",
"compatUpstreamHeaderValue": "Value",
"compatUpstreamHeadersHint": "High-privilege setting — same trust level as editing provider API credentials; only trusted admins should use it. Merged after OmniRoute adds auth from the provider API key. If a custom header uses the same name as an existing one (e.g. Authorization), your value fully replaces the auto-generated header (including the Bearer token) — the upstream only sees what you typed, not the key from settings. Misconfiguration can cause 401 or broken upstream auth. One row per header (e.g. extra Authentication for some gateways). Hover or focus the value to preview. Saves on blur, outside click, or closing this panel.",
"compatUpstreamHeadersLabel": "Extra upstream headers",
"compatUpstreamRemoveRow": "Remove row"
},
"settings": {
"title": "Nastavení",
@@ -1897,7 +1943,13 @@
"customPricingNote": "Výchozí ceny pro konkrétní modely můžete přepsat. Vlastní přepsání má přednost před automaticky zjištěnými cenami.",
"editPricing": "Upravit ceny",
"viewFullDetails": "Zobrazit všechny podrobnosti",
"themeCoral": "Korál"
"themeCoral": "Korál",
"routingAdvancedGuideTitle": "Pokročilé vedení trasy",
"routingAdvancedGuideHint1": "Použijte Fill First pro předvídatelnou prioritu, Round Robin pro spravedlnost a P2C pro odolnost vůči latenci.",
"routingAdvancedGuideHint2": "Pokud se poskytovatelé liší v kvalitě/ceně, začněte s Cost Opt pro práci na pozadí a Nejméně použité pro vyvážené opotřebení.",
"comboDefaultsGuideTitle": "Jak vyladit výchozí nastavení komba",
"comboDefaultsGuideHint1": "Udržujte počet opakování na nízké úrovni v tocích s nízkou latencí; prodlužte časový limit pouze u úloh s dlouhým generováním.",
"comboDefaultsGuideHint2": "Použít přepsání poskytovatele, když jeden poskytovatel potřebuje jiné chování časového limitu/opakování než globální výchozí hodnoty."
},
"translator": {
"title": "Překladatel",
@@ -2605,7 +2657,15 @@
"opencodeDetected": "opencode {version} detected",
"opencodeDesc": "Generate a ready-to-use {configFile} with your OmniRoute base URL and all available models — drop it in your project root and run {command}.",
"downloadConfig": "Download {file}",
"downloaded": "Downloaded!"
"downloaded": "Downloaded!",
"setupGuideTitle": "Setup guide",
"openCliTools": "Open CLI Tools",
"setupGuideDetectCliTitle": "Detect installed CLIs",
"setupGuideDetectCliDesc": "Click Refresh after installing or updating a CLI so OmniRoute can rescan binaries and versions.",
"setupGuideCustomAgentTitle": "Register custom binary",
"setupGuideCustomAgentDesc": "Use Add Custom Agent when your CLI is not in the built-in list. Provide binary name and version command.",
"setupGuideCommandMissingTitle": "Fix 'command not found'",
"setupGuideCommandMissingDesc": "Ensure the CLI command exists in PATH, open a new terminal session, and rerun Refresh."
},
"autoCombo": {
"title": "Auto-Kombo Engine",

View File

@@ -56,7 +56,9 @@
"warning": "Advarsel",
"note": "Bemærk",
"free": "Gratis",
"skipToContent": "Gå til indhold"
"skipToContent": "Gå til indhold",
"maintenanceServerIssues": "Server is experiencing issues. Some features may be unavailable.",
"maintenanceServerUnreachable": "Server is unreachable. Reconnecting..."
},
"sidebar": {
"home": "Hjem",
@@ -576,7 +578,23 @@
"mitmStep1": "1. Start MITM to route requests through OmniRoute.",
"mitmStep2Prefix": "2. Add",
"mitmStep2Suffix": "to your hosts file as 127.0.0.1.",
"mitmStep3": "3. Open {toolName} and requests will be proxied."
"mitmStep3": "3. Open {toolName} and requests will be proxied.",
"whenToUseLabel": "Hvornår skal bruges",
"openToolDocs": "Åbn værktøjsdokumenter",
"toolUseCases": {
"claude": "Brug, når du ønsker stærke planlægnings-arbejdsgange og lange multi-fil-refaktorer med Claude Code.",
"codex": "Bruges, når dit team er standardiseret på OpenAI Codex CLI-flows og profilbaseret godkendelse.",
"droid": "Brug, når du har brug for en letvægtsterminalagent med fokus på hurtig kodning og kommandoudførelsesløkker.",
"openclaw": "Brug, når du vil have en Open Claw-stil-kodningsagent, men dirigeret gennem OmniRoute-politikker.",
"cline": "Bruges, når du konfigurerer kodningsagenter i editorer og ønsker guidet opsætning med OmniRoute-modeller.",
"kilo": "Brug, når dit arbejdsflow afhænger af Kilo Code-kommandoer og hurtige iterative redigeringer.",
"cursor": "Brug, når du koder i Cursor, og du har brug for brugerdefinerede OpenAI-kompatible modeller gennem OmniRoute.",
"continue": "Brug, når du kører Continue i IDE'er, og du har brug for bærbar JSON-baseret udbyderkonfiguration.",
"opencode": "Brug, når du foretrækker terminal-native agentkørsler og scriptet automatisering via OpenCode.",
"kiro": "Bruges ved integration af Kiro og styring af modelrouting centralt fra OmniRoute.",
"antigravity": "Bruges, når Antigravity/Kiro-trafik skal opsnappes gennem MITM og dirigeres til OmniRoute.",
"copilot": "Brug, når du ønsker Copilot-chatstil UX, mens du håndhæver OmniRoute-nøgler og routingregler."
}
},
"combos": {
"title": "Combos",
@@ -1087,7 +1105,13 @@
"resetAllTitle": "Nulstil alle afbrydere til sund tilstand",
"resetting": "Nulstiller...",
"resetAll": "Nulstil alle",
"until": "Indtil {time}"
"until": "Indtil {time}",
"activeProviders": "{count} active",
"monitoredProviders": "{count} monitored",
"configuredProvidersLabel": "Konfigureret i dashboard",
"configuredProvidersHint": "Udbydere med legitimationsoplysninger gemt i /dashboard/udbydere, uanset køretidstilstand.",
"activeProvidersHint": "Konfigurerede udbydere, der i øjeblikket er aktiveret for routinganmodninger.",
"monitoredProvidersHint": "Udbydere, der i øjeblikket spores af strømafbryderens sundhedsmonitorer."
},
"limits": {
"title": "Grænser og kvoter",
@@ -1446,7 +1470,14 @@
"compatProtocolOpenAIResponses": "OpenAI Responses API",
"compatProtocolClaude": "Anthropic Messages",
"tokenRefreshed": "Token refreshed successfully",
"tokenRefreshFailed": "Token refresh failed"
"tokenRefreshFailed": "Token refresh failed",
"compatBadgeUpstreamHeaders": "Headers",
"compatUpstreamAddRow": "Add header",
"compatUpstreamHeaderName": "Header name",
"compatUpstreamHeaderValue": "Value",
"compatUpstreamHeadersHint": "High-privilege setting — same trust level as editing provider API credentials; only trusted admins should use it. Merged after OmniRoute adds auth from the provider API key. If a custom header uses the same name as an existing one (e.g. Authorization), your value fully replaces the auto-generated header (including the Bearer token) — the upstream only sees what you typed, not the key from settings. Misconfiguration can cause 401 or broken upstream auth. One row per header (e.g. extra Authentication for some gateways). Hover or focus the value to preview. Saves on blur, outside click, or closing this panel.",
"compatUpstreamHeadersLabel": "Extra upstream headers",
"compatUpstreamRemoveRow": "Remove row"
},
"settings": {
"title": "Indstillinger",
@@ -1849,7 +1880,13 @@
"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}"
"disableFingerprintTitle": "Disable fingerprint for {provider}",
"routingAdvancedGuideTitle": "Avanceret rutevejledning",
"routingAdvancedGuideHint1": "Brug Fill First for forudsigelig prioritet, Round Robin for retfærdighed og P2C for latensmodstandsdygtighed.",
"routingAdvancedGuideHint2": "Hvis udbydere varierer i kvalitet/omkostninger, start med Cost Opt for baggrundsarbejde og Mindst brugt for balanceret slid.",
"comboDefaultsGuideTitle": "Sådan indstiller du combo-standarder",
"comboDefaultsGuideHint1": "Hold lave genforsøg i flows med lav latens; øg kun timeout for lange generationsopgaver.",
"comboDefaultsGuideHint2": "Brug udbydertilsidesættelser, når en udbyder har brug for en anden timeout-/genforsøgsadfærd end globale standardindstillinger."
},
"translator": {
"title": "Oversætter",
@@ -2569,7 +2606,15 @@
"opencodeDetected": "opencode {version} detected",
"opencodeDesc": "Generate a ready-to-use {configFile} with your OmniRoute base URL and all available models — drop it in your project root and run {command}.",
"downloadConfig": "Download {file}",
"downloaded": "Downloaded!"
"downloaded": "Downloaded!",
"setupGuideTitle": "Setup guide",
"openCliTools": "Open CLI Tools",
"setupGuideDetectCliTitle": "Detect installed CLIs",
"setupGuideDetectCliDesc": "Click Refresh after installing or updating a CLI so OmniRoute can rescan binaries and versions.",
"setupGuideCustomAgentTitle": "Register custom binary",
"setupGuideCustomAgentDesc": "Use Add Custom Agent when your CLI is not in the built-in list. Provide binary name and version command.",
"setupGuideCommandMissingTitle": "Fix 'command not found'",
"setupGuideCommandMissingDesc": "Ensure the CLI command exists in PATH, open a new terminal session, and rerun Refresh."
},
"autoCombo": {
"title": "Auto-Combo Engine",
@@ -2628,6 +2673,21 @@
"rerank": "Rerank",
"rerankModel": "Rerank Model",
"positionDelta": "Position Change",
"emptyState": "Send a search query to see results"
"emptyState": "Send a search query to see results",
"safeSearchOff": "Off",
"safeSearchModerate": "Moderate",
"safeSearchStrict": "Strict",
"queryPlaceholder": "Enter search query...",
"providerAuto": "auto (cheapest)",
"searchTypeWeb": "web",
"searchTypeNews": "news",
"optionAny": "any",
"timeRangeDay": "Past day",
"timeRangeWeek": "Past week",
"timeRangeMonth": "Past month",
"timeRangeYear": "Past year",
"domainPlaceholder": "example.com",
"requestTimedOut": "Request timed out ({seconds}s)",
"networkError": "Network error"
}
}

View File

@@ -56,7 +56,9 @@
"warning": "Warnung",
"note": "Hinweis",
"free": "Kostenlos",
"skipToContent": "Zum Inhalt springen"
"skipToContent": "Zum Inhalt springen",
"maintenanceServerIssues": "Server is experiencing issues. Some features may be unavailable.",
"maintenanceServerUnreachable": "Server is unreachable. Reconnecting..."
},
"sidebar": {
"home": "Zuhause",
@@ -576,7 +578,23 @@
"mitmStep1": "1. Start MITM to route requests through OmniRoute.",
"mitmStep2Prefix": "2. Add",
"mitmStep2Suffix": "to your hosts file as 127.0.0.1.",
"mitmStep3": "3. Open {toolName} and requests will be proxied."
"mitmStep3": "3. Open {toolName} and requests will be proxied.",
"whenToUseLabel": "Wann zu verwenden",
"openToolDocs": "Tool-Dokumente öffnen",
"toolUseCases": {
"claude": "Verwenden Sie Claude Code, wenn Sie starke Planungsworkflows und lange Umgestaltungen mehrerer Dateien wünschen.",
"codex": "Verwenden Sie es, wenn Ihr Team auf OpenAI Codex CLI-Flows und profilbasierte Authentifizierung standardisiert ist.",
"droid": "Verwenden Sie ihn, wenn Sie einen kompakten Terminalagenten benötigen, der sich auf schnelle Codierung und Befehlsausführungsschleifen konzentriert.",
"openclaw": "Verwenden Sie diese Option, wenn Sie einen Codierungsagenten im Open-Claw-Stil wünschen, der jedoch über OmniRoute-Richtlinien weitergeleitet wird.",
"cline": "Verwenden Sie diese Option, wenn Sie Codierungsagenten in Editoren konfigurieren und eine geführte Einrichtung mit OmniRoute-Modellen wünschen.",
"kilo": "Verwenden Sie diese Option, wenn Ihr Arbeitsablauf auf Kilo-Code-Befehle und schnelle iterative Bearbeitungen angewiesen ist.",
"cursor": "Verwenden Sie diese Option, wenn Sie in Cursor codieren und benutzerdefinierte OpenAI-kompatible Modelle über OmniRoute benötigen.",
"continue": "Verwenden Sie diese Option, wenn Sie Continue in IDEs ausführen und eine portable JSON-basierte Anbieterkonfiguration benötigen.",
"opencode": "Verwenden Sie diese Option, wenn Sie terminalnative Agentenausführungen und Skriptautomatisierung über OpenCode bevorzugen.",
"kiro": "Zur Verwendung bei der Integration von Kiro und der zentralen Steuerung des Modellroutings über OmniRoute.",
"antigravity": "Wird verwendet, wenn Antigravity/Kiro-Verkehr über MITM abgefangen und an OmniRoute weitergeleitet werden muss.",
"copilot": "Verwenden Sie diese Option, wenn Sie eine UX im Copilot-Chat-Stil wünschen und gleichzeitig OmniRoute-Schlüssel und Routing-Regeln durchsetzen möchten."
}
},
"combos": {
"title": "Kombinationen",
@@ -1087,7 +1105,13 @@
"resetAllTitle": "Setzen Sie alle Leistungsschalter in den fehlerfreien Zustand zurück",
"resetting": "Zurücksetzen...",
"resetAll": "Alles zurücksetzen",
"until": "Bis {time}"
"until": "Bis {time}",
"activeProviders": "{count} active",
"monitoredProviders": "{count} monitored",
"configuredProvidersLabel": "Im Dashboard konfiguriert",
"configuredProvidersHint": "Anbieter mit in /dashboard/providers gespeicherten Anmeldeinformationen, unabhängig vom Laufzeitstatus.",
"activeProvidersHint": "Konfigurierte Anbieter, die derzeit für Routing-Anfragen aktiviert sind.",
"monitoredProvidersHint": "Anbieter, die derzeit von Leistungsschalter-Zustandsmonitoren überwacht werden."
},
"limits": {
"title": "Limits und Quoten",
@@ -1446,7 +1470,14 @@
"compatProtocolOpenAIResponses": "OpenAI Responses API",
"compatProtocolClaude": "Anthropic Messages",
"tokenRefreshed": "Token refreshed successfully",
"tokenRefreshFailed": "Token refresh failed"
"tokenRefreshFailed": "Token refresh failed",
"compatBadgeUpstreamHeaders": "Headers",
"compatUpstreamAddRow": "Add header",
"compatUpstreamHeaderName": "Header name",
"compatUpstreamHeaderValue": "Value",
"compatUpstreamHeadersHint": "High-privilege setting — same trust level as editing provider API credentials; only trusted admins should use it. Merged after OmniRoute adds auth from the provider API key. If a custom header uses the same name as an existing one (e.g. Authorization), your value fully replaces the auto-generated header (including the Bearer token) — the upstream only sees what you typed, not the key from settings. Misconfiguration can cause 401 or broken upstream auth. One row per header (e.g. extra Authentication for some gateways). Hover or focus the value to preview. Saves on blur, outside click, or closing this panel.",
"compatUpstreamHeadersLabel": "Extra upstream headers",
"compatUpstreamRemoveRow": "Remove row"
},
"settings": {
"title": "Einstellungen",
@@ -1849,7 +1880,13 @@
"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}"
"disableFingerprintTitle": "Disable fingerprint for {provider}",
"routingAdvancedGuideTitle": "Erweiterte Routenführung",
"routingAdvancedGuideHint1": "Verwenden Sie Fill First für vorhersehbare Priorität, Round Robin für Fairness und P2C für Latenzstabilität.",
"routingAdvancedGuideHint2": "Wenn sich die Qualität/Kosten der Anbieter unterscheiden, beginnen Sie mit „Cost Opt“ für Hintergrundarbeit und „Least Used“ für ausgewogene Abnutzung.",
"comboDefaultsGuideTitle": "So optimieren Sie die Combo-Standardeinstellungen",
"comboDefaultsGuideHint1": "Halten Sie die Wiederholungsversuche bei Datenflüssen mit geringer Latenz gering. Erhöhen Sie das Timeout nur für Aufgaben mit langer Generierung.",
"comboDefaultsGuideHint2": "Verwenden Sie Anbieterüberschreibungen, wenn ein Anbieter ein anderes Timeout-/Wiederholungsverhalten als die globalen Standardwerte benötigt."
},
"translator": {
"title": "Übersetzer",
@@ -2569,7 +2606,15 @@
"opencodeDetected": "opencode {version} detected",
"opencodeDesc": "Generate a ready-to-use {configFile} with your OmniRoute base URL and all available models — drop it in your project root and run {command}.",
"downloadConfig": "Download {file}",
"downloaded": "Downloaded!"
"downloaded": "Downloaded!",
"setupGuideTitle": "Setup guide",
"openCliTools": "Open CLI Tools",
"setupGuideDetectCliTitle": "Detect installed CLIs",
"setupGuideDetectCliDesc": "Click Refresh after installing or updating a CLI so OmniRoute can rescan binaries and versions.",
"setupGuideCustomAgentTitle": "Register custom binary",
"setupGuideCustomAgentDesc": "Use Add Custom Agent when your CLI is not in the built-in list. Provide binary name and version command.",
"setupGuideCommandMissingTitle": "Fix 'command not found'",
"setupGuideCommandMissingDesc": "Ensure the CLI command exists in PATH, open a new terminal session, and rerun Refresh."
},
"autoCombo": {
"title": "Auto-Combo Engine",
@@ -2628,6 +2673,21 @@
"rerank": "Rerank",
"rerankModel": "Rerank Model",
"positionDelta": "Position Change",
"emptyState": "Send a search query to see results"
"emptyState": "Send a search query to see results",
"safeSearchOff": "Off",
"safeSearchModerate": "Moderate",
"safeSearchStrict": "Strict",
"queryPlaceholder": "Enter search query...",
"providerAuto": "auto (cheapest)",
"searchTypeWeb": "web",
"searchTypeNews": "news",
"optionAny": "any",
"timeRangeDay": "Past day",
"timeRangeWeek": "Past week",
"timeRangeMonth": "Past month",
"timeRangeYear": "Past year",
"domainPlaceholder": "example.com",
"requestTimedOut": "Request timed out ({seconds}s)",
"networkError": "Network error"
}
}

View File

@@ -56,7 +56,9 @@
"warning": "Warning",
"note": "Note",
"free": "Free",
"skipToContent": "Skip to content"
"skipToContent": "Skip to content",
"maintenanceServerIssues": "Server is experiencing issues. Some features may be unavailable.",
"maintenanceServerUnreachable": "Server is unreachable. Reconnecting..."
},
"sidebar": {
"home": "Home",
@@ -354,6 +356,21 @@
"includeDomains": "Include Domains",
"excludeDomains": "Exclude Domains",
"safeSearch": "Safe Search",
"safeSearchOff": "Off",
"safeSearchModerate": "Moderate",
"safeSearchStrict": "Strict",
"queryPlaceholder": "Enter search query...",
"providerAuto": "auto (cheapest)",
"searchTypeWeb": "web",
"searchTypeNews": "news",
"optionAny": "any",
"timeRangeDay": "Past day",
"timeRangeWeek": "Past week",
"timeRangeMonth": "Past month",
"timeRangeYear": "Past year",
"domainPlaceholder": "example.com",
"requestTimedOut": "Request timed out ({seconds}s)",
"networkError": "Network error",
"formatted": "Formatted",
"rawJson": "JSON",
"cacheMiss": "cache miss",
@@ -515,6 +532,22 @@
"openClawManualConfiguration": "Open Claw - Manual Configuration",
"clineManualConfiguration": "Cline Manual Configuration",
"kiloManualConfiguration": "Kilo Code Manual Configuration",
"whenToUseLabel": "When to use",
"openToolDocs": "Open tool docs",
"toolUseCases": {
"claude": "Use when you want strong planning workflows and long multi-file refactors with Claude Code.",
"codex": "Use when your team is standardized on OpenAI Codex CLI flows and profile-based auth.",
"droid": "Use when you need a lightweight terminal agent focused on fast coding and command execution loops.",
"openclaw": "Use when you want an Open Claw style coding agent but routed through OmniRoute policies.",
"cline": "Use when you configure coding agents inside editors and want guided setup with OmniRoute models.",
"kilo": "Use when your workflow depends on Kilo Code commands and fast iterative edits.",
"cursor": "Use when coding in Cursor and you need custom OpenAI-compatible models through OmniRoute.",
"continue": "Use when running Continue in IDEs and you need portable JSON-based provider configuration.",
"opencode": "Use when you prefer terminal-native agent runs and scripted automation via OpenCode.",
"kiro": "Use when integrating Kiro and controlling model routing centrally from OmniRoute.",
"antigravity": "Use when Antigravity/Kiro traffic must be intercepted through MITM and routed to OmniRoute.",
"copilot": "Use when you want Copilot chat style UX while enforcing OmniRoute keys and routing rules."
},
"toolDescriptions": {
"antigravity": "Google Antigravity IDE with MITM",
"claude": "Anthropic Claude Code CLI",
@@ -1117,6 +1150,12 @@
"issuesLabel": "Issues Detected",
"operational": "Operational",
"providers": "Providers",
"configuredProvidersLabel": "Configured in dashboard",
"configuredProvidersHint": "Providers with credentials saved in /dashboard/providers, regardless of runtime state.",
"activeProviders": "{count} active",
"activeProvidersHint": "Configured providers currently enabled for routing requests.",
"monitoredProviders": "{count} monitored",
"monitoredProvidersHint": "Providers currently tracked by circuit-breaker health monitors.",
"healthyCount": "{count} healthy",
"nodeVersion": "Node {version}",
"failures": "{count} failure",
@@ -1443,6 +1482,13 @@
"compatProtocolOpenAI": "OpenAI Chat Completions",
"compatProtocolOpenAIResponses": "OpenAI Responses API",
"compatProtocolClaude": "Anthropic Messages",
"compatUpstreamHeadersLabel": "Extra upstream headers",
"compatUpstreamHeadersHint": "High-privilege setting — same trust level as editing provider API credentials; only trusted admins should use it. Merged after OmniRoute adds auth from the provider API key. If a custom header uses the same name as an existing one (e.g. Authorization), your value fully replaces the auto-generated header (including the Bearer token) — the upstream only sees what you typed, not the key from settings. Misconfiguration can cause 401 or broken upstream auth. One row per header (e.g. extra Authentication for some gateways). Hover or focus the value to preview. Saves on blur, outside click, or closing this panel.",
"compatUpstreamHeaderName": "Header name",
"compatUpstreamHeaderValue": "Value",
"compatUpstreamAddRow": "Add header",
"compatUpstreamRemoveRow": "Remove row",
"compatBadgeUpstreamHeaders": "Headers",
"modelId": "Model ID",
"customModelPlaceholder": "e.g. gpt-4.5-turbo",
"loading": "Loading...",
@@ -1645,6 +1691,9 @@
"enableFingerprintTitle": "Enable fingerprint for {provider}",
"disableFingerprintTitle": "Disable fingerprint for {provider}",
"routingStrategy": "Routing Strategy",
"routingAdvancedGuideTitle": "Advanced routing guidance",
"routingAdvancedGuideHint1": "Use Fill First for predictable priority, Round Robin for fairness, and P2C for latency resilience.",
"routingAdvancedGuideHint2": "If providers vary in quality/cost, start with Cost Opt for background work and Least Used for balanced wear.",
"fillFirst": "Fill First",
"fillFirstDesc": "Use accounts in priority order",
"roundRobin": "Round Robin",
@@ -1739,6 +1788,9 @@
"fillModelAndProviders": "Please fill model name and providers",
"addAtLeastOneProvider": "Add at least one provider",
"comboDefaultsTitle": "Combo Defaults",
"comboDefaultsGuideTitle": "How to tune combo defaults",
"comboDefaultsGuideHint1": "Keep retries low in low-latency flows; increase timeout only for long generation tasks.",
"comboDefaultsGuideHint2": "Use provider overrides when one provider needs different timeout/retry behavior than global defaults.",
"globalComboConfig": "Global combo configuration",
"defaultStrategy": "Default Strategy",
"defaultStrategyDesc": "Applied to new combos without explicit strategy",
@@ -2605,7 +2657,15 @@
"opencodeDetected": "opencode {version} detected",
"opencodeDesc": "Generate a ready-to-use {configFile} with your OmniRoute base URL and all available models — drop it in your project root and run {command}.",
"downloadConfig": "Download {file}",
"downloaded": "Downloaded!"
"downloaded": "Downloaded!",
"setupGuideTitle": "Setup guide",
"openCliTools": "Open CLI Tools",
"setupGuideDetectCliTitle": "Detect installed CLIs",
"setupGuideDetectCliDesc": "Click Refresh after installing or updating a CLI so OmniRoute can rescan binaries and versions.",
"setupGuideCustomAgentTitle": "Register custom binary",
"setupGuideCustomAgentDesc": "Use Add Custom Agent when your CLI is not in the built-in list. Provide binary name and version command.",
"setupGuideCommandMissingTitle": "Fix 'command not found'",
"setupGuideCommandMissingDesc": "Ensure the CLI command exists in PATH, open a new terminal session, and rerun Refresh."
},
"autoCombo": {
"title": "Auto-Combo Engine",

View File

@@ -56,7 +56,9 @@
"warning": "Advertencia",
"note": "Nota",
"free": "Gratis",
"skipToContent": "Saltar al contenido"
"skipToContent": "Saltar al contenido",
"maintenanceServerIssues": "Server is experiencing issues. Some features may be unavailable.",
"maintenanceServerUnreachable": "Server is unreachable. Reconnecting..."
},
"sidebar": {
"home": "Inicio",
@@ -576,7 +578,23 @@
"mitmStep1": "1. Start MITM to route requests through OmniRoute.",
"mitmStep2Prefix": "2. Add",
"mitmStep2Suffix": "to your hosts file as 127.0.0.1.",
"mitmStep3": "3. Open {toolName} and requests will be proxied."
"mitmStep3": "3. Open {toolName} and requests will be proxied.",
"whenToUseLabel": "cuando usar",
"openToolDocs": "Abrir documentos de herramientas",
"toolUseCases": {
"claude": "Úselo cuando desee flujos de trabajo de planificación sólidos y refactorizaciones largas de varios archivos con Claude Code.",
"codex": "Úselo cuando su equipo esté estandarizado en los flujos CLI de OpenAI Codex y la autenticación basada en perfiles.",
"droid": "Úselo cuando necesite un agente terminal liviano enfocado en codificación rápida y bucles de ejecución de comandos.",
"openclaw": "Úselo cuando desee un agente de codificación estilo Open Claw pero enrutado a través de políticas de OmniRoute.",
"cline": "Úselo cuando configure agentes de codificación dentro de editores y desee una configuración guiada con modelos OmniRoute.",
"kilo": "Úselo cuando su flujo de trabajo dependa de los comandos de Kilo Code y de ediciones iterativas rápidas.",
"cursor": "Úselo cuando codifique en Cursor y necesite modelos personalizados compatibles con OpenAI a través de OmniRoute.",
"continue": "Úselo cuando ejecute Continuar en IDE y necesite una configuración de proveedor portátil basada en JSON.",
"opencode": "Úselo cuando prefiera ejecuciones de agentes nativos de terminal y automatización con scripts a través de OpenCode.",
"kiro": "Utilícelo al integrar Kiro y controlar el enrutamiento de modelos de forma centralizada desde OmniRoute.",
"antigravity": "Úselo cuando el tráfico de Antigravity/Kiro debe interceptarse a través de MITM y enrutarse a OmniRoute.",
"copilot": "Úselo cuando desee una experiencia de usuario estilo chat Copilot mientras aplica las claves de OmniRoute y las reglas de enrutamiento."
}
},
"combos": {
"title": "combos",
@@ -1087,7 +1105,13 @@
"resetAllTitle": "Restablezca todos los disyuntores al estado saludable",
"resetting": "Restableciendo...",
"resetAll": "Restablecer todo",
"until": "Hasta {time}"
"until": "Hasta {time}",
"activeProviders": "{count} active",
"monitoredProviders": "{count} monitored",
"configuredProvidersLabel": "Configurado en el tablero",
"configuredProvidersHint": "Proveedores con credenciales guardadas en /dashboard/providers, independientemente del estado de ejecución.",
"activeProvidersHint": "Proveedores configurados actualmente habilitados para enrutar solicitudes.",
"monitoredProvidersHint": "Proveedores actualmente rastreados por monitores de estado de interruptores."
},
"limits": {
"title": "Límites y cuotas",
@@ -1446,7 +1470,14 @@
"compatProtocolOpenAIResponses": "OpenAI Responses API",
"compatProtocolClaude": "Anthropic Messages",
"tokenRefreshed": "Token refreshed successfully",
"tokenRefreshFailed": "Token refresh failed"
"tokenRefreshFailed": "Token refresh failed",
"compatBadgeUpstreamHeaders": "Headers",
"compatUpstreamAddRow": "Add header",
"compatUpstreamHeaderName": "Header name",
"compatUpstreamHeaderValue": "Value",
"compatUpstreamHeadersHint": "High-privilege setting — same trust level as editing provider API credentials; only trusted admins should use it. Merged after OmniRoute adds auth from the provider API key. If a custom header uses the same name as an existing one (e.g. Authorization), your value fully replaces the auto-generated header (including the Bearer token) — the upstream only sees what you typed, not the key from settings. Misconfiguration can cause 401 or broken upstream auth. One row per header (e.g. extra Authentication for some gateways). Hover or focus the value to preview. Saves on blur, outside click, or closing this panel.",
"compatUpstreamHeadersLabel": "Extra upstream headers",
"compatUpstreamRemoveRow": "Remove row"
},
"settings": {
"title": "Configuración",
@@ -1849,7 +1880,13 @@
"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}"
"disableFingerprintTitle": "Disable fingerprint for {provider}",
"routingAdvancedGuideTitle": "Guía de ruta avanzada",
"routingAdvancedGuideHint1": "Utilice Fill First para una prioridad predecible, Round Robin para equidad y P2C para resistencia a la latencia.",
"routingAdvancedGuideHint2": "Si los proveedores varían en calidad/costo, comience con Opción de costo para trabajo en segundo plano y Menos usado para desgaste equilibrado.",
"comboDefaultsGuideTitle": "Cómo ajustar los valores predeterminados del combo",
"comboDefaultsGuideHint1": "Mantenga bajos los reintentos en flujos de baja latencia; aumente el tiempo de espera solo para tareas de larga generación.",
"comboDefaultsGuideHint2": "Utilice anulaciones de proveedores cuando un proveedor necesite un comportamiento de tiempo de espera/reintento diferente al de los valores predeterminados globales."
},
"translator": {
"title": "Traductor",
@@ -2569,7 +2606,15 @@
"opencodeDetected": "opencode {version} detected",
"opencodeDesc": "Generate a ready-to-use {configFile} with your OmniRoute base URL and all available models — drop it in your project root and run {command}.",
"downloadConfig": "Download {file}",
"downloaded": "Downloaded!"
"downloaded": "Downloaded!",
"setupGuideTitle": "Setup guide",
"openCliTools": "Open CLI Tools",
"setupGuideDetectCliTitle": "Detect installed CLIs",
"setupGuideDetectCliDesc": "Click Refresh after installing or updating a CLI so OmniRoute can rescan binaries and versions.",
"setupGuideCustomAgentTitle": "Register custom binary",
"setupGuideCustomAgentDesc": "Use Add Custom Agent when your CLI is not in the built-in list. Provide binary name and version command.",
"setupGuideCommandMissingTitle": "Fix 'command not found'",
"setupGuideCommandMissingDesc": "Ensure the CLI command exists in PATH, open a new terminal session, and rerun Refresh."
},
"autoCombo": {
"title": "Auto-Combo Engine",
@@ -2628,6 +2673,21 @@
"rerank": "Rerank",
"rerankModel": "Rerank Model",
"positionDelta": "Position Change",
"emptyState": "Send a search query to see results"
"emptyState": "Send a search query to see results",
"safeSearchOff": "Off",
"safeSearchModerate": "Moderate",
"safeSearchStrict": "Strict",
"queryPlaceholder": "Enter search query...",
"providerAuto": "auto (cheapest)",
"searchTypeWeb": "web",
"searchTypeNews": "news",
"optionAny": "any",
"timeRangeDay": "Past day",
"timeRangeWeek": "Past week",
"timeRangeMonth": "Past month",
"timeRangeYear": "Past year",
"domainPlaceholder": "example.com",
"requestTimedOut": "Request timed out ({seconds}s)",
"networkError": "Network error"
}
}

View File

@@ -56,7 +56,9 @@
"warning": "Varoitus",
"note": "Huom",
"free": "Ilmainen",
"skipToContent": "Siirry sisältöön"
"skipToContent": "Siirry sisältöön",
"maintenanceServerIssues": "Server is experiencing issues. Some features may be unavailable.",
"maintenanceServerUnreachable": "Server is unreachable. Reconnecting..."
},
"sidebar": {
"home": "Kotiin",
@@ -576,7 +578,23 @@
"mitmStep1": "1. Start MITM to route requests through OmniRoute.",
"mitmStep2Prefix": "2. Add",
"mitmStep2Suffix": "to your hosts file as 127.0.0.1.",
"mitmStep3": "3. Open {toolName} and requests will be proxied."
"mitmStep3": "3. Open {toolName} and requests will be proxied.",
"whenToUseLabel": "Milloin käyttää",
"openToolDocs": "Avaa työkaludokumentit",
"toolUseCases": {
"claude": "Käytä, kun haluat vahvoja suunnittelutyönkulkuja ja pitkiä monitiedostoisia refraktoreita Claude Coden avulla.",
"codex": "Käytä, kun tiimisi on standardoitu OpenAI Codex CLI -virtojen ja profiilipohjaisen todennuksen kanssa.",
"droid": "Käytä, kun tarvitset kevyen pääteagentin, joka keskittyy nopeaan koodaukseen ja komentojen suoritussilmukoihin.",
"openclaw": "Käytä, kun haluat Open Claw -tyyppisen koodausagentin, mutta reititetyn OmniRoute-käytäntöjen kautta.",
"cline": "Käytä, kun määrität koodausagentteja editorien sisällä ja haluat ohjatun asennuksen OmniRoute-malleilla.",
"kilo": "Käytä, kun työnkulkusi riippuu Kilo Code -komennoista ja nopeista iteratiivisista muokkauksista.",
"cursor": "Käytä koodattaessa Cursorissa ja tarvitset mukautettuja OpenAI-yhteensopivia malleja OmniRouten kautta.",
"continue": "Käytä, kun käytät Continuea IDE:issä ja tarvitset kannettavan JSON-pohjaisen palveluntarjoajan määrityksen.",
"opencode": "Käytä, kun haluat käyttää päätealustaisia agentteja ja komentosarjottua automaatiota OpenCoden kautta.",
"kiro": "Käytä integroitaessa Kiroa ja ohjattaessa mallin reititystä keskitetysti OmniRoutesta.",
"antigravity": "Käytä, kun Antigravity/Kiro-liikenne on siepattava MITM:n kautta ja ohjattava OmniRouteen.",
"copilot": "Käytä, kun haluat Copilot-chat-tyylisen UX:n ja pakota OmniRoute-avaimia ja reitityssääntöjä."
}
},
"combos": {
"title": "Yhdistelmät",
@@ -1087,7 +1105,13 @@
"resetAllTitle": "Palauta kaikki katkaisijat terveeseen tilaan",
"resetting": "Nollataan...",
"resetAll": "Nollaa kaikki",
"until": "{time} asti"
"until": "{time} asti",
"activeProviders": "{count} active",
"monitoredProviders": "{count} monitored",
"configuredProvidersLabel": "Konfiguroitu kojelaudassa",
"configuredProvidersHint": "Palveluntarjoajat, joiden valtuustiedot on tallennettu kansioon /dashboard/providers, suorituksen tilasta riippumatta.",
"activeProvidersHint": "Määritetyt palveluntarjoajat, jotka ovat tällä hetkellä käytössä reitityspyyntöjä varten.",
"monitoredProvidersHint": "Palveluntarjoajat, joita tällä hetkellä seurataan katkaisijoiden kuntovalvojien avulla."
},
"limits": {
"title": "Rajoitukset ja kiintiöt",
@@ -1446,7 +1470,14 @@
"compatProtocolOpenAIResponses": "OpenAI Responses API",
"compatProtocolClaude": "Anthropic Messages",
"tokenRefreshed": "Token refreshed successfully",
"tokenRefreshFailed": "Token refresh failed"
"tokenRefreshFailed": "Token refresh failed",
"compatBadgeUpstreamHeaders": "Headers",
"compatUpstreamAddRow": "Add header",
"compatUpstreamHeaderName": "Header name",
"compatUpstreamHeaderValue": "Value",
"compatUpstreamHeadersHint": "High-privilege setting — same trust level as editing provider API credentials; only trusted admins should use it. Merged after OmniRoute adds auth from the provider API key. If a custom header uses the same name as an existing one (e.g. Authorization), your value fully replaces the auto-generated header (including the Bearer token) — the upstream only sees what you typed, not the key from settings. Misconfiguration can cause 401 or broken upstream auth. One row per header (e.g. extra Authentication for some gateways). Hover or focus the value to preview. Saves on blur, outside click, or closing this panel.",
"compatUpstreamHeadersLabel": "Extra upstream headers",
"compatUpstreamRemoveRow": "Remove row"
},
"settings": {
"title": "Asetukset",
@@ -1849,7 +1880,13 @@
"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}"
"disableFingerprintTitle": "Disable fingerprint for {provider}",
"routingAdvancedGuideTitle": "Edistynyt reititysopastus",
"routingAdvancedGuideHint1": "Käytä Täytä ensin ennustettavaa prioriteettia varten, Round Robinia oikeudenmukaisuuden varmistamiseksi ja P2C:tä latenssin kestävyyteen.",
"routingAdvancedGuideHint2": "Jos palveluntarjoajat vaihtelevat laadultaan/kustannuksiltaan, aloita Cost Opt -vaihtoehdolla taustatyössä ja Vähiten käytetyllä tasapainoiseen kulumiseen.",
"comboDefaultsGuideTitle": "Kuinka virittää yhdistelmäoletusasetukset",
"comboDefaultsGuideHint1": "Pidä uudelleenyritykset alhaisena matalan viiveen virroissa; lisää aikakatkaisua vain pitkiä sukupolvitehtäviä varten.",
"comboDefaultsGuideHint2": "Käytä palveluntarjoajan ohituksia, kun yksi palveluntarjoaja tarvitsee erilaista aikakatkaisu-/uudelleenyritystoimintaa kuin yleiset oletusasetukset."
},
"translator": {
"title": "Kääntäjä",
@@ -2569,7 +2606,15 @@
"opencodeDetected": "opencode {version} detected",
"opencodeDesc": "Generate a ready-to-use {configFile} with your OmniRoute base URL and all available models — drop it in your project root and run {command}.",
"downloadConfig": "Download {file}",
"downloaded": "Downloaded!"
"downloaded": "Downloaded!",
"setupGuideTitle": "Setup guide",
"openCliTools": "Open CLI Tools",
"setupGuideDetectCliTitle": "Detect installed CLIs",
"setupGuideDetectCliDesc": "Click Refresh after installing or updating a CLI so OmniRoute can rescan binaries and versions.",
"setupGuideCustomAgentTitle": "Register custom binary",
"setupGuideCustomAgentDesc": "Use Add Custom Agent when your CLI is not in the built-in list. Provide binary name and version command.",
"setupGuideCommandMissingTitle": "Fix 'command not found'",
"setupGuideCommandMissingDesc": "Ensure the CLI command exists in PATH, open a new terminal session, and rerun Refresh."
},
"autoCombo": {
"title": "Auto-Combo Engine",
@@ -2628,6 +2673,21 @@
"rerank": "Rerank",
"rerankModel": "Rerank Model",
"positionDelta": "Position Change",
"emptyState": "Send a search query to see results"
"emptyState": "Send a search query to see results",
"safeSearchOff": "Off",
"safeSearchModerate": "Moderate",
"safeSearchStrict": "Strict",
"queryPlaceholder": "Enter search query...",
"providerAuto": "auto (cheapest)",
"searchTypeWeb": "web",
"searchTypeNews": "news",
"optionAny": "any",
"timeRangeDay": "Past day",
"timeRangeWeek": "Past week",
"timeRangeMonth": "Past month",
"timeRangeYear": "Past year",
"domainPlaceholder": "example.com",
"requestTimedOut": "Request timed out ({seconds}s)",
"networkError": "Network error"
}
}

View File

@@ -56,7 +56,9 @@
"warning": "Avertissement",
"note": "Remarque",
"free": "Gratuit",
"skipToContent": "Passer au contenu"
"skipToContent": "Passer au contenu",
"maintenanceServerIssues": "Server is experiencing issues. Some features may be unavailable.",
"maintenanceServerUnreachable": "Server is unreachable. Reconnecting..."
},
"sidebar": {
"home": "Accueil",
@@ -576,7 +578,23 @@
"mitmStep1": "1. Start MITM to route requests through OmniRoute.",
"mitmStep2Prefix": "2. Add",
"mitmStep2Suffix": "to your hosts file as 127.0.0.1.",
"mitmStep3": "3. Open {toolName} and requests will be proxied."
"mitmStep3": "3. Open {toolName} and requests will be proxied.",
"whenToUseLabel": "Quand utiliser",
"openToolDocs": "Ouvrir la documentation de l'outil",
"toolUseCases": {
"claude": "À utiliser lorsque vous souhaitez des flux de travail de planification solides et de longs refactors multi-fichiers avec Claude Code.",
"codex": "À utiliser lorsque votre équipe est standardisée sur les flux CLI OpenAI Codex et l'authentification basée sur le profil.",
"droid": "À utiliser lorsque vous avez besoin d'un agent de terminal léger axé sur des boucles de codage et d'exécution de commandes rapides.",
"openclaw": "À utiliser lorsque vous souhaitez un agent de codage de style Open Claw mais acheminé via des stratégies OmniRoute.",
"cline": "À utiliser lorsque vous configurez des agents de codage dans des éditeurs et que vous souhaitez une configuration guidée avec des modèles OmniRoute.",
"kilo": "À utiliser lorsque votre flux de travail dépend des commandes Kilo Code et de modifications itératives rapides.",
"cursor": "À utiliser lors du codage dans Cursor et vous avez besoin de modèles personnalisés compatibles OpenAI via OmniRoute.",
"continue": "À utiliser lors de l'exécution de Continue dans les IDE et que vous avez besoin d'une configuration de fournisseur portable basée sur JSON.",
"opencode": "À utiliser lorsque vous préférez les exécutions d'agents natifs du terminal et l'automatisation par script via OpenCode.",
"kiro": "À utiliser lors de l'intégration de Kiro et du contrôle centralisé du routage de modèles à partir d'OmniRoute.",
"antigravity": "À utiliser lorsque le trafic Antigravity/Kiro doit être intercepté via MITM et acheminé vers OmniRoute.",
"copilot": "À utiliser lorsque vous souhaitez une UX de style chat Copilot tout en appliquant les clés OmniRoute et les règles de routage."
}
},
"combos": {
"title": "Combinaisons",
@@ -1087,7 +1105,13 @@
"resetAllTitle": "Réinitialisez tous les disjoncteurs à létat sain",
"resetting": "Réinitialisation...",
"resetAll": "Tout réinitialiser",
"until": "Jusqu'au {time}"
"until": "Jusqu'au {time}",
"activeProviders": "{count} active",
"monitoredProviders": "{count} monitored",
"configuredProvidersLabel": "Configuré dans le tableau de bord",
"configuredProvidersHint": "Fournisseurs dont les informations didentification sont enregistrées dans /dashboard/providers, quel que soit létat dexécution.",
"activeProvidersHint": "Fournisseurs configurés actuellement activés pour les demandes de routage.",
"monitoredProvidersHint": "Fournisseurs actuellement suivis par des moniteurs de santé des disjoncteurs."
},
"limits": {
"title": "Limites et quotas",
@@ -1446,7 +1470,14 @@
"compatProtocolOpenAIResponses": "OpenAI Responses API",
"compatProtocolClaude": "Anthropic Messages",
"tokenRefreshed": "Token refreshed successfully",
"tokenRefreshFailed": "Token refresh failed"
"tokenRefreshFailed": "Token refresh failed",
"compatBadgeUpstreamHeaders": "Headers",
"compatUpstreamAddRow": "Add header",
"compatUpstreamHeaderName": "Header name",
"compatUpstreamHeaderValue": "Value",
"compatUpstreamHeadersHint": "High-privilege setting — same trust level as editing provider API credentials; only trusted admins should use it. Merged after OmniRoute adds auth from the provider API key. If a custom header uses the same name as an existing one (e.g. Authorization), your value fully replaces the auto-generated header (including the Bearer token) — the upstream only sees what you typed, not the key from settings. Misconfiguration can cause 401 or broken upstream auth. One row per header (e.g. extra Authentication for some gateways). Hover or focus the value to preview. Saves on blur, outside click, or closing this panel.",
"compatUpstreamHeadersLabel": "Extra upstream headers",
"compatUpstreamRemoveRow": "Remove row"
},
"settings": {
"title": "Paramètres",
@@ -1849,7 +1880,13 @@
"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}"
"disableFingerprintTitle": "Disable fingerprint for {provider}",
"routingAdvancedGuideTitle": "Guidage d'itinéraire avancé",
"routingAdvancedGuideHint1": "Utilisez Fill First pour une priorité prévisible, Round Robin pour léquité et P2C pour la résilience en matière de latence.",
"routingAdvancedGuideHint2": "Si les prestataires varient en termes de qualité/coût, commencez par Opter pour le coût pour le travail de fond et par Moins utilisé pour une usure équilibrée.",
"comboDefaultsGuideTitle": "Comment régler les paramètres par défaut du combo",
"comboDefaultsGuideHint1": "Maintenez un faible nombre de tentatives dans les flux à faible latence ; augmentez le délai d'attente uniquement pour les tâches de génération longue.",
"comboDefaultsGuideHint2": "Utilisez les remplacements de fournisseur lorsqu'un fournisseur a besoin d'un comportement de délai d'attente/nouvelle tentative différent de celui des valeurs par défaut globales."
},
"translator": {
"title": "Traducteur",
@@ -2569,7 +2606,15 @@
"opencodeDetected": "opencode {version} detected",
"opencodeDesc": "Generate a ready-to-use {configFile} with your OmniRoute base URL and all available models — drop it in your project root and run {command}.",
"downloadConfig": "Download {file}",
"downloaded": "Downloaded!"
"downloaded": "Downloaded!",
"setupGuideTitle": "Setup guide",
"openCliTools": "Open CLI Tools",
"setupGuideDetectCliTitle": "Detect installed CLIs",
"setupGuideDetectCliDesc": "Click Refresh after installing or updating a CLI so OmniRoute can rescan binaries and versions.",
"setupGuideCustomAgentTitle": "Register custom binary",
"setupGuideCustomAgentDesc": "Use Add Custom Agent when your CLI is not in the built-in list. Provide binary name and version command.",
"setupGuideCommandMissingTitle": "Fix 'command not found'",
"setupGuideCommandMissingDesc": "Ensure the CLI command exists in PATH, open a new terminal session, and rerun Refresh."
},
"autoCombo": {
"title": "Auto-Combo Engine",
@@ -2628,6 +2673,21 @@
"rerank": "Rerank",
"rerankModel": "Rerank Model",
"positionDelta": "Position Change",
"emptyState": "Send a search query to see results"
"emptyState": "Send a search query to see results",
"safeSearchOff": "Off",
"safeSearchModerate": "Moderate",
"safeSearchStrict": "Strict",
"queryPlaceholder": "Enter search query...",
"providerAuto": "auto (cheapest)",
"searchTypeWeb": "web",
"searchTypeNews": "news",
"optionAny": "any",
"timeRangeDay": "Past day",
"timeRangeWeek": "Past week",
"timeRangeMonth": "Past month",
"timeRangeYear": "Past year",
"domainPlaceholder": "example.com",
"requestTimedOut": "Request timed out ({seconds}s)",
"networkError": "Network error"
}
}

View File

@@ -56,7 +56,9 @@
"warning": "אזהרה",
"note": "הערה",
"free": "חינם",
"skipToContent": "דלג לתוכן"
"skipToContent": "דלג לתוכן",
"maintenanceServerIssues": "Server is experiencing issues. Some features may be unavailable.",
"maintenanceServerUnreachable": "Server is unreachable. Reconnecting..."
},
"sidebar": {
"home": "בית",
@@ -576,7 +578,23 @@
"mitmStep1": "1. Start MITM to route requests through OmniRoute.",
"mitmStep2Prefix": "2. Add",
"mitmStep2Suffix": "to your hosts file as 127.0.0.1.",
"mitmStep3": "3. Open {toolName} and requests will be proxied."
"mitmStep3": "3. Open {toolName} and requests will be proxied.",
"whenToUseLabel": "מתי להשתמש",
"openToolDocs": "פתח את מסמכי הכלים",
"toolUseCases": {
"claude": "השתמש כאשר אתה רוצה זרימות עבודה תכנון חזקות וריפקטורים ארוכים מרובי קבצים עם Claude Code.",
"codex": "השתמש כאשר הצוות שלך סטנדרטי על זרימות OpenAI Codex CLI ואימות מבוסס פרופיל.",
"droid": "השתמש כאשר אתה צריך סוכן מסוף קל משקל המתמקד בלולאות קידוד מהיר וביצוע פקודות.",
"openclaw": "השתמש כאשר אתה רוצה סוכן קידוד בסגנון Open Claw אך מנותב דרך מדיניות OmniRoute.",
"cline": "השתמש כאשר אתה מגדיר סוכני קידוד בתוך עורכים ורוצה הגדרה מודרכת עם דגמי OmniRoute.",
"kilo": "השתמש כאשר זרימת העבודה שלך תלויה בפקודות Kilo Code ועריכות איטרטיביות מהירות.",
"cursor": "השתמש בעת קידוד בסמן ואתה צריך מודלים מותאמים אישית תואמי OpenAI דרך OmniRoute.",
"continue": "השתמש בעת הפעלת Continue ב-IDEs ואתה זקוק לתצורת ספק ניידת מבוססת JSON.",
"opencode": "השתמש כאשר אתה מעדיף ריצות סוכנים מקוריים ומאוטומציה של סקריפטים באמצעות OpenCode.",
"kiro": "השתמש בעת שילוב Kiro ושליטה בניתוב מודלים באופן מרכזי מ- OmniRoute.",
"antigravity": "השתמש כאשר יש ליירט תעבורת Antigravity/Kiro דרך MITM ולנתב אל OmniRoute.",
"copilot": "השתמש כאשר אתה רוצה UX בסגנון צ'אט Copilot תוך אכיפת מפתחות וכללי ניתוב OmniRoute."
}
},
"combos": {
"title": "שילובים",
@@ -1087,7 +1105,13 @@
"resetAllTitle": "אפס את כל המפסקים למצב תקין",
"resetting": "מאפס...",
"resetAll": "אפס הכל",
"until": "עד {time}"
"until": "עד {time}",
"activeProviders": "{count} active",
"monitoredProviders": "{count} monitored",
"configuredProvidersLabel": "מוגדר בלוח המחוונים",
"configuredProvidersHint": "ספקים עם אישורים שנשמרו ב-/dashboard/ספקים, ללא קשר למצב זמן הריצה.",
"activeProvidersHint": "ספקים מוגדרים מופעלים כעת עבור בקשות ניתוב.",
"monitoredProvidersHint": "ספקים שנמצאים כעת במעקב על-ידי מסכי תקינות מפסקים."
},
"limits": {
"title": "מגבלות ומכסות",
@@ -1446,7 +1470,14 @@
"compatProtocolOpenAIResponses": "OpenAI Responses API",
"compatProtocolClaude": "Anthropic Messages",
"tokenRefreshed": "Token refreshed successfully",
"tokenRefreshFailed": "Token refresh failed"
"tokenRefreshFailed": "Token refresh failed",
"compatBadgeUpstreamHeaders": "Headers",
"compatUpstreamAddRow": "Add header",
"compatUpstreamHeaderName": "Header name",
"compatUpstreamHeaderValue": "Value",
"compatUpstreamHeadersHint": "High-privilege setting — same trust level as editing provider API credentials; only trusted admins should use it. Merged after OmniRoute adds auth from the provider API key. If a custom header uses the same name as an existing one (e.g. Authorization), your value fully replaces the auto-generated header (including the Bearer token) — the upstream only sees what you typed, not the key from settings. Misconfiguration can cause 401 or broken upstream auth. One row per header (e.g. extra Authentication for some gateways). Hover or focus the value to preview. Saves on blur, outside click, or closing this panel.",
"compatUpstreamHeadersLabel": "Extra upstream headers",
"compatUpstreamRemoveRow": "Remove row"
},
"settings": {
"title": "הגדרות",
@@ -1849,7 +1880,13 @@
"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}"
"disableFingerprintTitle": "Disable fingerprint for {provider}",
"routingAdvancedGuideTitle": "הנחיית ניתוב מתקדמת",
"routingAdvancedGuideHint1": "השתמש ב-Fill First לקבלת עדיפות צפויה, ב-Round Robin להגינות, וב-P2C עבור חוסן חביון.",
"routingAdvancedGuideHint2": "אם הספקים משתנים באיכות/עלות, התחל עם Cost Opt עבור עבודת רקע והפחות בשימוש עבור בלאי מאוזן.",
"comboDefaultsGuideTitle": "כיצד לכוונן ברירות מחדל משולבות",
"comboDefaultsGuideHint1": "שמור על ניסיונות חוזרים נמוכים בזרימות עם אחזור נמוך; להגדיל את הזמן הקצוב רק עבור משימות דור ארוך.",
"comboDefaultsGuideHint2": "השתמש בעקיפות ספק כאשר ספק אחד זקוק להתנהגות שונה של זמן קצוב/ניסיון חוזר מאשר ברירות מחדל גלובליות."
},
"translator": {
"title": "מתרגם",
@@ -2569,7 +2606,15 @@
"opencodeDetected": "opencode {version} detected",
"opencodeDesc": "Generate a ready-to-use {configFile} with your OmniRoute base URL and all available models — drop it in your project root and run {command}.",
"downloadConfig": "Download {file}",
"downloaded": "Downloaded!"
"downloaded": "Downloaded!",
"setupGuideTitle": "Setup guide",
"openCliTools": "Open CLI Tools",
"setupGuideDetectCliTitle": "Detect installed CLIs",
"setupGuideDetectCliDesc": "Click Refresh after installing or updating a CLI so OmniRoute can rescan binaries and versions.",
"setupGuideCustomAgentTitle": "Register custom binary",
"setupGuideCustomAgentDesc": "Use Add Custom Agent when your CLI is not in the built-in list. Provide binary name and version command.",
"setupGuideCommandMissingTitle": "Fix 'command not found'",
"setupGuideCommandMissingDesc": "Ensure the CLI command exists in PATH, open a new terminal session, and rerun Refresh."
},
"autoCombo": {
"title": "Auto-Combo Engine",
@@ -2628,6 +2673,21 @@
"rerank": "Rerank",
"rerankModel": "Rerank Model",
"positionDelta": "Position Change",
"emptyState": "Send a search query to see results"
"emptyState": "Send a search query to see results",
"safeSearchOff": "Off",
"safeSearchModerate": "Moderate",
"safeSearchStrict": "Strict",
"queryPlaceholder": "Enter search query...",
"providerAuto": "auto (cheapest)",
"searchTypeWeb": "web",
"searchTypeNews": "news",
"optionAny": "any",
"timeRangeDay": "Past day",
"timeRangeWeek": "Past week",
"timeRangeMonth": "Past month",
"timeRangeYear": "Past year",
"domainPlaceholder": "example.com",
"requestTimedOut": "Request timed out ({seconds}s)",
"networkError": "Network error"
}
}

View File

@@ -56,7 +56,9 @@
"warning": "Figyelmeztetés",
"note": "Megjegyzés",
"free": "Ingyenes",
"skipToContent": "Ugrás a tartalomhoz"
"skipToContent": "Ugrás a tartalomhoz",
"maintenanceServerIssues": "Server is experiencing issues. Some features may be unavailable.",
"maintenanceServerUnreachable": "Server is unreachable. Reconnecting..."
},
"sidebar": {
"home": "Otthon",
@@ -576,7 +578,23 @@
"mitmStep1": "1. Start MITM to route requests through OmniRoute.",
"mitmStep2Prefix": "2. Add",
"mitmStep2Suffix": "to your hosts file as 127.0.0.1.",
"mitmStep3": "3. Open {toolName} and requests will be proxied."
"mitmStep3": "3. Open {toolName} and requests will be proxied.",
"whenToUseLabel": "Mikor kell használni",
"openToolDocs": "Nyissa meg az eszközdokumentumokat",
"toolUseCases": {
"claude": "Használja, ha erős tervezési munkafolyamatokat és hosszú, több fájlból álló refaktorokat szeretne a Claude Code segítségével.",
"codex": "Akkor használja, ha csapata szabványosítva van az OpenAI Codex CLI-folyamatokon és a profilalapú hitelesítésen.",
"droid": "Használja, ha egy könnyű terminálügynökre van szüksége, amely a gyors kódolásra és parancsvégrehajtási ciklusokra összpontosít.",
"openclaw": "Akkor használja, ha Open Claw stílusú kódoló ügynököt szeretne, de az OmniRoute házirendeken keresztül irányítja.",
"cline": "Akkor használja, ha kódoló ügynököket konfigurál a szerkesztőkön belül, és irányított beállítást szeretne az OmniRoute modellekkel.",
"kilo": "Akkor használja, ha a munkafolyamat a Kilo Code parancsoktól és a gyors iteratív szerkesztésektől függ.",
"cursor": "Használja a Kurzorban való kódoláshoz, és egyéni OpenAI-kompatibilis modellekre van szüksége az OmniRoute-on keresztül.",
"continue": "Használja a Continue futtatásakor az IDE-ben, és hordozható JSON-alapú szolgáltatói konfigurációra van szüksége.",
"opencode": "Akkor használja, ha a terminál-natív ügynökfuttatásokat és az OpenCode-on keresztüli parancsfájl-automatizálást részesíti előnyben.",
"kiro": "Használja a Kiro integrálásához és a modell-útválasztás központi vezérléséhez az OmniRoute-ból.",
"antigravity": "Akkor használja, ha az Antigravity/Kiro forgalmat MITM-en keresztül kell elfogni, és az OmniRoute-hoz kell irányítani.",
"copilot": "Használja, ha másodpilóta csevegési stílusú UX-et szeretne, miközben betartja az OmniRoute kulcsokat és útválasztási szabályokat."
}
},
"combos": {
"title": "Kombók",
@@ -1087,7 +1105,13 @@
"resetAllTitle": "Állítsa vissza az összes megszakítót egészséges állapotba",
"resetting": "Visszaállítás...",
"resetAll": "Összes visszaállítása",
"until": "{time}-ig"
"until": "{time}-ig",
"activeProviders": "{count} active",
"monitoredProviders": "{count} monitored",
"configuredProvidersLabel": "A műszerfalon konfigurálva",
"configuredProvidersHint": "A /dashboard/providers mappába mentett hitelesítő adatokkal rendelkező szolgáltatók, a futásidejű állapottól függetlenül.",
"activeProvidersHint": "Konfigurált szolgáltatók, amelyek jelenleg engedélyezve vannak az útválasztási kérelmek számára.",
"monitoredProvidersHint": "Azok a szolgáltatók, amelyeket jelenleg a megszakítók állapotfigyelői követnek nyomon."
},
"limits": {
"title": "Korlátok és kvóták",
@@ -1446,7 +1470,14 @@
"compatProtocolOpenAIResponses": "OpenAI Responses API",
"compatProtocolClaude": "Anthropic Messages",
"tokenRefreshed": "Token refreshed successfully",
"tokenRefreshFailed": "Token refresh failed"
"tokenRefreshFailed": "Token refresh failed",
"compatBadgeUpstreamHeaders": "Headers",
"compatUpstreamAddRow": "Add header",
"compatUpstreamHeaderName": "Header name",
"compatUpstreamHeaderValue": "Value",
"compatUpstreamHeadersHint": "High-privilege setting — same trust level as editing provider API credentials; only trusted admins should use it. Merged after OmniRoute adds auth from the provider API key. If a custom header uses the same name as an existing one (e.g. Authorization), your value fully replaces the auto-generated header (including the Bearer token) — the upstream only sees what you typed, not the key from settings. Misconfiguration can cause 401 or broken upstream auth. One row per header (e.g. extra Authentication for some gateways). Hover or focus the value to preview. Saves on blur, outside click, or closing this panel.",
"compatUpstreamHeadersLabel": "Extra upstream headers",
"compatUpstreamRemoveRow": "Remove row"
},
"settings": {
"title": "Beállítások elemre",
@@ -1849,7 +1880,13 @@
"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}"
"disableFingerprintTitle": "Disable fingerprint for {provider}",
"routingAdvancedGuideTitle": "Speciális útbaigazítás",
"routingAdvancedGuideHint1": "Használja a Fill First funkciót a kiszámítható prioritáshoz, a Round Robint a méltányossághoz és a P2C-t a késleltetési rugalmassághoz.",
"routingAdvancedGuideHint2": "Ha a szolgáltatók minősége/költségei eltérőek, kezdje a Cost Opt opcióval a háttérmunkához és a Least Used beállítással a kiegyensúlyozott viselet érdekében.",
"comboDefaultsGuideTitle": "A kombinált alapértelmezett beállítások hangolása",
"comboDefaultsGuideHint1": "Tartsa alacsonyan az újrapróbálkozásokat az alacsony késleltetésű folyamatokban; csak hosszú generációs feladatok esetén növelje az időtúllépést.",
"comboDefaultsGuideHint2": "Használja a szolgáltató felülbírálását, ha az egyik szolgáltatónak a globális alapértelmezetttől eltérő időtúllépési/újrapróbálkozási viselkedésre van szüksége."
},
"translator": {
"title": "Fordító",
@@ -2569,7 +2606,15 @@
"opencodeDetected": "opencode {version} detected",
"opencodeDesc": "Generate a ready-to-use {configFile} with your OmniRoute base URL and all available models — drop it in your project root and run {command}.",
"downloadConfig": "Download {file}",
"downloaded": "Downloaded!"
"downloaded": "Downloaded!",
"setupGuideTitle": "Setup guide",
"openCliTools": "Open CLI Tools",
"setupGuideDetectCliTitle": "Detect installed CLIs",
"setupGuideDetectCliDesc": "Click Refresh after installing or updating a CLI so OmniRoute can rescan binaries and versions.",
"setupGuideCustomAgentTitle": "Register custom binary",
"setupGuideCustomAgentDesc": "Use Add Custom Agent when your CLI is not in the built-in list. Provide binary name and version command.",
"setupGuideCommandMissingTitle": "Fix 'command not found'",
"setupGuideCommandMissingDesc": "Ensure the CLI command exists in PATH, open a new terminal session, and rerun Refresh."
},
"autoCombo": {
"title": "Auto-Combo Engine",
@@ -2628,6 +2673,21 @@
"rerank": "Rerank",
"rerankModel": "Rerank Model",
"positionDelta": "Position Change",
"emptyState": "Send a search query to see results"
"emptyState": "Send a search query to see results",
"safeSearchOff": "Off",
"safeSearchModerate": "Moderate",
"safeSearchStrict": "Strict",
"queryPlaceholder": "Enter search query...",
"providerAuto": "auto (cheapest)",
"searchTypeWeb": "web",
"searchTypeNews": "news",
"optionAny": "any",
"timeRangeDay": "Past day",
"timeRangeWeek": "Past week",
"timeRangeMonth": "Past month",
"timeRangeYear": "Past year",
"domainPlaceholder": "example.com",
"requestTimedOut": "Request timed out ({seconds}s)",
"networkError": "Network error"
}
}

View File

@@ -56,7 +56,9 @@
"warning": "Peringatan",
"note": "Catatan",
"free": "Gratis",
"skipToContent": "Lewati ke konten"
"skipToContent": "Lewati ke konten",
"maintenanceServerIssues": "Server is experiencing issues. Some features may be unavailable.",
"maintenanceServerUnreachable": "Server is unreachable. Reconnecting..."
},
"sidebar": {
"home": "Rumah",
@@ -576,7 +578,23 @@
"mitmStep1": "1. Start MITM to route requests through OmniRoute.",
"mitmStep2Prefix": "2. Add",
"mitmStep2Suffix": "to your hosts file as 127.0.0.1.",
"mitmStep3": "3. Open {toolName} and requests will be proxied."
"mitmStep3": "3. Open {toolName} and requests will be proxied.",
"whenToUseLabel": "Kapan harus digunakan",
"openToolDocs": "Buka dokumen alat",
"toolUseCases": {
"claude": "Gunakan saat Anda menginginkan alur kerja perencanaan yang kuat dan pemfaktoran ulang multi-file yang panjang dengan Claude Code.",
"codex": "Gunakan saat tim Anda terstandarisasi pada alur OpenAI Codex CLI dan autentikasi berbasis profil.",
"droid": "Gunakan saat Anda membutuhkan agen terminal ringan yang berfokus pada pengkodean cepat dan loop eksekusi perintah.",
"openclaw": "Gunakan saat Anda menginginkan agen pengkodean gaya Open Claw tetapi dirutekan melalui kebijakan OmniRoute.",
"cline": "Gunakan saat Anda mengonfigurasi agen pengkodean di dalam editor dan ingin penyiapan terpandu dengan model OmniRoute.",
"kilo": "Gunakan ketika alur kerja Anda bergantung pada perintah Kode Kilo dan pengeditan berulang yang cepat.",
"cursor": "Gunakan saat membuat kode di Cursor dan Anda memerlukan model khusus yang kompatibel dengan OpenAI melalui OmniRoute.",
"continue": "Gunakan saat menjalankan Lanjutkan di IDE dan Anda memerlukan konfigurasi penyedia portabel berbasis JSON.",
"opencode": "Gunakan saat Anda lebih suka menjalankan agen terminal-asli dan otomatisasi skrip melalui OpenCode.",
"kiro": "Gunakan saat mengintegrasikan Kiro dan mengontrol perutean model secara terpusat dari OmniRoute.",
"antigravity": "Gunakan ketika lalu lintas Antigravitasi/Kiro harus dicegat melalui MITM dan dialihkan ke OmniRoute.",
"copilot": "Gunakan saat Anda menginginkan UX gaya obrolan kopilot sambil menerapkan kunci OmniRoute dan aturan perutean."
}
},
"combos": {
"title": "kombo",
@@ -1087,7 +1105,13 @@
"resetAllTitle": "Setel ulang semua pemutus sirkuit ke kondisi sehat",
"resetting": "Menyetel ulang...",
"resetAll": "Atur Ulang Semua",
"until": "Sampai {time}"
"until": "Sampai {time}",
"activeProviders": "{count} active",
"monitoredProviders": "{count} monitored",
"configuredProvidersLabel": "Dikonfigurasi di dasbor",
"configuredProvidersHint": "Penyedia dengan kredensial yang disimpan di /dashboard/providers, apa pun status runtimenya.",
"activeProvidersHint": "Penyedia yang dikonfigurasi saat ini diaktifkan untuk permintaan perutean.",
"monitoredProvidersHint": "Penyedia saat ini dilacak oleh monitor kesehatan pemutus sirkuit."
},
"limits": {
"title": "Batas & Kuota",
@@ -1446,7 +1470,14 @@
"compatProtocolOpenAIResponses": "OpenAI Responses API",
"compatProtocolClaude": "Anthropic Messages",
"tokenRefreshed": "Token refreshed successfully",
"tokenRefreshFailed": "Token refresh failed"
"tokenRefreshFailed": "Token refresh failed",
"compatBadgeUpstreamHeaders": "Headers",
"compatUpstreamAddRow": "Add header",
"compatUpstreamHeaderName": "Header name",
"compatUpstreamHeaderValue": "Value",
"compatUpstreamHeadersHint": "High-privilege setting — same trust level as editing provider API credentials; only trusted admins should use it. Merged after OmniRoute adds auth from the provider API key. If a custom header uses the same name as an existing one (e.g. Authorization), your value fully replaces the auto-generated header (including the Bearer token) — the upstream only sees what you typed, not the key from settings. Misconfiguration can cause 401 or broken upstream auth. One row per header (e.g. extra Authentication for some gateways). Hover or focus the value to preview. Saves on blur, outside click, or closing this panel.",
"compatUpstreamHeadersLabel": "Extra upstream headers",
"compatUpstreamRemoveRow": "Remove row"
},
"settings": {
"title": "Pengaturan",
@@ -1849,7 +1880,13 @@
"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}"
"disableFingerprintTitle": "Disable fingerprint for {provider}",
"routingAdvancedGuideTitle": "Panduan perutean tingkat lanjut",
"routingAdvancedGuideHint1": "Gunakan Fill First untuk prioritas yang dapat diprediksi, Round Robin untuk keadilan, dan P2C untuk ketahanan latensi.",
"routingAdvancedGuideHint2": "Jika penyedia memiliki kualitas/biaya yang berbeda-beda, mulailah dengan Cost Opt (Pilihan Biaya) untuk pekerjaan latar belakang dan Paling Sedikit Digunakan untuk pemakaian yang seimbang.",
"comboDefaultsGuideTitle": "Cara menyetel default kombo",
"comboDefaultsGuideHint1": "Jaga agar percobaan ulang tetap rendah dalam aliran latensi rendah; menambah waktu tunggu hanya untuk tugas-tugas generasi panjang.",
"comboDefaultsGuideHint2": "Gunakan penggantian penyedia ketika satu penyedia memerlukan perilaku batas waktu/coba lagi yang berbeda dari default global."
},
"translator": {
"title": "Penerjemah",
@@ -2569,7 +2606,15 @@
"opencodeDetected": "opencode {version} detected",
"opencodeDesc": "Generate a ready-to-use {configFile} with your OmniRoute base URL and all available models — drop it in your project root and run {command}.",
"downloadConfig": "Download {file}",
"downloaded": "Downloaded!"
"downloaded": "Downloaded!",
"setupGuideTitle": "Setup guide",
"openCliTools": "Open CLI Tools",
"setupGuideDetectCliTitle": "Detect installed CLIs",
"setupGuideDetectCliDesc": "Click Refresh after installing or updating a CLI so OmniRoute can rescan binaries and versions.",
"setupGuideCustomAgentTitle": "Register custom binary",
"setupGuideCustomAgentDesc": "Use Add Custom Agent when your CLI is not in the built-in list. Provide binary name and version command.",
"setupGuideCommandMissingTitle": "Fix 'command not found'",
"setupGuideCommandMissingDesc": "Ensure the CLI command exists in PATH, open a new terminal session, and rerun Refresh."
},
"autoCombo": {
"title": "Auto-Combo Engine",
@@ -2628,6 +2673,21 @@
"rerank": "Rerank",
"rerankModel": "Rerank Model",
"positionDelta": "Position Change",
"emptyState": "Send a search query to see results"
"emptyState": "Send a search query to see results",
"safeSearchOff": "Off",
"safeSearchModerate": "Moderate",
"safeSearchStrict": "Strict",
"queryPlaceholder": "Enter search query...",
"providerAuto": "auto (cheapest)",
"searchTypeWeb": "web",
"searchTypeNews": "news",
"optionAny": "any",
"timeRangeDay": "Past day",
"timeRangeWeek": "Past week",
"timeRangeMonth": "Past month",
"timeRangeYear": "Past year",
"domainPlaceholder": "example.com",
"requestTimedOut": "Request timed out ({seconds}s)",
"networkError": "Network error"
}
}

View File

@@ -56,7 +56,9 @@
"warning": "चेतावनी",
"note": "नोट",
"free": "निःशुल्क",
"skipToContent": "सामग्री पर जाएं"
"skipToContent": "सामग्री पर जाएं",
"maintenanceServerIssues": "Server is experiencing issues. Some features may be unavailable.",
"maintenanceServerUnreachable": "Server is unreachable. Reconnecting..."
},
"sidebar": {
"home": "घर",
@@ -576,7 +578,23 @@
"mitmStep1": "1. Start MITM to route requests through OmniRoute.",
"mitmStep2Prefix": "2. Add",
"mitmStep2Suffix": "to your hosts file as 127.0.0.1.",
"mitmStep3": "3. Open {toolName} and requests will be proxied."
"mitmStep3": "3. Open {toolName} and requests will be proxied.",
"whenToUseLabel": "कब उपयोग करें",
"openToolDocs": "टूल दस्तावेज़ खोलें",
"toolUseCases": {
"claude": "जब आप क्लाउड कोड के साथ मजबूत नियोजन वर्कफ़्लो और लंबे मल्टी-फ़ाइल रिफैक्टर चाहते हों तो इसका उपयोग करें।",
"codex": "जब आपकी टीम OpenAI कोडेक्स CLI प्रवाह और प्रोफ़ाइल-आधारित प्रमाणीकरण पर मानकीकृत हो तो इसका उपयोग करें।",
"droid": "जब आपको तेज़ कोडिंग और कमांड निष्पादन लूप पर केंद्रित हल्के टर्मिनल एजेंट की आवश्यकता हो तो इसका उपयोग करें।",
"openclaw": "जब आप ओपन क्लॉ स्टाइल कोडिंग एजेंट चाहते हैं लेकिन ओमनीरूट नीतियों के माध्यम से रूट किया जाता है तो इसका उपयोग करें।",
"cline": "जब आप संपादकों के अंदर कोडिंग एजेंटों को कॉन्फ़िगर करते हैं और ओमनीरूट मॉडल के साथ निर्देशित सेटअप चाहते हैं तो इसका उपयोग करें।",
"kilo": "इसका उपयोग तब करें जब आपका वर्कफ़्लो किलो कोड कमांड और तेज़ पुनरावृत्त संपादन पर निर्भर हो।",
"cursor": "कर्सर में कोडिंग करते समय उपयोग करें और आपको ओमनीरूट के माध्यम से कस्टम ओपनएआई-संगत मॉडल की आवश्यकता है।",
"continue": "IDE में जारी रखें चलाते समय उपयोग करें और आपको पोर्टेबल JSON-आधारित प्रदाता कॉन्फ़िगरेशन की आवश्यकता है।",
"opencode": "जब आप ओपनकोड के माध्यम से टर्मिनल-नेटिव एजेंट रन और स्क्रिप्टेड ऑटोमेशन पसंद करते हैं तो इसका उपयोग करें।",
"kiro": "किरो को एकीकृत करते समय और ओमनीरूट से केंद्रीय रूप से मॉडल रूटिंग को नियंत्रित करते समय उपयोग करें।",
"antigravity": "इसका उपयोग तब करें जब एंटीग्रेविटी/किरो ट्रैफिक को एमआईटीएम के माध्यम से रोका जाना चाहिए और ओमनीरूट पर भेजा जाना चाहिए।",
"copilot": "जब आप ओम्निरूट कुंजी और रूटिंग नियमों को लागू करते समय कोपायलट चैट शैली यूएक्स चाहते हैं तो इसका उपयोग करें।"
}
},
"combos": {
"title": "संयोजन",
@@ -1087,7 +1105,13 @@
"resetAllTitle": "सभी सर्किट ब्रेकरों को स्वस्थ स्थिति में रीसेट करें",
"resetting": "रीसेट किया जा रहा है...",
"resetAll": "सभी रीसेट करें",
"until": "{time} तक"
"until": "{time} तक",
"activeProviders": "{count} active",
"monitoredProviders": "{count} monitored",
"configuredProvidersLabel": "डैशबोर्ड में कॉन्फ़िगर किया गया",
"configuredProvidersHint": "रनटाइम स्थिति की परवाह किए बिना, /डैशबोर्ड/प्रदाताओं में सहेजे गए क्रेडेंशियल वाले प्रदाता।",
"activeProvidersHint": "कॉन्फ़िगर किए गए प्रदाता वर्तमान में रूटिंग अनुरोधों के लिए सक्षम हैं।",
"monitoredProvidersHint": "प्रदाताओं को वर्तमान में सर्किट-ब्रेकर स्वास्थ्य मॉनिटर द्वारा ट्रैक किया जाता है।"
},
"limits": {
"title": "सीमाएँ और कोटा",
@@ -1446,7 +1470,14 @@
"compatProtocolOpenAIResponses": "OpenAI Responses API",
"compatProtocolClaude": "Anthropic Messages",
"tokenRefreshed": "Token refreshed successfully",
"tokenRefreshFailed": "Token refresh failed"
"tokenRefreshFailed": "Token refresh failed",
"compatBadgeUpstreamHeaders": "Headers",
"compatUpstreamAddRow": "Add header",
"compatUpstreamHeaderName": "Header name",
"compatUpstreamHeaderValue": "Value",
"compatUpstreamHeadersHint": "High-privilege setting — same trust level as editing provider API credentials; only trusted admins should use it. Merged after OmniRoute adds auth from the provider API key. If a custom header uses the same name as an existing one (e.g. Authorization), your value fully replaces the auto-generated header (including the Bearer token) — the upstream only sees what you typed, not the key from settings. Misconfiguration can cause 401 or broken upstream auth. One row per header (e.g. extra Authentication for some gateways). Hover or focus the value to preview. Saves on blur, outside click, or closing this panel.",
"compatUpstreamHeadersLabel": "Extra upstream headers",
"compatUpstreamRemoveRow": "Remove row"
},
"settings": {
"title": "सेटिंग्स",
@@ -1849,7 +1880,13 @@
"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}"
"disableFingerprintTitle": "Disable fingerprint for {provider}",
"routingAdvancedGuideTitle": "उन्नत रूटिंग मार्गदर्शन",
"routingAdvancedGuideHint1": "पूर्वानुमानित प्राथमिकता के लिए फ़िल फ़र्स्ट का उपयोग करें, निष्पक्षता के लिए राउंड रॉबिन और विलंबता लचीलेपन के लिए P2C का उपयोग करें।",
"routingAdvancedGuideHint2": "यदि प्रदाता गुणवत्ता/लागत में भिन्न हैं, तो पृष्ठभूमि कार्य के लिए कॉस्ट ऑप्ट और संतुलित पहनावे के लिए कम से कम उपयोग से शुरुआत करें।",
"comboDefaultsGuideTitle": "कॉम्बो डिफॉल्ट्स को कैसे ट्यून करें",
"comboDefaultsGuideHint1": "कम-विलंबता प्रवाह में पुनः प्रयास कम रखें; केवल लंबी पीढ़ी के कार्यों के लिए टाइमआउट बढ़ाएँ।",
"comboDefaultsGuideHint2": "जब एक प्रदाता को वैश्विक डिफ़ॉल्ट की तुलना में अलग टाइमआउट/पुनः प्रयास व्यवहार की आवश्यकता होती है तो प्रदाता ओवरराइड का उपयोग करें।"
},
"translator": {
"title": "अनुवादक",
@@ -2569,7 +2606,15 @@
"opencodeDetected": "opencode {version} detected",
"opencodeDesc": "Generate a ready-to-use {configFile} with your OmniRoute base URL and all available models — drop it in your project root and run {command}.",
"downloadConfig": "Download {file}",
"downloaded": "Downloaded!"
"downloaded": "Downloaded!",
"setupGuideTitle": "Setup guide",
"openCliTools": "Open CLI Tools",
"setupGuideDetectCliTitle": "Detect installed CLIs",
"setupGuideDetectCliDesc": "Click Refresh after installing or updating a CLI so OmniRoute can rescan binaries and versions.",
"setupGuideCustomAgentTitle": "Register custom binary",
"setupGuideCustomAgentDesc": "Use Add Custom Agent when your CLI is not in the built-in list. Provide binary name and version command.",
"setupGuideCommandMissingTitle": "Fix 'command not found'",
"setupGuideCommandMissingDesc": "Ensure the CLI command exists in PATH, open a new terminal session, and rerun Refresh."
},
"autoCombo": {
"title": "Auto-Combo Engine",
@@ -2628,6 +2673,21 @@
"rerank": "Rerank",
"rerankModel": "Rerank Model",
"positionDelta": "Position Change",
"emptyState": "Send a search query to see results"
"emptyState": "Send a search query to see results",
"safeSearchOff": "Off",
"safeSearchModerate": "Moderate",
"safeSearchStrict": "Strict",
"queryPlaceholder": "Enter search query...",
"providerAuto": "auto (cheapest)",
"searchTypeWeb": "web",
"searchTypeNews": "news",
"optionAny": "any",
"timeRangeDay": "Past day",
"timeRangeWeek": "Past week",
"timeRangeMonth": "Past month",
"timeRangeYear": "Past year",
"domainPlaceholder": "example.com",
"requestTimedOut": "Request timed out ({seconds}s)",
"networkError": "Network error"
}
}

View File

@@ -56,7 +56,9 @@
"warning": "Avvertimento",
"note": "Nota",
"free": "Gratuito",
"skipToContent": "Vai al contenuto"
"skipToContent": "Vai al contenuto",
"maintenanceServerIssues": "Server is experiencing issues. Some features may be unavailable.",
"maintenanceServerUnreachable": "Server is unreachable. Reconnecting..."
},
"sidebar": {
"home": "Casa",
@@ -576,7 +578,23 @@
"mitmStep1": "1. Start MITM to route requests through OmniRoute.",
"mitmStep2Prefix": "2. Add",
"mitmStep2Suffix": "to your hosts file as 127.0.0.1.",
"mitmStep3": "3. Open {toolName} and requests will be proxied."
"mitmStep3": "3. Open {toolName} and requests will be proxied.",
"whenToUseLabel": "Quando usarlo",
"openToolDocs": "Apri i documenti dello strumento",
"toolUseCases": {
"claude": "Utilizzalo quando desideri flussi di lavoro di pianificazione avanzati e lunghi refactoring multi-file con Claude Code.",
"codex": "Da utilizzare quando il tuo team è standardizzato sui flussi CLI di OpenAI Codex e sull'autenticazione basata sul profilo.",
"droid": "Da utilizzare quando è necessario un agente terminale leggero incentrato sulla codifica rapida e sui cicli di esecuzione dei comandi.",
"openclaw": "Da utilizzare quando si desidera un agente di codifica in stile Open Claw ma instradato tramite policy OmniRoute.",
"cline": "Da utilizzare quando si configurano agenti di codifica all'interno degli editor e si desidera un'impostazione guidata con i modelli OmniRoute.",
"kilo": "Da utilizzare quando il flusso di lavoro dipende dai comandi Kilo Code e da modifiche iterative rapide.",
"cursor": "Da utilizzare durante la codifica in Cursor e sono necessari modelli personalizzati compatibili con OpenAI tramite OmniRoute.",
"continue": "Da utilizzare quando si esegue Continue negli IDE ed è necessaria la configurazione portatile del provider basato su JSON.",
"opencode": "Utilizzalo quando preferisci l'esecuzione dell'agente nativo del terminale e l'automazione basata su script tramite OpenCode.",
"kiro": "Da utilizzare quando si integra Kiro e si controlla l'instradamento del modello centralmente da OmniRoute.",
"antigravity": "Da utilizzare quando il traffico Antigravity/Kiro deve essere intercettato tramite MITM e instradato a OmniRoute.",
"copilot": "Utilizzalo quando desideri un'esperienza utente in stile chat Copilot applicando al tempo stesso le chiavi OmniRoute e le regole di routing."
}
},
"combos": {
"title": "Combinazioni",
@@ -1087,7 +1105,13 @@
"resetAllTitle": "Ripristinare tutti gli interruttori automatici allo stato integro",
"resetting": "Reimpostazione...",
"resetAll": "Reimposta tutto",
"until": "Fino al {time}"
"until": "Fino al {time}",
"activeProviders": "{count} active",
"monitoredProviders": "{count} monitored",
"configuredProvidersLabel": "Configurato nel dashboard",
"configuredProvidersHint": "Provider con credenziali salvate in /dashboard/providers, indipendentemente dallo stato di runtime.",
"activeProvidersHint": "Provider configurati attualmente abilitati per le richieste di instradamento.",
"monitoredProvidersHint": "Fornitori attualmente monitorati dai monitoraggi dello stato degli interruttori automatici."
},
"limits": {
"title": "Limiti e quote",
@@ -1446,7 +1470,14 @@
"compatProtocolOpenAIResponses": "OpenAI Responses API",
"compatProtocolClaude": "Anthropic Messages",
"tokenRefreshed": "Token refreshed successfully",
"tokenRefreshFailed": "Token refresh failed"
"tokenRefreshFailed": "Token refresh failed",
"compatBadgeUpstreamHeaders": "Headers",
"compatUpstreamAddRow": "Add header",
"compatUpstreamHeaderName": "Header name",
"compatUpstreamHeaderValue": "Value",
"compatUpstreamHeadersHint": "High-privilege setting — same trust level as editing provider API credentials; only trusted admins should use it. Merged after OmniRoute adds auth from the provider API key. If a custom header uses the same name as an existing one (e.g. Authorization), your value fully replaces the auto-generated header (including the Bearer token) — the upstream only sees what you typed, not the key from settings. Misconfiguration can cause 401 or broken upstream auth. One row per header (e.g. extra Authentication for some gateways). Hover or focus the value to preview. Saves on blur, outside click, or closing this panel.",
"compatUpstreamHeadersLabel": "Extra upstream headers",
"compatUpstreamRemoveRow": "Remove row"
},
"settings": {
"title": "Impostazioni",
@@ -1849,7 +1880,13 @@
"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}"
"disableFingerprintTitle": "Disable fingerprint for {provider}",
"routingAdvancedGuideTitle": "Guida al percorso avanzata",
"routingAdvancedGuideHint1": "Utilizza Fill First per una priorità prevedibile, Round Robin per l'equità e P2C per la resilienza alla latenza.",
"routingAdvancedGuideHint2": "Se i fornitori variano in termini di qualità/costo, iniziare con Opzione costo per il lavoro in background e Meno utilizzato per un consumo equilibrato.",
"comboDefaultsGuideTitle": "Come ottimizzare le impostazioni predefinite della combo",
"comboDefaultsGuideHint1": "Mantenere bassi i tentativi nei flussi a bassa latenza; aumentare il timeout solo per attività di generazione prolungata.",
"comboDefaultsGuideHint2": "Utilizzare le sostituzioni del provider quando un provider necessita di un comportamento di timeout/riprova diverso rispetto alle impostazioni predefinite globali."
},
"translator": {
"title": "Traduttore",
@@ -2569,7 +2606,15 @@
"opencodeDetected": "opencode {version} detected",
"opencodeDesc": "Generate a ready-to-use {configFile} with your OmniRoute base URL and all available models — drop it in your project root and run {command}.",
"downloadConfig": "Download {file}",
"downloaded": "Downloaded!"
"downloaded": "Downloaded!",
"setupGuideTitle": "Setup guide",
"openCliTools": "Open CLI Tools",
"setupGuideDetectCliTitle": "Detect installed CLIs",
"setupGuideDetectCliDesc": "Click Refresh after installing or updating a CLI so OmniRoute can rescan binaries and versions.",
"setupGuideCustomAgentTitle": "Register custom binary",
"setupGuideCustomAgentDesc": "Use Add Custom Agent when your CLI is not in the built-in list. Provide binary name and version command.",
"setupGuideCommandMissingTitle": "Fix 'command not found'",
"setupGuideCommandMissingDesc": "Ensure the CLI command exists in PATH, open a new terminal session, and rerun Refresh."
},
"autoCombo": {
"title": "Auto-Combo Engine",
@@ -2628,6 +2673,21 @@
"rerank": "Rerank",
"rerankModel": "Rerank Model",
"positionDelta": "Position Change",
"emptyState": "Send a search query to see results"
"emptyState": "Send a search query to see results",
"safeSearchOff": "Off",
"safeSearchModerate": "Moderate",
"safeSearchStrict": "Strict",
"queryPlaceholder": "Enter search query...",
"providerAuto": "auto (cheapest)",
"searchTypeWeb": "web",
"searchTypeNews": "news",
"optionAny": "any",
"timeRangeDay": "Past day",
"timeRangeWeek": "Past week",
"timeRangeMonth": "Past month",
"timeRangeYear": "Past year",
"domainPlaceholder": "example.com",
"requestTimedOut": "Request timed out ({seconds}s)",
"networkError": "Network error"
}
}

View File

@@ -56,7 +56,9 @@
"warning": "警告",
"note": "注記",
"free": "無料",
"skipToContent": "コンテンツにスキップ"
"skipToContent": "コンテンツにスキップ",
"maintenanceServerIssues": "Server is experiencing issues. Some features may be unavailable.",
"maintenanceServerUnreachable": "Server is unreachable. Reconnecting..."
},
"sidebar": {
"home": "ホーム",
@@ -576,7 +578,23 @@
"mitmStep1": "1. Start MITM to route requests through OmniRoute.",
"mitmStep2Prefix": "2. Add",
"mitmStep2Suffix": "to your hosts file as 127.0.0.1.",
"mitmStep3": "3. Open {toolName} and requests will be proxied."
"mitmStep3": "3. Open {toolName} and requests will be proxied.",
"whenToUseLabel": "いつ使用するか",
"openToolDocs": "ツールドキュメントを開く",
"toolUseCases": {
"claude": "クロード コードを使用して強力な計画ワークフローと長い複数ファイルのリファクタリングが必要な場合に使用します。",
"codex": "チームが OpenAI Codex CLI フローとプロファイルベースの認証で標準化されている場合に使用します。",
"droid": "高速コーディングとコマンド実行ループに重点を置いた軽量のターミナル エージェントが必要な場合に使用します。",
"openclaw": "Open Claw スタイルのコーディング エージェントが必要だが、OmniRoute ポリシーを通じてルーティングされる場合に使用します。",
"cline": "エディター内でコーディング エージェントを構成し、OmniRoute モデルを使用したガイド付きセットアップが必要な場合に使用します。",
"kilo": "ワークフローが Kilo Code コマンドと高速反復編集に依存している場合に使用します。",
"cursor": "Cursor でコーディングし、OmniRoute を介してカスタム OpenAI 互換モデルが必要な場合に使用します。",
"continue": "IDE で続行を実行し、移植可能な JSON ベースのプロバイダー構成が必要な場合に使用します。",
"opencode": "ターミナルネイティブのエージェントの実行と OpenCode によるスクリプトによる自動化を希望する場合に使用します。",
"kiro": "Kiro を統合し、OmniRoute からモデルのルーティングを一元的に制御する場合に使用します。",
"antigravity": "Antigravity/Kiro トラフィックを MITM 経由でインターセプトし、OmniRoute にルーティングする必要がある場合に使用します。",
"copilot": "OmniRoute キーとルーティング ルールを適用しながら、Copilot チャット スタイルの UX が必要な場合に使用します。"
}
},
"combos": {
"title": "コンボ",
@@ -1087,7 +1105,13 @@
"resetAllTitle": "すべてのサーキットブレーカーを正常な状態にリセットします",
"resetting": "リセット中...",
"resetAll": "すべてリセット",
"until": "{time} まで"
"until": "{time} まで",
"activeProviders": "{count} active",
"monitoredProviders": "{count} monitored",
"configuredProvidersLabel": "ダッシュボードで設定",
"configuredProvidersHint": "実行時の状態に関係なく、認証情報を持つプロバイダーは /dashboard/providers に保存されます。",
"activeProvidersHint": "リクエストのルーティングが現在有効になっている構成済みプロバイダー。",
"monitoredProvidersHint": "現在、プロバイダーはサーキット ブレーカー ヘルス モニターによって追跡されています。"
},
"limits": {
"title": "制限と割り当て",
@@ -1446,7 +1470,14 @@
"compatProtocolOpenAIResponses": "OpenAI Responses API",
"compatProtocolClaude": "Anthropic Messages",
"tokenRefreshed": "Token refreshed successfully",
"tokenRefreshFailed": "Token refresh failed"
"tokenRefreshFailed": "Token refresh failed",
"compatBadgeUpstreamHeaders": "Headers",
"compatUpstreamAddRow": "Add header",
"compatUpstreamHeaderName": "Header name",
"compatUpstreamHeaderValue": "Value",
"compatUpstreamHeadersHint": "High-privilege setting — same trust level as editing provider API credentials; only trusted admins should use it. Merged after OmniRoute adds auth from the provider API key. If a custom header uses the same name as an existing one (e.g. Authorization), your value fully replaces the auto-generated header (including the Bearer token) — the upstream only sees what you typed, not the key from settings. Misconfiguration can cause 401 or broken upstream auth. One row per header (e.g. extra Authentication for some gateways). Hover or focus the value to preview. Saves on blur, outside click, or closing this panel.",
"compatUpstreamHeadersLabel": "Extra upstream headers",
"compatUpstreamRemoveRow": "Remove row"
},
"settings": {
"title": "設定",
@@ -1849,7 +1880,13 @@
"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}"
"disableFingerprintTitle": "Disable fingerprint for {provider}",
"routingAdvancedGuideTitle": "高度なルーティング ガイダンス",
"routingAdvancedGuideHint1": "予測可能な優先度を得るには Fill First を使用し、公平性を得るにはラウンド ロビンを、レイテンシの回復力を得るには P2C を使用します。",
"routingAdvancedGuideHint2": "プロバイダーによって品質/コストが異なる場合は、バックグラウンド作業についてはコスト最適化から開始し、バランスのとれた摩耗については最も使用されないようにします。",
"comboDefaultsGuideTitle": "コンボのデフォルトを調整する方法",
"comboDefaultsGuideHint1": "低遅延フローでは再試行を低く抑えます。長い世代のタスクの場合にのみタイムアウトを増やします。",
"comboDefaultsGuideHint2": "1 つのプロバイダーがグローバルなデフォルトとは異なるタイムアウト/再試行動作を必要とする場合は、プロバイダー オーバーライドを使用します。"
},
"translator": {
"title": "翻訳者",
@@ -2569,7 +2606,15 @@
"opencodeDetected": "opencode {version} detected",
"opencodeDesc": "Generate a ready-to-use {configFile} with your OmniRoute base URL and all available models — drop it in your project root and run {command}.",
"downloadConfig": "Download {file}",
"downloaded": "Downloaded!"
"downloaded": "Downloaded!",
"setupGuideTitle": "Setup guide",
"openCliTools": "Open CLI Tools",
"setupGuideDetectCliTitle": "Detect installed CLIs",
"setupGuideDetectCliDesc": "Click Refresh after installing or updating a CLI so OmniRoute can rescan binaries and versions.",
"setupGuideCustomAgentTitle": "Register custom binary",
"setupGuideCustomAgentDesc": "Use Add Custom Agent when your CLI is not in the built-in list. Provide binary name and version command.",
"setupGuideCommandMissingTitle": "Fix 'command not found'",
"setupGuideCommandMissingDesc": "Ensure the CLI command exists in PATH, open a new terminal session, and rerun Refresh."
},
"autoCombo": {
"title": "Auto-Combo Engine",
@@ -2628,6 +2673,21 @@
"rerank": "Rerank",
"rerankModel": "Rerank Model",
"positionDelta": "Position Change",
"emptyState": "Send a search query to see results"
"emptyState": "Send a search query to see results",
"safeSearchOff": "Off",
"safeSearchModerate": "Moderate",
"safeSearchStrict": "Strict",
"queryPlaceholder": "Enter search query...",
"providerAuto": "auto (cheapest)",
"searchTypeWeb": "web",
"searchTypeNews": "news",
"optionAny": "any",
"timeRangeDay": "Past day",
"timeRangeWeek": "Past week",
"timeRangeMonth": "Past month",
"timeRangeYear": "Past year",
"domainPlaceholder": "example.com",
"requestTimedOut": "Request timed out ({seconds}s)",
"networkError": "Network error"
}
}

View File

@@ -56,7 +56,9 @@
"warning": "경고",
"note": "참고",
"free": "무료",
"skipToContent": "콘텐츠로 건너뛰기"
"skipToContent": "콘텐츠로 건너뛰기",
"maintenanceServerIssues": "Server is experiencing issues. Some features may be unavailable.",
"maintenanceServerUnreachable": "Server is unreachable. Reconnecting..."
},
"sidebar": {
"home": "홈",
@@ -576,7 +578,23 @@
"mitmStep1": "1. Start MITM to route requests through OmniRoute.",
"mitmStep2Prefix": "2. Add",
"mitmStep2Suffix": "to your hosts file as 127.0.0.1.",
"mitmStep3": "3. Open {toolName} and requests will be proxied."
"mitmStep3": "3. Open {toolName} and requests will be proxied.",
"whenToUseLabel": "언제 사용하나요?",
"openToolDocs": "도구 문서 열기",
"toolUseCases": {
"claude": "Claude Code를 사용하여 강력한 계획 워크플로와 긴 다중 파일 리팩터링을 원할 때 사용하세요.",
"codex": "팀이 OpenAI Codex CLI 흐름 및 프로필 기반 인증으로 표준화된 경우에 사용하세요.",
"droid": "빠른 코딩 및 명령 실행 루프에 초점을 맞춘 경량 터미널 에이전트가 필요할 때 사용하세요.",
"openclaw": "Open Claw 스타일 코딩 에이전트를 원하지만 OmniRoute 정책을 통해 라우팅되는 경우에 사용합니다.",
"cline": "편집기 내에서 코딩 에이전트를 구성하고 OmniRoute 모델을 사용하여 안내 설정을 원하는 경우 사용합니다.",
"kilo": "작업 흐름이 Kilo Code 명령과 빠른 반복 편집에 의존하는 경우에 사용하세요.",
"cursor": "Cursor에서 코딩할 때 사용하고 OmniRoute를 통해 사용자 정의 OpenAI 호환 모델이 필요합니다.",
"continue": "IDE에서 계속을 실행할 때 사용하고 이식 가능한 JSON 기반 공급자 구성이 필요합니다.",
"opencode": "OpenCode를 통해 터미널 기반 에이전트 실행 및 스크립트 자동화를 선호할 때 사용하세요.",
"kiro": "Kiro를 통합하고 OmniRoute에서 중앙에서 모델 라우팅을 제어할 때 사용합니다.",
"antigravity": "Antigravity/Kiro 트래픽이 MITM을 통해 가로채어 OmniRoute로 라우팅되어야 하는 경우에 사용합니다.",
"copilot": "OmniRoute 키와 라우팅 규칙을 적용하면서 Copilot 채팅 스타일 UX를 원할 때 사용하세요."
}
},
"combos": {
"title": "콤보",
@@ -1087,7 +1105,13 @@
"resetAllTitle": "모든 회로 차단기를 정상 상태로 재설정",
"resetting": "재설정 중...",
"resetAll": "모두 재설정",
"until": "{time}까지"
"until": "{time}까지",
"activeProviders": "{count} active",
"monitoredProviders": "{count} monitored",
"configuredProvidersLabel": "대시보드에 구성됨",
"configuredProvidersHint": "런타임 상태에 관계없이 /dashboard/providers에 자격 증명이 저장된 공급자입니다.",
"activeProvidersHint": "현재 요청 라우팅을 위해 활성화된 구성된 공급자입니다.",
"monitoredProvidersHint": "현재 회로 차단기 상태 모니터로 추적되는 공급자입니다."
},
"limits": {
"title": "한도 및 할당량",
@@ -1446,7 +1470,14 @@
"compatProtocolOpenAIResponses": "OpenAI Responses API",
"compatProtocolClaude": "Anthropic Messages",
"tokenRefreshed": "Token refreshed successfully",
"tokenRefreshFailed": "Token refresh failed"
"tokenRefreshFailed": "Token refresh failed",
"compatBadgeUpstreamHeaders": "Headers",
"compatUpstreamAddRow": "Add header",
"compatUpstreamHeaderName": "Header name",
"compatUpstreamHeaderValue": "Value",
"compatUpstreamHeadersHint": "High-privilege setting — same trust level as editing provider API credentials; only trusted admins should use it. Merged after OmniRoute adds auth from the provider API key. If a custom header uses the same name as an existing one (e.g. Authorization), your value fully replaces the auto-generated header (including the Bearer token) — the upstream only sees what you typed, not the key from settings. Misconfiguration can cause 401 or broken upstream auth. One row per header (e.g. extra Authentication for some gateways). Hover or focus the value to preview. Saves on blur, outside click, or closing this panel.",
"compatUpstreamHeadersLabel": "Extra upstream headers",
"compatUpstreamRemoveRow": "Remove row"
},
"settings": {
"title": "설정",
@@ -1849,7 +1880,13 @@
"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}"
"disableFingerprintTitle": "Disable fingerprint for {provider}",
"routingAdvancedGuideTitle": "고급 경로 안내",
"routingAdvancedGuideHint1": "예측 가능한 우선순위를 위해서는 Fill First를 사용하고, 공정성을 위해서는 Round Robin을, 지연 시간 복원력을 위해서는 P2C를 사용하세요.",
"routingAdvancedGuideHint2": "서비스 제공업체의 품질/비용이 다양한 경우 백그라운드 작업에는 비용 선택(Cost Opt)으로 시작하고 균형 잡힌 착용에는 최소 사용(Least Used)으로 시작하세요.",
"comboDefaultsGuideTitle": "콤보 기본값을 조정하는 방법",
"comboDefaultsGuideHint1": "지연 시간이 짧은 흐름에서는 재시도 횟수를 낮게 유지하세요. 긴 세대 작업에 대해서만 시간 제한을 늘립니다.",
"comboDefaultsGuideHint2": "하나의 공급자가 전역 기본값과 다른 시간 초과/재시도 동작을 필요로 하는 경우 공급자 재정의를 사용합니다."
},
"translator": {
"title": "번역기",
@@ -2569,7 +2606,15 @@
"opencodeDetected": "opencode {version} detected",
"opencodeDesc": "Generate a ready-to-use {configFile} with your OmniRoute base URL and all available models — drop it in your project root and run {command}.",
"downloadConfig": "Download {file}",
"downloaded": "Downloaded!"
"downloaded": "Downloaded!",
"setupGuideTitle": "Setup guide",
"openCliTools": "Open CLI Tools",
"setupGuideDetectCliTitle": "Detect installed CLIs",
"setupGuideDetectCliDesc": "Click Refresh after installing or updating a CLI so OmniRoute can rescan binaries and versions.",
"setupGuideCustomAgentTitle": "Register custom binary",
"setupGuideCustomAgentDesc": "Use Add Custom Agent when your CLI is not in the built-in list. Provide binary name and version command.",
"setupGuideCommandMissingTitle": "Fix 'command not found'",
"setupGuideCommandMissingDesc": "Ensure the CLI command exists in PATH, open a new terminal session, and rerun Refresh."
},
"autoCombo": {
"title": "Auto-Combo Engine",
@@ -2628,6 +2673,21 @@
"rerank": "Rerank",
"rerankModel": "Rerank Model",
"positionDelta": "Position Change",
"emptyState": "Send a search query to see results"
"emptyState": "Send a search query to see results",
"safeSearchOff": "Off",
"safeSearchModerate": "Moderate",
"safeSearchStrict": "Strict",
"queryPlaceholder": "Enter search query...",
"providerAuto": "auto (cheapest)",
"searchTypeWeb": "web",
"searchTypeNews": "news",
"optionAny": "any",
"timeRangeDay": "Past day",
"timeRangeWeek": "Past week",
"timeRangeMonth": "Past month",
"timeRangeYear": "Past year",
"domainPlaceholder": "example.com",
"requestTimedOut": "Request timed out ({seconds}s)",
"networkError": "Network error"
}
}

View File

@@ -56,7 +56,9 @@
"warning": "Amaran",
"note": "Nota",
"free": "Percuma",
"skipToContent": "Langkau ke kandungan"
"skipToContent": "Langkau ke kandungan",
"maintenanceServerIssues": "Server is experiencing issues. Some features may be unavailable.",
"maintenanceServerUnreachable": "Server is unreachable. Reconnecting..."
},
"sidebar": {
"home": "Rumah",
@@ -576,7 +578,23 @@
"mitmStep1": "1. Start MITM to route requests through OmniRoute.",
"mitmStep2Prefix": "2. Add",
"mitmStep2Suffix": "to your hosts file as 127.0.0.1.",
"mitmStep3": "3. Open {toolName} and requests will be proxied."
"mitmStep3": "3. Open {toolName} and requests will be proxied.",
"whenToUseLabel": "Bila nak guna",
"openToolDocs": "Buka dokumen alat",
"toolUseCases": {
"claude": "Gunakan apabila anda mahukan aliran kerja perancangan yang kukuh dan refactor berbilang fail panjang dengan Kod Claude.",
"codex": "Gunakan apabila pasukan anda diseragamkan pada aliran OpenAI Codex CLI dan pengesahan berasaskan profil.",
"droid": "Gunakan apabila anda memerlukan ejen terminal ringan yang menumpukan pada pengekodan pantas dan gelung pelaksanaan perintah.",
"openclaw": "Gunakan apabila anda mahukan ejen pengekodan gaya Open Claw tetapi dihalakan melalui dasar OmniRoute.",
"cline": "Gunakan apabila anda mengkonfigurasi ejen pengekodan dalam editor dan mahukan persediaan berpandu dengan model OmniRoute.",
"kilo": "Gunakan apabila aliran kerja anda bergantung pada arahan Kilo Code dan pengeditan berulang yang pantas.",
"cursor": "Gunakan semasa pengekodan dalam Kursor dan anda memerlukan model serasi OpenAI tersuai melalui OmniRoute.",
"continue": "Gunakan semasa menjalankan Teruskan dalam IDE dan anda memerlukan konfigurasi pembekal berasaskan JSON mudah alih.",
"opencode": "Gunakan apabila anda lebih suka ejen terminal-native run dan automasi skrip melalui OpenCode.",
"kiro": "Gunakan apabila menyepadukan Kiro dan mengawal penghalaan model secara berpusat daripada OmniRoute.",
"antigravity": "Gunakan apabila trafik Antigraviti/Kiro mesti dipintas melalui MITM dan dihalakan ke OmniRoute.",
"copilot": "Gunakan apabila anda mahu Copilot gaya sembang UX sambil menguatkuasakan kekunci OmniRoute dan peraturan penghalaan."
}
},
"combos": {
"title": "Kombo",
@@ -1087,7 +1105,13 @@
"resetAllTitle": "Tetapkan semula semua pemutus litar kepada keadaan sihat",
"resetting": "Menetapkan semula...",
"resetAll": "Tetapkan Semula Semua",
"until": "Sehingga {time}"
"until": "Sehingga {time}",
"activeProviders": "{count} active",
"monitoredProviders": "{count} monitored",
"configuredProvidersLabel": "Dikonfigurasikan dalam papan pemuka",
"configuredProvidersHint": "Pembekal dengan bukti kelayakan disimpan dalam /papan pemuka/penyedia, tanpa mengira keadaan masa jalan.",
"activeProvidersHint": "Penyedia yang dikonfigurasikan pada masa ini didayakan untuk permintaan penghalaan.",
"monitoredProvidersHint": "Pembekal kini dijejaki oleh pemantau kesihatan pemutus litar."
},
"limits": {
"title": "Had & Kuota",
@@ -1446,7 +1470,14 @@
"compatProtocolOpenAIResponses": "OpenAI Responses API",
"compatProtocolClaude": "Anthropic Messages",
"tokenRefreshed": "Token refreshed successfully",
"tokenRefreshFailed": "Token refresh failed"
"tokenRefreshFailed": "Token refresh failed",
"compatBadgeUpstreamHeaders": "Headers",
"compatUpstreamAddRow": "Add header",
"compatUpstreamHeaderName": "Header name",
"compatUpstreamHeaderValue": "Value",
"compatUpstreamHeadersHint": "High-privilege setting — same trust level as editing provider API credentials; only trusted admins should use it. Merged after OmniRoute adds auth from the provider API key. If a custom header uses the same name as an existing one (e.g. Authorization), your value fully replaces the auto-generated header (including the Bearer token) — the upstream only sees what you typed, not the key from settings. Misconfiguration can cause 401 or broken upstream auth. One row per header (e.g. extra Authentication for some gateways). Hover or focus the value to preview. Saves on blur, outside click, or closing this panel.",
"compatUpstreamHeadersLabel": "Extra upstream headers",
"compatUpstreamRemoveRow": "Remove row"
},
"settings": {
"title": "tetapan",
@@ -1849,7 +1880,13 @@
"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}"
"disableFingerprintTitle": "Disable fingerprint for {provider}",
"routingAdvancedGuideTitle": "Panduan laluan lanjutan",
"routingAdvancedGuideHint1": "Gunakan Fill First untuk keutamaan yang boleh diramal, Round Robin untuk keadilan dan P2C untuk daya tahan latensi.",
"routingAdvancedGuideHint2": "Jika pembekal berbeza dalam kualiti/kos, mulakan dengan Pilihan Kos untuk kerja latar belakang dan Paling Kurang Digunakan untuk pemakaian seimbang.",
"comboDefaultsGuideTitle": "Bagaimana untuk menala lalai kombo",
"comboDefaultsGuideHint1": "Pastikan percubaan semula rendah dalam aliran kependaman rendah; tambahkan tamat masa hanya untuk tugas generasi panjang.",
"comboDefaultsGuideHint2": "Gunakan penggantian pembekal apabila satu pembekal memerlukan gelagat tamat masa/cuba semula yang berbeza daripada lalai global."
},
"translator": {
"title": "Penterjemah",
@@ -2569,7 +2606,15 @@
"opencodeDetected": "opencode {version} detected",
"opencodeDesc": "Generate a ready-to-use {configFile} with your OmniRoute base URL and all available models — drop it in your project root and run {command}.",
"downloadConfig": "Download {file}",
"downloaded": "Downloaded!"
"downloaded": "Downloaded!",
"setupGuideTitle": "Setup guide",
"openCliTools": "Open CLI Tools",
"setupGuideDetectCliTitle": "Detect installed CLIs",
"setupGuideDetectCliDesc": "Click Refresh after installing or updating a CLI so OmniRoute can rescan binaries and versions.",
"setupGuideCustomAgentTitle": "Register custom binary",
"setupGuideCustomAgentDesc": "Use Add Custom Agent when your CLI is not in the built-in list. Provide binary name and version command.",
"setupGuideCommandMissingTitle": "Fix 'command not found'",
"setupGuideCommandMissingDesc": "Ensure the CLI command exists in PATH, open a new terminal session, and rerun Refresh."
},
"autoCombo": {
"title": "Auto-Combo Engine",
@@ -2628,6 +2673,21 @@
"rerank": "Rerank",
"rerankModel": "Rerank Model",
"positionDelta": "Position Change",
"emptyState": "Send a search query to see results"
"emptyState": "Send a search query to see results",
"safeSearchOff": "Off",
"safeSearchModerate": "Moderate",
"safeSearchStrict": "Strict",
"queryPlaceholder": "Enter search query...",
"providerAuto": "auto (cheapest)",
"searchTypeWeb": "web",
"searchTypeNews": "news",
"optionAny": "any",
"timeRangeDay": "Past day",
"timeRangeWeek": "Past week",
"timeRangeMonth": "Past month",
"timeRangeYear": "Past year",
"domainPlaceholder": "example.com",
"requestTimedOut": "Request timed out ({seconds}s)",
"networkError": "Network error"
}
}

View File

@@ -56,7 +56,9 @@
"warning": "Waarschuwing",
"note": "Let op",
"free": "Gratis",
"skipToContent": "Ga naar de inhoud"
"skipToContent": "Ga naar de inhoud",
"maintenanceServerIssues": "Server is experiencing issues. Some features may be unavailable.",
"maintenanceServerUnreachable": "Server is unreachable. Reconnecting..."
},
"sidebar": {
"home": "Thuis",
@@ -576,7 +578,23 @@
"mitmStep1": "1. Start MITM to route requests through OmniRoute.",
"mitmStep2Prefix": "2. Add",
"mitmStep2Suffix": "to your hosts file as 127.0.0.1.",
"mitmStep3": "3. Open {toolName} and requests will be proxied."
"mitmStep3": "3. Open {toolName} and requests will be proxied.",
"whenToUseLabel": "Wanneer te gebruiken",
"openToolDocs": "Gereedschapsdocumentatie openen",
"toolUseCases": {
"claude": "Gebruik deze optie als u krachtige planningsworkflows en lange refactoren van meerdere bestanden wilt met Claude Code.",
"codex": "Gebruik wanneer uw team is gestandaardiseerd op OpenAI Codex CLI-stromen en op profielen gebaseerde authenticatie.",
"droid": "Gebruik deze wanneer u een lichtgewicht terminalagent nodig heeft die zich richt op snelle codering en opdrachtuitvoeringslussen.",
"openclaw": "Gebruik dit wanneer u een codeeragent in Open Claw-stijl wilt, maar gerouteerd via OmniRoute-beleid.",
"cline": "Gebruik deze optie wanneer u codeeragenten in editors configureert en begeleide installatie wilt met OmniRoute-modellen.",
"kilo": "Gebruik dit wanneer uw workflow afhankelijk is van Kilo Code-opdrachten en snelle iteratieve bewerkingen.",
"cursor": "Gebruik dit bij het coderen in Cursor en u hebt aangepaste OpenAI-compatibele modellen nodig via OmniRoute.",
"continue": "Gebruik dit wanneer u Doorgaan in IDE's uitvoert en u een draagbare, op JSON gebaseerde providerconfiguratie nodig hebt.",
"opencode": "Gebruik dit wanneer u de voorkeur geeft aan terminal-native agentruns en scriptautomatisering via OpenCode.",
"kiro": "Te gebruiken bij het integreren van Kiro en het centraal beheren van modelrouting vanuit OmniRoute.",
"antigravity": "Gebruik wanneer Antigravity/Kiro-verkeer moet worden onderschept via MITM en naar OmniRoute moet worden gerouteerd.",
"copilot": "Gebruik wanneer u UX in Copilot-chatstijl wilt terwijl u OmniRoute-sleutels en routeringsregels afdwingt."
}
},
"combos": {
"title": "Combo's",
@@ -1087,7 +1105,13 @@
"resetAllTitle": "Reset alle stroomonderbrekers naar de gezonde status",
"resetting": "Resetten...",
"resetAll": "Alles resetten",
"until": "Tot {time}"
"until": "Tot {time}",
"activeProviders": "{count} active",
"monitoredProviders": "{count} monitored",
"configuredProvidersLabel": "Geconfigureerd in dashboard",
"configuredProvidersHint": "Providers met inloggegevens opgeslagen in /dashboard/providers, ongeacht de runtimestatus.",
"activeProvidersHint": "Geconfigureerde providers die momenteel zijn ingeschakeld voor het routeren van verzoeken.",
"monitoredProvidersHint": "Providers worden momenteel gevolgd door gezondheidsmonitors van stroomonderbrekers."
},
"limits": {
"title": "Limieten en quota's",
@@ -1446,7 +1470,14 @@
"compatProtocolOpenAIResponses": "OpenAI Responses API",
"compatProtocolClaude": "Anthropic Messages",
"tokenRefreshed": "Token refreshed successfully",
"tokenRefreshFailed": "Token refresh failed"
"tokenRefreshFailed": "Token refresh failed",
"compatBadgeUpstreamHeaders": "Headers",
"compatUpstreamAddRow": "Add header",
"compatUpstreamHeaderName": "Header name",
"compatUpstreamHeaderValue": "Value",
"compatUpstreamHeadersHint": "High-privilege setting — same trust level as editing provider API credentials; only trusted admins should use it. Merged after OmniRoute adds auth from the provider API key. If a custom header uses the same name as an existing one (e.g. Authorization), your value fully replaces the auto-generated header (including the Bearer token) — the upstream only sees what you typed, not the key from settings. Misconfiguration can cause 401 or broken upstream auth. One row per header (e.g. extra Authentication for some gateways). Hover or focus the value to preview. Saves on blur, outside click, or closing this panel.",
"compatUpstreamHeadersLabel": "Extra upstream headers",
"compatUpstreamRemoveRow": "Remove row"
},
"settings": {
"title": "Instellingen",
@@ -1849,7 +1880,13 @@
"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}"
"disableFingerprintTitle": "Disable fingerprint for {provider}",
"routingAdvancedGuideTitle": "Geavanceerde routebegeleiding",
"routingAdvancedGuideHint1": "Gebruik Fill First voor voorspelbare prioriteit, Round Robin voor eerlijkheid en P2C voor latency-veerkracht.",
"routingAdvancedGuideHint2": "Als aanbieders variëren in kwaliteit/kosten, begin dan met Kosten Opt voor achtergrondwerk en Minst Gebruikt voor evenwichtige slijtage.",
"comboDefaultsGuideTitle": "Combo-standaardinstellingen afstemmen",
"comboDefaultsGuideHint1": "Houd het aantal nieuwe pogingen laag bij stromen met lage latentie; verhoog de time-out alleen voor lange generatietaken.",
"comboDefaultsGuideHint2": "Gebruik provideroverschrijvingen wanneer een provider ander time-out/opnieuw gedrag nodig heeft dan de algemene standaardwaarden."
},
"translator": {
"title": "Vertaler",
@@ -2569,7 +2606,15 @@
"opencodeDetected": "opencode {version} detected",
"opencodeDesc": "Generate a ready-to-use {configFile} with your OmniRoute base URL and all available models — drop it in your project root and run {command}.",
"downloadConfig": "Download {file}",
"downloaded": "Downloaded!"
"downloaded": "Downloaded!",
"setupGuideTitle": "Setup guide",
"openCliTools": "Open CLI Tools",
"setupGuideDetectCliTitle": "Detect installed CLIs",
"setupGuideDetectCliDesc": "Click Refresh after installing or updating a CLI so OmniRoute can rescan binaries and versions.",
"setupGuideCustomAgentTitle": "Register custom binary",
"setupGuideCustomAgentDesc": "Use Add Custom Agent when your CLI is not in the built-in list. Provide binary name and version command.",
"setupGuideCommandMissingTitle": "Fix 'command not found'",
"setupGuideCommandMissingDesc": "Ensure the CLI command exists in PATH, open a new terminal session, and rerun Refresh."
},
"autoCombo": {
"title": "Auto-Combo Engine",
@@ -2628,6 +2673,21 @@
"rerank": "Rerank",
"rerankModel": "Rerank Model",
"positionDelta": "Position Change",
"emptyState": "Send a search query to see results"
"emptyState": "Send a search query to see results",
"safeSearchOff": "Off",
"safeSearchModerate": "Moderate",
"safeSearchStrict": "Strict",
"queryPlaceholder": "Enter search query...",
"providerAuto": "auto (cheapest)",
"searchTypeWeb": "web",
"searchTypeNews": "news",
"optionAny": "any",
"timeRangeDay": "Past day",
"timeRangeWeek": "Past week",
"timeRangeMonth": "Past month",
"timeRangeYear": "Past year",
"domainPlaceholder": "example.com",
"requestTimedOut": "Request timed out ({seconds}s)",
"networkError": "Network error"
}
}

View File

@@ -56,7 +56,9 @@
"warning": "Advarsel",
"note": "Merk",
"free": "Gratis",
"skipToContent": "Gå til innhold"
"skipToContent": "Gå til innhold",
"maintenanceServerIssues": "Server is experiencing issues. Some features may be unavailable.",
"maintenanceServerUnreachable": "Server is unreachable. Reconnecting..."
},
"sidebar": {
"home": "Hjem",
@@ -576,7 +578,23 @@
"mitmStep1": "1. Start MITM to route requests through OmniRoute.",
"mitmStep2Prefix": "2. Add",
"mitmStep2Suffix": "to your hosts file as 127.0.0.1.",
"mitmStep3": "3. Open {toolName} and requests will be proxied."
"mitmStep3": "3. Open {toolName} and requests will be proxied.",
"whenToUseLabel": "Når du skal bruke",
"openToolDocs": "Åpne verktøydokumenter",
"toolUseCases": {
"claude": "Bruk når du vil ha sterke planleggingsarbeidsflyter og lange multifilrefaktorer med Claude Code.",
"codex": "Bruk når teamet ditt er standardisert på OpenAI Codex CLI-flyter og profilbasert autentisering.",
"droid": "Bruk når du trenger en lettvekts terminalagent fokusert på rask koding og løkker for kommandoutførelse.",
"openclaw": "Bruk når du vil ha en Open Claw-kodingsagent, men rutet gjennom OmniRoute-policyer.",
"cline": "Bruk når du konfigurerer kodeagenter inne i redaktører og ønsker veiledet oppsett med OmniRoute-modeller.",
"kilo": "Bruk når arbeidsflyten din avhenger av Kilo Code-kommandoer og raske iterative redigeringer.",
"cursor": "Bruk når du koder i Cursor og du trenger tilpassede OpenAI-kompatible modeller gjennom OmniRoute.",
"continue": "Bruk når du kjører Continue i IDEer og du trenger bærbar JSON-basert leverandørkonfigurasjon.",
"opencode": "Bruk når du foretrekker terminal-native agentkjøringer og skriptautomatisering via OpenCode.",
"kiro": "Brukes når du integrerer Kiro og kontrollerer modellruting sentralt fra OmniRoute.",
"antigravity": "Brukes når Antigravity/Kiro-trafikk må avskjæres gjennom MITM og rutes til OmniRoute.",
"copilot": "Bruk når du vil ha Copilot chat-stil UX mens du håndhever OmniRoute-nøkler og rutingsregler."
}
},
"combos": {
"title": "Combos",
@@ -1087,7 +1105,13 @@
"resetAllTitle": "Tilbakestill alle effektbrytere til sunn tilstand",
"resetting": "Tilbakestiller...",
"resetAll": "Tilbakestill alle",
"until": "Inntil {time}"
"until": "Inntil {time}",
"activeProviders": "{count} active",
"monitoredProviders": "{count} monitored",
"configuredProvidersLabel": "Konfigurert i dashbordet",
"configuredProvidersHint": "Leverandører med legitimasjon lagret i /dashboard/leverandører, uavhengig av kjøretidstilstand.",
"activeProvidersHint": "Konfigurerte leverandører er for øyeblikket aktivert for rutingforespørsler.",
"monitoredProvidersHint": "Leverandører spores for øyeblikket av strømbryterhelsemonitorer."
},
"limits": {
"title": "Grenser og kvoter",
@@ -1446,7 +1470,14 @@
"compatProtocolOpenAIResponses": "OpenAI Responses API",
"compatProtocolClaude": "Anthropic Messages",
"tokenRefreshed": "Token refreshed successfully",
"tokenRefreshFailed": "Token refresh failed"
"tokenRefreshFailed": "Token refresh failed",
"compatBadgeUpstreamHeaders": "Headers",
"compatUpstreamAddRow": "Add header",
"compatUpstreamHeaderName": "Header name",
"compatUpstreamHeaderValue": "Value",
"compatUpstreamHeadersHint": "High-privilege setting — same trust level as editing provider API credentials; only trusted admins should use it. Merged after OmniRoute adds auth from the provider API key. If a custom header uses the same name as an existing one (e.g. Authorization), your value fully replaces the auto-generated header (including the Bearer token) — the upstream only sees what you typed, not the key from settings. Misconfiguration can cause 401 or broken upstream auth. One row per header (e.g. extra Authentication for some gateways). Hover or focus the value to preview. Saves on blur, outside click, or closing this panel.",
"compatUpstreamHeadersLabel": "Extra upstream headers",
"compatUpstreamRemoveRow": "Remove row"
},
"settings": {
"title": "Innstillinger",
@@ -1849,7 +1880,13 @@
"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}"
"disableFingerprintTitle": "Disable fingerprint for {provider}",
"routingAdvancedGuideTitle": "Avansert ruteveiledning",
"routingAdvancedGuideHint1": "Bruk Fill First for forutsigbar prioritet, Round Robin for rettferdighet og P2C for latensmotstandskraft.",
"routingAdvancedGuideHint2": "Hvis leverandørene varierer i kvalitet/kostnad, start med Cost Opt for bakgrunnsarbeid og Minst brukt for balansert slitasje.",
"comboDefaultsGuideTitle": "Hvordan justere kombinasjonsstandarder",
"comboDefaultsGuideHint1": "Hold lave gjenforsøk i flyter med lav latens; øke tidsavbruddet bare for langgenerasjonsoppgaver.",
"comboDefaultsGuideHint2": "Bruk leverandøroverstyringer når en leverandør trenger annen tidsavbrudd/forsøk på nytt enn globale standardinnstillinger."
},
"translator": {
"title": "Oversetter",
@@ -2569,7 +2606,15 @@
"opencodeDetected": "opencode {version} detected",
"opencodeDesc": "Generate a ready-to-use {configFile} with your OmniRoute base URL and all available models — drop it in your project root and run {command}.",
"downloadConfig": "Download {file}",
"downloaded": "Downloaded!"
"downloaded": "Downloaded!",
"setupGuideTitle": "Setup guide",
"openCliTools": "Open CLI Tools",
"setupGuideDetectCliTitle": "Detect installed CLIs",
"setupGuideDetectCliDesc": "Click Refresh after installing or updating a CLI so OmniRoute can rescan binaries and versions.",
"setupGuideCustomAgentTitle": "Register custom binary",
"setupGuideCustomAgentDesc": "Use Add Custom Agent when your CLI is not in the built-in list. Provide binary name and version command.",
"setupGuideCommandMissingTitle": "Fix 'command not found'",
"setupGuideCommandMissingDesc": "Ensure the CLI command exists in PATH, open a new terminal session, and rerun Refresh."
},
"autoCombo": {
"title": "Auto-Combo Engine",
@@ -2628,6 +2673,21 @@
"rerank": "Rerank",
"rerankModel": "Rerank Model",
"positionDelta": "Position Change",
"emptyState": "Send a search query to see results"
"emptyState": "Send a search query to see results",
"safeSearchOff": "Off",
"safeSearchModerate": "Moderate",
"safeSearchStrict": "Strict",
"queryPlaceholder": "Enter search query...",
"providerAuto": "auto (cheapest)",
"searchTypeWeb": "web",
"searchTypeNews": "news",
"optionAny": "any",
"timeRangeDay": "Past day",
"timeRangeWeek": "Past week",
"timeRangeMonth": "Past month",
"timeRangeYear": "Past year",
"domainPlaceholder": "example.com",
"requestTimedOut": "Request timed out ({seconds}s)",
"networkError": "Network error"
}
}

View File

@@ -56,7 +56,9 @@
"warning": "Babala",
"note": "Tandaan",
"free": "Libre",
"skipToContent": "Lumaktaw sa nilalaman"
"skipToContent": "Lumaktaw sa nilalaman",
"maintenanceServerIssues": "Server is experiencing issues. Some features may be unavailable.",
"maintenanceServerUnreachable": "Server is unreachable. Reconnecting..."
},
"sidebar": {
"home": "Bahay",
@@ -576,7 +578,23 @@
"mitmStep1": "1. Start MITM to route requests through OmniRoute.",
"mitmStep2Prefix": "2. Add",
"mitmStep2Suffix": "to your hosts file as 127.0.0.1.",
"mitmStep3": "3. Open {toolName} and requests will be proxied."
"mitmStep3": "3. Open {toolName} and requests will be proxied.",
"whenToUseLabel": "Kailan gagamitin",
"openToolDocs": "Buksan ang tool docs",
"toolUseCases": {
"claude": "Gamitin kapag gusto mo ng matibay na mga daloy ng trabaho sa pagpaplano at mahabang multi-file refactor na may Claude Code.",
"codex": "Gamitin kapag ang iyong koponan ay na-standardize sa mga daloy ng OpenAI Codex CLI at profile-based na auth.",
"droid": "Gamitin kapag kailangan mo ng lightweight na terminal agent na nakatuon sa mabilis na coding at command execution loops.",
"openclaw": "Gamitin kapag gusto mo ng Open Claw style coding agent ngunit naruta sa pamamagitan ng mga patakaran ng OmniRoute.",
"cline": "Gamitin kapag nag-configure ka ng mga ahente ng coding sa loob ng mga editor at gusto mo ng may gabay na pag-setup gamit ang mga modelong OmniRoute.",
"kilo": "Gamitin kapag nakadepende ang iyong daloy ng trabaho sa mga command ng Kilo Code at mabilis na umuulit na pag-edit.",
"cursor": "Gamitin kapag nagko-coding sa Cursor at kailangan mo ng mga custom na modelong katugma sa OpenAI sa pamamagitan ng OmniRoute.",
"continue": "Gamitin kapag nagpapatakbo ng Magpatuloy sa mga IDE at kailangan mo ng portable JSON-based na configuration ng provider.",
"opencode": "Gamitin kapag mas gusto mo ang terminal-native agent run at scripted automation sa pamamagitan ng OpenCode.",
"kiro": "Gamitin kapag isinasama ang Kiro at kinokontrol ang pagruruta ng modelo sa gitna mula sa OmniRoute.",
"antigravity": "Gamitin kapag ang trapiko ng Antigravity/Kiro ay dapat ma-intercept sa MITM at iruta sa OmniRoute.",
"copilot": "Gamitin kapag gusto mong Copilot chat style UX habang ipinapatupad ang mga OmniRoute key at mga panuntunan sa pagruruta."
}
},
"combos": {
"title": "Mga combo",
@@ -1087,7 +1105,13 @@
"resetAllTitle": "I-reset ang lahat ng mga circuit breaker sa malusog na estado",
"resetting": "Nire-reset...",
"resetAll": "I-reset Lahat",
"until": "Hanggang {time}"
"until": "Hanggang {time}",
"activeProviders": "{count} active",
"monitoredProviders": "{count} monitored",
"configuredProvidersLabel": "Na-configure sa dashboard",
"configuredProvidersHint": "Mga provider na may mga kredensyal na naka-save sa /dashboard/providers, anuman ang estado ng runtime.",
"activeProvidersHint": "Kasalukuyang pinagana ang mga naka-configure na provider para sa mga kahilingan sa pagruruta.",
"monitoredProvidersHint": "Ang mga provider ay kasalukuyang sinusubaybayan ng mga circuit-breaker na monitor ng kalusugan."
},
"limits": {
"title": "Mga Limitasyon at Quota",
@@ -1446,7 +1470,14 @@
"compatProtocolOpenAIResponses": "OpenAI Responses API",
"compatProtocolClaude": "Anthropic Messages",
"tokenRefreshed": "Token refreshed successfully",
"tokenRefreshFailed": "Token refresh failed"
"tokenRefreshFailed": "Token refresh failed",
"compatBadgeUpstreamHeaders": "Headers",
"compatUpstreamAddRow": "Add header",
"compatUpstreamHeaderName": "Header name",
"compatUpstreamHeaderValue": "Value",
"compatUpstreamHeadersHint": "High-privilege setting — same trust level as editing provider API credentials; only trusted admins should use it. Merged after OmniRoute adds auth from the provider API key. If a custom header uses the same name as an existing one (e.g. Authorization), your value fully replaces the auto-generated header (including the Bearer token) — the upstream only sees what you typed, not the key from settings. Misconfiguration can cause 401 or broken upstream auth. One row per header (e.g. extra Authentication for some gateways). Hover or focus the value to preview. Saves on blur, outside click, or closing this panel.",
"compatUpstreamHeadersLabel": "Extra upstream headers",
"compatUpstreamRemoveRow": "Remove row"
},
"settings": {
"title": "Mga setting",
@@ -1849,7 +1880,13 @@
"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}"
"disableFingerprintTitle": "Disable fingerprint for {provider}",
"routingAdvancedGuideTitle": "Gabay sa advanced na pagruruta",
"routingAdvancedGuideHint1": "Gamitin ang Fill First para sa predictable priority, Round Robin para sa fairness, at P2C para sa latency resilience.",
"routingAdvancedGuideHint2": "Kung iba-iba ang kalidad/gastos ng mga provider, magsimula sa Cost Opt para sa background na trabaho at Least Used para sa balanseng pagsusuot.",
"comboDefaultsGuideTitle": "Paano i-tune ang mga default ng combo",
"comboDefaultsGuideHint1": "Panatilihing mababa ang mga muling pagsubok sa mga daloy na mababa ang latency; taasan ang timeout para lang sa mga gawaing pang-generation.",
"comboDefaultsGuideHint2": "Gumamit ng mga override ng provider kapag ang isang provider ay nangangailangan ng iba't ibang gawi sa pag-timeout/subukang muli kaysa sa mga pandaigdigang default."
},
"translator": {
"title": "Tagasalin",
@@ -2569,7 +2606,15 @@
"opencodeDetected": "opencode {version} detected",
"opencodeDesc": "Generate a ready-to-use {configFile} with your OmniRoute base URL and all available models — drop it in your project root and run {command}.",
"downloadConfig": "Download {file}",
"downloaded": "Downloaded!"
"downloaded": "Downloaded!",
"setupGuideTitle": "Setup guide",
"openCliTools": "Open CLI Tools",
"setupGuideDetectCliTitle": "Detect installed CLIs",
"setupGuideDetectCliDesc": "Click Refresh after installing or updating a CLI so OmniRoute can rescan binaries and versions.",
"setupGuideCustomAgentTitle": "Register custom binary",
"setupGuideCustomAgentDesc": "Use Add Custom Agent when your CLI is not in the built-in list. Provide binary name and version command.",
"setupGuideCommandMissingTitle": "Fix 'command not found'",
"setupGuideCommandMissingDesc": "Ensure the CLI command exists in PATH, open a new terminal session, and rerun Refresh."
},
"autoCombo": {
"title": "Auto-Combo Engine",
@@ -2628,6 +2673,21 @@
"rerank": "Rerank",
"rerankModel": "Rerank Model",
"positionDelta": "Position Change",
"emptyState": "Send a search query to see results"
"emptyState": "Send a search query to see results",
"safeSearchOff": "Off",
"safeSearchModerate": "Moderate",
"safeSearchStrict": "Strict",
"queryPlaceholder": "Enter search query...",
"providerAuto": "auto (cheapest)",
"searchTypeWeb": "web",
"searchTypeNews": "news",
"optionAny": "any",
"timeRangeDay": "Past day",
"timeRangeWeek": "Past week",
"timeRangeMonth": "Past month",
"timeRangeYear": "Past year",
"domainPlaceholder": "example.com",
"requestTimedOut": "Request timed out ({seconds}s)",
"networkError": "Network error"
}
}

View File

@@ -56,7 +56,9 @@
"warning": "Ostrzeżenie",
"note": "Uwaga",
"free": "Bezpłatny",
"skipToContent": "Przejdź do treści"
"skipToContent": "Przejdź do treści",
"maintenanceServerIssues": "Server is experiencing issues. Some features may be unavailable.",
"maintenanceServerUnreachable": "Server is unreachable. Reconnecting..."
},
"sidebar": {
"home": "Dom",
@@ -576,7 +578,23 @@
"mitmStep1": "1. Start MITM to route requests through OmniRoute.",
"mitmStep2Prefix": "2. Add",
"mitmStep2Suffix": "to your hosts file as 127.0.0.1.",
"mitmStep3": "3. Open {toolName} and requests will be proxied."
"mitmStep3": "3. Open {toolName} and requests will be proxied.",
"whenToUseLabel": "Kiedy używać",
"openToolDocs": "Otwórz dokumentację narzędzi",
"toolUseCases": {
"claude": "Użyj, jeśli chcesz mieć silne przepływy pracy związane z planowaniem i długie refaktoryzacje wielu plików za pomocą Claude Code.",
"codex": "Użyj, gdy Twój zespół jest ujednolicony w zakresie przepływów CLI OpenAI Codex i uwierzytelniania opartego na profilach.",
"droid": "Użyj, gdy potrzebujesz lekkiego agenta terminalowego skupionego na szybkim kodowaniu i pętlach wykonywania poleceń.",
"openclaw": "Użyj, jeśli chcesz agenta kodującego w stylu Open Claw, ale kierowanego przez zasady OmniRoute.",
"cline": "Użyj, gdy konfigurujesz agentów kodujących w edytorach i chcesz przeprowadzić konfigurację z pomocą modeli OmniRoute.",
"kilo": "Użyj, gdy przepływ pracy zależy od poleceń Kilo Code i szybkich edycji iteracyjnych.",
"cursor": "Użyj podczas kodowania w programie Cursor, jeśli potrzebujesz niestandardowych modeli zgodnych z OpenAI za pośrednictwem OmniRoute.",
"continue": "Użyj podczas uruchamiania Kontynuuj w środowiskach IDE, jeśli potrzebujesz przenośnej konfiguracji dostawcy opartej na formacie JSON.",
"opencode": "Użyj, jeśli wolisz uruchamianie agentów natywnych w terminalu i automatyzację skryptów za pośrednictwem OpenCode.",
"kiro": "Użyj podczas integracji Kiro i centralnego sterowania routingiem modeli z OmniRoute.",
"antigravity": "Użyj, gdy ruch antygrawitacyjny/Kiro musi zostać przechwycony przez MITM i skierowany do OmniRoute.",
"copilot": "Użyj, jeśli chcesz mieć UX w stylu czatu Copilot, jednocześnie wymuszając klucze OmniRoute i reguły routingu."
}
},
"combos": {
"title": "Kombinacje",
@@ -1087,7 +1105,13 @@
"resetAllTitle": "Zresetuj wszystkie wyłączniki automatyczne do stanu prawidłowego",
"resetting": "Resetuję...",
"resetAll": "Zresetuj wszystko",
"until": "Do {time}"
"until": "Do {time}",
"activeProviders": "{count} active",
"monitoredProviders": "{count} monitored",
"configuredProvidersLabel": "Skonfigurowane w panelu kontrolnym",
"configuredProvidersHint": "Dostawcy z poświadczeniami zapisanymi w /dashboard/providers, niezależnie od stanu środowiska wykonawczego.",
"activeProvidersHint": "Skonfigurowani dostawcy aktualnie obsługują żądania routingu.",
"monitoredProvidersHint": "Dostawcy są obecnie śledzeni przez monitory stanu wyłączników."
},
"limits": {
"title": "Limity i kwoty",
@@ -1446,7 +1470,14 @@
"compatProtocolOpenAIResponses": "OpenAI Responses API",
"compatProtocolClaude": "Anthropic Messages",
"tokenRefreshed": "Token refreshed successfully",
"tokenRefreshFailed": "Token refresh failed"
"tokenRefreshFailed": "Token refresh failed",
"compatBadgeUpstreamHeaders": "Headers",
"compatUpstreamAddRow": "Add header",
"compatUpstreamHeaderName": "Header name",
"compatUpstreamHeaderValue": "Value",
"compatUpstreamHeadersHint": "High-privilege setting — same trust level as editing provider API credentials; only trusted admins should use it. Merged after OmniRoute adds auth from the provider API key. If a custom header uses the same name as an existing one (e.g. Authorization), your value fully replaces the auto-generated header (including the Bearer token) — the upstream only sees what you typed, not the key from settings. Misconfiguration can cause 401 or broken upstream auth. One row per header (e.g. extra Authentication for some gateways). Hover or focus the value to preview. Saves on blur, outside click, or closing this panel.",
"compatUpstreamHeadersLabel": "Extra upstream headers",
"compatUpstreamRemoveRow": "Remove row"
},
"settings": {
"title": "Ustawienia",
@@ -1849,7 +1880,13 @@
"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}"
"disableFingerprintTitle": "Disable fingerprint for {provider}",
"routingAdvancedGuideTitle": "Zaawansowane wskazówki dotyczące tras",
"routingAdvancedGuideHint1": "Użyj opcji Fill First, aby uzyskać przewidywalny priorytet, Round Robin, aby zapewnić uczciwość, i P2C, aby zapewnić odporność na opóźnienia.",
"routingAdvancedGuideHint2": "Jeśli dostawcy różnią się jakością/kosztami, zacznij od opcji Koszt w przypadku pracy w tle i opcji Najmniej używane w celu zapewnienia zrównoważonego zużycia.",
"comboDefaultsGuideTitle": "Jak dostroić domyślne ustawienia kombinacji",
"comboDefaultsGuideHint1": "Utrzymuj niską liczbę ponownych prób w przepływach o małych opóźnieniach; zwiększaj limit czasu tylko dla zadań o długim generowaniu.",
"comboDefaultsGuideHint2": "Użyj zastąpienia dostawcy, gdy jeden z dostawców wymaga innego zachowania związanego z przekroczeniem limitu czasu/ponownej próby niż globalne ustawienia domyślne."
},
"translator": {
"title": "Tłumacz",
@@ -2569,7 +2606,15 @@
"opencodeDetected": "opencode {version} detected",
"opencodeDesc": "Generate a ready-to-use {configFile} with your OmniRoute base URL and all available models — drop it in your project root and run {command}.",
"downloadConfig": "Download {file}",
"downloaded": "Downloaded!"
"downloaded": "Downloaded!",
"setupGuideTitle": "Setup guide",
"openCliTools": "Open CLI Tools",
"setupGuideDetectCliTitle": "Detect installed CLIs",
"setupGuideDetectCliDesc": "Click Refresh after installing or updating a CLI so OmniRoute can rescan binaries and versions.",
"setupGuideCustomAgentTitle": "Register custom binary",
"setupGuideCustomAgentDesc": "Use Add Custom Agent when your CLI is not in the built-in list. Provide binary name and version command.",
"setupGuideCommandMissingTitle": "Fix 'command not found'",
"setupGuideCommandMissingDesc": "Ensure the CLI command exists in PATH, open a new terminal session, and rerun Refresh."
},
"autoCombo": {
"title": "Auto-Combo Engine",
@@ -2628,6 +2673,21 @@
"rerank": "Rerank",
"rerankModel": "Rerank Model",
"positionDelta": "Position Change",
"emptyState": "Send a search query to see results"
"emptyState": "Send a search query to see results",
"safeSearchOff": "Off",
"safeSearchModerate": "Moderate",
"safeSearchStrict": "Strict",
"queryPlaceholder": "Enter search query...",
"providerAuto": "auto (cheapest)",
"searchTypeWeb": "web",
"searchTypeNews": "news",
"optionAny": "any",
"timeRangeDay": "Past day",
"timeRangeWeek": "Past week",
"timeRangeMonth": "Past month",
"timeRangeYear": "Past year",
"domainPlaceholder": "example.com",
"requestTimedOut": "Request timed out ({seconds}s)",
"networkError": "Network error"
}
}

View File

@@ -56,7 +56,9 @@
"warning": "Aviso",
"note": "Nota",
"free": "Gratuito",
"skipToContent": "Pular para conteúdo"
"skipToContent": "Pular para conteúdo",
"maintenanceServerIssues": "Server is experiencing issues. Some features may be unavailable.",
"maintenanceServerUnreachable": "Server is unreachable. Reconnecting..."
},
"sidebar": {
"home": "Início",
@@ -576,7 +578,23 @@
"mitmStep1": "1. Start MITM to route requests through OmniRoute.",
"mitmStep2Prefix": "2. Add",
"mitmStep2Suffix": "to your hosts file as 127.0.0.1.",
"mitmStep3": "3. Open {toolName} and requests will be proxied."
"mitmStep3": "3. Open {toolName} and requests will be proxied.",
"whenToUseLabel": "Quando usar",
"openToolDocs": "Abrir documentos de ferramentas",
"toolUseCases": {
"claude": "Use quando desejar fluxos de trabalho de planejamento robustos e refatorações longas de vários arquivos com Claude Code.",
"codex": "Use quando sua equipe estiver padronizada em fluxos OpenAI Codex CLI e autenticação baseada em perfil.",
"droid": "Use quando precisar de um agente de terminal leve focado em codificação rápida e loops de execução de comandos.",
"openclaw": "Use quando desejar um agente de codificação no estilo Open Claw, mas roteado por meio de políticas OmniRoute.",
"cline": "Use quando você configura agentes de codificação dentro de editores e deseja configuração guiada com modelos OmniRoute.",
"kilo": "Use quando seu fluxo de trabalho depender de comandos Kilo Code e edições iterativas rápidas.",
"cursor": "Use ao codificar no Cursor e você precisar de modelos personalizados compatíveis com OpenAI por meio do OmniRoute.",
"continue": "Use ao executar Continue em IDEs e você precisar de uma configuração de provedor portátil baseada em JSON.",
"opencode": "Use quando preferir execuções de agentes nativos de terminal e automação com script via OpenCode.",
"kiro": "Use ao integrar o Kiro e controlar o roteamento do modelo centralmente no OmniRoute.",
"antigravity": "Use quando o tráfego Antigravity/Kiro deve ser interceptado através do MITM e roteado para OmniRoute.",
"copilot": "Use quando desejar UX no estilo de bate-papo do Copilot enquanto impõe chaves OmniRoute e regras de roteamento."
}
},
"combos": {
"title": "Combos",
@@ -1099,7 +1117,13 @@
"resetAllTitle": "Resetar todos os circuit breakers para estado saudável",
"resetting": "Resetando...",
"resetAll": "Resetar Tudo",
"until": "Até {time}"
"until": "Até {time}",
"activeProviders": "{count} active",
"monitoredProviders": "{count} monitored",
"configuredProvidersLabel": "Configurado no painel",
"configuredProvidersHint": "Provedores com credenciais salvas em /dashboard/providers, independentemente do estado do tempo de execução.",
"activeProvidersHint": "Provedores configurados atualmente habilitados para solicitações de roteamento.",
"monitoredProvidersHint": "Provedores atualmente monitorados por monitores de integridade dos disjuntores."
},
"limits": {
"title": "Limites e Cotas",
@@ -1458,7 +1482,14 @@
"compatProtocolOpenAIResponses": "OpenAI Responses API",
"compatProtocolClaude": "Anthropic Messages",
"tokenRefreshed": "Token refreshed successfully",
"tokenRefreshFailed": "Token refresh failed"
"tokenRefreshFailed": "Token refresh failed",
"compatBadgeUpstreamHeaders": "Headers",
"compatUpstreamAddRow": "Add header",
"compatUpstreamHeaderName": "Header name",
"compatUpstreamHeaderValue": "Value",
"compatUpstreamHeadersHint": "High-privilege setting — same trust level as editing provider API credentials; only trusted admins should use it. Merged after OmniRoute adds auth from the provider API key. If a custom header uses the same name as an existing one (e.g. Authorization), your value fully replaces the auto-generated header (including the Bearer token) — the upstream only sees what you typed, not the key from settings. Misconfiguration can cause 401 or broken upstream auth. One row per header (e.g. extra Authentication for some gateways). Hover or focus the value to preview. Saves on blur, outside click, or closing this panel.",
"compatUpstreamHeadersLabel": "Extra upstream headers",
"compatUpstreamRemoveRow": "Remove row"
},
"settings": {
"title": "Configurações",
@@ -1861,7 +1892,13 @@
"customPricingNote": "Você pode sobrescrever preços padrão para modelos específicos. Sobrescritas personalizadas têm prioridade sobre preços detectados automaticamente.",
"editPricing": "Editar Preços",
"viewFullDetails": "Ver Detalhes Completos",
"themeCoral": "Coral"
"themeCoral": "Coral",
"routingAdvancedGuideTitle": "Orientação avançada de roteamento",
"routingAdvancedGuideHint1": "Use Fill First para prioridade previsível, Round Robin para justiça e P2C para resiliência de latência.",
"routingAdvancedGuideHint2": "Se os fornecedores variarem em qualidade/custo, comece com Opção de custo para trabalho em segundo plano e Menos usado para desgaste equilibrado.",
"comboDefaultsGuideTitle": "Como ajustar os padrões de combinação",
"comboDefaultsGuideHint1": "Mantenha as tentativas baixas em fluxos de baixa latência; aumente o tempo limite apenas para tarefas de geração longa.",
"comboDefaultsGuideHint2": "Use substituições de provedor quando um provedor precisar de um comportamento de tempo limite/nova tentativa diferente dos padrões globais."
},
"translator": {
"title": "Tradutor",
@@ -2581,7 +2618,15 @@
"opencodeDetected": "opencode {version} detected",
"opencodeDesc": "Generate a ready-to-use {configFile} with your OmniRoute base URL and all available models — drop it in your project root and run {command}.",
"downloadConfig": "Download {file}",
"downloaded": "Downloaded!"
"downloaded": "Downloaded!",
"setupGuideTitle": "Setup guide",
"openCliTools": "Open CLI Tools",
"setupGuideDetectCliTitle": "Detect installed CLIs",
"setupGuideDetectCliDesc": "Click Refresh after installing or updating a CLI so OmniRoute can rescan binaries and versions.",
"setupGuideCustomAgentTitle": "Register custom binary",
"setupGuideCustomAgentDesc": "Use Add Custom Agent when your CLI is not in the built-in list. Provide binary name and version command.",
"setupGuideCommandMissingTitle": "Fix 'command not found'",
"setupGuideCommandMissingDesc": "Ensure the CLI command exists in PATH, open a new terminal session, and rerun Refresh."
},
"autoCombo": {
"title": "Auto-Combo Engine",
@@ -2640,6 +2685,21 @@
"rerank": "Rerank",
"rerankModel": "Rerank Model",
"positionDelta": "Position Change",
"emptyState": "Send a search query to see results"
"emptyState": "Send a search query to see results",
"safeSearchOff": "Off",
"safeSearchModerate": "Moderate",
"safeSearchStrict": "Strict",
"queryPlaceholder": "Enter search query...",
"providerAuto": "auto (cheapest)",
"searchTypeWeb": "web",
"searchTypeNews": "news",
"optionAny": "any",
"timeRangeDay": "Past day",
"timeRangeWeek": "Past week",
"timeRangeMonth": "Past month",
"timeRangeYear": "Past year",
"domainPlaceholder": "example.com",
"requestTimedOut": "Request timed out ({seconds}s)",
"networkError": "Network error"
}
}

View File

@@ -56,7 +56,9 @@
"warning": "Aviso",
"note": "Nota",
"free": "Grátis",
"skipToContent": "Pular para o conteúdo"
"skipToContent": "Pular para o conteúdo",
"maintenanceServerIssues": "Server is experiencing issues. Some features may be unavailable.",
"maintenanceServerUnreachable": "Server is unreachable. Reconnecting..."
},
"sidebar": {
"home": "Página inicial",
@@ -576,7 +578,23 @@
"mitmStep1": "1. Start MITM to route requests through OmniRoute.",
"mitmStep2Prefix": "2. Add",
"mitmStep2Suffix": "to your hosts file as 127.0.0.1.",
"mitmStep3": "3. Open {toolName} and requests will be proxied."
"mitmStep3": "3. Open {toolName} and requests will be proxied.",
"whenToUseLabel": "Quando usar",
"openToolDocs": "Abrir documentos de ferramentas",
"toolUseCases": {
"claude": "Use quando desejar fluxos de trabalho de planejamento robustos e refatorações longas de vários arquivos com Claude Code.",
"codex": "Use quando sua equipe estiver padronizada em fluxos OpenAI Codex CLI e autenticação baseada em perfil.",
"droid": "Use quando precisar de um agente de terminal leve focado em codificação rápida e loops de execução de comandos.",
"openclaw": "Use quando desejar um agente de codificação no estilo Open Claw, mas roteado por meio de políticas OmniRoute.",
"cline": "Use quando você configura agentes de codificação dentro de editores e deseja configuração guiada com modelos OmniRoute.",
"kilo": "Use quando seu fluxo de trabalho depender de comandos Kilo Code e edições iterativas rápidas.",
"cursor": "Use ao codificar no Cursor e você precisar de modelos personalizados compatíveis com OpenAI por meio do OmniRoute.",
"continue": "Use ao executar Continue em IDEs e você precisar de uma configuração de provedor portátil baseada em JSON.",
"opencode": "Use quando preferir execuções de agentes nativos de terminal e automação com script via OpenCode.",
"kiro": "Use ao integrar o Kiro e controlar o roteamento do modelo centralmente no OmniRoute.",
"antigravity": "Use quando o tráfego Antigravity/Kiro deve ser interceptado através do MITM e roteado para OmniRoute.",
"copilot": "Use quando desejar UX no estilo de bate-papo do Copilot enquanto impõe chaves OmniRoute e regras de roteamento."
}
},
"combos": {
"title": "Combos",
@@ -1099,7 +1117,13 @@
"resetAllTitle": "Reinicialize todos os disjuntores para um estado saudável",
"resetting": "Redefinindo...",
"resetAll": "Redefinir tudo",
"until": "Até {time}"
"until": "Até {time}",
"activeProviders": "{count} active",
"monitoredProviders": "{count} monitored",
"configuredProvidersLabel": "Configurado no painel",
"configuredProvidersHint": "Provedores com credenciais salvas em /dashboard/providers, independentemente do estado do tempo de execução.",
"activeProvidersHint": "Provedores configurados atualmente habilitados para solicitações de roteamento.",
"monitoredProvidersHint": "Provedores atualmente monitorados por monitores de integridade dos disjuntores."
},
"limits": {
"title": "Limites e cotas",
@@ -1458,7 +1482,14 @@
"compatProtocolOpenAIResponses": "OpenAI Responses API",
"compatProtocolClaude": "Anthropic Messages",
"tokenRefreshed": "Token refreshed successfully",
"tokenRefreshFailed": "Token refresh failed"
"tokenRefreshFailed": "Token refresh failed",
"compatBadgeUpstreamHeaders": "Headers",
"compatUpstreamAddRow": "Add header",
"compatUpstreamHeaderName": "Header name",
"compatUpstreamHeaderValue": "Value",
"compatUpstreamHeadersHint": "High-privilege setting — same trust level as editing provider API credentials; only trusted admins should use it. Merged after OmniRoute adds auth from the provider API key. If a custom header uses the same name as an existing one (e.g. Authorization), your value fully replaces the auto-generated header (including the Bearer token) — the upstream only sees what you typed, not the key from settings. Misconfiguration can cause 401 or broken upstream auth. One row per header (e.g. extra Authentication for some gateways). Hover or focus the value to preview. Saves on blur, outside click, or closing this panel.",
"compatUpstreamHeadersLabel": "Extra upstream headers",
"compatUpstreamRemoveRow": "Remove row"
},
"settings": {
"title": "Configurações",
@@ -1861,7 +1892,13 @@
"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}"
"disableFingerprintTitle": "Disable fingerprint for {provider}",
"routingAdvancedGuideTitle": "Orientação avançada de roteamento",
"routingAdvancedGuideHint1": "Use Fill First para prioridade previsível, Round Robin para justiça e P2C para resiliência de latência.",
"routingAdvancedGuideHint2": "Se os fornecedores variarem em qualidade/custo, comece com Opção de custo para trabalho em segundo plano e Menos usado para desgaste equilibrado.",
"comboDefaultsGuideTitle": "Como ajustar os padrões de combinação",
"comboDefaultsGuideHint1": "Mantenha as tentativas baixas em fluxos de baixa latência; aumente o tempo limite apenas para tarefas de geração longa.",
"comboDefaultsGuideHint2": "Use substituições de provedor quando um provedor precisar de um comportamento de tempo limite/nova tentativa diferente dos padrões globais."
},
"translator": {
"title": "Tradutor",
@@ -2569,7 +2606,15 @@
"opencodeDetected": "opencode {version} detected",
"opencodeDesc": "Generate a ready-to-use {configFile} with your OmniRoute base URL and all available models — drop it in your project root and run {command}.",
"downloadConfig": "Download {file}",
"downloaded": "Downloaded!"
"downloaded": "Downloaded!",
"setupGuideTitle": "Setup guide",
"openCliTools": "Open CLI Tools",
"setupGuideDetectCliTitle": "Detect installed CLIs",
"setupGuideDetectCliDesc": "Click Refresh after installing or updating a CLI so OmniRoute can rescan binaries and versions.",
"setupGuideCustomAgentTitle": "Register custom binary",
"setupGuideCustomAgentDesc": "Use Add Custom Agent when your CLI is not in the built-in list. Provide binary name and version command.",
"setupGuideCommandMissingTitle": "Fix 'command not found'",
"setupGuideCommandMissingDesc": "Ensure the CLI command exists in PATH, open a new terminal session, and rerun Refresh."
},
"autoCombo": {
"title": "Auto-Combo Engine",
@@ -2628,6 +2673,21 @@
"rerank": "Rerank",
"rerankModel": "Rerank Model",
"positionDelta": "Position Change",
"emptyState": "Send a search query to see results"
"emptyState": "Send a search query to see results",
"safeSearchOff": "Off",
"safeSearchModerate": "Moderate",
"safeSearchStrict": "Strict",
"queryPlaceholder": "Enter search query...",
"providerAuto": "auto (cheapest)",
"searchTypeWeb": "web",
"searchTypeNews": "news",
"optionAny": "any",
"timeRangeDay": "Past day",
"timeRangeWeek": "Past week",
"timeRangeMonth": "Past month",
"timeRangeYear": "Past year",
"domainPlaceholder": "example.com",
"requestTimedOut": "Request timed out ({seconds}s)",
"networkError": "Network error"
}
}

View File

@@ -56,7 +56,9 @@
"warning": "Avertisment",
"note": "Notă",
"free": "Gratuit",
"skipToContent": "Treci la conținut"
"skipToContent": "Treci la conținut",
"maintenanceServerIssues": "Server is experiencing issues. Some features may be unavailable.",
"maintenanceServerUnreachable": "Server is unreachable. Reconnecting..."
},
"sidebar": {
"home": "Acasă",
@@ -576,7 +578,23 @@
"mitmStep1": "1. Start MITM to route requests through OmniRoute.",
"mitmStep2Prefix": "2. Add",
"mitmStep2Suffix": "to your hosts file as 127.0.0.1.",
"mitmStep3": "3. Open {toolName} and requests will be proxied."
"mitmStep3": "3. Open {toolName} and requests will be proxied.",
"whenToUseLabel": "Când să utilizați",
"openToolDocs": "Deschideți documentele instrumentului",
"toolUseCases": {
"claude": "Utilizați atunci când doriți fluxuri de lucru puternice de planificare și refactorări lungi cu mai multe fișiere cu Claude Code.",
"codex": "Utilizați atunci când echipa dvs. este standardizată pe fluxurile OpenAI Codex CLI și pe autentificarea bazată pe profil.",
"droid": "Utilizați atunci când aveți nevoie de un agent terminal ușor, concentrat pe codificarea rapidă și buclele de execuție a comenzilor.",
"openclaw": "Utilizați atunci când doriți un agent de codare în stil Open Claw, dar direcționat prin politicile OmniRoute.",
"cline": "Utilizați atunci când configurați agenți de codare în cadrul editorilor și doriți o configurare ghidată cu modelele OmniRoute.",
"kilo": "Utilizați atunci când fluxul dvs. de lucru depinde de comenzile Kilo Code și de editările iterative rapide.",
"cursor": "Utilizați când codați în Cursor și aveți nevoie de modele personalizate compatibile cu OpenAI prin OmniRoute.",
"continue": "Utilizați atunci când rulați Continue în IDE-uri și aveți nevoie de o configurație portabilă a furnizorului bazată pe JSON.",
"opencode": "Utilizați atunci când preferați rulările de agent nativ terminal și automatizarea prin script prin OpenCode.",
"kiro": "Utilizați atunci când integrați Kiro și controlați rutarea modelului central din OmniRoute.",
"antigravity": "Utilizați atunci când traficul Antigravity/Kiro trebuie interceptat prin MITM și direcționat către OmniRoute.",
"copilot": "Utilizați atunci când doriți UX în stilul de chat Copilot, în timp ce aplicați cheile și regulile de rutare OmniRoute."
}
},
"combos": {
"title": "Combo-uri",
@@ -1087,7 +1105,13 @@
"resetAllTitle": "Resetați toate întreruptoarele la starea sănătoasă",
"resetting": "Se resetează...",
"resetAll": "Resetați toate",
"until": "Până la {time}"
"until": "Până la {time}",
"activeProviders": "{count} active",
"monitoredProviders": "{count} monitored",
"configuredProvidersLabel": "Configurat în tabloul de bord",
"configuredProvidersHint": "Furnizorii cu acreditări salvate în /dashboard/providers, indiferent de starea de rulare.",
"activeProvidersHint": "Furnizorii configurați activați în prezent pentru solicitările de rutare.",
"monitoredProvidersHint": "Furnizorii sunt urmăriți în prezent de monitoarele de sănătate ale întreruptoarelor."
},
"limits": {
"title": "Limite și cote",
@@ -1446,7 +1470,14 @@
"compatProtocolOpenAIResponses": "OpenAI Responses API",
"compatProtocolClaude": "Anthropic Messages",
"tokenRefreshed": "Token refreshed successfully",
"tokenRefreshFailed": "Token refresh failed"
"tokenRefreshFailed": "Token refresh failed",
"compatBadgeUpstreamHeaders": "Headers",
"compatUpstreamAddRow": "Add header",
"compatUpstreamHeaderName": "Header name",
"compatUpstreamHeaderValue": "Value",
"compatUpstreamHeadersHint": "High-privilege setting — same trust level as editing provider API credentials; only trusted admins should use it. Merged after OmniRoute adds auth from the provider API key. If a custom header uses the same name as an existing one (e.g. Authorization), your value fully replaces the auto-generated header (including the Bearer token) — the upstream only sees what you typed, not the key from settings. Misconfiguration can cause 401 or broken upstream auth. One row per header (e.g. extra Authentication for some gateways). Hover or focus the value to preview. Saves on blur, outside click, or closing this panel.",
"compatUpstreamHeadersLabel": "Extra upstream headers",
"compatUpstreamRemoveRow": "Remove row"
},
"settings": {
"title": "Setări",
@@ -1849,7 +1880,13 @@
"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}"
"disableFingerprintTitle": "Disable fingerprint for {provider}",
"routingAdvancedGuideTitle": "Ghid avansat de rutare",
"routingAdvancedGuideHint1": "Folosiți Fill First pentru o prioritate previzibilă, Round Robin pentru corectitudine și P2C pentru rezistență la latență.",
"routingAdvancedGuideHint2": "Dacă furnizorii variază în ceea ce privește calitatea/costul, începeți cu Cost Opt pentru munca de fundal și Least Used pentru uzura echilibrată.",
"comboDefaultsGuideTitle": "Cum să reglați setările implicite de combo",
"comboDefaultsGuideHint1": "Menține reîncercările scăzute în fluxurile cu latență scăzută; crește timpul de expirare numai pentru sarcini de generație lungă.",
"comboDefaultsGuideHint2": "Folosiți suprascrierile furnizorului atunci când un furnizor are nevoie de un comportament de timeout/reîncercare diferit față de valorile prestabilite globale."
},
"translator": {
"title": "Traducător",
@@ -2569,7 +2606,15 @@
"opencodeDetected": "opencode {version} detected",
"opencodeDesc": "Generate a ready-to-use {configFile} with your OmniRoute base URL and all available models — drop it in your project root and run {command}.",
"downloadConfig": "Download {file}",
"downloaded": "Downloaded!"
"downloaded": "Downloaded!",
"setupGuideTitle": "Setup guide",
"openCliTools": "Open CLI Tools",
"setupGuideDetectCliTitle": "Detect installed CLIs",
"setupGuideDetectCliDesc": "Click Refresh after installing or updating a CLI so OmniRoute can rescan binaries and versions.",
"setupGuideCustomAgentTitle": "Register custom binary",
"setupGuideCustomAgentDesc": "Use Add Custom Agent when your CLI is not in the built-in list. Provide binary name and version command.",
"setupGuideCommandMissingTitle": "Fix 'command not found'",
"setupGuideCommandMissingDesc": "Ensure the CLI command exists in PATH, open a new terminal session, and rerun Refresh."
},
"autoCombo": {
"title": "Auto-Combo Engine",
@@ -2628,6 +2673,21 @@
"rerank": "Rerank",
"rerankModel": "Rerank Model",
"positionDelta": "Position Change",
"emptyState": "Send a search query to see results"
"emptyState": "Send a search query to see results",
"safeSearchOff": "Off",
"safeSearchModerate": "Moderate",
"safeSearchStrict": "Strict",
"queryPlaceholder": "Enter search query...",
"providerAuto": "auto (cheapest)",
"searchTypeWeb": "web",
"searchTypeNews": "news",
"optionAny": "any",
"timeRangeDay": "Past day",
"timeRangeWeek": "Past week",
"timeRangeMonth": "Past month",
"timeRangeYear": "Past year",
"domainPlaceholder": "example.com",
"requestTimedOut": "Request timed out ({seconds}s)",
"networkError": "Network error"
}
}

View File

@@ -56,7 +56,9 @@
"warning": "Предупреждение",
"note": "Примечание",
"free": "Бесплатно",
"skipToContent": "Перейти к содержимому"
"skipToContent": "Перейти к содержимому",
"maintenanceServerIssues": "Server is experiencing issues. Some features may be unavailable.",
"maintenanceServerUnreachable": "Server is unreachable. Reconnecting..."
},
"sidebar": {
"home": "Главная",
@@ -576,7 +578,23 @@
"mitmStep1": "1. Start MITM to route requests through OmniRoute.",
"mitmStep2Prefix": "2. Add",
"mitmStep2Suffix": "to your hosts file as 127.0.0.1.",
"mitmStep3": "3. Open {toolName} and requests will be proxied."
"mitmStep3": "3. Open {toolName} and requests will be proxied.",
"whenToUseLabel": "Когда использовать",
"openToolDocs": "Открыть документацию по инструменту",
"toolUseCases": {
"claude": "Используйте, если вам нужны надежные рабочие процессы планирования и длительные рефакторинги нескольких файлов с помощью Claude Code.",
"codex": "Используйте, когда ваша команда стандартизирует потоки интерфейса командной строки OpenAI Codex и аутентификацию на основе профилей.",
"droid": "Используйте, когда вам нужен легкий агент терминала, ориентированный на быстрое кодирование и циклы выполнения команд.",
"openclaw": "Используйте, если вам нужен агент кодирования в стиле Open Claw, но маршрутизируемый через политики OmniRoute.",
"cline": "Используйте, если вы настраиваете агенты кодирования внутри редакторов и хотите выполнить управляемую настройку с помощью моделей OmniRoute.",
"kilo": "Используйте, когда ваш рабочий процесс зависит от команд Kilo Code и быстрого итеративного редактирования.",
"cursor": "Используйте при кодировании в Cursor, если вам нужны пользовательские модели, совместимые с OpenAI, через OmniRoute.",
"continue": "Используйте при запуске Продолжить в IDE, если вам нужна переносимая конфигурация поставщика на основе JSON.",
"opencode": "Используйте, если вы предпочитаете запускать собственные агенты терминала и автоматизировать скрипты через OpenCode.",
"kiro": "Используйте при интеграции Kiro и централизованном управлении маршрутизацией модели из OmniRoute.",
"antigravity": "Используйте, когда трафик Антигравитации/Киро необходимо перехватить через MITM и направить в OmniRoute.",
"copilot": "Используйте его, если вам нужен пользовательский интерфейс в стиле чата Copilot, одновременно обеспечивая соблюдение ключей OmniRoute и правил маршрутизации."
}
},
"combos": {
"title": "Комбо",
@@ -1087,7 +1105,13 @@
"resetAllTitle": "Верните все автоматические выключатели в исправное состояние.",
"resetting": "Сброс...",
"resetAll": "Сбросить все",
"until": "До {time}"
"until": "До {time}",
"activeProviders": "{count} active",
"monitoredProviders": "{count} monitored",
"configuredProvidersLabel": "Настраивается в приборной панели",
"configuredProvidersHint": "Поставщики, учетные данные которых сохранены в /dashboard/providers, независимо от состояния времени выполнения.",
"activeProvidersHint": "Настроенные поставщики в настоящее время включены для маршрутизации запросов.",
"monitoredProvidersHint": "Поставщики услуг в настоящее время отслеживаются с помощью мониторов состояния автоматических выключателей."
},
"limits": {
"title": "Лимиты и квоты",
@@ -1446,7 +1470,14 @@
"compatProtocolOpenAIResponses": "OpenAI Responses API",
"compatProtocolClaude": "Anthropic Messages",
"tokenRefreshed": "Token refreshed successfully",
"tokenRefreshFailed": "Token refresh failed"
"tokenRefreshFailed": "Token refresh failed",
"compatBadgeUpstreamHeaders": "Headers",
"compatUpstreamAddRow": "Add header",
"compatUpstreamHeaderName": "Header name",
"compatUpstreamHeaderValue": "Value",
"compatUpstreamHeadersHint": "High-privilege setting — same trust level as editing provider API credentials; only trusted admins should use it. Merged after OmniRoute adds auth from the provider API key. If a custom header uses the same name as an existing one (e.g. Authorization), your value fully replaces the auto-generated header (including the Bearer token) — the upstream only sees what you typed, not the key from settings. Misconfiguration can cause 401 or broken upstream auth. One row per header (e.g. extra Authentication for some gateways). Hover or focus the value to preview. Saves on blur, outside click, or closing this panel.",
"compatUpstreamHeadersLabel": "Extra upstream headers",
"compatUpstreamRemoveRow": "Remove row"
},
"settings": {
"title": "Настройки",
@@ -1849,7 +1880,13 @@
"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}"
"disableFingerprintTitle": "Disable fingerprint for {provider}",
"routingAdvancedGuideTitle": "Расширенное руководство по маршрутизации",
"routingAdvancedGuideHint1": "Используйте «Fill First» для предсказуемого приоритета, Round Robin для обеспечения справедливости и P2C для устойчивости к задержкам.",
"routingAdvancedGuideHint2": "Если поставщики различаются по качеству/стоимости, начните с варианта «Стоимость» для фоновой работы и «Наименее используемый» для сбалансированного износа.",
"comboDefaultsGuideTitle": "Как настроить комбо по умолчанию",
"comboDefaultsGuideHint1": "Сохраняйте низкий уровень повторных попыток в потоках с малой задержкой; увеличивайте таймаут только для задач длинной генерации.",
"comboDefaultsGuideHint2": "Используйте переопределения поставщика, если одному поставщику требуется другое поведение по тайм-ауту/повторной попытке, чем глобальные значения по умолчанию."
},
"translator": {
"title": "Переводчик",
@@ -2569,7 +2606,15 @@
"opencodeDetected": "opencode {version} detected",
"opencodeDesc": "Generate a ready-to-use {configFile} with your OmniRoute base URL and all available models — drop it in your project root and run {command}.",
"downloadConfig": "Download {file}",
"downloaded": "Downloaded!"
"downloaded": "Downloaded!",
"setupGuideTitle": "Setup guide",
"openCliTools": "Open CLI Tools",
"setupGuideDetectCliTitle": "Detect installed CLIs",
"setupGuideDetectCliDesc": "Click Refresh after installing or updating a CLI so OmniRoute can rescan binaries and versions.",
"setupGuideCustomAgentTitle": "Register custom binary",
"setupGuideCustomAgentDesc": "Use Add Custom Agent when your CLI is not in the built-in list. Provide binary name and version command.",
"setupGuideCommandMissingTitle": "Fix 'command not found'",
"setupGuideCommandMissingDesc": "Ensure the CLI command exists in PATH, open a new terminal session, and rerun Refresh."
},
"autoCombo": {
"title": "Auto-Combo Engine",
@@ -2628,6 +2673,21 @@
"rerank": "Rerank",
"rerankModel": "Rerank Model",
"positionDelta": "Position Change",
"emptyState": "Send a search query to see results"
"emptyState": "Send a search query to see results",
"safeSearchOff": "Off",
"safeSearchModerate": "Moderate",
"safeSearchStrict": "Strict",
"queryPlaceholder": "Enter search query...",
"providerAuto": "auto (cheapest)",
"searchTypeWeb": "web",
"searchTypeNews": "news",
"optionAny": "any",
"timeRangeDay": "Past day",
"timeRangeWeek": "Past week",
"timeRangeMonth": "Past month",
"timeRangeYear": "Past year",
"domainPlaceholder": "example.com",
"requestTimedOut": "Request timed out ({seconds}s)",
"networkError": "Network error"
}
}

View File

@@ -56,7 +56,9 @@
"warning": "Upozornenie",
"note": "Poznámka",
"free": "Zadarmo",
"skipToContent": "Preskočiť na obsah"
"skipToContent": "Preskočiť na obsah",
"maintenanceServerIssues": "Server is experiencing issues. Some features may be unavailable.",
"maintenanceServerUnreachable": "Server is unreachable. Reconnecting..."
},
"sidebar": {
"home": "Domov",
@@ -576,7 +578,23 @@
"mitmStep1": "1. Start MITM to route requests through OmniRoute.",
"mitmStep2Prefix": "2. Add",
"mitmStep2Suffix": "to your hosts file as 127.0.0.1.",
"mitmStep3": "3. Open {toolName} and requests will be proxied."
"mitmStep3": "3. Open {toolName} and requests will be proxied.",
"whenToUseLabel": "Kedy použiť",
"openToolDocs": "Otvorte dokumentáciu nástroja",
"toolUseCases": {
"claude": "Použite, keď chcete silné plánovacie pracovné postupy a dlhé viacsúborové refaktory s Claude Code.",
"codex": "Použite, keď je váš tím štandardizovaný na tokoch OpenAI Codex CLI a autentifikácii založenej na profile.",
"droid": "Použite, keď potrebujete ľahkého terminálového agenta zameraného na rýchle kódovanie a slučky vykonávania príkazov.",
"openclaw": "Použite, keď chcete kódovacieho agenta v štýle Open Claw, ale smerovaného cez zásady OmniRoute.",
"cline": "Použite, keď konfigurujete kódovacích agentov v editoroch a chcete riadiť nastavenie s modelmi OmniRoute.",
"kilo": "Použite, keď váš pracovný postup závisí od príkazov Kilo Code a rýchlych iteračných úprav.",
"cursor": "Použite pri kódovaní v Cursore a potrebujete vlastné modely kompatibilné s OpenAI cez OmniRoute.",
"continue": "Použite pri spustení Continue in IDE a potrebujete prenosnú konfiguráciu poskytovateľa založenú na JSON.",
"opencode": "Použite, ak uprednostňujete spúšťanie natívneho agenta a skriptovanú automatizáciu cez OpenCode.",
"kiro": "Použite pri integrácii Kiro a centrálnom riadení smerovania modelu z OmniRoute.",
"antigravity": "Použite, keď musí byť premávka Antigravity/Kiro zachytená cez MITM a nasmerovaná na OmniRoute.",
"copilot": "Použite, keď chcete UX v štýle chatu Copilot pri presadzovaní kľúčov OmniRoute a pravidiel smerovania."
}
},
"combos": {
"title": "kombá",
@@ -1087,7 +1105,13 @@
"resetAllTitle": "Resetujte všetky ističe do zdravého stavu",
"resetting": "Resetovanie...",
"resetAll": "Obnoviť všetko",
"until": "Do {time}"
"until": "Do {time}",
"activeProviders": "{count} active",
"monitoredProviders": "{count} monitored",
"configuredProvidersLabel": "Nakonfigurované na ovládacom paneli",
"configuredProvidersHint": "Poskytovatelia s povereniami uloženými v /dashboard/providers, bez ohľadu na stav runtime.",
"activeProvidersHint": "Konfigurovaní poskytovatelia majú momentálne povolené požiadavky na smerovanie.",
"monitoredProvidersHint": "Poskytovatelia v súčasnosti sledovaní monitormi zdravia ističov."
},
"limits": {
"title": "Limity a kvóty",
@@ -1446,7 +1470,14 @@
"compatProtocolOpenAIResponses": "OpenAI Responses API",
"compatProtocolClaude": "Anthropic Messages",
"tokenRefreshed": "Token refreshed successfully",
"tokenRefreshFailed": "Token refresh failed"
"tokenRefreshFailed": "Token refresh failed",
"compatBadgeUpstreamHeaders": "Headers",
"compatUpstreamAddRow": "Add header",
"compatUpstreamHeaderName": "Header name",
"compatUpstreamHeaderValue": "Value",
"compatUpstreamHeadersHint": "High-privilege setting — same trust level as editing provider API credentials; only trusted admins should use it. Merged after OmniRoute adds auth from the provider API key. If a custom header uses the same name as an existing one (e.g. Authorization), your value fully replaces the auto-generated header (including the Bearer token) — the upstream only sees what you typed, not the key from settings. Misconfiguration can cause 401 or broken upstream auth. One row per header (e.g. extra Authentication for some gateways). Hover or focus the value to preview. Saves on blur, outside click, or closing this panel.",
"compatUpstreamHeadersLabel": "Extra upstream headers",
"compatUpstreamRemoveRow": "Remove row"
},
"settings": {
"title": "Nastavenia",
@@ -1849,7 +1880,13 @@
"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}"
"disableFingerprintTitle": "Disable fingerprint for {provider}",
"routingAdvancedGuideTitle": "Pokročilé vedenie trasy",
"routingAdvancedGuideHint1": "Použite Fill First pre predvídateľnú prioritu, Round Robin pre spravodlivosť a P2C pre odolnosť latencie.",
"routingAdvancedGuideHint2": "Ak sa poskytovatelia líšia v kvalite/nákladoch, začnite s Cost Opt pre prácu na pozadí a Najmenej používané pre vyvážené opotrebovanie.",
"comboDefaultsGuideTitle": "Ako vyladiť predvolené nastavenia komba",
"comboDefaultsGuideHint1": "Udržujte počet opakovaní na nízkej úrovni v tokoch s nízkou latenciou; zvýšiť časový limit iba pre úlohy s dlhým generovaním.",
"comboDefaultsGuideHint2": "Použite prepísania poskytovateľa, keď jeden poskytovateľ potrebuje iné správanie pri uplynutí časového limitu/opakovania, ako sú globálne predvolené hodnoty."
},
"translator": {
"title": "Prekladateľ",
@@ -2569,7 +2606,15 @@
"opencodeDetected": "opencode {version} detected",
"opencodeDesc": "Generate a ready-to-use {configFile} with your OmniRoute base URL and all available models — drop it in your project root and run {command}.",
"downloadConfig": "Download {file}",
"downloaded": "Downloaded!"
"downloaded": "Downloaded!",
"setupGuideTitle": "Setup guide",
"openCliTools": "Open CLI Tools",
"setupGuideDetectCliTitle": "Detect installed CLIs",
"setupGuideDetectCliDesc": "Click Refresh after installing or updating a CLI so OmniRoute can rescan binaries and versions.",
"setupGuideCustomAgentTitle": "Register custom binary",
"setupGuideCustomAgentDesc": "Use Add Custom Agent when your CLI is not in the built-in list. Provide binary name and version command.",
"setupGuideCommandMissingTitle": "Fix 'command not found'",
"setupGuideCommandMissingDesc": "Ensure the CLI command exists in PATH, open a new terminal session, and rerun Refresh."
},
"autoCombo": {
"title": "Auto-Combo Engine",
@@ -2628,6 +2673,21 @@
"rerank": "Rerank",
"rerankModel": "Rerank Model",
"positionDelta": "Position Change",
"emptyState": "Send a search query to see results"
"emptyState": "Send a search query to see results",
"safeSearchOff": "Off",
"safeSearchModerate": "Moderate",
"safeSearchStrict": "Strict",
"queryPlaceholder": "Enter search query...",
"providerAuto": "auto (cheapest)",
"searchTypeWeb": "web",
"searchTypeNews": "news",
"optionAny": "any",
"timeRangeDay": "Past day",
"timeRangeWeek": "Past week",
"timeRangeMonth": "Past month",
"timeRangeYear": "Past year",
"domainPlaceholder": "example.com",
"requestTimedOut": "Request timed out ({seconds}s)",
"networkError": "Network error"
}
}

View File

@@ -56,7 +56,9 @@
"warning": "Varning",
"note": "Obs",
"free": "Gratis",
"skipToContent": "Hoppa till innehållet"
"skipToContent": "Hoppa till innehållet",
"maintenanceServerIssues": "Server is experiencing issues. Some features may be unavailable.",
"maintenanceServerUnreachable": "Server is unreachable. Reconnecting..."
},
"sidebar": {
"home": "Hem",
@@ -576,7 +578,23 @@
"mitmStep1": "1. Start MITM to route requests through OmniRoute.",
"mitmStep2Prefix": "2. Add",
"mitmStep2Suffix": "to your hosts file as 127.0.0.1.",
"mitmStep3": "3. Open {toolName} and requests will be proxied."
"mitmStep3": "3. Open {toolName} and requests will be proxied.",
"whenToUseLabel": "När du ska använda",
"openToolDocs": "Öppna verktygsdokument",
"toolUseCases": {
"claude": "Använd när du vill ha starka planeringsarbetsflöden och långa flerfilsrefaktorer med Claude Code.",
"codex": "Använd när ditt team är standardiserat på OpenAI Codex CLI-flöden och profilbaserad autentisering.",
"droid": "Använd när du behöver en lätt terminalagent fokuserad på snabb kodning och kommandokörningsloopar.",
"openclaw": "Använd när du vill ha en Open Claw-kodningsagent men dirigerad genom OmniRoute-policyer.",
"cline": "Använd när du konfigurerar kodningsagenter i redigerare och vill ha guidad installation med OmniRoute-modeller.",
"kilo": "Använd när ditt arbetsflöde beror på Kilo Code-kommandon och snabba iterativa redigeringar.",
"cursor": "Använd när du kodar i Cursor och du behöver anpassade OpenAI-kompatibla modeller genom OmniRoute.",
"continue": "Använd när du kör Continue i IDE och du behöver portabel JSON-baserad leverantörskonfiguration.",
"opencode": "Använd när du föredrar terminalbaserade agentkörningar och skriptautomation via OpenCode.",
"kiro": "Använd när du integrerar Kiro och styr modelldirigering centralt från OmniRoute.",
"antigravity": "Använd när Antigravity/Kiro-trafik måste avlyssnas genom MITM och dirigeras till OmniRoute.",
"copilot": "Använd när du vill ha Copilot chattstil UX samtidigt som du upprätthåller OmniRoute-nycklar och routingregler."
}
},
"combos": {
"title": "Combos",
@@ -1087,7 +1105,13 @@
"resetAllTitle": "Återställ alla strömbrytare till friskt tillstånd",
"resetting": "Återställer...",
"resetAll": "Återställ alla",
"until": "Tills {time}"
"until": "Tills {time}",
"activeProviders": "{count} active",
"monitoredProviders": "{count} monitored",
"configuredProvidersLabel": "Konfigurerad i instrumentpanelen",
"configuredProvidersHint": "Leverantörer med autentiseringsuppgifter sparade i /dashboard/providers, oavsett körtidstillstånd.",
"activeProvidersHint": "Konfigurerade leverantörer som för närvarande är aktiverade för routingförfrågningar.",
"monitoredProvidersHint": "Leverantörer spåras för närvarande av strömbrytare hälsoövervakare."
},
"limits": {
"title": "Gränser och kvoter",
@@ -1446,7 +1470,14 @@
"compatProtocolOpenAIResponses": "OpenAI Responses API",
"compatProtocolClaude": "Anthropic Messages",
"tokenRefreshed": "Token refreshed successfully",
"tokenRefreshFailed": "Token refresh failed"
"tokenRefreshFailed": "Token refresh failed",
"compatBadgeUpstreamHeaders": "Headers",
"compatUpstreamAddRow": "Add header",
"compatUpstreamHeaderName": "Header name",
"compatUpstreamHeaderValue": "Value",
"compatUpstreamHeadersHint": "High-privilege setting — same trust level as editing provider API credentials; only trusted admins should use it. Merged after OmniRoute adds auth from the provider API key. If a custom header uses the same name as an existing one (e.g. Authorization), your value fully replaces the auto-generated header (including the Bearer token) — the upstream only sees what you typed, not the key from settings. Misconfiguration can cause 401 or broken upstream auth. One row per header (e.g. extra Authentication for some gateways). Hover or focus the value to preview. Saves on blur, outside click, or closing this panel.",
"compatUpstreamHeadersLabel": "Extra upstream headers",
"compatUpstreamRemoveRow": "Remove row"
},
"settings": {
"title": "Inställningar",
@@ -1849,7 +1880,13 @@
"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}"
"disableFingerprintTitle": "Disable fingerprint for {provider}",
"routingAdvancedGuideTitle": "Avancerad vägledning",
"routingAdvancedGuideHint1": "Använd Fill First för förutsägbar prioritet, Round Robin för rättvisa och P2C för latensförmåga.",
"routingAdvancedGuideHint2": "Om leverantörer varierar i kvalitet/kostnad, börja med Cost Opt för bakgrundsarbete och Minst Används för balanserat slitage.",
"comboDefaultsGuideTitle": "Hur man ställer in kombinationsinställningar",
"comboDefaultsGuideHint1": "Håll låga omförsök i flöden med låg latens; öka timeout endast för långa generationsuppgifter.",
"comboDefaultsGuideHint2": "Använd åsidosättande av leverantörer när en leverantör behöver ett annat beteende för timeout/försök igen än globala standardinställningar."
},
"translator": {
"title": "Översättare",
@@ -2569,7 +2606,15 @@
"opencodeDetected": "opencode {version} detected",
"opencodeDesc": "Generate a ready-to-use {configFile} with your OmniRoute base URL and all available models — drop it in your project root and run {command}.",
"downloadConfig": "Download {file}",
"downloaded": "Downloaded!"
"downloaded": "Downloaded!",
"setupGuideTitle": "Setup guide",
"openCliTools": "Open CLI Tools",
"setupGuideDetectCliTitle": "Detect installed CLIs",
"setupGuideDetectCliDesc": "Click Refresh after installing or updating a CLI so OmniRoute can rescan binaries and versions.",
"setupGuideCustomAgentTitle": "Register custom binary",
"setupGuideCustomAgentDesc": "Use Add Custom Agent when your CLI is not in the built-in list. Provide binary name and version command.",
"setupGuideCommandMissingTitle": "Fix 'command not found'",
"setupGuideCommandMissingDesc": "Ensure the CLI command exists in PATH, open a new terminal session, and rerun Refresh."
},
"autoCombo": {
"title": "Auto-Combo Engine",
@@ -2628,6 +2673,21 @@
"rerank": "Rerank",
"rerankModel": "Rerank Model",
"positionDelta": "Position Change",
"emptyState": "Send a search query to see results"
"emptyState": "Send a search query to see results",
"safeSearchOff": "Off",
"safeSearchModerate": "Moderate",
"safeSearchStrict": "Strict",
"queryPlaceholder": "Enter search query...",
"providerAuto": "auto (cheapest)",
"searchTypeWeb": "web",
"searchTypeNews": "news",
"optionAny": "any",
"timeRangeDay": "Past day",
"timeRangeWeek": "Past week",
"timeRangeMonth": "Past month",
"timeRangeYear": "Past year",
"domainPlaceholder": "example.com",
"requestTimedOut": "Request timed out ({seconds}s)",
"networkError": "Network error"
}
}

View File

@@ -56,7 +56,9 @@
"warning": "คำเตือน",
"note": "หมายเหตุ",
"free": "ฟรี",
"skipToContent": "ข้ามไปที่เนื้อหา"
"skipToContent": "ข้ามไปที่เนื้อหา",
"maintenanceServerIssues": "Server is experiencing issues. Some features may be unavailable.",
"maintenanceServerUnreachable": "Server is unreachable. Reconnecting..."
},
"sidebar": {
"home": "บ้าน",
@@ -576,7 +578,23 @@
"mitmStep1": "1. Start MITM to route requests through OmniRoute.",
"mitmStep2Prefix": "2. Add",
"mitmStep2Suffix": "to your hosts file as 127.0.0.1.",
"mitmStep3": "3. Open {toolName} and requests will be proxied."
"mitmStep3": "3. Open {toolName} and requests will be proxied.",
"whenToUseLabel": "เมื่อจะใช้",
"openToolDocs": "เปิดเอกสารเครื่องมือ",
"toolUseCases": {
"claude": "ใช้เมื่อคุณต้องการเวิร์กโฟลว์การวางแผนที่แข็งแกร่งและรีแฟคเตอร์หลายไฟล์แบบยาวด้วย Claude Code",
"codex": "ใช้เมื่อทีมของคุณได้รับมาตรฐานในโฟลว์ OpenAI Codex CLI และการตรวจสอบสิทธิ์ตามโปรไฟล์",
"droid": "ใช้เมื่อคุณต้องการเอเจนต์เทอร์มินัลน้ำหนักเบาที่เน้นไปที่การเขียนโค้ดที่รวดเร็วและลูปการดำเนินการคำสั่ง",
"openclaw": "ใช้เมื่อคุณต้องการเอเจนต์การเข้ารหัสสไตล์ Open Claw แต่กำหนดเส้นทางผ่านนโยบาย OmniRoute",
"cline": "ใช้เมื่อคุณกำหนดค่าเอเจนต์การเขียนโค้ดภายในโปรแกรมแก้ไข และต้องการการตั้งค่าที่แนะนำด้วยโมเดล OmniRoute",
"kilo": "ใช้เมื่อเวิร์กโฟลว์ของคุณขึ้นอยู่กับคำสั่ง Kilo Code และการแก้ไขซ้ำอย่างรวดเร็ว",
"cursor": "ใช้เมื่อเขียนโค้ดในเคอร์เซอร์และคุณต้องการโมเดลที่เข้ากันได้กับ OpenAI แบบกำหนดเองผ่าน OmniRoute",
"continue": "ใช้เมื่อเรียกใช้ดำเนินการต่อใน IDE และคุณต้องกำหนดค่าผู้ให้บริการที่ใช้ JSON แบบพกพา",
"opencode": "ใช้เมื่อคุณต้องการเรียกใช้ Terminal-Native Agent และใช้สคริปต์อัตโนมัติผ่าน OpenCode",
"kiro": "ใช้เมื่อรวม Kiro และควบคุมการกำหนดเส้นทางโมเดลจากส่วนกลางจาก OmniRoute",
"antigravity": "ใช้เมื่อต้องสกัดกั้นการรับส่งข้อมูล Antigravity/Kiro ผ่าน MITM และกำหนดเส้นทางไปยัง OmniRoute",
"copilot": "ใช้เมื่อคุณต้องการ UX รูปแบบการแชทของ Copilot ในขณะที่บังคับใช้คีย์ OmniRoute และกฎการกำหนดเส้นทาง"
}
},
"combos": {
"title": "คอมโบ",
@@ -1087,7 +1105,13 @@
"resetAllTitle": "รีเซ็ตเบรกเกอร์วงจรทั้งหมดให้อยู่ในสถานะปกติ",
"resetting": "กำลังรีเซ็ต...",
"resetAll": "รีเซ็ตทั้งหมด",
"until": "จนถึง {time}"
"until": "จนถึง {time}",
"activeProviders": "{count} active",
"monitoredProviders": "{count} monitored",
"configuredProvidersLabel": "กำหนดค่าในแดชบอร์ด",
"configuredProvidersHint": "ผู้ให้บริการที่มีข้อมูลรับรองบันทึกไว้ใน /dashboard/providers โดยไม่คำนึงถึงสถานะรันไทม์",
"activeProvidersHint": "ผู้ให้บริการที่กำหนดค่าไว้เปิดใช้งานอยู่สำหรับการร้องขอการกำหนดเส้นทาง",
"monitoredProvidersHint": "ผู้ให้บริการในปัจจุบันได้รับการติดตามโดยเครื่องตรวจสุขภาพของเซอร์กิตเบรกเกอร์"
},
"limits": {
"title": "ขีดจำกัดและโควต้า",
@@ -1446,7 +1470,14 @@
"compatProtocolOpenAIResponses": "OpenAI Responses API",
"compatProtocolClaude": "Anthropic Messages",
"tokenRefreshed": "Token refreshed successfully",
"tokenRefreshFailed": "Token refresh failed"
"tokenRefreshFailed": "Token refresh failed",
"compatBadgeUpstreamHeaders": "Headers",
"compatUpstreamAddRow": "Add header",
"compatUpstreamHeaderName": "Header name",
"compatUpstreamHeaderValue": "Value",
"compatUpstreamHeadersHint": "High-privilege setting — same trust level as editing provider API credentials; only trusted admins should use it. Merged after OmniRoute adds auth from the provider API key. If a custom header uses the same name as an existing one (e.g. Authorization), your value fully replaces the auto-generated header (including the Bearer token) — the upstream only sees what you typed, not the key from settings. Misconfiguration can cause 401 or broken upstream auth. One row per header (e.g. extra Authentication for some gateways). Hover or focus the value to preview. Saves on blur, outside click, or closing this panel.",
"compatUpstreamHeadersLabel": "Extra upstream headers",
"compatUpstreamRemoveRow": "Remove row"
},
"settings": {
"title": "การตั้งค่า",
@@ -1849,7 +1880,13 @@
"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}"
"disableFingerprintTitle": "Disable fingerprint for {provider}",
"routingAdvancedGuideTitle": "คำแนะนำการกำหนดเส้นทางขั้นสูง",
"routingAdvancedGuideHint1": "ใช้ Fill First เพื่อลำดับความสำคัญที่คาดการณ์ได้ Round Robin เพื่อความยุติธรรม และ P2C เพื่อความยืดหยุ่นในการตอบสนอง",
"routingAdvancedGuideHint2": "หากผู้ให้บริการมีคุณภาพ/ต้นทุนแตกต่างกัน ให้เริ่มด้วยการเลือกต้นทุนสำหรับงานเบื้องหลังและใช้งานน้อยที่สุดสำหรับการสึกหรอที่สมดุล",
"comboDefaultsGuideTitle": "วิธีปรับแต่งค่าเริ่มต้นคอมโบ",
"comboDefaultsGuideHint1": "พยายามลองใหม่ให้ต่ำในกระแสเวลาแฝงต่ำ เพิ่มการหมดเวลาเฉพาะสำหรับงานที่ใช้เวลานานเท่านั้น",
"comboDefaultsGuideHint2": "ใช้การแทนที่ผู้ให้บริการเมื่อผู้ให้บริการรายหนึ่งต้องการพฤติกรรมการหมดเวลา/การลองใหม่ที่แตกต่างไปจากค่าเริ่มต้นส่วนกลาง"
},
"translator": {
"title": "นักแปล",
@@ -2569,7 +2606,15 @@
"opencodeDetected": "opencode {version} detected",
"opencodeDesc": "Generate a ready-to-use {configFile} with your OmniRoute base URL and all available models — drop it in your project root and run {command}.",
"downloadConfig": "Download {file}",
"downloaded": "Downloaded!"
"downloaded": "Downloaded!",
"setupGuideTitle": "Setup guide",
"openCliTools": "Open CLI Tools",
"setupGuideDetectCliTitle": "Detect installed CLIs",
"setupGuideDetectCliDesc": "Click Refresh after installing or updating a CLI so OmniRoute can rescan binaries and versions.",
"setupGuideCustomAgentTitle": "Register custom binary",
"setupGuideCustomAgentDesc": "Use Add Custom Agent when your CLI is not in the built-in list. Provide binary name and version command.",
"setupGuideCommandMissingTitle": "Fix 'command not found'",
"setupGuideCommandMissingDesc": "Ensure the CLI command exists in PATH, open a new terminal session, and rerun Refresh."
},
"autoCombo": {
"title": "Auto-Combo Engine",
@@ -2628,6 +2673,21 @@
"rerank": "Rerank",
"rerankModel": "Rerank Model",
"positionDelta": "Position Change",
"emptyState": "Send a search query to see results"
"emptyState": "Send a search query to see results",
"safeSearchOff": "Off",
"safeSearchModerate": "Moderate",
"safeSearchStrict": "Strict",
"queryPlaceholder": "Enter search query...",
"providerAuto": "auto (cheapest)",
"searchTypeWeb": "web",
"searchTypeNews": "news",
"optionAny": "any",
"timeRangeDay": "Past day",
"timeRangeWeek": "Past week",
"timeRangeMonth": "Past month",
"timeRangeYear": "Past year",
"domainPlaceholder": "example.com",
"requestTimedOut": "Request timed out ({seconds}s)",
"networkError": "Network error"
}
}

View File

@@ -56,7 +56,9 @@
"warning": "Попередження",
"note": "Примітка",
"free": "безкоштовно",
"skipToContent": "Перейти до вмісту"
"skipToContent": "Перейти до вмісту",
"maintenanceServerIssues": "Server is experiencing issues. Some features may be unavailable.",
"maintenanceServerUnreachable": "Server is unreachable. Reconnecting..."
},
"sidebar": {
"home": "додому",
@@ -576,7 +578,23 @@
"mitmStep1": "1. Start MITM to route requests through OmniRoute.",
"mitmStep2Prefix": "2. Add",
"mitmStep2Suffix": "to your hosts file as 127.0.0.1.",
"mitmStep3": "3. Open {toolName} and requests will be proxied."
"mitmStep3": "3. Open {toolName} and requests will be proxied.",
"whenToUseLabel": "Коли використовувати",
"openToolDocs": "Відкрийте документацію інструмента",
"toolUseCases": {
"claude": "Використовуйте, коли вам потрібні потужні робочі процеси планування та довгі багатофайлові рефактори з Claude Code.",
"codex": "Використовуйте, коли ваша команда стандартизована на потоках OpenAI Codex CLI та автентифікації на основі профілю.",
"droid": "Використовуйте, коли вам потрібен легкий термінальний агент, зосереджений на швидкому кодуванні та циклах виконання команд.",
"openclaw": "Використовуйте, якщо вам потрібен агент кодування у стилі Open Claw, але маршрутизується через політики OmniRoute.",
"cline": "Використовуйте, коли ви налаштовуєте агенти кодування в редакторах і бажаєте кероване налаштування за допомогою моделей OmniRoute.",
"kilo": "Використовуйте, коли ваш робочий процес залежить від команд Kilo Code та швидких ітеративних редагувань.",
"cursor": "Використовуйте під час кодування в Cursor, і вам потрібні власні моделі, сумісні з OpenAI через OmniRoute.",
"continue": "Використовуйте під час запуску Продовжити в IDE і вам потрібна портативна конфігурація постачальника на основі JSON.",
"opencode": "Використовуйте, якщо ви віддаєте перевагу запуску агента на терміналі та автоматизації за сценарієм через OpenCode.",
"kiro": "Використовуйте під час інтеграції Kiro та централізованого керування маршрутизацією моделі з OmniRoute.",
"antigravity": "Використовуйте, коли трафік Antigravity/Kiro потрібно перехопити через MITM і направити на OmniRoute.",
"copilot": "Використовуйте, коли вам потрібен UX у стилі чату Copilot із застосуванням ключів OmniRoute і правил маршрутизації."
}
},
"combos": {
"title": "Комбо",
@@ -1087,7 +1105,13 @@
"resetAllTitle": "Скиньте всі автоматичні вимикачі в справний стан",
"resetting": "Скидання...",
"resetAll": "Скинути все",
"until": "До {time}"
"until": "До {time}",
"activeProviders": "{count} active",
"monitoredProviders": "{count} monitored",
"configuredProvidersLabel": "Налаштовано на інформаційній панелі",
"configuredProvidersHint": "Постачальники з обліковими даними, збереженими в /dashboard/providers, незалежно від стану виконання.",
"activeProvidersHint": "Налаштовані постачальники наразі ввімкнено для запитів маршрутизації.",
"monitoredProvidersHint": "Постачальники наразі відстежуються моніторами справності автоматичних вимикачів."
},
"limits": {
"title": "Ліміти та квоти",
@@ -1446,7 +1470,14 @@
"compatProtocolOpenAIResponses": "OpenAI Responses API",
"compatProtocolClaude": "Anthropic Messages",
"tokenRefreshed": "Token refreshed successfully",
"tokenRefreshFailed": "Token refresh failed"
"tokenRefreshFailed": "Token refresh failed",
"compatBadgeUpstreamHeaders": "Headers",
"compatUpstreamAddRow": "Add header",
"compatUpstreamHeaderName": "Header name",
"compatUpstreamHeaderValue": "Value",
"compatUpstreamHeadersHint": "High-privilege setting — same trust level as editing provider API credentials; only trusted admins should use it. Merged after OmniRoute adds auth from the provider API key. If a custom header uses the same name as an existing one (e.g. Authorization), your value fully replaces the auto-generated header (including the Bearer token) — the upstream only sees what you typed, not the key from settings. Misconfiguration can cause 401 or broken upstream auth. One row per header (e.g. extra Authentication for some gateways). Hover or focus the value to preview. Saves on blur, outside click, or closing this panel.",
"compatUpstreamHeadersLabel": "Extra upstream headers",
"compatUpstreamRemoveRow": "Remove row"
},
"settings": {
"title": "Налаштування",
@@ -1849,7 +1880,13 @@
"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}"
"disableFingerprintTitle": "Disable fingerprint for {provider}",
"routingAdvancedGuideTitle": "Розширені вказівки щодо маршрутизації",
"routingAdvancedGuideHint1": "Використовуйте Fill First для передбачуваного пріоритету, Round Robin для справедливості та P2C для стійкості до затримок.",
"routingAdvancedGuideHint2": "Якщо постачальники відрізняються за якістю/вартістю, почніть із Cost Opt для фонової роботи та Least Used для збалансованого зносу.",
"comboDefaultsGuideTitle": "Як налаштувати параметри комбо за замовчуванням",
"comboDefaultsGuideHint1": "Зберігайте низькі повторні спроби в потоках із низькою затримкою; збільшити час очікування лише для завдань тривалого покоління.",
"comboDefaultsGuideHint2": "Використовуйте перевизначення постачальника, коли одному постачальнику потрібна інша поведінка тайм-ауту/повторної спроби, ніж глобальні стандартні налаштування."
},
"translator": {
"title": "Перекладач",
@@ -2569,7 +2606,15 @@
"opencodeDetected": "opencode {version} detected",
"opencodeDesc": "Generate a ready-to-use {configFile} with your OmniRoute base URL and all available models — drop it in your project root and run {command}.",
"downloadConfig": "Download {file}",
"downloaded": "Downloaded!"
"downloaded": "Downloaded!",
"setupGuideTitle": "Setup guide",
"openCliTools": "Open CLI Tools",
"setupGuideDetectCliTitle": "Detect installed CLIs",
"setupGuideDetectCliDesc": "Click Refresh after installing or updating a CLI so OmniRoute can rescan binaries and versions.",
"setupGuideCustomAgentTitle": "Register custom binary",
"setupGuideCustomAgentDesc": "Use Add Custom Agent when your CLI is not in the built-in list. Provide binary name and version command.",
"setupGuideCommandMissingTitle": "Fix 'command not found'",
"setupGuideCommandMissingDesc": "Ensure the CLI command exists in PATH, open a new terminal session, and rerun Refresh."
},
"autoCombo": {
"title": "Auto-Combo Engine",
@@ -2628,6 +2673,21 @@
"rerank": "Rerank",
"rerankModel": "Rerank Model",
"positionDelta": "Position Change",
"emptyState": "Send a search query to see results"
"emptyState": "Send a search query to see results",
"safeSearchOff": "Off",
"safeSearchModerate": "Moderate",
"safeSearchStrict": "Strict",
"queryPlaceholder": "Enter search query...",
"providerAuto": "auto (cheapest)",
"searchTypeWeb": "web",
"searchTypeNews": "news",
"optionAny": "any",
"timeRangeDay": "Past day",
"timeRangeWeek": "Past week",
"timeRangeMonth": "Past month",
"timeRangeYear": "Past year",
"domainPlaceholder": "example.com",
"requestTimedOut": "Request timed out ({seconds}s)",
"networkError": "Network error"
}
}

View File

@@ -56,7 +56,9 @@
"warning": "Cảnh báo",
"note": "Lưu ý",
"free": "miễn phí",
"skipToContent": "Chuyển đến nội dung"
"skipToContent": "Chuyển đến nội dung",
"maintenanceServerIssues": "Server is experiencing issues. Some features may be unavailable.",
"maintenanceServerUnreachable": "Server is unreachable. Reconnecting..."
},
"sidebar": {
"home": "Trang chủ",
@@ -576,7 +578,23 @@
"mitmStep1": "1. Start MITM to route requests through OmniRoute.",
"mitmStep2Prefix": "2. Add",
"mitmStep2Suffix": "to your hosts file as 127.0.0.1.",
"mitmStep3": "3. Open {toolName} and requests will be proxied."
"mitmStep3": "3. Open {toolName} and requests will be proxied.",
"whenToUseLabel": "Khi nào nên sử dụng",
"openToolDocs": "Mở tài liệu công cụ",
"toolUseCases": {
"claude": "Sử dụng khi bạn muốn quy trình lập kế hoạch mạnh mẽ và các trình tái cấu trúc nhiều tệp dài với Mã Claude.",
"codex": "Sử dụng khi nhóm của bạn được chuẩn hóa trên các luồng OpenAI Codex CLI và xác thực dựa trên hồ sơ.",
"droid": "Sử dụng khi bạn cần một tác nhân đầu cuối nhẹ tập trung vào các vòng lặp thực thi lệnh và mã hóa nhanh.",
"openclaw": "Sử dụng khi bạn muốn một tác nhân mã hóa kiểu Open Claw nhưng được định tuyến thông qua các chính sách OmniRoute.",
"cline": "Sử dụng khi bạn định cấu hình các tác nhân mã hóa bên trong trình chỉnh sửa và muốn thiết lập có hướng dẫn với các mô hình OmniRoute.",
"kilo": "Sử dụng khi quy trình làm việc của bạn phụ thuộc vào các lệnh Kilo Code và các chỉnh sửa lặp lại nhanh chóng.",
"cursor": "Sử dụng khi mã hóa bằng Con trỏ và bạn cần các mô hình tương thích với OpenAI tùy chỉnh thông qua OmniRoute.",
"continue": "Sử dụng khi chạy Tiếp tục trong IDE và bạn cần cấu hình nhà cung cấp dựa trên JSON di động.",
"opencode": "Sử dụng khi bạn thích chạy tác nhân gốc trên thiết bị đầu cuối và tự động hóa theo kịch bản thông qua OpenCode.",
"kiro": "Sử dụng khi tích hợp Kiro và điều khiển định tuyến mô hình tập trung từ OmniRoute.",
"antigravity": "Sử dụng khi lưu lượng truy cập AntiGravity/Kiro phải bị chặn thông qua MITM và được định tuyến đến OmniRoute.",
"copilot": "Sử dụng khi bạn muốn UX kiểu trò chuyện Copilot trong khi thực thi các khóa OmniRoute và quy tắc định tuyến."
}
},
"combos": {
"title": "Combo",
@@ -1087,7 +1105,13 @@
"resetAllTitle": "Đặt lại tất cả các bộ ngắt mạch về trạng thái khỏe mạnh",
"resetting": "Đang đặt lại...",
"resetAll": "Đặt lại tất cả",
"until": "Cho đến {time}"
"until": "Cho đến {time}",
"activeProviders": "{count} active",
"monitoredProviders": "{count} monitored",
"configuredProvidersLabel": "Được định cấu hình trong bảng điều khiển",
"configuredProvidersHint": "Nhà cung cấp có thông tin xác thực được lưu trong /dashboard/providers, bất kể trạng thái thời gian chạy.",
"activeProvidersHint": "Nhà cung cấp được định cấu hình hiện đã được bật cho các yêu cầu định tuyến.",
"monitoredProvidersHint": "Các nhà cung cấp hiện được theo dõi bởi máy theo dõi tình trạng ngắt mạch."
},
"limits": {
"title": "Giới hạn & hạn ngạch",
@@ -1446,7 +1470,14 @@
"compatProtocolOpenAIResponses": "OpenAI Responses API",
"compatProtocolClaude": "Anthropic Messages",
"tokenRefreshed": "Token refreshed successfully",
"tokenRefreshFailed": "Token refresh failed"
"tokenRefreshFailed": "Token refresh failed",
"compatBadgeUpstreamHeaders": "Headers",
"compatUpstreamAddRow": "Add header",
"compatUpstreamHeaderName": "Header name",
"compatUpstreamHeaderValue": "Value",
"compatUpstreamHeadersHint": "High-privilege setting — same trust level as editing provider API credentials; only trusted admins should use it. Merged after OmniRoute adds auth from the provider API key. If a custom header uses the same name as an existing one (e.g. Authorization), your value fully replaces the auto-generated header (including the Bearer token) — the upstream only sees what you typed, not the key from settings. Misconfiguration can cause 401 or broken upstream auth. One row per header (e.g. extra Authentication for some gateways). Hover or focus the value to preview. Saves on blur, outside click, or closing this panel.",
"compatUpstreamHeadersLabel": "Extra upstream headers",
"compatUpstreamRemoveRow": "Remove row"
},
"settings": {
"title": "Cài đặt",
@@ -1849,7 +1880,13 @@
"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}"
"disableFingerprintTitle": "Disable fingerprint for {provider}",
"routingAdvancedGuideTitle": "Hướng dẫn định tuyến nâng cao",
"routingAdvancedGuideHint1": "Sử dụng Fill First để có mức độ ưu tiên có thể dự đoán được, Round Robin để đảm bảo tính công bằng và P2C để có khả năng phục hồi độ trễ.",
"routingAdvancedGuideHint2": "Nếu các nhà cung cấp khác nhau về chất lượng/chi phí, hãy bắt đầu với Cost Opt cho công việc nền và Ít được sử dụng nhất để cân bằng độ hao mòn.",
"comboDefaultsGuideTitle": "Cách điều chỉnh mặc định kết hợp",
"comboDefaultsGuideHint1": "Giữ số lần thử ở mức thấp trong các luồng có độ trễ thấp; chỉ tăng thời gian chờ cho các tác vụ tạo dài.",
"comboDefaultsGuideHint2": "Sử dụng ghi đè nhà cung cấp khi một nhà cung cấp cần hành vi hết thời gian chờ/thử lại khác với mặc định chung."
},
"translator": {
"title": "Người phiên dịch",
@@ -2569,7 +2606,15 @@
"opencodeDetected": "opencode {version} detected",
"opencodeDesc": "Generate a ready-to-use {configFile} with your OmniRoute base URL and all available models — drop it in your project root and run {command}.",
"downloadConfig": "Download {file}",
"downloaded": "Downloaded!"
"downloaded": "Downloaded!",
"setupGuideTitle": "Setup guide",
"openCliTools": "Open CLI Tools",
"setupGuideDetectCliTitle": "Detect installed CLIs",
"setupGuideDetectCliDesc": "Click Refresh after installing or updating a CLI so OmniRoute can rescan binaries and versions.",
"setupGuideCustomAgentTitle": "Register custom binary",
"setupGuideCustomAgentDesc": "Use Add Custom Agent when your CLI is not in the built-in list. Provide binary name and version command.",
"setupGuideCommandMissingTitle": "Fix 'command not found'",
"setupGuideCommandMissingDesc": "Ensure the CLI command exists in PATH, open a new terminal session, and rerun Refresh."
},
"autoCombo": {
"title": "Auto-Combo Engine",
@@ -2628,6 +2673,21 @@
"rerank": "Rerank",
"rerankModel": "Rerank Model",
"positionDelta": "Position Change",
"emptyState": "Send a search query to see results"
"emptyState": "Send a search query to see results",
"safeSearchOff": "Off",
"safeSearchModerate": "Moderate",
"safeSearchStrict": "Strict",
"queryPlaceholder": "Enter search query...",
"providerAuto": "auto (cheapest)",
"searchTypeWeb": "web",
"searchTypeNews": "news",
"optionAny": "any",
"timeRangeDay": "Past day",
"timeRangeWeek": "Past week",
"timeRangeMonth": "Past month",
"timeRangeYear": "Past year",
"domainPlaceholder": "example.com",
"requestTimedOut": "Request timed out ({seconds}s)",
"networkError": "Network error"
}
}

View File

@@ -56,7 +56,9 @@
"warning": "警告",
"note": "注意事项",
"free": "免费",
"skipToContent": "跳至内容"
"skipToContent": "跳至内容",
"maintenanceServerIssues": "Server is experiencing issues. Some features may be unavailable.",
"maintenanceServerUnreachable": "Server is unreachable. Reconnecting..."
},
"sidebar": {
"home": "首页",
@@ -364,7 +366,22 @@
"rerank": "重排",
"rerankModel": "重排模型",
"positionDelta": "排名变化",
"emptyState": "发送搜索请求后即可查看结果"
"emptyState": "发送搜索请求后即可查看结果",
"safeSearchOff": "Off",
"safeSearchModerate": "Moderate",
"safeSearchStrict": "Strict",
"queryPlaceholder": "Enter search query...",
"providerAuto": "auto (cheapest)",
"searchTypeWeb": "web",
"searchTypeNews": "news",
"optionAny": "any",
"timeRangeDay": "Past day",
"timeRangeWeek": "Past week",
"timeRangeMonth": "Past month",
"timeRangeYear": "Past year",
"domainPlaceholder": "example.com",
"requestTimedOut": "Request timed out ({seconds}s)",
"networkError": "Network error"
},
"cliTools": {
"title": "CLI工具",
@@ -612,7 +629,23 @@
"mitmStep1": "1. Start MITM to route requests through OmniRoute.",
"mitmStep2Prefix": "2. Add",
"mitmStep2Suffix": "to your hosts file as 127.0.0.1.",
"mitmStep3": "3. Open {toolName} and requests will be proxied."
"mitmStep3": "3. Open {toolName} and requests will be proxied.",
"whenToUseLabel": "何时使用",
"openToolDocs": "打开工具文档",
"toolUseCases": {
"claude": "当您需要强大的规划工作流程和使用 Claude Code 进行长的多文件重构时使用。",
"codex": "当您的团队在 OpenAI Codex CLI 流程和基于配置文件的身份验证方面实现标准化时使用。",
"droid": "当您需要专注于快速编码和命令执行循环的轻量级终端代理时使用。",
"openclaw": "当您需要 Open Claw 风格的编码代理但通过 OmniRoute 策略进行路由时使用。",
"cline": "当您在编辑器内配置编码代理并希望使用 OmniRoute 模型进行引导设置时使用。",
"kilo": "当您的工作流程依赖于 Kilo Code 命令和快速迭代编辑时使用。",
"cursor": "在 Cursor 中编码并且需要通过 OmniRoute 自定义 OpenAI 兼容模型时使用。",
"continue": "在 IDE 中运行“Continue”并且需要可移植的基于 JSON 的提供程序配置时使用。",
"opencode": "当您更喜欢通过 OpenCode 进行终端本机代理运行和脚本自动化时使用。",
"kiro": "在集成 Kiro 并从 OmniRoute 集中控制模型路由时使用。",
"antigravity": "当必须通过 MITM 拦截 Antigravity/Kiro 流量并将其路由到 OmniRoute 时使用。",
"copilot": "当您想要 Copilot 聊天风格的 UX 同时强制执行 OmniRoute 键和路由规则时使用。"
}
},
"combos": {
"title": "组合",
@@ -1135,7 +1168,13 @@
"resetAllTitle": "将所有断路器重置为正常状态",
"resetting": "正在重置...",
"resetAll": "全部重置",
"until": "直到 {time}"
"until": "直到 {time}",
"activeProviders": "{count} active",
"monitoredProviders": "{count} monitored",
"configuredProvidersLabel": "在仪表板中配置",
"configuredProvidersHint": "凭证保存在 /dashboard/providers 中的提供者,无论运行时状态如何。",
"activeProvidersHint": "当前启用路由请求的已配置提供程序。",
"monitoredProvidersHint": "目前由断路器运行状况监控器跟踪提供者。"
},
"limits": {
"title": "限制和配额",
@@ -1443,6 +1482,13 @@
"compatProtocolOpenAI": "OpenAI Chat Completions",
"compatProtocolOpenAIResponses": "OpenAI Responses API",
"compatProtocolClaude": "Anthropic Messages",
"compatUpstreamHeadersLabel": "上游额外请求头",
"compatUpstreamHeadersHint": "与修改厂商连接/API 配置同属高权限能力,仅可信管理员应使用。这些头会在 OmniRoute 按厂商 API Key 自动加好鉴权头之后再合并。若「名称」与系统已加的头相同(例如都叫 Authorization则以你填的值为准会整段替换自动那条含 Bearer 令牌),上游请求里不再使用面板里保存的密钥来生成 Authorization。填错可能导致 401请谨慎。每个请求头单独一行部分网关需要额外 Authentication 等可在此加。鼠标移入或聚焦「值」可暂时看明文。点空白处、关闭本面板或切走焦点即保存。",
"compatUpstreamHeaderName": "请求头名称",
"compatUpstreamHeaderValue": "值",
"compatUpstreamAddRow": "添加请求头",
"compatUpstreamRemoveRow": "删除此行",
"compatBadgeUpstreamHeaders": "请求头",
"modelId": "模型 ID",
"customModelPlaceholder": "例如gpt-4.5-turbo",
"loading": "正在加载...",
@@ -1897,7 +1943,13 @@
"customPricingNote": "你可以覆盖特定模型的默认定价。自定义覆盖会优先于自动检测到的定价。",
"editPricing": "编辑定价",
"viewFullDetails": "查看完整详情",
"themeCoral": "珊瑚色"
"themeCoral": "珊瑚色",
"routingAdvancedGuideTitle": "高级路线指导",
"routingAdvancedGuideHint1": "使用 Fill First 实现可预测的优先级,使用 Round Robin 实现公平性,使用 P2C 实现延迟弹性。",
"routingAdvancedGuideHint2": "如果提供商的质量/成本各不相同,请从“成本选择”开始进行后台工作,并从“最少使用”开始进行平衡磨损。",
"comboDefaultsGuideTitle": "如何调整组合默认值",
"comboDefaultsGuideHint1": "在低延迟流中保持较低的重试次数;仅增加长生成任务的超时。",
"comboDefaultsGuideHint2": "当一个提供程序需要与全局默认值不同的超时/重试行为时,请使用提供程序覆盖。"
},
"translator": {
"title": "翻译者",
@@ -2605,7 +2657,15 @@
"opencodeDetected": "opencode {version} detected",
"opencodeDesc": "Generate a ready-to-use {configFile} with your OmniRoute base URL and all available models — drop it in your project root and run {command}.",
"downloadConfig": "Download {file}",
"downloaded": "Downloaded!"
"downloaded": "Downloaded!",
"setupGuideTitle": "Setup guide",
"openCliTools": "Open CLI Tools",
"setupGuideDetectCliTitle": "Detect installed CLIs",
"setupGuideDetectCliDesc": "Click Refresh after installing or updating a CLI so OmniRoute can rescan binaries and versions.",
"setupGuideCustomAgentTitle": "Register custom binary",
"setupGuideCustomAgentDesc": "Use Add Custom Agent when your CLI is not in the built-in list. Provide binary name and version command.",
"setupGuideCommandMissingTitle": "Fix 'command not found'",
"setupGuideCommandMissingDesc": "Ensure the CLI command exists in PATH, open a new terminal session, and rerun Refresh."
},
"autoCombo": {
"title": "自动组合引擎",

View File

@@ -10,11 +10,10 @@
export async function register() {
if (process.env.NEXT_RUNTIME === "nodejs") {
// Computed path prevents Turbopack from statically resolving the import
// for the Edge instrumentation bundle, avoiding spurious warnings about
// Node.js modules not being available in the Edge Runtime.
const nodeMod = "./instrumentation-" + "node";
const { registerNodejs } = await import(nodeMod);
// Literal path so Webpack emits the chunk (computed string breaks dev:
// MODULE_NOT_FOUND for ./instrumentation-node at runtime).
// Turbopack may still avoid tracing this into Edge when guarded by NEXT_RUNTIME.
const { registerNodejs } = await import("./instrumentation-node");
await registerNodejs();
}
}

View File

@@ -8,6 +8,7 @@ import {
MODEL_COMPAT_PROTOCOL_KEYS,
type ModelCompatProtocolKey,
} from "@/shared/constants/modelCompat";
import { isForbiddenUpstreamHeaderName } from "@/shared/constants/upstreamHeaders";
type JsonRecord = Record<string, unknown>;
@@ -19,6 +20,8 @@ export { MODEL_COMPAT_PROTOCOL_KEYS, type ModelCompatProtocolKey };
export type ModelCompatPerProtocol = {
normalizeToolCallId?: boolean;
preserveOpenAIDeveloperRole?: boolean;
/** Merged into upstream HTTP requests for this model (after default auth headers). */
upstreamHeaders?: Record<string, string>;
};
type CompatByProtocolMap = Partial<Record<ModelCompatProtocolKey, ModelCompatPerProtocol>>;
@@ -27,6 +30,43 @@ function isCompatProtocolKey(p: string): p is ModelCompatProtocolKey {
return (MODEL_COMPAT_PROTOCOL_KEYS as readonly string[]).includes(p);
}
const UPSTREAM_HEADERS_MAX = 16;
const UPSTREAM_HEADER_NAME_MAX = 128;
const UPSTREAM_HEADER_VALUE_MAX = 4096;
function isValidUpstreamHeaderName(k: string): boolean {
if (!k || k.length > UPSTREAM_HEADER_NAME_MAX) return false;
if (isForbiddenUpstreamHeaderName(k)) return false;
if (/[\r\n\0]/.test(k)) return false;
if (/\s/.test(k)) return false;
if (k.includes(":")) return false;
return true;
}
/** Sanitize user-provided upstream header map (used when persisting and when reading for requests). */
export function sanitizeUpstreamHeadersMap(
raw: Record<string, unknown> | null | undefined
): Record<string, string> {
const out: Record<string, string> = {};
if (!raw || typeof raw !== "object") return out;
for (const [k0, v0] of Object.entries(raw)) {
const k = String(k0).trim();
if (!k || !isValidUpstreamHeaderName(k)) {
continue;
}
const v =
typeof v0 === "string"
? v0.trim().slice(0, UPSTREAM_HEADER_VALUE_MAX)
: String(v0 ?? "")
.trim()
.slice(0, UPSTREAM_HEADER_VALUE_MAX);
if (v.includes("\r") || v.includes("\n")) continue;
out[k] = v;
if (Object.keys(out).length >= UPSTREAM_HEADERS_MAX) break;
}
return out;
}
function deepMergeCompatByProtocol(
prev: CompatByProtocolMap | undefined,
patch: Partial<Record<ModelCompatProtocolKey, Partial<ModelCompatPerProtocol>>>
@@ -38,7 +78,8 @@ function deepMergeCompatByProtocol(
if (!deltas || typeof deltas !== "object") continue;
const hasDelta =
Object.prototype.hasOwnProperty.call(deltas, "normalizeToolCallId") ||
Object.prototype.hasOwnProperty.call(deltas, "preserveOpenAIDeveloperRole");
Object.prototype.hasOwnProperty.call(deltas, "preserveOpenAIDeveloperRole") ||
Object.prototype.hasOwnProperty.call(deltas, "upstreamHeaders");
if (!hasDelta) continue;
const cur: ModelCompatPerProtocol = { ...(out[key] || {}) };
if ("normalizeToolCallId" in deltas) {
@@ -47,6 +88,16 @@ function deepMergeCompatByProtocol(
if ("preserveOpenAIDeveloperRole" in deltas) {
cur.preserveOpenAIDeveloperRole = Boolean(deltas.preserveOpenAIDeveloperRole);
}
if ("upstreamHeaders" in deltas) {
const uh = deltas.upstreamHeaders;
if (uh === undefined) {
/* skip */
} else {
const s = sanitizeUpstreamHeadersMap(uh as Record<string, unknown>);
if (Object.keys(s).length === 0) delete cur.upstreamHeaders;
else cur.upstreamHeaders = s;
}
}
if (Object.keys(cur).length === 0) delete out[key];
else out[key] = cur;
}
@@ -58,6 +109,7 @@ export type ModelCompatOverride = {
normalizeToolCallId?: boolean;
preserveOpenAIDeveloperRole?: boolean;
compatByProtocol?: CompatByProtocolMap;
upstreamHeaders?: Record<string, string>;
};
function readCompatList(providerId: string): ModelCompatOverride[] {
@@ -100,6 +152,8 @@ export type ModelCompatPatch = {
normalizeToolCallId?: boolean;
preserveOpenAIDeveloperRole?: boolean | null;
compatByProtocol?: CompatByProtocolMap;
/** Replace top-level extra headers for override-only rows; omit to leave unchanged. */
upstreamHeaders?: Record<string, string> | null;
};
function compatByProtocolHasEntries(map: CompatByProtocolMap | undefined): boolean {
@@ -135,12 +189,23 @@ export function mergeModelCompatOverride(
if (compatByProtocolHasEntries(merged)) next.compatByProtocol = merged;
else delete next.compatByProtocol;
}
if ("upstreamHeaders" in patch) {
if (patch.upstreamHeaders === null) {
delete next.upstreamHeaders;
} else if (patch.upstreamHeaders && typeof patch.upstreamHeaders === "object") {
const s = sanitizeUpstreamHeadersMap(patch.upstreamHeaders as Record<string, unknown>);
if (Object.keys(s).length === 0) delete next.upstreamHeaders;
else next.upstreamHeaders = s;
}
}
const filtered = list.filter((e) => e.id !== modelId);
const hasPreserveFlag = Object.prototype.hasOwnProperty.call(next, "preserveOpenAIDeveloperRole");
const hasTopUpstream = next.upstreamHeaders && Object.keys(next.upstreamHeaders).length > 0;
if (
next.normalizeToolCallId ||
hasPreserveFlag ||
compatByProtocolHasEntries(next.compatByProtocol)
compatByProtocolHasEntries(next.compatByProtocol) ||
hasTopUpstream
) {
filtered.push(next);
}
@@ -388,6 +453,17 @@ export async function updateCustomModel(
}
}
if (Object.prototype.hasOwnProperty.call(updates, "upstreamHeaders")) {
const uh = updates.upstreamHeaders;
if (uh === null || uh === undefined) {
delete next.upstreamHeaders;
} else if (typeof uh === "object" && !Array.isArray(uh)) {
const s = sanitizeUpstreamHeadersMap(uh as Record<string, unknown>);
if (Object.keys(s).length === 0) delete next.upstreamHeaders;
else next.upstreamHeaders = s;
}
}
models[index] = next;
db.prepare("UPDATE key_value SET value = ? WHERE namespace = 'customModels' AND key = ?").run(
@@ -491,3 +567,61 @@ export function getModelPreserveOpenAIDeveloperRole(
}
return undefined;
}
function readUpstreamFromJsonRecord(
row: JsonRecord | null | undefined,
key: "upstreamHeaders"
): Record<string, string> | undefined {
if (!row) return undefined;
const raw = row[key];
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return undefined;
const s = sanitizeUpstreamHeadersMap(raw as Record<string, unknown>);
return Object.keys(s).length > 0 ? s : undefined;
}
/**
* Extra HTTP headers to send to the upstream provider for this model (after executor auth headers).
* Order: top-level `upstreamHeaders` on the custom model row (override list merged under custom),
* then per-protocol `compatByProtocol[sourceFormat].upstreamHeaders` (wins on key conflict).
* Use for gateways that expect `Authentication`, `X-API-Key`, etc. alongside Bearer.
*
* `modelId` should be the **canonical** model id when known. Callers that accept client aliases
* (e.g. chat proxy) should merge results for both alias and `resolveModelAlias(alias)` so UI
* config on the resolved id still applies — see `chatCore` merge.
*/
export function getModelUpstreamExtraHeaders(
providerId: string,
modelId: string,
sourceFormat?: string | null
): Record<string, string> {
const protocol = sourceFormat && isCompatProtocolKey(sourceFormat) ? sourceFormat : null;
const m = getCustomModelRow(providerId, modelId);
const base: Record<string, string> = {};
if (m) {
const fromModel = readUpstreamFromJsonRecord(m, "upstreamHeaders");
if (fromModel) Object.assign(base, fromModel);
if (protocol) {
const pc = (m.compatByProtocol as CompatByProtocolMap | undefined)?.[protocol];
const fromProto = pc?.upstreamHeaders;
if (fromProto && typeof fromProto === "object") {
Object.assign(base, sanitizeUpstreamHeadersMap(fromProto as Record<string, unknown>));
}
}
return base;
}
const co = readCompatList(providerId).find((e) => e.id === modelId);
if (co?.upstreamHeaders) {
Object.assign(base, sanitizeUpstreamHeadersMap(co.upstreamHeaders as Record<string, unknown>));
}
if (protocol && co?.compatByProtocol?.[protocol]?.upstreamHeaders) {
Object.assign(
base,
sanitizeUpstreamHeadersMap(
co.compatByProtocol[protocol]!.upstreamHeaders as Record<string, unknown>
)
);
}
return base;
}

View File

@@ -55,6 +55,7 @@ export {
removeModelCompatOverride,
getModelNormalizeToolCallId,
getModelPreserveOpenAIDeveloperRole,
getModelUpstreamExtraHeaders,
} from "./db/models";
export type { ModelCompatPerProtocol, ModelCompatPatch } from "./db/models";

View File

@@ -4,14 +4,31 @@
* Extracts OAuth credentials from OS keychain where Zed IDE stores them.
* Supports macOS (Keychain), Windows (Credential Manager), and Linux (libsecret).
*
* `keytar` is a native module (e.g. libsecret on Linux). Load it only via dynamic import
* so `next build` / CI without those libs does not fail when this module is analyzed.
*
* @see https://zed.dev/docs/ai/llm-providers - Official Zed documentation confirming keychain storage
*/
import keytar from "keytar";
import fs from "fs";
import os from "os";
import path from "path";
/** Minimal keytar surface (CJS/native; typings may not expose `default`). */
type KeytarModule = {
findCredentials: (service: string) => Promise<Array<{ account: string; password: string }>>;
getPassword: (service: string, account: string) => Promise<string | null>;
};
async function loadKeytar(): Promise<KeytarModule | null> {
try {
const mod = (await import("keytar")) as { default?: KeytarModule } & KeytarModule;
return mod.default ?? mod;
} catch {
return null;
}
}
export interface ZedCredential {
provider: string;
service: string;
@@ -87,6 +104,12 @@ function extractProviderFromService(service: string): string {
* @returns Array of discovered credentials with provider, service, and token
*/
export async function discoverZedCredentials(): Promise<ZedCredential[]> {
const keytar = await loadKeytar();
if (!keytar) {
console.debug("[Zed keychain] keytar not available — skipping keychain read");
return [];
}
const credentials: ZedCredential[] = [];
for (const pattern of ZED_SERVICE_PATTERNS) {
@@ -128,6 +151,9 @@ export async function discoverZedCredentials(): Promise<ZedCredential[]> {
* @returns The credential if found, null otherwise
*/
export async function getZedCredential(provider: string): Promise<ZedCredential | null> {
const keytar = await loadKeytar();
if (!keytar) return null;
const patterns = ZED_SERVICE_PATTERNS.filter((p) =>
p.toLowerCase().includes(provider.toLowerCase())
);

View File

@@ -9,27 +9,46 @@
*/
import { useState, useEffect } from "react";
import { useRef } from "react";
import { useTranslations } from "next-intl";
export default function MaintenanceBanner() {
const [show, setShow] = useState(false);
const [message, setMessage] = useState("");
const consecutiveFailuresRef = useRef(0);
const dismissedUntilRecoveryRef = useRef(false);
const t = useTranslations("common");
useEffect(() => {
const checkHealth = async () => {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 8000);
try {
const res = await fetch("/api/monitoring/health", {
signal: AbortSignal.timeout(3000),
signal: controller.signal,
cache: "no-store",
});
if (res.ok) {
consecutiveFailuresRef.current = 0;
dismissedUntilRecoveryRef.current = false;
setShow(false);
setMessage("");
} else {
setShow(true);
setMessage("Server is experiencing issues. Some features may be unavailable.");
consecutiveFailuresRef.current += 1;
// Require at least 2 failed checks to avoid transient false positives.
if (consecutiveFailuresRef.current >= 2 && !dismissedUntilRecoveryRef.current) {
setShow(true);
setMessage(t("maintenanceServerIssues"));
}
}
} catch {
setShow(true);
setMessage("Server is unreachable. Reconnecting...");
consecutiveFailuresRef.current += 1;
if (consecutiveFailuresRef.current >= 2 && !dismissedUntilRecoveryRef.current) {
setShow(true);
setMessage(t("maintenanceServerUnreachable"));
}
} finally {
clearTimeout(timeoutId);
}
};
@@ -37,7 +56,7 @@ export default function MaintenanceBanner() {
checkHealth();
const interval = setInterval(checkHealth, 10000);
return () => clearInterval(interval);
}, []); // empty deps — checkHealth is defined inside effect, no stale closure
}, [t]);
if (!show) return null;
@@ -50,9 +69,12 @@ export default function MaintenanceBanner() {
<span className="text-sm text-amber-200">{message}</span>
</div>
<button
onClick={() => setShow(false)}
onClick={() => {
dismissedUntilRecoveryRef.current = true;
setShow(false);
}}
className="p-1 rounded hover:bg-white/5 text-text-muted hover:text-text-main transition-colors"
aria-label="Dismiss"
aria-label={t("close")}
>
<span className="material-symbols-outlined text-[16px]">close</span>
</button>

View File

@@ -0,0 +1,30 @@
import { CLI_TOOLS } from "./cliTools";
/**
* Provider IDs toggled in Settings -> CLI Fingerprint.
*
* Source of truth:
* - derive from visible CLI tools when a provider mapping exists
* - keep legacy-compatible IDs that are still used by existing setups
*/
const TOOL_ID_TO_PROVIDER_ID: Record<string, string> = {
kilo: "kilocode",
copilot: "github",
};
const DERIVED_PROVIDER_IDS = Object.values(CLI_TOOLS)
.map((tool: any) => TOOL_ID_TO_PROVIDER_ID[tool.id] ?? tool.id)
// "continue" currently has no provider id in AI_PROVIDERS
.filter((providerId) => providerId !== "continue");
const LEGACY_PROVIDER_IDS = [
// Keep to avoid breaking setups that saved old IDs
"copilot",
"kimi-coding",
"qwen",
];
export const CLI_COMPAT_PROVIDER_IDS = Array.from(
new Set([...DERIVED_PROVIDER_IDS, ...LEGACY_PROVIDER_IDS])
);

View File

@@ -6,6 +6,7 @@ export const CLI_TOOLS = {
icon: "terminal",
color: "#D97757",
description: "Anthropic Claude Code CLI",
docsUrl: "https://docs.anthropic.com/en/docs/claude-code/overview",
configType: "env",
envVars: {
baseUrl: "ANTHROPIC_BASE_URL",
@@ -47,6 +48,7 @@ export const CLI_TOOLS = {
image: "/providers/codex.png",
color: "#10A37F",
description: "OpenAI Codex CLI",
docsUrl: "https://github.com/openai/codex",
configType: "custom",
defaultCommand: "codex",
},
@@ -56,6 +58,7 @@ export const CLI_TOOLS = {
image: "/providers/droid.png",
color: "#00D4FF",
description: "Factory Droid AI Assistant",
docsUrl: "/docs?section=cli-tools&tool=droid",
configType: "custom",
defaultCommand: "droid",
},
@@ -65,6 +68,7 @@ export const CLI_TOOLS = {
image: "/providers/openclaw.png",
color: "#FF6B35",
description: "Open Claw AI Assistant",
docsUrl: "/docs?section=cli-tools&tool=openclaw",
configType: "custom",
defaultCommand: "openclaw",
},
@@ -74,6 +78,7 @@ export const CLI_TOOLS = {
image: "/providers/cursor.png",
color: "#000000",
description: "Cursor AI Code Editor",
docsUrl: "https://docs.cursor.com/settings/models",
configType: "guide",
requiresCloud: true,
defaultCommands: ["agent", "cursor"],
@@ -99,6 +104,7 @@ export const CLI_TOOLS = {
image: "/providers/cline.png",
color: "#00D1B2",
description: "Cline AI Coding Assistant CLI",
docsUrl: "https://docs.cline.bot/",
configType: "custom",
defaultCommand: "cline",
},
@@ -108,6 +114,7 @@ export const CLI_TOOLS = {
image: "/providers/kilocode.png",
color: "#FF6B6B",
description: "Kilo Code AI Assistant CLI",
docsUrl: "/docs?section=cli-tools&tool=kilocode",
configType: "custom",
defaultCommand: "kilocode",
},
@@ -117,6 +124,7 @@ export const CLI_TOOLS = {
image: "/providers/continue.png",
color: "#7C3AED",
description: "Continue AI Assistant",
docsUrl: "https://docs.continue.dev/",
configType: "guide",
guideSteps: [
{ step: 1, title: "Open Config", desc: "Open Continue configuration file" },
@@ -145,6 +153,7 @@ export const CLI_TOOLS = {
image: "/providers/antigravity.png",
color: "#4285F4",
description: "Google Antigravity IDE with MITM",
docsUrl: "/docs?section=cli-tools&tool=antigravity",
configType: "mitm",
modelAliases: [
"claude-opus-4-6-thinking",
@@ -177,6 +186,7 @@ export const CLI_TOOLS = {
image: "/providers/copilot.png",
color: "#1F6FEB",
description: "GitHub Copilot Chat — VS Code Extension",
docsUrl: "https://code.visualstudio.com/docs/copilot/overview",
configType: "custom",
},
opencode: {
@@ -186,6 +196,7 @@ export const CLI_TOOLS = {
icon: "terminal",
color: "#FF6B35",
description: "OpenCode AI coding agent (Terminal)",
docsUrl: "/docs?section=cli-tools&tool=opencode",
configType: "guide",
defaultCommand: "opencode",
notes: [
@@ -236,6 +247,7 @@ export const CLI_TOOLS = {
icon: "psychology_alt",
color: "#FF6B35",
description: "Amazon Kiro — AI-powered IDE with MITM",
docsUrl: "/docs?section=cli-tools&tool=kiro",
configType: "mitm",
guideSteps: [
{ step: 1, title: "Open Kiro Settings", desc: "Go to Settings → AI Provider" },

View File

@@ -0,0 +1,94 @@
export type RoutingStrategyValue =
| "priority"
| "weighted"
| "round-robin"
| "fill-first"
| "p2c"
| "random"
| "least-used"
| "cost-optimized"
| "strict-random";
type RoutingStrategyOption = {
value: RoutingStrategyValue;
labelKey: string;
combosDescKey: string;
settingsDescKey: string;
icon: string;
};
export const ROUTING_STRATEGIES: RoutingStrategyOption[] = [
{
value: "priority",
labelKey: "priority",
combosDescKey: "priorityDesc",
settingsDescKey: "priorityDesc",
icon: "sort",
},
{
value: "weighted",
labelKey: "weighted",
combosDescKey: "weightedDesc",
settingsDescKey: "weightedDesc",
icon: "percent",
},
{
value: "round-robin",
labelKey: "roundRobin",
combosDescKey: "roundRobinDesc",
settingsDescKey: "roundRobinDesc",
icon: "autorenew",
},
{
value: "fill-first",
labelKey: "fillFirst",
combosDescKey: "fillFirstDesc",
settingsDescKey: "fillFirstDesc",
icon: "vertical_align_top",
},
{
value: "p2c",
labelKey: "p2c",
combosDescKey: "p2cDesc",
settingsDescKey: "p2cDesc",
icon: "balance",
},
{
value: "random",
labelKey: "random",
combosDescKey: "randomDesc",
settingsDescKey: "randomDesc",
icon: "shuffle",
},
{
value: "least-used",
labelKey: "leastUsed",
combosDescKey: "leastUsedDesc",
settingsDescKey: "leastUsedDesc",
icon: "low_priority",
},
{
value: "cost-optimized",
labelKey: "costOpt",
combosDescKey: "costOptimizedDesc",
settingsDescKey: "costOptDesc",
icon: "savings",
},
{
value: "strict-random",
labelKey: "strictRandom",
combosDescKey: "strictRandomDesc",
settingsDescKey: "strictRandomDesc",
icon: "casino",
},
];
export const SETTINGS_FALLBACK_STRATEGY_VALUES: RoutingStrategyValue[] = [
"fill-first",
"round-robin",
"p2c",
"random",
"least-used",
"cost-optimized",
"strict-random",
];

View File

@@ -0,0 +1,22 @@
/**
* User-supplied upstream extra headers: names we never forward (Host / hop-by-hop / framing).
* Changing this list requires syncing: `sanitizeUpstreamHeadersMap` (models.ts), Zod
* `upstreamHeaderNameSchema` / record refine (schemas.ts), and `upstream-headers-sanitize` tests.
*/
const FORBIDDEN = new Set(
[
"host",
"connection",
"content-length",
"keep-alive",
"proxy-connection",
"transfer-encoding",
"te",
"trailer",
"upgrade",
].map((s) => s.toLowerCase())
);
export function isForbiddenUpstreamHeaderName(name: string): boolean {
return FORBIDDEN.has(String(name).trim().toLowerCase());
}

View File

@@ -1,4 +1,5 @@
import { z } from "zod";
import { isForbiddenUpstreamHeaderName } from "@/shared/constants/upstreamHeaders";
function isHttpUrl(value: string): boolean {
try {
@@ -107,6 +108,7 @@ export const createComboSchema = z.object({
system_message: z.string().max(50000).optional(),
tool_filter_regex: z.string().max(1000).optional(),
context_cache_protection: z.boolean().optional(),
context_length: z.number().int().min(1000).max(2000000).optional(),
});
// ──── Auto-Combo Schemas ────
@@ -340,10 +342,34 @@ export const clearModelAvailabilitySchema = z.object({
model: modelIdSchema,
});
/** Align with `sanitizeUpstreamHeadersMap` — allow non-ASCII names; reject Host / hop-by-hop / whitespace / ":". */
const upstreamHeaderNameSchema = z
.string()
.trim()
.min(1)
.max(128)
.refine((s) => !/[\r\n\0]/.test(s), { message: "header name cannot contain control characters" })
.refine((s) => !/\s/.test(s), { message: "header name cannot contain whitespace" })
.refine((s) => !s.includes(":"), { message: "header name cannot contain ':'" })
.refine((s) => !isForbiddenUpstreamHeaderName(s), { message: "header name is not allowed" });
const upstreamHeaderValueSchema = z
.string()
.max(4096)
.refine((s) => !/[\r\n]/.test(s), { message: "header value cannot contain line breaks" });
const upstreamHeadersRecordSchema = z
.record(upstreamHeaderNameSchema, upstreamHeaderValueSchema)
.refine((rec) => Object.keys(rec).length <= 16, { message: "at most 16 custom headers" })
.refine((rec) => !Object.keys(rec).some((k) => isForbiddenUpstreamHeaderName(k)), {
message: "forbidden header name in record",
});
const modelCompatPerProtocolSchema = z
.object({
normalizeToolCallId: z.boolean().optional(),
preserveOpenAIDeveloperRole: z.boolean().optional(),
upstreamHeaders: upstreamHeadersRecordSchema.optional(),
})
.strict();
@@ -356,8 +382,10 @@ export const providerModelMutationSchema = z.object({
supportedEndpoints: z.array(z.enum(["chat", "embeddings", "images", "audio"])).default(["chat"]),
normalizeToolCallId: z.boolean().optional(),
preserveOpenAIDeveloperRole: z.boolean().nullable().optional(),
upstreamHeaders: upstreamHeadersRecordSchema.nullable().optional(),
/** Zod 4: `z.record(z.enum([...]), …)` requires every enum key; use `partialRecord` for sparse patches. */
compatByProtocol: z
.record(z.enum(["openai", "openai-responses", "claude"]), modelCompatPerProtocolSchema)
.partialRecord(z.enum(["openai", "openai-responses", "claude"]), modelCompatPerProtocolSchema)
.optional(),
});

View File

@@ -42,6 +42,8 @@ export const updateSettingsSchema = z.object({
a2aEnabled: z.boolean().optional(),
// CLI Fingerprint compatibility (per-provider)
cliCompatProviders: z.array(z.string().max(100)).optional(),
// Strip provider/model prefix at proxy layer (e.g. "openai/gpt-4" → "gpt-4")
stripModelPrefix: z.boolean().optional(),
// Custom CLI agent definitions for ACP
customAgents: z
.array(

View File

@@ -1,5 +1,6 @@
// Re-export from open-sse with localDb integration
import { getModelAliases, getComboByName, getProviderNodes, getCustomModels } from "@/lib/localDb";
import { getSettings } from "@/lib/localDb";
import {
parseModel,
resolveModelAliasFromMap,
@@ -77,6 +78,18 @@ export async function getModelInfo(modelStr) {
...(apiFormat && { apiFormat }),
};
}
// stripModelPrefix: if enabled, strip provider prefix and re-resolve
// the bare model name using existing heuristics (claude-* → anthropic, etc.)
try {
const settings = await getSettings();
if (settings.stripModelPrefix === true) {
const strippedResult = await getModelInfoCore(parsed.model, getModelAliases);
return { ...strippedResult, extendedContext };
}
} catch {
// If settings read fails, fall through to normal resolution
}
}
if (!parsed.isAlias) {

View File

@@ -0,0 +1,28 @@
import { test } from "node:test";
import assert from "node:assert/strict";
import { sanitizeUpstreamHeadersMap } from "../../src/lib/db/models.ts";
test("sanitizeUpstreamHeadersMap: drops hop-by-hop / Host names", () => {
const out = sanitizeUpstreamHeadersMap({
Host: "evil",
Connection: "close",
"Content-Length": "999",
"X-Custom": "ok",
});
assert.deepEqual(out, { "X-Custom": "ok" });
});
test("sanitizeUpstreamHeadersMap: drops values with CR/LF", () => {
const out = sanitizeUpstreamHeadersMap({
Good: "a",
Bad: "x\ny",
Bad2: "x\ry",
});
assert.deepEqual(out, { Good: "a" });
});
test("sanitizeUpstreamHeadersMap: caps count at 16", () => {
const raw = Object.fromEntries(Array.from({ length: 20 }, (_, i) => [`H${i}`, String(i)]));
const out = sanitizeUpstreamHeadersMap(raw);
assert.strictEqual(Object.keys(out).length, 16);
});