Compare commits
55 Commits
v2.8.9
...
v3.0.0-rc.2
| Author | SHA1 | Date | |
|---|---|---|---|
| 8b9abcb6cc | |||
| 1ecc1908c7 | |||
| 6a2c7b467d | |||
| 0acef57865 | |||
| 43046ee649 | |||
| a15fda0c08 | |||
| e5988764ce | |||
| 9c9d9b5a8d | |||
| 44dc564d85 | |||
| 83e367afab | |||
| 8b7e7c2669 | |||
| 53474021b7 | |||
| da1ed1b5b2 | |||
| e08d661600 | |||
| 1aa1bc7a26 | |||
| 47634e942e | |||
| 15466cbf1a | |||
| 2a749db427 | |||
| ecccce86e4 | |||
| bf3f64bea4 | |||
| 2f2d6b8535 | |||
| d68c884649 | |||
| 8b556de03b | |||
| 7229af53c3 | |||
| 81b3034c2f | |||
| f0419396b5 | |||
| 6b9c2754e8 | |||
| 8edb131f8b | |||
| d6f6520a79 | |||
| cc2bb4d719 | |||
| 3859f1c9ae | |||
| 5f8d774e19 | |||
| 538a3e855c | |||
| 03f2ef1e2b | |||
| 237d0746cf | |||
| 33b6c58087 | |||
| e96b023d04 | |||
| 7ac1d4621b | |||
| a2d7cbe8fe | |||
| c74ed29739 | |||
| 6c8501f122 | |||
| 941e945f74 | |||
| f2844d59e4 | |||
| 047ff187f6 | |||
| 1136c40811 | |||
| 5a78dc864f | |||
| 15c98c3048 | |||
| 0a5b005ce5 | |||
| 47959ee395 | |||
| c54a57838e | |||
| 13c45807ef | |||
| 00df10c29a | |||
| 41d91d628a | |||
| 605c3f9be1 | |||
| 2f0894c220 |
@@ -4,16 +4,36 @@ description: Create a new release, bump version up to 1.x.10 threshold, update c
|
||||
|
||||
# Generate Release Workflow
|
||||
|
||||
Bump version, finalize CHANGELOG, commit, tag, push, publish to npm, and create GitHub release.
|
||||
Bump version, finalize CHANGELOG, commit, open a **PR to main** and wait for user confirmation before tagging, publishing, and deploying.
|
||||
|
||||
> **VERSION RULE: Always use PATCH bumps (2.x.y → 2.x.y+1)**
|
||||
> NEVER use `npm version minor` or `npm version major`.
|
||||
> Always use: `npm version patch --no-git-tag-version`
|
||||
> The threshold rule: when `y` reaches 10, bump to `2.(x+1).0` — e.g. `2.1.10` → `2.2.0`.
|
||||
|
||||
## Steps
|
||||
---
|
||||
|
||||
### 1. Determine new version
|
||||
## ⚠️ Two-Phase Flow
|
||||
|
||||
```
|
||||
Phase 1 (automated): bump → docs → i18n → commit → push → open PR
|
||||
↕ 🛑 STOP: Notify user, wait for PR confirmation
|
||||
Phase 2 (post-merge): tag → publish → GitHub release → Docker → deploy
|
||||
```
|
||||
|
||||
**NEVER push directly to main or create tags before the user confirms the PR.**
|
||||
|
||||
---
|
||||
|
||||
## Phase 1: Pre-Merge
|
||||
|
||||
### 1. Create release branch
|
||||
|
||||
```bash
|
||||
git checkout -b release/v2.x.y
|
||||
```
|
||||
|
||||
### 2. Determine new version
|
||||
|
||||
Check current version in `package.json` and increment the **patch** number only:
|
||||
|
||||
@@ -27,11 +47,6 @@ Version format: `2.x.y` — examples:
|
||||
- `2.1.9` → `2.1.10` (patch)
|
||||
- `2.1.10` → `2.2.0` (minor threshold — do manually with `sed`)
|
||||
|
||||
```bash
|
||||
# ALWAYS use patch:
|
||||
npm version patch --no-git-tag-version
|
||||
```
|
||||
|
||||
> **⚠️ ATOMIC COMMIT RULE — Version bump MUST happen before committing feature files.**
|
||||
>
|
||||
> **CORRECT order:**
|
||||
@@ -53,7 +68,7 @@ npm version patch --no-git-tag-version
|
||||
> This ensures that `git show v2.x.y` always contains both code changes and the version bump together.
|
||||
> The GitHub release tag will point to a commit that includes ALL changes for that version.
|
||||
|
||||
### 2. Regenerate lock file (REQUIRED after version bump)
|
||||
### 3. Regenerate lock file (REQUIRED after version bump)
|
||||
|
||||
**Mandatory** — skipping causes `@swc/helpers` lock mismatch and CI failures:
|
||||
|
||||
@@ -61,7 +76,7 @@ npm version patch --no-git-tag-version
|
||||
npm install
|
||||
```
|
||||
|
||||
### 3. Finalize CHANGELOG.md
|
||||
### 4. Finalize CHANGELOG.md
|
||||
|
||||
Replace `[Unreleased]` header with the new version and date.
|
||||
Keep an empty `## [Unreleased]` section above it.
|
||||
@@ -74,7 +89,7 @@ Keep an empty `## [Unreleased]` section above it.
|
||||
## [2.x.y] — YYYY-MM-DD
|
||||
```
|
||||
|
||||
### 4. Update openapi.yaml version ⚠️ MANDATORY
|
||||
### 5. Update openapi.yaml version ⚠️ MANDATORY
|
||||
|
||||
> **CI will fail** if `docs/openapi.yaml` version ≠ `package.json` version (`check:docs-sync` enforces this).
|
||||
|
||||
@@ -84,33 +99,97 @@ Keep an empty `## [Unreleased]` section above it.
|
||||
VERSION=$(node -p "require('./package.json').version") && sed -i "s/ version: .*/ version: $VERSION/" docs/openapi.yaml && echo "✓ openapi.yaml → $VERSION"
|
||||
```
|
||||
|
||||
### 5. Stage, commit, and tag
|
||||
### 6. Update README.md and i18n docs
|
||||
|
||||
Run `/update-docs` workflow steps to:
|
||||
|
||||
- Update feature table rows in `README.md`
|
||||
- Sync changes to all 29 language `docs/i18n/*/README.md` files
|
||||
- Update `docs/FEATURES.md` if Settings section changed
|
||||
|
||||
### 7. Run tests
|
||||
|
||||
// turbo
|
||||
|
||||
```bash
|
||||
npm test
|
||||
```
|
||||
|
||||
All tests must pass before creating the PR.
|
||||
|
||||
### 8. Stage, commit, and push
|
||||
|
||||
// turbo-all
|
||||
|
||||
```bash
|
||||
git add package.json package-lock.json CHANGELOG.md docs/openapi.yaml
|
||||
git add -A
|
||||
git commit -m "chore(release): v2.x.y — summary of changes"
|
||||
git push origin release/v2.x.y
|
||||
```
|
||||
|
||||
### 9. Open PR to main
|
||||
|
||||
```bash
|
||||
gh pr create \
|
||||
--repo diegosouzapw/OmniRoute \
|
||||
--base main \
|
||||
--head release/v2.x.y \
|
||||
--title "chore(release): v2.x.y — summary" \
|
||||
--body "## 🚀 Release v2.x.y
|
||||
|
||||
### Changes
|
||||
...
|
||||
|
||||
### Tests
|
||||
- X/X tests pass
|
||||
|
||||
### ⚠️ After merging: run Phase 2 steps to tag, publish, and deploy."
|
||||
```
|
||||
|
||||
### 10. 🛑 STOP — Notify User & Await PR Confirmation
|
||||
|
||||
**This is a mandatory stop point.** Use `notify_user` with `BlockedOnUser: true`:
|
||||
|
||||
Inform the user:
|
||||
|
||||
- PR URL
|
||||
- Summary of changes
|
||||
- Test results
|
||||
- List of files changed
|
||||
|
||||
**DO NOT proceed to Phase 2 until the user confirms the PR looks good and merges it.**
|
||||
|
||||
---
|
||||
|
||||
## Phase 2: Post-Merge (only after user confirms)
|
||||
|
||||
> Run these steps only AFTER the user has merged the PR.
|
||||
|
||||
### 11. Pull main and create tag
|
||||
|
||||
```bash
|
||||
git checkout main
|
||||
git pull origin main
|
||||
git tag -a v2.x.y -m "Release v2.x.y"
|
||||
```
|
||||
|
||||
### 6. Push to GitHub
|
||||
### 12. Push tag to GitHub
|
||||
|
||||
```bash
|
||||
git push origin main --tags
|
||||
git push origin --tags
|
||||
```
|
||||
|
||||
### 7. Create GitHub release
|
||||
### 13. Create GitHub release
|
||||
|
||||
```bash
|
||||
gh release create v2.x.y --title "v2.x.y — summary" --notes "..."
|
||||
```
|
||||
|
||||
### 8. 🐳 Trigger Docker Hub build (MANDATORY — keep npm and Docker in sync)
|
||||
### 14. 🐳 Trigger Docker Hub build (MANDATORY — keep npm and Docker in sync)
|
||||
|
||||
> **CRITICAL**: Docker Hub and npm MUST always publish the same version.
|
||||
> The Docker image is built automatically via GitHub Actions when a new tag is pushed.
|
||||
> After pushing the tag in step 5-6, **verify the workflow runs**:
|
||||
> After pushing the tag in step 11-12, **verify the workflow runs**:
|
||||
|
||||
```bash
|
||||
# Verify the Docker workflow triggered
|
||||
@@ -129,7 +208,7 @@ If the Docker build was not triggered automatically, trigger it manually:
|
||||
gh workflow run docker-publish.yml --repo diegosouzapw/OmniRoute --ref v2.x.y
|
||||
```
|
||||
|
||||
### 9. Deploy to BOTH VPS environments (MANDATORY)
|
||||
### 15. Deploy to BOTH VPS environments (MANDATORY)
|
||||
|
||||
> Always deploy to **both** environments after every release.
|
||||
> See `/deploy-vps` workflow for detailed steps.
|
||||
@@ -151,18 +230,27 @@ curl -s -o /dev/null -w "LOCAL: HTTP %{http_code}\n" http://192.168.0.15:20128/
|
||||
curl -s -o /dev/null -w "AKAMAI: HTTP %{http_code}\n" http://69.164.221.35:20128/
|
||||
```
|
||||
|
||||
### 16. Clean up release branch
|
||||
|
||||
```bash
|
||||
git branch -d release/v2.x.y
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Notes
|
||||
|
||||
- Always run `/update-docs` BEFORE this workflow (ensures CHANGELOG and README are current)
|
||||
- The `prepublishOnly` script runs `npm run build:cli` automatically during `npm publish`
|
||||
- After npm publish, verify with `npm info omniroute version`
|
||||
- Lock file sync errors are caused by skipping `npm install` after version bump
|
||||
- Use `gh auth switch -u diegosouzapw` if git push fails with wrong account
|
||||
|
||||
## Known CI Pitfalls
|
||||
|
||||
| CI failure | Cause | Fix |
|
||||
| ------------------------------------------------------------------------- | -------------------------------------------------------- | ---------------------------------------------------------------------- |
|
||||
| `[docs-sync] FAIL - OpenAPI version differs from package.json` | Skipped step 4 — `docs/openapi.yaml` version not updated | Run step 4 (`sed -i ...`) and commit |
|
||||
| `[docs-sync] FAIL - OpenAPI version differs from package.json` | Skipped step 5 — `docs/openapi.yaml` version not updated | Run step 5 (`sed -i ...`) and commit |
|
||||
| `[docs-sync] FAIL - CHANGELOG.md first section must be "## [Unreleased]"` | `## [Unreleased]` missing or not at top of CHANGELOG | Add `## [Unreleased]\n\n---\n` before the first versioned `## [x.y.z]` |
|
||||
| Electron Linux `.deb` build fails (`FpmTarget` error) | `fpm` Ruby gem not installed on `ubuntu-latest` runner | Already fixed in `electron-release.yml` (`gem install fpm` step) |
|
||||
| Docker Hub `502 error writing layer blob` | Transient Docker Hub network error during ARM64 push | Re-run the Docker publish workflow; no code change needed |
|
||||
|
||||
+190
@@ -4,6 +4,196 @@
|
||||
|
||||
---
|
||||
|
||||
## [3.0.0-rc.2] - 2026-03-22
|
||||
|
||||
### 🔧 Bug Fixes
|
||||
|
||||
- **#536** — LongCat AI key validation: fixed baseUrl (`api.longcat.chat/openai`) and authHeader (`Authorization: Bearer`)
|
||||
- **#535** — Pinned model override: `body.model` is now set to `pinnedModel` when context-cache protection detects a pinned model
|
||||
- **#524** — OpenCode config now saved correctly: added `saveOpenCodeConfig()` handler (XDG_CONFIG_HOME aware, writes TOML)
|
||||
|
||||
---
|
||||
|
||||
## [3.0.0-rc.1] - 2026-03-22
|
||||
|
||||
### 🔧 Bug Fixes
|
||||
|
||||
- **#521** — Login no longer gets stuck after skipping password setup (redirects to onboarding)
|
||||
- **#522** — API Manager: Removed misleading "Copy masked key" button (replaced with lock icon tooltip)
|
||||
- **#527** — Claude Code + Codex superpowers loop: `tool_result` blocks now converted to text instead of dropped
|
||||
- **#532** — OpenCode GO API key validation now uses the correct `zen/v1` endpoint (`testKeyBaseUrl`)
|
||||
- **#489** — Antigravity: missing `googleProjectId` returns structured 422 error with reconnect guidance
|
||||
- **#510** — Windows: MSYS2/Git-Bash paths (`/c/Program Files/...`) are now normalized to `C:\\Program Files\\...`
|
||||
- **#492** — `omniroute` CLI now detects `mise`/`nvm` when `app/server.js` is missing and shows targeted fix
|
||||
|
||||
### 📖 Documentation
|
||||
|
||||
- **#513** — Docker password reset: `INITIAL_PASSWORD` env var workaround documented
|
||||
- **#520** — pnpm: `pnpm approve-builds better-sqlite3` documented
|
||||
|
||||
### ✅ Closed Issues
|
||||
|
||||
#489, #492, #510, #513, #520, #521, #522, #525, #527, #532
|
||||
|
||||
---
|
||||
|
||||
## [2.9.5] — 2026-03-22
|
||||
|
||||
> Sprint: New OpenCode providers, embedding credentials fix, CLI masked key bug, CACHE_TAG_PATTERN fix.
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
- **CLI tools save masked API key to config files** — `claude-settings`, `cline-settings`, and `openclaw-settings` POST routes now accept a `keyId` param and resolve the real API key from DB before writing to disk. `ClaudeToolCard` updated to send `keyId` instead of the masked display string. Fixes #523, #526.
|
||||
- **Custom embedding providers: `No credentials` error** — `/v1/embeddings` now tracks `credentialsProviderId` separately from the routing prefix, so credentials are fetched from the matching provider node ID rather than the public prefix string. Fixes a regression where `google/gemini-embedding-001` and similar custom-provider models would always fail with a credentials error. Fixes #532-related. (PR #528 by @jacob2826)
|
||||
- **Context cache protection regex misses `\n` prefix** — `CACHE_TAG_PATTERN` in `comboAgentMiddleware.ts` updated to match both literal `\n` (backslash-n) and actual newline U+000A that `combo.ts` streaming injects around the `<omniModel>` tag after fix #515. Fixes #531.
|
||||
|
||||
### ✨ New Providers
|
||||
|
||||
- **OpenCode Zen** — Free tier gateway at `opencode.ai/zen/v1` with 3 models: `minimax-m2.5-free`, `big-pickle`, `gpt-5-nano`
|
||||
- **OpenCode Go** — Subscription service at `opencode.ai/zen/go/v1` with 4 models: `glm-5`, `kimi-k2.5`, `minimax-m2.7` (Claude format), `minimax-m2.5` (Claude format)
|
||||
- Both providers use the new `OpencodeExecutor` which routes dynamically to `/chat/completions`, `/messages`, `/responses`, or `/models/{model}:generateContent` based on the requested model. (PR #530 by @kang-heewon)
|
||||
|
||||
---
|
||||
|
||||
## [2.9.4] — 2026-03-21
|
||||
|
||||
> Sprint: Bug fixes — preserve Codex prompt cache key, fix tagContent JSON escaping, sync expired token status to DB.
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
- **fix(translator)**: Preserve `prompt_cache_key` in Responses API → Chat Completions translation (#517)
|
||||
— The field is a cache-affinity signal used by Codex; stripping it was preventing prompt cache hits.
|
||||
Fixed in `openai-responses.ts` and `responsesApiHelper.ts`.
|
||||
|
||||
- **fix(combo)**: Escape `\n` in `tagContent` so injected JSON string is valid (#515)
|
||||
— Template literal newlines (U+000A) are not allowed unescaped inside JSON string values.
|
||||
Replaced with `\\n` literal sequences in `open-sse/services/combo.ts`.
|
||||
|
||||
- **fix(usage)**: Sync expired token status back to DB on live auth failure (#491)
|
||||
— When the Limits & Quotas live check returns 401/403, the connection `testStatus` is now updated
|
||||
to `"expired"` in the database so the Providers page reflects the same degraded state.
|
||||
Fixed in `src/app/api/usage/[connectionId]/route.ts`.
|
||||
|
||||
---
|
||||
|
||||
## [2.9.3] — 2026-03-21
|
||||
|
||||
> Sprint: Add 5 new free AI providers — LongCat, Pollinations, Cloudflare AI, Scaleway, AI/ML API.
|
||||
|
||||
### ✨ New Providers
|
||||
|
||||
- **feat(providers/longcat)**: Add LongCat AI (`lc/`) — 50M tokens/day free (Flash-Lite) + 500K/day (Chat/Thinking) during public beta. OpenAI-compatible, standard Bearer auth.
|
||||
- **feat(providers/pollinations)**: Add Pollinations AI (`pol/`) — no API key required. Proxies GPT-5, Claude, Gemini, DeepSeek V3, Llama 4 (1 req/15s free). Custom executor handles optional auth.
|
||||
- **feat(providers/cloudflare-ai)**: Add Cloudflare Workers AI (`cf/`) — 10K Neurons/day free (~150 LLM responses or 500s Whisper audio). 50+ models on global edge. Custom executor builds dynamic URL with `accountId` from credentials.
|
||||
- **feat(providers/scaleway)**: Add Scaleway Generative APIs (`scw/`) — 1M free tokens for new accounts. EU/GDPR compliant (Paris). Qwen3 235B, Llama 3.1 70B, Mistral Small 3.2.
|
||||
- **feat(providers/aimlapi)**: Add AI/ML API (`aiml/`) — $0.025/day free credit, 200+ models (GPT-4o, Claude, Gemini, Llama) via single aggregator endpoint.
|
||||
|
||||
### 🔄 Provider Updates
|
||||
|
||||
- **feat(providers/together)**: Add `hasFree: true` + 3 permanently free model IDs: `Llama-3.3-70B-Instruct-Turbo-Free`, `Llama-Vision-Free`, `DeepSeek-R1-Distill-Llama-70B-Free`
|
||||
- **feat(providers/gemini)**: Add `hasFree: true` + `freeNote` (1,500 req/day, no credit card needed, aistudio.google.com)
|
||||
- **chore(providers/gemini)**: Rename display name to `Gemini (Google AI Studio)` for clarity
|
||||
|
||||
### ⚙️ Infrastructure
|
||||
|
||||
- **feat(executors/pollinations)**: New `PollinationsExecutor` — omits `Authorization` header when no API key provided
|
||||
- **feat(executors/cloudflare-ai)**: New `CloudflareAIExecutor` — dynamic URL construction requires `accountId` in provider credentials
|
||||
- **feat(executors)**: Register `pollinations`, `pol`, `cloudflare-ai`, `cf` executor mappings
|
||||
|
||||
### 📝 Documentation
|
||||
|
||||
- **docs(readme)**: Expanded free combo stack to 11 providers ($0 forever)
|
||||
- **docs(readme)**: Added 4 new free provider sections (LongCat, Pollinations, Cloudflare AI, Scaleway) with model tables
|
||||
- **docs(readme)**: Updated pricing table with 4 new free tier rows
|
||||
- **docs(i18n/pt-BR)**: Updated pricing table + added LongCat/Pollinations/Cloudflare AI/Scaleway sections in Portuguese
|
||||
- **docs(new-features/ai)**: 10 task spec files + master implementation plan in `docs/new-features/ai/`
|
||||
|
||||
### 🧪 Tests
|
||||
|
||||
- Test suite: **821 tests, 0 failures** (unchanged)
|
||||
|
||||
---
|
||||
|
||||
## [2.9.2] — 2026-03-21
|
||||
|
||||
> Sprint: Fix media transcription (Deepgram/HuggingFace Content-Type, language detection) and TTS error display.
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
- **fix(transcription)**: Deepgram and HuggingFace audio transcription now correctly map `video/mp4` → `audio/mp4` and other media MIME types via new `resolveAudioContentType()` helper. Previously, uploading `.mp4` files consistently returned "No speech detected" because Deepgram was receiving `Content-Type: video/mp4`.
|
||||
- **fix(transcription)**: Added `detect_language=true` to Deepgram requests — auto-detects audio language (Portuguese, Spanish, etc.) instead of defaulting to English. Fixes non-English transcriptions returning empty or garbage results.
|
||||
- **fix(transcription)**: Added `punctuate=true` to Deepgram requests for higher-quality transcription output with correct punctuation.
|
||||
- **fix(tts)**: `[object Object]` error display in Text-to-Speech responses fixed in both `audioSpeech.ts` and `audioTranscription.ts`. The `upstreamErrorResponse()` function now correctly extracts nested string messages from providers like ElevenLabs that return `{ error: { message: "...", status_code: 401 } }` instead of a flat error string.
|
||||
|
||||
### 🧪 Tests
|
||||
|
||||
- Test suite: **821 tests, 0 failures** (unchanged)
|
||||
|
||||
### Triaged Issues
|
||||
|
||||
- **#508** — Tool call format regression: requested proxy logs and provider chain info (`needs-info`)
|
||||
- **#510** — Windows CLI healthcheck path: requested shell/Node version info (`needs-info`)
|
||||
- **#485** — Kiro MCP tool calls: closed as external Kiro issue (not OmniRoute)
|
||||
- **#442** — Baseten /models endpoint: closed (documented manual workaround)
|
||||
- **#464** — Key provisioning API: acknowledged as roadmap item
|
||||
|
||||
---
|
||||
|
||||
## [2.9.1] — 2026-03-21
|
||||
|
||||
> Sprint: Fix SSE omniModel data loss, merge per-protocol model compatibility.
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **#511** — Critical: `<omniModel>` tag was sent after `finish_reason:stop` in SSE streams, causing data loss. Tag is now injected into the first non-empty content chunk, guaranteeing delivery before SDKs close the connection.
|
||||
|
||||
### Merged PRs
|
||||
|
||||
- **PR #512** (@zhangqiang8vip): Per-protocol model compatibility — `normalizeToolCallId` and `preserveOpenAIDeveloperRole` can now be configured per client protocol (OpenAI, Claude, Responses API). New `compatByProtocol` field in model config with Zod validation.
|
||||
|
||||
### Triaged Issues
|
||||
|
||||
- **#510** — Windows CLI healthcheck_failed: requested PATH/version info
|
||||
- **#509** — Turbopack Electron regression: upstream Next.js bug, documented workarounds
|
||||
- **#508** — macOS black screen: suggested `--disable-gpu` workaround
|
||||
|
||||
---
|
||||
|
||||
## [2.9.0] — 2026-03-20
|
||||
|
||||
> Sprint: Cross-platform machineId fix, per-API-key rate limits, streaming context cache, Alibaba DashScope, search analytics, ZWS v5, and 8 issues closed.
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
- **feat(search)**: Search Analytics tab in `/dashboard/analytics` — provider breakdown, cache hit rate, cost tracking. New API: `GET /api/v1/search/analytics` (#feat/search-provider-routing)
|
||||
- **feat(provider)**: Alibaba Cloud DashScope added with custom endpoint path validation — configurable `chatPath` and `modelsPath` per node (#feat/custom-endpoint-paths)
|
||||
- **feat(api)**: Per-API-key request-count limits — `max_requests_per_day` and `max_requests_per_minute` columns with in-memory sliding-window enforcement returning HTTP 429 (#452)
|
||||
- **feat(dev)**: ZWS v5 — HMR leak fix (485 DB connections → 1), memory 2.4GB → 195MB, `globalThis` singletons, Edge Runtime warning fix (@zhangqiang8vip)
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
- **fix(#506)**: Cross-platform `machineId` — `getMachineIdRaw()` rewritten with try/catch waterfall (Windows REG.exe → macOS ioreg → Linux file read → hostname → `os.hostname()`). Eliminates `process.platform` branching that Next.js bundler dead-code-eliminated, fixing `'head' is not recognized` on Windows. Also fixes #466.
|
||||
- **fix(#493)**: Custom provider model naming — removed incorrect prefix stripping in `DefaultExecutor.transformRequest()` that mangled org-scoped model IDs like `zai-org/GLM-5-FP8`.
|
||||
- **fix(#490)**: Streaming + context cache protection — `TransformStream` intercepts SSE to inject `<omniModel>` tag before `[DONE]` marker, enabling context cache protection for streaming responses.
|
||||
- **fix(#458)**: Combo schema validation — `system_message`, `tool_filter_regex`, `context_cache_protection` fields now pass Zod validation on save.
|
||||
- **fix(#487)**: KIRO MITM card cleanup — removed ZWS_README, generified `AntigravityToolCard` to use dynamic tool metadata.
|
||||
|
||||
### 🧪 Tests
|
||||
|
||||
- Added Anthropic-format tools filter unit tests (PR #397) — 8 regression tests for `tool.name` without `.function` wrapper
|
||||
- Test suite: **821 tests, 0 failures** (up from 813)
|
||||
|
||||
### 📋 Issues Closed (8)
|
||||
|
||||
- **#506** — Windows machineId `head` not recognized (fixed)
|
||||
- **#493** — Custom provider model naming (fixed)
|
||||
- **#490** — Streaming context cache (fixed)
|
||||
- **#452** — Per-API-key request limits (implemented)
|
||||
- **#466** — Windows login failure (same root cause as #506)
|
||||
- **#504** — MITM inactive (expected behavior)
|
||||
- **#462** — Gemini CLI PSA (resolved)
|
||||
- **#434** — Electron app crash (duplicate of #402)
|
||||
|
||||
## [2.8.9] — 2026-03-20
|
||||
|
||||
> Sprint: Merge community PRs, fix KIRO MITM card, dependency updates.
|
||||
|
||||
@@ -11,7 +11,9 @@ _Your universal API proxy — one endpoint, 44+ providers, zero downtime. Now wi
|
||||
<div align="center">
|
||||
|
||||
[](https://www.npmjs.com/package/omniroute)
|
||||
[](https://www.npmjs.com/package/omniroute)
|
||||
[](https://hub.docker.com/r/diegosouzapw/omniroute)
|
||||
[](https://hub.docker.com/r/diegosouzapw/omniroute)
|
||||
[](https://github.com/diegosouzapw/OmniRoute/blob/main/LICENSE)
|
||||
[](https://omniroute.online)
|
||||
[](https://chat.whatsapp.com/JI7cDQ1GyaiDHhVBpLxf8b?mode=gi_t)
|
||||
@@ -716,7 +718,7 @@ Outcome: deep fallback depth for deadline-critical workloads
|
||||
|
||||
**Point any IDE/CLI to:** `http://localhost:20128/v1` · API Key: `any-string` · Done.
|
||||
|
||||
> **Optional extra coverage (also free):** Groq API key (30 RPM free), NVIDIA NIM (40 RPM free, 70+ models), Cerebras (1M tok/day).
|
||||
> **Optional extra coverage (also free):** Groq API key (30 RPM free), NVIDIA NIM (40 RPM free, 70+ models), Cerebras (1M tok/day), LongCat API key (50M tokens/day!), Cloudflare Workers AI (10K Neurons/day, 50+ models).
|
||||
|
||||
## ⚡ Quick Start
|
||||
|
||||
@@ -921,18 +923,28 @@ When minimized, OmniRoute lives in your system tray with quick actions:
|
||||
| **🆓 FREE** | iFlow | **$0** | Unlimited | 5 models unlimited |
|
||||
| | Qwen | **$0** | Unlimited | 4 models unlimited |
|
||||
| | Kiro | **$0** | Unlimited | Claude Sonnet/Haiku (AWS Builder) |
|
||||
| | LongCat Flash-Lite 🆕 | **$0** (50M tok/day 🔥) | 1 RPS | Largest free quota on Earth |
|
||||
| | Pollinations AI 🆕 | **$0** (no key needed) | 1 req/15s | GPT-5, Claude, DeepSeek, Llama 4 |
|
||||
| | Cloudflare Workers AI 🆕 | **$0** (10K Neurons/day) | ~150 resp/day | 50+ models, global edge |
|
||||
| | Scaleway AI 🆕 | **$0** (1M tokens total) | Rate limited | EU/GDPR, Qwen3 235B, Llama 70B |
|
||||
|
||||
> 🆕 **New models added (Mar 2026):** Grok-4 Fast family at $0.20/$0.50/M (benchmarked at 1143ms — 30% faster than Gemini 2.5 Flash), GLM-5 via Z.AI with 128K output, MiniMax M2.5 reasoning, DeepSeek V3.2 updated pricing, Kimi K2.5 via Moonshot direct API.
|
||||
|
||||
**💡 $0 Combo Stack — The Complete Free Setup:**
|
||||
|
||||
```
|
||||
Gemini CLI (180K/mo free)
|
||||
→ iFlow (unlimited: kimi-k2-thinking, qwen3-coder-plus, deepseek-r1)
|
||||
→ Kiro (Claude Sonnet 4.5 + Haiku — unlimited, via AWS Builder ID)
|
||||
→ Qwen (4 models — unlimited)
|
||||
→ Groq (14.4K req/day — ultra-fast)
|
||||
→ NVIDIA NIM (70+ models — 40 RPM forever)
|
||||
# 🆓 Ultimate Free Stack 2026 — 11 Providers, $0 Forever
|
||||
Kiro (kr/) → Claude Sonnet/Haiku UNLIMITED
|
||||
iFlow (if/) → kimi-k2-thinking, qwen3-coder-plus, deepseek-r1 UNLIMITED
|
||||
LongCat Lite (lc/) → LongCat-Flash-Lite — 50M tokens/day 🔥
|
||||
Pollinations (pol/) → GPT-5, Claude, DeepSeek, Llama 4 — no key needed
|
||||
Qwen (qw/) → qwen3-coder-plus, qwen3-coder-flash, qwen3-coder-next UNLIMITED
|
||||
Gemini (gemini/) → Gemini 2.5 Flash — 1,500 req/day free API key
|
||||
Cloudflare AI (cf/) → Llama 70B, Gemma 3, Mistral — 10K Neurons/day
|
||||
Scaleway (scw/) → Qwen3 235B, Llama 70B — 1M free tokens (EU)
|
||||
Groq (groq/) → Llama/Gemma ultra-fast — 14.4K req/day
|
||||
NVIDIA NIM (nvidia/) → 70+ open models — 40 RPM forever
|
||||
Cerebras (cerebras/) → Llama/Qwen world-fastest — 1M tok/day
|
||||
```
|
||||
|
||||
**Zero cost. Never stops coding.** Configure this as one OmniRoute combo and all fallbacks happen automatically — no manual switching ever.
|
||||
@@ -1003,19 +1015,66 @@ Available free: `llama-3.3-70b`, `llama-3.1-8b`, `deepseek-r1-distill-llama-70b`
|
||||
|
||||
Available free: `llama-3.3-70b-versatile`, `gemma2-9b-it`, `mixtral-8x7b`, `whisper-large-v3`
|
||||
|
||||
> **💡 The Ultimate Free Stack:**
|
||||
### 🔴 LONGCAT AI (Free API Key — longcat.chat) 🆕
|
||||
|
||||
| Model | Prefix | Daily Free Quota | Notes |
|
||||
| ----------------------------- | ------ | ----------------- | ----------------------- |
|
||||
| `LongCat-Flash-Lite` | `lc/` | **50M tokens** 💥 | Largest free quota ever |
|
||||
| `LongCat-Flash-Chat` | `lc/` | 500K tokens | Multi-turn chat |
|
||||
| `LongCat-Flash-Thinking` | `lc/` | 500K tokens | Reasoning / CoT |
|
||||
| `LongCat-Flash-Thinking-2601` | `lc/` | 500K tokens | Jan 2026 version |
|
||||
| `LongCat-Flash-Omni-2603` | `lc/` | 500K tokens | Multimodal |
|
||||
|
||||
> 100% free while in public beta. Sign up at [longcat.chat](https://longcat.chat) with email or phone. Resets daily 00:00 UTC.
|
||||
|
||||
### 🟢 POLLINATIONS AI (No API Key Required) 🆕
|
||||
|
||||
| Model | Prefix | Rate Limit | Provider Behind |
|
||||
| ---------- | ------ | ---------- | ------------------ |
|
||||
| `openai` | `pol/` | 1 req/15s | GPT-5 |
|
||||
| `claude` | `pol/` | 1 req/15s | Anthropic Claude |
|
||||
| `gemini` | `pol/` | 1 req/15s | Google Gemini |
|
||||
| `deepseek` | `pol/` | 1 req/15s | DeepSeek V3 |
|
||||
| `llama` | `pol/` | 1 req/15s | Meta Llama 4 Scout |
|
||||
| `mistral` | `pol/` | 1 req/15s | Mistral AI |
|
||||
|
||||
> ✨ **Zero friction:** No signup, no API key. Add the Pollinations provider with an empty key field and it works immediately.
|
||||
|
||||
### 🟠 CLOUDFLARE WORKERS AI (Free API Key — cloudflare.com) 🆕
|
||||
|
||||
| Tier | Daily Neurons | Equivalent Usage | Notes |
|
||||
| ---- | ------------- | --------------------------------------- | ----------------------- |
|
||||
| Free | **10,000** | ~150 LLM resp / 500s audio / 15K embeds | Global edge, 50+ models |
|
||||
|
||||
Popular free models: `@cf/meta/llama-3.3-70b-instruct`, `@cf/google/gemma-3-12b-it`, `@cf/openai/whisper-large-v3-turbo` (free audio!), `@cf/qwen/qwen2.5-coder-15b-instruct`
|
||||
|
||||
> Requires API Token + Account ID from [dash.cloudflare.com](https://dash.cloudflare.com). Store Account ID in provider settings.
|
||||
|
||||
### 🟣 SCALEWAY AI (1M Free Tokens — scaleway.com) 🆕
|
||||
|
||||
| Tier | Free Quota | Location | Notes |
|
||||
| ---- | ------------- | ------------ | ----------------------------------- |
|
||||
| Free | **1M tokens** | 🇫🇷 Paris, EU | No credit card needed within limits |
|
||||
|
||||
Available free: `qwen3-235b-a22b-instruct-2507` (Qwen3 235B!), `llama-3.1-70b-instruct`, `mistral-small-3.2-24b-instruct-2506`, `deepseek-v3-0324`
|
||||
|
||||
> EU/GDPR compliant. Get API key at [console.scaleway.com](https://console.scaleway.com).
|
||||
|
||||
> **💡 The Ultimate Free Stack (11 Providers, $0 Forever):**
|
||||
>
|
||||
> ```
|
||||
> Kiro (Claude, unlimited)
|
||||
> → iFlow (5 models, unlimited)
|
||||
> → Qwen (4 models, unlimited)
|
||||
> → Gemini CLI (180K/mo)
|
||||
> → Cerebras (1M tok/day)
|
||||
> → Groq (14.4K req/day)
|
||||
> → NVIDIA NIM (40 RPM, 70+ models)
|
||||
> Kiro (kr/) → Claude Sonnet/Haiku UNLIMITED
|
||||
> iFlow (if/) → kimi-k2-thinking, qwen3-coder-plus, deepseek-r1 UNLIMITED
|
||||
> LongCat Lite (lc/) → LongCat-Flash-Lite — 50M tokens/day 🔥
|
||||
> Pollinations (pol/) → GPT-5, Claude, DeepSeek, Llama 4 — no key needed
|
||||
> Qwen (qw/) → qwen3-coder models UNLIMITED
|
||||
> Gemini (gemini/) → Gemini 2.5 Flash — 1,500 req/day free
|
||||
> Cloudflare AI (cf/) → 50+ models — 10K Neurons/day
|
||||
> Scaleway (scw/) → Qwen3 235B, Llama 70B — 1M free tokens (EU)
|
||||
> Groq (groq/) → Llama/Gemma — 14.4K req/day ultra-fast
|
||||
> NVIDIA NIM (nvidia/) → 70+ open models — 40 RPM forever
|
||||
> Cerebras (cerebras/) → Llama/Qwen world-fastest — 1M tok/day
|
||||
> ```
|
||||
>
|
||||
> Configure this as an OmniRoute combo and you'll never pay for AI again.
|
||||
|
||||
## 🎙️ Free Transcription Combo
|
||||
|
||||
@@ -1105,17 +1164,17 @@ OmniRoute v2.0 is built as an operational platform, not just a relay proxy.
|
||||
|
||||
### 🎵 Multi-Modal APIs
|
||||
|
||||
| Feature | What It Does |
|
||||
| -------------------------- | ------------------------------------------------------------------------------------------------------------ |
|
||||
| 🖼️ **Image Generation** | `/v1/images/generations` with cloud and local backends |
|
||||
| 📐 **Embeddings** | `/v1/embeddings` for search and RAG pipelines |
|
||||
| 🎤 **Audio Transcription** | `/v1/audio/transcriptions` (Whisper and additional providers) |
|
||||
| 🔊 **Text-to-Speech** | `/v1/audio/speech` (multiple engines/providers) |
|
||||
| 🎬 **Video Generation** | `/v1/videos/generations` (ComfyUI + SD WebUI workflows) |
|
||||
| 🎵 **Music Generation** | `/v1/music/generations` (ComfyUI workflows) |
|
||||
| 🛡️ **Moderations** | `/v1/moderations` safety checks |
|
||||
| 🔀 **Reranking** | `/v1/rerank` for relevance scoring |
|
||||
| 🔍 **Web Search** 🆕 | `/v1/search` — 5 providers (Serper, Brave, Perplexity, Exa, Tavily), 6,500+ free/month, auto-failover, cache |
|
||||
| Feature | What It Does |
|
||||
| -------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| 🖼️ **Image Generation** | `/v1/images/generations` with cloud and local backends |
|
||||
| 📐 **Embeddings** | `/v1/embeddings` for search and RAG pipelines |
|
||||
| 🎤 **Audio Transcription** | `/v1/audio/transcriptions` — 7 providers (Deepgram Nova 3, AssemblyAI, Groq Whisper, HuggingFace, ElevenLabs, OpenAI, Azure), auto-language detection, MP4/MP3/WAV support |
|
||||
| 🔊 **Text-to-Speech** | `/v1/audio/speech` — 10 providers (ElevenLabs, OpenAI, Deepgram, Cartesia, PlayHT, HuggingFace, Nvidia NIM, Inworld, Coqui, Tortoise) with correct error messages |
|
||||
| 🎬 **Video Generation** | `/v1/videos/generations` (ComfyUI + SD WebUI workflows) |
|
||||
| 🎵 **Music Generation** | `/v1/music/generations` (ComfyUI workflows) |
|
||||
| 🛡️ **Moderations** | `/v1/moderations` safety checks |
|
||||
| 🔀 **Reranking** | `/v1/rerank` for relevance scoring |
|
||||
| 🔍 **Web Search** 🆕 | `/v1/search` — 5 providers (Serper, Brave, Perplexity, Exa, Tavily), 6,500+ free/month, auto-failover, cache |
|
||||
|
||||
### 🛡️ Resilience, Security & Governance
|
||||
|
||||
|
||||
@@ -0,0 +1,332 @@
|
||||
# ZWS_README_V5 — 按协议配置模型兼容性 + 前端性能优化
|
||||
|
||||
V4 内容(HMR 泄漏修复、Edge 警告消除、测试稳定性)已完成;V5 在 V4 基础上实现**按协议维度配置模型兼容性**,新增前端查找性能优化与类型安全改进。
|
||||
|
||||
---
|
||||
|
||||
## 一、如何发现问题
|
||||
|
||||
### 现象
|
||||
|
||||
- 同一模型被 **OpenAI Chat Completions**、**OpenAI Responses API**、**Anthropic Messages** 三种客户端请求形态调用时,V2 的兼容性开关(工具 ID 9 位、不保留 developer 角色)是**全局生效**的——无法为不同协议设置不同的兼容策略。
|
||||
- 例如:用户希望 OpenAI Responses API 请求时不保留 developer 角色(MiniMax 422 修复),但 OpenAI Chat Completions 请求时保留。V2 下只能二选一。
|
||||
- 前端兼容性弹层未标明当前配置对应哪种协议,容易误导。
|
||||
- 前端组件中 `Array.find()` 在每次渲染时对 customModels 和 modelCompatOverrides 做 O(n) 线性扫描,模型数量多时存在不必要的性能开销。
|
||||
- `ModelCompatPatch` 类型定义与运行时逻辑不一致:`preserveOpenAIDeveloperRole` 字段需要支持 `null`(表示取消设置/恢复默认),但类型仅允许 `boolean`。
|
||||
|
||||
### 排查过程
|
||||
|
||||
1. **需求分析**:梳理 `detectFormat(body)` 返回的三种协议键(`openai`、`openai-responses`、`claude`),确认每种协议对 developer 角色和 tool call ID 的需求不同。
|
||||
2. **数据模型设计**:在现有 `normalizeToolCallId` / `preserveOpenAIDeveloperRole` 顶层字段基础上,设计 `compatByProtocol` 嵌套结构,按协议键细分。
|
||||
3. **构建问题**:客户端 `"use client"` 组件直接从 `@/lib/localDb` 引入常量时,间接拉入了 `node:crypto`(经由 `db/proxies.ts`),触发 Webpack `UnhandledSchemeError`。需将常量拆到 `shared/` 层。
|
||||
4. **前端性能**:通过 React DevTools 和代码审计发现 `effectiveNormalizeForProtocol` 等函数每次调用都对数组做 `find()`,在渲染列表时存在 O(n²) 的隐患。
|
||||
|
||||
---
|
||||
|
||||
## 二、根因分析
|
||||
|
||||
### 根因 1(P0):兼容选项无协议维度
|
||||
|
||||
V2 的 `normalizeToolCallId` / `preserveOpenAIDeveloperRole` 存储在模型级别的顶层字段,无法区分请求来源协议。`chatCore.ts` 中的 getter 函数只接收 `(providerId, modelId)` 两个参数,不感知当前请求的 `sourceFormat`。
|
||||
|
||||
**影响**:跨协议场景下用户只能设置一个全局值,无法精确控制。
|
||||
|
||||
### 根因 2(P1):客户端构建拉入 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 模块的路径。
|
||||
|
||||
### 根因 3(P2):前端查找性能
|
||||
|
||||
`effectiveNormalizeForProtocol`、`effectivePreserveForProtocol`、`anyNormalizeCompatBadge`、`anyNoPreserveCompatBadge` 四个函数每次调用都使用 `Array.find()` 在 `customModels` 和 `modelCompatOverrides` 数组中查找目标模型。在模型列表渲染时,每个模型行会调用多次这些函数,导致 O(n × m) 的查找开销(n = 模型数,m = 每行调用次数)。
|
||||
|
||||
### 根因 4(P2):类型定义与运行时不一致
|
||||
|
||||
```typescript
|
||||
// V3 暂存区版本(有问题)
|
||||
export type ModelCompatPatch = Partial<
|
||||
Pick<
|
||||
ModelCompatOverride,
|
||||
"normalizeToolCallId" | "preserveOpenAIDeveloperRole" | "compatByProtocol"
|
||||
>
|
||||
>;
|
||||
```
|
||||
|
||||
`ModelCompatOverride.preserveOpenAIDeveloperRole` 类型为 `boolean | undefined`,但 `mergeModelCompatOverride()` 内部有 `=== null` 判断(用于取消设置/恢复默认),类型层面无法覆盖。
|
||||
|
||||
---
|
||||
|
||||
## 三、修复方案
|
||||
|
||||
### 修复 1:`compatByProtocol` 存储与读取(models.ts)
|
||||
|
||||
**新增数据结构**:
|
||||
|
||||
```typescript
|
||||
type CompatByProtocolMap = Partial<Record<ModelCompatProtocolKey, ModelCompatPerProtocol>>;
|
||||
|
||||
export type ModelCompatOverride = {
|
||||
id: string;
|
||||
normalizeToolCallId?: boolean;
|
||||
preserveOpenAIDeveloperRole?: boolean;
|
||||
compatByProtocol?: CompatByProtocolMap; // 新增
|
||||
};
|
||||
```
|
||||
|
||||
**读取优先级链**(适用于 `getModelNormalizeToolCallId` 和 `getModelPreserveOpenAIDeveloperRole`):
|
||||
|
||||
```
|
||||
compatByProtocol[sourceFormat].field → 顶层 field → 默认值
|
||||
```
|
||||
|
||||
1. 若 `sourceFormat` 属于已知协议键(`openai` / `openai-responses` / `claude`),且 `compatByProtocol[sourceFormat]` 中存在目标字段,使用该值。
|
||||
2. 否则回退到顶层字段。
|
||||
3. 顶层字段也不存在时使用默认值(normalizeToolCallId=false,preserveOpenAIDeveloperRole=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:请求管线传入 sourceFormat(chatCore.ts)
|
||||
|
||||
```typescript
|
||||
const normalizeToolCallId = getModelNormalizeToolCallId(
|
||||
provider || "",
|
||||
model || "",
|
||||
sourceFormat // 新增第三参
|
||||
);
|
||||
const preserveDeveloperRole = getModelPreserveOpenAIDeveloperRole(
|
||||
provider || "",
|
||||
model || "",
|
||||
sourceFormat // 新增第三参
|
||||
);
|
||||
```
|
||||
|
||||
`sourceFormat` 由已有的 `detectFormat(body)` 返回,无需新增检测逻辑。
|
||||
|
||||
**优点**:
|
||||
|
||||
- 改动仅 2 行,精准传参。
|
||||
- 不影响其他 handler(embeddings、imageGeneration 等不涉及 developer 角色和 tool call ID)。
|
||||
|
||||
### 修复 3:API 路由支持 compatByProtocol(route.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,不依赖父组件。
|
||||
|
||||
### 修复 7:ModelCompatPatch 类型修正(models.ts)
|
||||
|
||||
```typescript
|
||||
// 修复后 — 显式允许 null
|
||||
export type ModelCompatPatch = {
|
||||
normalizeToolCallId?: boolean;
|
||||
preserveOpenAIDeveloperRole?: boolean | null; // null = 取消设置/恢复默认
|
||||
compatByProtocol?: CompatByProtocolMap;
|
||||
};
|
||||
```
|
||||
|
||||
与 `mergeModelCompatOverride()` 内的 `=== null` 判断逻辑一致,类型安全。
|
||||
|
||||
### 修复 8:CompatByProtocolMap 类型收紧(page.tsx)
|
||||
|
||||
客户端 `CompatByProtocolMap` 从 `Record<string, ...>` 改为 `Record<ModelCompatProtocolKey, ...>`,增强类型安全,防止传入未知协议键。
|
||||
|
||||
### 修复 9:i18n 文案新增
|
||||
|
||||
| 键名 | 中文 | 英文 |
|
||||
| ------------------------------- | --------------------------------------------- | -------------------------------------------------------------- |
|
||||
| `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.get(useMemo 缓存) |
|
||||
| ModelCompatPatch 类型安全 | null 值无类型覆盖 | 显式 `boolean \| null` |
|
||||
| 客户端构建风险 | 可能引入 Node.js 模块 | 已隔离(shared/constants 层) |
|
||||
| API 验证 | 无 compatByProtocol | Zod strict schema 校验 |
|
||||
| 深色模式 | 协议选择器不可读 | bg/text 适配 dark 主题 |
|
||||
|
||||
---
|
||||
|
||||
## 六、涉及文件清单
|
||||
|
||||
| 区域 | 文件 | 改动类型 |
|
||||
| ---------- | ----------------------------------------------------------- | ------------------------------------------------------------------------------------------------ |
|
||||
| 协议常量 | `src/shared/constants/modelCompat.ts` | **新建**,客户端安全的协议键与类型 |
|
||||
| 存储与读写 | `src/lib/db/models.ts` | `compatByProtocol` 数据结构、深度合并、getter 第三参 `sourceFormat`、`ModelCompatPatch` 类型修正 |
|
||||
| 再导出层 | `src/lib/localDb.ts` | 新增 `ModelCompatPatch` 类型导出 |
|
||||
| API 路由 | `src/app/api/provider-models/route.ts` | PUT 支持 `compatByProtocol`,使用 `ModelCompatPatch` 类型 |
|
||||
| 输入校验 | `src/shared/validation/schemas.ts` | `modelCompatPerProtocolSchema`(strict)+ `compatByProtocol` 记录校验 |
|
||||
| 请求管线 | `open-sse/handlers/chatCore.ts` | `getModelNormalizeToolCallId` / `getModelPreserveOpenAIDeveloperRole` 传入 `sourceFormat` |
|
||||
| 前端 UI | `src/app/(dashboard)/dashboard/providers/[id]/page.tsx` | 协议选择器、按协议解析/保存、角标逻辑、Map 性能优化、类型收紧 |
|
||||
| i18n | `src/i18n/messages/en.json`,`src/i18n/messages/zh-CN.json` | 5 条新文案 |
|
||||
|
||||
---
|
||||
|
||||
## 七、回退方案
|
||||
|
||||
- **禁用按协议配置**:删除 `compatByProtocol` 字段后,getter 自动回退到顶层字段,行为与 V2 一致。
|
||||
- **前端 Map 优化回退**:将 `Map.get()` 改回 `Array.find()` 即可,纯性能优化无功能耦合。
|
||||
- **客户端常量回退**:将 `MODEL_COMPAT_PROTOCOL_KEYS` 定义移回 `models.ts` 并从 `localDb.ts` 导出(需同时确保 `node:crypto` 问题不再存在)。
|
||||
- **生产环境**:以上修复对生产构建无负面影响。`compatByProtocol` 为可选字段,未配置时默认行为不变。API Zod 校验确保不会接受畸形数据。
|
||||
+21
-2
@@ -189,8 +189,27 @@ const serverJs = join(APP_DIR, "server.js");
|
||||
|
||||
if (!existsSync(serverJs)) {
|
||||
console.error("\x1b[31m✖ Server not found at:\x1b[0m", serverJs);
|
||||
console.error(" This usually means the package was not built correctly.");
|
||||
console.error(" Try reinstalling: npm install -g omniroute");
|
||||
console.error(" The package may not have been built correctly.");
|
||||
console.error("");
|
||||
// (#492) Detect common non-standard Node managers that cause this issue
|
||||
const nodeExec = process.execPath || "";
|
||||
const isMise = nodeExec.includes("mise") || nodeExec.includes(".local/share/mise");
|
||||
const isNvm = nodeExec.includes(".nvm") || nodeExec.includes("nvm");
|
||||
if (isMise) {
|
||||
console.error(
|
||||
" \x1b[33m⚠ mise detected:\x1b[0m If you installed via `npm install -g omniroute`,"
|
||||
);
|
||||
console.error(" try: \x1b[36mnpx omniroute@latest\x1b[0m (downloads a fresh copy)");
|
||||
console.error(" or: \x1b[36mmise exec -- npx omniroute\x1b[0m");
|
||||
} else if (isNvm) {
|
||||
console.error(
|
||||
" \x1b[33m⚠ nvm detected:\x1b[0m Try reinstalling after loading the correct Node version:"
|
||||
);
|
||||
console.error(" \x1b[36mnvm use --lts && npm install -g omniroute\x1b[0m");
|
||||
} else {
|
||||
console.error(" Try: \x1b[36mnpm install -g omniroute\x1b[0m (reinstall)");
|
||||
console.error(" Or: \x1b[36mnpx omniroute@latest\x1b[0m");
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
|
||||
@@ -932,8 +932,8 @@ npm run electron:build:linux # Linux (.AppImage)
|
||||
| ميزة | ماذا يفعل || -------------------------- | ------------------------------------------------------------- |
|
||||
| 🖼️ **إنشاء الصور** | `/v1/images/generations` مع الواجهات الخلفية السحابية والمحلية |
|
||||
| 📐 **المضامين** | `/v1/embeddings` للبحث وخطوط أنابيب RAG |
|
||||
| 🎤 **نسخ صوتي** | `/v1/audio/transcriptions` (مقدمو خدمات الهمس والإضافيون) |
|
||||
| 🔊 **تحويل النص إلى كلام** | `/v1/audio/speech` (محركات/موفرو متعددون) |
|
||||
| 🎤 **نسخ صوتي** | `/v1/audio/transcriptions` — 7 providers (Deepgram Nova 3, AssemblyAI, Groq Whisper, HuggingFace, ElevenLabs, OpenAI, Azure), auto-language detection, MP4/MP3/WAV support |
|
||||
| 🔊 **تحويل النص إلى كلام** | `/v1/audio/speech` — 10 providers (ElevenLabs, OpenAI, Deepgram, Cartesia, PlayHT, HuggingFace, Nvidia NIM, Inworld, Coqui, Tortoise) |
|
||||
| 🎬 **توليد الفيديو** | `/v1/videos/generations` (سير عمل ComfyUI + SD WebUI) |
|
||||
| 🎵 **جيل الموسيقى** | `/v1/music/generations` (سير عمل ComfyUI) |
|
||||
| 🛡️ **اعتدالات** | فحوصات السلامة `/v1/moderations` |
|
||||
|
||||
@@ -933,8 +933,8 @@ OmniRoute v2.0 е създаден като операционна платфо
|
||||
| Характеристика | Какво прави || -------------------------- | ------------------------------------------------------------ |
|
||||
| 🖼️ **Генериране на изображения** | `/v1/images/generations` с облак и локален бекенд |
|
||||
| 📐 **Вграждания** | `/v1/embeddings` за търсене и RAG тръбопроводи |
|
||||
| 🎤 **Аудио транскрипция** | `/v1/audio/transcriptions` (Whisper и допълнителни доставчици) |
|
||||
| 🔊 **Текст към говор** | `/v1/audio/speech` (множество машини/доставчици) |
|
||||
| 🎤 **Аудио транскрипция** | `/v1/audio/transcriptions` — 7 providers (Deepgram Nova 3, AssemblyAI, Groq Whisper, HuggingFace, ElevenLabs, OpenAI, Azure), auto-language detection, MP4/MP3/WAV support |
|
||||
| 🔊 **Текст към говор** | `/v1/audio/speech` — 10 providers (ElevenLabs, OpenAI, Deepgram, Cartesia, PlayHT, HuggingFace, Nvidia NIM, Inworld, Coqui, Tortoise) |
|
||||
| 🎬 **Видео генериране** | `/v1/videos/generations` (работни процеси ComfyUI + SD WebUI) |
|
||||
| 🎵 **Музикално поколение** | `/v1/music/generations` (работни процеси на ComfyUI) |
|
||||
| 🛡️ **Модерации** | `/v1/moderations` проверки за безопасност |
|
||||
|
||||
+225
-349
@@ -2,7 +2,7 @@
|
||||
|
||||
### Nikdy nepřestávejte s kódováním. Chytré směrování k **BEZPLATNÝM a levným modelům AI** s automatickým přepínáním mezi záložními systémy.
|
||||
|
||||
*Váš univerzální API proxy – jeden endpoint, více než 44 poskytovatelů, nulové výpadky. Nyní s orchestrací agentů **MCP a A2A** .*
|
||||
_Váš univerzální API proxy – jeden endpoint, více než 44 poskytovatelů, nulové výpadky. Nyní s orchestrací agentů **MCP a A2A** ._
|
||||
|
||||
**Dokončení chatu • Vkládání • Generování obrázků • Video • Hudba • Audio • Změna pořadí • **Vyhledávání na webu** • MCP server • A2A protokol • 100% TypeScript**
|
||||
|
||||
@@ -30,26 +30,23 @@
|
||||
<summary><b>Kliknutím zobrazíte snímky obrazovky z řídicího panelu</b></summary>
|
||||
</details>
|
||||
|
||||
Strana | Snímek obrazovky
|
||||
--- | ---
|
||||
**Poskytovatelé** | 
|
||||
**Kombinace** | 
|
||||
**Analytika** | 
|
||||
**Zdraví** | 
|
||||
**Překladatel** | 
|
||||
**Nastavení** | 
|
||||
**Nástroje CLI** | 
|
||||
**Protokoly používání** | 
|
||||
**Koncové body** | 
|
||||
|
||||
|
||||
|
||||
| Strana | Snímek obrazovky |
|
||||
| ----------------------- | --------------------------------------------------- |
|
||||
| **Poskytovatelé** |  |
|
||||
| **Kombinace** |  |
|
||||
| **Analytika** |  |
|
||||
| **Zdraví** |  |
|
||||
| **Překladatel** |  |
|
||||
| **Nastavení** |  |
|
||||
| **Nástroje CLI** |  |
|
||||
| **Protokoly používání** |  |
|
||||
| **Koncové body** |  |
|
||||
|
||||
---
|
||||
|
||||
### 🤖 Bezplatný poskytovatel umělé inteligence pro vaše oblíbené programátory
|
||||
|
||||
*Připojte libovolný nástroj IDE nebo CLI s umělou inteligencí přes OmniRoute — bezplatnou API bránu pro neomezené kódování.*
|
||||
_Připojte libovolný nástroj IDE nebo CLI s umělou inteligencí přes OmniRoute — bezplatnou API bránu pro neomezené kódování._
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
@@ -68,7 +65,6 @@ Strana | Snímek obrazovky
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
|
||||
<sub>📡 Všichni agenti se připojují přes <code>http://localhost:20128/v1</code> nebo <code>http://cloud.omniroute.online/v1</code> — jedna konfigurace, neomezené modely a kvóty</sub>
|
||||
|
||||
---
|
||||
@@ -161,9 +157,6 @@ Vývojáři platí za Claude Pro, Codex Pro nebo GitHub Copilot 20–200 dolarů
|
||||
- **Vlastní kombinace** — Přizpůsobitelné záložní řetězce se 6 strategiemi vyvažování (fill-first, round robin, P2C, náhodné, nejméně používané, nákladově optimalizované)
|
||||
- **Codex Business Quotas** — Sledování kvót pracovního prostoru firmy/týmu přímo v dashboardu
|
||||
|
||||
|
||||
|
||||
|
||||
<details>
|
||||
<summary><b>🔌 2. „Potřebuji použít více poskytovatelů, ale každý má jiné API“</b></summary>
|
||||
</details>
|
||||
@@ -180,9 +173,6 @@ OpenAI používá jeden formát, Claude (Anthropic) jiný a Gemini ještě třet
|
||||
- **Strukturovaný výstup pro Gemini** — `json_schema` → automatická konverze `responseMimeType` / `responseSchema`
|
||||
- **Výchozí hodnota `stream` je `false`** – Odpovídá specifikaci OpenAI, čímž se zabrání neočekávanému SSE v Python/Rust/Go SDK.
|
||||
|
||||
|
||||
|
||||
|
||||
<details>
|
||||
<summary><b>🌐 3. „Můj poskytovatel AI blokuje můj region/zemi“</b></summary>
|
||||
</details>
|
||||
@@ -199,9 +189,6 @@ Poskytovatelé jako OpenAI/Codex blokují přístup z určitých geografických
|
||||
- **TLS Fingerprint Spoofing** — Otisk prstu TLS podobný prohlížeči pomocí `wreq-js` pro obcházení detekce botů
|
||||
- **🔏 Porovnávání otisků prstů v CLI** — Změní pořadí záhlaví a polí v těle serveru tak, aby odpovídala nativním binárním podpisům v CLI, čímž drasticky snižuje riziko nahlašování účtu. IP adresa proxy je zachována — získáte současně stealth **i** maskování IP adresy.
|
||||
|
||||
|
||||
|
||||
|
||||
<details>
|
||||
<summary><b>🆓 4. „Chci používat umělou inteligenci pro kódování, ale nemám peníze“</b></summary>
|
||||
</details>
|
||||
@@ -216,9 +203,6 @@ Ne každý si může dovolit zaplatit 20–200 dolarů měsíčně za předplatn
|
||||
- **NVIDIA NIM Free Access** — ~40 RPM developerský přístup k více než 70 modelům na build.nvidia.com (přechod z kreditů na čisté limity rychlosti)
|
||||
- **Strategie optimalizace nákladů** – Strategie směrování, která automaticky vybere nejlevnějšího dostupného poskytovatele
|
||||
|
||||
|
||||
|
||||
|
||||
<details>
|
||||
<summary><b>🔒 5. „Potřebuji chránit svou bránu umělé inteligence před neoprávněným přístupem“</b></summary>
|
||||
</details>
|
||||
@@ -236,9 +220,6 @@ Při zpřístupnění brány umělé inteligence síti (LAN, VPS, Docker) může
|
||||
- **Ochrana proti vkládání výzev** – Sanitizace proti škodlivým vzorcům výzev
|
||||
- **Šifrování AES-256-GCM** – přihlašovací údaje jsou v klidovém stavu šifrovány
|
||||
|
||||
|
||||
|
||||
|
||||
<details>
|
||||
<summary><b>🛑 6. „Můj poskytovatel selhal a já ztratil/a programovací tok“</b></summary>
|
||||
</details>
|
||||
@@ -254,9 +235,6 @@ Poskytovatelé umělé inteligence se mohou stát nestabilními, vracet chyby 5x
|
||||
- **Kombinovaný jistič** – Automaticky deaktivuje selhávajícího poskytovatele v rámci kombinovaného řetězce
|
||||
- **Dashboard stavu** — Monitorování provozuschopnosti, stavy jističů, uzamčení, statistiky mezipaměti, latence p50/p95/p99
|
||||
|
||||
|
||||
|
||||
|
||||
<details>
|
||||
<summary><b>🔧 7. „Konfigurace každého nástroje umělé inteligence je zdlouhavá a opakující se“</b></summary>
|
||||
</details>
|
||||
@@ -270,9 +248,6 @@ Vývojáři používají Cursor, Claude Code, Codex CLI, OpenClaw, Gemini CLI, K
|
||||
- **Průvodce zaváděním** – 4krokové nastavení pro začínající uživatele
|
||||
- **Jeden koncový bod, všechny modely** – jednou nakonfigurujte `http://localhost:20128/v1` a získejte přístup k více než 44 poskytovatelům
|
||||
|
||||
|
||||
|
||||
|
||||
<details>
|
||||
<summary><b>🔑 8. „Správa OAuth tokenů od více poskytovatelů je peklo“</b></summary>
|
||||
</details>
|
||||
@@ -288,9 +263,6 @@ Claude Code, Codex, Gemini CLI, Copilot – všechny používají OAuth 2.0 s to
|
||||
- **OAuth Behind Nginx** — Používá `window.location.origin` pro kompatibilitu s reverzní proxy
|
||||
- **Průvodce vzdáleným OAuth** – Podrobný návod k přihlašovacím údajům Google Cloud na VPS/Dockeru
|
||||
|
||||
|
||||
|
||||
|
||||
<details>
|
||||
<summary><b>📊 9. „Nevím, kolik utrácím ani kde“</b></summary>
|
||||
</details>
|
||||
@@ -305,9 +277,6 @@ Vývojáři používají více placených poskytovatelů, ale nemají jednotný
|
||||
- **Statistiky použití pro každý klíč API** — Počet požadavků a časové razítko posledního použití pro každý klíč
|
||||
- **Analytický panel** – Statistické karty, graf využití modelu, tabulka poskytovatelů s mírou úspěšnosti a latencí
|
||||
|
||||
|
||||
|
||||
|
||||
<details>
|
||||
<summary><b>🐛 10. „Nedokážu diagnostikovat chyby a problémy ve volání umělé inteligence.“</b></summary>
|
||||
</details>
|
||||
@@ -324,9 +293,6 @@ Když volání selže, vývojář neví, zda se jednalo o limit rychlosti, vypr
|
||||
- **Souborové protokolování s rotací** – Konzolový interceptor zachycuje vše do protokolu JSON s rotací na základě velikosti
|
||||
- **Zpráva o systémových informacích** — příkaz `npm run system-info` vygeneruje `system-info.txt` s kompletním popisem vašeho prostředí (verze uzlu, verze OmniRoute, operační systém, nástroje CLI, stav Dockeru/PM2). Přiložte jej při hlášení problémů pro okamžité třídění.
|
||||
|
||||
|
||||
|
||||
|
||||
<details>
|
||||
<summary><b>🏗️ 11. „Nasazení a údržba brány je složitá“</b></summary>
|
||||
</details>
|
||||
@@ -343,9 +309,6 @@ Instalace, konfigurace a údržba AI proxy v různých prostředích (lokální,
|
||||
- **Cloud Sync** – Konfigurace synchronizace mezi zařízeními pomocí Cloudflare Workers
|
||||
- **Zálohy databází** — Automatické zálohování, obnovení, export a import všech nastavení
|
||||
|
||||
|
||||
|
||||
|
||||
<details>
|
||||
<summary><b>🌍 12. „Rozhraní je pouze v angličtině a můj tým nemluví anglicky“</b></summary>
|
||||
</details>
|
||||
@@ -359,9 +322,6 @@ Týmy v neanglicky mluvících zemích, zejména v Latinské Americe, Asii a Evr
|
||||
- **Vícejazyčné soubory README** — 30 kompletních překladů dokumentace
|
||||
- **Výběr jazyka** — Ikona glóbu v záhlaví pro přepínání v reálném čase
|
||||
|
||||
|
||||
|
||||
|
||||
<details>
|
||||
<summary><b>🔄 13. „Potřebuji víc než jen chat – potřebuji vložené soubory, obrázky, zvuk.“</b></summary>
|
||||
</details>
|
||||
@@ -380,9 +340,6 @@ Umělá inteligence není jen dokončování chatu. Vývojáři potřebují gene
|
||||
- **Změna pořadí** — `/v1/rerank` — Změna pořadí relevance dokumentu
|
||||
- **Responses API** — Plná podpora `/v1/responses` pro Codex
|
||||
|
||||
|
||||
|
||||
|
||||
<details>
|
||||
<summary><b>🧪 14. „Nemám způsob, jak testovat a porovnávat kvalitu napříč modely.“</b></summary>
|
||||
</details>
|
||||
@@ -397,9 +354,6 @@ Vývojáři chtějí vědět, který model je pro jejich případ použití nejl
|
||||
- **Tester chatu** — Kompletní okružní cesta s vizuálním vykreslováním odpovědí
|
||||
- **Živý monitor** — Stream všech požadavků procházejících proxy serverem v reálném čase
|
||||
|
||||
|
||||
|
||||
|
||||
<details>
|
||||
<summary><b>📈 15. „Potřebuji škálovat bez ztráty výkonu“</b></summary>
|
||||
</details>
|
||||
@@ -415,9 +369,6 @@ S rostoucím objemem požadavků generují stejné otázky bez ukládání do me
|
||||
- **Mezipaměť pro ověření klíčů API** — třívrstvá mezipaměť pro výkon produkčního prostředí
|
||||
- **Dashboard s telemetrií** – latence p50/p95/p99, statistiky mezipaměti, dostupnost
|
||||
|
||||
|
||||
|
||||
|
||||
<details>
|
||||
<summary><b>🤖 16. „Chci mít chování modelů globálně pod kontrolou“</b></summary>
|
||||
</details>
|
||||
@@ -434,9 +385,6 @@ Vývojáři, kteří chtějí všechny odpovědi v určitém jazyce, se specific
|
||||
- **Přepínání poskytovatele** – Povolení/zakázání všech připojení pro poskytovatele jedním kliknutím
|
||||
- **Blokovaní poskytovatelé** – Vyloučení konkrétních poskytovatelů ze seznamu `/v1/models`
|
||||
|
||||
|
||||
|
||||
|
||||
<details>
|
||||
<summary><b>🧰 17. „Potřebuji nástroje MCP jako prvotřídní produktové funkce.“</b></summary>
|
||||
</details>
|
||||
@@ -449,9 +397,6 @@ Mnoho bran umělé inteligence odhaluje MCP pouze jako skrytý implementační d
|
||||
- Vyhrazená stránka pro správu MCP s procesy, nástroji, rozsahy a auditem
|
||||
- Vestavěný rychlý start pro `omniroute --mcp` a onboarding klienta
|
||||
|
||||
|
||||
|
||||
|
||||
<details>
|
||||
<summary><b>🧠 18. „Potřebuji orchestraci A2A se synchronizací a cestami úloh streamu.“</b></summary>
|
||||
</details>
|
||||
@@ -464,9 +409,6 @@ Pracovní postupy agentů vyžadují jak přímé odpovědi, tak dlouhodobé str
|
||||
- Streamování SSE s šířením stavu terminálu
|
||||
- Rozhraní API životního cyklu úloh pro `tasks/get` a `tasks/cancel`
|
||||
|
||||
|
||||
|
||||
|
||||
<details>
|
||||
<summary><b>🛰️ 19. „Potřebuji skutečný stav procesu MCP, ne odhadovaný stav.“</b></summary>
|
||||
</details>
|
||||
@@ -479,9 +421,6 @@ Provozní týmy potřebují vědět, zda je MCP skutečně aktivní, nejen zda j
|
||||
- API stavu MCP kombinující prezenční signál a nedávnou aktivitu
|
||||
- Karty stavu uživatelského rozhraní pro zobrazení aktuálnosti procesů/provozuschopnosti/prezenčního signálu
|
||||
|
||||
|
||||
|
||||
|
||||
<details>
|
||||
<summary><b>📋 20. „Potřebuji auditovatelné provedení nástroje MCP“</b></summary>
|
||||
</details>
|
||||
@@ -494,9 +433,6 @@ Když nástroje mění konfiguraci nebo spouštějí operační akce, týmy pot
|
||||
- Filtruje podle nástroje, úspěchu/neúspěchu, klíče API a stránkování
|
||||
- Tabulka auditu dashboardu + koncové body statistik pro automatizaci
|
||||
|
||||
|
||||
|
||||
|
||||
<details>
|
||||
<summary><b>🔐 21. „Potřebuji omezená oprávnění MCP pro každou integraci.“</b></summary>
|
||||
</details>
|
||||
@@ -509,9 +445,6 @@ Různí klienti by měli mít přístup ke kategoriím nástrojů s nejnižším
|
||||
- Vynucení rozsahu a viditelnost v uživatelském rozhraní správy MCP
|
||||
- Bezpečná výchozí poloha pro provozní nástroje
|
||||
|
||||
|
||||
|
||||
|
||||
<details>
|
||||
<summary><b>⚙️ 22. „Potřebuji provozní kontroly bez nutnosti přesouvání“</b></summary>
|
||||
</details>
|
||||
@@ -524,9 +457,6 @@ Týmy potřebují rychlé změny v běhovém prostředí během incidentů nebo
|
||||
- Používejte profily odolnosti z předdefinovaných balíčků zásad
|
||||
- Resetujte stav jističe ze stejného ovládacího panelu
|
||||
|
||||
|
||||
|
||||
|
||||
<details>
|
||||
<summary><b>🔄 23. „Potřebuji živý přehled o životním cyklu úkolů A2A a jejich zrušení.“</b></summary>
|
||||
</details>
|
||||
@@ -539,9 +469,6 @@ Bez přehledu o životním cyklu je obtížné třídit incidenty úkolů.
|
||||
- Podrobný přehled metadat úloh, událostí a artefaktů
|
||||
- Koncový bod zrušení úlohy a akce uživatelského rozhraní s potvrzením
|
||||
|
||||
|
||||
|
||||
|
||||
<details>
|
||||
<summary><b>🌊 24. „Potřebuji metriky aktivního streamu pro A2A zátěž“</b></summary>
|
||||
</details>
|
||||
@@ -554,9 +481,6 @@ Streamovací pracovní postupy vyžadují provozní přehled o souběžnosti a
|
||||
- Časové razítko posledního úkolu a počty pro jednotlivé stavy
|
||||
- Karty A2A dashboardu pro monitorování provozu v reálném čase
|
||||
|
||||
|
||||
|
||||
|
||||
<details>
|
||||
<summary><b>🪪 25. „Potřebuji standardní vyhledávání agentů pro klienty“</b></summary>
|
||||
</details>
|
||||
@@ -569,9 +493,6 @@ Externí klienti a orchestratoři potřebují pro onboarding strojově čitelná
|
||||
- Schopnosti a dovednosti zobrazené v uživatelském rozhraní pro správu
|
||||
- API pro stav A2A zahrnuje metadata pro zjišťování pro automatizaci
|
||||
|
||||
|
||||
|
||||
|
||||
<details>
|
||||
<summary><b>🧭 26. „Potřebuji v uživatelském rozhraní produktu zjistitelnost protokolu.“</b></summary>
|
||||
</details>
|
||||
@@ -584,9 +505,6 @@ Pokud uživatelé nemohou objevit protokolové povrchy, kvalita přijetí a podp
|
||||
- Přepínání stavu inline služby (Online/Offline) pro MCP a A2A
|
||||
- Odkazy z přehledu na vyhrazené karty pro správu
|
||||
|
||||
|
||||
|
||||
|
||||
<details>
|
||||
<summary><b>🧪 27. „Potřebuji komplexní ověření protokolu se skutečnými klienty.“</b></summary>
|
||||
</details>
|
||||
@@ -599,9 +517,6 @@ Simulované testy nestačí k ověření kompatibility protokolu před vydáním
|
||||
- Klientské testy A2A pro toky zjišťování, odesílání, streamování, načítání a zrušení
|
||||
- Křížová kontrola tvrzení oproti API pro audit MCP a úkoly A2A
|
||||
|
||||
|
||||
|
||||
|
||||
<details>
|
||||
<summary><b>📡 28. „Potřebuji jednotnou pozorovatelnost napříč všemi rozhraními“</b></summary>
|
||||
</details>
|
||||
@@ -614,9 +529,6 @@ Rozdělení pozorovatelnosti podle protokolu vytváří slepá místa a delší
|
||||
- Stav + audit + telemetrie požadavků napříč vrstvami OpenAI, MCP a A2A
|
||||
- Provozní API pro stav a automatizaci
|
||||
|
||||
|
||||
|
||||
|
||||
<details>
|
||||
<summary><b>💼 29. „Potřebuji jeden runtime pro proxy + nástroje + orchestraci agentů“</b></summary>
|
||||
</details>
|
||||
@@ -629,9 +541,6 @@ Spouštění mnoha samostatných služeb zvyšuje provozní náklady a počet po
|
||||
- Sdílené ověřování, odolnost, úložiště dat a pozorovatelnost
|
||||
- Konzistentní model politik napříč všemi interakčními plochami
|
||||
|
||||
|
||||
|
||||
|
||||
<details>
|
||||
<summary><b>🚀 30. „Potřebuji agentské pracovní postupy bez slepení kódu.“</b></summary>
|
||||
</details>
|
||||
@@ -644,9 +553,6 @@ Týmy ztrácejí rychlost při spojování více ad-hoc služeb a skriptů.
|
||||
- Vestavěná uživatelská rozhraní pro správu protokolů a cesty pro ověřování kouře
|
||||
- Základy připravené pro produkční prostředí (zabezpečení, protokolování, odolnost, zálohování)
|
||||
|
||||
|
||||
|
||||
|
||||
### Příklady herních plánů (integrované případy užití)
|
||||
|
||||
**Příručka A: Maximalizace placeného předplatného + levné zálohování**
|
||||
@@ -701,13 +607,13 @@ Outcome: deep fallback depth for deadline-critical workloads
|
||||
|
||||
> Nastavte si kódování s umělou inteligencí během několika minut za **0 $/měsíc** . Propojte tyto bezplatné účty a využijte vestavěnou kombinaci **Free Stack** .
|
||||
|
||||
Krok | Akce | Poskytovatelé odemčeni
|
||||
--- | --- | ---
|
||||
1 | Připojení **Kiro** (AWS Builder ID OAuth) | Claude Sonnet 4.5, Haiku 4.5 – **neomezeně**
|
||||
2 | Připojení k **iFlow** (Google OAuth) | kimi-k2-myšlení, qwen3-coder-plus, deepseek-r1... — **neomezeně**
|
||||
3 | Připojení **Qwen** (kód zařízení) | qwen3-coder-plus, qwen3-coder-flash... — **neomezeně**
|
||||
4 | Připojení **rozhraní příkazového řádku Gemini** (Google OAuth) | gemini-3-flash, gemini-2.5-pro — **180 000 GBP/měsíc zdarma**
|
||||
5 | `/dashboard/combos` → Šablona **Free Stack (0 $)** | Automatické zařazení všech bezplatných poskytovatelů do routingu
|
||||
| Krok | Akce | Poskytovatelé odemčeni |
|
||||
| ---- | -------------------------------------------------------------- | ----------------------------------------------------------------- |
|
||||
| 1 | Připojení **Kiro** (AWS Builder ID OAuth) | Claude Sonnet 4.5, Haiku 4.5 – **neomezeně** |
|
||||
| 2 | Připojení k **iFlow** (Google OAuth) | kimi-k2-myšlení, qwen3-coder-plus, deepseek-r1... — **neomezeně** |
|
||||
| 3 | Připojení **Qwen** (kód zařízení) | qwen3-coder-plus, qwen3-coder-flash... — **neomezeně** |
|
||||
| 4 | Připojení **rozhraní příkazového řádku Gemini** (Google OAuth) | gemini-3-flash, gemini-2.5-pro — **180 000 GBP/měsíc zdarma** |
|
||||
| 5 | `/dashboard/combos` → Šablona **Free Stack (0 $)** | Automatické zařazení všech bezplatných poskytovatelů do routingu |
|
||||
|
||||
**V libovolném IDE/CLI naveďte:** `http://localhost:20128/v1` · Klíč API: `any-string` · Hotovo.
|
||||
|
||||
@@ -732,13 +638,13 @@ omniroute
|
||||
|
||||
Dashboard se otevírá na `http://localhost:20128` a základní URL API je `http://localhost:20128/v1` .
|
||||
|
||||
Příkaz | Popis
|
||||
--- | ---
|
||||
`omniroute` | Spuštění serveru ( `PORT=20128` , API a dashboard na stejném portu)
|
||||
`omniroute --port 3000` | Nastavte kanonický/API port na 3000
|
||||
`omniroute --mcp` | Spuštění MCP serveru (transport stdio)
|
||||
`omniroute --no-open` | Neotevírat prohlížeč automaticky
|
||||
`omniroute --help` | Zobrazit nápovědu
|
||||
| Příkaz | Popis |
|
||||
| ----------------------- | ------------------------------------------------------------------- |
|
||||
| `omniroute` | Spuštění serveru ( `PORT=20128` , API a dashboard na stejném portu) |
|
||||
| `omniroute --port 3000` | Nastavte kanonický/API port na 3000 |
|
||||
| `omniroute --mcp` | Spuštění MCP serveru (transport stdio) |
|
||||
| `omniroute --no-open` | Neotevírat prohlížeč automaticky |
|
||||
| `omniroute --help` | Zobrazit nápovědu |
|
||||
|
||||
Volitelný režim s rozděleným portem:
|
||||
|
||||
@@ -847,10 +753,10 @@ docker compose --profile base up -d
|
||||
docker compose --profile cli up -d
|
||||
```
|
||||
|
||||
Obraz | Štítek | Velikost | Popis
|
||||
--- | --- | --- | ---
|
||||
`diegosouzapw/omniroute` | `latest` | ~250 MB | Nejnovější stabilní verze
|
||||
`diegosouzapw/omniroute` | `1.0.3` | ~250 MB | Aktuální verze
|
||||
| Obraz | Štítek | Velikost | Popis |
|
||||
| ------------------------ | -------- | -------- | ------------------------- |
|
||||
| `diegosouzapw/omniroute` | `latest` | ~250 MB | Nejnovější stabilní verze |
|
||||
| `diegosouzapw/omniroute` | `1.0.3` | ~250 MB | Aktuální verze |
|
||||
|
||||
---
|
||||
|
||||
@@ -893,41 +799,47 @@ Po minimalizaci se OmniRoute nachází v systémové liště a nabízí rychlé
|
||||
|
||||
## 💰 Přehled cen
|
||||
|
||||
Úroveň | Poskytovatel | Náklady | Obnovení kvóty | Nejlepší pro
|
||||
--- | --- | --- | --- | ---
|
||||
**💳 PŘEDPLATNÉ** | Claude Code (profesionál) | 20 dolarů měsíčně | 5 hodin + týdně | Již přihlášen/a k odběru
|
||||
| Kodex (Plus/Pro) | 20–200 USD/měsíc | 5 hodin + týdně | Uživatelé OpenAI
|
||||
| Rozhraní příkazového řádku Gemini | **UVOLNIT** | 180 tisíc měsíčně + 1 tisíc denně | Každý!
|
||||
| GitHub Copilot | 10–19 USD/měsíc | Měsíční | Uživatelé GitHubu
|
||||
**🔑 KLÍČ API** | NVIDIA NIM | **ZDARMA** (vývoj navždy) | ~40 ot./min | 70+ otevřených modelů
|
||||
| Mozky | **ZDARMA** (1 milion tok/den) | 60 000 otáček za minutu / 30 ot./min | Nejrychlejší na světě
|
||||
| Groq | **ZDARMA** (30 ot./min.) | 14,4 tisíc otáček za minutu | Ultrarychlá lama/gema
|
||||
| DeepSeek V3.2 | 0,27/1,10 USD za 1 milion | Žádný | Nejlepší zdůvodnění ceny a kvality
|
||||
| xAI Grok-4 Rychlý | **0,20/0,50 USD za 1 milion** 🆕 | Žádný | Nejrychlejší + volání nástroje, ultranízké
|
||||
| xAI Grok-4 (standardní) | 0,20/1,50 USD za 1 milion 🆕 | Žádný | Vlajková loď Reasoning od xAI
|
||||
| Mistral | Zkušební verze zdarma + placené | Omezená sazba | Evropská umělá inteligence
|
||||
| OpenRouter | Platba za použití | Žádný | Více než 100 modelů agregováno.
|
||||
**💰 LEVNÉ** | GLM-5 (přes Z.AI) 🆕 | 0,5 USD/1 milion | Denně v 10:00 | Výstup 128 tisíc obrazových bodů, nejnovější vlajková loď
|
||||
| GLM-4.7 | 0,6 USD/1 milion | Denně v 10:00 | Záloha rozpočtu
|
||||
| MiniMax M2.5 🆕 | Vstup 0,3 USD/1 milion | 5hodinové válcování | Úvaha + agentní úkoly
|
||||
| MiniMax M2.1 | 0,2 USD/1 milion | 5hodinové válcování | Nejlevnější varianta
|
||||
| Kimi K2.5 (Moonshot API) 🆕 | Platba za použití | Žádný | Přímý přístup k Moonshot API
|
||||
| Kimi K2 | 9 dolarů měsíčně bez závazků | 10 milionů tokenů/měsíc | Předvídatelné náklady
|
||||
**🆓 ZDARMA** | iFlow | **0 dolarů** | Neomezený | 5 modelů neomezeně
|
||||
| Qwen | **0 dolarů** | Neomezený | 4 modely neomezeně
|
||||
| Kiro | **0 dolarů** | Neomezený | Claude Sonnet/Haiku (tvorce AWS)
|
||||
| Úroveň | Poskytovatel | Náklady | Obnovení kvóty | Nejlepší pro |
|
||||
| --------------------------------- | -------------------------------- | ------------------------------------ | ------------------------------------------ | --------------------------------------------------------- |
|
||||
| **💳 PŘEDPLATNÉ** | Claude Code (profesionál) | 20 dolarů měsíčně | 5 hodin + týdně | Již přihlášen/a k odběru |
|
||||
| Kodex (Plus/Pro) | 20–200 USD/měsíc | 5 hodin + týdně | Uživatelé OpenAI |
|
||||
| Rozhraní příkazového řádku Gemini | **UVOLNIT** | 180 tisíc měsíčně + 1 tisíc denně | Každý! |
|
||||
| GitHub Copilot | 10–19 USD/měsíc | Měsíční | Uživatelé GitHubu |
|
||||
| **🔑 KLÍČ API** | NVIDIA NIM | **ZDARMA** (vývoj navždy) | ~40 ot./min | 70+ otevřených modelů |
|
||||
| Mozky | **ZDARMA** (1 milion tok/den) | 60 000 otáček za minutu / 30 ot./min | Nejrychlejší na světě |
|
||||
| Groq | **ZDARMA** (30 ot./min.) | 14,4 tisíc otáček za minutu | Ultrarychlá lama/gema |
|
||||
| DeepSeek V3.2 | 0,27/1,10 USD za 1 milion | Žádný | Nejlepší zdůvodnění ceny a kvality |
|
||||
| xAI Grok-4 Rychlý | **0,20/0,50 USD za 1 milion** 🆕 | Žádný | Nejrychlejší + volání nástroje, ultranízké |
|
||||
| xAI Grok-4 (standardní) | 0,20/1,50 USD za 1 milion 🆕 | Žádný | Vlajková loď Reasoning od xAI |
|
||||
| Mistral | Zkušební verze zdarma + placené | Omezená sazba | Evropská umělá inteligence |
|
||||
| OpenRouter | Platba za použití | Žádný | Více než 100 modelů agregováno. |
|
||||
| **💰 LEVNÉ** | GLM-5 (přes Z.AI) 🆕 | 0,5 USD/1 milion | Denně v 10:00 | Výstup 128 tisíc obrazových bodů, nejnovější vlajková loď |
|
||||
| GLM-4.7 | 0,6 USD/1 milion | Denně v 10:00 | Záloha rozpočtu |
|
||||
| MiniMax M2.5 🆕 | Vstup 0,3 USD/1 milion | 5hodinové válcování | Úvaha + agentní úkoly |
|
||||
| MiniMax M2.1 | 0,2 USD/1 milion | 5hodinové válcování | Nejlevnější varianta |
|
||||
| Kimi K2.5 (Moonshot API) 🆕 | Platba za použití | Žádný | Přímý přístup k Moonshot API |
|
||||
| Kimi K2 | 9 dolarů měsíčně bez závazků | 10 milionů tokenů/měsíc | Předvídatelné náklady |
|
||||
| **🆓 ZDARMA** | iFlow | **0 dolarů** | Neomezený | 5 modelů neomezeně |
|
||||
| Qwen | **0 dolarů** | Neomezený | 4 modely neomezeně |
|
||||
| Kiro | **0 dolarů** | Neomezený | Claude Sonnet/Haiku (tvorce AWS) |
|
||||
|
||||
> 🆕 **Přidány nové modely (březen 2026):** řada Grok-4 Fast za 0,20 USD/0,50 USD/M (benchmarkováno na 1143 ms – o 30 % rychlejší než Gemini 2.5 Flash), GLM-5 přes Z.AI s výstupem 128K, uvažování MiniMax M2.5, aktualizované ceny DeepSeek V3.2, Kimi K2.5 přes Moonshot Direct API.
|
||||
|
||||
**💡 Kombinovaný balík za 0 $ — Kompletní bezplatná instalace:**
|
||||
|
||||
```
|
||||
Gemini CLI (180K/mo free)
|
||||
→ iFlow (unlimited: kimi-k2-thinking, qwen3-coder-plus, deepseek-r1)
|
||||
→ Kiro (Claude Sonnet 4.5 + Haiku — unlimited, via AWS Builder ID)
|
||||
→ Qwen (4 models — unlimited)
|
||||
→ Groq (14.4K req/day — ultra-fast)
|
||||
→ NVIDIA NIM (70+ models — 40 RPM forever)
|
||||
# 🆓 Ultimate Free Stack 2026 — 11 Providers, $0 Forever
|
||||
Kiro (kr/) → Claude Sonnet/Haiku UNLIMITED
|
||||
iFlow (if/) → kimi-k2-thinking, qwen3-coder-plus, deepseek-r1 UNLIMITED
|
||||
LongCat Lite (lc/) → LongCat-Flash-Lite — 50M tokens/day 🔥
|
||||
Pollinations (pol/) → GPT-5, Claude, DeepSeek, Llama 4 — no key needed
|
||||
Qwen (qw/) → qwen3-coder-plus, qwen3-coder-flash, qwen3-coder-next UNLIMITED
|
||||
Gemini (gemini/) → Gemini 2.5 Flash — 1,500 req/day free API key
|
||||
Cloudflare AI (cf/) → Llama 70B, Gemma 3, Mistral — 10K Neurons/day
|
||||
Scaleway (scw/) → Qwen3 235B, Llama 70B — 1M free tokens (EU)
|
||||
Groq (groq/) → Llama/Gemma ultra-fast — 14.4K req/day
|
||||
NVIDIA NIM (nvidia/) → 70+ open models — 40 RPM forever
|
||||
Cerebras (cerebras/) → Llama/Qwen world-fastest — 1M tok/day
|
||||
```
|
||||
|
||||
**Nulové náklady. Nikdy nepřestávejte s kódováním.** Nakonfigurujte si to jako jednu kombinaci OmniRoute a všechny záložní režimy se provede automaticky – žádné ruční přepínání.
|
||||
@@ -942,59 +854,59 @@ Gemini CLI (180K/mo free)
|
||||
|
||||
### 🔵 CLAUDE MODELS (přes Kiro — AWS Builder ID)
|
||||
|
||||
Model | Předpona | Omezit | Limit rychlosti
|
||||
--- | --- | --- | ---
|
||||
`claude-sonnet-4.5` | `kr/` | **Neomezený** | Žádný hlášený denní limit
|
||||
`claude-haiku-4.5` | `kr/` | **Neomezený** | Žádný hlášený denní limit
|
||||
`claude-opus-4.6` | `kr/` | **Neomezený** | Nejnovější opus od Kira
|
||||
| Model | Předpona | Omezit | Limit rychlosti |
|
||||
| ------------------- | -------- | ------------- | ------------------------- |
|
||||
| `claude-sonnet-4.5` | `kr/` | **Neomezený** | Žádný hlášený denní limit |
|
||||
| `claude-haiku-4.5` | `kr/` | **Neomezený** | Žádný hlášený denní limit |
|
||||
| `claude-opus-4.6` | `kr/` | **Neomezený** | Nejnovější opus od Kira |
|
||||
|
||||
### 🟢 MODELY IFLOW (Bezplatné OAuth — bez nutnosti platit kreditní kartou)
|
||||
|
||||
Model | Předpona | Omezit | Limit rychlosti
|
||||
--- | --- | --- | ---
|
||||
`kimi-k2-thinking` | `if/` | **Neomezený** | Žádný hlášený strop
|
||||
`qwen3-coder-plus` | `if/` | **Neomezený** | Žádný hlášený strop
|
||||
`deepseek-r1` | `if/` | **Neomezený** | Žádný hlášený strop
|
||||
`minimax-m2.1` | `if/` | **Neomezený** | Žádný hlášený strop
|
||||
`kimi-k2` | `if/` | **Neomezený** | Žádný hlášený strop
|
||||
| Model | Předpona | Omezit | Limit rychlosti |
|
||||
| ------------------ | -------- | ------------- | ------------------- |
|
||||
| `kimi-k2-thinking` | `if/` | **Neomezený** | Žádný hlášený strop |
|
||||
| `qwen3-coder-plus` | `if/` | **Neomezený** | Žádný hlášený strop |
|
||||
| `deepseek-r1` | `if/` | **Neomezený** | Žádný hlášený strop |
|
||||
| `minimax-m2.1` | `if/` | **Neomezený** | Žádný hlášený strop |
|
||||
| `kimi-k2` | `if/` | **Neomezený** | Žádný hlášený strop |
|
||||
|
||||
### 🟡 MODELY QWEN (Ověření kódu zařízení)
|
||||
|
||||
Model | Předpona | Omezit | Limit rychlosti
|
||||
--- | --- | --- | ---
|
||||
`qwen3-coder-plus` | `qw/` | **Neomezený** | Žádný hlášený strop
|
||||
`qwen3-coder-flash` | `qw/` | **Neomezený** | Žádný hlášený strop
|
||||
`qwen3-coder-next` | `qw/` | **Neomezený** | Žádný hlášený strop
|
||||
`vision-model` | `qw/` | **Neomezený** | Multimodální (obrázky)
|
||||
| Model | Předpona | Omezit | Limit rychlosti |
|
||||
| ------------------- | -------- | ------------- | ---------------------- |
|
||||
| `qwen3-coder-plus` | `qw/` | **Neomezený** | Žádný hlášený strop |
|
||||
| `qwen3-coder-flash` | `qw/` | **Neomezený** | Žádný hlášený strop |
|
||||
| `qwen3-coder-next` | `qw/` | **Neomezený** | Žádný hlášený strop |
|
||||
| `vision-model` | `qw/` | **Neomezený** | Multimodální (obrázky) |
|
||||
|
||||
### 🟣 Rozhraní GEMINI CLI (Google OAuth)
|
||||
|
||||
Model | Předpona | Omezit | Limit rychlosti
|
||||
--- | --- | --- | ---
|
||||
`gemini-3-flash-preview` | `gc/` | **180 tisíc tok/měsíc** + 1 tisíc/den | Měsíční reset
|
||||
`gemini-2.5-pro` | `gc/` | 180 tisíc měsíčně (sdílený bazén) | Vysoká kvalita
|
||||
| Model | Předpona | Omezit | Limit rychlosti |
|
||||
| ------------------------ | -------- | ------------------------------------- | --------------- |
|
||||
| `gemini-3-flash-preview` | `gc/` | **180 tisíc tok/měsíc** + 1 tisíc/den | Měsíční reset |
|
||||
| `gemini-2.5-pro` | `gc/` | 180 tisíc měsíčně (sdílený bazén) | Vysoká kvalita |
|
||||
|
||||
### ⚫ NVIDIA NIM (Bezplatný klíč API — build.nvidia.com)
|
||||
|
||||
Úroveň | Denní limit | Limit rychlosti | Poznámky
|
||||
--- | --- | --- | ---
|
||||
Zdarma (vývojář) | Žádný limit tokenů | **~40 ot./min** | Více než 70 modelů; přechod na čisté limity sazeb v polovině roku 2025
|
||||
| Úroveň | Denní limit | Limit rychlosti | Poznámky |
|
||||
| ---------------- | ------------------ | --------------- | ---------------------------------------------------------------------- |
|
||||
| Zdarma (vývojář) | Žádný limit tokenů | **~40 ot./min** | Více než 70 modelů; přechod na čisté limity sazeb v polovině roku 2025 |
|
||||
|
||||
Oblíbené bezplatné modely: `moonshotai/kimi-k2.5` (Kimi K2.5), `z-ai/glm4.7` (GLM 4.7), `deepseek-ai/deepseek-v3.2` (DeepSeek V3.2), `nvidia/llama-3.3-70b-instruct` , `deepseek/deepseek-r1`
|
||||
|
||||
### ⚪ CEREBRAS (Bezplatný klíč API — inference.cerebras.ai)
|
||||
|
||||
Úroveň | Denní limit | Limit rychlosti | Poznámky
|
||||
--- | --- | --- | ---
|
||||
Uvolnit | **1 milion tokenů/den** | 60 000 otáček za minutu / 30 ot./min | Nejrychlejší inference LLM na světě; denně se resetuje
|
||||
| Úroveň | Denní limit | Limit rychlosti | Poznámky |
|
||||
| ------- | ----------------------- | ------------------------------------ | ------------------------------------------------------ |
|
||||
| Uvolnit | **1 milion tokenů/den** | 60 000 otáček za minutu / 30 ot./min | Nejrychlejší inference LLM na světě; denně se resetuje |
|
||||
|
||||
Dostupné zdarma: `llama-3.3-70b` , `llama-3.1-8b` , `deepseek-r1-distill-llama-70b`
|
||||
|
||||
### 🔴 GROQ (Bezplatný API klíč — console.groq.com)
|
||||
|
||||
Úroveň | Denní limit | Limit rychlosti | Poznámky
|
||||
--- | --- | --- | ---
|
||||
Uvolnit | **14,4 tisíc otáček za minutu** | 30 ot./min na model | Žádná kreditní karta; limit 429, neúčtováno
|
||||
| Úroveň | Denní limit | Limit rychlosti | Poznámky |
|
||||
| ------- | ------------------------------- | ------------------- | ------------------------------------------- |
|
||||
| Uvolnit | **14,4 tisíc otáček za minutu** | 30 ot./min na model | Žádná kreditní karta; limit 429, neúčtováno |
|
||||
|
||||
K dispozici zdarma: `llama-3.3-70b-versatile` , `gemma2-9b-it` , `mixtral-8x7b` , `whisper-large-v3`
|
||||
|
||||
@@ -1016,11 +928,11 @@ K dispozici zdarma: `llama-3.3-70b-versatile` , `gemma2-9b-it` , `mixtral-8x7b`
|
||||
|
||||
> Přepisujte libovolné audio/video za **0 $** – Deepgram leady za 200 $ zdarma, AssemblyAI za 50 $ jako záložní nástroj, Groq Whisper jako neomezená nouzová záloha.
|
||||
|
||||
Poskytovatel | Bezplatné kredity | Nejlepší model | Limit rychlosti
|
||||
--- | --- | --- | ---
|
||||
🟢 **Deepgram** | **200 dolarů zdarma** (registrace) | `nova-3` — nejvyšší přesnost, více než 30 jazyků | Žádný limit RPM pro kredity zdarma
|
||||
🔵 **AssemblyAI** | **50 dolarů zdarma** (registrace) | `universal-3-pro` — kapitoly, sentiment, osobní údaje | Žádný limit RPM pro kredity zdarma
|
||||
🔴 **Groq** | **Navždy zdarma** | `whisper-large-v3` — OpenAI Šepot | 30 ot./min (omezená rychlost)
|
||||
| Poskytovatel | Bezplatné kredity | Nejlepší model | Limit rychlosti |
|
||||
| ----------------- | ---------------------------------- | ----------------------------------------------------- | ---------------------------------- |
|
||||
| 🟢 **Deepgram** | **200 dolarů zdarma** (registrace) | `nova-3` — nejvyšší přesnost, více než 30 jazyků | Žádný limit RPM pro kredity zdarma |
|
||||
| 🔵 **AssemblyAI** | **50 dolarů zdarma** (registrace) | `universal-3-pro` — kapitoly, sentiment, osobní údaje | Žádný limit RPM pro kredity zdarma |
|
||||
| 🔴 **Groq** | **Navždy zdarma** | `whisper-large-v3` — OpenAI Šepot | 30 ot./min (omezená rychlost) |
|
||||
|
||||
**Navrhovaná kombinace v `/dashboard/combos` :**
|
||||
|
||||
@@ -1041,118 +953,118 @@ OmniRoute v2.0 je navržen jako operační platforma, nikoli pouze jako proxy pr
|
||||
|
||||
### 🆕 Nové — Vylepšení inspirovaná ClawRouterem (březen 2026)
|
||||
|
||||
Funkce | Co to dělá
|
||||
--- | ---
|
||||
⚡ **Grok-4 Rychlá rodina** | Modely xAI za 0,20 USD/0,50 USD/M – v benchmarku 1143 ms (o 30 % rychlejší než Gemini 2.5 Flash)
|
||||
🧠 **GLM-5 přes Z.AI** | 128 tisíc výstupních dat, 0,5 USD/1 milion USD – nejnovější vlajková loď rodiny GLM
|
||||
🔮 **MiniMax M2.5** | Úvaha + agentní úkoly za 0,30 USD/1 milion – významný upgrade oproti M2.1
|
||||
🎯 **Příznak volání nástroje pro každý model** | `toolCalling: true/false` v registru — AutoCombo přeskakuje modely, které nepodporují nástroje.
|
||||
🌍 **Detekce vícejazyčného záměru** | Klíčová slova PT/ZH/ES/AR v bodování AutoCombo – lepší výběr modelu pro neanglický obsah
|
||||
📊 **Záložní metody řízené benchmarkem** | Skutečná latence p95 z živých požadavků poskytuje kombinované skóre – AutoCombo se učí ze skutečných dat
|
||||
🔁 **Požádat o deduplikaci** | Okno pro deduplikaci na základě hashování obsahu – bezpečné pro více agentů, zabraňuje duplicitním platbám
|
||||
🔌 **Strategie pro zásuvné routery** | Rozšiřitelné rozhraní `RouterStrategy` – přidejte si vlastní logiku směrování jako pluginy
|
||||
| Funkce | Co to dělá |
|
||||
| ---------------------------------------------- | ---------------------------------------------------------------------------------------------------------- |
|
||||
| ⚡ **Grok-4 Rychlá rodina** | Modely xAI za 0,20 USD/0,50 USD/M – v benchmarku 1143 ms (o 30 % rychlejší než Gemini 2.5 Flash) |
|
||||
| 🧠 **GLM-5 přes Z.AI** | 128 tisíc výstupních dat, 0,5 USD/1 milion USD – nejnovější vlajková loď rodiny GLM |
|
||||
| 🔮 **MiniMax M2.5** | Úvaha + agentní úkoly za 0,30 USD/1 milion – významný upgrade oproti M2.1 |
|
||||
| 🎯 **Příznak volání nástroje pro každý model** | `toolCalling: true/false` v registru — AutoCombo přeskakuje modely, které nepodporují nástroje. |
|
||||
| 🌍 **Detekce vícejazyčného záměru** | Klíčová slova PT/ZH/ES/AR v bodování AutoCombo – lepší výběr modelu pro neanglický obsah |
|
||||
| 📊 **Záložní metody řízené benchmarkem** | Skutečná latence p95 z živých požadavků poskytuje kombinované skóre – AutoCombo se učí ze skutečných dat |
|
||||
| 🔁 **Požádat o deduplikaci** | Okno pro deduplikaci na základě hashování obsahu – bezpečné pro více agentů, zabraňuje duplicitním platbám |
|
||||
| 🔌 **Strategie pro zásuvné routery** | Rozšiřitelné rozhraní `RouterStrategy` – přidejte si vlastní logiku směrování jako pluginy |
|
||||
|
||||
### 🚀 Předchozí verze v2.0.9+ — Hřiště, otisky prstů v CLI a ACP
|
||||
|
||||
Funkce | Co to dělá
|
||||
--- | ---
|
||||
🎮 **Modelové hřiště** | Stránka řídicího panelu pro přímé testování libovolného modelu – selektory poskytovatele/modelu/koncového bodu, editor Monaco, streamování, přerušení, načasování
|
||||
🔏 **Porovnávání otisků prstů v CLI** | Řazení hlaviček/těl serveru podle poskytovatele tak, aby odpovídalo nativním podpisům CLI – přepínání pro jednotlivé poskytovatele v Nastavení > Zabezpečení. **Vaše IP adresa proxy serveru je zachována.**
|
||||
🤝 **Podpora ACP (Agent Client Protocol)** | Vyhledávání agentů CLI (Codex, Claude, Goose, Gemini CLI, OpenClaw + 9 dalších), generátor procesů, koncový bod `/api/acp/agents`
|
||||
🤖 **Řídicí panel agentů ACP** | Ladění › Stránka Agenti — mřížka 14 agentů se stavem instalace, verzí a formulářem pro vlastní agenta pro libovolný nástroj CLI. Uživatelé **OpenCode** získají tlačítko „Stáhnout opencode.json“, které automaticky vygeneruje konfiguraci připravenou k použití se všemi dostupnými modely.
|
||||
🔧 **Směrování `apiFormat` pro vlastní model** | Vlastní modely s `apiFormat: "responses"` nyní správně směrují do překladače Responses API.
|
||||
🏢 **Izolace pracovního prostoru Codexu** | Více pracovních prostorů Codexu na jeden e-mail – OAuth správně odděluje připojení podle ID pracovního prostoru
|
||||
🔄 **Automatická aktualizace elektronů** | Desktopová aplikace kontroluje aktualizace + automaticky se instaluje po restartu
|
||||
| Funkce | Co to dělá |
|
||||
| ---------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| 🎮 **Modelové hřiště** | Stránka řídicího panelu pro přímé testování libovolného modelu – selektory poskytovatele/modelu/koncového bodu, editor Monaco, streamování, přerušení, načasování |
|
||||
| 🔏 **Porovnávání otisků prstů v CLI** | Řazení hlaviček/těl serveru podle poskytovatele tak, aby odpovídalo nativním podpisům CLI – přepínání pro jednotlivé poskytovatele v Nastavení > Zabezpečení. **Vaše IP adresa proxy serveru je zachována.** |
|
||||
| 🤝 **Podpora ACP (Agent Client Protocol)** | Vyhledávání agentů CLI (Codex, Claude, Goose, Gemini CLI, OpenClaw + 9 dalších), generátor procesů, koncový bod `/api/acp/agents` |
|
||||
| 🤖 **Řídicí panel agentů ACP** | Ladění › Stránka Agenti — mřížka 14 agentů se stavem instalace, verzí a formulářem pro vlastní agenta pro libovolný nástroj CLI. Uživatelé **OpenCode** získají tlačítko „Stáhnout opencode.json“, které automaticky vygeneruje konfiguraci připravenou k použití se všemi dostupnými modely. |
|
||||
| 🔧 **Směrování `apiFormat` pro vlastní model** | Vlastní modely s `apiFormat: "responses"` nyní správně směrují do překladače Responses API. |
|
||||
| 🏢 **Izolace pracovního prostoru Codexu** | Více pracovních prostorů Codexu na jeden e-mail – OAuth správně odděluje připojení podle ID pracovního prostoru |
|
||||
| 🔄 **Automatická aktualizace elektronů** | Desktopová aplikace kontroluje aktualizace + automaticky se instaluje po restartu |
|
||||
|
||||
### 🤖 Operace s agenty a protokoly (v2.0)
|
||||
|
||||
Funkce | Co to dělá
|
||||
--- | ---
|
||||
🔧 **MCP Server (16 nástrojů)** | Nástroje IDE/agent prostřednictvím 3 transportů: stdio, SSE ( `/api/mcp/sse` ), Streamovatelný HTTP ( `/api/mcp/stream` )
|
||||
🤝 **A2A server (JSON-RPC + SSE)** | Spouštění úloh mezi agenty se synchronizací a streamováním
|
||||
🧭 **Konsolidovaná stránka koncových bodů** | Stránka pro správu s kartami Endpoint Proxy, MCP, A2A a API Endpoints
|
||||
🎚️ **Přepínače pro povolení/zakázání služby** | Přepínače ZAP/VYP pro MCP a A2A s trvalým nastavením (výchozí: VYP)
|
||||
🛰️ **Srdeční tep za běhu MCP** | Skutečný stav procesu (pid, doba provozuschopnosti, stáří heartbeatu, transport, režim rozsahu)
|
||||
📋 **Auditní záznam MCP** | Filtrovatelné protokoly auditu s hodnocením úspěchu/neúspěchu a klíčovým přiřazením
|
||||
🔐 **Vynucování rozsahu MCP** | 9 podrobných oprávnění pro řízený přístup k nástrojům
|
||||
📡 **Správa životního cyklu úkolů A2A** | Seznam/filtrování úloh, kontrola událostí/artefaktů, zrušení spuštěných úloh
|
||||
📋 **Objevení karty agenta** | `/.well-known/agent.json` pro automatické vyhledávání klientů
|
||||
🧪 **Testovací postroj Protocol E2E** | Skutečné MCP SDK + toky klientů A2A v `test:protocols:e2e`
|
||||
⚙️ **Provozní kontroly** | Kombinace přepínačů, použití profilů odolnosti, resetování jističů z jednoho ovládacího panelu
|
||||
| Funkce | Co to dělá |
|
||||
| --------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- |
|
||||
| 🔧 **MCP Server (16 nástrojů)** | Nástroje IDE/agent prostřednictvím 3 transportů: stdio, SSE ( `/api/mcp/sse` ), Streamovatelný HTTP ( `/api/mcp/stream` ) |
|
||||
| 🤝 **A2A server (JSON-RPC + SSE)** | Spouštění úloh mezi agenty se synchronizací a streamováním |
|
||||
| 🧭 **Konsolidovaná stránka koncových bodů** | Stránka pro správu s kartami Endpoint Proxy, MCP, A2A a API Endpoints |
|
||||
| 🎚️ **Přepínače pro povolení/zakázání služby** | Přepínače ZAP/VYP pro MCP a A2A s trvalým nastavením (výchozí: VYP) |
|
||||
| 🛰️ **Srdeční tep za běhu MCP** | Skutečný stav procesu (pid, doba provozuschopnosti, stáří heartbeatu, transport, režim rozsahu) |
|
||||
| 📋 **Auditní záznam MCP** | Filtrovatelné protokoly auditu s hodnocením úspěchu/neúspěchu a klíčovým přiřazením |
|
||||
| 🔐 **Vynucování rozsahu MCP** | 9 podrobných oprávnění pro řízený přístup k nástrojům |
|
||||
| 📡 **Správa životního cyklu úkolů A2A** | Seznam/filtrování úloh, kontrola událostí/artefaktů, zrušení spuštěných úloh |
|
||||
| 📋 **Objevení karty agenta** | `/.well-known/agent.json` pro automatické vyhledávání klientů |
|
||||
| 🧪 **Testovací postroj Protocol E2E** | Skutečné MCP SDK + toky klientů A2A v `test:protocols:e2e` |
|
||||
| ⚙️ **Provozní kontroly** | Kombinace přepínačů, použití profilů odolnosti, resetování jističů z jednoho ovládacího panelu |
|
||||
|
||||
### 🧠 Směrování a inteligence
|
||||
|
||||
Funkce | Co to dělá
|
||||
--- | ---
|
||||
🎯 **Inteligentní čtyřúrovňový záložní systém** | Automatická trasa: Předplatné → API klíč → Levné → Zdarma
|
||||
📊 **Sledování kvót v reálném čase** | Počet tokenů v reálném čase + odpočet resetování pro každého poskytovatele
|
||||
🔄 **Překlad formátu** | OpenAI ↔ Claude ↔ Gemini ↔ Odpovědi s konverzemi bezpečnými pro schéma
|
||||
👥 **Podpora více účtů** | Více účtů na poskytovatele s inteligentním výběrem
|
||||
🔄 **Automatická aktualizace tokenů** | Tokeny OAuth se automaticky obnovují při opakovaném pokusu.
|
||||
🎨 **Vlastní kombinace** | 6 vyvažovacích strategií + řízení záložního řetězce
|
||||
🌐 **Směrovač se zástupnými znaky** | dynamické směrování `provider/*`
|
||||
🧠 **Přemýšlení o rozpočtových kontrolách** | Limity pro průchozí, automatické, vlastní a adaptivní uvažování
|
||||
🔀 **Aliasy modelů** | Vestavěné + vlastní aliasování modelů a bezpečnost migrace
|
||||
⚡ **Degradace pozadí** | Směrujte úlohy na pozadí s nízkou prioritou na levnější modely
|
||||
🧪 **Chytré směrování s ohledem na úkoly** | Automatický výběr modelu podle typu obsahu (kódování/vize/analýza/sumarizace)
|
||||
💬 **Vstřikování do systému** | Globální kontroly chování uplatňované konzistentně
|
||||
📄 **Kompatibilita API pro odpovědi** | Plná podpora `/v1/responses` pro Codex a pokročilé agentické pracovní postupy
|
||||
| Funkce | Co to dělá |
|
||||
| ----------------------------------------------- | ----------------------------------------------------------------------------- |
|
||||
| 🎯 **Inteligentní čtyřúrovňový záložní systém** | Automatická trasa: Předplatné → API klíč → Levné → Zdarma |
|
||||
| 📊 **Sledování kvót v reálném čase** | Počet tokenů v reálném čase + odpočet resetování pro každého poskytovatele |
|
||||
| 🔄 **Překlad formátu** | OpenAI ↔ Claude ↔ Gemini ↔ Odpovědi s konverzemi bezpečnými pro schéma |
|
||||
| 👥 **Podpora více účtů** | Více účtů na poskytovatele s inteligentním výběrem |
|
||||
| 🔄 **Automatická aktualizace tokenů** | Tokeny OAuth se automaticky obnovují při opakovaném pokusu. |
|
||||
| 🎨 **Vlastní kombinace** | 6 vyvažovacích strategií + řízení záložního řetězce |
|
||||
| 🌐 **Směrovač se zástupnými znaky** | dynamické směrování `provider/*` |
|
||||
| 🧠 **Přemýšlení o rozpočtových kontrolách** | Limity pro průchozí, automatické, vlastní a adaptivní uvažování |
|
||||
| 🔀 **Aliasy modelů** | Vestavěné + vlastní aliasování modelů a bezpečnost migrace |
|
||||
| ⚡ **Degradace pozadí** | Směrujte úlohy na pozadí s nízkou prioritou na levnější modely |
|
||||
| 🧪 **Chytré směrování s ohledem na úkoly** | Automatický výběr modelu podle typu obsahu (kódování/vize/analýza/sumarizace) |
|
||||
| 💬 **Vstřikování do systému** | Globální kontroly chování uplatňované konzistentně |
|
||||
| 📄 **Kompatibilita API pro odpovědi** | Plná podpora `/v1/responses` pro Codex a pokročilé agentické pracovní postupy |
|
||||
|
||||
### 🎵 Multimodální API
|
||||
|
||||
Funkce | Co to dělá
|
||||
--- | ---
|
||||
🖼️ **Generování obrázků** | `/v1/images/generations` s cloudovým a lokálním backendem
|
||||
📐 **Vložení** | `/v1/embeddings` pro vyhledávání a RAG pipelines
|
||||
🎤 **Přepis zvuku** | `/v1/audio/transcriptions` (Whisper a další poskytovatelé)
|
||||
🔊 **Převod textu na řeč** | `/v1/audio/speech` (více enginů/poskytovatelů)
|
||||
🎬 **Generování videa** | `/v1/videos/generations` (pracovní postupy ComfyUI + SD WebUI)
|
||||
🎵 **Hudební generace** | `/v1/music/generations` (pracovní postupy ComfyUI)
|
||||
🛡️ **Moderování** | Bezpečnostní kontroly `/v1/moderations`
|
||||
🔀 **Změna pořadí** | `/v1/rerank` pro hodnocení relevance
|
||||
🔍 **Vyhledávání na webu** 🆕 | `/v1/search` — 5 poskytovatelů (Serper, Brave, Perplexity, Exa, Tavily), více než 6 500 zdarma/měsíc, automatické přepnutí na záložní systém, mezipaměť
|
||||
| Funkce | Co to dělá |
|
||||
| ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| 🖼️ **Generování obrázků** | `/v1/images/generations` s cloudovým a lokálním backendem |
|
||||
| 📐 **Vložení** | `/v1/embeddings` pro vyhledávání a RAG pipelines |
|
||||
| 🎤 **Přepis zvuku** | `/v1/audio/transcriptions` (Whisper a další poskytovatelé) |
|
||||
| 🔊 **Převod textu na řeč** | `/v1/audio/speech` (více enginů/poskytovatelů) |
|
||||
| 🎬 **Generování videa** | `/v1/videos/generations` (pracovní postupy ComfyUI + SD WebUI) |
|
||||
| 🎵 **Hudební generace** | `/v1/music/generations` (pracovní postupy ComfyUI) |
|
||||
| 🛡️ **Moderování** | Bezpečnostní kontroly `/v1/moderations` |
|
||||
| 🔀 **Změna pořadí** | `/v1/rerank` pro hodnocení relevance |
|
||||
| 🔍 **Vyhledávání na webu** 🆕 | `/v1/search` — 5 poskytovatelů (Serper, Brave, Perplexity, Exa, Tavily), více než 6 500 zdarma/měsíc, automatické přepnutí na záložní systém, mezipaměť |
|
||||
|
||||
### 🛡️ Odolnost, bezpečnost a správa věcí veřejných
|
||||
|
||||
Funkce | Co to dělá
|
||||
--- | ---
|
||||
🔌 **Jističe** | Vypnutí/obnovení pro každý model s ovládáním prahových hodnot
|
||||
🎯 **Modely s ohledem na koncové body** | Vlastní modely deklarují podporované koncové body + formát API
|
||||
🛡️ **Stádo proti hromům** | Ochrana mutexu a semaforu při událostech opakování/rychlosti
|
||||
🧠 **Sémantická + podpisová mezipaměť** | Snížení nákladů/latence díky dvěma vrstvám mezipaměti
|
||||
⚡ **Žádost o idempotenci** | Okno ochrany proti duplikacím
|
||||
🔒 **Falšování otisků prstů pomocí TLS** | Otisk TLS podobný prohlížeči – **snižuje detekci botů a nahlašování účtů**
|
||||
🔏 **Porovnávání otisků prstů v CLI** | Shoduje se s nativními podpisy požadavků CLI – **snižuje riziko zablokování a zároveň zachovává IP adresu proxy**
|
||||
🌐 **Filtrování IP adres** | Ovládání seznamu povolených/blokovaných položek pro odhalená nasazení
|
||||
📊 **Upravitelné limity rychlosti** | Konfigurovatelné globální/na úrovni poskytovatele limity s perzistencí
|
||||
🔑 **Správa klíčů API a stanovení rozsahu** | Bezpečné vydávání/rotace klíčů a kontroly modelu/poskytovatele
|
||||
🛡️ **Chráněné `/models`** | Volitelné ověřování a skrytí poskytovatele pro katalog modelů
|
||||
| Funkce | Co to dělá |
|
||||
| ------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- |
|
||||
| 🔌 **Jističe** | Vypnutí/obnovení pro každý model s ovládáním prahových hodnot |
|
||||
| 🎯 **Modely s ohledem na koncové body** | Vlastní modely deklarují podporované koncové body + formát API |
|
||||
| 🛡️ **Stádo proti hromům** | Ochrana mutexu a semaforu při událostech opakování/rychlosti |
|
||||
| 🧠 **Sémantická + podpisová mezipaměť** | Snížení nákladů/latence díky dvěma vrstvám mezipaměti |
|
||||
| ⚡ **Žádost o idempotenci** | Okno ochrany proti duplikacím |
|
||||
| 🔒 **Falšování otisků prstů pomocí TLS** | Otisk TLS podobný prohlížeči – **snižuje detekci botů a nahlašování účtů** |
|
||||
| 🔏 **Porovnávání otisků prstů v CLI** | Shoduje se s nativními podpisy požadavků CLI – **snižuje riziko zablokování a zároveň zachovává IP adresu proxy** |
|
||||
| 🌐 **Filtrování IP adres** | Ovládání seznamu povolených/blokovaných položek pro odhalená nasazení |
|
||||
| 📊 **Upravitelné limity rychlosti** | Konfigurovatelné globální/na úrovni poskytovatele limity s perzistencí |
|
||||
| 🔑 **Správa klíčů API a stanovení rozsahu** | Bezpečné vydávání/rotace klíčů a kontroly modelu/poskytovatele |
|
||||
| 🛡️ **Chráněné `/models`** | Volitelné ověřování a skrytí poskytovatele pro katalog modelů |
|
||||
|
||||
### 📊 Pozorovatelnost a analytika
|
||||
|
||||
Funkce | Co to dělá
|
||||
--- | ---
|
||||
📝 **Žádost + protokolování proxy** | Úplné protokolování požadavků/odpovědí a proxy
|
||||
📋 **Sjednocený panel protokolů** | Zobrazení požadavků, proxy, auditu a konzole na jedné stránce
|
||||
🔍 **Vyžádat si telemetrii** | Latence p50/p95/p99 a trasování požadavků
|
||||
🏥 **Panel zdraví** | Doba provozuschopnosti, stavy jističů, uzamčení, statistiky mezipaměti
|
||||
💰 **Sledování nákladů** | Kontrola rozpočtu a přehled o cenách pro jednotlivé modely
|
||||
📈 **Analytické vizualizace** | Přehledy využití modelů/poskytovatelů a zobrazení trendů
|
||||
🧪 **Rámec hodnocení** | Testování zlaté sady s konfigurovatelnými strategiemi shody
|
||||
| Funkce | Co to dělá |
|
||||
| ----------------------------------- | ---------------------------------------------------------------------- |
|
||||
| 📝 **Žádost + protokolování proxy** | Úplné protokolování požadavků/odpovědí a proxy |
|
||||
| 📋 **Sjednocený panel protokolů** | Zobrazení požadavků, proxy, auditu a konzole na jedné stránce |
|
||||
| 🔍 **Vyžádat si telemetrii** | Latence p50/p95/p99 a trasování požadavků |
|
||||
| 🏥 **Panel zdraví** | Doba provozuschopnosti, stavy jističů, uzamčení, statistiky mezipaměti |
|
||||
| 💰 **Sledování nákladů** | Kontrola rozpočtu a přehled o cenách pro jednotlivé modely |
|
||||
| 📈 **Analytické vizualizace** | Přehledy využití modelů/poskytovatelů a zobrazení trendů |
|
||||
| 🧪 **Rámec hodnocení** | Testování zlaté sady s konfigurovatelnými strategiemi shody |
|
||||
|
||||
### ☁️ Nasazení a platforma
|
||||
|
||||
Funkce | Co to dělá
|
||||
--- | ---
|
||||
🌐 **Nasazení kdekoli** | Localhost, VPS, Docker, cloudová prostředí
|
||||
💾 **Synchronizace s cloudem** | Synchronizace konfigurace přes cloud worker
|
||||
🔄 **Zálohování/Obnovení** | Toky exportu/importu a obnovy po havárii
|
||||
🧙 **Průvodce nástupem** | Průvodce prvním spuštěním
|
||||
🔧 **Panel nástrojů CLI** | Nastavení oblíbených kódovacích nástrojů jedním kliknutím
|
||||
🎮 **Modelové hřiště** | Otestujte libovolného poskytovatele/model/koncový bod z řídicího panelu
|
||||
🔏 **Přepínač otisků prstů v příkazovém řádku** | Porovnávání otisků prstů podle poskytovatele v Nastavení > Zabezpečení
|
||||
🌐 **i18n (30 jazyků)** | Plná jazyková podpora dashboardu a dokumentace s psaním zprava doleva
|
||||
📂 **Adresář vlastních dat** | Přepsání `DATA_DIR` pro umístění úložiště
|
||||
| Funkce | Co to dělá |
|
||||
| ----------------------------------------------- | ------------------------------------------------------------------------- |
|
||||
| 🌐 **Nasazení kdekoli** | Localhost, VPS, Docker, cloudová prostředí |
|
||||
| 💾 **Synchronizace s cloudem** | Synchronizace konfigurace přes cloud worker |
|
||||
| 🔄 **Zálohování/Obnovení** | Toky exportu/importu a obnovy po havárii |
|
||||
| 🧙 **Průvodce nástupem** | Průvodce prvním spuštěním |
|
||||
| 🔧 **Panel nástrojů CLI** | Nastavení oblíbených kódovacích nástrojů jedním kliknutím |
|
||||
| 🎮 **Modelové hřiště** | Otestujte libovolného poskytovatele/model/koncový bod z řídicího panelu |
|
||||
| 🔏 **Přepínač otisků prstů v příkazovém řádku** | Porovnávání otisků prstů podle poskytovatele v Nastavení > Zabezpečení |
|
||||
| 🌐 **i18n (30 jazyků)** | Plná jazyková podpora dashboardu a dokumentace s psaním zprava doleva |
|
||||
| 📂 **Adresář vlastních dat** | Přepsání `DATA_DIR` pro umístění úložiště |
|
||||
|
||||
### Hluboký pohled na funkce
|
||||
|
||||
@@ -1203,12 +1115,12 @@ Předinstalovaná sada „OmniRoute Golden Set“ obsahuje testovací případy
|
||||
|
||||
### Strategie hodnocení
|
||||
|
||||
Strategie | Popis | Příklad
|
||||
--- | --- | ---
|
||||
`exact` | Výstup se musí přesně shodovat | `"4"`
|
||||
`contains` | Výstup musí obsahovat podřetězec (bez rozlišení velkých a malých písmen) | `"Paris"`
|
||||
`regex` | Výstup musí odpovídat vzoru regulárních výrazů | `"1.*2.*3"`
|
||||
`custom` | Vlastní JS funkce vrací true/false | `(output) => output.length > 10`
|
||||
| Strategie | Popis | Příklad |
|
||||
| ---------- | ------------------------------------------------------------------------ | -------------------------------- |
|
||||
| `exact` | Výstup se musí přesně shodovat | `"4"` |
|
||||
| `contains` | Výstup musí obsahovat podřetězec (bez rozlišení velkých a malých písmen) | `"Paris"` |
|
||||
| `regex` | Výstup musí odpovídat vzoru regulárních výrazů | `"1.*2.*3"` |
|
||||
| `custom` | Vlastní JS funkce vrací true/false | `(output) => output.length > 10` |
|
||||
|
||||
---
|
||||
|
||||
@@ -1240,9 +1152,6 @@ Užitečná API pro automatizaci:
|
||||
- `GET /api/mcp/audit`
|
||||
- `GET /api/mcp/audit/stats`
|
||||
|
||||
|
||||
|
||||
|
||||
<details>
|
||||
<summary><b>🤝 Nastavení A2A (Agent2Agent)</b></summary>
|
||||
</details>
|
||||
@@ -1272,9 +1181,6 @@ Provozní uživatelské rozhraní:
|
||||
|
||||
- `/dashboard/a2a` pro pozorovatelnost úloh/stavů/streamů a akce kouření
|
||||
|
||||
|
||||
|
||||
|
||||
<details>
|
||||
<summary><b>🧪 Komplexní validace protokolu</b></summary>
|
||||
</details>
|
||||
@@ -1291,9 +1197,6 @@ Tím se ověřuje:
|
||||
- A2A objevování/odesílání/streamování/získávání/zrušení
|
||||
- Křížová kontrola dat v auditu MCP a API pro správu úloh A2A
|
||||
|
||||
|
||||
|
||||
|
||||
<details>
|
||||
<summary><b>💳 Poskytovatelé předplatného</b></summary>
|
||||
</details>
|
||||
@@ -1369,9 +1272,6 @@ Models:
|
||||
gh/gemini-3-pro
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
<details>
|
||||
<summary><b>🔑 Poskytovatelé klíčů API</b></summary>
|
||||
</details>
|
||||
@@ -1381,7 +1281,7 @@ Models:
|
||||
1. Registrace: [build.nvidia.com](https://build.nvidia.com)
|
||||
2. Získejte zdarma klíč API (včetně 1000 inferenčních kreditů)
|
||||
3. Ovládací panel → Přidat poskytovatele → NVIDIA NIM:
|
||||
- Klíč API: `nvapi-your-key`
|
||||
- Klíč API: `nvapi-your-key`
|
||||
|
||||
**Modely:** `nvidia/llama-3.3-70b-instruct` , `nvidia/mistral-7b-instruct` a více než 50 dalších
|
||||
|
||||
@@ -1413,9 +1313,6 @@ Models:
|
||||
|
||||
**Modely:** Získejte přístup k více než 100 modelům od všech hlavních poskytovatelů prostřednictvím jediného klíče API.
|
||||
|
||||
|
||||
|
||||
|
||||
<details>
|
||||
<summary><b>💰 Levní poskytovatelé (záložní)</b></summary>
|
||||
</details>
|
||||
@@ -1425,8 +1322,8 @@ Models:
|
||||
1. Registrace: [Zhipu AI](https://open.bigmodel.cn/)
|
||||
2. Získejte klíč API z kódovacího plánu
|
||||
3. Nástěnka → Přidat klíč API:
|
||||
- Poskytovatel: `glm`
|
||||
- Klíč API: `your-key`
|
||||
- Poskytovatel: `glm`
|
||||
- Klíč API: `your-key`
|
||||
|
||||
**Použití:** `glm/glm-4.7`
|
||||
|
||||
@@ -1452,9 +1349,6 @@ Models:
|
||||
|
||||
**Tip pro profesionály:** Fixních 9 $/měsíc za 10 milionů tokenů = efektivní náklady 0,90 $/1 milion!
|
||||
|
||||
|
||||
|
||||
|
||||
<details>
|
||||
<summary><b>🆓 BEZPLATNÍ poskytovatelé (nouzové zálohování)</b></summary>
|
||||
</details>
|
||||
@@ -1498,9 +1392,6 @@ Models:
|
||||
kr/claude-haiku-4.5
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
<details>
|
||||
<summary><b>🎨 Vytvořte kombinace</b></summary>
|
||||
</details>
|
||||
@@ -1531,9 +1422,6 @@ Models:
|
||||
Cost: $0 forever!
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
<details>
|
||||
<summary><b>🔧 Integrace s rozhraním příkazového řádku</b></summary>
|
||||
</details>
|
||||
@@ -1637,9 +1525,6 @@ opencode
|
||||
|
||||
> **Tip:** Do sekce `models` přidejte jakýkoli model dostupný ve vašem koncovém bodu OmniRoute `/v1/models` . Použijte formát `provider/model-id` z vašeho dashboardu OmniRoute.
|
||||
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
## 🐛 Řešení problémů
|
||||
@@ -1880,14 +1765,8 @@ Chcete-li získat přístup k kriterii pověření, můžete použít adresu **U
|
||||
|
||||
> Toto řešení funguje na základě autorizačního kódu na adrese URL a nezávislého přesměrování přesměrování nebo jiného.
|
||||
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
|
||||
|
||||
|
||||
## 🛠️ Technologický stack
|
||||
|
||||
<details>
|
||||
@@ -1909,28 +1788,25 @@ Chcete-li získat přístup k kriterii pověření, můžete použít adresu **U
|
||||
- **Docker** : [hub.docker.com/r/diegosouzapw/omniroute](https://hub.docker.com/r/diegosouzapw/omniroute)
|
||||
- **Odolnost** : Jistič, exponenciální odstavení, ochrana proti hromům, falešné TLS, automatické kombinované samoopravování
|
||||
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
## 📖 Dokumentace
|
||||
|
||||
Dokument | Popis
|
||||
--- | ---
|
||||
[Uživatelská příručka](docs/USER_GUIDE.md) | Poskytovatelé, kombinace, integrace CLI, nasazení
|
||||
[Referenční informace k API](docs/API_REFERENCE.md) | Všechny koncové body s příklady
|
||||
[MCP server](open-sse/mcp-server/README.md) | 16 nástrojů MCP, konfigurace IDE, klienti Python/TS/Go
|
||||
[Server A2A](src/lib/a2a/README.md) | Protokol JSON-RPC 2.0, dovednosti, streamování, správa úloh
|
||||
[Auto-Combo Engine](docs/auto-combo.md) | 6faktorové bodování, balíčky režimů, samoléčba
|
||||
[Odstraňování problémů](docs/TROUBLESHOOTING.md) | Běžné problémy a jejich řešení
|
||||
[Architektura](docs/ARCHITECTURE.md) | Architektura a interní prvky systému
|
||||
[Přispívání](CONTRIBUTING.md) | Nastavení a pokyny pro vývoj
|
||||
[Specifikace OpenAPI](docs/openapi.yaml) | Specifikace OpenAPI 3.0
|
||||
[Bezpečnostní zásady](SECURITY.md) | Hlášení zranitelností a bezpečnostní postupy
|
||||
[Nasazení virtuálního počítače](docs/VM_DEPLOYMENT_GUIDE.md) | Kompletní průvodce: Nastavení virtuálního počítače + nginx + Cloudflare
|
||||
[Galerie funkcí](docs/FEATURES.md) | Vizuální prohlídka řídicího panelu se snímky obrazovky
|
||||
[Kontrolní seznam vydání](docs/RELEASE_CHECKLIST.md) | Kroky ověření před vydáním
|
||||
| Dokument | Popis |
|
||||
| ------------------------------------------------------------ | ----------------------------------------------------------------------- |
|
||||
| [Uživatelská příručka](docs/USER_GUIDE.md) | Poskytovatelé, kombinace, integrace CLI, nasazení |
|
||||
| [Referenční informace k API](docs/API_REFERENCE.md) | Všechny koncové body s příklady |
|
||||
| [MCP server](open-sse/mcp-server/README.md) | 16 nástrojů MCP, konfigurace IDE, klienti Python/TS/Go |
|
||||
| [Server A2A](src/lib/a2a/README.md) | Protokol JSON-RPC 2.0, dovednosti, streamování, správa úloh |
|
||||
| [Auto-Combo Engine](docs/auto-combo.md) | 6faktorové bodování, balíčky režimů, samoléčba |
|
||||
| [Odstraňování problémů](docs/TROUBLESHOOTING.md) | Běžné problémy a jejich řešení |
|
||||
| [Architektura](docs/ARCHITECTURE.md) | Architektura a interní prvky systému |
|
||||
| [Přispívání](CONTRIBUTING.md) | Nastavení a pokyny pro vývoj |
|
||||
| [Specifikace OpenAPI](docs/openapi.yaml) | Specifikace OpenAPI 3.0 |
|
||||
| [Bezpečnostní zásady](SECURITY.md) | Hlášení zranitelností a bezpečnostní postupy |
|
||||
| [Nasazení virtuálního počítače](docs/VM_DEPLOYMENT_GUIDE.md) | Kompletní průvodce: Nastavení virtuálního počítače + nginx + Cloudflare |
|
||||
| [Galerie funkcí](docs/FEATURES.md) | Vizuální prohlídka řídicího panelu se snímky obrazovky |
|
||||
| [Kontrolní seznam vydání](docs/RELEASE_CHECKLIST.md) | Kroky ověření před vydáním |
|
||||
|
||||
---
|
||||
|
||||
@@ -1938,14 +1814,14 @@ Dokument | Popis
|
||||
|
||||
OmniRoute má **v plánu více než 210 funkcí** v několika fázích vývoje. Zde jsou klíčové oblasti:
|
||||
|
||||
Kategorie | Plánované funkce | Hlavní body
|
||||
--- | --- | ---
|
||||
🧠 **Směrování a inteligence** | 25+ | Směrování s nejnižší latencí, směrování založené na tagech, kontrola kvót před výstupem, výběr účtu P2C
|
||||
🔒 **Zabezpečení a dodržování předpisů** | 20+ | Zpevnění SSRF, maskování přihlašovacích údajů, limit rychlosti pro každý koncový bod, stanovení rozsahu klíčů pro správu
|
||||
📊 **Pozorovatelnost** | 15+ | Integrace OpenTelemetry, sledování kvót v reálném čase, sledování nákladů podle modelu
|
||||
🔄 **Integrace poskytovatelů** | 20+ | Dynamický registr modelů, doba zchlazení poskytovatelů, Codex pro více účtů, analýza kvót Copilota
|
||||
⚡ **Výkon** | 15+ | Dvojitá vrstva mezipaměti, mezipaměť výzev, mezipaměť odpovědí, udržování streamování, dávkové API
|
||||
🌐 **Ekosystém** | 10+ | WebSocket API, horké opětovné načítání konfigurace, distribuované úložiště konfigurace, komerční režim
|
||||
| Kategorie | Plánované funkce | Hlavní body |
|
||||
| ---------------------------------------- | ---------------- | ------------------------------------------------------------------------------------------------------------------------ |
|
||||
| 🧠 **Směrování a inteligence** | 25+ | Směrování s nejnižší latencí, směrování založené na tagech, kontrola kvót před výstupem, výběr účtu P2C |
|
||||
| 🔒 **Zabezpečení a dodržování předpisů** | 20+ | Zpevnění SSRF, maskování přihlašovacích údajů, limit rychlosti pro každý koncový bod, stanovení rozsahu klíčů pro správu |
|
||||
| 📊 **Pozorovatelnost** | 15+ | Integrace OpenTelemetry, sledování kvót v reálném čase, sledování nákladů podle modelu |
|
||||
| 🔄 **Integrace poskytovatelů** | 20+ | Dynamický registr modelů, doba zchlazení poskytovatelů, Codex pro více účtů, analýza kvót Copilota |
|
||||
| ⚡ **Výkon** | 15+ | Dvojitá vrstva mezipaměti, mezipaměť výzev, mezipaměť odpovědí, udržování streamování, dávkové API |
|
||||
| 🌐 **Ekosystém** | 10+ | WebSocket API, horké opětovné načítání konfigurace, distribuované úložiště konfigurace, komerční režim |
|
||||
|
||||
### 🔜 Již brzy
|
||||
|
||||
|
||||
@@ -934,8 +934,8 @@ OmniRoute v2.0 er bygget som en operationel platform, ikke kun en relæ-proxy.
|
||||
| Funktion | Hvad det gør || -------------------------- | -------------------------------------------------------------------- |
|
||||
| 🖼️ **Billedgenerering** | `/v1/images/generations` med cloud og lokale backends |
|
||||
| 📐 **Indlejringer** | `/v1/embeddings` til søgning og RAG-rørledninger |
|
||||
| 🎤 **Lydtransskription** | `/v1/audio/transcriptions` (Whisper og yderligere udbydere) |
|
||||
| 🔊 **Tekst-til-tale** | `/v1/audio/speech` (flere motorer/udbydere) |
|
||||
| 🎤 **Lydtransskription** | `/v1/audio/transcriptions` — 7 providers (Deepgram Nova 3, AssemblyAI, Groq Whisper, HuggingFace, ElevenLabs, OpenAI, Azure), auto-language detection, MP4/MP3/WAV support |
|
||||
| 🔊 **Tekst-til-tale** | `/v1/audio/speech` — 10 providers (ElevenLabs, OpenAI, Deepgram, Cartesia, PlayHT, HuggingFace, Nvidia NIM, Inworld, Coqui, Tortoise) |
|
||||
| 🎬 **Videogenerering** | `/v1/videos/generations` (ComfyUI + SD WebUI-arbejdsgange) |
|
||||
| 🎵 **Music Generation** | `/v1/music/generations` (ComfyUI-arbejdsgange) |
|
||||
| 🛡️ **Moderationer** | `/v1/moderations` sikkerhedstjek |
|
||||
|
||||
@@ -939,8 +939,8 @@ OmniRoute v2.0 ist als Betriebsplattform konzipiert und nicht nur als Relay-Prox
|
||||
| Funktion | Was es tut || -------------------------- | ------------------------------------------------------------- |
|
||||
| 🖼️ **Bilderzeugung** | `/v1/images/generations` mit Cloud- und lokalen Backends |
|
||||
| 📐 **Einbettungen** | `/v1/embeddings` für Such- und RAG-Pipelines |
|
||||
| 🎤 **Audio-Transkription** | `/v1/audio/transcriptions` (Whisper und zusätzliche Anbieter) |
|
||||
| 🔊 **Text-to-Speech** | `/v1/audio/speech` (mehrere Engines/Anbieter) |
|
||||
| 🎤 **Audio-Transkription** | `/v1/audio/transcriptions` — 7 providers (Deepgram Nova 3, AssemblyAI, Groq Whisper, HuggingFace, ElevenLabs, OpenAI, Azure), auto-language detection, MP4/MP3/WAV support |
|
||||
| 🔊 **Text-to-Speech** | `/v1/audio/speech` — 10 providers (ElevenLabs, OpenAI, Deepgram, Cartesia, PlayHT, HuggingFace, Nvidia NIM, Inworld, Coqui, Tortoise) |
|
||||
| 🎬 **Videogenerierung** | `/v1/videos/generations` (ComfyUI + SD WebUI-Workflows) |
|
||||
| 🎵 **Musikgeneration** | `/v1/music/generations` (ComfyUI-Workflows) |
|
||||
| 🛡️ **Moderationen** | `/v1/moderations` Sicherheitsprüfungen |
|
||||
|
||||
@@ -877,14 +877,14 @@ npm run electron:build:linux # Linux (.AppImage)
|
||||
|
||||
### 🎵 APIs Multi-Modal
|
||||
|
||||
| Característica | Qué Hace |
|
||||
| ----------------------------- | ------------------------------------------------------ |
|
||||
| 🖼️ **Generación de Imágenes** | `/v1/images/generations` — 4 proveedores, 9+ modelos |
|
||||
| 📐 **Embeddings** | `/v1/embeddings` — 6 proveedores, 9+ modelos |
|
||||
| 🎤 **Transcripción de Audio** | `/v1/audio/transcriptions` — Compatible con Whisper |
|
||||
| 🔊 **Texto a Voz** | `/v1/audio/speech` — Síntesis de audio multi-proveedor |
|
||||
| 🛡️ **Moderaciones** | `/v1/moderations` — Verificaciones de seguridad |
|
||||
| 🔀 **Reranking** | `/v1/rerank` — Reranking de relevancia de documentos |
|
||||
| Característica | Qué Hace |
|
||||
| ----------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| 🖼️ **Generación de Imágenes** | `/v1/images/generations` — 4 proveedores, 9+ modelos |
|
||||
| 📐 **Embeddings** | `/v1/embeddings` — 6 proveedores, 9+ modelos |
|
||||
| 🎤 **Transcripción de Audio** | `/v1/audio/transcriptions` — 7 providers (Deepgram Nova 3, AssemblyAI, Groq Whisper, HuggingFace, ElevenLabs, OpenAI, Azure), auto-language detection, MP4/MP3/WAV support |
|
||||
| 🔊 **Texto a Voz** | `/v1/audio/speech` — 10 providers (ElevenLabs, OpenAI, Deepgram, Cartesia, PlayHT, HuggingFace, Nvidia NIM, Inworld, Coqui, Tortoise) |
|
||||
| 🛡️ **Moderaciones** | `/v1/moderations` — Verificaciones de seguridad |
|
||||
| 🔀 **Reranking** | `/v1/rerank` — Reranking de relevancia de documentos |
|
||||
|
||||
### 🛡️ Resiliencia y Seguridad
|
||||
|
||||
|
||||
@@ -874,14 +874,14 @@ npm run electron:build:linux # Linux (.AppImage)
|
||||
|
||||
### 🎵 Multimodaaliset sovellusliittymät
|
||||
|
||||
| Ominaisuus | Mitä se tekee |
|
||||
| ------------------------- | --------------------------------------------------------- |
|
||||
| 🖼️ **Kuvan luominen** | `/v1/images/generations` — 4 toimittajaa, 9+ mallia |
|
||||
| 📐 **Upotukset** | `/v1/embeddings` — 6 toimittajaa, 9+ mallia |
|
||||
| 🎤 **Äänitranskriptio** | `/v1/audio/transcriptions` — Kuiskausyhteensopiva |
|
||||
| 🔊 **Tekstistä puheeksi** | `/v1/audio/speech` — Usean palveluntarjoajan äänisynteesi |
|
||||
| 🛡️ **Moderaatiot** | `/v1/moderations` — Sisällön turvallisuustarkistukset |
|
||||
| 🔀 **Uudelleenjärjestys** | `/v1/rerank` — Asiakirjan osuvuuden uudelleensijoitus |
|
||||
| Ominaisuus | Mitä se tekee |
|
||||
| ------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| 🖼️ **Kuvan luominen** | `/v1/images/generations` — 4 toimittajaa, 9+ mallia |
|
||||
| 📐 **Upotukset** | `/v1/embeddings` — 6 toimittajaa, 9+ mallia |
|
||||
| 🎤 **Äänitranskriptio** | `/v1/audio/transcriptions` — 7 providers (Deepgram Nova 3, AssemblyAI, Groq Whisper, HuggingFace, ElevenLabs, OpenAI, Azure), auto-language detection, MP4/MP3/WAV support |
|
||||
| 🔊 **Tekstistä puheeksi** | `/v1/audio/speech` — 10 providers (ElevenLabs, OpenAI, Deepgram, Cartesia, PlayHT, HuggingFace, Nvidia NIM, Inworld, Coqui, Tortoise) |
|
||||
| 🛡️ **Moderaatiot** | `/v1/moderations` — Sisällön turvallisuustarkistukset |
|
||||
| 🔀 **Uudelleenjärjestys** | `/v1/rerank` — Asiakirjan osuvuuden uudelleensijoitus |
|
||||
|
||||
### 🛡️ Joustavuus ja turvallisuus
|
||||
|
||||
|
||||
@@ -875,14 +875,14 @@ npm run electron:build:linux # Linux (.AppImage)
|
||||
|
||||
### 🎵 APIs multi-modales
|
||||
|
||||
| Fonctionnalité | Ce qu'elle fait |
|
||||
| -------------------------- | ------------------------------------------------------- |
|
||||
| 🖼️ **Génération d'images** | `/v1/images/generations` — 4 fournisseurs, 9+ modèles |
|
||||
| 📐 **Embeddings** | `/v1/embeddings` — 6 fournisseurs, 9+ modèles |
|
||||
| 🎤 **Transcription audio** | `/v1/audio/transcriptions` — compatible Whisper |
|
||||
| 🔊 **Texte vers parole** | `/v1/audio/speech` — synthèse audio multi-fournisseur |
|
||||
| 🛡️ **Modérations** | `/v1/moderations` — vérifications de sécurité |
|
||||
| 🔀 **Reranking** | `/v1/rerank` — reclassement de pertinence des documents |
|
||||
| Fonctionnalité | Ce qu'elle fait |
|
||||
| -------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| 🖼️ **Génération d'images** | `/v1/images/generations` — 4 fournisseurs, 9+ modèles |
|
||||
| 📐 **Embeddings** | `/v1/embeddings` — 6 fournisseurs, 9+ modèles |
|
||||
| 🎤 **Transcription audio** | `/v1/audio/transcriptions` — 7 providers (Deepgram Nova 3, AssemblyAI, Groq Whisper, HuggingFace, ElevenLabs, OpenAI, Azure), auto-language detection, MP4/MP3/WAV support |
|
||||
| 🔊 **Texte vers parole** | `/v1/audio/speech` — 10 providers (ElevenLabs, OpenAI, Deepgram, Cartesia, PlayHT, HuggingFace, Nvidia NIM, Inworld, Coqui, Tortoise) |
|
||||
| 🛡️ **Modérations** | `/v1/moderations` — vérifications de sécurité |
|
||||
| 🔀 **Reranking** | `/v1/rerank` — reclassement de pertinence des documents |
|
||||
|
||||
### 🛡️ Résilience & Sécurité
|
||||
|
||||
|
||||
@@ -873,14 +873,14 @@ npm run electron:build:linux # Linux (.AppImage)
|
||||
|
||||
### 🎵 ממשקי API רב-מודאליים
|
||||
|
||||
| תכונה | מה זה עושה |
|
||||
| ------------------- | --------------------------------------------- |
|
||||
| 🖼️ **יצירת תמונות** | `/v1/images/generations` — 4 ספקים, 9+ דגמים |
|
||||
| 📐 **הטבעות** | `/v1/embeddings` — 6 ספקים, 9+ דגמים |
|
||||
| 🎤 **תמלול אודיו** | `/v1/audio/transcriptions` — תואם לחישה |
|
||||
| 🔊 **טקסט לדיבור** | `/v1/audio/speech` — סינתזת אודיו מרובה ספקים |
|
||||
| 🛡️ **מנחים** | `/v1/moderations` — בדיקות בטיחות תוכן |
|
||||
| 🔀 **דירוג מחדש** | `/v1/rerank` — דירוג מחדש של רלוונטיות המסמך |
|
||||
| תכונה | מה זה עושה |
|
||||
| ------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| 🖼️ **יצירת תמונות** | `/v1/images/generations` — 4 ספקים, 9+ דגמים |
|
||||
| 📐 **הטבעות** | `/v1/embeddings` — 6 ספקים, 9+ דגמים |
|
||||
| 🎤 **תמלול אודיו** | `/v1/audio/transcriptions` — 7 providers (Deepgram Nova 3, AssemblyAI, Groq Whisper, HuggingFace, ElevenLabs, OpenAI, Azure), auto-language detection, MP4/MP3/WAV support |
|
||||
| 🔊 **טקסט לדיבור** | `/v1/audio/speech` — 10 providers (ElevenLabs, OpenAI, Deepgram, Cartesia, PlayHT, HuggingFace, Nvidia NIM, Inworld, Coqui, Tortoise) |
|
||||
| 🛡️ **מנחים** | `/v1/moderations` — בדיקות בטיחות תוכן |
|
||||
| 🔀 **דירוג מחדש** | `/v1/rerank` — דירוג מחדש של רלוונטיות המסמך |
|
||||
|
||||
### 🛡️ חוסן וביטחון
|
||||
|
||||
|
||||
@@ -874,14 +874,14 @@ npm run electron:build:linux # Linux (.AppImage)
|
||||
|
||||
### 🎵 Multimodális API-k
|
||||
|
||||
| Funkció | Mit csinál |
|
||||
| ---------------------- | ------------------------------------------------------------ |
|
||||
| 🖼️ **Képgenerálás** | `/v1/images/generations` — 4 szolgáltató, 9+ modell |
|
||||
| 📐 **Beágyazás** | `/v1/embeddings` — 6 szolgáltató, 9+ modell |
|
||||
| 🎤 **Audio átírás** | `/v1/audio/transcriptions` — Suttogás-kompatibilis |
|
||||
| 🔊 **Szövegfelolvasó** | `/v1/audio/speech` — Hangszintézis több szolgáltatónál |
|
||||
| 🛡️ **Moderálás** | `/v1/moderations` — Tartalombiztonsági ellenőrzések |
|
||||
| 🔀 **Átsorolás** | `/v1/rerank` — A dokumentumok relevancia szerinti átsorolása |
|
||||
| Funkció | Mit csinál |
|
||||
| ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| 🖼️ **Képgenerálás** | `/v1/images/generations` — 4 szolgáltató, 9+ modell |
|
||||
| 📐 **Beágyazás** | `/v1/embeddings` — 6 szolgáltató, 9+ modell |
|
||||
| 🎤 **Audio átírás** | `/v1/audio/transcriptions` — 7 providers (Deepgram Nova 3, AssemblyAI, Groq Whisper, HuggingFace, ElevenLabs, OpenAI, Azure), auto-language detection, MP4/MP3/WAV support |
|
||||
| 🔊 **Szövegfelolvasó** | `/v1/audio/speech` — 10 providers (ElevenLabs, OpenAI, Deepgram, Cartesia, PlayHT, HuggingFace, Nvidia NIM, Inworld, Coqui, Tortoise) |
|
||||
| 🛡️ **Moderálás** | `/v1/moderations` — Tartalombiztonsági ellenőrzések |
|
||||
| 🔀 **Átsorolás** | `/v1/rerank` — A dokumentumok relevancia szerinti átsorolása |
|
||||
|
||||
### 🛡️ Rugalmasság és biztonság
|
||||
|
||||
|
||||
@@ -874,14 +874,14 @@ npm run electron:build:linux # Linux (.AppImage)
|
||||
|
||||
### 🎵 API Multi-Modal
|
||||
|
||||
| Fitur | Apa Fungsinya |
|
||||
| -------------------------- | ------------------------------------------------------ |
|
||||
| 🖼️ **Pembuatan Gambar** | `/v1/images/generations` — 4 penyedia, 9+ model |
|
||||
| 📐 **Sematan** | `/v1/embeddings` — 6 penyedia, 9+ model |
|
||||
| 🎤 **Transkripsi Audio** | `/v1/audio/transcriptions` — Kompatibel dengan bisikan |
|
||||
| 🔊 **Teks-ke-Ucapan** | `/v1/audio/speech` — Sintesis audio multi-penyedia |
|
||||
| 🛡️ **Moderasi** | `/v1/moderations` — Pemeriksaan keamanan konten |
|
||||
| 🔀 **Pemeringkatan Ulang** | `/v1/rerank` — Pemeringkatan ulang relevansi dokumen |
|
||||
| Fitur | Apa Fungsinya |
|
||||
| -------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| 🖼️ **Pembuatan Gambar** | `/v1/images/generations` — 4 penyedia, 9+ model |
|
||||
| 📐 **Sematan** | `/v1/embeddings` — 6 penyedia, 9+ model |
|
||||
| 🎤 **Transkripsi Audio** | `/v1/audio/transcriptions` — 7 providers (Deepgram Nova 3, AssemblyAI, Groq Whisper, HuggingFace, ElevenLabs, OpenAI, Azure), auto-language detection, MP4/MP3/WAV support |
|
||||
| 🔊 **Teks-ke-Ucapan** | `/v1/audio/speech` — 10 providers (ElevenLabs, OpenAI, Deepgram, Cartesia, PlayHT, HuggingFace, Nvidia NIM, Inworld, Coqui, Tortoise) |
|
||||
| 🛡️ **Moderasi** | `/v1/moderations` — Pemeriksaan keamanan konten |
|
||||
| 🔀 **Pemeringkatan Ulang** | `/v1/rerank` — Pemeringkatan ulang relevansi dokumen |
|
||||
|
||||
### 🛡️ Ketahanan & Keamanan
|
||||
|
||||
|
||||
@@ -770,14 +770,14 @@ npm run electron:build:linux # Linux (.AppImage)
|
||||
|
||||
### 🎵 मल्टी-मॉडल एपीआई
|
||||
|
||||
| फ़ीचर | यह क्या करता है |
|
||||
| ---------------------------- | ------------------------------------------------- |
|
||||
| 🖼️ **छवि निर्माण** | `/v1/images/generations` - 4 प्रदाता, 9+ मॉडल |
|
||||
| 📐 **एंबेडिंग** | `/v1/embeddings` — 6 प्रदाता, 9+ मॉडल |
|
||||
| 🎤 **ऑडियो ट्रांस्क्रिप्शन** | `/v1/audio/transcriptions` - कानाफूसी-संगत |
|
||||
| 🔊 **टेक्स्ट-टू-स्पीच** | `/v1/audio/speech` - बहु-प्रदाता ऑडियो संश्लेषण |
|
||||
| 🛡️ **संयम** | `/v1/moderations` — सामग्री सुरक्षा जांच |
|
||||
| 🔀 **पुनर्रैंकिंग** | `/v1/rerank` — दस्तावेज़ प्रासंगिकता पुनर्रैंकिंग |
|
||||
| फ़ीचर | यह क्या करता है |
|
||||
| ---------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| 🖼️ **छवि निर्माण** | `/v1/images/generations` - 4 प्रदाता, 9+ मॉडल |
|
||||
| 📐 **एंबेडिंग** | `/v1/embeddings` — 6 प्रदाता, 9+ मॉडल |
|
||||
| 🎤 **ऑडियो ट्रांस्क्रिप्शन** | `/v1/audio/transcriptions` — 7 providers (Deepgram Nova 3, AssemblyAI, Groq Whisper, HuggingFace, ElevenLabs, OpenAI, Azure), auto-language detection, MP4/MP3/WAV support |
|
||||
| 🔊 **टेक्स्ट-टू-स्पीच** | `/v1/audio/speech` — 10 providers (ElevenLabs, OpenAI, Deepgram, Cartesia, PlayHT, HuggingFace, Nvidia NIM, Inworld, Coqui, Tortoise) |
|
||||
| 🛡️ **संयम** | `/v1/moderations` — सामग्री सुरक्षा जांच |
|
||||
| 🔀 **पुनर्रैंकिंग** | `/v1/rerank` — दस्तावेज़ प्रासंगिकता पुनर्रैंकिंग |
|
||||
|
||||
### 🛡️ लचीलापन और सुरक्षा
|
||||
|
||||
|
||||
@@ -874,14 +874,14 @@ npm run electron:build:linux # Linux (.AppImage)
|
||||
|
||||
### 🎵 API Multi-modali
|
||||
|
||||
| Funzionalità | Cosa Fa |
|
||||
| --------------------------- | ---------------------------------------------------- |
|
||||
| 🖼️ **Generazione immagini** | `/v1/images/generations` — 4 provider, 9+ modelli |
|
||||
| 📐 **Embeddings** | `/v1/embeddings` — 6 provider, 9+ modelli |
|
||||
| 🎤 **Trascrizione audio** | `/v1/audio/transcriptions` — Compatibile Whisper |
|
||||
| 🔊 **Testo a voce** | `/v1/audio/speech` — Sintesi audio multi-provider |
|
||||
| 🛡️ **Moderazioni** | `/v1/moderations` — Controlli di sicurezza |
|
||||
| 🔀 **Reranking** | `/v1/rerank` — Riclassificazione rilevanza documenti |
|
||||
| Funzionalità | Cosa Fa |
|
||||
| --------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| 🖼️ **Generazione immagini** | `/v1/images/generations` — 4 provider, 9+ modelli |
|
||||
| 📐 **Embeddings** | `/v1/embeddings` — 6 provider, 9+ modelli |
|
||||
| 🎤 **Trascrizione audio** | `/v1/audio/transcriptions` — 7 providers (Deepgram Nova 3, AssemblyAI, Groq Whisper, HuggingFace, ElevenLabs, OpenAI, Azure), auto-language detection, MP4/MP3/WAV support |
|
||||
| 🔊 **Testo a voce** | `/v1/audio/speech` — 10 providers (ElevenLabs, OpenAI, Deepgram, Cartesia, PlayHT, HuggingFace, Nvidia NIM, Inworld, Coqui, Tortoise) |
|
||||
| 🛡️ **Moderazioni** | `/v1/moderations` — Controlli di sicurezza |
|
||||
| 🔀 **Reranking** | `/v1/rerank` — Riclassificazione rilevanza documenti |
|
||||
|
||||
### 🛡️ Resilienza & Sicurezza
|
||||
|
||||
|
||||
@@ -874,14 +874,14 @@ npm run electron:build:linux # Linux (.AppImage)
|
||||
|
||||
### 🎵 マルチモーダル API
|
||||
|
||||
| 特集 | 何をするのか |
|
||||
| ----------------------- | --------------------------------------------------------------- |
|
||||
| 🖼️ **画像生成** | `/v1/images/generations` — 4 つのプロバイダー、9 つ以上のモデル |
|
||||
| 📐 **埋め込み** | `/v1/embeddings` — 6 つのプロバイダー、9 つ以上のモデル |
|
||||
| 🎤 **音声文字起こし** | `/v1/audio/transcriptions` — ウィスパー互換 |
|
||||
| 🔊 **テキスト読み上げ** | `/v1/audio/speech` — マルチプロバイダーのオーディオ合成 |
|
||||
| 🛡️ **モデレーション** | `/v1/moderations` — コンテンツの安全性チェック |
|
||||
| 🔀 **再ランキング** | `/v1/rerank` — ドキュメントの関連性の再ランキング |
|
||||
| 特集 | 何をするのか |
|
||||
| ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| 🖼️ **画像生成** | `/v1/images/generations` — 4 つのプロバイダー、9 つ以上のモデル |
|
||||
| 📐 **埋め込み** | `/v1/embeddings` — 6 つのプロバイダー、9 つ以上のモデル |
|
||||
| 🎤 **音声文字起こし** | `/v1/audio/transcriptions` — 7 providers (Deepgram Nova 3, AssemblyAI, Groq Whisper, HuggingFace, ElevenLabs, OpenAI, Azure), auto-language detection, MP4/MP3/WAV support |
|
||||
| 🔊 **テキスト読み上げ** | `/v1/audio/speech` — 10 providers (ElevenLabs, OpenAI, Deepgram, Cartesia, PlayHT, HuggingFace, Nvidia NIM, Inworld, Coqui, Tortoise) |
|
||||
| 🛡️ **モデレーション** | `/v1/moderations` — コンテンツの安全性チェック |
|
||||
| 🔀 **再ランキング** | `/v1/rerank` — ドキュメントの関連性の再ランキング |
|
||||
|
||||
### 🛡️ 復元力とセキュリティ
|
||||
|
||||
|
||||
@@ -873,14 +873,14 @@ npm run electron:build:linux # Linux (.AppImage)
|
||||
|
||||
### 🎵 다중 모드 API
|
||||
|
||||
| 기능 | 그것이 하는 일 |
|
||||
| ----------------------- | ------------------------------------------------------ |
|
||||
| 🖼️ **이미지 생성** | `/v1/images/generations` — 4개 공급자, 9개 이상의 모델 |
|
||||
| 📐 **임베딩** | `/v1/embeddings` — 6개 공급자, 9개 이상의 모델 |
|
||||
| 🎤 **오디오 전사** | `/v1/audio/transcriptions` — 속삭임 호환 |
|
||||
| 🔊 **텍스트 음성 변환** | `/v1/audio/speech` — 다중 제공자 오디오 합성 |
|
||||
| 🛡️ **조정** | `/v1/moderations` — 콘텐츠 안전 확인 |
|
||||
| 🔀 **재순위** | `/v1/rerank` — 문서 관련성 재순위 |
|
||||
| 기능 | 그것이 하는 일 |
|
||||
| ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| 🖼️ **이미지 생성** | `/v1/images/generations` — 4개 공급자, 9개 이상의 모델 |
|
||||
| 📐 **임베딩** | `/v1/embeddings` — 6개 공급자, 9개 이상의 모델 |
|
||||
| 🎤 **오디오 전사** | `/v1/audio/transcriptions` — 7 providers (Deepgram Nova 3, AssemblyAI, Groq Whisper, HuggingFace, ElevenLabs, OpenAI, Azure), auto-language detection, MP4/MP3/WAV support |
|
||||
| 🔊 **텍스트 음성 변환** | `/v1/audio/speech` — 10 providers (ElevenLabs, OpenAI, Deepgram, Cartesia, PlayHT, HuggingFace, Nvidia NIM, Inworld, Coqui, Tortoise) |
|
||||
| 🛡️ **조정** | `/v1/moderations` — 콘텐츠 안전 확인 |
|
||||
| 🔀 **재순위** | `/v1/rerank` — 문서 관련성 재순위 |
|
||||
|
||||
### 🛡️ 복원력 및 보안
|
||||
|
||||
|
||||
@@ -873,14 +873,14 @@ npm run electron:build:linux # Linux (.AppImage)
|
||||
|
||||
### 🎵 API Berbilang Modal
|
||||
|
||||
| Ciri | Apa yang Dilakukan |
|
||||
| ------------------------ | ------------------------------------------------------ |
|
||||
| 🖼️ **Penjanaan Imej** | `/v1/images/generations` — 4 pembekal, 9+ model |
|
||||
| 📐 **Pembenaman** | `/v1/embeddings` — 6 pembekal, 9+ model |
|
||||
| 🎤 **Transkripsi Audio** | `/v1/audio/transcriptions` — Serasi dengan bisikan |
|
||||
| 🔊 **Teks-ke-Ucapan** | `/v1/audio/speech` — Sintesis audio berbilang pembekal |
|
||||
| 🛡️ **Kesederhanaan** | `/v1/moderations` — Pemeriksaan keselamatan kandungan |
|
||||
| 🔀 **Penyusunan semula** | `/v1/rerank` — Penarafan semula perkaitan dokumen |
|
||||
| Ciri | Apa yang Dilakukan |
|
||||
| ------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| 🖼️ **Penjanaan Imej** | `/v1/images/generations` — 4 pembekal, 9+ model |
|
||||
| 📐 **Pembenaman** | `/v1/embeddings` — 6 pembekal, 9+ model |
|
||||
| 🎤 **Transkripsi Audio** | `/v1/audio/transcriptions` — 7 providers (Deepgram Nova 3, AssemblyAI, Groq Whisper, HuggingFace, ElevenLabs, OpenAI, Azure), auto-language detection, MP4/MP3/WAV support |
|
||||
| 🔊 **Teks-ke-Ucapan** | `/v1/audio/speech` — 10 providers (ElevenLabs, OpenAI, Deepgram, Cartesia, PlayHT, HuggingFace, Nvidia NIM, Inworld, Coqui, Tortoise) |
|
||||
| 🛡️ **Kesederhanaan** | `/v1/moderations` — Pemeriksaan keselamatan kandungan |
|
||||
| 🔀 **Penyusunan semula** | `/v1/rerank` — Penarafan semula perkaitan dokumen |
|
||||
|
||||
### 🛡️ Ketahanan & Keselamatan
|
||||
|
||||
|
||||
@@ -873,14 +873,14 @@ npm run electron:build:linux # Linux (.AppImage)
|
||||
|
||||
### 🎵 Multimodale API's
|
||||
|
||||
| Kenmerk | Wat het doet |
|
||||
| ------------------------ | --------------------------------------------------------- |
|
||||
| 🖼️ **Beeldgeneratie** | `/v1/images/generations` — 4 providers, 9+ modellen |
|
||||
| 📐 **Insluitingen** | `/v1/embeddings` — 6 providers, 9+ modellen |
|
||||
| 🎤 **Audiotranscriptie** | `/v1/audio/transcriptions` — Whisper-compatibel |
|
||||
| 🔊 **Tekst-naar-spraak** | `/v1/audio/speech` — Audiosynthese van meerdere providers |
|
||||
| 🛡️ **Moderaties** | `/v1/moderations` — Veiligheidscontroles van inhoud |
|
||||
| 🔀 **Herschikking** | `/v1/rerank` — Herschikking van documentrelevantie |
|
||||
| Kenmerk | Wat het doet |
|
||||
| ------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| 🖼️ **Beeldgeneratie** | `/v1/images/generations` — 4 providers, 9+ modellen |
|
||||
| 📐 **Insluitingen** | `/v1/embeddings` — 6 providers, 9+ modellen |
|
||||
| 🎤 **Audiotranscriptie** | `/v1/audio/transcriptions` — 7 providers (Deepgram Nova 3, AssemblyAI, Groq Whisper, HuggingFace, ElevenLabs, OpenAI, Azure), auto-language detection, MP4/MP3/WAV support |
|
||||
| 🔊 **Tekst-naar-spraak** | `/v1/audio/speech` — 10 providers (ElevenLabs, OpenAI, Deepgram, Cartesia, PlayHT, HuggingFace, Nvidia NIM, Inworld, Coqui, Tortoise) |
|
||||
| 🛡️ **Moderaties** | `/v1/moderations` — Veiligheidscontroles van inhoud |
|
||||
| 🔀 **Herschikking** | `/v1/rerank` — Herschikking van documentrelevantie |
|
||||
|
||||
### 🛡️ Veerkracht en veiligheid
|
||||
|
||||
|
||||
@@ -873,14 +873,14 @@ npm run electron:build:linux # Linux (.AppImage)
|
||||
|
||||
### 🎵 Multi-Modal APIer
|
||||
|
||||
| Funksjon | Hva det gjør |
|
||||
| ----------------------- | ------------------------------------------------------ |
|
||||
| 🖼️ **Bildegenerering** | `/v1/images/generations` — 4 leverandører, 9+ modeller |
|
||||
| 📐 **Innbygging** | `/v1/embeddings` — 6 leverandører, 9+ modeller |
|
||||
| 🎤 **Lydtranskripsjon** | `/v1/audio/transcriptions` — Whisper-kompatibel |
|
||||
| 🔊 **Tekst-til-tale** | `/v1/audio/speech` — Multi-leverandør lydsyntese |
|
||||
| 🛡️ **Moderasjoner** | `/v1/moderations` — Innholdssikkerhetssjekker |
|
||||
| 🔀 **Omrangering** | `/v1/rerank` — Rerangering av dokumentrelevans |
|
||||
| Funksjon | Hva det gjør |
|
||||
| ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| 🖼️ **Bildegenerering** | `/v1/images/generations` — 4 leverandører, 9+ modeller |
|
||||
| 📐 **Innbygging** | `/v1/embeddings` — 6 leverandører, 9+ modeller |
|
||||
| 🎤 **Lydtranskripsjon** | `/v1/audio/transcriptions` — 7 providers (Deepgram Nova 3, AssemblyAI, Groq Whisper, HuggingFace, ElevenLabs, OpenAI, Azure), auto-language detection, MP4/MP3/WAV support |
|
||||
| 🔊 **Tekst-til-tale** | `/v1/audio/speech` — 10 providers (ElevenLabs, OpenAI, Deepgram, Cartesia, PlayHT, HuggingFace, Nvidia NIM, Inworld, Coqui, Tortoise) |
|
||||
| 🛡️ **Moderasjoner** | `/v1/moderations` — Innholdssikkerhetssjekker |
|
||||
| 🔀 **Omrangering** | `/v1/rerank` — Rerangering av dokumentrelevans |
|
||||
|
||||
### 🛡️ Spenst og sikkerhet
|
||||
|
||||
|
||||
@@ -873,14 +873,14 @@ npm run electron:build:linux # Linux (.AppImage)
|
||||
|
||||
### 🎵 Mga Multi-Modal na API
|
||||
|
||||
| Tampok | Ano ang Ginagawa Nito |
|
||||
| -------------------------- | ------------------------------------------------------------ |
|
||||
| 🖼️ **Pagbuo ng Larawan** | `/v1/images/generations` — 4 na provider, 9+ na modelo |
|
||||
| 📐 **Mga Pag-embed** | `/v1/embeddings` — 6 na provider, 9+ na modelo |
|
||||
| 🎤 **Audio Transcription** | `/v1/audio/transcriptions` — Whisper-compatible |
|
||||
| 🔊 **Text-to-Speech** | `/v1/audio/speech` — Multi-provider audio synthesis |
|
||||
| 🛡️ **Mga Pag-moderate** | `/v1/moderations` — Mga pagsusuri sa kaligtasan ng nilalaman |
|
||||
| 🔀 **Reranking** | `/v1/rerank` — Muling pagraranggo ng kaugnayan ng dokumento |
|
||||
| Tampok | Ano ang Ginagawa Nito |
|
||||
| -------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| 🖼️ **Pagbuo ng Larawan** | `/v1/images/generations` — 4 na provider, 9+ na modelo |
|
||||
| 📐 **Mga Pag-embed** | `/v1/embeddings` — 6 na provider, 9+ na modelo |
|
||||
| 🎤 **Audio Transcription** | `/v1/audio/transcriptions` — 7 providers (Deepgram Nova 3, AssemblyAI, Groq Whisper, HuggingFace, ElevenLabs, OpenAI, Azure), auto-language detection, MP4/MP3/WAV support |
|
||||
| 🔊 **Text-to-Speech** | `/v1/audio/speech` — 10 providers (ElevenLabs, OpenAI, Deepgram, Cartesia, PlayHT, HuggingFace, Nvidia NIM, Inworld, Coqui, Tortoise) |
|
||||
| 🛡️ **Mga Pag-moderate** | `/v1/moderations` — Mga pagsusuri sa kaligtasan ng nilalaman |
|
||||
| 🔀 **Reranking** | `/v1/rerank` — Muling pagraranggo ng kaugnayan ng dokumento |
|
||||
|
||||
### 🛡️ Katatagan at Seguridad
|
||||
|
||||
|
||||
@@ -873,14 +873,14 @@ npm run electron:build:linux # Linux (.AppImage)
|
||||
|
||||
### 🎵 Wielomodalne interfejsy API
|
||||
|
||||
| Funkcja | Co to robi |
|
||||
| ----------------------------- | ------------------------------------------------------ |
|
||||
| 🖼️ **Generowanie obrazu** | `/v1/images/generations` — 4 dostawców, ponad 9 modeli |
|
||||
| 📐 **Osadzenia** | `/v1/embeddings` — 6 dostawców, ponad 9 modeli |
|
||||
| 🎤 **Transkrypcja audio** | `/v1/audio/transcriptions` — Kompatybilny z szeptem |
|
||||
| 🔊 **Zamiana tekstu na mowę** | `/v1/audio/speech` — Synteza dźwięku wielu dostawców |
|
||||
| 🛡️ **Moderacje** | `/v1/moderations` — Kontrola bezpieczeństwa treści |
|
||||
| 🔀 **Ponowna pozycja** | `/v1/rerank` — Zmiana rankingu trafności dokumentu |
|
||||
| Funkcja | Co to robi |
|
||||
| ----------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| 🖼️ **Generowanie obrazu** | `/v1/images/generations` — 4 dostawców, ponad 9 modeli |
|
||||
| 📐 **Osadzenia** | `/v1/embeddings` — 6 dostawców, ponad 9 modeli |
|
||||
| 🎤 **Transkrypcja audio** | `/v1/audio/transcriptions` — 7 providers (Deepgram Nova 3, AssemblyAI, Groq Whisper, HuggingFace, ElevenLabs, OpenAI, Azure), auto-language detection, MP4/MP3/WAV support |
|
||||
| 🔊 **Zamiana tekstu na mowę** | `/v1/audio/speech` — 10 providers (ElevenLabs, OpenAI, Deepgram, Cartesia, PlayHT, HuggingFace, Nvidia NIM, Inworld, Coqui, Tortoise) |
|
||||
| 🛡️ **Moderacje** | `/v1/moderations` — Kontrola bezpieczeństwa treści |
|
||||
| 🔀 **Ponowna pozycja** | `/v1/rerank` — Zmiana rankingu trafności dokumentu |
|
||||
|
||||
### 🛡️ Odporność i bezpieczeństwo
|
||||
|
||||
|
||||
+79
-28
@@ -819,24 +819,28 @@ Quando minimizado, o OmniRoute fica na bandeja do sistema com ações rápidas:
|
||||
|
||||
## 💰 Preços Resumidos
|
||||
|
||||
| Tier | Provedor | Custo | Reset de Cota | Melhor Para |
|
||||
| ----------------- | ----------------- | ---------------------------- | ----------------- | ----------------------- |
|
||||
| **💳 ASSINATURA** | Claude Code (Pro) | $20/mês | 5h + semanal | Já é assinante |
|
||||
| | Codex (Plus/Pro) | $20-200/mês | 5h + semanal | Usuários OpenAI |
|
||||
| | Gemini CLI | **GRATUITO** | 180K/mês + 1K/dia | Todos! |
|
||||
| | GitHub Copilot | $10-19/mês | Mensal | Usuários GitHub |
|
||||
| **🔑 API KEY** | NVIDIA NIM | **GRATUITO** (1000 créditos) | Único | Testes gratuitos |
|
||||
| | DeepSeek | Por uso | Nenhum | Melhor preço/qualidade |
|
||||
| | Groq | Tier gratuito + pago | Limitado | Inferência ultra-rápida |
|
||||
| | xAI (Grok) | Por uso | Nenhum | Modelos Grok |
|
||||
| | Mistral | Tier gratuito + pago | Limitado | IA Europeia |
|
||||
| | OpenRouter | Por uso | Nenhum | 100+ modelos |
|
||||
| **💰 BARATO** | GLM-4.7 | $0.6/1M | Diário 10h | Backup econômico |
|
||||
| | MiniMax M2.1 | $0.2/1M | Rotativo 5h | Opção mais barata |
|
||||
| | Kimi K2 | $9/mês fixo | 10M tokens/mês | Custo previsível |
|
||||
| **🆓 GRATUITO** | iFlow | $0 | Ilimitado | 8 modelos gratuitos |
|
||||
| | Qwen | $0 | Ilimitado | 3 modelos gratuitos |
|
||||
| | Kiro | $0 | Ilimitado | Claude gratuito |
|
||||
| Tier | Provedor | Custo | Reset de Cota | Melhor Para |
|
||||
| ----------------- | ----------------- | ---------------------------- | ----------------- | ------------------------------ |
|
||||
| **💳 ASSINATURA** | Claude Code (Pro) | $20/mês | 5h + semanal | Já é assinante |
|
||||
| | Codex (Plus/Pro) | $20-200/mês | 5h + semanal | Usuários OpenAI |
|
||||
| | Gemini CLI | **GRATUITO** | 180K/mês + 1K/dia | Todos! |
|
||||
| | GitHub Copilot | $10-19/mês | Mensal | Usuários GitHub |
|
||||
| **🔑 API KEY** | NVIDIA NIM | **GRATUITO** (1000 créditos) | Único | Testes gratuitos |
|
||||
| | DeepSeek | Por uso | Nenhum | Melhor preço/qualidade |
|
||||
| | Groq | Tier gratuito + pago | Limitado | Inferência ultra-rápida |
|
||||
| | xAI (Grok) | Por uso | Nenhum | Modelos Grok |
|
||||
| | Mistral | Tier gratuito + pago | Limitado | IA Europeia |
|
||||
| | OpenRouter | Por uso | Nenhum | 100+ modelos |
|
||||
| **💰 BARATO** | GLM-4.7 | $0.6/1M | Diário 10h | Backup econômico |
|
||||
| | MiniMax M2.1 | $0.2/1M | Rotativo 5h | Opção mais barata |
|
||||
| | Kimi K2 | $9/mês fixo | 10M tokens/mês | Custo previsível |
|
||||
| **🆓 GRATUITO** | iFlow | $0 | Ilimitado | 8 modelos gratuitos |
|
||||
| | Qwen | $0 | Ilimitado | 3 modelos gratuitos |
|
||||
| | Kiro | $0 | Ilimitado | Claude gratuito |
|
||||
| | LongCat 🆕 | **$0** (50M tok/dia 🔥) | 1 req/s | Maior cota grátis do mundo |
|
||||
| | Pollinations 🆕 | **$0** (sem chave API) | 1 req/15s | GPT-5, Claude, DeepSeek, Llama |
|
||||
| | Cloudflare AI 🆕 | **$0** (10K Neurons/dia) | ~150 resp/dia | 50+ modelos, edge global |
|
||||
| | Scaleway AI 🆕 | **$0** (1M tokens total) | Limitado por taxa | EU/GDPR, Qwen3 235B, Llama 70B |
|
||||
|
||||
**💡 Dica Pro:** Comece com Gemini CLI (180K grátis/mês) + iFlow (ilimitado grátis) = $0 de custo!
|
||||
|
||||
@@ -879,16 +883,16 @@ Por que isso é relevante:
|
||||
|
||||
### 🎵 APIs Multi-Modal
|
||||
|
||||
| Funcionalidade | O que Faz |
|
||||
| --------------------------- | ----------------------------------------------------------------------------------------------------------- |
|
||||
| 🖼️ **Geração de Imagem** | `/v1/images/generations` — 10 provedores, 20+ modelos (cloud + local) |
|
||||
| 📐 **Embeddings** | `/v1/embeddings` — 6 provedores, 9+ modelos |
|
||||
| 🎤 **Transcrição de Áudio** | `/v1/audio/transcriptions` — Whisper + Nvidia NIM, HuggingFace, Qwen3 |
|
||||
| 🔊 **Texto para Fala** | `/v1/audio/speech` — ElevenLabs, Nvidia NIM, HuggingFace, Coqui, Tortoise, Qwen3, Inworld, Cartesia, PlayHT |
|
||||
| 🎬 **Geração de Vídeo** | `/v1/videos/generations` — ComfyUI (AnimateDiff, SVD), SD WebUI |
|
||||
| 🎵 **Geração de Música** | `/v1/music/generations` — ComfyUI (Stable Audio Open, MusicGen) |
|
||||
| 🛡️ **Moderações** | `/v1/moderations` — Verificações de segurança |
|
||||
| 🔀 **Reranking** | `/v1/rerank` — Reranking de relevância de documentos |
|
||||
| Funcionalidade | O que Faz |
|
||||
| --------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| 🖼️ **Geração de Imagem** | `/v1/images/generations` — 10 provedores, 20+ modelos (cloud + local) |
|
||||
| 📐 **Embeddings** | `/v1/embeddings` — 6 provedores, 9+ modelos |
|
||||
| 🎤 **Transcrição de Áudio** | `/v1/audio/transcriptions` — 7 providers (Deepgram Nova 3, AssemblyAI, Groq Whisper, HuggingFace, ElevenLabs, OpenAI, Azure), auto-language detection, MP4/MP3/WAV support |
|
||||
| 🔊 **Texto para Fala** | `/v1/audio/speech` — 10 providers (ElevenLabs, OpenAI, Deepgram, Cartesia, PlayHT, HuggingFace, Nvidia NIM, Inworld, Coqui, Tortoise) |
|
||||
| 🎬 **Geração de Vídeo** | `/v1/videos/generations` — ComfyUI (AnimateDiff, SVD), SD WebUI |
|
||||
| 🎵 **Geração de Música** | `/v1/music/generations` — ComfyUI (Stable Audio Open, MusicGen) |
|
||||
| 🛡️ **Moderações** | `/v1/moderations` — Verificações de segurança |
|
||||
| 🔀 **Reranking** | `/v1/rerank` — Reranking de relevância de documentos |
|
||||
|
||||
### 🛡️ Resiliência e Segurança
|
||||
|
||||
@@ -1223,6 +1227,53 @@ Modelos:
|
||||
kr/claude-haiku-4.5
|
||||
```
|
||||
|
||||
### LongCat AI (GRATUITO 50M tokens/dia!) 🆕
|
||||
|
||||
1. Cadastre-se: [longcat.chat](https://longcat.chat) com e-mail ou telefone
|
||||
2. Gere uma chave de API gratuita
|
||||
3. Dashboard → Adicionar Provedor → LongCat
|
||||
|
||||
**Modelos:**
|
||||
|
||||
- `lc/LongCat-Flash-Lite` — **50M tokens/dia** 💥 (maior cota gratuita do mundo!)
|
||||
- `lc/LongCat-Flash-Chat` — 500K tokens/dia
|
||||
- `lc/LongCat-Flash-Thinking` — 500K tokens/dia (raciocínio)
|
||||
|
||||
> 100% gratuito durante o beta público. Reset diário à meia-noite UTC.
|
||||
|
||||
### Pollinations AI (SEM CHAVE NECESSÁRIA!) 🆕
|
||||
|
||||
1. Adicione o provedor Pollinations no Dashboard
|
||||
2. Deixe o campo de chave API vazio (ou coloque qualquer string)
|
||||
3. Comece a usar imediatamente!
|
||||
|
||||
**Modelos via `pol/`:** `openai` (GPT-5), `claude`, `gemini`, `deepseek`, `llama` (Llama 4)
|
||||
|
||||
> Sem cadastro, sem chave, sem cartão de crédito. 1 req/15s ilimitado.
|
||||
|
||||
### Cloudflare Workers AI (GRATUITO 10K Neurons/dia!) 🆕
|
||||
|
||||
1. Cadastre-se: [dash.cloudflare.com](https://dash.cloudflare.com)
|
||||
2. Gere um API Token em Profile → API Tokens
|
||||
3. Copie seu Account ID (coluna direita do dashboard)
|
||||
4. Dashboard → Adicionar Provedor → Cloudflare AI
|
||||
- API Key: seu token
|
||||
- Account ID: seu account ID
|
||||
|
||||
**Modelos via `cf/`:** `@cf/meta/llama-3.3-70b-instruct`, `@cf/google/gemma-3-12b-it`, 50+ mais
|
||||
|
||||
> 10K Neurons/dia ≈ 150 respostas de LLM ou 500s de transcrição Whisper gratuita!
|
||||
|
||||
### Scaleway AI (1M tokens gratuitos!) 🆕
|
||||
|
||||
1. Cadastre-se: [console.scaleway.com](https://console.scaleway.com)
|
||||
2. Gere uma chave de API IAM
|
||||
3. Dashboard → Adicionar Provedor → Scaleway
|
||||
|
||||
**Modelos via `scw/`:** `qwen3-235b-a22b-instruct-2507` (Qwen3 235B!), `llama-3.1-70b-instruct`
|
||||
|
||||
> 1M tokens gratuitos para novas contas. Dados processados na 🇫🇷 França (EU/GDPR).
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
|
||||
@@ -874,14 +874,14 @@ npm run electron:build:linux # Linux (.AppImage)
|
||||
|
||||
### 🎵 APIs multimodais
|
||||
|
||||
| Recurso | O que faz |
|
||||
| --------------------------------- | ----------------------------------------------------------- |
|
||||
| 🖼️ **Geração de imagens** | `/v1/images/generations` — 4 provedores, mais de 9 modelos |
|
||||
| 📐 **Incorporações** | `/v1/embeddings` — 6 provedores, mais de 9 modelos |
|
||||
| 🎤 **Transcrição de áudio** | `/v1/audio/transcriptions` — Compatível com sussurro |
|
||||
| 🔊 **Conversão de texto em fala** | `/v1/audio/speech` — Síntese de áudio multiprovedor |
|
||||
| 🛡️ **Moderações** | `/v1/moderations` — Verificações de segurança de conteúdo |
|
||||
| 🔀 **Reclassificação** | `/v1/rerank` — Reclassificação da relevância dos documentos |
|
||||
| Recurso | O que faz |
|
||||
| --------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| 🖼️ **Geração de imagens** | `/v1/images/generations` — 4 provedores, mais de 9 modelos |
|
||||
| 📐 **Incorporações** | `/v1/embeddings` — 6 provedores, mais de 9 modelos |
|
||||
| 🎤 **Transcrição de áudio** | `/v1/audio/transcriptions` — 7 providers (Deepgram Nova 3, AssemblyAI, Groq Whisper, HuggingFace, ElevenLabs, OpenAI, Azure), auto-language detection, MP4/MP3/WAV support |
|
||||
| 🔊 **Conversão de texto em fala** | `/v1/audio/speech` — 10 providers (ElevenLabs, OpenAI, Deepgram, Cartesia, PlayHT, HuggingFace, Nvidia NIM, Inworld, Coqui, Tortoise) |
|
||||
| 🛡️ **Moderações** | `/v1/moderations` — Verificações de segurança de conteúdo |
|
||||
| 🔀 **Reclassificação** | `/v1/rerank` — Reclassificação da relevância dos documentos |
|
||||
|
||||
### 🛡️ Resiliência e segurança
|
||||
|
||||
|
||||
@@ -875,14 +875,14 @@ npm run electron:build:linux # Linux (.AppImage)
|
||||
|
||||
### 🎵 API-uri multimodale
|
||||
|
||||
| Caracteristica | Ce face |
|
||||
| ------------------------- | ---------------------------------------------------------- |
|
||||
| 🖼️ **Generarea imaginii** | `/v1/images/generations` — 4 furnizori, peste 9 modele |
|
||||
| 📐 **Inglobări** | `/v1/embeddings` — 6 furnizori, peste 9 modele |
|
||||
| 🎤 **Transcriere audio** | `/v1/audio/transcriptions` — Compatibil cu Whisper |
|
||||
| 🔊 **Text-to-speech** | `/v1/audio/speech` — Sinteză audio cu mai mulți furnizori |
|
||||
| 🛡️ **Moderații** | `/v1/moderations` — Verificări de siguranță a conținutului |
|
||||
| 🔀 **Reclasificare** | `/v1/rerank` — Reclasificarea relevanței documentului |
|
||||
| Caracteristica | Ce face |
|
||||
| ------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| 🖼️ **Generarea imaginii** | `/v1/images/generations` — 4 furnizori, peste 9 modele |
|
||||
| 📐 **Inglobări** | `/v1/embeddings` — 6 furnizori, peste 9 modele |
|
||||
| 🎤 **Transcriere audio** | `/v1/audio/transcriptions` — 7 providers (Deepgram Nova 3, AssemblyAI, Groq Whisper, HuggingFace, ElevenLabs, OpenAI, Azure), auto-language detection, MP4/MP3/WAV support |
|
||||
| 🔊 **Text-to-speech** | `/v1/audio/speech` — 10 providers (ElevenLabs, OpenAI, Deepgram, Cartesia, PlayHT, HuggingFace, Nvidia NIM, Inworld, Coqui, Tortoise) |
|
||||
| 🛡️ **Moderații** | `/v1/moderations` — Verificări de siguranță a conținutului |
|
||||
| 🔀 **Reclasificare** | `/v1/rerank` — Reclasificarea relevanței documentului |
|
||||
|
||||
### 🛡️ Reziliență și securitate
|
||||
|
||||
|
||||
@@ -873,14 +873,14 @@ npm run electron:build:linux # Linux (.AppImage)
|
||||
|
||||
### 🎵 Мультимодальные API
|
||||
|
||||
| Функция | Что делает |
|
||||
| ---------------------------- | --------------------------------------------------- |
|
||||
| 🖼️ **Генерация изображений** | `/v1/images/generations` — 4 провайдера, 9+ моделей |
|
||||
| 📐 **Embeddings** | `/v1/embeddings` — 6 провайдеров, 9+ моделей |
|
||||
| 🎤 **Транскрипция аудио** | `/v1/audio/transcriptions` — Совместимо с Whisper |
|
||||
| 🔊 **Текст в речь** | `/v1/audio/speech` — Мульти-провайдерный синтез |
|
||||
| 🛡️ **Модерация** | `/v1/moderations` — Проверки безопасности контента |
|
||||
| 🔀 **Reranking** | `/v1/rerank` — Переранжирование релевантности |
|
||||
| Функция | Что делает |
|
||||
| ---------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| 🖼️ **Генерация изображений** | `/v1/images/generations` — 4 провайдера, 9+ моделей |
|
||||
| 📐 **Embeddings** | `/v1/embeddings` — 6 провайдеров, 9+ моделей |
|
||||
| 🎤 **Транскрипция аудио** | `/v1/audio/transcriptions` — 7 providers (Deepgram Nova 3, AssemblyAI, Groq Whisper, HuggingFace, ElevenLabs, OpenAI, Azure), auto-language detection, MP4/MP3/WAV support |
|
||||
| 🔊 **Текст в речь** | `/v1/audio/speech` — 10 providers (ElevenLabs, OpenAI, Deepgram, Cartesia, PlayHT, HuggingFace, Nvidia NIM, Inworld, Coqui, Tortoise) |
|
||||
| 🛡️ **Модерация** | `/v1/moderations` — Проверки безопасности контента |
|
||||
| 🔀 **Reranking** | `/v1/rerank` — Переранжирование релевантности |
|
||||
|
||||
### 🛡️ Устойчивость и безопасность
|
||||
|
||||
|
||||
@@ -877,14 +877,14 @@ npm run electron:build:linux # Linux (.AppImage)
|
||||
|
||||
### 🎵 Multimodálne API
|
||||
|
||||
| Funkcia | Čo to robí |
|
||||
| --------------------------- | ---------------------------------------------------------------- |
|
||||
| 🖼️ **Generovanie obrázkov** | `/v1/images/generations` — 4 poskytovatelia, 9+ modelov |
|
||||
| 📐 **Vloženie** | `/v1/embeddings` — 6 poskytovateľov, 9+ modelov |
|
||||
| 🎤 **Prepis zvuku** | `/v1/audio/transcriptions` — Kompatibilné so šepotom |
|
||||
| 🔊 **Prevod textu na reč** | `/v1/audio/speech` — Zvuková syntéza od viacerých poskytovateľov |
|
||||
| 🛡️ **Moderovania** | `/v1/moderations` — Kontroly bezpečnosti obsahu |
|
||||
| 🔀 **Reranking** | `/v1/rerank` — Zmena poradia relevantnosti dokumentu |
|
||||
| Funkcia | Čo to robí |
|
||||
| --------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| 🖼️ **Generovanie obrázkov** | `/v1/images/generations` — 4 poskytovatelia, 9+ modelov |
|
||||
| 📐 **Vloženie** | `/v1/embeddings` — 6 poskytovateľov, 9+ modelov |
|
||||
| 🎤 **Prepis zvuku** | `/v1/audio/transcriptions` — 7 providers (Deepgram Nova 3, AssemblyAI, Groq Whisper, HuggingFace, ElevenLabs, OpenAI, Azure), auto-language detection, MP4/MP3/WAV support |
|
||||
| 🔊 **Prevod textu na reč** | `/v1/audio/speech` — 10 providers (ElevenLabs, OpenAI, Deepgram, Cartesia, PlayHT, HuggingFace, Nvidia NIM, Inworld, Coqui, Tortoise) |
|
||||
| 🛡️ **Moderovania** | `/v1/moderations` — Kontroly bezpečnosti obsahu |
|
||||
| 🔀 **Reranking** | `/v1/rerank` — Zmena poradia relevantnosti dokumentu |
|
||||
|
||||
### 🛡️ Odolnosť a bezpečnosť
|
||||
|
||||
|
||||
@@ -873,14 +873,14 @@ npm run electron:build:linux # Linux (.AppImage)
|
||||
|
||||
### 🎵 Multimodala API:er
|
||||
|
||||
| Funktion | Vad det gör |
|
||||
| ------------------------ | ------------------------------------------------------ |
|
||||
| 🖼️ **Bildgenerering** | `/v1/images/generations` — 4 leverantörer, 9+ modeller |
|
||||
| 📐 **Inbäddningar** | `/v1/embeddings` — 6 leverantörer, 9+ modeller |
|
||||
| 🎤 **Ljudtranskription** | `/v1/audio/transcriptions` — Whisper-kompatibel |
|
||||
| 🔊 **Text-till-tal** | `/v1/audio/speech` — Ljudsyntes med flera leverantörer |
|
||||
| 🛡️ **Moderationer** | `/v1/moderations` — Innehållssäkerhetskontroller |
|
||||
| 🔀 **Omrankning** | `/v1/rerank` — Omrankning av dokumentrelevans |
|
||||
| Funktion | Vad det gör |
|
||||
| ------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| 🖼️ **Bildgenerering** | `/v1/images/generations` — 4 leverantörer, 9+ modeller |
|
||||
| 📐 **Inbäddningar** | `/v1/embeddings` — 6 leverantörer, 9+ modeller |
|
||||
| 🎤 **Ljudtranskription** | `/v1/audio/transcriptions` — 7 providers (Deepgram Nova 3, AssemblyAI, Groq Whisper, HuggingFace, ElevenLabs, OpenAI, Azure), auto-language detection, MP4/MP3/WAV support |
|
||||
| 🔊 **Text-till-tal** | `/v1/audio/speech` — 10 providers (ElevenLabs, OpenAI, Deepgram, Cartesia, PlayHT, HuggingFace, Nvidia NIM, Inworld, Coqui, Tortoise) |
|
||||
| 🛡️ **Moderationer** | `/v1/moderations` — Innehållssäkerhetskontroller |
|
||||
| 🔀 **Omrankning** | `/v1/rerank` — Omrankning av dokumentrelevans |
|
||||
|
||||
### 🛡️ Motståndskraft och säkerhet
|
||||
|
||||
|
||||
@@ -874,14 +874,14 @@ npm run electron:build:linux # Linux (.AppImage)
|
||||
|
||||
### 🎵 Multi-Modal API
|
||||
|
||||
| คุณสมบัติ | มันทำอะไร |
|
||||
| ----------------------- | ------------------------------------------------------------- |
|
||||
| 🖼️ **การสร้างภาพ** | `/v1/images/generations` — ผู้ให้บริการ 4 ราย รุ่น 9+ |
|
||||
| 📐 **การฝัง** | `/v1/embeddings` — ผู้ให้บริการ 6 ราย รุ่น 9+ |
|
||||
| 🎶 **การถอดเสียง** | `/v1/audio/transcriptions` — รองรับการกระซิบ |
|
||||
| 🔊 **ข้อความเป็นคำพูด** | `/v1/audio/speech` — การสังเคราะห์เสียงจากผู้ให้บริการหลายราย |
|
||||
| 🛡️ **การกลั่นกรอง** | `/v1/moderations` — การตรวจสอบความปลอดภัยของเนื้อหา |
|
||||
| 🔀 **จัดอันดับ** | `/v1/rerank` — การจัดอันดับความเกี่ยวข้องของเอกสาร |
|
||||
| คุณสมบัติ | มันทำอะไร |
|
||||
| ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| 🖼️ **การสร้างภาพ** | `/v1/images/generations` — ผู้ให้บริการ 4 ราย รุ่น 9+ |
|
||||
| 📐 **การฝัง** | `/v1/embeddings` — ผู้ให้บริการ 6 ราย รุ่น 9+ |
|
||||
| 🎶 **การถอดเสียง** | `/v1/audio/transcriptions` — รองรับการกระซิบ |
|
||||
| 🔊 **ข้อความเป็นคำพูด** | `/v1/audio/speech` — 10 providers (ElevenLabs, OpenAI, Deepgram, Cartesia, PlayHT, HuggingFace, Nvidia NIM, Inworld, Coqui, Tortoise) |
|
||||
| 🛡️ **การกลั่นกรอง** | `/v1/moderations` — การตรวจสอบความปลอดภัยของเนื้อหา |
|
||||
| 🔀 **จัดอันดับ** | `/v1/rerank` — การจัดอันดับความเกี่ยวข้องของเอกสาร |
|
||||
|
||||
### 🛡️ ความยืดหยุ่นและความปลอดภัย
|
||||
|
||||
|
||||
@@ -878,14 +878,14 @@ npm run electron:build:linux # Linux (.AppImage)
|
||||
|
||||
### 🎵 Мультимодальні API
|
||||
|
||||
| Особливість | Що він робить |
|
||||
| ---------------------------------- | ----------------------------------------------------- |
|
||||
| 🖼️ **Створення зображень** | `/v1/images/generations` — 4 провайдери, 9+ моделей |
|
||||
| 📐 **Вбудовування** | `/v1/embeddings` — 6 провайдерів, 9+ моделей |
|
||||
| 🎤 **Транскрипція аудіо** | `/v1/audio/transcriptions` — сумісний із Whisper |
|
||||
| 🔊 **Створення тексту в мовлення** | `/v1/audio/speech` — Багатопровайдерний аудіосинтез |
|
||||
| 🛡️ **Модерації** | `/v1/moderations` — Перевірка безпеки вмісту |
|
||||
| 🔀 **Переранжування** | `/v1/rerank` — Переранжування релевантності документа |
|
||||
| Особливість | Що він робить |
|
||||
| ---------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| 🖼️ **Створення зображень** | `/v1/images/generations` — 4 провайдери, 9+ моделей |
|
||||
| 📐 **Вбудовування** | `/v1/embeddings` — 6 провайдерів, 9+ моделей |
|
||||
| 🎤 **Транскрипція аудіо** | `/v1/audio/transcriptions` — 7 providers (Deepgram Nova 3, AssemblyAI, Groq Whisper, HuggingFace, ElevenLabs, OpenAI, Azure), auto-language detection, MP4/MP3/WAV support |
|
||||
| 🔊 **Створення тексту в мовлення** | `/v1/audio/speech` — 10 providers (ElevenLabs, OpenAI, Deepgram, Cartesia, PlayHT, HuggingFace, Nvidia NIM, Inworld, Coqui, Tortoise) |
|
||||
| 🛡️ **Модерації** | `/v1/moderations` — Перевірка безпеки вмісту |
|
||||
| 🔀 **Переранжування** | `/v1/rerank` — Переранжування релевантності документа |
|
||||
|
||||
### 🛡️ Стійкість і безпека
|
||||
|
||||
|
||||
@@ -874,14 +874,14 @@ npm run electron:build:linux # Linux (.AppImage)
|
||||
|
||||
### 🎵 API đa phương thức
|
||||
|
||||
| Tính năng | Nó làm gì |
|
||||
| ------------------------------------- | ------------------------------------------------------------ |
|
||||
| 🖼️ **Tạo hình ảnh** | `/v1/images/generations` — 4 nhà cung cấp, hơn 9 mô hình |
|
||||
| 📐 **Nhúng** | `/v1/embeddings` — 6 nhà cung cấp, hơn 9 mô hình |
|
||||
| 🎤 **Phiên âm âm thanh** | `/v1/audio/transcriptions` — Tương thích với lời thì thầm |
|
||||
| 🔊 **Chuyển văn bản thành giọng nói** | `/v1/audio/speech` — Tổng hợp âm thanh từ nhiều nhà cung cấp |
|
||||
| 🛡️ **Kiểm duyệt** | `/v1/moderations` — Kiểm tra an toàn nội dung |
|
||||
| 🔀 **Sắp xếp lại** | `/v1/rerank` — Sắp xếp lại mức độ liên quan của tài liệu |
|
||||
| Tính năng | Nó làm gì |
|
||||
| ------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| 🖼️ **Tạo hình ảnh** | `/v1/images/generations` — 4 nhà cung cấp, hơn 9 mô hình |
|
||||
| 📐 **Nhúng** | `/v1/embeddings` — 6 nhà cung cấp, hơn 9 mô hình |
|
||||
| 🎤 **Phiên âm âm thanh** | `/v1/audio/transcriptions` — 7 providers (Deepgram Nova 3, AssemblyAI, Groq Whisper, HuggingFace, ElevenLabs, OpenAI, Azure), auto-language detection, MP4/MP3/WAV support |
|
||||
| 🔊 **Chuyển văn bản thành giọng nói** | `/v1/audio/speech` — 10 providers (ElevenLabs, OpenAI, Deepgram, Cartesia, PlayHT, HuggingFace, Nvidia NIM, Inworld, Coqui, Tortoise) |
|
||||
| 🛡️ **Kiểm duyệt** | `/v1/moderations` — Kiểm tra an toàn nội dung |
|
||||
| 🔀 **Sắp xếp lại** | `/v1/rerank` — Sắp xếp lại mức độ liên quan của tài liệu |
|
||||
|
||||
### 🛡️ Khả năng phục hồi và bảo mật
|
||||
|
||||
|
||||
@@ -873,14 +873,14 @@ npm run electron:build:linux # Linux (.AppImage)
|
||||
|
||||
### 🎵 多模态 API
|
||||
|
||||
| 功能 | 功能描述 |
|
||||
| ----------------- | ---------------------------------------------- |
|
||||
| 🖼️ **图像生成** | `/v1/images/generations` — 4 个提供商,9+ 模型 |
|
||||
| 📐 **Embeddings** | `/v1/embeddings` — 6 个提供商,9+ 模型 |
|
||||
| 🎤 **音频转录** | `/v1/audio/transcriptions` — Whisper 兼容 |
|
||||
| 🔊 **文字转语音** | `/v1/audio/speech` — 多提供商音频合成 |
|
||||
| 🛡️ **内容审核** | `/v1/moderations` — 内容安全检查 |
|
||||
| 🔀 **重排序** | `/v1/rerank` — 文档相关性重排序 |
|
||||
| 功能 | 功能描述 |
|
||||
| ----------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| 🖼️ **图像生成** | `/v1/images/generations` — 4 个提供商,9+ 模型 |
|
||||
| 📐 **Embeddings** | `/v1/embeddings` — 6 个提供商,9+ 模型 |
|
||||
| 🎤 **音频转录** | `/v1/audio/transcriptions` — 7 providers (Deepgram Nova 3, AssemblyAI, Groq Whisper, HuggingFace, ElevenLabs, OpenAI, Azure), auto-language detection, MP4/MP3/WAV support |
|
||||
| 🔊 **文字转语音** | `/v1/audio/speech` — 10 providers (ElevenLabs, OpenAI, Deepgram, Cartesia, PlayHT, HuggingFace, Nvidia NIM, Inworld, Coqui, Tortoise) |
|
||||
| 🛡️ **内容审核** | `/v1/moderations` — 内容安全检查 |
|
||||
| 🔀 **重排序** | `/v1/rerank` — 文档相关性重排序 |
|
||||
|
||||
### 🛡️ 弹性与安全
|
||||
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
openapi: 3.1.0
|
||||
info:
|
||||
title: OmniRoute API
|
||||
version: 2.8.9
|
||||
version: 3.0.0-rc.2
|
||||
description: |
|
||||
OmniRoute is a local-first AI API proxy router. It provides an OpenAI-compatible
|
||||
endpoint that routes requests to multiple AI providers with load balancing,
|
||||
|
||||
@@ -17,6 +17,7 @@ export interface EmbeddingProvider {
|
||||
}
|
||||
|
||||
export interface EmbeddingProviderNodeRow {
|
||||
id?: string;
|
||||
prefix: string;
|
||||
name: string;
|
||||
baseUrl: string;
|
||||
|
||||
@@ -47,6 +47,8 @@ export interface RegistryEntry {
|
||||
executor: string;
|
||||
baseUrl?: string;
|
||||
baseUrls?: string[];
|
||||
/** Override base URL used only for API key validation (e.g., opencode-go validates on zen/v1) */
|
||||
testKeyBaseUrl?: string;
|
||||
responsesBaseUrl?: string;
|
||||
urlSuffix?: string;
|
||||
urlBuilder?: (base: string, model: string, stream: boolean) => string;
|
||||
@@ -495,6 +497,41 @@ export const REGISTRY: Record<string, RegistryEntry> = {
|
||||
],
|
||||
},
|
||||
|
||||
"opencode-go": {
|
||||
id: "opencode-go",
|
||||
alias: "opencode-go",
|
||||
format: "openai",
|
||||
executor: "opencode",
|
||||
baseUrl: "https://opencode.ai/zen/go/v1",
|
||||
// (#532) Key validation must hit the main zen endpoint (same key works for both tiers)
|
||||
testKeyBaseUrl: "https://opencode.ai/zen/v1",
|
||||
authType: "apikey",
|
||||
authHeader: "Authorization",
|
||||
authPrefix: "Bearer",
|
||||
models: [
|
||||
{ id: "glm-5", name: "GLM-5" },
|
||||
{ id: "kimi-k2.5", name: "Kimi K2.5" },
|
||||
{ id: "minimax-m2.7", name: "MiniMax M2.7", targetFormat: "claude" },
|
||||
{ id: "minimax-m2.5", name: "MiniMax M2.5", targetFormat: "claude" },
|
||||
],
|
||||
},
|
||||
|
||||
"opencode-zen": {
|
||||
id: "opencode-zen",
|
||||
alias: "opencode-zen",
|
||||
format: "openai",
|
||||
executor: "opencode",
|
||||
baseUrl: "https://opencode.ai/zen/v1",
|
||||
authType: "apikey",
|
||||
authHeader: "Authorization",
|
||||
authPrefix: "Bearer",
|
||||
models: [
|
||||
{ id: "minimax-m2.5-free", name: "MiniMax M2.5 Free" },
|
||||
{ id: "big-pickle", name: "Big Pickle" },
|
||||
{ id: "gpt-5-nano", name: "GPT 5 Nano" },
|
||||
],
|
||||
},
|
||||
|
||||
openrouter: {
|
||||
id: "openrouter",
|
||||
alias: "openrouter",
|
||||
@@ -883,6 +920,12 @@ export const REGISTRY: Record<string, RegistryEntry> = {
|
||||
authType: "apikey",
|
||||
authHeader: "bearer",
|
||||
models: [
|
||||
{ id: "meta-llama/Llama-3.3-70B-Instruct-Turbo-Free", name: "Llama 3.3 70B Turbo (🆓 Free)" },
|
||||
{ id: "meta-llama/Llama-Vision-Free", name: "Llama Vision (🆓 Free)" },
|
||||
{
|
||||
id: "deepseek-ai/DeepSeek-R1-Distill-Llama-70B-Free",
|
||||
name: "DeepSeek R1 Distill 70B (🆓 Free)",
|
||||
},
|
||||
{ id: "meta-llama/Llama-3.3-70B-Instruct-Turbo", name: "Llama 3.3 70B Turbo" },
|
||||
{ id: "deepseek-ai/DeepSeek-R1", name: "DeepSeek R1" },
|
||||
{ id: "Qwen/Qwen3-235B-A22B", name: "Qwen3 235B" },
|
||||
@@ -1125,6 +1168,137 @@ export const REGISTRY: Record<string, RegistryEntry> = {
|
||||
{ id: "claude-sonnet-4-5@20251101", name: "Claude Sonnet 4.5 (Vertex)" },
|
||||
],
|
||||
},
|
||||
|
||||
alibaba: {
|
||||
id: "alibaba",
|
||||
alias: "ali",
|
||||
format: "openai",
|
||||
executor: "default",
|
||||
// DashScope international OpenAI-compatible endpoint.
|
||||
// China users should set providerSpecificData.baseUrl to:
|
||||
// https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions
|
||||
baseUrl: "https://dashscope-intl.aliyuncs.com/compatible-mode/v1/chat/completions",
|
||||
modelsUrl: "https://dashscope-intl.aliyuncs.com/compatible-mode/v1/models",
|
||||
authType: "apikey",
|
||||
authHeader: "bearer",
|
||||
models: [
|
||||
{ id: "qwen-max", name: "Qwen Max" },
|
||||
{ id: "qwen-max-2025-01-25", name: "Qwen Max (2025-01-25)" },
|
||||
{ id: "qwen-plus", name: "Qwen Plus" },
|
||||
{ id: "qwen-plus-2025-07-14", name: "Qwen Plus (2025-07-14)" },
|
||||
{ id: "qwen-turbo", name: "Qwen Turbo" },
|
||||
{ id: "qwen-turbo-2025-11-01", name: "Qwen Turbo (2025-11-01)" },
|
||||
{ id: "qwen3-coder-plus", name: "Qwen3 Coder Plus" },
|
||||
{ id: "qwen3-coder-flash", name: "Qwen3 Coder Flash" },
|
||||
{ id: "qwq-plus", name: "QwQ Plus (Reasoning)" },
|
||||
{ id: "qwq-32b", name: "QwQ 32B" },
|
||||
{ id: "qwen3-32b", name: "Qwen3 32B" },
|
||||
{ id: "qwen3-235b-a22b", name: "Qwen3 235B A22B" },
|
||||
],
|
||||
passthroughModels: true,
|
||||
},
|
||||
|
||||
// ── New Free Providers (2026) ─────────────────────────────────────────────
|
||||
|
||||
longcat: {
|
||||
id: "longcat",
|
||||
alias: "lc",
|
||||
format: "openai",
|
||||
executor: "default",
|
||||
// (#536) Correct OpenAI-compatible base URL — was longcat.chat/api/v1/chat/completions
|
||||
// which is the chat endpoint directly, not the base. Key validation and routing must
|
||||
// use https://api.longcat.chat/openai which resolves /v1/models and /v1/chat/completions
|
||||
baseUrl: "https://api.longcat.chat/openai",
|
||||
authType: "apikey",
|
||||
authHeader: "Authorization",
|
||||
authPrefix: "Bearer",
|
||||
// Free tier: 50M tokens/day (Flash-Lite) + 500K/day (Chat/Thinking) — 100% free while public beta
|
||||
models: [
|
||||
{ id: "LongCat-Flash-Lite", name: "LongCat Flash-Lite (50M tok/day 🆓)" },
|
||||
{ id: "LongCat-Flash-Chat", name: "LongCat Flash-Chat (500K tok/day 🆓)" },
|
||||
{ id: "LongCat-Flash-Thinking", name: "LongCat Flash-Thinking (500K tok/day 🆓)" },
|
||||
{ id: "LongCat-Flash-Thinking-2601", name: "LongCat Flash-Thinking-2601 (🆓)" },
|
||||
{ id: "LongCat-Flash-Omni-2603", name: "LongCat Flash-Omni-2603 (🆓)" },
|
||||
],
|
||||
},
|
||||
|
||||
pollinations: {
|
||||
id: "pollinations",
|
||||
alias: "pol",
|
||||
format: "openai",
|
||||
executor: "pollinations",
|
||||
// No API key required for basic use. Proxy to GPT-5, Claude, Gemini, DeepSeek, Llama 4.
|
||||
baseUrl: "https://text.pollinations.ai/openai/chat/completions",
|
||||
authType: "apikey", // Optional — works without one too
|
||||
authHeader: "bearer",
|
||||
models: [
|
||||
{ id: "openai", name: "GPT-5 via Pollinations (🆓)" },
|
||||
{ id: "claude", name: "Claude via Pollinations (🆓)" },
|
||||
{ id: "gemini", name: "Gemini via Pollinations (🆓)" },
|
||||
{ id: "deepseek", name: "DeepSeek V3 via Pollinations (🆓)" },
|
||||
{ id: "llama", name: "Llama 4 via Pollinations (🆓)" },
|
||||
{ id: "mistral", name: "Mistral via Pollinations (🆓)" },
|
||||
],
|
||||
},
|
||||
|
||||
"cloudflare-ai": {
|
||||
id: "cloudflare-ai",
|
||||
alias: "cf",
|
||||
format: "openai",
|
||||
executor: "cloudflare-ai",
|
||||
// URL is dynamic: uses accountId from credentials. The executor builds it.
|
||||
baseUrl: "https://api.cloudflare.com/client/v4/accounts",
|
||||
authType: "apikey",
|
||||
authHeader: "bearer",
|
||||
// 10K Neurons/day free: ~150 LLM responses or 500s Whisper audio — global edge
|
||||
models: [
|
||||
{ id: "@cf/meta/llama-3.3-70b-instruct", name: "Llama 3.3 70B (🆓 ~150 resp/day)" },
|
||||
{ id: "@cf/meta/llama-3.1-8b-instruct", name: "Llama 3.1 8B (🆓)" },
|
||||
{ id: "@cf/google/gemma-3-12b-it", name: "Gemma 3 12B (🆓)" },
|
||||
{ id: "@cf/mistral/mistral-7b-instruct-v0.2-lora", name: "Mistral 7B (🆓)" },
|
||||
{ id: "@cf/qwen/qwen2.5-coder-15b-instruct", name: "Qwen 2.5 Coder 15B (🆓)" },
|
||||
{ id: "@cf/deepseek-ai/deepseek-r1-distill-qwen-32b", name: "DeepSeek R1 Distill 32B (🆓)" },
|
||||
],
|
||||
},
|
||||
|
||||
scaleway: {
|
||||
id: "scaleway",
|
||||
alias: "scw",
|
||||
format: "openai",
|
||||
executor: "default",
|
||||
baseUrl: "https://api.scaleway.ai/v1/chat/completions",
|
||||
authType: "apikey",
|
||||
authHeader: "bearer",
|
||||
// 1M tokens free for new accounts — EU/GDPR (Paris), no credit card needed under limit
|
||||
models: [
|
||||
{ id: "qwen3-235b-a22b-instruct-2507", name: "Qwen3 235B A22B (1M free tok 🆓)" },
|
||||
{ id: "llama-3.1-70b-instruct", name: "Llama 3.1 70B (🆓 EU)" },
|
||||
{ id: "llama-3.1-8b-instruct", name: "Llama 3.1 8B (🆓 EU)" },
|
||||
{ id: "mistral-small-3.2-24b-instruct-2506", name: "Mistral Small 3.2 (🆓 EU)" },
|
||||
{ id: "deepseek-v3-0324", name: "DeepSeek V3 (🆓 EU)" },
|
||||
{ id: "gpt-oss-120b", name: "GPT-OSS 120B (🆓 EU)" },
|
||||
],
|
||||
},
|
||||
|
||||
aimlapi: {
|
||||
id: "aimlapi",
|
||||
alias: "aiml",
|
||||
format: "openai",
|
||||
executor: "default",
|
||||
baseUrl: "https://api.aimlapi.com/v1/chat/completions",
|
||||
authType: "apikey",
|
||||
authHeader: "bearer",
|
||||
// $0.025/day free credits — 200+ models via single aggregator endpoint
|
||||
models: [
|
||||
{ id: "gpt-4o", name: "GPT-4o (via AI/ML API)" },
|
||||
{ id: "claude-3-5-sonnet-20241022", name: "Claude 3.5 Sonnet (via AI/ML API)" },
|
||||
{ id: "gemini-1.5-pro", name: "Gemini 1.5 Pro (via AI/ML API)" },
|
||||
{ id: "meta-llama/Meta-Llama-3.1-70B-Instruct-Turbo", name: "Llama 3.1 70B (via AI/ML API)" },
|
||||
{ id: "deepseek-chat", name: "DeepSeek Chat (via AI/ML API)" },
|
||||
{ id: "mistral-large-latest", name: "Mistral Large (via AI/ML API)" },
|
||||
],
|
||||
passthroughModels: true,
|
||||
},
|
||||
};
|
||||
|
||||
// ── Generator Functions ───────────────────────────────────────────────────
|
||||
|
||||
@@ -44,12 +44,28 @@ export class AntigravityExecutor extends BaseExecutor {
|
||||
// stale/wrong client-side values causing 404/403 from Cloud Code endpoints.
|
||||
// Opt-in escape hatch: set OMNIROUTE_ALLOW_BODY_PROJECT_OVERRIDE=1.
|
||||
const projectId =
|
||||
allowBodyProjectOverride && bodyProjectId ? bodyProjectId : credentialsProjectId || bodyProjectId;
|
||||
allowBodyProjectOverride && bodyProjectId
|
||||
? bodyProjectId
|
||||
: credentialsProjectId || bodyProjectId;
|
||||
|
||||
if (!projectId) {
|
||||
throw new Error(
|
||||
"Missing Google projectId for Antigravity account. Please reconnect OAuth so OmniRoute can fetch your real Cloud Code project (loadCodeAssist)."
|
||||
);
|
||||
// (#489) Return a structured error instead of throwing — gives the client a clear signal
|
||||
// to show a "Reconnect OAuth" prompt rather than an opaque "Internal Server Error".
|
||||
const errorMsg =
|
||||
"Missing Google projectId for Antigravity account. Please reconnect OAuth in Providers → Antigravity so OmniRoute can fetch your Cloud Code project.";
|
||||
const errorBody = {
|
||||
error: {
|
||||
message: errorMsg,
|
||||
type: "oauth_missing_project_id",
|
||||
code: "missing_project_id",
|
||||
},
|
||||
};
|
||||
const resp = new Response(JSON.stringify(errorBody), {
|
||||
status: 422,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
// Returning a Response object signals the executor to stop and forward it
|
||||
return resp as unknown as never;
|
||||
}
|
||||
|
||||
// Fix contents for Claude models via Antigravity
|
||||
|
||||
@@ -2,6 +2,20 @@ import { HTTP_STATUS, FETCH_TIMEOUT_MS } from "../config/constants.ts";
|
||||
import { applyFingerprint, isCliCompatEnabled } from "../config/cliFingerprints.ts";
|
||||
import { getRotatingApiKey } from "../services/apiKeyRotator.ts";
|
||||
|
||||
/**
|
||||
* Sanitizes a custom API path to prevent path traversal attacks.
|
||||
* Valid paths must start with '/', contain no '..' segments,
|
||||
* no null bytes, and be reasonable in length.
|
||||
*/
|
||||
function sanitizePath(path: string): boolean {
|
||||
if (typeof path !== "string") return false;
|
||||
if (!path.startsWith("/")) return false;
|
||||
if (path.includes("\0")) return false; // null byte
|
||||
if (path.includes("..")) return false; // path traversal
|
||||
if (path.length > 512) return false; // sanity limit
|
||||
return true;
|
||||
}
|
||||
|
||||
type JsonRecord = Record<string, unknown>;
|
||||
|
||||
export type ProviderConfig = {
|
||||
@@ -103,7 +117,9 @@ export class BaseExecutor {
|
||||
const psd = credentials?.providerSpecificData;
|
||||
const baseUrl = typeof psd?.baseUrl === "string" ? psd.baseUrl : "https://api.openai.com/v1";
|
||||
const normalized = baseUrl.replace(/\/$/, "");
|
||||
const customPath = typeof psd?.chatPath === "string" && psd.chatPath ? psd.chatPath : null;
|
||||
// Sanitize custom path: must start with '/', no path traversal, no null bytes
|
||||
const rawPath = typeof psd?.chatPath === "string" && psd.chatPath ? psd.chatPath : null;
|
||||
const customPath = rawPath && sanitizePath(rawPath) ? rawPath : null;
|
||||
if (customPath) return `${normalized}${customPath}`;
|
||||
const path = this.provider.includes("responses") ? "/responses" : "/chat/completions";
|
||||
return `${normalized}${path}`;
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
import { BaseExecutor } from "./base.ts";
|
||||
import { PROVIDERS } from "../config/constants.ts";
|
||||
|
||||
/**
|
||||
* CloudflareAIExecutor — handles dynamic URL construction with accountId.
|
||||
* Cloudflare Workers AI uses the authenticated user's account ID in the URL.
|
||||
*
|
||||
* URL pattern: https://api.cloudflare.com/client/v4/accounts/{accountId}/ai/v1/chat/completions
|
||||
* Auth: Bearer <API Token>
|
||||
* Docs: https://developers.cloudflare.com/workers-ai/
|
||||
*
|
||||
* Free tier: 10,000 Neurons/day = ~150 LLM responses or 500s Whisper audio
|
||||
* API Token: dash.cloudflare.com/profile/api-tokens
|
||||
* Account ID: right sidebar of dash.cloudflare.com
|
||||
*/
|
||||
export class CloudflareAIExecutor extends BaseExecutor {
|
||||
constructor() {
|
||||
super("cloudflare-ai", PROVIDERS["cloudflare-ai"] || { format: "openai" });
|
||||
}
|
||||
|
||||
buildUrl(_model: string, _stream: boolean, _urlIndex = 0, credentials: any = null): string {
|
||||
// Account ID can be stored in providerSpecificData or at top level credentials
|
||||
const accountId =
|
||||
credentials?.providerSpecificData?.accountId ||
|
||||
credentials?.accountId ||
|
||||
process.env.CLOUDFLARE_ACCOUNT_ID;
|
||||
|
||||
if (!accountId) {
|
||||
throw new Error(
|
||||
"Cloudflare Workers AI requires an Account ID. " +
|
||||
"Add it in provider settings under 'Account ID'. " +
|
||||
"Find it at: https://dash.cloudflare.com (right sidebar)."
|
||||
);
|
||||
}
|
||||
|
||||
return `https://api.cloudflare.com/client/v4/accounts/${accountId}/ai/v1/chat/completions`;
|
||||
}
|
||||
|
||||
buildHeaders(credentials: any, stream = true): Record<string, string> {
|
||||
const headers: Record<string, string> = {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${credentials.apiKey || credentials.accessToken}`,
|
||||
};
|
||||
|
||||
if (stream) {
|
||||
headers["Accept"] = "text/event-stream";
|
||||
}
|
||||
|
||||
return headers;
|
||||
}
|
||||
|
||||
transformRequest(_model: string, body: any, _stream: boolean, _credentials: any): any {
|
||||
// Cloudflare uses full model paths like @cf/meta/llama-3.3-70b-instruct
|
||||
// No transformation needed — user sends the full Cloudflare model path.
|
||||
return body;
|
||||
}
|
||||
}
|
||||
|
||||
export default CloudflareAIExecutor;
|
||||
@@ -80,18 +80,14 @@ export class DefaultExecutor extends BaseExecutor {
|
||||
}
|
||||
|
||||
/**
|
||||
* For compatible providers, ensure the model name sent upstream
|
||||
* is the clean model name without internal routing prefixes.
|
||||
* e.g. "openapi-chat-anti/claude-opus-4-6-thinking" → "claude-opus-4-6-thinking"
|
||||
* For compatible providers, the model name is already clean by the time
|
||||
* it reaches the executor (chatCore sets body.model = modelInfo.model,
|
||||
* which is the parsed model ID without internal routing prefixes).
|
||||
*
|
||||
* Models may legitimately contain "/" as part of their ID (e.g. "zai-org/GLM-5-FP8",
|
||||
* "org/model-name") — we must NOT strip path segments. (Fix #493)
|
||||
*/
|
||||
transformRequest(model, body, stream, credentials) {
|
||||
if (
|
||||
this.provider?.startsWith?.("openai-compatible-") ||
|
||||
this.provider?.startsWith?.("anthropic-compatible-")
|
||||
) {
|
||||
const cleanModel = model.includes("/") ? model.split("/").slice(1).join("/") : model;
|
||||
return { ...body, model: cleanModel };
|
||||
}
|
||||
return body;
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,9 @@ import { KiroExecutor } from "./kiro.ts";
|
||||
import { CodexExecutor } from "./codex.ts";
|
||||
import { CursorExecutor } from "./cursor.ts";
|
||||
import { DefaultExecutor } from "./default.ts";
|
||||
import { PollinationsExecutor } from "./pollinations.ts";
|
||||
import { CloudflareAIExecutor } from "./cloudflare-ai.ts";
|
||||
import { OpencodeExecutor } from "./opencode.ts";
|
||||
|
||||
const executors = {
|
||||
antigravity: new AntigravityExecutor(),
|
||||
@@ -16,6 +19,12 @@ const executors = {
|
||||
codex: new CodexExecutor(),
|
||||
cursor: new CursorExecutor(),
|
||||
cu: new CursorExecutor(), // Alias for cursor
|
||||
pollinations: new PollinationsExecutor(),
|
||||
pol: new PollinationsExecutor(), // Alias
|
||||
"cloudflare-ai": new CloudflareAIExecutor(),
|
||||
cf: new CloudflareAIExecutor(), // Alias
|
||||
"opencode-zen": new OpencodeExecutor("opencode-zen"),
|
||||
"opencode-go": new OpencodeExecutor("opencode-go"),
|
||||
};
|
||||
|
||||
const defaultCache = new Map();
|
||||
@@ -39,3 +48,6 @@ export { KiroExecutor } from "./kiro.ts";
|
||||
export { CodexExecutor } from "./codex.ts";
|
||||
export { CursorExecutor } from "./cursor.ts";
|
||||
export { DefaultExecutor } from "./default.ts";
|
||||
export { PollinationsExecutor } from "./pollinations.ts";
|
||||
export { CloudflareAIExecutor } from "./cloudflare-ai.ts";
|
||||
export { OpencodeExecutor } from "./opencode.ts";
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
import { BaseExecutor, type ExecuteInput, type ProviderCredentials } from "./base.ts";
|
||||
import { PROVIDERS } from "../config/constants.ts";
|
||||
import { getModelTargetFormat } from "../config/providerModels.ts";
|
||||
|
||||
export class OpencodeExecutor extends BaseExecutor {
|
||||
_requestFormat: string | null = null;
|
||||
|
||||
constructor(provider: string) {
|
||||
super(provider, PROVIDERS[provider] || PROVIDERS.openai);
|
||||
}
|
||||
|
||||
async execute(input: ExecuteInput) {
|
||||
this._requestFormat = getModelTargetFormat(this.provider, input.model) || "openai";
|
||||
try {
|
||||
return await super.execute(input);
|
||||
} finally {
|
||||
this._requestFormat = null;
|
||||
}
|
||||
}
|
||||
|
||||
buildUrl(
|
||||
model: string,
|
||||
stream: boolean,
|
||||
urlIndex = 0,
|
||||
credentials: ProviderCredentials | null = null
|
||||
) {
|
||||
void urlIndex;
|
||||
void credentials;
|
||||
|
||||
const base = this.config.baseUrl;
|
||||
switch (this._requestFormat) {
|
||||
case "claude":
|
||||
return `${base}/messages`;
|
||||
case "openai-responses":
|
||||
return `${base}/responses`;
|
||||
case "gemini":
|
||||
return `${base}/models/${model}:${stream ? "streamGenerateContent?alt=sse" : "generateContent"}`;
|
||||
default:
|
||||
return `${base}/chat/completions`;
|
||||
}
|
||||
}
|
||||
|
||||
buildHeaders(credentials: ProviderCredentials | null, stream = true) {
|
||||
const headers: Record<string, string> = { "Content-Type": "application/json" };
|
||||
const key = credentials?.apiKey || credentials?.accessToken;
|
||||
|
||||
if (key) {
|
||||
headers["Authorization"] = `Bearer ${key}`;
|
||||
}
|
||||
|
||||
if (this._requestFormat === "claude") {
|
||||
headers["anthropic-version"] = "2023-06-01";
|
||||
}
|
||||
|
||||
if (stream) {
|
||||
headers["Accept"] = "text/event-stream";
|
||||
}
|
||||
|
||||
return headers;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import { BaseExecutor } from "./base.ts";
|
||||
import { PROVIDERS } from "../config/constants.ts";
|
||||
|
||||
/**
|
||||
* PollinationsExecutor — handles optional API key auth.
|
||||
* Pollinations AI works WITHOUT any API key for basic use (1 req/15s).
|
||||
* If an API key is provided, higher rate limits apply.
|
||||
*
|
||||
* Endpoint: https://text.pollinations.ai/openai/chat/completions
|
||||
* Docs: https://pollinations.ai/docs
|
||||
*/
|
||||
export class PollinationsExecutor extends BaseExecutor {
|
||||
constructor() {
|
||||
super("pollinations", PROVIDERS["pollinations"] || { format: "openai" });
|
||||
}
|
||||
|
||||
buildUrl(_model: string, _stream: boolean, _urlIndex = 0, _credentials = null): string {
|
||||
return "https://text.pollinations.ai/openai/chat/completions";
|
||||
}
|
||||
|
||||
buildHeaders(credentials: any, stream = true): Record<string, string> {
|
||||
const headers: Record<string, string> = {
|
||||
"Content-Type": "application/json",
|
||||
};
|
||||
|
||||
// API key is OPTIONAL — skip Authorization header if no key provided
|
||||
const key = credentials?.apiKey || credentials?.accessToken;
|
||||
if (key) {
|
||||
headers["Authorization"] = `Bearer ${key}`;
|
||||
}
|
||||
|
||||
if (stream) {
|
||||
headers["Accept"] = "text/event-stream";
|
||||
}
|
||||
|
||||
return headers;
|
||||
}
|
||||
|
||||
transformRequest(model: string, body: any, _stream: boolean, _credentials: any): any {
|
||||
// Pollinations uses model names directly like "openai", "claude", "deepseek", etc.
|
||||
// No transformation needed — the model name is already the Pollinations alias.
|
||||
return body;
|
||||
}
|
||||
}
|
||||
|
||||
export default PollinationsExecutor;
|
||||
@@ -28,13 +28,17 @@ function upstreamErrorResponse(res, errText) {
|
||||
let errorMessage: string;
|
||||
try {
|
||||
const parsed = JSON.parse(errText);
|
||||
errorMessage =
|
||||
// Extract a human-readable message from various error response shapes.
|
||||
// Guard against `parsed.error` being an object (e.g. ElevenLabs returns
|
||||
// { error: { message: "...", status_code: 401 } } or { detail: { ... } })
|
||||
const raw =
|
||||
parsed?.err_msg ||
|
||||
parsed?.error?.message ||
|
||||
parsed?.error ||
|
||||
(typeof parsed?.error === "string" ? parsed.error : null) ||
|
||||
parsed?.message ||
|
||||
parsed?.detail ||
|
||||
errText;
|
||||
(typeof parsed?.detail === "string" ? parsed.detail : parsed?.detail?.message) ||
|
||||
null;
|
||||
errorMessage = raw ? String(raw) : errText || `Upstream error (${res.status})`;
|
||||
} catch {
|
||||
errorMessage = errText || `Upstream error (${res.status})`;
|
||||
}
|
||||
|
||||
@@ -34,13 +34,15 @@ function upstreamErrorResponse(res, errText) {
|
||||
let errorMessage: string;
|
||||
try {
|
||||
const parsed = JSON.parse(errText);
|
||||
errorMessage =
|
||||
// Guard against `parsed.error` or `parsed.detail` being objects
|
||||
const raw =
|
||||
parsed?.err_msg ||
|
||||
parsed?.error?.message ||
|
||||
parsed?.error ||
|
||||
(typeof parsed?.error === "string" ? parsed.error : null) ||
|
||||
parsed?.message ||
|
||||
parsed?.detail ||
|
||||
errText;
|
||||
(typeof parsed?.detail === "string" ? parsed.detail : parsed?.detail?.message) ||
|
||||
null;
|
||||
errorMessage = raw ? String(raw) : errText || `Upstream error (${res.status})`;
|
||||
} catch {
|
||||
errorMessage = errText || `Upstream error (${res.status})`;
|
||||
}
|
||||
@@ -65,13 +67,67 @@ function getUploadedFileName(file: Blob & { name?: unknown }): string {
|
||||
return typeof file.name === "string" && file.name.length > 0 ? file.name : "audio.wav";
|
||||
}
|
||||
|
||||
/**
|
||||
* Infer a suitable Content-Type for Deepgram from the browser-provided MIME
|
||||
* type and the original filename. Deepgram accepts `audio/*` and many raw
|
||||
* formats, but `video/*` causes it to silently fail with "no speech detected".
|
||||
*
|
||||
* Strategy:
|
||||
* 1. If the browser says `audio/*`, keep it as-is.
|
||||
* 2. If it's `video/*` (e.g. `.mp4`), remap to the audio equivalent so
|
||||
* Deepgram extracts the audio track. `.mp4` → `audio/mp4`, etc.
|
||||
* 3. Fall back to `application/octet-stream` which tells Deepgram to
|
||||
* auto-detect from the raw bytes (most reliable for unknown formats).
|
||||
*/
|
||||
function resolveAudioContentType(file: Blob & { name?: unknown }): string {
|
||||
const browserType = (file.type || "").toLowerCase();
|
||||
const fileName = typeof file.name === "string" ? file.name.toLowerCase() : "";
|
||||
|
||||
// 1) Browser already says it's audio — trust it
|
||||
if (browserType.startsWith("audio/")) return browserType;
|
||||
|
||||
// 2) Derive from file extension (covers video/* and empty MIME)
|
||||
const ext = fileName.includes(".") ? fileName.split(".").pop() : "";
|
||||
const EXT_TO_MIME: Record<string, string> = {
|
||||
mp3: "audio/mpeg",
|
||||
mp4: "audio/mp4",
|
||||
m4a: "audio/mp4",
|
||||
wav: "audio/wav",
|
||||
ogg: "audio/ogg",
|
||||
flac: "audio/flac",
|
||||
webm: "audio/webm",
|
||||
aac: "audio/aac",
|
||||
wma: "audio/x-ms-wma",
|
||||
opus: "audio/opus",
|
||||
};
|
||||
if (ext && EXT_TO_MIME[ext]) return EXT_TO_MIME[ext];
|
||||
|
||||
// 3) Fallback — let Deepgram auto-detect from raw bytes
|
||||
return "application/octet-stream";
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle Deepgram transcription (raw binary audio, model via query param)
|
||||
*/
|
||||
async function handleDeepgramTranscription(providerConfig, file, modelId, token) {
|
||||
async function handleDeepgramTranscription(
|
||||
providerConfig,
|
||||
file,
|
||||
modelId,
|
||||
token,
|
||||
formData?: FormData
|
||||
) {
|
||||
const url = new URL(providerConfig.baseUrl);
|
||||
url.searchParams.set("model", modelId);
|
||||
url.searchParams.set("smart_format", "true");
|
||||
url.searchParams.set("punctuate", "true");
|
||||
|
||||
// Language: if caller specified one, use it; otherwise let Deepgram auto-detect
|
||||
const langParam = formData?.get("language");
|
||||
if (typeof langParam === "string" && langParam.trim()) {
|
||||
url.searchParams.set("language", langParam.trim());
|
||||
} else {
|
||||
url.searchParams.set("detect_language", "true");
|
||||
}
|
||||
|
||||
const arrayBuffer = await file.arrayBuffer();
|
||||
|
||||
@@ -79,7 +135,7 @@ async function handleDeepgramTranscription(providerConfig, file, modelId, token)
|
||||
method: "POST",
|
||||
headers: {
|
||||
...buildAuthHeaders(providerConfig, token),
|
||||
"Content-Type": file.type || "audio/wav",
|
||||
"Content-Type": resolveAudioContentType(file),
|
||||
},
|
||||
body: arrayBuffer,
|
||||
});
|
||||
@@ -212,7 +268,7 @@ async function handleHuggingFaceTranscription(providerConfig, file, modelId, tok
|
||||
method: "POST",
|
||||
headers: {
|
||||
...buildAuthHeaders(providerConfig, token),
|
||||
"Content-Type": file.type || "audio/wav",
|
||||
"Content-Type": resolveAudioContentType(file),
|
||||
},
|
||||
body: arrayBuffer,
|
||||
});
|
||||
@@ -283,7 +339,7 @@ export async function handleAudioTranscription({
|
||||
|
||||
// Route to provider-specific handler
|
||||
if (providerConfig.format === "deepgram") {
|
||||
return handleDeepgramTranscription(providerConfig, file, modelId, token);
|
||||
return handleDeepgramTranscription(providerConfig, file, modelId, token, formData);
|
||||
}
|
||||
|
||||
if (providerConfig.format === "assemblyai") {
|
||||
|
||||
@@ -308,6 +308,27 @@ export async function handleChatCore({
|
||||
}
|
||||
return [];
|
||||
}
|
||||
// (#527) tool_result → convert to text instead of dropping.
|
||||
// When Claude Code + superpowers routes through Codex, it sends tool_result
|
||||
// blocks in user messages. Silently dropping them causes Codex to loop
|
||||
// because it never receives the tool response and keeps re-requesting it.
|
||||
if (block.type === "tool_result") {
|
||||
const toolId = block.tool_use_id ?? block.id ?? "unknown";
|
||||
const resultContent = block.content ?? block.text ?? block.output ?? "";
|
||||
const resultText =
|
||||
typeof resultContent === "string"
|
||||
? resultContent
|
||||
: Array.isArray(resultContent)
|
||||
? resultContent
|
||||
.filter((c: Record<string, unknown>) => c.type === "text")
|
||||
.map((c: Record<string, unknown>) => c.text)
|
||||
.join("\n")
|
||||
: JSON.stringify(resultContent);
|
||||
if (resultText.length > 0) {
|
||||
return [{ type: "text", text: `[Tool Result: ${toolId}]\n${resultText}` }];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
// Unknown types: drop silently
|
||||
log?.debug?.("CONTENT", `Dropped unsupported content part type="${block.type}"`);
|
||||
return [];
|
||||
@@ -317,10 +338,15 @@ export async function handleChatCore({
|
||||
}
|
||||
}
|
||||
|
||||
const normalizeToolCallId = getModelNormalizeToolCallId(provider || "", model || "");
|
||||
const normalizeToolCallId = getModelNormalizeToolCallId(
|
||||
provider || "",
|
||||
model || "",
|
||||
sourceFormat
|
||||
);
|
||||
const preserveDeveloperRole = getModelPreserveOpenAIDeveloperRole(
|
||||
provider || "",
|
||||
model || ""
|
||||
model || "",
|
||||
sourceFormat
|
||||
);
|
||||
translatedBody = translateRequest(
|
||||
sourceFormat,
|
||||
|
||||
@@ -447,8 +447,10 @@ export async function handleComboChat({
|
||||
const handleSingleModelWrapped = combo.context_cache_protection
|
||||
? async (b, modelStr) => {
|
||||
const res = await handleSingleModel(b, modelStr);
|
||||
// Inject tag only on success and only for non-streaming non-binary responses
|
||||
if (res.ok && !b.stream) {
|
||||
if (!res.ok) return res;
|
||||
|
||||
// Non-streaming: inject tag into JSON response (existing logic)
|
||||
if (!b.stream) {
|
||||
try {
|
||||
const json = await res.clone().json();
|
||||
const msgs = Array.isArray(json?.messages) ? json.messages : [];
|
||||
@@ -460,10 +462,71 @@ export async function handleComboChat({
|
||||
});
|
||||
}
|
||||
} catch {
|
||||
/* non-JSON or stream — skip tagging */
|
||||
/* non-JSON — skip tagging */
|
||||
}
|
||||
return res;
|
||||
}
|
||||
return res;
|
||||
|
||||
// Streaming (Fix #490 + #511): prepend omniModel tag into the first
|
||||
// non-empty content chunk so it arrives BEFORE finish_reason:stop.
|
||||
// SDKs close the connection on finish_reason, so anything sent after
|
||||
// that marker is silently dropped.
|
||||
if (!res.body) return res;
|
||||
const tagContent = `\\n<omniModel>${modelStr}</omniModel>\\n`;
|
||||
const encoder = new TextEncoder();
|
||||
const decoder = new TextDecoder();
|
||||
let tagInjected = false;
|
||||
|
||||
const transform = new TransformStream({
|
||||
transform(chunk, controller) {
|
||||
if (tagInjected) {
|
||||
// Already injected — passthrough
|
||||
controller.enqueue(chunk);
|
||||
return;
|
||||
}
|
||||
|
||||
const text = decoder.decode(chunk, { stream: true });
|
||||
|
||||
// Look for the first SSE data line with non-empty content
|
||||
// Pattern: "content":"<non-empty>" — we inject tag at the start
|
||||
const contentMatch = text.match(/"content":"([^"]+)/);
|
||||
if (contentMatch) {
|
||||
// Inject tag at the beginning of the first content value
|
||||
const injected = text.replace(
|
||||
/"content":"([^"]+)/,
|
||||
`"content":"${tagContent.replace(/"/g, '\\"')}$1`
|
||||
);
|
||||
tagInjected = true;
|
||||
controller.enqueue(encoder.encode(injected));
|
||||
return;
|
||||
}
|
||||
|
||||
// No content yet — passthrough
|
||||
controller.enqueue(chunk);
|
||||
},
|
||||
flush(controller) {
|
||||
// If stream ends without ever finding content (edge case),
|
||||
// inject tag as a standalone chunk before the stream closes
|
||||
if (!tagInjected) {
|
||||
const tagChunk = `data: ${JSON.stringify({
|
||||
choices: [
|
||||
{
|
||||
delta: { content: tagContent },
|
||||
index: 0,
|
||||
finish_reason: null,
|
||||
},
|
||||
],
|
||||
})}\n\n`;
|
||||
controller.enqueue(encoder.encode(tagChunk));
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const transformedStream = res.body.pipeThrough(transform);
|
||||
return new Response(transformedStream, {
|
||||
status: res.status,
|
||||
headers: res.headers,
|
||||
});
|
||||
}
|
||||
: handleSingleModel;
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -34,7 +34,11 @@ interface Message {
|
||||
|
||||
// ── Context Caching Tag ─────────────────────────────────────────────────────
|
||||
|
||||
const CACHE_TAG_PATTERN = /<omniModel>([^<]+)<\/omniModel>/;
|
||||
// Handles both actual newlines (U+000A) and literal \n sequences injected
|
||||
// by combo.ts streaming around the <omniModel> tag (#531). Non-global so that
|
||||
// .exec() and .test() stay stateless; callers that need full replacement use
|
||||
// String.prototype.replace() which replaces all non-overlapping matches.
|
||||
const CACHE_TAG_PATTERN = /(?:\\n|\n)?<omniModel>([^<]+)<\/omniModel>(?:\\n|\n)?/;
|
||||
|
||||
/**
|
||||
* Inject the model tag into the last assistant message (or append a new one).
|
||||
@@ -165,7 +169,11 @@ export function applyComboAgentMiddleware(
|
||||
if (comboConfig.context_cache_protection) {
|
||||
pinnedModel = extractPinnedModel(messages);
|
||||
if (pinnedModel) {
|
||||
// Model is pinned — caller should override model selection
|
||||
// (#535) Model is pinned via <omniModel> tag — override body.model so the combo
|
||||
// router uses exactly this model instead of picking a different one. Without this,
|
||||
// the extracted pinnedModel is returned but body.model is unchanged, breaking
|
||||
// context cache sessions by sending subsequent turns to a different model.
|
||||
body = { ...body, model: pinnedModel };
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -93,10 +93,11 @@ export function convertResponsesApiFormat(body) {
|
||||
}
|
||||
|
||||
// Cleanup Responses API specific fields
|
||||
// Note: prompt_cache_key is intentionally preserved — it is used by Codex and other
|
||||
// providers as a cache-affinity signal. Stripping it breaks prompt caching (#517).
|
||||
delete result.input;
|
||||
delete result.instructions;
|
||||
delete result.include;
|
||||
delete result.prompt_cache_key;
|
||||
delete result.store;
|
||||
delete result.reasoning;
|
||||
|
||||
|
||||
@@ -227,10 +227,11 @@ export function openaiResponsesToOpenAIRequest(
|
||||
});
|
||||
|
||||
// Cleanup Responses API specific fields
|
||||
// Note: prompt_cache_key is intentionally preserved — it is used by Codex and other
|
||||
// providers as a cache-affinity signal. Stripping it breaks prompt caching (#517).
|
||||
delete result.input;
|
||||
delete result.instructions;
|
||||
delete result.include;
|
||||
delete result.prompt_cache_key;
|
||||
delete result.store;
|
||||
delete result.reasoning;
|
||||
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "omniroute",
|
||||
"version": "2.8.9",
|
||||
"version": "3.0.0-rc.2",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "omniroute",
|
||||
"version": "2.8.9",
|
||||
"version": "3.0.0-rc.2",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"workspaces": [
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "omniroute",
|
||||
"version": "2.8.9",
|
||||
"version": "3.0.0-rc.2",
|
||||
"description": "Smart AI Router with auto fallback — route to FREE & cheap models, zero downtime. Works with Cursor, Cline, Claude Desktop, Codex, and any OpenAI-compatible tool.",
|
||||
"type": "module",
|
||||
"bin": {
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 38 KiB |
@@ -0,0 +1 @@
|
||||
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Cloudflare</title><path d="M16.5088 16.8447c.1475-.5068.0908-.9707-.1553-1.3154-.2246-.3164-.6045-.499-1.0615-.5205l-8.6592-.1123a.1559.1559 0 0 1-.1333-.0713c-.0283-.042-.0351-.0986-.021-.1553.0278-.084.1123-.1484.2036-.1562l8.7359-.1123c1.0351-.0489 2.1601-.8868 2.5537-1.9136l.499-1.3013c.0215-.0561.0293-.1128.0147-.168-.5625-2.5463-2.835-4.4453-5.5499-4.4453-2.5039 0-4.6284 1.6177-5.3876 3.8614-.4927-.3658-1.1187-.5625-1.794-.499-1.2026.119-2.1665 1.083-2.2861 2.2856-.0283.31-.0069.6128.0635.894C1.5683 13.171 0 14.7754 0 16.752c0 .1748.0142.3515.0352.5273.0141.083.0844.1475.1689.1475h15.9814c.0909 0 .1758-.0645.2032-.1553l.12-.4268zm2.7568-5.5634c-.0771 0-.1611 0-.2383.0112-.0566 0-.1054.0415-.127.0976l-.3378 1.1744c-.1475.5068-.0918.9707.1543 1.3164.2256.3164.6055.498 1.0625.5195l1.8437.1133c.0557 0 .1055.0263.1329.0703.0283.043.0351.1074.0214.1562-.0283.084-.1132.1485-.204.1553l-1.921.1123c-1.041.0488-2.1582.8867-2.5527 1.914l-.1406.3585c-.0283.0713.0215.1416.0986.1416h6.5977c.0771 0 .1474-.0489.169-.126.1122-.4082.1757-.837.1757-1.2803 0-2.6025-2.125-4.727-4.7344-4.727"/></svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 14 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 18 KiB |
@@ -0,0 +1 @@
|
||||
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Scaleway</title><path d="M16.605 11.11v5.72a1.77 1.77 0 01-1.54 1.69h-4a1.43 1.43 0 01-1.31-1.22 1.09 1.09 0 010-.18 1.37 1.37 0 011.37-1.36h1.74a1 1 0 001-1v-3.62a1.4 1.4 0 011.18-1.39h.17a1.37 1.37 0 011.39 1.36zm-6.46 1.74V9.26a1 1 0 011-1h1.85a1.37 1.37 0 001.37-1.37 1 1 0 000-.17 1.45 1.45 0 00-1.41-1.2h-3.96a1.81 1.81 0 00-1.58 1.66v5.7a1.37 1.37 0 001.37 1.37h.21a1.4 1.4 0 001.15-1.4zm12-4.29V20a4.53 4.53 0 01-4.15 4h-7.58a8.57 8.57 0 01-8.56-8.57V4.54A4.54 4.54 0 016.395 0h7.18a8.56 8.56 0 018.56 8.56zm-2.74 0a5.83 5.83 0 00-5.82-5.82h-7.19a1.79 1.79 0 00-1.8 1.8v10.89a5.83 5.83 0 005.82 5.8h7.44a1.79 1.79 0 001.54-1.48z"/></svg>
|
||||
|
After Width: | Height: | Size: 723 B |
@@ -48,7 +48,8 @@ function extractChangelogSections(content) {
|
||||
}
|
||||
|
||||
function isSemver(value) {
|
||||
return /^\d+\.\d+\.\d+$/.test(value);
|
||||
// Accept X.Y.Z and X.Y.Z-prerelease.N (e.g. 3.0.0-rc.1, 3.0.0-beta.2)
|
||||
return /^\d+\.\d+\.\d+(-[a-zA-Z0-9.]+)?$/.test(value);
|
||||
}
|
||||
|
||||
let hasFailure = false;
|
||||
|
||||
@@ -0,0 +1,196 @@
|
||||
/**
|
||||
* Search Analytics Tab
|
||||
*
|
||||
* Shows search request stats from call_logs (request_type = 'search'),
|
||||
* provider breakdown, cache hit rate, and cost summary.
|
||||
*/
|
||||
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
interface SearchStats {
|
||||
total: number;
|
||||
today: number;
|
||||
cached: number;
|
||||
errors: number;
|
||||
totalCostUsd: number;
|
||||
byProvider: Record<string, { count: number; costUsd: number }>;
|
||||
last24h: Array<{ hour: string; count: number }>;
|
||||
cacheHitRate: number;
|
||||
avgDurationMs: number;
|
||||
}
|
||||
|
||||
function StatCard({
|
||||
icon,
|
||||
label,
|
||||
value,
|
||||
sub,
|
||||
}: {
|
||||
icon: string;
|
||||
label: string;
|
||||
value: string | number;
|
||||
sub?: string;
|
||||
}) {
|
||||
return (
|
||||
<div className="card p-4 flex flex-col gap-1">
|
||||
<div className="flex items-center gap-2 text-text-muted text-sm">
|
||||
<span className="material-symbols-outlined text-[18px]">{icon}</span>
|
||||
{label}
|
||||
</div>
|
||||
<div className="text-2xl font-bold text-text">{value}</div>
|
||||
{sub && <div className="text-xs text-text-muted">{sub}</div>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ProviderBar({
|
||||
provider,
|
||||
count,
|
||||
total,
|
||||
costUsd,
|
||||
}: {
|
||||
provider: string;
|
||||
count: number;
|
||||
total: number;
|
||||
costUsd: number;
|
||||
}) {
|
||||
const pct = total > 0 ? Math.round((count / total) * 100) : 0;
|
||||
return (
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="font-medium text-text">{provider}</span>
|
||||
<span className="text-text-muted">
|
||||
{count} queries · ${costUsd.toFixed(4)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="h-2 rounded-full bg-bg-muted overflow-hidden">
|
||||
<div
|
||||
className="h-full rounded-full bg-primary transition-all"
|
||||
style={{ width: `${pct}%` }}
|
||||
/>
|
||||
</div>
|
||||
<div className="text-xs text-text-muted text-right">{pct}%</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function SearchAnalyticsTab() {
|
||||
const [stats, setStats] = useState<SearchStats | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
fetch("/api/v1/search/analytics")
|
||||
.then((r) => r.json())
|
||||
.then((d) => {
|
||||
setStats(d);
|
||||
setLoading(false);
|
||||
})
|
||||
.catch((e) => {
|
||||
setError(e.message);
|
||||
setLoading(false);
|
||||
});
|
||||
}, []);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center py-16 text-text-muted">
|
||||
<span className="material-symbols-outlined animate-spin mr-2">progress_activity</span>
|
||||
Loading search analytics…
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error || !stats) {
|
||||
return (
|
||||
<div className="card p-6 text-center text-text-muted">
|
||||
<span className="material-symbols-outlined text-[32px] mb-2 block">search_off</span>
|
||||
{error || "No search data available yet."}
|
||||
<p className="text-xs mt-2">
|
||||
Search requests will appear here after the first search via /v1/search.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const providers = Object.entries(stats.byProvider).sort(([, a], [, b]) => b.count - a.count);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-6">
|
||||
{/* KPI Cards */}
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
<StatCard
|
||||
icon="manage_search"
|
||||
label="Total Searches"
|
||||
value={stats.total.toLocaleString()}
|
||||
sub={`${stats.today} today`}
|
||||
/>
|
||||
<StatCard
|
||||
icon="cached"
|
||||
label="Cache Hit Rate"
|
||||
value={`${stats.cacheHitRate}%`}
|
||||
sub={`${stats.cached} cached requests`}
|
||||
/>
|
||||
<StatCard
|
||||
icon="attach_money"
|
||||
label="Total Cost"
|
||||
value={`$${stats.totalCostUsd.toFixed(4)}`}
|
||||
sub="search API costs"
|
||||
/>
|
||||
<StatCard
|
||||
icon="timer"
|
||||
label="Avg Response"
|
||||
value={`${stats.avgDurationMs}ms`}
|
||||
sub={stats.errors > 0 ? `${stats.errors} errors` : "No errors"}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Provider Breakdown */}
|
||||
{providers.length > 0 && (
|
||||
<div className="card p-5">
|
||||
<h3 className="font-semibold text-text mb-4 flex items-center gap-2">
|
||||
<span className="material-symbols-outlined text-primary text-[20px]">hub</span>
|
||||
Provider Breakdown
|
||||
</h3>
|
||||
<div className="flex flex-col gap-4">
|
||||
{providers.map(([prov, data]) => (
|
||||
<ProviderBar
|
||||
key={prov}
|
||||
provider={prov}
|
||||
count={data.count}
|
||||
total={stats.total}
|
||||
costUsd={data.costUsd}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Empty state */}
|
||||
{stats.total === 0 && (
|
||||
<div className="card p-8 text-center text-text-muted">
|
||||
<span className="material-symbols-outlined text-[48px] mb-3 block text-primary opacity-50">
|
||||
travel_explore
|
||||
</span>
|
||||
<p className="font-medium text-text">No searches yet</p>
|
||||
<p className="text-sm mt-1">
|
||||
Use <code className="bg-bg-muted px-1 rounded">POST /v1/search</code> to start routing
|
||||
web searches.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Free tier note */}
|
||||
<div className="text-xs text-text-muted border border-border rounded-lg p-3 flex items-start gap-2">
|
||||
<span className="material-symbols-outlined text-[16px] text-green-500 mt-0.5">
|
||||
check_circle
|
||||
</span>
|
||||
<span>
|
||||
<strong>Free tier available:</strong> Serper (2,500/mo), Brave (2,000/mo), Exa (1,000/mo),
|
||||
Tavily (1,000/mo) — total 6,500+ free searches/month with automatic failover.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -3,15 +3,17 @@
|
||||
import { useState, Suspense } from "react";
|
||||
import { UsageAnalytics, CardSkeleton, SegmentedControl } from "@/shared/components";
|
||||
import EvalsTab from "../usage/components/EvalsTab";
|
||||
import SearchAnalyticsTab from "./SearchAnalyticsTab";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
export default function AnalyticsPage() {
|
||||
const [activeTab, setActiveTab] = useState("overview");
|
||||
const t = useTranslations("analytics");
|
||||
|
||||
const tabDescriptions = {
|
||||
const tabDescriptions: Record<string, string> = {
|
||||
overview: t("overviewDescription"),
|
||||
evals: t("evalsDescription"),
|
||||
search: "Search request analytics — provider breakdown, cache hit rate, and cost tracking.",
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -29,6 +31,7 @@ export default function AnalyticsPage() {
|
||||
options={[
|
||||
{ value: "overview", label: t("overview") },
|
||||
{ value: "evals", label: t("evals") },
|
||||
{ value: "search", label: "Search" },
|
||||
]}
|
||||
value={activeTab}
|
||||
onChange={setActiveTab}
|
||||
@@ -40,6 +43,7 @@ export default function AnalyticsPage() {
|
||||
</Suspense>
|
||||
)}
|
||||
{activeTab === "evals" && <EvalsTab />}
|
||||
{activeTab === "search" && <SearchAnalyticsTab />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -523,15 +523,12 @@ export default function ApiManagerPageClient() {
|
||||
</div>
|
||||
<div className="col-span-3 flex items-center gap-1.5">
|
||||
<code className="text-sm text-text-muted font-mono truncate">{key.key}</code>
|
||||
<button
|
||||
onClick={() => copy(key.key, key.id)}
|
||||
className="p-1 hover:bg-black/5 dark:hover:bg-white/5 rounded text-text-muted hover:text-primary opacity-0 group-hover:opacity-100 transition-all shrink-0"
|
||||
title={t("copyMaskedKey")}
|
||||
<span
|
||||
className="p-1 text-text-muted/40 opacity-0 group-hover:opacity-100 transition-all shrink-0 cursor-help"
|
||||
title={t("keyOnlyAvailableAtCreation")}
|
||||
>
|
||||
<span className="material-symbols-outlined text-[14px]">
|
||||
{copied === key.id ? "check" : "content_copy"}
|
||||
</span>
|
||||
</button>
|
||||
<span className="material-symbols-outlined text-[14px]">lock</span>
|
||||
</span>
|
||||
</div>
|
||||
<div className="col-span-2 flex items-center">
|
||||
<div className="flex flex-col items-start gap-1">
|
||||
|
||||
@@ -58,8 +58,10 @@ export default function ClaudeToolCard({
|
||||
const effectiveConfigStatus = configStatus || batchStatus?.configStatus || null;
|
||||
|
||||
useEffect(() => {
|
||||
// (#523) Store the key *id* (not the masked string) so the backend can
|
||||
// resolve the real secret from DB before writing to settings.json.
|
||||
if (apiKeys?.length > 0 && !selectedApiKey) {
|
||||
setSelectedApiKey(apiKeys[0].key);
|
||||
setSelectedApiKey(apiKeys[0].id);
|
||||
}
|
||||
}, [apiKeys, selectedApiKey]);
|
||||
|
||||
@@ -95,10 +97,11 @@ export default function ClaudeToolCard({
|
||||
}
|
||||
}
|
||||
});
|
||||
// Only set selectedApiKey if it exists in apiKeys list
|
||||
// Restore selected key from file: match token stored in file against known keys
|
||||
const tokenFromFile = env.ANTHROPIC_AUTH_TOKEN;
|
||||
if (tokenFromFile && apiKeys?.some((k) => k.key === tokenFromFile)) {
|
||||
setSelectedApiKey(tokenFromFile);
|
||||
if (tokenFromFile) {
|
||||
const matchedKey = apiKeys?.find((k) => k.key === tokenFromFile);
|
||||
if (matchedKey) setSelectedApiKey(matchedKey.id);
|
||||
}
|
||||
}
|
||||
}, [claudeStatus, apiKeys, tool.defaultModels, onModelMappingChange]);
|
||||
@@ -132,24 +135,27 @@ export default function ClaudeToolCard({
|
||||
try {
|
||||
const env: any = { ANTHROPIC_BASE_URL: getEffectiveBaseUrl() };
|
||||
|
||||
// Get key from dropdown, fallback to first key or sk_omniroute for localhost
|
||||
const keyToUse =
|
||||
selectedApiKey?.trim() ||
|
||||
(apiKeys?.length > 0 ? apiKeys[0].key : null) ||
|
||||
(!cloudEnabled ? "sk_omniroute" : null);
|
||||
// (#523) Prefer keyId lookup so the backend writes the real key to disk.
|
||||
// Fall back to sk_omniroute for localhost-only setups without a key.
|
||||
const selectedKeyId = selectedApiKey?.trim() || (apiKeys?.length > 0 ? apiKeys[0].id : null);
|
||||
const skOmnirouteFallback = !cloudEnabled ? "sk_omniroute" : null;
|
||||
|
||||
if (keyToUse) {
|
||||
env.ANTHROPIC_AUTH_TOKEN = keyToUse;
|
||||
if (!selectedKeyId && skOmnirouteFallback) {
|
||||
env.ANTHROPIC_AUTH_TOKEN = skOmnirouteFallback;
|
||||
}
|
||||
|
||||
tool.defaultModels.forEach((model) => {
|
||||
const targetModel = modelMappings[model.alias];
|
||||
if (targetModel && model.envKey) env[model.envKey] = targetModel;
|
||||
});
|
||||
|
||||
const postBody: Record<string, unknown> = { env };
|
||||
if (selectedKeyId) postBody.keyId = selectedKeyId;
|
||||
|
||||
const res = await fetch("/api/cli-tools/claude-settings", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ env }),
|
||||
body: JSON.stringify(postBody),
|
||||
});
|
||||
const data = await res.json();
|
||||
if (res.ok) {
|
||||
@@ -412,7 +418,7 @@ export default function ClaudeToolCard({
|
||||
className="flex-1 px-2 py-1.5 bg-surface rounded text-xs border border-border focus:outline-none focus:ring-1 focus:ring-primary/50"
|
||||
>
|
||||
{apiKeys.map((key) => (
|
||||
<option key={key.id} value={key.key}>
|
||||
<option key={key.id} value={key.id}>
|
||||
{key.key}
|
||||
</option>
|
||||
))}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect, useCallback, useRef } from "react";
|
||||
import { useState, useEffect, useCallback, useRef, useMemo } from "react";
|
||||
import { useNotificationStore } from "@/store/notificationStore";
|
||||
import PropTypes from "prop-types";
|
||||
import { useParams, useRouter } from "next/navigation";
|
||||
@@ -31,6 +31,129 @@ import {
|
||||
} from "@/shared/constants/providers";
|
||||
import { getModelsByProviderId } from "@/shared/constants/models";
|
||||
import { useCopyToClipboard } from "@/shared/hooks/useCopyToClipboard";
|
||||
import {
|
||||
MODEL_COMPAT_PROTOCOL_KEYS,
|
||||
type ModelCompatProtocolKey,
|
||||
} from "@/shared/constants/modelCompat";
|
||||
|
||||
type CompatByProtocolMap = Partial<
|
||||
Record<
|
||||
ModelCompatProtocolKey,
|
||||
{ normalizeToolCallId?: boolean; preserveOpenAIDeveloperRole?: boolean }
|
||||
>
|
||||
>;
|
||||
type CompatModelRow = {
|
||||
id?: string;
|
||||
name?: string;
|
||||
source?: string;
|
||||
apiFormat?: string;
|
||||
supportedEndpoints?: string[];
|
||||
normalizeToolCallId?: boolean;
|
||||
preserveOpenAIDeveloperRole?: boolean;
|
||||
compatByProtocol?: CompatByProtocolMap;
|
||||
};
|
||||
|
||||
type CompatModelMap = Map<string, CompatModelRow>;
|
||||
|
||||
function buildCompatMap(rows: CompatModelRow[]): CompatModelMap {
|
||||
const m = new Map<string, CompatModelRow>();
|
||||
for (const r of rows) if (r.id) m.set(r.id, r);
|
||||
return m;
|
||||
}
|
||||
|
||||
function getProtoSlice(
|
||||
c: CompatModelRow | undefined,
|
||||
o: CompatModelRow | undefined,
|
||||
protocol: string
|
||||
) {
|
||||
return c?.compatByProtocol?.[protocol] ?? o?.compatByProtocol?.[protocol];
|
||||
}
|
||||
|
||||
function effectiveNormalizeForProtocol(
|
||||
modelId: string,
|
||||
protocol: string,
|
||||
customMap: CompatModelMap,
|
||||
overrideMap: CompatModelMap
|
||||
): boolean {
|
||||
const c = customMap.get(modelId);
|
||||
const o = overrideMap.get(modelId);
|
||||
const pc = getProtoSlice(c, o, protocol);
|
||||
if (pc && Object.prototype.hasOwnProperty.call(pc, "normalizeToolCallId")) {
|
||||
return Boolean(pc.normalizeToolCallId);
|
||||
}
|
||||
if (c?.normalizeToolCallId) return true;
|
||||
return Boolean(o?.normalizeToolCallId);
|
||||
}
|
||||
|
||||
function effectivePreserveForProtocol(
|
||||
modelId: string,
|
||||
protocol: string,
|
||||
customMap: CompatModelMap,
|
||||
overrideMap: CompatModelMap
|
||||
): boolean {
|
||||
const c = customMap.get(modelId);
|
||||
const o = overrideMap.get(modelId);
|
||||
const pc = getProtoSlice(c, o, protocol);
|
||||
if (pc && Object.prototype.hasOwnProperty.call(pc, "preserveOpenAIDeveloperRole")) {
|
||||
return Boolean(pc.preserveOpenAIDeveloperRole);
|
||||
}
|
||||
if (c && Object.prototype.hasOwnProperty.call(c, "preserveOpenAIDeveloperRole")) {
|
||||
return Boolean(c.preserveOpenAIDeveloperRole);
|
||||
}
|
||||
if (o && Object.prototype.hasOwnProperty.call(o, "preserveOpenAIDeveloperRole")) {
|
||||
return Boolean(o.preserveOpenAIDeveloperRole);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function anyNormalizeCompatBadge(
|
||||
modelId: string,
|
||||
customMap: CompatModelMap,
|
||||
overrideMap: CompatModelMap
|
||||
): boolean {
|
||||
const c = customMap.get(modelId);
|
||||
const o = overrideMap.get(modelId);
|
||||
if (c?.normalizeToolCallId || o?.normalizeToolCallId) return true;
|
||||
for (const p of MODEL_COMPAT_PROTOCOL_KEYS) {
|
||||
const pc = getProtoSlice(c, o, p);
|
||||
if (pc?.normalizeToolCallId) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function anyNoPreserveCompatBadge(
|
||||
modelId: string,
|
||||
customMap: CompatModelMap,
|
||||
overrideMap: CompatModelMap
|
||||
): boolean {
|
||||
const c = customMap.get(modelId);
|
||||
const o = overrideMap.get(modelId);
|
||||
if (
|
||||
c &&
|
||||
Object.prototype.hasOwnProperty.call(c, "preserveOpenAIDeveloperRole") &&
|
||||
c.preserveOpenAIDeveloperRole === false
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
if (
|
||||
o &&
|
||||
Object.prototype.hasOwnProperty.call(o, "preserveOpenAIDeveloperRole") &&
|
||||
o.preserveOpenAIDeveloperRole === false
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
for (const p of MODEL_COMPAT_PROTOCOL_KEYS) {
|
||||
const pc = getProtoSlice(c, o, p);
|
||||
if (
|
||||
pc &&
|
||||
Object.prototype.hasOwnProperty.call(pc, "preserveOpenAIDeveloperRole") &&
|
||||
pc.preserveOpenAIDeveloperRole === false
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
interface ModelRowProps {
|
||||
model: { id: string };
|
||||
@@ -40,10 +163,16 @@ interface ModelRowProps {
|
||||
onCopy: (text: string, key: string) => void;
|
||||
t: (key: string, values?: Record<string, unknown>) => string;
|
||||
showDeveloperToggle?: boolean;
|
||||
normalizeToolCallId?: boolean;
|
||||
preserveDeveloperRole?: boolean;
|
||||
onNormalizeChange?: (v: boolean) => void;
|
||||
onPreserveChange?: (v: boolean) => void;
|
||||
effectiveModelNormalize: (modelId: string, protocol?: string) => boolean;
|
||||
effectiveModelPreserveDeveloper: (modelId: string, protocol?: string) => boolean;
|
||||
saveModelCompatFlags: (
|
||||
modelId: string,
|
||||
patch: {
|
||||
normalizeToolCallId?: boolean;
|
||||
preserveOpenAIDeveloperRole?: boolean;
|
||||
compatByProtocol?: CompatByProtocolMap;
|
||||
}
|
||||
) => void;
|
||||
compatDisabled?: boolean;
|
||||
}
|
||||
|
||||
@@ -55,10 +184,16 @@ interface PassthroughModelRowProps {
|
||||
onDeleteAlias: () => void;
|
||||
t: (key: string, values?: Record<string, unknown>) => string;
|
||||
showDeveloperToggle?: boolean;
|
||||
normalizeToolCallId?: boolean;
|
||||
preserveDeveloperRole?: boolean;
|
||||
onNormalizeChange?: (v: boolean) => void;
|
||||
onPreserveChange?: (v: boolean) => void;
|
||||
effectiveModelNormalize: (modelId: string, protocol?: string) => boolean;
|
||||
effectiveModelPreserveDeveloper: (modelId: string, protocol?: string) => boolean;
|
||||
saveModelCompatFlags: (
|
||||
modelId: string,
|
||||
patch: {
|
||||
normalizeToolCallId?: boolean;
|
||||
preserveOpenAIDeveloperRole?: boolean;
|
||||
compatByProtocol?: CompatByProtocolMap;
|
||||
}
|
||||
) => void;
|
||||
compatDisabled?: boolean;
|
||||
}
|
||||
|
||||
@@ -230,27 +365,43 @@ function normalizeCodexLimitPolicy(policy: unknown): { use5h: boolean; useWeekly
|
||||
};
|
||||
}
|
||||
|
||||
function compatProtocolLabelKey(protocol: string): string {
|
||||
if (protocol === "openai") return "compatProtocolOpenAI";
|
||||
if (protocol === "openai-responses") return "compatProtocolOpenAIResponses";
|
||||
if (protocol === "claude") return "compatProtocolClaude";
|
||||
return "compatProtocolOpenAI";
|
||||
}
|
||||
|
||||
function ModelCompatPopover({
|
||||
t,
|
||||
normalizeToolCallId,
|
||||
preserveDeveloperRole,
|
||||
effectiveModelNormalize,
|
||||
effectiveModelPreserveDeveloper,
|
||||
onCompatPatch,
|
||||
showDeveloperToggle = true,
|
||||
onNormalizeChange,
|
||||
onPreserveChange,
|
||||
disabled,
|
||||
}: {
|
||||
t: (key: string) => string;
|
||||
normalizeToolCallId: boolean;
|
||||
preserveDeveloperRole?: boolean;
|
||||
effectiveModelNormalize: (protocol: string) => boolean;
|
||||
effectiveModelPreserveDeveloper: (protocol: string) => boolean;
|
||||
onCompatPatch: (
|
||||
protocol: string,
|
||||
payload: {
|
||||
normalizeToolCallId?: boolean;
|
||||
preserveOpenAIDeveloperRole?: boolean;
|
||||
}
|
||||
) => void;
|
||||
showDeveloperToggle?: boolean;
|
||||
onNormalizeChange: (v: boolean) => void;
|
||||
onPreserveChange: (v: boolean) => void;
|
||||
disabled?: boolean;
|
||||
}) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [protocol, setProtocol] = useState<string>(MODEL_COMPAT_PROTOCOL_KEYS[0]);
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const panelRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
const normalizeToolCallId = effectiveModelNormalize(protocol);
|
||||
const preserveDeveloperRole = effectiveModelPreserveDeveloper(protocol);
|
||||
const devToggle = showDeveloperToggle && protocol !== "claude";
|
||||
|
||||
// Click-outside: check both trigger and panel so that if the panel is ever rendered
|
||||
// in a portal (outside this subtree), clicks inside the panel still do not close it.
|
||||
useEffect(() => {
|
||||
@@ -280,32 +431,47 @@ function ModelCompatPopover({
|
||||
{open && (
|
||||
<div
|
||||
ref={panelRef}
|
||||
className="absolute left-0 top-full mt-1 z-50 min-w-[200px] p-3 rounded-lg border border-border bg-white dark:bg-zinc-900 shadow-xl ring-1 ring-black/5 dark:ring-white/10"
|
||||
className="absolute left-0 top-full mt-1 z-50 min-w-[220px] max-w-[92vw] p-3 rounded-lg border border-border bg-white dark:bg-zinc-900 shadow-xl ring-1 ring-black/5 dark:ring-white/10"
|
||||
>
|
||||
<p className="text-[10px] font-semibold uppercase tracking-wide text-text-muted mb-2">
|
||||
<p className="text-[10px] font-semibold uppercase tracking-wide text-text-muted mb-1">
|
||||
{t("compatAdjustmentsTitle")}
|
||||
</p>
|
||||
<p className="text-[10px] text-text-muted mb-2 leading-snug">{t("compatProtocolHint")}</p>
|
||||
<label className="block text-[10px] font-medium text-text-muted mb-1">
|
||||
{t("compatProtocolLabel")}
|
||||
</label>
|
||||
<select
|
||||
value={protocol}
|
||||
onChange={(e) => setProtocol(e.target.value)}
|
||||
disabled={disabled}
|
||||
className="w-full mb-3 px-2 py-1.5 text-xs rounded-md border border-border bg-white dark:bg-zinc-800 text-zinc-900 dark:text-zinc-100 focus:outline-none focus:ring-1 focus:ring-primary/50"
|
||||
>
|
||||
{MODEL_COMPAT_PROTOCOL_KEYS.map((p) => (
|
||||
<option key={p} value={p}>
|
||||
{t(compatProtocolLabelKey(p))}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<div className="flex flex-col gap-3">
|
||||
<Toggle
|
||||
size="sm"
|
||||
label={t("compatToolIdShort")}
|
||||
title={t("normalizeToolCallIdLabel")}
|
||||
checked={normalizeToolCallId}
|
||||
onChange={onNormalizeChange}
|
||||
onChange={(v) => onCompatPatch(protocol, { normalizeToolCallId: v })}
|
||||
disabled={disabled}
|
||||
/>
|
||||
{showDeveloperToggle && (
|
||||
<>
|
||||
{/* Inversion: Toggle checked = "do not preserve" (UI). onPreserveChange(val) = value to store (true = preserve, false = do not preserve), so we pass !checked. */}
|
||||
<Toggle
|
||||
size="sm"
|
||||
label={t("compatDoNotPreserveDeveloper")}
|
||||
title={t("preserveDeveloperRoleLabel")}
|
||||
checked={preserveDeveloperRole === false}
|
||||
onChange={(checked) => onPreserveChange(!checked)}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</>
|
||||
{devToggle && (
|
||||
<Toggle
|
||||
size="sm"
|
||||
label={t("compatDoNotPreserveDeveloper")}
|
||||
title={t("preserveDeveloperRoleLabel")}
|
||||
checked={preserveDeveloperRole === false}
|
||||
onChange={(checked) =>
|
||||
onCompatPatch(protocol, { preserveOpenAIDeveloperRole: !checked })
|
||||
}
|
||||
disabled={disabled}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
@@ -350,12 +516,8 @@ export default function ProviderDetailPage() {
|
||||
importedCount: 0,
|
||||
});
|
||||
const [modelMeta, setModelMeta] = useState<{
|
||||
customModels: Record<string, unknown>[];
|
||||
modelCompatOverrides: {
|
||||
id: string;
|
||||
normalizeToolCallId?: boolean;
|
||||
preserveOpenAIDeveloperRole?: boolean;
|
||||
}[];
|
||||
customModels: CompatModelRow[];
|
||||
modelCompatOverrides: Array<CompatModelRow & { id: string }>;
|
||||
}>({ customModels: [], modelCompatOverrides: [] });
|
||||
const [compatSavingModelId, setCompatSavingModelId] = useState<string | null>(null);
|
||||
|
||||
@@ -1020,48 +1182,91 @@ export default function ProviderDetailPage() {
|
||||
|
||||
const canImportModels = connections.some((conn) => conn.isActive !== false);
|
||||
|
||||
const effectiveModelNormalize = (modelId: string) => {
|
||||
const c = modelMeta.customModels.find((m: { id?: string }) => m.id === modelId) as
|
||||
| { normalizeToolCallId?: boolean }
|
||||
| undefined;
|
||||
if (c) return Boolean(c.normalizeToolCallId);
|
||||
const o = modelMeta.modelCompatOverrides.find((e) => e.id === modelId);
|
||||
return Boolean(o?.normalizeToolCallId);
|
||||
};
|
||||
const customMap = useMemo(() => buildCompatMap(modelMeta.customModels), [modelMeta.customModels]);
|
||||
const overrideMap = useMemo(
|
||||
() => buildCompatMap(modelMeta.modelCompatOverrides),
|
||||
[modelMeta.modelCompatOverrides]
|
||||
);
|
||||
|
||||
const effectiveModelPreserveDeveloper = (modelId: string) => {
|
||||
const c = modelMeta.customModels.find((m: { id?: string }) => m.id === modelId) as
|
||||
| Record<string, unknown>
|
||||
| undefined;
|
||||
if (c && Object.prototype.hasOwnProperty.call(c, "preserveOpenAIDeveloperRole")) {
|
||||
return Boolean(c.preserveOpenAIDeveloperRole);
|
||||
}
|
||||
const o = modelMeta.modelCompatOverrides.find((e) => e.id === modelId);
|
||||
if (o && Object.prototype.hasOwnProperty.call(o, "preserveOpenAIDeveloperRole")) {
|
||||
return Boolean(o.preserveOpenAIDeveloperRole);
|
||||
}
|
||||
return true;
|
||||
};
|
||||
const effectiveModelNormalize = (modelId: string, protocol = MODEL_COMPAT_PROTOCOL_KEYS[0]) =>
|
||||
effectiveNormalizeForProtocol(modelId, protocol, customMap, overrideMap);
|
||||
|
||||
const effectiveModelPreserveDeveloper = (
|
||||
modelId: string,
|
||||
protocol = MODEL_COMPAT_PROTOCOL_KEYS[0]
|
||||
) => effectivePreserveForProtocol(modelId, protocol, customMap, overrideMap);
|
||||
|
||||
const saveModelCompatFlags = async (
|
||||
modelId: string,
|
||||
patch: { normalizeToolCallId?: boolean; preserveOpenAIDeveloperRole?: boolean }
|
||||
patch: {
|
||||
normalizeToolCallId?: boolean;
|
||||
preserveOpenAIDeveloperRole?: boolean;
|
||||
compatByProtocol?: CompatByProtocolMap;
|
||||
}
|
||||
) => {
|
||||
setCompatSavingModelId(modelId);
|
||||
try {
|
||||
const body: Record<string, unknown> = { provider: providerId, modelId, ...patch };
|
||||
const c = customMap.get(modelId) as Record<string, unknown> | undefined;
|
||||
let body: Record<string, unknown>;
|
||||
const onlyCompatByProtocol =
|
||||
patch.compatByProtocol &&
|
||||
patch.normalizeToolCallId === undefined &&
|
||||
patch.preserveOpenAIDeveloperRole === undefined;
|
||||
|
||||
if (c) {
|
||||
if (onlyCompatByProtocol) {
|
||||
body = {
|
||||
provider: providerId,
|
||||
modelId,
|
||||
compatByProtocol: patch.compatByProtocol,
|
||||
};
|
||||
} else {
|
||||
body = {
|
||||
provider: providerId,
|
||||
modelId,
|
||||
modelName: (c.name as string) || modelId,
|
||||
source: (c.source as string) || "manual",
|
||||
apiFormat: (c.apiFormat as string) || "chat-completions",
|
||||
supportedEndpoints:
|
||||
Array.isArray(c.supportedEndpoints) && (c.supportedEndpoints as unknown[]).length
|
||||
? c.supportedEndpoints
|
||||
: ["chat"],
|
||||
normalizeToolCallId:
|
||||
patch.normalizeToolCallId !== undefined
|
||||
? patch.normalizeToolCallId
|
||||
: Boolean(c.normalizeToolCallId),
|
||||
preserveOpenAIDeveloperRole:
|
||||
patch.preserveOpenAIDeveloperRole !== undefined
|
||||
? patch.preserveOpenAIDeveloperRole
|
||||
: Object.prototype.hasOwnProperty.call(c, "preserveOpenAIDeveloperRole")
|
||||
? Boolean(c.preserveOpenAIDeveloperRole)
|
||||
: true,
|
||||
};
|
||||
if (patch.compatByProtocol) body.compatByProtocol = patch.compatByProtocol;
|
||||
}
|
||||
} else {
|
||||
body = { provider: providerId, modelId, ...patch };
|
||||
}
|
||||
const res = await fetch("/api/provider-models", {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
if (res.ok) await fetchProviderModelMeta();
|
||||
else notify.error(t("failedSaveCustomModel"));
|
||||
if (!res.ok) {
|
||||
notify.error(t("failedSaveCustomModel"));
|
||||
return;
|
||||
}
|
||||
} catch {
|
||||
notify.error(t("failedSaveCustomModel"));
|
||||
return;
|
||||
} finally {
|
||||
setCompatSavingModelId(null);
|
||||
}
|
||||
try {
|
||||
await fetchProviderModelMeta();
|
||||
} catch {
|
||||
/* refresh failure is non-critical — data was already saved */
|
||||
}
|
||||
};
|
||||
|
||||
const renderModelsSection = () => {
|
||||
@@ -1166,14 +1371,9 @@ export default function ProviderDetailPage() {
|
||||
onCopy={copy}
|
||||
t={t}
|
||||
showDeveloperToggle
|
||||
normalizeToolCallId={effectiveModelNormalize(model.id)}
|
||||
preserveDeveloperRole={effectiveModelPreserveDeveloper(model.id)}
|
||||
onNormalizeChange={(v) =>
|
||||
saveModelCompatFlags(model.id, { normalizeToolCallId: v })
|
||||
}
|
||||
onPreserveChange={(v) =>
|
||||
saveModelCompatFlags(model.id, { preserveOpenAIDeveloperRole: v })
|
||||
}
|
||||
effectiveModelNormalize={effectiveModelNormalize}
|
||||
effectiveModelPreserveDeveloper={effectiveModelPreserveDeveloper}
|
||||
saveModelCompatFlags={saveModelCompatFlags}
|
||||
compatDisabled={compatSavingModelId === model.id}
|
||||
/>
|
||||
);
|
||||
@@ -1813,10 +2013,9 @@ function ModelRow({
|
||||
onCopy,
|
||||
t,
|
||||
showDeveloperToggle = true,
|
||||
normalizeToolCallId,
|
||||
preserveDeveloperRole,
|
||||
onNormalizeChange,
|
||||
onPreserveChange,
|
||||
effectiveModelNormalize,
|
||||
effectiveModelPreserveDeveloper,
|
||||
saveModelCompatFlags,
|
||||
compatDisabled,
|
||||
}: ModelRowProps) {
|
||||
return (
|
||||
@@ -1840,11 +2039,12 @@ function ModelRow({
|
||||
</div>
|
||||
<ModelCompatPopover
|
||||
t={t}
|
||||
normalizeToolCallId={Boolean(normalizeToolCallId)}
|
||||
preserveDeveloperRole={preserveDeveloperRole}
|
||||
effectiveModelNormalize={(p) => effectiveModelNormalize(model.id, p)}
|
||||
effectiveModelPreserveDeveloper={(p) => effectiveModelPreserveDeveloper(model.id, p)}
|
||||
onCompatPatch={(protocol, payload) =>
|
||||
saveModelCompatFlags(model.id, { compatByProtocol: { [protocol]: payload } })
|
||||
}
|
||||
showDeveloperToggle={showDeveloperToggle}
|
||||
onNormalizeChange={onNormalizeChange}
|
||||
onPreserveChange={onPreserveChange}
|
||||
disabled={compatDisabled}
|
||||
/>
|
||||
</div>
|
||||
@@ -1861,10 +2061,9 @@ ModelRow.propTypes = {
|
||||
onCopy: PropTypes.func.isRequired,
|
||||
t: PropTypes.func,
|
||||
showDeveloperToggle: PropTypes.bool,
|
||||
normalizeToolCallId: PropTypes.bool,
|
||||
preserveDeveloperRole: PropTypes.bool,
|
||||
onNormalizeChange: PropTypes.func,
|
||||
onPreserveChange: PropTypes.func,
|
||||
effectiveModelNormalize: PropTypes.func.isRequired,
|
||||
effectiveModelPreserveDeveloper: PropTypes.func.isRequired,
|
||||
saveModelCompatFlags: PropTypes.func.isRequired,
|
||||
compatDisabled: PropTypes.bool,
|
||||
};
|
||||
|
||||
@@ -1960,12 +2159,9 @@ function PassthroughModelsSection({
|
||||
onDeleteAlias={() => onDeleteAlias(alias)}
|
||||
t={t}
|
||||
showDeveloperToggle
|
||||
normalizeToolCallId={effectiveModelNormalize(modelId)}
|
||||
preserveDeveloperRole={effectiveModelPreserveDeveloper(modelId)}
|
||||
onNormalizeChange={(v) => saveModelCompatFlags(modelId, { normalizeToolCallId: v })}
|
||||
onPreserveChange={(v) =>
|
||||
saveModelCompatFlags(modelId, { preserveOpenAIDeveloperRole: v })
|
||||
}
|
||||
effectiveModelNormalize={effectiveModelNormalize}
|
||||
effectiveModelPreserveDeveloper={effectiveModelPreserveDeveloper}
|
||||
saveModelCompatFlags={saveModelCompatFlags}
|
||||
compatDisabled={compatSavingModelId === modelId}
|
||||
/>
|
||||
))}
|
||||
@@ -1997,10 +2193,9 @@ function PassthroughModelRow({
|
||||
onDeleteAlias,
|
||||
t,
|
||||
showDeveloperToggle = true,
|
||||
normalizeToolCallId,
|
||||
preserveDeveloperRole,
|
||||
onNormalizeChange,
|
||||
onPreserveChange,
|
||||
effectiveModelNormalize,
|
||||
effectiveModelPreserveDeveloper,
|
||||
saveModelCompatFlags,
|
||||
compatDisabled,
|
||||
}: PassthroughModelRowProps) {
|
||||
return (
|
||||
@@ -2037,11 +2232,12 @@ function PassthroughModelRow({
|
||||
<div className="pl-9">
|
||||
<ModelCompatPopover
|
||||
t={t}
|
||||
normalizeToolCallId={Boolean(normalizeToolCallId)}
|
||||
preserveDeveloperRole={preserveDeveloperRole}
|
||||
effectiveModelNormalize={(p) => effectiveModelNormalize(modelId, p)}
|
||||
effectiveModelPreserveDeveloper={(p) => effectiveModelPreserveDeveloper(modelId, p)}
|
||||
onCompatPatch={(protocol, payload) =>
|
||||
saveModelCompatFlags(modelId, { compatByProtocol: { [protocol]: payload } })
|
||||
}
|
||||
showDeveloperToggle={showDeveloperToggle}
|
||||
onNormalizeChange={onNormalizeChange}
|
||||
onPreserveChange={onPreserveChange}
|
||||
disabled={compatDisabled}
|
||||
/>
|
||||
</div>
|
||||
@@ -2057,10 +2253,9 @@ PassthroughModelRow.propTypes = {
|
||||
onDeleteAlias: PropTypes.func.isRequired,
|
||||
t: PropTypes.func,
|
||||
showDeveloperToggle: PropTypes.bool,
|
||||
normalizeToolCallId: PropTypes.bool,
|
||||
preserveDeveloperRole: PropTypes.bool,
|
||||
onNormalizeChange: PropTypes.func,
|
||||
onPreserveChange: PropTypes.func,
|
||||
effectiveModelNormalize: PropTypes.func.isRequired,
|
||||
effectiveModelPreserveDeveloper: PropTypes.func.isRequired,
|
||||
saveModelCompatFlags: PropTypes.func.isRequired,
|
||||
compatDisabled: PropTypes.bool,
|
||||
};
|
||||
|
||||
@@ -2075,7 +2270,10 @@ function CustomModelsSection({
|
||||
}: CustomModelsSectionProps) {
|
||||
const t = useTranslations("providers");
|
||||
const notify = useNotificationStore();
|
||||
const [customModels, setCustomModels] = useState([]);
|
||||
const [customModels, setCustomModels] = useState<CompatModelRow[]>([]);
|
||||
const [modelCompatOverrides, setModelCompatOverrides] = useState<
|
||||
Array<CompatModelRow & { id: string }>
|
||||
>([]);
|
||||
const [newModelId, setNewModelId] = useState("");
|
||||
const [newModelName, setNewModelName] = useState("");
|
||||
const [newApiFormat, setNewApiFormat] = useState("chat-completions");
|
||||
@@ -2085,16 +2283,18 @@ function CustomModelsSection({
|
||||
const [editingModelId, setEditingModelId] = useState<string | null>(null);
|
||||
const [editingApiFormat, setEditingApiFormat] = useState("chat-completions");
|
||||
const [editingEndpoints, setEditingEndpoints] = useState<string[]>(["chat"]);
|
||||
const [editingNormalizeToolCallId, setEditingNormalizeToolCallId] = useState(false);
|
||||
const [editingPreserveDeveloperRole, setEditingPreserveDeveloperRole] = useState(false);
|
||||
const [savingModelId, setSavingModelId] = useState<string | null>(null);
|
||||
|
||||
const customMap = useMemo(() => buildCompatMap(customModels), [customModels]);
|
||||
const overrideMap = useMemo(() => buildCompatMap(modelCompatOverrides), [modelCompatOverrides]);
|
||||
|
||||
const fetchCustomModels = useCallback(async () => {
|
||||
try {
|
||||
const res = await fetch(`/api/provider-models?provider=${encodeURIComponent(providerId)}`);
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
setCustomModels(data.models || []);
|
||||
setModelCompatOverrides(data.modelCompatOverrides || []);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Failed to fetch custom models:", e);
|
||||
@@ -2160,23 +2360,44 @@ function CustomModelsSection({
|
||||
? model.supportedEndpoints
|
||||
: ["chat"]
|
||||
);
|
||||
setEditingNormalizeToolCallId(Boolean(model.normalizeToolCallId));
|
||||
setEditingPreserveDeveloperRole(
|
||||
Object.prototype.hasOwnProperty.call(model, "preserveOpenAIDeveloperRole")
|
||||
? Boolean(model.preserveOpenAIDeveloperRole)
|
||||
: true
|
||||
);
|
||||
};
|
||||
|
||||
const cancelEdit = () => {
|
||||
setEditingModelId(null);
|
||||
setEditingApiFormat("chat-completions");
|
||||
setEditingEndpoints(["chat"]);
|
||||
setEditingNormalizeToolCallId(false);
|
||||
setEditingPreserveDeveloperRole(true);
|
||||
setSavingModelId(null);
|
||||
};
|
||||
|
||||
const saveCustomCompat = async (
|
||||
modelId: string,
|
||||
patch: { compatByProtocol?: CompatByProtocolMap }
|
||||
) => {
|
||||
setSavingModelId(modelId);
|
||||
try {
|
||||
const res = await fetch("/api/provider-models", {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ provider: providerId, modelId, ...patch }),
|
||||
});
|
||||
if (!res.ok) {
|
||||
notify.error(t("failedSaveCustomModel"));
|
||||
return;
|
||||
}
|
||||
} catch {
|
||||
notify.error(t("failedSaveCustomModel"));
|
||||
return;
|
||||
} finally {
|
||||
setSavingModelId(null);
|
||||
}
|
||||
try {
|
||||
await fetchCustomModels();
|
||||
onModelsChanged?.();
|
||||
} catch {
|
||||
/* refresh failure is non-critical — data was already saved */
|
||||
}
|
||||
};
|
||||
|
||||
const saveEdit = async (modelId) => {
|
||||
if (!editingModelId || editingModelId !== modelId) return;
|
||||
if (!editingEndpoints.length) {
|
||||
@@ -2197,8 +2418,6 @@ function CustomModelsSection({
|
||||
source: model?.source || "manual",
|
||||
apiFormat: editingApiFormat,
|
||||
supportedEndpoints: editingEndpoints,
|
||||
normalizeToolCallId: editingNormalizeToolCallId,
|
||||
preserveOpenAIDeveloperRole: editingPreserveDeveloperRole,
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -2361,7 +2580,7 @@ function CustomModelsSection({
|
||||
🔊 Audio
|
||||
</span>
|
||||
)}
|
||||
{model.normalizeToolCallId && (
|
||||
{anyNormalizeCompatBadge(model.id, customMap, overrideMap) && (
|
||||
<span
|
||||
className="text-[10px] px-1.5 py-0.5 rounded-full bg-slate-500/15 text-slate-400 font-medium"
|
||||
title={t("normalizeToolCallIdLabel")}
|
||||
@@ -2369,7 +2588,7 @@ function CustomModelsSection({
|
||||
ID×9
|
||||
</span>
|
||||
)}
|
||||
{model.preserveOpenAIDeveloperRole === false && (
|
||||
{anyNoPreserveCompatBadge(model.id, customMap, overrideMap) && (
|
||||
<span
|
||||
className="text-[10px] px-1.5 py-0.5 rounded-full bg-cyan-500/15 text-cyan-400 font-medium"
|
||||
title={t("compatDoNotPreserveDeveloper")}
|
||||
@@ -2433,11 +2652,18 @@ function CustomModelsSection({
|
||||
<div className="mt-3 pt-3 border-t border-border/80 w-full">
|
||||
<ModelCompatPopover
|
||||
t={t}
|
||||
normalizeToolCallId={editingNormalizeToolCallId}
|
||||
preserveDeveloperRole={editingPreserveDeveloperRole}
|
||||
effectiveModelNormalize={(p) =>
|
||||
effectiveNormalizeForProtocol(model.id, p, customMap, overrideMap)
|
||||
}
|
||||
effectiveModelPreserveDeveloper={(p) =>
|
||||
effectivePreserveForProtocol(model.id, p, customMap, overrideMap)
|
||||
}
|
||||
onCompatPatch={(protocol, payload) =>
|
||||
saveCustomCompat(model.id, {
|
||||
compatByProtocol: { [protocol]: payload },
|
||||
})
|
||||
}
|
||||
showDeveloperToggle
|
||||
onNormalizeChange={setEditingNormalizeToolCallId}
|
||||
onPreserveChange={setEditingPreserveDeveloperRole}
|
||||
disabled={savingModelId === model.id}
|
||||
/>
|
||||
</div>
|
||||
@@ -2716,12 +2942,9 @@ function CompatibleModelsSection({
|
||||
onDeleteAlias={() => handleDeleteModel(modelId, alias)}
|
||||
t={t}
|
||||
showDeveloperToggle={!isAnthropic}
|
||||
normalizeToolCallId={effectiveModelNormalize(modelId)}
|
||||
preserveDeveloperRole={effectiveModelPreserveDeveloper(modelId)}
|
||||
onNormalizeChange={(v) => saveModelCompatFlags(modelId, { normalizeToolCallId: v })}
|
||||
onPreserveChange={(v) =>
|
||||
saveModelCompatFlags(modelId, { preserveOpenAIDeveloperRole: v })
|
||||
}
|
||||
effectiveModelNormalize={effectiveModelNormalize}
|
||||
effectiveModelPreserveDeveloper={effectiveModelPreserveDeveloper}
|
||||
saveModelCompatFlags={saveModelCompatFlags}
|
||||
compatDisabled={compatSavingModelId === modelId}
|
||||
/>
|
||||
))}
|
||||
|
||||
@@ -12,6 +12,7 @@ import { createBackup } from "@/shared/services/backupService";
|
||||
import { saveCliToolLastConfigured, deleteCliToolLastConfigured } from "@/lib/db/cliToolState";
|
||||
import { cliSettingsEnvSchema } from "@/shared/validation/schemas";
|
||||
import { isValidationFailure, validateBody } from "@/shared/validation/helpers";
|
||||
import { getApiKeyById } from "@/lib/localDb";
|
||||
|
||||
// Get claude settings path based on OS
|
||||
const getClaudeSettingsPath = () => getCliPrimaryConfigPath("claude");
|
||||
@@ -100,6 +101,22 @@ export async function POST(request: Request) {
|
||||
}
|
||||
const { env } = validation.data;
|
||||
|
||||
// (#523/#526) If a keyId was provided, resolve the real API key from DB.
|
||||
// The /api/keys list endpoint returns masked key strings — sending those to
|
||||
// disk would save an unusable half-hidden token. Resolving by ID guarantees
|
||||
// we always write the full key value to the config file.
|
||||
const keyId = typeof rawBody?.keyId === "string" ? rawBody.keyId.trim() : null;
|
||||
if (keyId) {
|
||||
try {
|
||||
const keyRecord = await getApiKeyById(keyId);
|
||||
if (keyRecord?.key) {
|
||||
env.ANTHROPIC_AUTH_TOKEN = keyRecord.key as string;
|
||||
}
|
||||
} catch {
|
||||
// Non-critical: fall back to whatever value was in env (e.g. sk_omniroute)
|
||||
}
|
||||
}
|
||||
|
||||
const settingsPath = getClaudeSettingsPath();
|
||||
const claudeDir = path.dirname(settingsPath);
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import { createBackup } from "@/shared/services/backupService";
|
||||
import { saveCliToolLastConfigured, deleteCliToolLastConfigured } from "@/lib/db/cliToolState";
|
||||
import { cliModelConfigSchema } from "@/shared/validation/schemas";
|
||||
import { isValidationFailure, validateBody } from "@/shared/validation/helpers";
|
||||
import { getApiKeyById } from "@/lib/localDb";
|
||||
|
||||
const CLINE_DATA_DIR = path.join(os.homedir(), ".cline", "data");
|
||||
const GLOBAL_STATE_PATH = path.join(CLINE_DATA_DIR, "globalState.json");
|
||||
@@ -125,7 +126,18 @@ export async function POST(request: Request) {
|
||||
if (isValidationFailure(validation)) {
|
||||
return NextResponse.json({ error: validation.error }, { status: 400 });
|
||||
}
|
||||
const { baseUrl, apiKey, model } = validation.data;
|
||||
let { baseUrl, apiKey, model } = validation.data;
|
||||
|
||||
// (#526) Resolve real key from DB if keyId was provided
|
||||
const keyId = typeof rawBody?.keyId === "string" ? rawBody.keyId.trim() : null;
|
||||
if (keyId) {
|
||||
try {
|
||||
const keyRecord = await getApiKeyById(keyId);
|
||||
if (keyRecord?.key) apiKey = keyRecord.key as string;
|
||||
} catch {
|
||||
/* non-critical */
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure directory exists
|
||||
await fs.mkdir(CLINE_DATA_DIR, { recursive: true });
|
||||
|
||||
@@ -39,6 +39,10 @@ export async function POST(request, { params }) {
|
||||
switch (toolId) {
|
||||
case "continue":
|
||||
return await saveContinueConfig({ baseUrl, apiKey, model });
|
||||
case "opencode":
|
||||
// (#524) OpenCode config was never saved because only 'continue' was handled here.
|
||||
// opencode reads ~/.config/opencode/config.toml — write the OmniRoute settings there.
|
||||
return await saveOpenCodeConfig({ baseUrl, apiKey, model });
|
||||
default:
|
||||
return NextResponse.json(
|
||||
{ error: `Direct config save not supported for: ${toolId}` },
|
||||
@@ -125,3 +129,56 @@ async function saveContinueConfig({ baseUrl, apiKey, model }) {
|
||||
configPath,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Save OpenCode config to ~/.config/opencode/config.toml (XDG_CONFIG_HOME aware).
|
||||
* (#524) OpenCode was silently failing because this handler was missing.
|
||||
*/
|
||||
async function saveOpenCodeConfig({ baseUrl, apiKey, model }) {
|
||||
const { apiPort } = getRuntimePorts();
|
||||
// Honour $XDG_CONFIG_HOME if set, otherwise use ~/.config per the XDG Base Directory spec
|
||||
const xdgConfigHome = process.env.XDG_CONFIG_HOME || path.join(os.homedir(), ".config");
|
||||
const configPath = path.join(xdgConfigHome, "opencode", "config.toml");
|
||||
const configDir = path.dirname(configPath);
|
||||
|
||||
// Ensure ~/.config/opencode/ exists
|
||||
await fs.mkdir(configDir, { recursive: true });
|
||||
|
||||
const normalizedBaseUrl = String(baseUrl || "")
|
||||
.trim()
|
||||
.replace(/\/+$/, "");
|
||||
|
||||
// Read existing TOML to preserve any user settings outside our block
|
||||
let existingContent = "";
|
||||
try {
|
||||
existingContent = await fs.readFile(configPath, "utf-8");
|
||||
} catch {
|
||||
// File doesn't exist yet — start fresh
|
||||
}
|
||||
|
||||
// Build the OmniRoute TOML block.
|
||||
// opencode config.toml uses the [provider.X] table format.
|
||||
void apiPort; // available for future port-based detection
|
||||
const omniBlock = `
|
||||
# OmniRoute managed — updated automatically by OmniRoute CLI Tools
|
||||
[provider.omniroute]
|
||||
api_key = "${apiKey || "sk_omniroute"}"
|
||||
base_url = "${normalizedBaseUrl}"
|
||||
model = "${model}"
|
||||
`;
|
||||
|
||||
// Remove old OmniRoute-managed block (if any) then append fresh one
|
||||
const cleanedContent = existingContent
|
||||
.replace(/\n?# OmniRoute managed[\s\S]*?(?=\n\[|$)/, "")
|
||||
.trimEnd();
|
||||
|
||||
const newContent = (cleanedContent ? cleanedContent + "\n" : "") + omniBlock;
|
||||
|
||||
await fs.writeFile(configPath, newContent, "utf-8");
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: `OpenCode config saved to ${configPath}`,
|
||||
configPath,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import { createBackup } from "@/shared/services/backupService";
|
||||
import { saveCliToolLastConfigured, deleteCliToolLastConfigured } from "@/lib/db/cliToolState";
|
||||
import { cliModelConfigSchema } from "@/shared/validation/schemas";
|
||||
import { isValidationFailure, validateBody } from "@/shared/validation/helpers";
|
||||
import { getApiKeyById } from "@/lib/localDb";
|
||||
|
||||
const getOpenClawSettingsPath = () => getCliPrimaryConfigPath("openclaw");
|
||||
const getOpenClawDir = () => path.dirname(getOpenClawSettingsPath());
|
||||
@@ -101,7 +102,18 @@ export async function POST(request: Request) {
|
||||
if (isValidationFailure(validation)) {
|
||||
return NextResponse.json({ error: validation.error }, { status: 400 });
|
||||
}
|
||||
const { baseUrl, apiKey, model } = validation.data;
|
||||
let { baseUrl, apiKey, model } = validation.data;
|
||||
|
||||
// (#526) Resolve real key from DB if keyId was provided
|
||||
const keyId = typeof rawBody?.keyId === "string" ? rawBody.keyId.trim() : null;
|
||||
if (keyId) {
|
||||
try {
|
||||
const keyRecord = await getApiKeyById(keyId);
|
||||
if (keyRecord?.key) apiKey = keyRecord.key as string;
|
||||
} catch {
|
||||
/* non-critical */
|
||||
}
|
||||
}
|
||||
|
||||
const openclawDir = getOpenClawDir();
|
||||
const settingsPath = getOpenClawSettingsPath();
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
updateCustomModel,
|
||||
getModelCompatOverrides,
|
||||
mergeModelCompatOverride,
|
||||
type ModelCompatPatch,
|
||||
} from "@/lib/localDb";
|
||||
import {
|
||||
AI_PROVIDERS,
|
||||
@@ -129,6 +130,7 @@ export async function PUT(request) {
|
||||
supportedEndpoints,
|
||||
normalizeToolCallId,
|
||||
preserveOpenAIDeveloperRole,
|
||||
compatByProtocol,
|
||||
} = validation.data;
|
||||
|
||||
const raw = rawBody as Record<string, unknown>;
|
||||
@@ -139,6 +141,9 @@ export async function PUT(request) {
|
||||
if ("normalizeToolCallId" in raw) updates.normalizeToolCallId = normalizeToolCallId;
|
||||
if ("preserveOpenAIDeveloperRole" in raw)
|
||||
updates.preserveOpenAIDeveloperRole = preserveOpenAIDeveloperRole;
|
||||
if ("compatByProtocol" in raw && compatByProtocol !== undefined) {
|
||||
updates.compatByProtocol = compatByProtocol;
|
||||
}
|
||||
|
||||
const model = await updateCustomModel(provider, modelId, updates);
|
||||
|
||||
@@ -147,9 +152,17 @@ export async function PUT(request) {
|
||||
const compatOnly =
|
||||
rawKeys.length > 0 &&
|
||||
rawKeys.every((k) =>
|
||||
["provider", "modelId", "normalizeToolCallId", "preserveOpenAIDeveloperRole"].includes(k)
|
||||
[
|
||||
"provider",
|
||||
"modelId",
|
||||
"normalizeToolCallId",
|
||||
"preserveOpenAIDeveloperRole",
|
||||
"compatByProtocol",
|
||||
].includes(k)
|
||||
) &&
|
||||
("normalizeToolCallId" in raw || "preserveOpenAIDeveloperRole" in raw);
|
||||
("normalizeToolCallId" in raw ||
|
||||
"preserveOpenAIDeveloperRole" in raw ||
|
||||
"compatByProtocol" in raw);
|
||||
if (compatOnly) {
|
||||
const knownProvider =
|
||||
!!provider &&
|
||||
@@ -165,10 +178,7 @@ export async function PUT(request) {
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
const patch: {
|
||||
normalizeToolCallId?: boolean;
|
||||
preserveOpenAIDeveloperRole?: boolean | null;
|
||||
} = {};
|
||||
const patch: ModelCompatPatch = {};
|
||||
if ("normalizeToolCallId" in raw && typeof normalizeToolCallId === "boolean") {
|
||||
patch.normalizeToolCallId = normalizeToolCallId;
|
||||
}
|
||||
@@ -178,6 +188,9 @@ export async function PUT(request) {
|
||||
? preserveOpenAIDeveloperRole
|
||||
: undefined;
|
||||
}
|
||||
if ("compatByProtocol" in raw && compatByProtocol && typeof compatByProtocol === "object") {
|
||||
patch.compatByProtocol = compatByProtocol;
|
||||
}
|
||||
if (Object.keys(patch).length > 0) {
|
||||
mergeModelCompatOverride(provider, modelId, patch);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import { getProviderConnectionById, updateProviderConnection, resolveProxyForConnection } from "@/lib/localDb";
|
||||
import {
|
||||
getProviderConnectionById,
|
||||
updateProviderConnection,
|
||||
resolveProxyForConnection,
|
||||
} from "@/lib/localDb";
|
||||
import { getMachineId } from "@/shared/utils/machine";
|
||||
import { getUsageForProvider } from "@omniroute/open-sse/services/usage.ts";
|
||||
import { getExecutor } from "@omniroute/open-sse/executors/index.ts";
|
||||
@@ -109,7 +113,10 @@ async function refreshAndUpdateCredentials(connection: any) {
|
||||
/**
|
||||
* GET /api/usage/[connectionId] - Get usage data for a specific connection
|
||||
*/
|
||||
export async function GET(request: Request, { params }: { params: Promise<{ connectionId: string }> }) {
|
||||
export async function GET(
|
||||
request: Request,
|
||||
{ params }: { params: Promise<{ connectionId: string }> }
|
||||
) {
|
||||
try {
|
||||
const { connectionId } = await params;
|
||||
|
||||
@@ -155,7 +162,34 @@ export async function GET(request: Request, { params }: { params: Promise<{ conn
|
||||
|
||||
// Populate quota cache for quota-aware account selection
|
||||
if (isRecord(usage?.quotas)) {
|
||||
setQuotaCache(connectionId, connection.provider, usage.quotas);
|
||||
setQuotaCache(
|
||||
connectionId,
|
||||
connection.provider as string,
|
||||
usage.quotas as Record<string, unknown>
|
||||
);
|
||||
}
|
||||
|
||||
// (#491) If the live usage check returned an auth error, sync the expired status
|
||||
// back to the DB so the Providers page reflects the same degraded state as
|
||||
// Limits & Quotas (which performs the live check).
|
||||
const errorMessage = typeof usage?.message === "string" ? usage.message.toLowerCase() : "";
|
||||
const isAuthError =
|
||||
errorMessage.includes("token expired") ||
|
||||
errorMessage.includes("access denied") ||
|
||||
errorMessage.includes("re-authenticate") ||
|
||||
errorMessage.includes("unauthorized");
|
||||
|
||||
if (isAuthError && connection.testStatus !== "expired") {
|
||||
try {
|
||||
await updateProviderConnection(connection.id as string, {
|
||||
testStatus: "expired",
|
||||
lastErrorType: "token_expired",
|
||||
lastErrorAt: new Date().toISOString(),
|
||||
});
|
||||
} catch (dbErr) {
|
||||
// Non-critical: log but don't block the response
|
||||
console.error("[Usage API] Failed to sync expired status to DB:", dbErr);
|
||||
}
|
||||
}
|
||||
|
||||
return Response.json(usage);
|
||||
|
||||
@@ -160,6 +160,7 @@ export async function POST(request) {
|
||||
// Resolve provider config — dynamic first (local override), then hardcoded
|
||||
let providerConfig: EmbeddingProvider | null =
|
||||
dynamicProviders.find((dp) => dp.id === provider) || getEmbeddingProvider(provider) || null;
|
||||
let credentialsProviderId = provider;
|
||||
|
||||
// #496: Fallback — resolve from ALL provider_nodes (not just localhost)
|
||||
// This enables custom embedding models (e.g. google/gemini-embedding-001) whose
|
||||
@@ -180,6 +181,7 @@ export async function POST(request) {
|
||||
authHeader: "bearer",
|
||||
models: [],
|
||||
};
|
||||
credentialsProviderId = matchingNode.id || provider;
|
||||
log.info(
|
||||
"EMBED",
|
||||
`Resolved custom embedding provider: ${provider} → ${providerConfig.baseUrl}`
|
||||
@@ -200,7 +202,7 @@ export async function POST(request) {
|
||||
// Get credentials — skip for local providers (authType: "none")
|
||||
let credentials = null;
|
||||
if (providerConfig && providerConfig.authType !== "none") {
|
||||
credentials = await getProviderCredentials(provider);
|
||||
credentials = await getProviderCredentials(credentialsProviderId);
|
||||
if (!credentials) {
|
||||
return errorResponse(
|
||||
HTTP_STATUS.BAD_REQUEST,
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
/**
|
||||
* GET /api/v1/search/analytics
|
||||
*
|
||||
* Returns search request statistics from call_logs (request_type = 'search').
|
||||
* Includes provider breakdown, cache hit rate, cost summary, and error count.
|
||||
*/
|
||||
|
||||
import { NextResponse } from "next/server";
|
||||
import { getDbInstance } from "@/lib/db/core";
|
||||
import { enforceApiKeyPolicy } from "@/shared/utils/apiKeyPolicy";
|
||||
|
||||
export async function GET(req: Request) {
|
||||
const policy = await enforceApiKeyPolicy(req, "analytics");
|
||||
if (policy.rejection) return policy.rejection;
|
||||
|
||||
try {
|
||||
const db = getDbInstance();
|
||||
|
||||
// Total search requests
|
||||
const totalRow = db
|
||||
.prepare(`SELECT COUNT(*) as cnt FROM call_logs WHERE request_type = 'search'`)
|
||||
.get() as { cnt: number };
|
||||
const total = totalRow?.cnt ?? 0;
|
||||
|
||||
// Today's searches (UTC date)
|
||||
const todayStart = new Date();
|
||||
todayStart.setUTCHours(0, 0, 0, 0);
|
||||
const todayRow = db
|
||||
.prepare(
|
||||
`SELECT COUNT(*) as cnt FROM call_logs WHERE request_type = 'search' AND timestamp >= ?`
|
||||
)
|
||||
.get(todayStart.toISOString()) as { cnt: number };
|
||||
const today = todayRow?.cnt ?? 0;
|
||||
|
||||
// Errors
|
||||
const errRow = db
|
||||
.prepare(
|
||||
`SELECT COUNT(*) as cnt FROM call_logs WHERE request_type = 'search' AND (status >= 400 OR error IS NOT NULL)`
|
||||
)
|
||||
.get() as { cnt: number };
|
||||
const errors = errRow?.cnt ?? 0;
|
||||
|
||||
// Avg duration
|
||||
const durRow = db
|
||||
.prepare(
|
||||
`SELECT AVG(duration) as avg FROM call_logs WHERE request_type = 'search' AND duration > 0`
|
||||
)
|
||||
.get() as { avg: number | null };
|
||||
const avgDurationMs = Math.round(durRow?.avg ?? 0);
|
||||
|
||||
// Per-provider breakdown (provider column stores search provider id)
|
||||
const provRows = db
|
||||
.prepare(
|
||||
`SELECT provider, COUNT(*) as cnt
|
||||
FROM call_logs WHERE request_type = 'search'
|
||||
GROUP BY provider ORDER BY cnt DESC`
|
||||
)
|
||||
.all() as Array<{ provider: string; cnt: number }>;
|
||||
|
||||
// Cost per search provider (matching searchRegistry.ts rates)
|
||||
const COST_PER_QUERY: Record<string, number> = {
|
||||
"serper-search": 0.001,
|
||||
"brave-search": 0.003,
|
||||
"perplexity-search": 0.005,
|
||||
"exa-search": 0.01,
|
||||
"tavily-search": 0.004,
|
||||
};
|
||||
|
||||
const byProvider: Record<string, { count: number; costUsd: number }> = {};
|
||||
let totalCostUsd = 0;
|
||||
for (const row of provRows) {
|
||||
const cost = (COST_PER_QUERY[row.provider] ?? 0.001) * row.cnt;
|
||||
byProvider[row.provider] = { count: row.cnt, costUsd: cost };
|
||||
totalCostUsd += cost;
|
||||
}
|
||||
|
||||
// Cached: very fast responses (< 5ms) indicate cache hits
|
||||
const cachedRow = db
|
||||
.prepare(
|
||||
`SELECT COUNT(*) as cnt FROM call_logs
|
||||
WHERE request_type = 'search' AND duration > 0 AND duration < 5`
|
||||
)
|
||||
.get() as { cnt: number };
|
||||
const cached = cachedRow?.cnt ?? 0;
|
||||
const cacheHitRate = total > 0 ? Math.round((cached / total) * 100) : 0;
|
||||
|
||||
return NextResponse.json({
|
||||
total,
|
||||
today,
|
||||
cached,
|
||||
errors,
|
||||
totalCostUsd,
|
||||
byProvider,
|
||||
cacheHitRate,
|
||||
avgDurationMs,
|
||||
last24h: [],
|
||||
});
|
||||
} catch (err: unknown) {
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
console.error("[/api/v1/search/analytics]", msg);
|
||||
return NextResponse.json({ error: "Internal server error" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -69,6 +69,11 @@ export default function LoginPage() {
|
||||
router.refresh();
|
||||
} else {
|
||||
const data = await res.json();
|
||||
// (#521) If no password is set, redirect to onboarding instead of showing an error
|
||||
if (data.needsSetup) {
|
||||
router.push("/dashboard/onboarding");
|
||||
return;
|
||||
}
|
||||
setError(data.error || t("invalidPassword"));
|
||||
}
|
||||
} catch (err) {
|
||||
|
||||
@@ -281,6 +281,7 @@
|
||||
"failedUpdatePermissionsRetry": "Failed to update permissions. Please try again.",
|
||||
"unknownProvider": "unknown",
|
||||
"copyMaskedKey": "Copy masked key",
|
||||
"keyOnlyAvailableAtCreation": "Full key available only at creation time — copy it when you first create the key",
|
||||
"modelsCount": "{count, plural, one {# model} other {# models}}",
|
||||
"lastUsedOn": "Last: {date}",
|
||||
"editPermissions": "Edit permissions",
|
||||
@@ -1437,6 +1438,11 @@
|
||||
"compatDeveloperShort": "Developer role",
|
||||
"compatDoNotPreserveDeveloper": "Do not preserve developer role",
|
||||
"compatBadgeNoPreserve": "No preserve",
|
||||
"compatProtocolLabel": "Client request protocol",
|
||||
"compatProtocolHint": "These options apply when OmniRoute detects this request shape (OpenAI Chat, Responses API, or Anthropic Messages).",
|
||||
"compatProtocolOpenAI": "OpenAI Chat Completions",
|
||||
"compatProtocolOpenAIResponses": "OpenAI Responses API",
|
||||
"compatProtocolClaude": "Anthropic Messages",
|
||||
"modelId": "Model ID",
|
||||
"customModelPlaceholder": "e.g. gpt-4.5-turbo",
|
||||
"loading": "Loading...",
|
||||
|
||||
@@ -1432,6 +1432,11 @@
|
||||
"compatDeveloperShort": "Developer 角色",
|
||||
"compatDoNotPreserveDeveloper": "不保留 developer 角色",
|
||||
"compatBadgeNoPreserve": "不保留",
|
||||
"compatProtocolLabel": "客户端请求协议",
|
||||
"compatProtocolHint": "以下选项在 OmniRoute 识别到该请求形态(OpenAI Chat、Responses API 或 Anthropic Messages)时生效。",
|
||||
"compatProtocolOpenAI": "OpenAI Chat Completions",
|
||||
"compatProtocolOpenAIResponses": "OpenAI Responses API",
|
||||
"compatProtocolClaude": "Anthropic Messages",
|
||||
"modelId": "模型 ID",
|
||||
"customModelPlaceholder": "例如:gpt-4.5-turbo",
|
||||
"loading": "正在加载...",
|
||||
|
||||
+35
-2
@@ -38,6 +38,8 @@ interface ApiKeyMetadata {
|
||||
autoResolve: boolean;
|
||||
isActive: boolean;
|
||||
accessSchedule: AccessSchedule | null;
|
||||
maxRequestsPerDay: number | null;
|
||||
maxRequestsPerMinute: number | null;
|
||||
}
|
||||
|
||||
interface ApiKeyRow extends JsonRecord {
|
||||
@@ -187,6 +189,14 @@ function ensureApiKeysColumns(db: ApiKeysDbLike) {
|
||||
db.exec("ALTER TABLE api_keys ADD COLUMN access_schedule TEXT");
|
||||
console.log("[DB] Added api_keys.access_schedule column");
|
||||
}
|
||||
if (!columnNames.has("max_requests_per_day")) {
|
||||
db.exec("ALTER TABLE api_keys ADD COLUMN max_requests_per_day INTEGER");
|
||||
console.log("[DB] Added api_keys.max_requests_per_day column");
|
||||
}
|
||||
if (!columnNames.has("max_requests_per_minute")) {
|
||||
db.exec("ALTER TABLE api_keys ADD COLUMN max_requests_per_minute INTEGER");
|
||||
console.log("[DB] Added api_keys.max_requests_per_minute column");
|
||||
}
|
||||
_schemaChecked = true;
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
@@ -212,7 +222,7 @@ function getPreparedStatements(db: ApiKeysDbLike): ApiKeysStatements {
|
||||
_stmtGetKeyById = db.prepare<ApiKeyRow>("SELECT * FROM api_keys WHERE id = ?");
|
||||
_stmtValidateKey = db.prepare<JsonRecord>("SELECT 1 FROM api_keys WHERE key = ?");
|
||||
_stmtGetKeyMetadata = db.prepare<ApiKeyRow>(
|
||||
"SELECT id, name, machine_id, allowed_models, allowed_connections, no_log, auto_resolve, is_active, access_schedule FROM api_keys WHERE key = ?"
|
||||
"SELECT id, name, machine_id, allowed_models, allowed_connections, no_log, auto_resolve, is_active, access_schedule, max_requests_per_day, max_requests_per_minute FROM api_keys WHERE key = ?"
|
||||
);
|
||||
_stmtInsertKey = db.prepare(
|
||||
"INSERT INTO api_keys (id, name, key, machine_id, allowed_models, no_log, created_at) VALUES (?, ?, ?, ?, ?, ?, ?)"
|
||||
@@ -406,6 +416,8 @@ export async function updateApiKeyPermissions(
|
||||
autoResolve?: boolean;
|
||||
isActive?: boolean;
|
||||
accessSchedule?: AccessSchedule | null;
|
||||
maxRequestsPerDay?: number | null;
|
||||
maxRequestsPerMinute?: number | null;
|
||||
}
|
||||
) {
|
||||
const db = getDbInstance() as ApiKeysDbLike;
|
||||
@@ -422,6 +434,8 @@ export async function updateApiKeyPermissions(
|
||||
autoResolve: update.autoResolve,
|
||||
isActive: update.isActive,
|
||||
accessSchedule: update.accessSchedule,
|
||||
maxRequestsPerDay: update.maxRequestsPerDay,
|
||||
maxRequestsPerMinute: update.maxRequestsPerMinute,
|
||||
};
|
||||
|
||||
if (
|
||||
@@ -431,7 +445,9 @@ export async function updateApiKeyPermissions(
|
||||
normalized.noLog === undefined &&
|
||||
normalized.autoResolve === undefined &&
|
||||
normalized.isActive === undefined &&
|
||||
normalized.accessSchedule === undefined
|
||||
normalized.accessSchedule === undefined &&
|
||||
normalized.maxRequestsPerDay === undefined &&
|
||||
normalized.maxRequestsPerMinute === undefined
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
@@ -446,6 +462,8 @@ export async function updateApiKeyPermissions(
|
||||
autoResolve?: number;
|
||||
isActive?: number;
|
||||
accessSchedule?: string | null;
|
||||
maxRequestsPerDay?: number | null;
|
||||
maxRequestsPerMinute?: number | null;
|
||||
} = { id };
|
||||
|
||||
if (normalized.name !== undefined) {
|
||||
@@ -486,6 +504,16 @@ export async function updateApiKeyPermissions(
|
||||
normalized.accessSchedule !== null ? JSON.stringify(normalized.accessSchedule) : null;
|
||||
}
|
||||
|
||||
if (normalized.maxRequestsPerDay !== undefined) {
|
||||
updates.push("max_requests_per_day = @maxRequestsPerDay");
|
||||
params.maxRequestsPerDay = normalized.maxRequestsPerDay;
|
||||
}
|
||||
|
||||
if (normalized.maxRequestsPerMinute !== undefined) {
|
||||
updates.push("max_requests_per_minute = @maxRequestsPerMinute");
|
||||
params.maxRequestsPerMinute = normalized.maxRequestsPerMinute;
|
||||
}
|
||||
|
||||
const result = db.prepare(`UPDATE api_keys SET ${updates.join(", ")} WHERE id = @id`).run(params);
|
||||
|
||||
if (result.changes === 0) return false;
|
||||
@@ -574,6 +602,9 @@ export async function getApiKeyMetadata(
|
||||
const machineIdRaw = record.machine_id ?? record.machineId;
|
||||
const metadataMachineId = typeof machineIdRaw === "string" ? machineIdRaw : null;
|
||||
|
||||
const rawMaxRPD = record.max_requests_per_day ?? record.maxRequestsPerDay;
|
||||
const rawMaxRPM = record.max_requests_per_minute ?? record.maxRequestsPerMinute;
|
||||
|
||||
const metadata: ApiKeyMetadata = {
|
||||
id: metadataId,
|
||||
name: metadataName,
|
||||
@@ -586,6 +617,8 @@ export async function getApiKeyMetadata(
|
||||
autoResolve: parseAutoResolve(record.auto_resolve ?? record.autoResolve),
|
||||
isActive: parseIsActive(record.is_active ?? record.isActive),
|
||||
accessSchedule: parseAccessSchedule(record.access_schedule ?? record.accessSchedule),
|
||||
maxRequestsPerDay: typeof rawMaxRPD === "number" && rawMaxRPD > 0 ? rawMaxRPD : null,
|
||||
maxRequestsPerMinute: typeof rawMaxRPM === "number" && rawMaxRPM > 0 ? rawMaxRPM : null,
|
||||
};
|
||||
|
||||
if (!metadata.id) {
|
||||
|
||||
+134
-8
@@ -4,16 +4,60 @@
|
||||
|
||||
import { getDbInstance } from "./core";
|
||||
import { backupDbFile } from "./backup";
|
||||
import {
|
||||
MODEL_COMPAT_PROTOCOL_KEYS,
|
||||
type ModelCompatProtocolKey,
|
||||
} from "@/shared/constants/modelCompat";
|
||||
|
||||
type JsonRecord = Record<string, unknown>;
|
||||
|
||||
/** Built-in / alias models: tool-call + developer-role flags without a full custom row */
|
||||
const MODEL_COMPAT_NAMESPACE = "modelCompatOverrides";
|
||||
|
||||
export { MODEL_COMPAT_PROTOCOL_KEYS, type ModelCompatProtocolKey };
|
||||
|
||||
export type ModelCompatPerProtocol = {
|
||||
normalizeToolCallId?: boolean;
|
||||
preserveOpenAIDeveloperRole?: boolean;
|
||||
};
|
||||
|
||||
type CompatByProtocolMap = Partial<Record<ModelCompatProtocolKey, ModelCompatPerProtocol>>;
|
||||
|
||||
function isCompatProtocolKey(p: string): p is ModelCompatProtocolKey {
|
||||
return (MODEL_COMPAT_PROTOCOL_KEYS as readonly string[]).includes(p);
|
||||
}
|
||||
|
||||
function deepMergeCompatByProtocol(
|
||||
prev: CompatByProtocolMap | undefined,
|
||||
patch: Partial<Record<ModelCompatProtocolKey, Partial<ModelCompatPerProtocol>>>
|
||||
): CompatByProtocolMap {
|
||||
const out: CompatByProtocolMap = { ...(prev || {}) };
|
||||
for (const key of Object.keys(patch) as ModelCompatProtocolKey[]) {
|
||||
if (!isCompatProtocolKey(key)) continue;
|
||||
const deltas = patch[key];
|
||||
if (!deltas || typeof deltas !== "object") continue;
|
||||
const hasDelta =
|
||||
Object.prototype.hasOwnProperty.call(deltas, "normalizeToolCallId") ||
|
||||
Object.prototype.hasOwnProperty.call(deltas, "preserveOpenAIDeveloperRole");
|
||||
if (!hasDelta) continue;
|
||||
const cur: ModelCompatPerProtocol = { ...(out[key] || {}) };
|
||||
if ("normalizeToolCallId" in deltas) {
|
||||
cur.normalizeToolCallId = Boolean(deltas.normalizeToolCallId);
|
||||
}
|
||||
if ("preserveOpenAIDeveloperRole" in deltas) {
|
||||
cur.preserveOpenAIDeveloperRole = Boolean(deltas.preserveOpenAIDeveloperRole);
|
||||
}
|
||||
if (Object.keys(cur).length === 0) delete out[key];
|
||||
else out[key] = cur;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
export type ModelCompatOverride = {
|
||||
id: string;
|
||||
normalizeToolCallId?: boolean;
|
||||
preserveOpenAIDeveloperRole?: boolean;
|
||||
compatByProtocol?: CompatByProtocolMap;
|
||||
};
|
||||
|
||||
function readCompatList(providerId: string): ModelCompatOverride[] {
|
||||
@@ -52,13 +96,24 @@ export function getModelCompatOverrides(providerId: string): ModelCompatOverride
|
||||
return readCompatList(providerId);
|
||||
}
|
||||
|
||||
export type ModelCompatPatch = {
|
||||
normalizeToolCallId?: boolean;
|
||||
preserveOpenAIDeveloperRole?: boolean | null;
|
||||
compatByProtocol?: CompatByProtocolMap;
|
||||
};
|
||||
|
||||
function compatByProtocolHasEntries(map: CompatByProtocolMap | undefined): boolean {
|
||||
if (!map || typeof map !== "object") return false;
|
||||
return Object.keys(map).some((k) => {
|
||||
const v = map[k as ModelCompatProtocolKey];
|
||||
return v && typeof v === "object" && Object.keys(v).length > 0;
|
||||
});
|
||||
}
|
||||
|
||||
export function mergeModelCompatOverride(
|
||||
providerId: string,
|
||||
modelId: string,
|
||||
patch: Partial<{
|
||||
normalizeToolCallId: boolean;
|
||||
preserveOpenAIDeveloperRole: boolean | null;
|
||||
}>
|
||||
patch: ModelCompatPatch
|
||||
) {
|
||||
const list = readCompatList(providerId);
|
||||
const idx = list.findIndex((e) => e.id === modelId);
|
||||
@@ -75,9 +130,18 @@ export function mergeModelCompatOverride(
|
||||
next.preserveOpenAIDeveloperRole = Boolean(patch.preserveOpenAIDeveloperRole);
|
||||
}
|
||||
}
|
||||
if (patch.compatByProtocol && Object.keys(patch.compatByProtocol).length > 0) {
|
||||
const merged = deepMergeCompatByProtocol(next.compatByProtocol, patch.compatByProtocol);
|
||||
if (compatByProtocolHasEntries(merged)) next.compatByProtocol = merged;
|
||||
else delete next.compatByProtocol;
|
||||
}
|
||||
const filtered = list.filter((e) => e.id !== modelId);
|
||||
const hasPreserveFlag = Object.prototype.hasOwnProperty.call(next, "preserveOpenAIDeveloperRole");
|
||||
if (next.normalizeToolCallId || hasPreserveFlag) {
|
||||
if (
|
||||
next.normalizeToolCallId ||
|
||||
hasPreserveFlag ||
|
||||
compatByProtocolHasEntries(next.compatByProtocol)
|
||||
) {
|
||||
filtered.push(next);
|
||||
}
|
||||
writeCompatList(providerId, filtered);
|
||||
@@ -281,6 +345,23 @@ export async function updateCustomModel(
|
||||
if (index === -1) return null;
|
||||
|
||||
const current = models[index];
|
||||
const currentCompat = (current as JsonRecord).compatByProtocol as CompatByProtocolMap | undefined;
|
||||
let mergedCompat: CompatByProtocolMap | undefined = currentCompat;
|
||||
if (
|
||||
updates.compatByProtocol !== undefined &&
|
||||
typeof updates.compatByProtocol === "object" &&
|
||||
updates.compatByProtocol !== null &&
|
||||
!Array.isArray(updates.compatByProtocol)
|
||||
) {
|
||||
mergedCompat = deepMergeCompatByProtocol(
|
||||
currentCompat,
|
||||
updates.compatByProtocol as Partial<
|
||||
Record<ModelCompatProtocolKey, Partial<ModelCompatPerProtocol>>
|
||||
>
|
||||
);
|
||||
if (!compatByProtocolHasEntries(mergedCompat)) mergedCompat = undefined;
|
||||
}
|
||||
|
||||
const next: JsonRecord = {
|
||||
...current,
|
||||
...(updates.modelName !== undefined ? { name: updates.modelName || current.name } : {}),
|
||||
@@ -299,6 +380,13 @@ export async function updateCustomModel(
|
||||
next.preserveOpenAIDeveloperRole = Boolean(updates.preserveOpenAIDeveloperRole);
|
||||
}
|
||||
}
|
||||
if (updates.compatByProtocol !== undefined) {
|
||||
if (mergedCompat && compatByProtocolHasEntries(mergedCompat)) {
|
||||
next.compatByProtocol = mergedCompat;
|
||||
} else {
|
||||
delete next.compatByProtocol;
|
||||
}
|
||||
}
|
||||
|
||||
models[index] = next;
|
||||
|
||||
@@ -335,11 +423,33 @@ function getCustomModelRow(providerId: string, modelId: string): JsonRecord | nu
|
||||
/**
|
||||
* Whether the given provider/model has "normalize tool call id" (9-char Mistral-style) enabled.
|
||||
* Custom model row wins; otherwise {@link getModelCompatOverrides}.
|
||||
* When `sourceFormat` is one of `openai` | `openai-responses` | `claude`, per-protocol
|
||||
* `compatByProtocol[sourceFormat].normalizeToolCallId` overrides the legacy top-level flag.
|
||||
*/
|
||||
export function getModelNormalizeToolCallId(providerId: string, modelId: string): boolean {
|
||||
export function getModelNormalizeToolCallId(
|
||||
providerId: string,
|
||||
modelId: string,
|
||||
sourceFormat?: string | null
|
||||
): boolean {
|
||||
const m = getCustomModelRow(providerId, modelId);
|
||||
if (m) return Boolean(m.normalizeToolCallId);
|
||||
const protocol = sourceFormat && isCompatProtocolKey(sourceFormat) ? sourceFormat : null;
|
||||
|
||||
if (m) {
|
||||
if (protocol) {
|
||||
const pc = (m.compatByProtocol as CompatByProtocolMap | undefined)?.[protocol];
|
||||
if (pc && Object.prototype.hasOwnProperty.call(pc, "normalizeToolCallId")) {
|
||||
return Boolean(pc.normalizeToolCallId);
|
||||
}
|
||||
}
|
||||
return Boolean(m.normalizeToolCallId);
|
||||
}
|
||||
const co = readCompatList(providerId).find((e) => e.id === modelId);
|
||||
if (protocol && co?.compatByProtocol?.[protocol]) {
|
||||
const pc = co.compatByProtocol[protocol]!;
|
||||
if (Object.prototype.hasOwnProperty.call(pc, "normalizeToolCallId")) {
|
||||
return Boolean(pc.normalizeToolCallId);
|
||||
}
|
||||
}
|
||||
return Boolean(co?.normalizeToolCallId);
|
||||
}
|
||||
|
||||
@@ -347,19 +457,35 @@ export function getModelNormalizeToolCallId(providerId: string, modelId: string)
|
||||
* Explicit preserve-openai-developer preference for this provider/model.
|
||||
* `undefined` = unset → routing keeps legacy default (preserve developer for OpenAI format).
|
||||
* `false` = map developer → system (e.g. MiniMax). `true` = keep developer.
|
||||
* Per-protocol overrides live under `compatByProtocol[sourceFormat]` when `sourceFormat` matches.
|
||||
*/
|
||||
export function getModelPreserveOpenAIDeveloperRole(
|
||||
providerId: string,
|
||||
modelId: string
|
||||
modelId: string,
|
||||
sourceFormat?: string | null
|
||||
): boolean | undefined {
|
||||
const m = getCustomModelRow(providerId, modelId);
|
||||
const protocol = sourceFormat && isCompatProtocolKey(sourceFormat) ? sourceFormat : null;
|
||||
|
||||
if (m) {
|
||||
if (protocol) {
|
||||
const pc = (m.compatByProtocol as CompatByProtocolMap | undefined)?.[protocol];
|
||||
if (pc && Object.prototype.hasOwnProperty.call(pc, "preserveOpenAIDeveloperRole")) {
|
||||
return Boolean(pc.preserveOpenAIDeveloperRole);
|
||||
}
|
||||
}
|
||||
if (Object.prototype.hasOwnProperty.call(m, "preserveOpenAIDeveloperRole")) {
|
||||
return Boolean(m.preserveOpenAIDeveloperRole);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
const co = readCompatList(providerId).find((e) => e.id === modelId);
|
||||
if (protocol && co?.compatByProtocol?.[protocol]) {
|
||||
const pc = co.compatByProtocol[protocol]!;
|
||||
if (Object.prototype.hasOwnProperty.call(pc, "preserveOpenAIDeveloperRole")) {
|
||||
return Boolean(pc.preserveOpenAIDeveloperRole);
|
||||
}
|
||||
}
|
||||
if (co && Object.prototype.hasOwnProperty.call(co, "preserveOpenAIDeveloperRole")) {
|
||||
return Boolean(co.preserveOpenAIDeveloperRole);
|
||||
}
|
||||
|
||||
@@ -48,6 +48,8 @@ export {
|
||||
getModelPreserveOpenAIDeveloperRole,
|
||||
} from "./db/models";
|
||||
|
||||
export type { ModelCompatPerProtocol, ModelCompatPatch } from "./db/models";
|
||||
|
||||
export {
|
||||
// Combos
|
||||
getCombos,
|
||||
|
||||
@@ -610,7 +610,12 @@ export async function validateProviderApiKey({ provider, apiKey, providerSpecifi
|
||||
}
|
||||
|
||||
const modelId = entry.models?.[0]?.id || null;
|
||||
const baseUrl = resolveBaseUrl(entry, providerSpecificData);
|
||||
// (#532) Use testKeyBaseUrl if defined — some providers validate keys on a different endpoint
|
||||
// than where requests are sent (e.g. opencode-go validates on zen/v1, not zen/go/v1)
|
||||
const validationEntry = entry.testKeyBaseUrl
|
||||
? { ...entry, baseUrl: entry.testKeyBaseUrl }
|
||||
: entry;
|
||||
const baseUrl = resolveBaseUrl(validationEntry, providerSpecificData);
|
||||
|
||||
try {
|
||||
if (OPENAI_LIKE_FORMATS.has(entry.format)) {
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
/**
|
||||
* Model compatibility protocol keys — shared between client UI and server.
|
||||
* Must not import Node or DB code so client components can import safely.
|
||||
*/
|
||||
|
||||
/** Client request shapes from detectFormat — compat options apply when the client uses this protocol */
|
||||
export const MODEL_COMPAT_PROTOCOL_KEYS = ["openai", "openai-responses", "claude"] as const;
|
||||
|
||||
export type ModelCompatProtocolKey = (typeof MODEL_COMPAT_PROTOCOL_KEYS)[number];
|
||||
@@ -161,11 +161,14 @@ export const APIKEY_PROVIDERS = {
|
||||
gemini: {
|
||||
id: "gemini",
|
||||
alias: "gemini",
|
||||
name: "Gemini",
|
||||
name: "Gemini (Google AI Studio)",
|
||||
icon: "diamond",
|
||||
color: "#4285F4",
|
||||
textIcon: "GE",
|
||||
website: "https://ai.google.dev",
|
||||
hasFree: true,
|
||||
freeNote:
|
||||
"Free forever: 1,500 req/day for Gemini 2.5 Flash — no credit card, get key at aistudio.google.com",
|
||||
},
|
||||
deepseek: {
|
||||
id: "deepseek",
|
||||
@@ -231,6 +234,9 @@ export const APIKEY_PROVIDERS = {
|
||||
color: "#0F6FFF",
|
||||
textIcon: "TG",
|
||||
website: "https://www.together.ai",
|
||||
hasFree: true,
|
||||
freeNote:
|
||||
"$25 signup credits + 3 permanently free models: Llama 3.3 70B, Vision, DeepSeek-R1 distill",
|
||||
},
|
||||
fireworks: {
|
||||
id: "fireworks",
|
||||
@@ -490,6 +496,93 @@ export const APIKEY_PROVIDERS = {
|
||||
website: "https://tavily.com",
|
||||
authHint: "API key from app.tavily.com (format: tvly-...)",
|
||||
},
|
||||
"opencode-zen": {
|
||||
id: "opencode-zen",
|
||||
alias: "opencode-zen",
|
||||
name: "OpenCode Zen",
|
||||
icon: "opencode",
|
||||
color: "#6366f1",
|
||||
website: "https://opencode.ai/zen",
|
||||
},
|
||||
"opencode-go": {
|
||||
id: "opencode-go",
|
||||
alias: "opencode-go",
|
||||
name: "OpenCode Go",
|
||||
icon: "opencode",
|
||||
color: "#6366f1",
|
||||
website: "https://opencode.ai/zen/go",
|
||||
},
|
||||
alibaba: {
|
||||
id: "alibaba",
|
||||
alias: "ali",
|
||||
name: "Alibaba Cloud (DashScope)",
|
||||
icon: "cloud_queue",
|
||||
color: "#FF6600",
|
||||
textIcon: "AL",
|
||||
website: "https://dashscope-intl.aliyuncs.com",
|
||||
hasFree: false,
|
||||
},
|
||||
longcat: {
|
||||
id: "longcat",
|
||||
alias: "lc",
|
||||
name: "LongCat AI",
|
||||
icon: "auto_awesome",
|
||||
color: "#FF6B9D",
|
||||
textIcon: "LC",
|
||||
website: "https://longcat.chat",
|
||||
hasFree: true,
|
||||
freeNote:
|
||||
"50M tokens/day (Flash-Lite) + 500K/day (Chat/Thinking) — 100% free while public beta",
|
||||
},
|
||||
pollinations: {
|
||||
id: "pollinations",
|
||||
alias: "pol",
|
||||
name: "Pollinations AI",
|
||||
icon: "local_florist",
|
||||
color: "#4CAF50",
|
||||
textIcon: "PO",
|
||||
website: "https://pollinations.ai",
|
||||
hasFree: true,
|
||||
freeNote:
|
||||
"No API key needed — access GPT-5, Claude, Gemini, DeepSeek V3, Llama 4 free (1 req/15s)",
|
||||
},
|
||||
"cloudflare-ai": {
|
||||
id: "cloudflare-ai",
|
||||
alias: "cf",
|
||||
name: "Cloudflare Workers AI",
|
||||
icon: "cloud",
|
||||
color: "#F48120",
|
||||
textIcon: "CF",
|
||||
website: "https://developers.cloudflare.com/workers-ai/",
|
||||
hasFree: true,
|
||||
freeNote:
|
||||
"Free 10K Neurons/day: ~150 LLM responses or 500s Whisper audio — edge inference globally",
|
||||
authHint: "Requires API Token AND Account ID (found at dash.cloudflare.com)",
|
||||
},
|
||||
scaleway: {
|
||||
id: "scaleway",
|
||||
alias: "scw",
|
||||
name: "Scaleway AI",
|
||||
icon: "cloud",
|
||||
color: "#4F0599",
|
||||
textIcon: "SCW",
|
||||
website: "https://www.scaleway.com/en/ai/generative-apis/",
|
||||
hasFree: true,
|
||||
freeNote: "1M free tokens for new accounts — EU/GDPR compliant (Paris), Qwen3 235B & Llama 70B",
|
||||
},
|
||||
aimlapi: {
|
||||
id: "aimlapi",
|
||||
alias: "aiml",
|
||||
name: "AI/ML API",
|
||||
icon: "hub",
|
||||
color: "#6366F1",
|
||||
textIcon: "AI",
|
||||
website: "https://aimlapi.com",
|
||||
hasFree: true,
|
||||
freeNote:
|
||||
"$0.025/day free credits — 200+ models (GPT-4o, Claude, Gemini, Llama) via single endpoint",
|
||||
passthroughModels: true,
|
||||
},
|
||||
};
|
||||
|
||||
export const OPENAI_COMPATIBLE_PREFIX = "openai-compatible-";
|
||||
|
||||
@@ -105,6 +105,24 @@ const CLI_TOOLS: Record<string, any> = {
|
||||
|
||||
const isWindows = () => process.platform === "win32";
|
||||
|
||||
/**
|
||||
* (#510) Normalize MSYS2/Git-Bash style paths to Windows-native paths.
|
||||
* On Windows with Git Bash, 'where claude' may return '/c/Program Files/...'
|
||||
* instead of 'C:\\Program Files\\...'. Convert these so the path is usable
|
||||
* by Node's fs and child_process modules.
|
||||
*/
|
||||
const normalizeMsys2Path = (p: string): string => {
|
||||
if (!p || !isWindows()) return p;
|
||||
// Match /letter/rest-of-path — MSYS2 POSIX-style drive mount
|
||||
const msys2Match = p.match(/^\/([a-zA-Z])\/(.+)$/);
|
||||
if (msys2Match) {
|
||||
const drive = msys2Match[1].toUpperCase();
|
||||
const rest = msys2Match[2].replace(/\//g, "\\");
|
||||
return `${drive}:\\${rest}`;
|
||||
}
|
||||
return p;
|
||||
};
|
||||
|
||||
const parseBoolean = (value: unknown, defaultValue = true) => {
|
||||
if (value == null || value === "") return defaultValue;
|
||||
return !FALSE_VALUES.has(String(value).trim().toLowerCase());
|
||||
@@ -256,7 +274,7 @@ const locateCommand = async (command: string, env: Record<string, string | undef
|
||||
const first =
|
||||
located.stdout
|
||||
.split(/\r?\n/)
|
||||
.map((line) => line.trim())
|
||||
.map((line) => normalizeMsys2Path(line.trim()))
|
||||
.find(Boolean) || null;
|
||||
return { installed: !!first, commandPath: first, reason: first ? null : "not_found" };
|
||||
}
|
||||
@@ -271,7 +289,7 @@ const locateCommand = async (command: string, env: Record<string, string | undef
|
||||
const first =
|
||||
located.stdout
|
||||
.split(/\r?\n/)
|
||||
.map((line) => line.trim())
|
||||
.map((line) => normalizeMsys2Path(line.trim()))
|
||||
.find(Boolean) || null;
|
||||
return { installed: !!first, commandPath: first, reason: first ? null : "not_found" };
|
||||
};
|
||||
|
||||
@@ -35,6 +35,8 @@ export interface ApiKeyMetadata {
|
||||
usedBudget?: number;
|
||||
isActive?: boolean;
|
||||
accessSchedule?: AccessSchedule | null;
|
||||
maxRequestsPerDay?: number | null;
|
||||
maxRequestsPerMinute?: number | null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -103,6 +105,65 @@ function isWithinSchedule(schedule: AccessSchedule): boolean {
|
||||
return localMinutes >= fromMinutes && localMinutes < untilMinutes;
|
||||
}
|
||||
|
||||
// ── In-memory request counter for per-key rate limits (#452) ──
|
||||
|
||||
/** Sliding-window request timestamps per API key */
|
||||
const _requestTimestamps = new Map<string, number[]>();
|
||||
const REQUEST_COUNTER_MAX_KEYS = 5000;
|
||||
const REQUEST_DAY_MS = 24 * 60 * 60 * 1000;
|
||||
const REQUEST_MINUTE_MS = 60 * 1000;
|
||||
|
||||
/** Record a request and check per-key limits. Returns null if OK, or an error message. */
|
||||
function checkRequestCountLimits(
|
||||
apiKeyId: string,
|
||||
maxPerDay: number | null | undefined,
|
||||
maxPerMinute: number | null | undefined
|
||||
): string | null {
|
||||
if (!maxPerDay && !maxPerMinute) return null;
|
||||
|
||||
const now = Date.now();
|
||||
|
||||
// Get or create timestamp array for this key
|
||||
let timestamps = _requestTimestamps.get(apiKeyId);
|
||||
if (!timestamps) {
|
||||
timestamps = [];
|
||||
_requestTimestamps.set(apiKeyId, timestamps);
|
||||
// Prevent unbounded growth
|
||||
if (_requestTimestamps.size > REQUEST_COUNTER_MAX_KEYS) {
|
||||
const firstKey = _requestTimestamps.keys().next().value;
|
||||
if (firstKey) _requestTimestamps.delete(firstKey);
|
||||
}
|
||||
}
|
||||
|
||||
// Prune timestamps older than 24h
|
||||
const dayAgo = now - REQUEST_DAY_MS;
|
||||
while (timestamps.length > 0 && timestamps[0] < dayAgo) {
|
||||
timestamps.shift();
|
||||
}
|
||||
|
||||
// Check per-minute limit (before recording this request)
|
||||
if (maxPerMinute && maxPerMinute > 0) {
|
||||
const minuteAgo = now - REQUEST_MINUTE_MS;
|
||||
const recentCount = timestamps.filter((t) => t >= minuteAgo).length;
|
||||
if (recentCount >= maxPerMinute) {
|
||||
return `Per-minute request limit exceeded (${maxPerMinute} RPM). Try again in a few seconds.`;
|
||||
}
|
||||
}
|
||||
|
||||
// Check per-day limit
|
||||
if (maxPerDay && maxPerDay > 0) {
|
||||
if (timestamps.length >= maxPerDay) {
|
||||
return `Daily request limit exceeded (${maxPerDay} RPD). Resets in ${Math.ceil(
|
||||
(timestamps[0] + REQUEST_DAY_MS - now) / 60000
|
||||
)} minutes.`;
|
||||
}
|
||||
}
|
||||
|
||||
// All checks passed — record this request
|
||||
timestamps.push(now);
|
||||
return null;
|
||||
}
|
||||
|
||||
export interface ApiKeyPolicyResult {
|
||||
/** API key string (null if no key provided) */
|
||||
apiKey: string | null;
|
||||
@@ -224,5 +285,21 @@ export async function enforceApiKeyPolicy(
|
||||
}
|
||||
}
|
||||
|
||||
// ── Check 5: Request-count limits (#452) ──
|
||||
if (apiKeyInfo.id && (apiKeyInfo.maxRequestsPerDay || apiKeyInfo.maxRequestsPerMinute)) {
|
||||
const limitError = checkRequestCountLimits(
|
||||
apiKeyInfo.id,
|
||||
apiKeyInfo.maxRequestsPerDay,
|
||||
apiKeyInfo.maxRequestsPerMinute
|
||||
);
|
||||
if (limitError) {
|
||||
return {
|
||||
apiKey,
|
||||
apiKeyInfo,
|
||||
rejection: errorResponse(HTTP_STATUS.RATE_LIMITED, limitError),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return { apiKey, apiKeyInfo, rejection: null };
|
||||
}
|
||||
|
||||
@@ -1,39 +1,86 @@
|
||||
import { execSync, execFileSync } from "child_process";
|
||||
import { existsSync, readFileSync } from "fs";
|
||||
|
||||
/**
|
||||
* Get raw machine ID using OS-specific methods.
|
||||
*
|
||||
* IMPORTANT: We do NOT use `if (process.platform === ...)` branching here.
|
||||
* Next.js SWC bundler evaluates `process.platform` at BUILD time, so when the
|
||||
* project is built on Linux, the win32/darwin branches get dead-code-eliminated
|
||||
* and the Linux fallback (which uses `head`) runs on Windows at runtime.
|
||||
*
|
||||
* Instead, we use a try/catch waterfall: try each OS method and fall through
|
||||
* to the next on failure. The correct method always succeeds on the target OS.
|
||||
*/
|
||||
function getMachineIdRaw(): string {
|
||||
if (process.platform === "win32") {
|
||||
// Strategy 1: Windows — REG.exe query for MachineGuid
|
||||
try {
|
||||
const sysRoot = process.env.SystemRoot || process.env.windir || "C:\\Windows";
|
||||
const output = execFileSync(
|
||||
`${sysRoot}\\System32\\REG.exe`,
|
||||
["QUERY", "HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Cryptography", "/v", "MachineGuid"],
|
||||
{ encoding: "utf8" }
|
||||
);
|
||||
return (
|
||||
output
|
||||
.toString()
|
||||
const regPath = `${sysRoot}\\System32\\REG.exe`;
|
||||
if (existsSync(regPath)) {
|
||||
const output = execFileSync(
|
||||
regPath,
|
||||
["QUERY", "HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Cryptography", "/v", "MachineGuid"],
|
||||
{ encoding: "utf8", timeout: 5000 }
|
||||
);
|
||||
const id = output
|
||||
.split("REG_SZ")[1]
|
||||
?.replace(/\r+|\n+|\s+/gi, "")
|
||||
?.toLowerCase() ?? ""
|
||||
);
|
||||
?.toLowerCase();
|
||||
if (id && id.length > 8) return id;
|
||||
}
|
||||
} catch {
|
||||
// Not Windows or REG.exe failed — continue
|
||||
}
|
||||
if (process.platform === "darwin") {
|
||||
const output = execSync("ioreg -rd1 -c IOPlatformExpertDevice", { encoding: "utf8" });
|
||||
return (
|
||||
output
|
||||
|
||||
// Strategy 2: macOS — ioreg IOPlatformUUID
|
||||
try {
|
||||
const output = execSync("ioreg -rd1 -c IOPlatformExpertDevice", {
|
||||
encoding: "utf8",
|
||||
timeout: 5000,
|
||||
});
|
||||
if (output.includes("IOPlatformUUID")) {
|
||||
const id = output
|
||||
.split("IOPlatformUUID")[1]
|
||||
?.split("\n")[0]
|
||||
?.replace(/\=|\s+|\"/gi, "")
|
||||
?.toLowerCase() ?? ""
|
||||
);
|
||||
?.replace(/=|\s+|"/gi, "")
|
||||
?.toLowerCase();
|
||||
if (id && id.length > 8) return id;
|
||||
}
|
||||
} catch {
|
||||
// Not macOS or ioreg not available — continue
|
||||
}
|
||||
const output = execSync(
|
||||
"( cat /var/lib/dbus/machine-id /etc/machine-id 2> /dev/null || hostname ) | head -n 1 || :",
|
||||
{ encoding: "utf8" }
|
||||
);
|
||||
return output
|
||||
.toString()
|
||||
.replace(/\r+|\n+|\s+/gi, "")
|
||||
.toLowerCase();
|
||||
|
||||
// Strategy 3: Linux — read machine-id files directly (no `head` or pipe)
|
||||
try {
|
||||
for (const filePath of ["/etc/machine-id", "/var/lib/dbus/machine-id"]) {
|
||||
if (existsSync(filePath)) {
|
||||
const content = readFileSync(filePath, "utf8").trim().toLowerCase();
|
||||
if (content.length > 8) return content;
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Files not readable — continue
|
||||
}
|
||||
|
||||
// Strategy 4: Hostname fallback (works on all platforms)
|
||||
try {
|
||||
const hostname = execSync("hostname", { encoding: "utf8", timeout: 5000 });
|
||||
const id = hostname.trim().toLowerCase();
|
||||
if (id) return id;
|
||||
} catch {
|
||||
// hostname failed — continue
|
||||
}
|
||||
|
||||
// Strategy 5: Node.js os.hostname() (no exec needed)
|
||||
try {
|
||||
const os = require("os");
|
||||
return os.hostname().toLowerCase();
|
||||
} catch {
|
||||
// Final fallback
|
||||
}
|
||||
|
||||
return "unknown-machine";
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -340,6 +340,13 @@ export const clearModelAvailabilitySchema = z.object({
|
||||
model: modelIdSchema,
|
||||
});
|
||||
|
||||
const modelCompatPerProtocolSchema = z
|
||||
.object({
|
||||
normalizeToolCallId: z.boolean().optional(),
|
||||
preserveOpenAIDeveloperRole: z.boolean().optional(),
|
||||
})
|
||||
.strict();
|
||||
|
||||
export const providerModelMutationSchema = z.object({
|
||||
provider: z.string().trim().min(1, "provider is required").max(120),
|
||||
modelId: z.string().trim().min(1, "modelId is required").max(240),
|
||||
@@ -349,6 +356,9 @@ export const providerModelMutationSchema = z.object({
|
||||
supportedEndpoints: z.array(z.enum(["chat", "embeddings", "images", "audio"])).default(["chat"]),
|
||||
normalizeToolCallId: z.boolean().optional(),
|
||||
preserveOpenAIDeveloperRole: z.boolean().nullable().optional(),
|
||||
compatByProtocol: z
|
||||
.record(z.enum(["openai", "openai-responses", "claude"]), modelCompatPerProtocolSchema)
|
||||
.optional(),
|
||||
});
|
||||
|
||||
const pricingFieldsSchema = z
|
||||
|
||||
@@ -106,3 +106,60 @@ test("embeddings route clears stale provider error metadata on success", async (
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
});
|
||||
|
||||
test("embeddings route uses provider node id for compatible provider credentials", async () => {
|
||||
await resetStorage();
|
||||
|
||||
const providerNode = await providersDb.createProviderNode({
|
||||
id: "openai-compatible-responses-google-embeddings",
|
||||
type: "openai-compatible",
|
||||
name: "Gemini Embeddings",
|
||||
prefix: "google",
|
||||
apiType: "responses",
|
||||
baseUrl: "https://generativelanguage.googleapis.com/v1beta/openai",
|
||||
});
|
||||
|
||||
const created = await providersDb.createProviderConnection({
|
||||
provider: providerNode.id,
|
||||
authType: "apikey",
|
||||
email: null,
|
||||
name: "google-compatible-key",
|
||||
apiKey: "google-compatible-test-key",
|
||||
testStatus: "active",
|
||||
lastError: null,
|
||||
lastErrorType: "token_refresh_failed",
|
||||
lastErrorSource: "oauth",
|
||||
errorCode: "refresh_failed",
|
||||
rateLimitedUntil: null,
|
||||
backoffLevel: 2,
|
||||
});
|
||||
|
||||
const originalFetch = globalThis.fetch;
|
||||
globalThis.fetch = async (url, init) => {
|
||||
assert.equal(url, "https://generativelanguage.googleapis.com/v1beta/openai/embeddings");
|
||||
assert.equal(init?.headers?.Authorization, "Bearer google-compatible-test-key");
|
||||
return Response.json({
|
||||
data: [{ object: "embedding", index: 0, embedding: [0.1, 0.2] }],
|
||||
usage: { prompt_tokens: 3, total_tokens: 3 },
|
||||
});
|
||||
};
|
||||
|
||||
try {
|
||||
const request = new Request("http://localhost/v1/embeddings", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ model: "google/gemini-embedding-001", input: "hello" }),
|
||||
});
|
||||
|
||||
const response = await embeddingsRoute.POST(request);
|
||||
assert.equal(response.status, 200);
|
||||
|
||||
const updated = await readConnection(created.id);
|
||||
assert.equal(updated.testStatus, "active");
|
||||
assert.equal(updated.errorCode, undefined);
|
||||
assert.equal(updated.lastErrorType, undefined);
|
||||
assert.equal(updated.lastErrorSource, undefined);
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,189 @@
|
||||
import { afterEach, beforeEach, describe, it } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
const { OpencodeExecutor } = await import("../../open-sse/executors/opencode.ts");
|
||||
const { PROVIDER_MODELS } = await import("../../open-sse/config/providerModels.ts");
|
||||
|
||||
function createMockResponse() {
|
||||
return new Response(JSON.stringify({ ok: true }), {
|
||||
status: 200,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
}
|
||||
|
||||
function createInput(model, stream = true, credentials = { apiKey: "test-key" }) {
|
||||
return {
|
||||
model,
|
||||
stream,
|
||||
credentials,
|
||||
body: {
|
||||
model,
|
||||
stream,
|
||||
messages: [{ role: "user", content: "hello" }],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function registerModel(provider, model) {
|
||||
PROVIDER_MODELS[provider] = [...(PROVIDER_MODELS[provider] || []), model];
|
||||
}
|
||||
|
||||
describe("OpencodeExecutor", () => {
|
||||
let zenExecutor;
|
||||
let goExecutor;
|
||||
let fetchCalls;
|
||||
let originalFetch;
|
||||
let originalZenModels;
|
||||
let originalGoModels;
|
||||
|
||||
beforeEach(() => {
|
||||
zenExecutor = new OpencodeExecutor("opencode-zen");
|
||||
goExecutor = new OpencodeExecutor("opencode-go");
|
||||
fetchCalls = [];
|
||||
originalFetch = globalThis.fetch;
|
||||
originalZenModels = [...(PROVIDER_MODELS["opencode-zen"] || [])];
|
||||
originalGoModels = [...(PROVIDER_MODELS["opencode-go"] || [])];
|
||||
globalThis.fetch = async (url, options) => {
|
||||
fetchCalls.push({ url, options });
|
||||
return createMockResponse();
|
||||
};
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
globalThis.fetch = originalFetch;
|
||||
PROVIDER_MODELS["opencode-zen"] = originalZenModels;
|
||||
PROVIDER_MODELS["opencode-go"] = originalGoModels;
|
||||
});
|
||||
|
||||
describe("execute", () => {
|
||||
it("routes opencode zen default models to chat completions", async () => {
|
||||
const minimaxResult = await zenExecutor.execute(createInput("minimax-m2.5-free"));
|
||||
assert.equal(minimaxResult.url, "https://opencode.ai/zen/v1/chat/completions");
|
||||
assert.equal(fetchCalls[0].url, "https://opencode.ai/zen/v1/chat/completions");
|
||||
|
||||
const pickleResult = await zenExecutor.execute(createInput("big-pickle"));
|
||||
assert.equal(pickleResult.url, "https://opencode.ai/zen/v1/chat/completions");
|
||||
assert.equal(fetchCalls[1].url, "https://opencode.ai/zen/v1/chat/completions");
|
||||
|
||||
const nanoResult = await zenExecutor.execute(createInput("gpt-5-nano"));
|
||||
assert.equal(nanoResult.url, "https://opencode.ai/zen/v1/chat/completions");
|
||||
assert.equal(fetchCalls[2].url, "https://opencode.ai/zen/v1/chat/completions");
|
||||
});
|
||||
|
||||
it("routes claude target format models to messages endpoint", async () => {
|
||||
const m27Result = await goExecutor.execute(
|
||||
createInput("minimax-m2.7", true, { apiKey: "claude-key" })
|
||||
);
|
||||
assert.equal(m27Result.url, "https://opencode.ai/zen/go/v1/messages");
|
||||
assert.equal(fetchCalls[0].url, "https://opencode.ai/zen/go/v1/messages");
|
||||
assert.equal(m27Result.headers["anthropic-version"], "2023-06-01");
|
||||
|
||||
const m25Result = await goExecutor.execute(
|
||||
createInput("minimax-m2.5", true, { apiKey: "claude-key" })
|
||||
);
|
||||
assert.equal(m25Result.url, "https://opencode.ai/zen/go/v1/messages");
|
||||
assert.equal(fetchCalls[1].url, "https://opencode.ai/zen/go/v1/messages");
|
||||
assert.equal(m25Result.headers["anthropic-version"], "2023-06-01");
|
||||
});
|
||||
|
||||
it("routes openai responses target format models to responses endpoint", async () => {
|
||||
registerModel("opencode-zen", {
|
||||
id: "gpt-5-responses",
|
||||
name: "GPT 5 Responses",
|
||||
targetFormat: "openai-responses",
|
||||
});
|
||||
|
||||
const result = await zenExecutor.execute(createInput("gpt-5-responses"));
|
||||
|
||||
assert.equal(result.url, "https://opencode.ai/zen/v1/responses");
|
||||
assert.equal(fetchCalls[0].url, "https://opencode.ai/zen/v1/responses");
|
||||
});
|
||||
|
||||
it("routes gemini streaming requests to streamGenerateContent", async () => {
|
||||
registerModel("opencode-zen", {
|
||||
id: "gemini-2.5-pro",
|
||||
name: "Gemini 2.5 Pro",
|
||||
targetFormat: "gemini",
|
||||
});
|
||||
|
||||
const result = await zenExecutor.execute(createInput("gemini-2.5-pro"));
|
||||
|
||||
assert.equal(
|
||||
result.url,
|
||||
"https://opencode.ai/zen/v1/models/gemini-2.5-pro:streamGenerateContent?alt=sse"
|
||||
);
|
||||
assert.equal(
|
||||
fetchCalls[0].url,
|
||||
"https://opencode.ai/zen/v1/models/gemini-2.5-pro:streamGenerateContent?alt=sse"
|
||||
);
|
||||
});
|
||||
|
||||
it("routes gemini non streaming requests to generateContent", async () => {
|
||||
registerModel("opencode-zen", {
|
||||
id: "gemini-2.5-pro",
|
||||
name: "Gemini 2.5 Pro",
|
||||
targetFormat: "gemini",
|
||||
});
|
||||
|
||||
const result = await zenExecutor.execute(createInput("gemini-2.5-pro", false));
|
||||
|
||||
assert.equal(result.url, "https://opencode.ai/zen/v1/models/gemini-2.5-pro:generateContent");
|
||||
assert.equal(
|
||||
fetchCalls[0].url,
|
||||
"https://opencode.ai/zen/v1/models/gemini-2.5-pro:generateContent"
|
||||
);
|
||||
});
|
||||
|
||||
it("falls back to chat completions for unknown models", async () => {
|
||||
const result = await zenExecutor.execute(createInput("unknown-model"));
|
||||
|
||||
assert.equal(result.url, "https://opencode.ai/zen/v1/chat/completions");
|
||||
assert.equal(fetchCalls[0].url, "https://opencode.ai/zen/v1/chat/completions");
|
||||
});
|
||||
|
||||
it("builds default headers for standard models", async () => {
|
||||
const result = await zenExecutor.execute(createInput("gpt-5-nano"));
|
||||
|
||||
assert.deepEqual(result.headers, {
|
||||
Authorization: "Bearer test-key",
|
||||
"Content-Type": "application/json",
|
||||
Accept: "text/event-stream",
|
||||
});
|
||||
assert.deepEqual(fetchCalls[0].options.headers, result.headers);
|
||||
});
|
||||
|
||||
it("adds anthropic version for claude target format", async () => {
|
||||
const result = await goExecutor.execute(
|
||||
createInput("minimax-m2.7", true, { apiKey: "claude-key" })
|
||||
);
|
||||
|
||||
assert.deepEqual(result.headers, {
|
||||
Authorization: "Bearer claude-key",
|
||||
"Content-Type": "application/json",
|
||||
"anthropic-version": "2023-06-01",
|
||||
Accept: "text/event-stream",
|
||||
});
|
||||
assert.deepEqual(fetchCalls[0].options.headers, result.headers);
|
||||
});
|
||||
|
||||
it("omits accept header when stream is false", async () => {
|
||||
const result = await zenExecutor.execute(createInput("big-pickle", false));
|
||||
|
||||
assert.deepEqual(result.headers, {
|
||||
Authorization: "Bearer test-key",
|
||||
"Content-Type": "application/json",
|
||||
});
|
||||
assert.deepEqual(fetchCalls[0].options.headers, result.headers);
|
||||
});
|
||||
|
||||
it("omits authorization when credentials are missing", async () => {
|
||||
const result = await zenExecutor.execute(createInput("minimax-m2.5-free", true, null));
|
||||
|
||||
assert.deepEqual(result.headers, {
|
||||
"Content-Type": "application/json",
|
||||
Accept: "text/event-stream",
|
||||
});
|
||||
assert.deepEqual(fetchCalls[0].options.headers, result.headers);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,93 @@
|
||||
/**
|
||||
* Unit tests for PR #397 — Anthropic-format tools filter fix (#346)
|
||||
*
|
||||
* Verifies that tools arriving in Anthropic format (`tool.name` without `.function`)
|
||||
* are NOT dropped by the empty-name filter in chatCore.ts.
|
||||
* Before the fix, ALL anthropic-format tools were silently dropped, causing
|
||||
* `400: tool_choice.any may only be specified while providing tools` from Anthropic.
|
||||
*/
|
||||
|
||||
import { describe, it } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
// Inline the filter logic from chatCore.ts (lines 225-231 after #397 fix)
|
||||
function filterEmptyNameTools(tools) {
|
||||
return tools.filter((tool) => {
|
||||
const fn = tool.function;
|
||||
const name = fn?.name ?? tool.name;
|
||||
return name && String(name).trim().length > 0;
|
||||
});
|
||||
}
|
||||
|
||||
describe("tools empty-name filter — #346 / PR #397", () => {
|
||||
it("should keep tools with valid OpenAI format name (tool.function.name)", () => {
|
||||
const tools = [
|
||||
{ type: "function", function: { name: "get_weather", description: "Get weather" } },
|
||||
];
|
||||
assert.equal(filterEmptyNameTools(tools).length, 1);
|
||||
});
|
||||
|
||||
it("should keep tools with valid Anthropic format name (tool.name)", () => {
|
||||
const tools = [
|
||||
{ name: "get_weather", description: "Get weather", input_schema: { type: "object" } },
|
||||
];
|
||||
assert.equal(filterEmptyNameTools(tools).length, 1);
|
||||
});
|
||||
|
||||
it("should drop tools with empty OpenAI format name (tool.function.name = '')", () => {
|
||||
const tools = [{ type: "function", function: { name: "" } }];
|
||||
assert.equal(filterEmptyNameTools(tools).length, 0);
|
||||
});
|
||||
|
||||
it("should drop tools with empty Anthropic format name (tool.name = '')", () => {
|
||||
const tools = [{ name: "", description: "Ghost tool", input_schema: { type: "object" } }];
|
||||
assert.equal(filterEmptyNameTools(tools).length, 0);
|
||||
});
|
||||
|
||||
it("should NOT drop Anthropic-format tools when function wrapper is absent (regression for PR #397)", () => {
|
||||
// Before fix: fn was undefined, fn?.name was undefined, filter returned false → ALL tools dropped
|
||||
// After fix: fn?.name ?? tool.name → falls back to tool.name → keeps valid tools
|
||||
const tools = [
|
||||
{
|
||||
name: "search",
|
||||
description: "Search the web",
|
||||
input_schema: { type: "object", properties: {} },
|
||||
},
|
||||
{ name: "code_exec", description: "Execute code", input_schema: { type: "object" } },
|
||||
];
|
||||
const result = filterEmptyNameTools(tools);
|
||||
assert.equal(result.length, 2, "Both anthropic-format tools should be preserved");
|
||||
});
|
||||
|
||||
it("should handle mixed format tools in the same array", () => {
|
||||
const tools = [
|
||||
{ type: "function", function: { name: "openai_tool" } }, // OpenAI format
|
||||
{ name: "anthropic_tool", input_schema: { type: "object" } }, // Anthropic format
|
||||
{ type: "function", function: { name: "" } }, // Empty OpenAI — should be dropped
|
||||
{ name: "", input_schema: { type: "object" } }, // Empty Anthropic — should be dropped
|
||||
];
|
||||
const result = filterEmptyNameTools(tools);
|
||||
assert.equal(result.length, 2, "Should keep 2 valid tools (one of each format)");
|
||||
assert.ok(
|
||||
result.some((t) => t.function?.name === "openai_tool"),
|
||||
"OpenAI tool preserved"
|
||||
);
|
||||
assert.ok(
|
||||
result.some((t) => t.name === "anthropic_tool"),
|
||||
"Anthropic tool preserved"
|
||||
);
|
||||
});
|
||||
|
||||
it("should handle tools with whitespace-only names", () => {
|
||||
const tools = [{ name: " ", input_schema: { type: "object" } }];
|
||||
assert.equal(filterEmptyNameTools(tools).length, 0);
|
||||
});
|
||||
|
||||
it("should handle null/undefined tool.name gracefully", () => {
|
||||
const tools = [
|
||||
{ input_schema: { type: "object" } }, // Neither name nor function
|
||||
{ name: null, input_schema: { type: "object" } },
|
||||
];
|
||||
assert.equal(filterEmptyNameTools(tools).length, 0);
|
||||
});
|
||||
});
|
||||
+2
-1
@@ -34,7 +34,8 @@
|
||||
"**/*.js",
|
||||
"**/*.jsx",
|
||||
".next/types/**/*.ts",
|
||||
".next/dev/types/**/*.ts"
|
||||
".next/dev/types/**/*.ts",
|
||||
".next/dev/dev/types/**/*.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
|
||||
Reference in New Issue
Block a user