Compare commits
1 Commits
fix/oidc-s
...
chore/file
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
61db75e3e5 |
11
.env.example
@@ -925,11 +925,6 @@ NEXT_PUBLIC_ENABLE_SOCKS5_PROXY=true
|
||||
# web_fetch). Default: 60000. Used by: open-sse/mcp-server/fetchTimeout.ts
|
||||
# OMNIROUTE_MCP_UPSTREAM_TIMEOUT_MS=60000
|
||||
|
||||
# Maximum number of local-corpus index instances cached in memory.
|
||||
# Used by: src/lib/localCorpus/configured.ts — bounds the LRU cache of
|
||||
# LocalCorpusIndex objects (one per indexed root directory). Default: 5.
|
||||
# OMNIROUTE_CORPUS_CACHE_SIZE=5
|
||||
|
||||
# Model catalog sync interval in hours.
|
||||
# Used by: src/shared/services/modelSyncScheduler.ts — periodic model refresh.
|
||||
# Default: 24
|
||||
@@ -3067,9 +3062,3 @@ QUOTA_STORE_DRIVER=sqlite
|
||||
# without a configured budget are always considered affordable. Requires the
|
||||
# provider_quota_state table (migration 148).
|
||||
# OMNIROUTE_QUOTA_AWARE_ROUTING=0
|
||||
|
||||
# ─── LOCAL CORPUS (opt-in document index) ───
|
||||
# Size of the in-memory LRU index cache for the local document corpus used by
|
||||
# corpus-aware retrieval. Higher values keep more index entries hot.
|
||||
# Used by: src/lib/localCorpus/configured.ts
|
||||
# OMNIROUTE_CORPUS_CACHE_SIZE=5
|
||||
|
||||
@@ -46,7 +46,7 @@ Repository map and Reference Documentation sections below.
|
||||
|
||||
## Project at a Glance
|
||||
|
||||
**OmniRoute** — unified AI proxy/router. One endpoint, 354 LLM providers, auto-fallback.
|
||||
**OmniRoute** — unified AI proxy/router. One endpoint, 353 LLM providers, auto-fallback.
|
||||
|
||||
| Layer | Location | Purpose |
|
||||
| ------------- | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
- **feat(providers):** add **Nimble** as a web-search and web-fetch provider (`nimble-search`) — `/v1/search` routes to Nimble's search API at `lite` depth (locale, freshness and include/exclude domain filters mapped onto the shared request contract), and `/v1/web/fetch` routes to Nimble Extract, which covers all four fetch formats (`markdown`, `html`, `links`, `screenshot`) from a single call. One API key serves both surfaces.
|
||||
@@ -1 +0,0 @@
|
||||
- fix(handoff): enforce provider allowlist for universal handoff (#11602) — universal handoff now skips summarization when the selected summary model's provider is not included in the configured provider allowlist.
|
||||
@@ -1 +0,0 @@
|
||||
- **fix(dashboard):** Show a stable error state when Search Analytics returns an HTTP error or malformed data ([#11603](https://github.com/diegosouzapw/OmniRoute/pull/11603)) — thanks @pacocartones
|
||||
@@ -1 +0,0 @@
|
||||
- **fix(dashboard):** Keep the Profile level and progress aligned with aggregate XP, including bounded handling for invalid totals ([#11604](https://github.com/diegosouzapw/OmniRoute/pull/11604)) — thanks @pacocartones
|
||||
@@ -1 +0,0 @@
|
||||
- **fix(dashboard):** Prevent locked hidden badges from revealing their icon or opening private badge details before they are earned ([#11605](https://github.com/diegosouzapw/OmniRoute/pull/11605)) — thanks @pacocartones
|
||||
@@ -1 +0,0 @@
|
||||
- **fix(dashboard):** Restore keyboard focus after shared modals close and cancel delayed autofocus during cleanup ([#11607](https://github.com/diegosouzapw/OmniRoute/pull/11607)) — thanks @pacocartones
|
||||
@@ -1 +0,0 @@
|
||||
- **fix(dashboard):** Enable Enter and Space activation for clickable data-table rows without hijacking nested controls ([#11610](https://github.com/diegosouzapw/OmniRoute/pull/11610)) — thanks @pacocartones
|
||||
@@ -1 +0,0 @@
|
||||
- **fix(autoCombo,sse):** vendor-retired catalog ids are dropped from the auto-combo candidate pool and no longer win on leftover `arena_elo` / `user_override` rows; `getModelLifecycleDecision` consults `model-lifecycle.json` (prefix-stripped) so aggregator traffic is not `untracked` for ids the snapshot already knows ([#11625](https://github.com/diegosouzapw/OmniRoute/issues/11625))
|
||||
@@ -3093,6 +3093,11 @@
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"src/sse/services/auth.ts": {
|
||||
"@typescript-eslint/no-unused-vars": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"src/sse/services/model.ts": {
|
||||
"no-restricted-imports": {
|
||||
"count": 2
|
||||
|
||||
@@ -435,7 +435,7 @@
|
||||
"src/shared/components/analytics/charts.tsx": 1346,
|
||||
"src/shared/services/cliRuntime.ts": 1459,
|
||||
"src/sse/handlers/chat.ts": 2493,
|
||||
"src/sse/services/auth.ts": 3432,
|
||||
"src/sse/services/auth.ts": 3376,
|
||||
"_rebaseline_2026_08_24_lasterror_provider_error_detail": "PR (ntdat812) own growth: src/sse/services/auth.ts 3344->3346 (+2). One line is the import of describeUpstreamFailure from @/shared/utils/upstreamError, which replaces the string-only collapse `typeof errorText === \"string\" ? errorText.slice(0, 100) : \"Provider error\"` at the single markAccountUnavailable chokepoint (net 0 lines there) — the logic itself lives in upstreamError.ts, next to the extractErrorMessage it reuses, so nothing else moved into this file. The second line is the repo's own lint-staged prettier pass splitting a pre-existing two-statements-on-one-line at getProviderCredentials (`invalidateManagedLease(...); log.warn(...)`); it re-applies on any commit that touches this file, so it is not separable from the change. Covered by tests/unit/provider-error-detail-lastError.test.ts.",
|
||||
"_rebaseline_2026_08_23_11186_synced_inventory_routing": "PR #11186 (pacocartones) own growth: src/sse/services/auth.ts 3260->3337 (+77, loadAdvertisedModelsForSelfHostedConnections + the modelNotAdvertised candidate-filter predicate — pins chat routing to the connection whose synced inventory actually advertises the model, fixing spurious model-not-found on multi-host self-hosted setups; at the existing credential-selection chokepoint, not extractable without splitting the selection flow). Covered by tests/unit/chat-routing-synced-inventory-11089.test.ts. Owner pre-authorized baseline bumps 2026-08-22.",
|
||||
"tests/unit/account-fallback-service.test.ts": 2044,
|
||||
@@ -459,7 +459,7 @@
|
||||
"_rebaseline_2026_08_22_11156_enter_check_disabled": "PR #11156 (rqzbeh) own growth: AddApiKeyModal.tsx 1080->1082 (+2, Enter keydown handler now mirrors the isCheckDisabled condition — owner-requested post-merge polish from #11056; the rest of the diff is Prettier reflow). Covered by tests/unit/ui/add-api-key-modal-enter-key.test.tsx (jsdom render test, Enter dispatch assertions).",
|
||||
"src/app/(dashboard)/dashboard/providers/[id]/hooks/useProviderConnections.ts": 1051,
|
||||
"src/shared/components/ModelSelectModal.tsx": 1138,
|
||||
"src/shared/constants/providers/apikey/gateways.ts": 1330,
|
||||
"src/shared/constants/providers/apikey/gateways.ts": 1321,
|
||||
"open-sse/vendor/codex-chatgpt-web/bridge.ts": 1387,
|
||||
"_rebaseline_2026_08_11_v3850_merge_storm_provider_registry": "DRIFT do merge-storm 2026-08-11 (99 PRs mergeados no release/v3.8.50). AddApiKeyModal.tsx (PR #8949 ChatGPT Web provider) e useProviderConnections.ts/ModelSelectModal.tsx (PRs #9011 combo test-all, #9499 image combos) = UI nova legitima acima do cap; gateways.ts = god-file de catalogo de providers que cresceu com PRs #9009/#9421/#9468/#9594 (qualquer split arriscaria corromper o merge de novo — o proprio PR #9421 quebrou o arquivo); bridge.ts (PR #8949) = ponte Chromium vendored; proxyFetch.ts 1207->1220 = drift herdado de merges. Owner autorizou rebaseline com anotacao (2026-08-11).",
|
||||
"src/lib/modelCapabilities.ts": 1072,
|
||||
@@ -480,8 +480,7 @@
|
||||
"src/lib/guardrails/videoBridgeRuntime.ts": 1009,
|
||||
"_rebaseline_2026_08_24_video_bridge_fu02_fu07_sampler": "PRs #11344 (FU-02 one-frame scene-aware determinism) + #11381 (FU-07 opt-in segment_aware structural sampling) own growth: videoBridgeRuntime.ts <1000->1009, +9 (sum of both boarded together in the same merge-batch). #11344 adds the deterministic one-frame midpoint fallback + policyEffective=uniform report at the existing scene_aware seam; #11381 adds the bounded local-only FFmpeg structural pre-analysis pass (scene/freeze/blur/exposure/SI-TI) and its budget-reallocation logic. Covered by tests/unit/guardrails/videoBridgeSampler.test.ts, tests/unit/guardrails/videoBridgeFu07StructuralSampling.test.ts, tests/integration/video-bridge-sampler-ffmpeg.test.ts. Owner pre-authorized rebaseline for legitimate PR growth (2026-08-19 directive).",
|
||||
"open-sse/services/autoCombo/virtualFactory.ts": 1130,
|
||||
"src/lib/cloudflaredTunnel.ts": 1078,
|
||||
"src/shared/components/RequestLoggerDetail.tsx": 1018
|
||||
"src/lib/cloudflaredTunnel.ts": 1078
|
||||
},
|
||||
"_rebaseline_base_2026_08_10_proxyfetch": "Base-red fix (green-prs sweep, issue #9985): open-sse/utils/proxyFetch.ts 1207 > cap 1000 — new proxied-TLS fetch helper introduced by the Fal reference-image work. Owner-authorized quick rebaseline to green; structural slim tracked for v3.9.0.",
|
||||
"_rebaseline_2026_07_27_v3849_train2": "Merge-train 2 (7 PRs) — owner-approved 2026-07-27. Single entry: chatCore.ts 4955->5006 (#8595, Responses multi-turn image compaction before the context hard-reject). Genuine irreducible growth at the existing compaction chokepoint in handleChatCore — the PR adds a last-resort retry against the concrete budget plus the estimateFinalInputTokens helper, both wired at the pre-existing call site rather than a new branch. Covered by tests/unit/8560-responses-image-compaction.test.ts (4 tests).",
|
||||
@@ -651,6 +650,5 @@
|
||||
"_rebaseline_2026_08_20_8338_cursor_image_provider": "PR (reimplementation of #8338, @valvesss): imageRegistry.ts 1019->1033 = new cursor IMAGE_PROVIDERS entry (Cursor plan image generation via Agent CLI), +14 lines of declarative provider metadata. Same god-registry no-split rationale as prior imageRegistry/gateways rebaselines.",
|
||||
"_rebaseline_2026_08_20_imageregistry_1034": "imageRegistry.ts 1033->1034: +1 line drift between #10842 (cursor image provider, froze at 1033) and its actual merged state on release (measured 1034) — trivial rebaseline, not a new feature.",
|
||||
"_rebaseline_2026_08_25_11146_subscription_first_auto": "PR #11146 (@yourspraveen, subscription-first auto groupings auto/subscription+auto/thrifty): open-sse/services/autoCombo/virtualFactory.ts is a NEW file in this PR landing at 1128 lines (+2 margin) — two opt-in flat auto ids built on the established auto/best-free pattern (connectionBillingCatalog + subscriptionLadder pure functions). Frozen at merge size per owner-authorized rebaseline directive (2026-08-19, merge-batch Step 4); no further growth without split rationale.",
|
||||
"_rebaseline_2026_08_26_mergebatch_v3851_batch1": "/merge-batch 2026-08-26 (v3.8.51): three legitimate growths from this batch. #11448 src/app/api/providers/[id]/test/route.ts 1237->1262 (auto-test-on-create wiring). #11495 src/sse/services/auth.ts 3346->3376 (web-cookie health-sweep verify-only path). #11561 src/lib/cloudflaredTunnel.ts new named-tunnel mode, lands at 1078 (+78 over the 1000 new-file cap) for the CLOUDFLARED_CONFIG named-tunnel flow (login->create->route dns config parsing + readiness detection). Owner-authorized rebaseline per merge-batch Step 4 (2026-08-19 directive); no further growth without split rationale.",
|
||||
"_rebaseline_2026_08_26_mergebatch_v3851_batch2": "/merge-batch 2026-08-26 (v3.8.51) batch 2: three legitimate growths. #11083 src/shared/components/RequestLoggerDetail.tsx new-file cap, lands at 1018 (+18 over 1000) — copy-all button for request detail modal. #11631 src/shared/constants/providers/apikey/gateways.ts 1321->1330 (1min.ai gateway entry). #11628 src/sse/services/auth.ts 3376->3432 (credential-health isolation from model failures). Owner-authorized rebaseline per merge-batch Step 4 (2026-08-19 directive); no further growth without split rationale."
|
||||
"_rebaseline_2026_08_26_mergebatch_v3851_batch1": "/merge-batch 2026-08-26 (v3.8.51): three legitimate growths from this batch. #11448 src/app/api/providers/[id]/test/route.ts 1237->1262 (auto-test-on-create wiring). #11495 src/sse/services/auth.ts 3346->3376 (web-cookie health-sweep verify-only path). #11561 src/lib/cloudflaredTunnel.ts new named-tunnel mode, lands at 1078 (+78 over the 1000 new-file cap) for the CLOUDFLARED_CONFIG named-tunnel flow (login->create->route dns config parsing + readiness detection). Owner-authorized rebaseline per merge-batch Step 4 (2026-08-19 directive); no further growth without split rationale."
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<svg viewBox="0 0 1200 350" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Animated terminal demoing the OmniRoute CLI: omniroute providers list (354 providers registered, anthropic, codex, glm, kimi shown active), omniroute combo list (always-on priority, cost-saver, fusion-panel, context-relay) and omniroute health (healthy, 18412 requests in 24h, p95 412ms, circuit breakers 24 closed, 1 half-open, 0 open), cycling over 85 top-level commands: providers, oauth, keys, combo, nodes, models, cache, compression, cost, usage, quota, health, resilience, telemetry, logs, audit, mcp, a2a, cloud, memory, skills, eval, doctor, repl, tunnel, backup, sync, webhooks, policy, pricing, translator, simulate and more.">
|
||||
<svg viewBox="0 0 1200 350" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Animated terminal demoing the OmniRoute CLI: omniroute providers list (353 providers registered, anthropic, codex, glm, kimi shown active), omniroute combo list (always-on priority, cost-saver, fusion-panel, context-relay) and omniroute health (healthy, 18412 requests in 24h, p95 412ms, circuit breakers 24 closed, 1 half-open, 0 open), cycling over 85 top-level commands: providers, oauth, keys, combo, nodes, models, cache, compression, cost, usage, quota, health, resilience, telemetry, logs, audit, mcp, a2a, cloud, memory, skills, eval, doctor, repl, tunnel, backup, sync, webhooks, policy, pricing, translator, simulate and more.">
|
||||
<desc>Compact animated terminal cycling three real OmniRoute CLI commands with a typewriter effect and a scrolling subcommand ticker; the first frame shows the completed providers-list screen.</desc>
|
||||
<defs><clipPath id="tickerClip"><rect x="12" y="304" width="1176" height="40"/></clipPath><clipPath id="tw0"><rect x="64" y="46" height="26" width="0"><animate attributeName="width" calcMode="discrete" values="0;31;61;92;122;153;184;214;245;245" keyTimes="0;0.012;0.018;0.024;0.030;0.036;0.042;0.048;0.054;1" dur="18s" repeatCount="indefinite"/></rect></clipPath><clipPath id="tw1"><rect x="64" y="46" height="26" width="0"><animate attributeName="width" calcMode="discrete" values="0;26;51;76;102;128;153;178;204;204" keyTimes="0;0.348;0.351;0.357;0.363;0.369;0.375;0.381;0.387;1" dur="18s" repeatCount="indefinite"/></rect></clipPath><clipPath id="tw2"><rect x="64" y="46" height="26" width="0"><animate attributeName="width" calcMode="discrete" values="0;20;41;61;82;102;122;143;163;163" keyTimes="0;0.678;0.684;0.690;0.696;0.702;0.708;0.714;0.720;1" dur="18s" repeatCount="indefinite"/></rect></clipPath></defs>
|
||||
<rect width="1200" height="350" fill="#0d1117"/>
|
||||
|
||||
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
@@ -1,4 +1,4 @@
|
||||
<svg viewBox="0 0 1200 780" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Comparison table: OmniRoute versus 9router, OpenRouter, CLIProxyAPI and LiteLLM across 13 capabilities. OmniRoute is the only one with the full set: 354 providers, 90+ free providers built-in, 19 routing strategies, 12-engine token compression, a built-in MCP server with 110 tools, A2A protocol, persistent memory, guardrails, cloud agents, TLS fingerprint stealth, desktop/Termux/PWA, 43 UI locales and 100% MIT self-hosted. 9router has free providers, RTK compression and translation but no MCP, A2A, memory, guardrails, cloud agents or stealth. OpenRouter is a hosted SaaS with 400+ models, guardrails and a hosted MCP but is not self-hosted and lacks A2A, memory, cloud agents and stealth. CLIProxyAPI is a light OAuth proxy with two routing strategies. LiteLLM has 100+ providers, A2A and extensive guardrails but no memory, compression, free tier, stealth or cloud agents.">
|
||||
<svg viewBox="0 0 1200 780" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Comparison table: OmniRoute versus 9router, OpenRouter, CLIProxyAPI and LiteLLM across 13 capabilities. OmniRoute is the only one with the full set: 353 providers, 90+ free providers built-in, 19 routing strategies, 12-engine token compression, a built-in MCP server with 110 tools, A2A protocol, persistent memory, guardrails, cloud agents, TLS fingerprint stealth, desktop/Termux/PWA, 43 UI locales and 100% MIT self-hosted. 9router has free providers, RTK compression and translation but no MCP, A2A, memory, guardrails, cloud agents or stealth. OpenRouter is a hosted SaaS with 400+ models, guardrails and a hosted MCP but is not self-hosted and lacks A2A, memory, cloud agents and stealth. CLIProxyAPI is a light OAuth proxy with two routing strategies. LiteLLM has 100+ providers, A2A and extensive guardrails but no memory, compression, free tier, stealth or cloud agents.">
|
||||
<desc>Static-header comparison table where each capability row fades in top to bottom; the OmniRoute column is highlighted and shows a check or a leading value in every row, while competitors show a mix of checks, partials and crosses.</desc>
|
||||
<defs>
|
||||
<pattern id="gC" width="32" height="32" patternUnits="userSpaceOnUse"><path d="M 32 0 L 0 0 0 32" fill="none" stroke="#ffffff" stroke-opacity="0.05" stroke-width="1"/></pattern>
|
||||
|
||||
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 13 KiB |
@@ -1,4 +1,4 @@
|
||||
<svg viewBox="0 0 1200 540" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="The OmniRoute promise: one endpoint and 354 providers. Six pillars. Resilient fallback: automatic routing continues while another healthy target is available. Save up to 95 percent of eligible tokens: RTK plus Caveman stacked compression averages about 89 percent on tool-heavy sessions. Zero dollars to start: 90+ providers with a free tier and 56 recurring or keyless free-forever providers. Every tool works: 35 CLI and agent integration records, including Claude Code, Codex, Cursor, Cline, Copilot and Antigravity, through one config. One endpoint: OpenAI, Claude, Gemini and Responses API translation at /v1. Production controls: circuit breakers, TLS stealth, MCP with 110 tools, A2A, memory, guardrails, evals, and 39,000+ static test declarations across 5,100+ tracked test files.">
|
||||
<svg viewBox="0 0 1200 540" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="The OmniRoute promise: one endpoint and 353 providers. Six pillars. Resilient fallback: automatic routing continues while another healthy target is available. Save up to 95 percent of eligible tokens: RTK plus Caveman stacked compression averages about 89 percent on tool-heavy sessions. Zero dollars to start: 90+ providers with a free tier and 56 recurring or keyless free-forever providers. Every tool works: 35 CLI and agent integration records, including Claude Code, Codex, Cursor, Cline, Copilot and Antigravity, through one config. One endpoint: OpenAI, Claude, Gemini and Responses API translation at /v1. Production controls: circuit breakers, TLS stealth, MCP with 110 tools, A2A, memory, guardrails, evals, and 39,000+ static test declarations across 5,100+ tracked test files.">
|
||||
<desc>Animated promise card: six pillar tiles fade in in reading order, then a soft colored border highlight sweeps from tile to tile in a continuous cycle.</desc>
|
||||
<defs>
|
||||
<pattern id="gridPaperP" width="32" height="32" patternUnits="userSpaceOnUse">
|
||||
@@ -21,7 +21,7 @@
|
||||
<line x1="150" y1="53" x2="1160" y2="53" stroke="#232b38" stroke-width="1.5"/>
|
||||
</g>
|
||||
<g>
|
||||
<text x="40" y="100" font-family="Inter, 'Segoe UI', Arial, Helvetica, system-ui, sans-serif" font-size="23" font-weight="600" fill="#c9d1d9">One endpoint. <tspan fill="#a78bfa" font-weight="800">354 providers.</tspan> Never stop building — OmniRoute picks <tspan fill="#7ee787" font-weight="700">the cheapest one that works</tspan>.</text>
|
||||
<text x="40" y="100" font-family="Inter, 'Segoe UI', Arial, Helvetica, system-ui, sans-serif" font-size="23" font-weight="600" fill="#c9d1d9">One endpoint. <tspan fill="#a78bfa" font-weight="800">353 providers.</tspan> Never stop building — OmniRoute picks <tspan fill="#7ee787" font-weight="700">the cheapest one that works</tspan>.</text>
|
||||
</g>
|
||||
|
||||
<g font-family="Inter, 'Segoe UI', Arial, Helvetica, system-ui, sans-serif">
|
||||
@@ -38,7 +38,7 @@
|
||||
<line x1="3.9" y1="3.9" x2="18.1" y2="18.1"/>
|
||||
</g>
|
||||
<text x="102" y="170" font-size="18" font-weight="800" fill="#74b9ff">Never hit limits</text>
|
||||
<text x="66" y="204" font-size="13.5" fill="#a1a1aa">Auto-fallback across 354 providers in</text>
|
||||
<text x="66" y="204" font-size="13.5" fill="#a1a1aa">Auto-fallback across 353 providers in</text>
|
||||
<text x="66" y="226" font-size="13.5" fill="#a1a1aa">milliseconds. Quota out? The next provider</text>
|
||||
<text x="66" y="248" font-size="13.5" fill="#a1a1aa">takes over while a healthy target remains.</text>
|
||||
</g>
|
||||
|
||||
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 10 KiB |
@@ -1,4 +1,4 @@
|
||||
<svg viewBox="0 0 1200 548" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="OmniRoute hero: Never stop coding. Every AI tool to 354 providers — 90+ free — through one endpoint. Claude Code, Codex, Cursor, Cline, Copilot and Antigravity into free Claude, GPT and Gemini with auto-fallback. RTK + Caveman stacked compression saves 15 to 95 percent of tokens — about 89 percent average on tool-heavy sessions — so you never hit limits. Stats: 354 AI providers, 90+ free tiers, about 1.51B free tokens per month, 15 to 95 percent token savings, 19 routing strategies, zero dollars to start.">
|
||||
<svg viewBox="0 0 1200 548" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="OmniRoute hero: Never stop coding. Every AI tool to 353 providers — 90+ free — through one endpoint. Claude Code, Codex, Cursor, Cline, Copilot and Antigravity into free Claude, GPT and Gemini with auto-fallback. RTK + Caveman stacked compression saves 15 to 95 percent of tokens — about 89 percent average on tool-heavy sessions — so you never hit limits. Stats: 353 AI providers, 90+ free tiers, about 1.51B free tokens per month, 15 to 95 percent token savings, 19 routing strategies, zero dollars to start.">
|
||||
<desc>Animated hero card: a pulse travels the divider line and a compression bar demo repeatedly shrinks a prompt by up to 95 percent; all headline content is static and readable on the first frame.</desc>
|
||||
<defs>
|
||||
<pattern id="gridPaperH" width="32" height="32" patternUnits="userSpaceOnUse">
|
||||
@@ -28,7 +28,7 @@
|
||||
<text x="48" y="138" font-family="Inter, 'Segoe UI', Arial, Helvetica, system-ui, sans-serif" font-size="60" font-weight="800" fill="#e9edf3">Never stop coding<tspan fill="#a855f7">.</tspan></text>
|
||||
|
||||
<!-- subheadline -->
|
||||
<text x="48" y="184" font-family="Inter, 'Segoe UI', Arial, Helvetica, system-ui, sans-serif" font-size="25" font-weight="600" fill="#c9d1d9">Every AI tool → <tspan fill="#a78bfa" font-weight="800">354 providers</tspan> — <tspan fill="#7ee787" font-weight="800">90+ free</tspan> — through one endpoint.</text>
|
||||
<text x="48" y="184" font-family="Inter, 'Segoe UI', Arial, Helvetica, system-ui, sans-serif" font-size="25" font-weight="600" fill="#c9d1d9">Every AI tool → <tspan fill="#a78bfa" font-weight="800">353 providers</tspan> — <tspan fill="#7ee787" font-weight="800">90+ free</tspan> — through one endpoint.</text>
|
||||
|
||||
<!-- plug line -->
|
||||
<text x="48" y="222" font-family="Inter, 'Segoe UI', Arial, Helvetica, system-ui, sans-serif" font-size="16.5" fill="#a1a1aa">Claude Code · Codex · Cursor · Cline · Copilot · Antigravity  →  <tspan fill="#7ee787" font-weight="700">FREE</tspan> Claude / GPT / Gemini · auto-fallback</text>
|
||||
|
||||
|
Before Width: | Height: | Size: 7.3 KiB After Width: | Height: | Size: 7.3 KiB |
@@ -57,7 +57,7 @@ New tab for extracting content from a URL via `POST /v1/web/fetch` (created in p
|
||||
- Submit → fetch → render `ScrapeResult.tsx`.
|
||||
- `ScrapeResult` renders markdown preview + raw toggle.
|
||||
- Cap: if response body > **256 KB**, UI shows `(truncated, view raw)` and opens raw in a Monaco modal (D21).
|
||||
- Metadata panel: provider (firecrawl/jina-reader/tavily-search/tinyfish/nimble-search), latency, cost, response size, links count.
|
||||
- Metadata panel: provider (firecrawl/jina-reader/tavily-search/tinyfish), latency, cost, response size, links count.
|
||||
- Uses `useScrapeFetch.ts` hook.
|
||||
|
||||
### Compare Tab
|
||||
@@ -108,7 +108,7 @@ Runs the same query/URL across up to **4 providers in parallel** (D22):
|
||||
| Field | Source |
|
||||
| ------------------------------ | ------------------------------------------------------------------------------------------ |
|
||||
| `id`, `name` | `searchRegistry.ts` |
|
||||
| `kind` | `"search"` or `"fetch"` (firecrawl, jina-reader, tavily-search, tinyfish, nimble-search) |
|
||||
| `kind` | `"search"` (12 providers) or `"fetch"` (firecrawl, jina-reader, tavily-search, tinyfish) |
|
||||
| `costPerQuery` | Registry data |
|
||||
| `freeMonthlyQuota` | Registry data |
|
||||
| `searchTypes` / `fetchFormats` | Registry data |
|
||||
@@ -136,7 +136,7 @@ Only one backend change was needed for this feature:
|
||||
|
||||
`src/app/api/search/providers/route.ts` was extended to:
|
||||
|
||||
- Include every fetch provider (`firecrawl`, `jina-reader`, `tavily-search`, `tinyfish`, `nimble-search`) in the array.
|
||||
- Include all 4 fetch providers (`firecrawl`, `jina-reader`, `tavily-search`, `tinyfish`) in the array.
|
||||
- Add `kind: "search" | "fetch"` to every item.
|
||||
- Add `status: "configured" | "missing" | "rate_limited"` derived from live credential state.
|
||||
- Maintain backward compatibility — existing fields (`id`, `name`, etc.) unchanged.
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
---
|
||||
|
||||
> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 354 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
|
||||
> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 353 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
|
||||
|
||||
## Overview
|
||||
|
||||
@@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
|
||||
## Key Features (v3.8.50)
|
||||
|
||||
### Core Proxy
|
||||
- **354 AI providers** with automatic format translation
|
||||
- **353 AI providers** with automatic format translation
|
||||
- **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible)
|
||||
- **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline
|
||||
- **4-tier fallback**: Subscription → API Key → Cheap → Free
|
||||
@@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
|
||||
|
||||
## v3.8.x Highlights
|
||||
|
||||
- **354-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
|
||||
- **353-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
|
||||
- **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay`
|
||||
- **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown
|
||||
- **MCP server expanded to 110 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules)
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
---
|
||||
|
||||
> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 354 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
|
||||
> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 353 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
|
||||
|
||||
## Overview
|
||||
|
||||
@@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
|
||||
## Key Features (v3.8.50)
|
||||
|
||||
### Core Proxy
|
||||
- **354 AI providers** with automatic format translation
|
||||
- **353 AI providers** with automatic format translation
|
||||
- **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible)
|
||||
- **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline
|
||||
- **4-tier fallback**: Subscription → API Key → Cheap → Free
|
||||
@@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
|
||||
|
||||
## v3.8.x Highlights
|
||||
|
||||
- **354-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
|
||||
- **353-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
|
||||
- **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay`
|
||||
- **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown
|
||||
- **MCP server expanded to 110 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules)
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
---
|
||||
|
||||
> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 354 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
|
||||
> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 353 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
|
||||
|
||||
## Overview
|
||||
|
||||
@@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
|
||||
## Key Features (v3.8.50)
|
||||
|
||||
### Core Proxy
|
||||
- **354 AI providers** with automatic format translation
|
||||
- **353 AI providers** with automatic format translation
|
||||
- **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible)
|
||||
- **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline
|
||||
- **4-tier fallback**: Subscription → API Key → Cheap → Free
|
||||
@@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
|
||||
|
||||
## v3.8.x Highlights
|
||||
|
||||
- **354-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
|
||||
- **353-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
|
||||
- **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay`
|
||||
- **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown
|
||||
- **MCP server expanded to 110 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules)
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
---
|
||||
|
||||
> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 354 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
|
||||
> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 353 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
|
||||
|
||||
## Overview
|
||||
|
||||
@@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
|
||||
## Key Features (v3.8.50)
|
||||
|
||||
### Core Proxy
|
||||
- **354 AI providers** with automatic format translation
|
||||
- **353 AI providers** with automatic format translation
|
||||
- **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible)
|
||||
- **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline
|
||||
- **4-tier fallback**: Subscription → API Key → Cheap → Free
|
||||
@@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
|
||||
|
||||
## v3.8.x Highlights
|
||||
|
||||
- **354-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
|
||||
- **353-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
|
||||
- **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay`
|
||||
- **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown
|
||||
- **MCP server expanded to 110 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules)
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
---
|
||||
|
||||
> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 354 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
|
||||
> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 353 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
|
||||
|
||||
## Overview
|
||||
|
||||
@@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
|
||||
## Key Features (v3.8.50)
|
||||
|
||||
### Core Proxy
|
||||
- **354 AI providers** with automatic format translation
|
||||
- **353 AI providers** with automatic format translation
|
||||
- **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible)
|
||||
- **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline
|
||||
- **4-tier fallback**: Subscription → API Key → Cheap → Free
|
||||
@@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
|
||||
|
||||
## v3.8.x Highlights
|
||||
|
||||
- **354-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
|
||||
- **353-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
|
||||
- **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay`
|
||||
- **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown
|
||||
- **MCP server expanded to 110 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules)
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
---
|
||||
|
||||
> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 354 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
|
||||
> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 353 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
|
||||
|
||||
## Overview
|
||||
|
||||
@@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
|
||||
## Key Features (v3.8.50)
|
||||
|
||||
### Core Proxy
|
||||
- **354 AI providers** with automatic format translation
|
||||
- **353 AI providers** with automatic format translation
|
||||
- **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible)
|
||||
- **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline
|
||||
- **4-tier fallback**: Subscription → API Key → Cheap → Free
|
||||
@@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
|
||||
|
||||
## v3.8.x Highlights
|
||||
|
||||
- **354-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
|
||||
- **353-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
|
||||
- **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay`
|
||||
- **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown
|
||||
- **MCP server expanded to 110 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules)
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
---
|
||||
|
||||
> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 354 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
|
||||
> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 353 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
|
||||
|
||||
## Overview
|
||||
|
||||
@@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
|
||||
## Key Features (v3.8.50)
|
||||
|
||||
### Core Proxy
|
||||
- **354 AI providers** with automatic format translation
|
||||
- **353 AI providers** with automatic format translation
|
||||
- **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible)
|
||||
- **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline
|
||||
- **4-tier fallback**: Subscription → API Key → Cheap → Free
|
||||
@@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
|
||||
|
||||
## v3.8.x Highlights
|
||||
|
||||
- **354-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
|
||||
- **353-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
|
||||
- **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay`
|
||||
- **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown
|
||||
- **MCP server expanded to 110 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules)
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
---
|
||||
|
||||
> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 354 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
|
||||
> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 353 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
|
||||
|
||||
## Overview
|
||||
|
||||
@@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
|
||||
## Key Features (v3.8.50)
|
||||
|
||||
### Core Proxy
|
||||
- **354 AI providers** with automatic format translation
|
||||
- **353 AI providers** with automatic format translation
|
||||
- **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible)
|
||||
- **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline
|
||||
- **4-tier fallback**: Subscription → API Key → Cheap → Free
|
||||
@@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
|
||||
|
||||
## v3.8.x Highlights
|
||||
|
||||
- **354-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
|
||||
- **353-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
|
||||
- **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay`
|
||||
- **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown
|
||||
- **MCP server expanded to 110 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules)
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
---
|
||||
|
||||
> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 354 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
|
||||
> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 353 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
|
||||
|
||||
## Overview
|
||||
|
||||
@@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
|
||||
## Key Features (v3.8.50)
|
||||
|
||||
### Core Proxy
|
||||
- **354 AI providers** with automatic format translation
|
||||
- **353 AI providers** with automatic format translation
|
||||
- **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible)
|
||||
- **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline
|
||||
- **4-tier fallback**: Subscription → API Key → Cheap → Free
|
||||
@@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
|
||||
|
||||
## v3.8.x Highlights
|
||||
|
||||
- **354-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
|
||||
- **353-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
|
||||
- **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay`
|
||||
- **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown
|
||||
- **MCP server expanded to 110 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules)
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
---
|
||||
|
||||
> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 354 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
|
||||
> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 353 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
|
||||
|
||||
## Overview
|
||||
|
||||
@@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
|
||||
## Key Features (v3.8.50)
|
||||
|
||||
### Core Proxy
|
||||
- **354 AI providers** with automatic format translation
|
||||
- **353 AI providers** with automatic format translation
|
||||
- **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible)
|
||||
- **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline
|
||||
- **4-tier fallback**: Subscription → API Key → Cheap → Free
|
||||
@@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
|
||||
|
||||
## v3.8.x Highlights
|
||||
|
||||
- **354-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
|
||||
- **353-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
|
||||
- **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay`
|
||||
- **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown
|
||||
- **MCP server expanded to 110 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules)
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
---
|
||||
|
||||
> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 354 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
|
||||
> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 353 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
|
||||
|
||||
## Overview
|
||||
|
||||
@@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
|
||||
## Key Features (v3.8.50)
|
||||
|
||||
### Core Proxy
|
||||
- **354 AI providers** with automatic format translation
|
||||
- **353 AI providers** with automatic format translation
|
||||
- **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible)
|
||||
- **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline
|
||||
- **4-tier fallback**: Subscription → API Key → Cheap → Free
|
||||
@@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
|
||||
|
||||
## v3.8.x Highlights
|
||||
|
||||
- **354-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
|
||||
- **353-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
|
||||
- **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay`
|
||||
- **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown
|
||||
- **MCP server expanded to 110 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules)
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
---
|
||||
|
||||
> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 354 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
|
||||
> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 353 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
|
||||
|
||||
## Overview
|
||||
|
||||
@@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
|
||||
## Key Features (v3.8.50)
|
||||
|
||||
### Core Proxy
|
||||
- **354 AI providers** with automatic format translation
|
||||
- **353 AI providers** with automatic format translation
|
||||
- **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible)
|
||||
- **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline
|
||||
- **4-tier fallback**: Subscription → API Key → Cheap → Free
|
||||
@@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
|
||||
|
||||
## v3.8.x Highlights
|
||||
|
||||
- **354-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
|
||||
- **353-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
|
||||
- **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay`
|
||||
- **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown
|
||||
- **MCP server expanded to 110 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules)
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
---
|
||||
|
||||
> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 354 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
|
||||
> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 353 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
|
||||
|
||||
## Overview
|
||||
|
||||
@@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
|
||||
## Key Features (v3.8.50)
|
||||
|
||||
### Core Proxy
|
||||
- **354 AI providers** with automatic format translation
|
||||
- **353 AI providers** with automatic format translation
|
||||
- **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible)
|
||||
- **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline
|
||||
- **4-tier fallback**: Subscription → API Key → Cheap → Free
|
||||
@@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
|
||||
|
||||
## v3.8.x Highlights
|
||||
|
||||
- **354-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
|
||||
- **353-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
|
||||
- **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay`
|
||||
- **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown
|
||||
- **MCP server expanded to 110 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules)
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
---
|
||||
|
||||
> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 354 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
|
||||
> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 353 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
|
||||
|
||||
## Overview
|
||||
|
||||
@@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
|
||||
## Key Features (v3.8.50)
|
||||
|
||||
### Core Proxy
|
||||
- **354 AI providers** with automatic format translation
|
||||
- **353 AI providers** with automatic format translation
|
||||
- **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible)
|
||||
- **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline
|
||||
- **4-tier fallback**: Subscription → API Key → Cheap → Free
|
||||
@@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
|
||||
|
||||
## v3.8.x Highlights
|
||||
|
||||
- **354-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
|
||||
- **353-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
|
||||
- **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay`
|
||||
- **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown
|
||||
- **MCP server expanded to 110 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules)
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
---
|
||||
|
||||
> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 354 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
|
||||
> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 353 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
|
||||
|
||||
## Overview
|
||||
|
||||
@@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
|
||||
## Key Features (v3.8.50)
|
||||
|
||||
### Core Proxy
|
||||
- **354 AI providers** with automatic format translation
|
||||
- **353 AI providers** with automatic format translation
|
||||
- **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible)
|
||||
- **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline
|
||||
- **4-tier fallback**: Subscription → API Key → Cheap → Free
|
||||
@@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
|
||||
|
||||
## v3.8.x Highlights
|
||||
|
||||
- **354-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
|
||||
- **353-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
|
||||
- **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay`
|
||||
- **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown
|
||||
- **MCP server expanded to 110 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules)
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
---
|
||||
|
||||
> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 354 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
|
||||
> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 353 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
|
||||
|
||||
## Overview
|
||||
|
||||
@@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
|
||||
## Key Features (v3.8.50)
|
||||
|
||||
### Core Proxy
|
||||
- **354 AI providers** with automatic format translation
|
||||
- **353 AI providers** with automatic format translation
|
||||
- **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible)
|
||||
- **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline
|
||||
- **4-tier fallback**: Subscription → API Key → Cheap → Free
|
||||
@@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
|
||||
|
||||
## v3.8.x Highlights
|
||||
|
||||
- **354-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
|
||||
- **353-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
|
||||
- **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay`
|
||||
- **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown
|
||||
- **MCP server expanded to 110 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules)
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
---
|
||||
|
||||
> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 354 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
|
||||
> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 353 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
|
||||
|
||||
## Overview
|
||||
|
||||
@@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
|
||||
## Key Features (v3.8.50)
|
||||
|
||||
### Core Proxy
|
||||
- **354 AI providers** with automatic format translation
|
||||
- **353 AI providers** with automatic format translation
|
||||
- **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible)
|
||||
- **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline
|
||||
- **4-tier fallback**: Subscription → API Key → Cheap → Free
|
||||
@@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
|
||||
|
||||
## v3.8.x Highlights
|
||||
|
||||
- **354-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
|
||||
- **353-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
|
||||
- **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay`
|
||||
- **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown
|
||||
- **MCP server expanded to 110 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules)
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
---
|
||||
|
||||
> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 354 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
|
||||
> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 353 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
|
||||
|
||||
## Overview
|
||||
|
||||
@@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
|
||||
## Key Features (v3.8.50)
|
||||
|
||||
### Core Proxy
|
||||
- **354 AI providers** with automatic format translation
|
||||
- **353 AI providers** with automatic format translation
|
||||
- **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible)
|
||||
- **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline
|
||||
- **4-tier fallback**: Subscription → API Key → Cheap → Free
|
||||
@@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
|
||||
|
||||
## v3.8.x Highlights
|
||||
|
||||
- **354-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
|
||||
- **353-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
|
||||
- **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay`
|
||||
- **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown
|
||||
- **MCP server expanded to 110 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules)
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
---
|
||||
|
||||
> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 354 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
|
||||
> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 353 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
|
||||
|
||||
## Overview
|
||||
|
||||
@@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
|
||||
## Key Features (v3.8.50)
|
||||
|
||||
### Core Proxy
|
||||
- **354 AI providers** with automatic format translation
|
||||
- **353 AI providers** with automatic format translation
|
||||
- **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible)
|
||||
- **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline
|
||||
- **4-tier fallback**: Subscription → API Key → Cheap → Free
|
||||
@@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
|
||||
|
||||
## v3.8.x Highlights
|
||||
|
||||
- **354-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
|
||||
- **353-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
|
||||
- **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay`
|
||||
- **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown
|
||||
- **MCP server expanded to 110 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules)
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
---
|
||||
|
||||
> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 354 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
|
||||
> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 353 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
|
||||
|
||||
## Overview
|
||||
|
||||
@@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
|
||||
## Key Features (v3.8.50)
|
||||
|
||||
### Core Proxy
|
||||
- **354 AI providers** with automatic format translation
|
||||
- **353 AI providers** with automatic format translation
|
||||
- **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible)
|
||||
- **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline
|
||||
- **4-tier fallback**: Subscription → API Key → Cheap → Free
|
||||
@@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
|
||||
|
||||
## v3.8.x Highlights
|
||||
|
||||
- **354-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
|
||||
- **353-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
|
||||
- **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay`
|
||||
- **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown
|
||||
- **MCP server expanded to 110 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules)
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
---
|
||||
|
||||
> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 354 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
|
||||
> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 353 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
|
||||
|
||||
## Overview
|
||||
|
||||
@@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
|
||||
## Key Features (v3.8.50)
|
||||
|
||||
### Core Proxy
|
||||
- **354 AI providers** with automatic format translation
|
||||
- **353 AI providers** with automatic format translation
|
||||
- **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible)
|
||||
- **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline
|
||||
- **4-tier fallback**: Subscription → API Key → Cheap → Free
|
||||
@@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
|
||||
|
||||
## v3.8.x Highlights
|
||||
|
||||
- **354-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
|
||||
- **353-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
|
||||
- **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay`
|
||||
- **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown
|
||||
- **MCP server expanded to 110 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules)
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
---
|
||||
|
||||
> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 354 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
|
||||
> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 353 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
|
||||
|
||||
## Overview
|
||||
|
||||
@@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
|
||||
## Key Features (v3.8.50)
|
||||
|
||||
### Core Proxy
|
||||
- **354 AI providers** with automatic format translation
|
||||
- **353 AI providers** with automatic format translation
|
||||
- **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible)
|
||||
- **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline
|
||||
- **4-tier fallback**: Subscription → API Key → Cheap → Free
|
||||
@@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
|
||||
|
||||
## v3.8.x Highlights
|
||||
|
||||
- **354-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
|
||||
- **353-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
|
||||
- **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay`
|
||||
- **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown
|
||||
- **MCP server expanded to 110 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules)
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
---
|
||||
|
||||
> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 354 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
|
||||
> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 353 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
|
||||
|
||||
## Overview
|
||||
|
||||
@@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
|
||||
## Key Features (v3.8.50)
|
||||
|
||||
### Core Proxy
|
||||
- **354 AI providers** with automatic format translation
|
||||
- **353 AI providers** with automatic format translation
|
||||
- **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible)
|
||||
- **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline
|
||||
- **4-tier fallback**: Subscription → API Key → Cheap → Free
|
||||
@@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
|
||||
|
||||
## v3.8.x Highlights
|
||||
|
||||
- **354-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
|
||||
- **353-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
|
||||
- **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay`
|
||||
- **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown
|
||||
- **MCP server expanded to 110 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules)
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
---
|
||||
|
||||
> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 354 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
|
||||
> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 353 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
|
||||
|
||||
## Overview
|
||||
|
||||
@@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
|
||||
## Key Features (v3.8.50)
|
||||
|
||||
### Core Proxy
|
||||
- **354 AI providers** with automatic format translation
|
||||
- **353 AI providers** with automatic format translation
|
||||
- **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible)
|
||||
- **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline
|
||||
- **4-tier fallback**: Subscription → API Key → Cheap → Free
|
||||
@@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
|
||||
|
||||
## v3.8.x Highlights
|
||||
|
||||
- **354-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
|
||||
- **353-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
|
||||
- **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay`
|
||||
- **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown
|
||||
- **MCP server expanded to 110 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules)
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
---
|
||||
|
||||
> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 354 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
|
||||
> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 353 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
|
||||
|
||||
## Overview
|
||||
|
||||
@@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
|
||||
## Key Features (v3.8.50)
|
||||
|
||||
### Core Proxy
|
||||
- **354 AI providers** with automatic format translation
|
||||
- **353 AI providers** with automatic format translation
|
||||
- **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible)
|
||||
- **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline
|
||||
- **4-tier fallback**: Subscription → API Key → Cheap → Free
|
||||
@@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
|
||||
|
||||
## v3.8.x Highlights
|
||||
|
||||
- **354-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
|
||||
- **353-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
|
||||
- **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay`
|
||||
- **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown
|
||||
- **MCP server expanded to 110 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules)
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
---
|
||||
|
||||
> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 354 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
|
||||
> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 353 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
|
||||
|
||||
## Overview
|
||||
|
||||
@@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
|
||||
## Key Features (v3.8.50)
|
||||
|
||||
### Core Proxy
|
||||
- **354 AI providers** with automatic format translation
|
||||
- **353 AI providers** with automatic format translation
|
||||
- **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible)
|
||||
- **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline
|
||||
- **4-tier fallback**: Subscription → API Key → Cheap → Free
|
||||
@@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
|
||||
|
||||
## v3.8.x Highlights
|
||||
|
||||
- **354-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
|
||||
- **353-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
|
||||
- **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay`
|
||||
- **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown
|
||||
- **MCP server expanded to 110 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules)
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
---
|
||||
|
||||
> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 354 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
|
||||
> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 353 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
|
||||
|
||||
## Overview
|
||||
|
||||
@@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
|
||||
## Key Features (v3.8.50)
|
||||
|
||||
### Core Proxy
|
||||
- **354 AI providers** with automatic format translation
|
||||
- **353 AI providers** with automatic format translation
|
||||
- **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible)
|
||||
- **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline
|
||||
- **4-tier fallback**: Subscription → API Key → Cheap → Free
|
||||
@@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
|
||||
|
||||
## v3.8.x Highlights
|
||||
|
||||
- **354-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
|
||||
- **353-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
|
||||
- **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay`
|
||||
- **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown
|
||||
- **MCP server expanded to 110 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules)
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
---
|
||||
|
||||
> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 354 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
|
||||
> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 353 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
|
||||
|
||||
## Overview
|
||||
|
||||
@@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
|
||||
## Key Features (v3.8.50)
|
||||
|
||||
### Core Proxy
|
||||
- **354 AI providers** with automatic format translation
|
||||
- **353 AI providers** with automatic format translation
|
||||
- **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible)
|
||||
- **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline
|
||||
- **4-tier fallback**: Subscription → API Key → Cheap → Free
|
||||
@@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
|
||||
|
||||
## v3.8.x Highlights
|
||||
|
||||
- **354-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
|
||||
- **353-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
|
||||
- **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay`
|
||||
- **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown
|
||||
- **MCP server expanded to 110 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules)
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
---
|
||||
|
||||
> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 354 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
|
||||
> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 353 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
|
||||
|
||||
## Overview
|
||||
|
||||
@@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
|
||||
## Key Features (v3.8.50)
|
||||
|
||||
### Core Proxy
|
||||
- **354 AI providers** with automatic format translation
|
||||
- **353 AI providers** with automatic format translation
|
||||
- **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible)
|
||||
- **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline
|
||||
- **4-tier fallback**: Subscription → API Key → Cheap → Free
|
||||
@@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
|
||||
|
||||
## v3.8.x Highlights
|
||||
|
||||
- **354-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
|
||||
- **353-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
|
||||
- **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay`
|
||||
- **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown
|
||||
- **MCP server expanded to 110 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules)
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
---
|
||||
|
||||
> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 354 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
|
||||
> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 353 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
|
||||
|
||||
## Overview
|
||||
|
||||
@@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
|
||||
## Key Features (v3.8.50)
|
||||
|
||||
### Core Proxy
|
||||
- **354 AI providers** with automatic format translation
|
||||
- **353 AI providers** with automatic format translation
|
||||
- **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible)
|
||||
- **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline
|
||||
- **4-tier fallback**: Subscription → API Key → Cheap → Free
|
||||
@@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
|
||||
|
||||
## v3.8.x Highlights
|
||||
|
||||
- **354-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
|
||||
- **353-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
|
||||
- **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay`
|
||||
- **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown
|
||||
- **MCP server expanded to 110 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules)
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
---
|
||||
|
||||
> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 354 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
|
||||
> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 353 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
|
||||
|
||||
## Overview
|
||||
|
||||
@@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
|
||||
## Key Features (v3.8.50)
|
||||
|
||||
### Core Proxy
|
||||
- **354 AI providers** with automatic format translation
|
||||
- **353 AI providers** with automatic format translation
|
||||
- **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible)
|
||||
- **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline
|
||||
- **4-tier fallback**: Subscription → API Key → Cheap → Free
|
||||
@@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
|
||||
|
||||
## v3.8.x Highlights
|
||||
|
||||
- **354-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
|
||||
- **353-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
|
||||
- **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay`
|
||||
- **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown
|
||||
- **MCP server expanded to 110 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules)
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
---
|
||||
|
||||
> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 354 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
|
||||
> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 353 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
|
||||
|
||||
## Overview
|
||||
|
||||
@@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
|
||||
## Key Features (v3.8.50)
|
||||
|
||||
### Core Proxy
|
||||
- **354 AI providers** with automatic format translation
|
||||
- **353 AI providers** with automatic format translation
|
||||
- **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible)
|
||||
- **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline
|
||||
- **4-tier fallback**: Subscription → API Key → Cheap → Free
|
||||
@@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
|
||||
|
||||
## v3.8.x Highlights
|
||||
|
||||
- **354-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
|
||||
- **353-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
|
||||
- **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay`
|
||||
- **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown
|
||||
- **MCP server expanded to 110 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules)
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
---
|
||||
|
||||
> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 354 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
|
||||
> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 353 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
|
||||
|
||||
## Overview
|
||||
|
||||
@@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
|
||||
## Key Features (v3.8.50)
|
||||
|
||||
### Core Proxy
|
||||
- **354 AI providers** with automatic format translation
|
||||
- **353 AI providers** with automatic format translation
|
||||
- **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible)
|
||||
- **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline
|
||||
- **4-tier fallback**: Subscription → API Key → Cheap → Free
|
||||
@@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
|
||||
|
||||
## v3.8.x Highlights
|
||||
|
||||
- **354-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
|
||||
- **353-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
|
||||
- **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay`
|
||||
- **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown
|
||||
- **MCP server expanded to 110 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules)
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
---
|
||||
|
||||
> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 354 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
|
||||
> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 353 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
|
||||
|
||||
## Overview
|
||||
|
||||
@@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
|
||||
## Key Features (v3.8.50)
|
||||
|
||||
### Core Proxy
|
||||
- **354 AI providers** with automatic format translation
|
||||
- **353 AI providers** with automatic format translation
|
||||
- **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible)
|
||||
- **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline
|
||||
- **4-tier fallback**: Subscription → API Key → Cheap → Free
|
||||
@@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
|
||||
|
||||
## v3.8.x Highlights
|
||||
|
||||
- **354-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
|
||||
- **353-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
|
||||
- **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay`
|
||||
- **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown
|
||||
- **MCP server expanded to 110 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules)
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
---
|
||||
|
||||
> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 354 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
|
||||
> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 353 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
|
||||
|
||||
## Overview
|
||||
|
||||
@@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
|
||||
## Key Features (v3.8.50)
|
||||
|
||||
### Core Proxy
|
||||
- **354 AI providers** with automatic format translation
|
||||
- **353 AI providers** with automatic format translation
|
||||
- **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible)
|
||||
- **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline
|
||||
- **4-tier fallback**: Subscription → API Key → Cheap → Free
|
||||
@@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
|
||||
|
||||
## v3.8.x Highlights
|
||||
|
||||
- **354-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
|
||||
- **353-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
|
||||
- **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay`
|
||||
- **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown
|
||||
- **MCP server expanded to 110 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules)
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
---
|
||||
|
||||
> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 354 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
|
||||
> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 353 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
|
||||
|
||||
## Overview
|
||||
|
||||
@@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
|
||||
## Key Features (v3.8.50)
|
||||
|
||||
### Core Proxy
|
||||
- **354 AI providers** with automatic format translation
|
||||
- **353 AI providers** with automatic format translation
|
||||
- **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible)
|
||||
- **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline
|
||||
- **4-tier fallback**: Subscription → API Key → Cheap → Free
|
||||
@@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
|
||||
|
||||
## v3.8.x Highlights
|
||||
|
||||
- **354-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
|
||||
- **353-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
|
||||
- **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay`
|
||||
- **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown
|
||||
- **MCP server expanded to 110 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules)
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
---
|
||||
|
||||
> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 354 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
|
||||
> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 353 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
|
||||
|
||||
## Overview
|
||||
|
||||
@@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
|
||||
## Key Features (v3.8.50)
|
||||
|
||||
### Core Proxy
|
||||
- **354 AI providers** with automatic format translation
|
||||
- **353 AI providers** with automatic format translation
|
||||
- **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible)
|
||||
- **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline
|
||||
- **4-tier fallback**: Subscription → API Key → Cheap → Free
|
||||
@@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
|
||||
|
||||
## v3.8.x Highlights
|
||||
|
||||
- **354-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
|
||||
- **353-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
|
||||
- **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay`
|
||||
- **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown
|
||||
- **MCP server expanded to 110 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules)
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
---
|
||||
|
||||
> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 354 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
|
||||
> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 353 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
|
||||
|
||||
## Overview
|
||||
|
||||
@@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
|
||||
## Key Features (v3.8.50)
|
||||
|
||||
### Core Proxy
|
||||
- **354 AI providers** with automatic format translation
|
||||
- **353 AI providers** with automatic format translation
|
||||
- **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible)
|
||||
- **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline
|
||||
- **4-tier fallback**: Subscription → API Key → Cheap → Free
|
||||
@@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
|
||||
|
||||
## v3.8.x Highlights
|
||||
|
||||
- **354-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
|
||||
- **353-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
|
||||
- **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay`
|
||||
- **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown
|
||||
- **MCP server expanded to 110 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules)
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
---
|
||||
|
||||
> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 354 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
|
||||
> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 353 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
|
||||
|
||||
## Overview
|
||||
|
||||
@@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
|
||||
## Key Features (v3.8.50)
|
||||
|
||||
### Core Proxy
|
||||
- **354 AI providers** with automatic format translation
|
||||
- **353 AI providers** with automatic format translation
|
||||
- **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible)
|
||||
- **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline
|
||||
- **4-tier fallback**: Subscription → API Key → Cheap → Free
|
||||
@@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
|
||||
|
||||
## v3.8.x Highlights
|
||||
|
||||
- **354-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
|
||||
- **353-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
|
||||
- **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay`
|
||||
- **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown
|
||||
- **MCP server expanded to 110 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules)
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
---
|
||||
|
||||
> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 354 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
|
||||
> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 353 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
|
||||
|
||||
## Overview
|
||||
|
||||
@@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
|
||||
## Key Features (v3.8.50)
|
||||
|
||||
### Core Proxy
|
||||
- **354 AI providers** with automatic format translation
|
||||
- **353 AI providers** with automatic format translation
|
||||
- **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible)
|
||||
- **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline
|
||||
- **4-tier fallback**: Subscription → API Key → Cheap → Free
|
||||
@@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
|
||||
|
||||
## v3.8.x Highlights
|
||||
|
||||
- **354-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
|
||||
- **353-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
|
||||
- **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay`
|
||||
- **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown
|
||||
- **MCP server expanded to 110 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules)
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
---
|
||||
|
||||
> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 354 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
|
||||
> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 353 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
|
||||
|
||||
## Overview
|
||||
|
||||
@@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
|
||||
## Key Features (v3.8.50)
|
||||
|
||||
### Core Proxy
|
||||
- **354 AI providers** with automatic format translation
|
||||
- **353 AI providers** with automatic format translation
|
||||
- **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible)
|
||||
- **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline
|
||||
- **4-tier fallback**: Subscription → API Key → Cheap → Free
|
||||
@@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
|
||||
|
||||
## v3.8.x Highlights
|
||||
|
||||
- **354-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
|
||||
- **353-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
|
||||
- **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay`
|
||||
- **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown
|
||||
- **MCP server expanded to 110 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules)
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
---
|
||||
|
||||
> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 354 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
|
||||
> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 353 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
|
||||
|
||||
## Overview
|
||||
|
||||
@@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
|
||||
## Key Features (v3.8.50)
|
||||
|
||||
### Core Proxy
|
||||
- **354 AI providers** with automatic format translation
|
||||
- **353 AI providers** with automatic format translation
|
||||
- **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible)
|
||||
- **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline
|
||||
- **4-tier fallback**: Subscription → API Key → Cheap → Free
|
||||
@@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
|
||||
|
||||
## v3.8.x Highlights
|
||||
|
||||
- **354-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
|
||||
- **353-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
|
||||
- **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay`
|
||||
- **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown
|
||||
- **MCP server expanded to 110 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules)
|
||||
|
||||
@@ -529,7 +529,7 @@ Web/search provider abstraction (Tavily, Brave, Exa, Serper, etc.).
|
||||
## Web Fetch API
|
||||
|
||||
Extract content from a URL via a configured web-fetch provider (Firecrawl, Jina
|
||||
Reader, Tavily Extract, TinyFish Fetch, Nimble Extract).
|
||||
Reader, Tavily Extract, TinyFish Fetch).
|
||||
|
||||
| Method | Path | Description |
|
||||
| ------ | --------------- | --------------------------------------------------------- |
|
||||
@@ -538,8 +538,7 @@ Reader, Tavily Extract, TinyFish Fetch, Nimble Extract).
|
||||
**Auth:** Bearer API key (`extractApiKey` + `isValidApiKey`). Policy enforced via `enforceApiKeyPolicy`.
|
||||
|
||||
**Quota-aware fallback (#8297):** when no explicit `provider` is given, the pool
|
||||
(`firecrawl` → `jina-reader` → `tavily-search` → `tinyfish` → `nimble-search`) is
|
||||
walked in fixed
|
||||
(`firecrawl` → `jina-reader` → `tavily-search` → `tinyfish`) is walked in fixed
|
||||
priority order (fill-first) — a rate-limited-but-configured provider is skipped
|
||||
instead of short-circuiting the request, and a retryable/quota upstream failure
|
||||
(HTTP 429 always; 402/403 for Firecrawl/Tavily/TinyFish quota-style free tiers —
|
||||
@@ -662,21 +661,11 @@ refusal. On success:
|
||||
{
|
||||
"allowed": true,
|
||||
// present only when the key opted into per-key usage limits (daily/weekly USD):
|
||||
"personal": {
|
||||
"dailySpentUsd": 1.25,
|
||||
"dailyLimitUsd": 5,
|
||||
"dailyResetAtIso": "…",
|
||||
"weeklySpentUsd": 8,
|
||||
"weeklyLimitUsd": 20,
|
||||
"weeklyResetAtIso": "…" /* … */,
|
||||
},
|
||||
"personal": { "dailySpentUsd": 1.25, "dailyLimitUsd": 5, "dailyResetAtIso": "…", "weeklySpentUsd": 8, "weeklyLimitUsd": 20, "weeklyResetAtIso": "…" /* … */ },
|
||||
// the selected provider quota snapshot, or null when nothing is cached yet:
|
||||
"provider": { "connectionId": "…", "provider": "claude", "plan": "…", "quotas": {/* … */} },
|
||||
"provider": { "connectionId": "…", "provider": "claude", "plan": "…", "quotas": { /* … */ } },
|
||||
// every connection's snapshot, so a UI can render several providers side by side:
|
||||
"providers": [
|
||||
{ "connectionId": "…", "provider": "claude" /* … */ },
|
||||
{ "provider": "codex" /* … */ },
|
||||
],
|
||||
"providers": [ { "connectionId": "…", "provider": "claude", /* … */ }, { "provider": "codex", /* … */ } ]
|
||||
}
|
||||
```
|
||||
|
||||
@@ -685,7 +674,7 @@ On refusal (`401` bad key / `403` not allowed) the same route returns
|
||||
(key allowed, nothing learned yet) is a different state from a refusal, and only the JSON form
|
||||
distinguishes them.
|
||||
|
||||
**Auth:** the caller's own Bearer API key, validated with `isValidApiKey` — this is _not_ the
|
||||
**Auth:** the caller's own Bearer API key, validated with `isValidApiKey` — this is *not* the
|
||||
management surface (`/api/keys/…`), which stays behind `requireManagementAuth`.
|
||||
|
||||
---
|
||||
|
||||
@@ -512,7 +512,6 @@ detection above).
|
||||
| `OMNIROUTE_MCP_DESCRIPTION_COMPRESSION` | `rtk` | `open-sse/mcp-server/descriptionCompressor.ts` | Compression algorithm/profile. Disable values: `0`, `false`, `off`. |
|
||||
| `OMNIROUTE_MCP_FETCH_TIMEOUT_MS` | `10000` | `open-sse/mcp-server/fetchTimeout.ts` | Abort budget (ms) for MCP-server internal management reads (health, resilience, combos, quota, usage). |
|
||||
| `OMNIROUTE_MCP_UPSTREAM_TIMEOUT_MS` | `60000` | `open-sse/mcp-server/fetchTimeout.ts` | Abort budget (ms) for MCP hops that wait on a provider (`route_request`, `web_search`, `web_fetch`). |
|
||||
| `OMNIROUTE_CORPUS_CACHE_SIZE` | `5` | `src/lib/localCorpus/configured.ts` | Maximum number of local-corpus index instances cached in memory (LRU, one per indexed root directory). Clamped to a minimum of `1`. |
|
||||
| `MODEL_SYNC_INTERVAL_HOURS` | `24` | `src/shared/services/modelSyncScheduler.ts` | Model catalog sync interval in hours. |
|
||||
| `PROVIDER_LIMITS_SYNC_INTERVAL_MINUTES` | `70` | `src/lib/usage/providerLimits.ts` | Provider rate-limit and quota polling interval. |
|
||||
| `PROVIDER_LIMITS_SYNC_SPACING_MS` | `1500` | `src/lib/usage/providerLimits.ts` | Gap (ms) between consecutive OAuth quota fetches in a bulk sync; OAuth connections are fetched one at a time to avoid bursting an upstream. `0` opts out (concurrent). |
|
||||
|
||||
@@ -380,7 +380,7 @@ Use the dashboard at `/dashboard/providers` to enable, configure, and test each
|
||||
| `vllm` | `vllm` | vLLM | Local, self-hosted | [link](https://github.com/vllm-project/vllm) | API key optional. Configure the local vLLM OpenAI-compatible base URL (default: http://localhost:8000/v1). |
|
||||
| `xinference` | `xinference` | XInference | Local, self-hosted | [link](https://inference.readthedocs.io) | API key optional. Configure the local XInference OpenAI-compatible base URL (default: http://localhost:9997/v1). |
|
||||
|
||||
## Search Providers (16)
|
||||
## Search Providers (15)
|
||||
|
||||
| ID | Alias | Name | Tags | Website | Notes |
|
||||
|----|-------|------|------|---------|-------|
|
||||
@@ -390,7 +390,6 @@ Use the dashboard at `/dashboard/providers` to enable, configure, and test each
|
||||
| `firecrawl` | `fc` | Firecrawl | Search | [link](https://firecrawl.dev) | API key from firecrawl.dev/app/api-keys (or set your self-hosted Firecrawl base URL) |
|
||||
| `google-pse-search` | `google-pse` | Google Programmable Search | Search | [link](https://developers.google.com/custom-search/v1/overview) | Requires a Google API key and your Programmable Search Engine ID (cx) |
|
||||
| `linkup-search` | `linkup` | Linkup Search | Search | [link](https://docs.linkup.so) | Bearer API key from the Linkup dashboard |
|
||||
| `nimble-search` | `nimble` | Nimble Search | Search | [link](https://docs.nimbleway.com/nimble-sdk/web-tools/search) | Bearer API key from the Nimble dashboard |
|
||||
| `ollama-search` | `ollama-search` | Ollama Search | Search | [link](https://ollama.com/settings/keys) | Same API key as Ollama Cloud (from ollama.com/settings/keys) |
|
||||
| `perplexity-search` | `pplx-search` | Perplexity Search | Search | [link](https://docs.perplexity.ai/guides/search-quickstart) | Same API key as Perplexity (pplx-...) |
|
||||
| `searchapi-search` | `searchapi` | SearchAPI | Search | [link](https://www.searchapi.io/docs/google) | API key from SearchAPI (query param or Bearer auth) |
|
||||
@@ -443,7 +442,7 @@ Use the dashboard at `/dashboard/providers` to enable, configure, and test each
|
||||
|
||||
- Catalog: [`src/shared/constants/providers.ts`](../../src/shared/constants/providers.ts)
|
||||
- Registry (per-model details): [`open-sse/config/providerRegistry.ts`](../../open-sse/config/providerRegistry.ts)
|
||||
- Executors: [`open-sse/executors/`](../../open-sse/executors/) (110 implementations)
|
||||
- Executors: [`open-sse/executors/`](../../open-sse/executors/) (109 implementations)
|
||||
- Translators: [`open-sse/translator/`](../../open-sse/translator/)
|
||||
|
||||
## See Also
|
||||
|
||||
6
llm.txt
@@ -1,6 +1,6 @@
|
||||
# OmniRoute
|
||||
|
||||
> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 354 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
|
||||
> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 353 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
|
||||
|
||||
## Overview
|
||||
|
||||
@@ -277,7 +277,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
|
||||
## Key Features (v3.8.50)
|
||||
|
||||
### Core Proxy
|
||||
- **354 AI providers** with automatic format translation
|
||||
- **353 AI providers** with automatic format translation
|
||||
- **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible)
|
||||
- **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline
|
||||
- **4-tier fallback**: Subscription → API Key → Cheap → Free
|
||||
@@ -475,7 +475,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
|
||||
|
||||
## v3.8.x Highlights
|
||||
|
||||
- **354-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
|
||||
- **353-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
|
||||
- **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay`
|
||||
- **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown
|
||||
- **MCP server expanded to 110 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules)
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
/**
|
||||
* Nimble shared client constants.
|
||||
*
|
||||
* Nimble asks every integration to identify itself with a stable client-source
|
||||
* header so calls can be attributed to the host product. Both Nimble surfaces
|
||||
* in OmniRoute — search (`POST /v1/search`, wired in open-sse/handlers/search.ts)
|
||||
* and fetch (`POST /v1/extract`, open-sse/executors/nimble-fetch.ts) — send it,
|
||||
* and both read the value from here so the two can never drift apart.
|
||||
*
|
||||
* Docs: https://docs.nimbleway.com/api-reference/introduction
|
||||
*/
|
||||
|
||||
/** Header Nimble uses to attribute a request to the calling product. */
|
||||
export const NIMBLE_CLIENT_SOURCE_HEADER = "X-Client-Source";
|
||||
|
||||
/** The value OmniRoute sends. Do not vary it per surface or per request. */
|
||||
export const NIMBLE_CLIENT_SOURCE = "omniroute";
|
||||
@@ -63,7 +63,6 @@ import { nubeProvider } from "./registry/nube/index.ts";
|
||||
import { clinepassProvider } from "./registry/clinepass/index.ts";
|
||||
import { sparkdeskProvider } from "./registry/sparkdesk/index.ts";
|
||||
import { nlpcloudProvider } from "./registry/nlpcloud/index.ts";
|
||||
import { oneminaiProvider } from "./registry/oneminai/index.ts";
|
||||
import { nvidiaProvider } from "./registry/nvidia/index.ts";
|
||||
import { api_airforceProvider } from "./registry/api-airforce/index.ts";
|
||||
import { mistralProvider } from "./registry/mistral/index.ts";
|
||||
@@ -333,7 +332,6 @@ export const REGISTRY: Record<string, RegistryEntry> = {
|
||||
clinepass: clinepassProvider,
|
||||
sparkdesk: sparkdeskProvider,
|
||||
nlpcloud: nlpcloudProvider,
|
||||
oneminai: oneminaiProvider,
|
||||
nvidia: nvidiaProvider,
|
||||
"api-airforce": api_airforceProvider,
|
||||
mistral: mistralProvider,
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
import type { RegistryEntry } from "../../shared.ts";
|
||||
|
||||
// 1min.ai (docs.1min.ai) — a chat aggregator exposing many upstream models
|
||||
// through one custom API. Not OpenAI-compatible at the wire level (single
|
||||
// `promptObject.prompt` string instead of a `messages` array, real SSE with
|
||||
// event:/data: framing instead of raw text deltas, "API-KEY" auth header
|
||||
// instead of Authorization: Bearer) — see open-sse/executors/oneminai.ts for
|
||||
// the request/response translation. `format: "openai"` here describes the
|
||||
// client-facing surface OmniRoute exposes, not 1min.ai's actual wire format.
|
||||
export const oneminaiProvider: RegistryEntry = {
|
||||
id: "oneminai",
|
||||
alias: "1min",
|
||||
format: "openai",
|
||||
executor: "default",
|
||||
baseUrl: "https://api.1min.ai/api/chat-with-ai",
|
||||
authType: "apikey",
|
||||
authHeader: "api-key",
|
||||
// The model catalog is loaded dynamically per-account/plan on 1min.ai's own
|
||||
// dashboard rather than published as a stable public list, so only the
|
||||
// model shown in every one of 1min.ai's own docs examples is statically
|
||||
// catalogued; passthroughModels lets any other slug the account has access
|
||||
// to be used by id.
|
||||
passthroughModels: true,
|
||||
liveCatalogAuthoritative: false,
|
||||
// No tool/function-calling, JSON mode, or vision support is wired up by the
|
||||
// executor's translation (1min.ai's attachments.images/files feature would
|
||||
// need separate Asset API upload plumbing this provider doesn't implement).
|
||||
unsupportedParams: ["tools", "tool_choice", "functions", "function_call", "response_format"],
|
||||
models: [{ id: "gpt-4o-mini", name: "GPT-4o Mini" }],
|
||||
};
|
||||
@@ -116,27 +116,6 @@ export const SEARCH_PROVIDERS: Record<string, SearchProviderConfig> = {
|
||||
cacheTTLMs: 5 * 60 * 1000,
|
||||
},
|
||||
|
||||
// Nimble also serves POST /v1/web/fetch through the same credential — see
|
||||
// open-sse/executors/nimble-fetch.ts.
|
||||
"nimble-search": {
|
||||
id: "nimble-search",
|
||||
name: "Nimble Search",
|
||||
baseUrl: "https://sdk.nimbleway.com/v1/search",
|
||||
method: "POST",
|
||||
authType: "apikey",
|
||||
authHeader: "bearer",
|
||||
costPerQuery: 0.005,
|
||||
freeMonthlyQuota: 0,
|
||||
searchTypes: ["web", "news"],
|
||||
defaultMaxResults: 5,
|
||||
maxMaxResults: 100,
|
||||
// Kept below GLOBAL_TIMEOUT_MS (handlers/search.ts) so a stalled Nimble call
|
||||
// still leaves budget for the failover provider instead of burning the whole
|
||||
// request window.
|
||||
timeoutMs: 10_000,
|
||||
cacheTTLMs: 5 * 60 * 1000,
|
||||
},
|
||||
|
||||
firecrawl: {
|
||||
id: "firecrawl",
|
||||
name: "Firecrawl",
|
||||
|
||||
@@ -60,8 +60,6 @@ const lazyExecutors: Record<string, () => Promise<BaseExecutor>> = {
|
||||
gitlab: () => import("./gitlab.ts").then((m) => new m.GitlabExecutor()),
|
||||
"gitlab-duo": () => import("./gitlab.ts").then((m) => new m.GitlabExecutor("gitlab-duo")),
|
||||
nlpcloud: () => import("./nlpcloud.ts").then((m) => new m.NlpCloudExecutor()),
|
||||
oneminai: () => import("./oneminai.ts").then((m) => new m.OneMinAiExecutor()),
|
||||
"1min": () => import("./oneminai.ts").then((m) => new m.OneMinAiExecutor()), // Alias
|
||||
pollinations: () => import("./pollinations.ts").then((m) => new m.PollinationsExecutor()),
|
||||
pol: () => import("./pollinations.ts").then((m) => new m.PollinationsExecutor()), // Alias
|
||||
"cloudflare-ai": () => import("./cloudflare-ai.ts").then((m) => new m.CloudflareAIExecutor()),
|
||||
|
||||
@@ -243,14 +243,6 @@ export function resolveKiroRegion(
|
||||
// kiroRuntimeHost from this executor keep working.
|
||||
export { kiroRuntimeHost };
|
||||
|
||||
/**
|
||||
* Status codes for which trying the next candidate endpoint may succeed where the
|
||||
* current one failed (auth/profile mismatch, not a payload problem). Mirrors
|
||||
* 9router's KIRO_ENDPOINT_FALLBACK_STATUSES — a 400 (malformed body) is deliberately
|
||||
* excluded since resending the same body to another host cannot fix it.
|
||||
*/
|
||||
const KIRO_ENDPOINT_FALLBACK_STATUSES = new Set([401, 403, 404]);
|
||||
|
||||
/**
|
||||
* KiroExecutor - Executor for Kiro AI (AWS CodeWhisperer)
|
||||
* Uses AWS CodeWhisperer streaming API with AWS EventStream binary format
|
||||
@@ -342,47 +334,17 @@ export class KiroExecutor extends BaseExecutor {
|
||||
// Center accounts (e.g. eu-central-1) are rejected by the default us-east-1 host; only the
|
||||
// regional endpoint accepts the region-bound token + profileArn.
|
||||
const region = resolveKiroRegion(credentials);
|
||||
const regionalUrl = `${kiroRuntimeHost(region)}/generateAssistantResponse`;
|
||||
|
||||
// The Kiro IDE's own branded gateway (runtime.*.kiro.dev) only exists for
|
||||
// us-east-1 and only accepts Kiro OIDC/social tokens — it rejects
|
||||
// TokenType=API_KEY and external-IdP/IdC SSO tokens outright (403 "bearer
|
||||
// token invalid"), so those auth methods go straight to the region-resolved
|
||||
// CodeWhisperer/Amazon Q surface (mirrors 9router's getOrderedBaseUrls in
|
||||
// open-sse/executors/kiro.js). For everything else, try the branded gateway
|
||||
// first — it is the surface the native Kiro IDE itself talks to — and fall
|
||||
// back to the raw AWS host on an auth/profile-shaped failure.
|
||||
const authMethod =
|
||||
typeof credentials.providerSpecificData?.authMethod === "string"
|
||||
? credentials.providerSpecificData.authMethod
|
||||
: undefined;
|
||||
const isCodeWhispererOnly =
|
||||
authMethod === "api_key" || authMethod === "idc" || isExternalIdpAuthMethod(authMethod);
|
||||
const candidateUrls =
|
||||
region === "us-east-1" && !isCodeWhispererOnly
|
||||
? ["https://runtime.us-east-1.kiro.dev/generateAssistantResponse", regionalUrl]
|
||||
: [regionalUrl];
|
||||
|
||||
const url = `${kiroRuntimeHost(region)}/generateAssistantResponse`;
|
||||
const headers = this.buildHeaders(credentials, stream);
|
||||
mergeUpstreamExtraHeaders(headers, upstreamExtraHeaders);
|
||||
const transformedBody = await this.transformRequest(model, body, stream, credentials);
|
||||
const requestBody = JSON.stringify(transformedBody);
|
||||
|
||||
let response!: Response;
|
||||
let url = candidateUrls[0];
|
||||
for (let i = 0; i < candidateUrls.length; i++) {
|
||||
url = candidateUrls[i];
|
||||
response = await fetch(url, {
|
||||
method: "POST",
|
||||
headers,
|
||||
body: requestBody,
|
||||
signal,
|
||||
});
|
||||
const hasFallback = i + 1 < candidateUrls.length;
|
||||
if (response.ok || !hasFallback || !KIRO_ENDPOINT_FALLBACK_STATUSES.has(response.status)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
const response = await fetch(url, {
|
||||
method: "POST",
|
||||
headers,
|
||||
body: JSON.stringify(transformedBody),
|
||||
signal,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
return { response, url, headers, transformedBody };
|
||||
|
||||
@@ -1,222 +0,0 @@
|
||||
/**
|
||||
* Nimble Web Fetch Executor
|
||||
*
|
||||
* Fetches content from a URL using the Nimble Extract API.
|
||||
* POST https://sdk.nimbleway.com/v1/extract
|
||||
*
|
||||
* Extract returns the requested formats side by side under `data`, so one call
|
||||
* covers every OmniRoute fetch format: `markdown`, `html`, `links` and
|
||||
* `screenshot` (base64 PNG, surfaced as a data URL).
|
||||
*
|
||||
* Docs: https://docs.nimbleway.com/nimble-sdk/web-tools/extract/quickstart
|
||||
*/
|
||||
|
||||
import { sanitizeErrorMessage, buildErrorBody } from "../utils/error.ts";
|
||||
import { NIMBLE_CLIENT_SOURCE, NIMBLE_CLIENT_SOURCE_HEADER } from "../config/nimble.ts";
|
||||
import type { WebFetchResult, WebFetchFormat, WebFetchCredentials } from "../handlers/webFetch.ts";
|
||||
|
||||
const NIMBLE_EXTRACT_URL = "https://sdk.nimbleway.com/v1/extract";
|
||||
const NIMBLE_TIMEOUT_MS = 30_000;
|
||||
|
||||
/** Max characters kept from an HTML <title> / meta description. */
|
||||
const META_MAX_CHARS = 500;
|
||||
|
||||
// These run over untrusted upstream HTML, so every quantifier is over a character
|
||||
// class that cannot cross its own terminator — no backtracking (see AGENTS.md →
|
||||
// "Regex Security"). Length is capped by META_MAX_CHARS after the match, not by the
|
||||
// quantifier: bounding the capture instead would make an over-long title fail to
|
||||
// match at all rather than truncate.
|
||||
const TITLE_RE = /<title[^>]{0,200}>([^<]*)<\/title>/i;
|
||||
// One pattern per quote style. A shared ["'] class for the closing delimiter would
|
||||
// cut a double-quoted description at its first apostrophe ("Don't miss…" → "Don").
|
||||
const META_DESCRIPTION_DOUBLE_RE =
|
||||
/<meta[^>]{0,200}name=["']description["'][^>]{0,200}content="([^"]*)"/i;
|
||||
const META_DESCRIPTION_SINGLE_RE =
|
||||
/<meta[^>]{0,200}name=["']description["'][^>]{0,200}content='([^']*)'/i;
|
||||
|
||||
/** Map an OmniRoute fetch format onto the Nimble Extract format name. */
|
||||
function mapFormat(format: WebFetchFormat): string {
|
||||
switch (format) {
|
||||
case "html":
|
||||
return "html";
|
||||
case "links":
|
||||
return "links";
|
||||
case "screenshot":
|
||||
return "screenshot";
|
||||
case "markdown":
|
||||
default:
|
||||
return "markdown";
|
||||
}
|
||||
}
|
||||
|
||||
function readString(source: Record<string, unknown>, key: string): string {
|
||||
const value = source[key];
|
||||
return value != null ? String(value) : "";
|
||||
}
|
||||
|
||||
/**
|
||||
* Pull a title / description out of raw HTML.
|
||||
* Extract has no dedicated metadata format, so metadata is only available when
|
||||
* the caller asked for it and we requested `html` alongside their format.
|
||||
*/
|
||||
function parseHtmlMetadata(html: string): { title: string | null; description: string | null } {
|
||||
if (!html) return { title: null, description: null };
|
||||
|
||||
const title = TITLE_RE.exec(html)?.[1]?.trim().slice(0, META_MAX_CHARS);
|
||||
const rawDescription =
|
||||
META_DESCRIPTION_DOUBLE_RE.exec(html)?.[1] ?? META_DESCRIPTION_SINGLE_RE.exec(html)?.[1];
|
||||
const description = rawDescription?.trim().slice(0, META_MAX_CHARS);
|
||||
|
||||
return {
|
||||
title: title ? title : null,
|
||||
description: description ? description : null,
|
||||
};
|
||||
}
|
||||
|
||||
interface NimbleFetchOptions {
|
||||
url: string;
|
||||
format: WebFetchFormat;
|
||||
includeMetadata: boolean;
|
||||
credentials: WebFetchCredentials;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a Nimble Extract request.
|
||||
*/
|
||||
export async function nimbleFetch(opts: NimbleFetchOptions): Promise<WebFetchResult> {
|
||||
const { url, format, includeMetadata, credentials } = opts;
|
||||
|
||||
if (!credentials.apiKey) {
|
||||
const body = buildErrorBody(401, "Nimble API key required");
|
||||
return { success: false, status: 401, error: body.error.message };
|
||||
}
|
||||
|
||||
const requested = mapFormat(format);
|
||||
|
||||
// `links` always comes back so WebFetchResponse.links is populated; `html` is
|
||||
// added only when the caller asked for metadata, since it is the sole source
|
||||
// of a page title/description.
|
||||
const formats = [...new Set([requested, "links", ...(includeMetadata ? ["html"] : [])])];
|
||||
|
||||
const requestBody: Record<string, unknown> = {
|
||||
url,
|
||||
formats,
|
||||
// Extract types `render` as `boolean | "auto"`; "auto" lets Nimble select the
|
||||
// driver per target domain rather than forcing a browser on every static page.
|
||||
render: "auto",
|
||||
};
|
||||
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => {
|
||||
const err = new Error(`nimble-fetch timeout after ${NIMBLE_TIMEOUT_MS}ms`);
|
||||
err.name = "TimeoutError";
|
||||
controller.abort(err);
|
||||
}, NIMBLE_TIMEOUT_MS);
|
||||
|
||||
try {
|
||||
const response = await fetch(NIMBLE_EXTRACT_URL, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${credentials.apiKey}`,
|
||||
[NIMBLE_CLIENT_SOURCE_HEADER]: NIMBLE_CLIENT_SOURCE,
|
||||
},
|
||||
body: JSON.stringify(requestBody),
|
||||
signal: controller.signal,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
// Nimble returns a plain-text body on auth failures, so this must be
|
||||
// sanitized before it can reach a response.
|
||||
const rawError = await response.text().catch(() => `HTTP ${response.status}`);
|
||||
const msg = sanitizeErrorMessage(`Nimble error ${response.status}: ${rawError}`);
|
||||
const body = buildErrorBody(response.status, msg);
|
||||
return { success: false, status: response.status, error: body.error.message };
|
||||
}
|
||||
|
||||
const payload = (await response.json()) as Record<string, unknown>;
|
||||
|
||||
// A 200 can still carry a failed extraction — Extract reports the target's own
|
||||
// outcome in the envelope. Without this the caller would get an empty document
|
||||
// marked successful, and the pool in /v1/web/fetch would never fall through to
|
||||
// the next provider.
|
||||
const taskStatus = typeof payload.status === "string" ? payload.status : "";
|
||||
const taskStatusCode = typeof payload.status_code === "number" ? payload.status_code : null;
|
||||
if (
|
||||
(taskStatus && taskStatus !== "success") ||
|
||||
(taskStatusCode !== null && taskStatusCode >= 400)
|
||||
) {
|
||||
const detail = taskStatus || `status ${taskStatusCode}`;
|
||||
const msg = sanitizeErrorMessage(`Nimble extraction did not succeed: ${detail}`);
|
||||
const body = buildErrorBody(502, msg);
|
||||
return { success: false, status: 502, error: body.error.message };
|
||||
}
|
||||
|
||||
const data = (payload.data as Record<string, unknown> | null) ?? {};
|
||||
|
||||
const rawLinks = data.links;
|
||||
const links: string[] = Array.isArray(rawLinks) ? rawLinks.map((l) => String(l)) : [];
|
||||
|
||||
const screenshot = readString(data, "screenshot");
|
||||
const screenshotUrl =
|
||||
format === "screenshot" && screenshot
|
||||
? screenshot.startsWith("data:")
|
||||
? screenshot
|
||||
: `data:image/png;base64,${screenshot}`
|
||||
: null;
|
||||
|
||||
// The requested format must actually be present. An absent key means Extract
|
||||
// succeeded but produced nothing for what the caller asked for; returning an
|
||||
// empty document as a success would stop /v1/web/fetch from trying the next
|
||||
// provider. An empty *value* is legitimate (a genuinely blank page) and passes.
|
||||
if (!(requested in data)) {
|
||||
const msg = sanitizeErrorMessage(`Nimble returned no ${requested} content for the request`);
|
||||
const body = buildErrorBody(502, msg);
|
||||
return { success: false, status: 502, error: body.error.message };
|
||||
}
|
||||
|
||||
let content: string;
|
||||
switch (format) {
|
||||
case "html":
|
||||
content = readString(data, "html");
|
||||
break;
|
||||
case "links":
|
||||
content = JSON.stringify(links);
|
||||
break;
|
||||
case "screenshot":
|
||||
content = "";
|
||||
break;
|
||||
case "markdown":
|
||||
default:
|
||||
content = readString(data, "markdown");
|
||||
break;
|
||||
}
|
||||
|
||||
const metadata = includeMetadata ? parseHtmlMetadata(readString(data, "html")) : null;
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
provider: "nimble-search",
|
||||
url,
|
||||
content,
|
||||
links,
|
||||
metadata,
|
||||
screenshot_url: screenshotUrl,
|
||||
},
|
||||
};
|
||||
} catch (err: unknown) {
|
||||
// The abort reason above is named "TimeoutError"; an external abort surfaces as
|
||||
// "AbortError". Both must reach the 504 branch.
|
||||
if (err instanceof Error && (err.name === "TimeoutError" || err.name === "AbortError")) {
|
||||
const body = buildErrorBody(504, "Nimble request timed out");
|
||||
return { success: false, status: 504, error: body.error.message };
|
||||
}
|
||||
const msg =
|
||||
err instanceof Error ? sanitizeErrorMessage(err.message) : sanitizeErrorMessage(String(err));
|
||||
const body = buildErrorBody(502, msg);
|
||||
return { success: false, status: 502, error: body.error.message };
|
||||
} finally {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
}
|
||||
@@ -1,313 +0,0 @@
|
||||
import { randomUUID } from "node:crypto";
|
||||
|
||||
import {
|
||||
BaseExecutor,
|
||||
mergeUpstreamExtraHeaders,
|
||||
type ExecuteInput,
|
||||
type ProviderCredentials,
|
||||
} from "./base.ts";
|
||||
import { PROVIDERS } from "../config/constants.ts";
|
||||
import { buildErrorBody } from "../utils/error.ts";
|
||||
|
||||
type JsonRecord = Record<string, unknown>;
|
||||
type OpenAIMessage = {
|
||||
role?: string;
|
||||
content?: unknown;
|
||||
};
|
||||
|
||||
const CHAT_URL = "https://api.1min.ai/api/chat-with-ai";
|
||||
const ROLE_LABELS: Record<string, string> = {
|
||||
system: "System",
|
||||
developer: "System",
|
||||
user: "User",
|
||||
assistant: "Assistant",
|
||||
};
|
||||
|
||||
function asRecord(value: unknown): JsonRecord {
|
||||
return value && typeof value === "object" && !Array.isArray(value) ? (value as JsonRecord) : {};
|
||||
}
|
||||
|
||||
function extractTextContent(content: unknown): string {
|
||||
if (typeof content === "string") return content;
|
||||
if (!Array.isArray(content)) return "";
|
||||
return content
|
||||
.map((part) => {
|
||||
if (!part || typeof part !== "object") return "";
|
||||
const item = part as Record<string, unknown>;
|
||||
return item.type === "text" && typeof item.text === "string" ? item.text : "";
|
||||
})
|
||||
.filter((text) => text.length > 0)
|
||||
.join("\n");
|
||||
}
|
||||
|
||||
/**
|
||||
* 1min.ai's Chat with AI API takes one `promptObject.prompt` string, not an
|
||||
* OpenAI `messages` array — multi-turn context is normally carried server-side
|
||||
* via `promptObject.conversationId` (see docs.1min.ai/docs/api/chat-with-ai-api),
|
||||
* which requires a prior POST /api/conversations call and a stable conversation
|
||||
* identity that stateless OpenAI-compatible clients don't provide. Rather than
|
||||
* half-implement that, a single user message passes through unchanged and
|
||||
* multi-turn history is flattened into a labeled transcript.
|
||||
*/
|
||||
export function buildPrompt(messages: OpenAIMessage[] | undefined): string {
|
||||
const list = Array.isArray(messages) ? messages : [];
|
||||
if (list.length === 1 && list[0]?.role === "user") {
|
||||
return extractTextContent(list[0].content);
|
||||
}
|
||||
return list
|
||||
.map((message) => {
|
||||
const role = String(message?.role || "user").toLowerCase();
|
||||
const text = extractTextContent(message?.content);
|
||||
const label = ROLE_LABELS[role] || role;
|
||||
return `${label}: ${text}`;
|
||||
})
|
||||
.filter((line) => line.length > 0)
|
||||
.join("\n\n");
|
||||
}
|
||||
|
||||
function buildSseChunk(data: unknown): string {
|
||||
return `data: ${JSON.stringify(data)}\n\n`;
|
||||
}
|
||||
|
||||
function buildOpenAiJsonCompletion(content: string, model: string, id: string, created: number): Response {
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
id,
|
||||
object: "chat.completion",
|
||||
created,
|
||||
model,
|
||||
choices: [{ index: 0, message: { role: "assistant", content }, finish_reason: "stop" }],
|
||||
// 1min.ai's response shape carries no token-usage fields.
|
||||
usage: { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 },
|
||||
}),
|
||||
{ status: 200, headers: { "Content-Type": "application/json" } }
|
||||
);
|
||||
}
|
||||
|
||||
function toOpenAiErrorResponse(status: number, message: string, upstreamDetails?: unknown): Response {
|
||||
return new Response(JSON.stringify(buildErrorBody(status, message, upstreamDetails)), {
|
||||
status,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse 1min.ai's real Server-Sent Events (event: content|result|done|error,
|
||||
* data: {...}) from the upstream Response body and re-emit them as standard
|
||||
* OpenAI chat.completion.chunk SSE.
|
||||
*/
|
||||
function translateSseStream(upstreamBody: ReadableStream<Uint8Array>, model: string, id: string, created: number): ReadableStream<Uint8Array> {
|
||||
const decoder = new TextDecoder();
|
||||
const encoder = new TextEncoder();
|
||||
|
||||
return new ReadableStream<Uint8Array>({
|
||||
async start(controller) {
|
||||
controller.enqueue(
|
||||
encoder.encode(
|
||||
buildSseChunk({
|
||||
id,
|
||||
object: "chat.completion.chunk",
|
||||
created,
|
||||
model,
|
||||
choices: [{ index: 0, delta: { role: "assistant" }, finish_reason: null }],
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
const reader = upstreamBody.getReader();
|
||||
let buffer = "";
|
||||
let finished = false;
|
||||
|
||||
const finish = () => {
|
||||
if (finished) return;
|
||||
finished = true;
|
||||
controller.enqueue(
|
||||
encoder.encode(
|
||||
buildSseChunk({
|
||||
id,
|
||||
object: "chat.completion.chunk",
|
||||
created,
|
||||
model,
|
||||
choices: [{ index: 0, delta: {}, finish_reason: "stop" }],
|
||||
})
|
||||
)
|
||||
);
|
||||
controller.enqueue(encoder.encode("data: [DONE]\n\n"));
|
||||
controller.close();
|
||||
};
|
||||
|
||||
const emitContent = (text: string) => {
|
||||
if (!text) return;
|
||||
controller.enqueue(
|
||||
encoder.encode(
|
||||
buildSseChunk({
|
||||
id,
|
||||
object: "chat.completion.chunk",
|
||||
created,
|
||||
model,
|
||||
choices: [{ index: 0, delta: { content: text }, finish_reason: null }],
|
||||
})
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
// SSE event framing: "event:"/"data:" lines, blank-line separated records.
|
||||
const processEvent = (eventText: string) => {
|
||||
let eventType = "message";
|
||||
const dataLines: string[] = [];
|
||||
for (const rawLine of eventText.split("\n")) {
|
||||
if (rawLine.startsWith("event:")) {
|
||||
eventType = rawLine.slice(6).trim();
|
||||
} else if (rawLine.startsWith("data:")) {
|
||||
dataLines.push(rawLine.slice(5).trim());
|
||||
}
|
||||
}
|
||||
const data = dataLines.join("\n");
|
||||
if (eventType === "content") {
|
||||
try {
|
||||
const parsed = asRecord(JSON.parse(data));
|
||||
if (typeof parsed.content === "string") emitContent(parsed.content);
|
||||
} catch {
|
||||
// Ignore malformed content events rather than surfacing partial JSON.
|
||||
}
|
||||
} else if (eventType === "error") {
|
||||
emitContent(`\n[1min.ai error: ${data}]`);
|
||||
finish();
|
||||
} else if (eventType === "done") {
|
||||
finish();
|
||||
}
|
||||
// "result" carries the final full aiRecord, redundant with the content
|
||||
// events already streamed — intentionally ignored.
|
||||
};
|
||||
|
||||
try {
|
||||
while (!finished) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
buffer += decoder.decode(value, { stream: true });
|
||||
let separatorIndex = buffer.indexOf("\n\n");
|
||||
while (separatorIndex !== -1) {
|
||||
processEvent(buffer.slice(0, separatorIndex));
|
||||
buffer = buffer.slice(separatorIndex + 2);
|
||||
separatorIndex = buffer.indexOf("\n\n");
|
||||
}
|
||||
}
|
||||
if (!finished && buffer.trim()) processEvent(buffer);
|
||||
finish();
|
||||
} catch (error) {
|
||||
controller.error(error);
|
||||
} finally {
|
||||
reader.releaseLock();
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export class OneMinAiExecutor extends BaseExecutor {
|
||||
constructor() {
|
||||
super("oneminai", PROVIDERS["oneminai"] || { format: "openai", baseUrl: CHAT_URL });
|
||||
}
|
||||
|
||||
buildUrl(_model: string, stream: boolean): string {
|
||||
return stream ? `${CHAT_URL}?isStreaming=true` : CHAT_URL;
|
||||
}
|
||||
|
||||
buildHeaders(credentials: ProviderCredentials | null): Record<string, string> {
|
||||
const key = credentials?.apiKey || credentials?.accessToken || "";
|
||||
return {
|
||||
"Content-Type": "application/json",
|
||||
"API-KEY": key,
|
||||
};
|
||||
}
|
||||
|
||||
transformRequest(model: string, body: unknown): JsonRecord {
|
||||
const payload = asRecord(body);
|
||||
const messages = Array.isArray(payload.messages) ? (payload.messages as OpenAIMessage[]) : [];
|
||||
return {
|
||||
type: "UNIFY_CHAT_WITH_AI",
|
||||
model,
|
||||
promptObject: { prompt: buildPrompt(messages) },
|
||||
};
|
||||
}
|
||||
|
||||
async execute({ model, body, stream, credentials, signal, upstreamExtraHeaders }: ExecuteInput) {
|
||||
const url = this.buildUrl(model, stream);
|
||||
const headers = this.buildHeaders(credentials);
|
||||
mergeUpstreamExtraHeaders(headers, upstreamExtraHeaders);
|
||||
const payload = this.transformRequest(model, body);
|
||||
|
||||
const id = `chatcmpl-oneminai-${randomUUID()}`;
|
||||
const created = Math.floor(Date.now() / 1000);
|
||||
|
||||
try {
|
||||
this.assertOutboundUrlAllowed(url);
|
||||
const response = await fetch(url, {
|
||||
method: "POST",
|
||||
headers,
|
||||
body: JSON.stringify(payload),
|
||||
signal,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
let message = `1min.ai API failed with status ${response.status}`;
|
||||
try {
|
||||
const parsed = asRecord(JSON.parse(errorText));
|
||||
const err = asRecord(parsed.error);
|
||||
if (typeof err.message === "string") message = err.message;
|
||||
} catch {
|
||||
if (errorText) message = errorText;
|
||||
}
|
||||
return {
|
||||
response: toOpenAiErrorResponse(response.status, message),
|
||||
url,
|
||||
headers,
|
||||
transformedBody: payload,
|
||||
};
|
||||
}
|
||||
|
||||
if (stream) {
|
||||
if (!response.body) {
|
||||
return {
|
||||
response: toOpenAiErrorResponse(502, "1min.ai returned an empty stream"),
|
||||
url,
|
||||
headers,
|
||||
transformedBody: payload,
|
||||
};
|
||||
}
|
||||
return {
|
||||
response: new Response(translateSseStream(response.body, model, id, created), {
|
||||
status: 200,
|
||||
headers: { "Content-Type": "text/event-stream" },
|
||||
}),
|
||||
url,
|
||||
headers,
|
||||
transformedBody: payload,
|
||||
};
|
||||
}
|
||||
|
||||
const json = asRecord(await response.json());
|
||||
const aiRecord = asRecord(json.aiRecord);
|
||||
const detail = asRecord(aiRecord.aiRecordDetail);
|
||||
const resultObject = Array.isArray(detail.resultObject) ? detail.resultObject : [];
|
||||
const content = resultObject.filter((part): part is string => typeof part === "string").join("");
|
||||
|
||||
return {
|
||||
response: buildOpenAiJsonCompletion(content, model, id, created),
|
||||
url,
|
||||
headers,
|
||||
transformedBody: payload,
|
||||
};
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error || "Unknown error");
|
||||
return {
|
||||
response: toOpenAiErrorResponse(502, `1min.ai fetch error: ${message}`),
|
||||
url,
|
||||
headers,
|
||||
transformedBody: payload,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default OneMinAiExecutor;
|
||||
@@ -90,19 +90,13 @@ async function resolveZaiBrowserAttachments(
|
||||
> {
|
||||
try {
|
||||
// Browser-page upload: keep the original bytes/mimeType (no Cursor wire prep).
|
||||
// EncodedImage.mimeType is optional on the wire type, but every producer
|
||||
// reachable here (decodeDataUrl / fetchImageBytes) validates an image/*
|
||||
// string before pushing; the fallback only satisfies the attachment type.
|
||||
const images = await resolveCursorImages(imageUrls, { prepareForWire: false });
|
||||
return {
|
||||
attachments: images.map((image, index) => {
|
||||
const mimeType = image.mimeType ?? "image/jpeg";
|
||||
return {
|
||||
name: zaiImageFileName(mimeType, index),
|
||||
mimeType,
|
||||
buffer: image.data,
|
||||
};
|
||||
}),
|
||||
attachments: images.map((image, index) => ({
|
||||
name: zaiImageFileName(image.mimeType, index),
|
||||
mimeType: image.mimeType,
|
||||
buffer: image.data,
|
||||
})),
|
||||
};
|
||||
} catch (error) {
|
||||
const message =
|
||||
|
||||
@@ -704,9 +704,8 @@ export async function handleChatCore({
|
||||
const recordKeyHealthStatus = (
|
||||
status: number,
|
||||
creds: Record<string, unknown> | null | undefined,
|
||||
transport?: string,
|
||||
failureDetail?: string
|
||||
): void => recordKeyHealthStatusFor(status, creds, log, transport, failureDetail);
|
||||
transport?: string
|
||||
): void => recordKeyHealthStatusFor(status, creds, log, transport);
|
||||
// ── Phase 9.2: Idempotency check ──
|
||||
// Resolve the idempotency key once here and reuse it at the Phase 9.2 save site below,
|
||||
// rather than re-deriving it. (#3821-review LEDGER-6)
|
||||
@@ -3175,20 +3174,11 @@ export async function handleChatCore({
|
||||
});
|
||||
|
||||
if (
|
||||
stream &&
|
||||
(res.response.ok ||
|
||||
res.response.status === HTTP_STATUS.UNAUTHORIZED ||
|
||||
res.response.status === HTTP_STATUS.FORBIDDEN) &&
|
||||
res.response.status === 401 &&
|
||||
executionConnectionId &&
|
||||
!(await shouldIsolateProbeFailures())
|
||||
) {
|
||||
const failureDetail = res.response.ok
|
||||
? ""
|
||||
: await res.response
|
||||
.clone()
|
||||
.text()
|
||||
.catch(() => "");
|
||||
recordKeyHealthStatus(res.response.status, execCreds, res.transport, failureDetail);
|
||||
recordKeyHealthStatus(401, execCreds);
|
||||
}
|
||||
|
||||
if (isModelScope() && res.response.status === 429 && attempts < maxAttempts - 1) {
|
||||
@@ -3539,6 +3529,13 @@ export async function handleChatCore({
|
||||
// Non-stream: release semaphore immediately after reading full response body.
|
||||
const status = rawResult.response.status;
|
||||
|
||||
// Use execution credentials captured during request processing
|
||||
if (
|
||||
rawResult._executionCredentials?.connectionId &&
|
||||
rawResult._executionCredentials?.apiKey
|
||||
) {
|
||||
recordKeyHealthStatus(status, rawResult._executionCredentials, rawResult.transport);
|
||||
}
|
||||
releaseRawResultAccountSemaphore =
|
||||
typeof rawResult._accountSemaphoreRelease === "function"
|
||||
? rawResult._accountSemaphoreRelease
|
||||
@@ -3564,19 +3561,6 @@ export async function handleChatCore({
|
||||
contentType,
|
||||
upstreamStream
|
||||
);
|
||||
// Use the exact execution credential selected for this request. Model capability
|
||||
// failures stay in routing telemetry; authoritative success only recovers this key.
|
||||
if (
|
||||
rawResult._executionCredentials?.connectionId &&
|
||||
(rawResult._executionCredentials.apiKey || rawResult._executionCredentials.accessToken)
|
||||
) {
|
||||
recordKeyHealthStatus(
|
||||
status,
|
||||
rawResult._executionCredentials,
|
||||
rawResult.transport,
|
||||
status >= 400 ? payload : ""
|
||||
);
|
||||
}
|
||||
releaseRawResultAccountSemaphore();
|
||||
releaseRawResultAccountSemaphore = () => {};
|
||||
|
||||
@@ -4257,84 +4241,79 @@ export async function handleChatCore({
|
||||
`[provider] Node ${errorConnectionId} probe ${errorType} (${statusCode}) — connection stays active`
|
||||
);
|
||||
} else {
|
||||
// Kimi's 403 says "billing cycle" for both an exhausted subscription and a
|
||||
// temporary request window. Read its official usage endpoint before making
|
||||
// the connection terminal: a non-zero Weekly quota plus an empty Ratelimit
|
||||
// window must recover automatically at the reported reset time.
|
||||
let kimiRateLimitResetAt: string | null = null;
|
||||
if (provider === "kimi-coding") {
|
||||
try {
|
||||
const { fetchAndPersistProviderLimits } =
|
||||
await import("@/lib/usage/providerLimits");
|
||||
const { usage } = await fetchAndPersistProviderLimits(
|
||||
errorConnectionId,
|
||||
"manual"
|
||||
);
|
||||
kimiRateLimitResetAt = getKimiTemporaryRateLimitResetAt(usage);
|
||||
} catch {
|
||||
// Preserve the existing quota handling when Kimi's usage endpoint is unavailable.
|
||||
}
|
||||
// Kimi's 403 says "billing cycle" for both an exhausted subscription and a
|
||||
// temporary request window. Read its official usage endpoint before making
|
||||
// the connection terminal: a non-zero Weekly quota plus an empty Ratelimit
|
||||
// window must recover automatically at the reported reset time.
|
||||
let kimiRateLimitResetAt: string | null = null;
|
||||
if (provider === "kimi-coding") {
|
||||
try {
|
||||
const { fetchAndPersistProviderLimits } =
|
||||
await import("@/lib/usage/providerLimits");
|
||||
const { usage } = await fetchAndPersistProviderLimits(errorConnectionId, "manual");
|
||||
kimiRateLimitResetAt = getKimiTemporaryRateLimitResetAt(usage);
|
||||
} catch {
|
||||
// Preserve the existing quota handling when Kimi's usage endpoint is unavailable.
|
||||
}
|
||||
}
|
||||
|
||||
// Providers with per-model quotas — lock the model only, not the connection
|
||||
const quotaCooldownMs = kimiRateLimitResetAt
|
||||
? Math.max(new Date(kimiRateLimitResetAt).getTime() - Date.now(), 0)
|
||||
: retryAfterMs || COOLDOWN_MS.rateLimit;
|
||||
const accountSemaphoreKey = resolveAccountSemaphoreKey({
|
||||
provider,
|
||||
model: currentModel,
|
||||
connectionId: errorConnectionId,
|
||||
credentials,
|
||||
// Providers with per-model quotas — lock the model only, not the connection
|
||||
const quotaCooldownMs = kimiRateLimitResetAt
|
||||
? Math.max(new Date(kimiRateLimitResetAt).getTime() - Date.now(), 0)
|
||||
: retryAfterMs || COOLDOWN_MS.rateLimit;
|
||||
const accountSemaphoreKey = resolveAccountSemaphoreKey({
|
||||
provider,
|
||||
model: currentModel,
|
||||
connectionId: errorConnectionId,
|
||||
credentials,
|
||||
});
|
||||
if (accountSemaphoreKey) {
|
||||
markAccountSemaphoreBlocked(accountSemaphoreKey, quotaCooldownMs);
|
||||
}
|
||||
if (kimiRateLimitResetAt) {
|
||||
await updateProviderConnection(errorConnectionId, {
|
||||
testStatus: "unavailable",
|
||||
rateLimitedUntil: kimiRateLimitResetAt,
|
||||
backoffLevel: 0,
|
||||
lastErrorType: PROVIDER_ERROR_TYPES.RATE_LIMITED,
|
||||
lastError: message,
|
||||
errorCode: statusCode,
|
||||
});
|
||||
if (accountSemaphoreKey) {
|
||||
markAccountSemaphoreBlocked(accountSemaphoreKey, quotaCooldownMs);
|
||||
}
|
||||
if (kimiRateLimitResetAt) {
|
||||
await updateProviderConnection(errorConnectionId, {
|
||||
testStatus: "unavailable",
|
||||
rateLimitedUntil: kimiRateLimitResetAt,
|
||||
backoffLevel: 0,
|
||||
lastErrorType: PROVIDER_ERROR_TYPES.RATE_LIMITED,
|
||||
console.warn(
|
||||
`[provider] Node ${errorConnectionId} Kimi request window exhausted (${statusCode}) — retrying after ${kimiRateLimitResetAt}`
|
||||
);
|
||||
} else if (isModelScope() && errorConnectionId) {
|
||||
const lockFn = provider === "antigravity" ? lockExactModel : lockModel;
|
||||
lockFn(provider, errorConnectionId, model, "quota_exhausted", quotaCooldownMs);
|
||||
console.warn(
|
||||
`[provider] Node ${errorConnectionId} ModelScope model quota exhausted (${statusCode}) for ${model} - ${Math.ceil(quotaCooldownMs / 1000)}s (connection stays active)`
|
||||
);
|
||||
} else if (
|
||||
lockModelIfPerModelQuota(
|
||||
provider,
|
||||
errorConnectionId,
|
||||
model,
|
||||
"quota_exhausted",
|
||||
quotaCooldownMs
|
||||
)
|
||||
) {
|
||||
const quotaScope = getQuotaScopeLabelForProvider(provider, model);
|
||||
console.warn(
|
||||
`[provider] Node ${errorConnectionId} ${quotaScope}-only quota exhausted (${statusCode}) for ${model} - ${Math.ceil(quotaCooldownMs / 1000)}s (cooldown_scope=${quotaScope}, ttl_source=${retryAfterMs ? "upstream" : "inferred"}, connection stays active)`
|
||||
);
|
||||
} else {
|
||||
await writeTerminalStatus(
|
||||
errorConnectionId,
|
||||
{
|
||||
testStatus: "credits_exhausted",
|
||||
lastError: message,
|
||||
errorCode: statusCode,
|
||||
});
|
||||
console.warn(
|
||||
`[provider] Node ${errorConnectionId} Kimi request window exhausted (${statusCode}) — retrying after ${kimiRateLimitResetAt}`
|
||||
);
|
||||
} else if (isModelScope() && errorConnectionId) {
|
||||
const lockFn = provider === "antigravity" ? lockExactModel : lockModel;
|
||||
lockFn(provider, errorConnectionId, model, "quota_exhausted", quotaCooldownMs);
|
||||
console.warn(
|
||||
`[provider] Node ${errorConnectionId} ModelScope model quota exhausted (${statusCode}) for ${model} - ${Math.ceil(quotaCooldownMs / 1000)}s (connection stays active)`
|
||||
);
|
||||
} else if (
|
||||
lockModelIfPerModelQuota(
|
||||
provider,
|
||||
errorConnectionId,
|
||||
model,
|
||||
"quota_exhausted",
|
||||
quotaCooldownMs
|
||||
)
|
||||
) {
|
||||
const quotaScope = getQuotaScopeLabelForProvider(provider, model);
|
||||
console.warn(
|
||||
`[provider] Node ${errorConnectionId} ${quotaScope}-only quota exhausted (${statusCode}) for ${model} - ${Math.ceil(quotaCooldownMs / 1000)}s (cooldown_scope=${quotaScope}, ttl_source=${retryAfterMs ? "upstream" : "inferred"}, connection stays active)`
|
||||
);
|
||||
} else {
|
||||
await writeTerminalStatus(
|
||||
errorConnectionId,
|
||||
{
|
||||
testStatus: "credits_exhausted",
|
||||
lastError: message,
|
||||
lastErrorType: errorType,
|
||||
errorCode: String(statusCode),
|
||||
},
|
||||
"production"
|
||||
);
|
||||
console.warn(
|
||||
`[provider] Node ${errorConnectionId} exhausted quota (${statusCode})`
|
||||
);
|
||||
}
|
||||
lastErrorType: errorType,
|
||||
errorCode: String(statusCode),
|
||||
},
|
||||
"production"
|
||||
);
|
||||
console.warn(`[provider] Node ${errorConnectionId} exhausted quota (${statusCode})`);
|
||||
}
|
||||
} // close probeIsolated3 else
|
||||
}
|
||||
} else if (errorType === PROVIDER_ERROR_TYPES.UNAUTHORIZED) {
|
||||
|
||||
@@ -6,13 +6,12 @@
|
||||
* handleChatCore. Translates an upstream HTTP status into the in-memory key-health state
|
||||
* (apiKeyRotator) for the connection's currently-selected key, and persists the change to the
|
||||
* provider connection so it survives process restarts:
|
||||
* - genuine 401/403 credential rejection → record a failure (warning, then invalid at the
|
||||
* threshold), always persisted.
|
||||
* - 401 → record a failure (warning, then invalid at the threshold), always persisted.
|
||||
* - 402 → terminal (insufficient balance); mark the current key invalid immediately (#5239),
|
||||
* persisted on the active→invalid transition.
|
||||
* - 2xx → record a success, persisted only when recovering from a warning/invalid state.
|
||||
* Model availability failures remain model/routing telemetry even when an upstream reports them
|
||||
* with 401/403. Any other status only refreshes the tracked extra-key set.
|
||||
* Any other status only refreshes the tracked extra-key set. The handler binds its `log` once and
|
||||
* delegates here, keeping the existing call sites unchanged.
|
||||
*/
|
||||
|
||||
import {
|
||||
@@ -22,7 +21,6 @@ import {
|
||||
trackConnectionExtraKeys,
|
||||
type KeyHealth,
|
||||
} from "../../services/apiKeyRotator.ts";
|
||||
import { isModelUnavailableError } from "../../services/modelFamilyFallback.ts";
|
||||
import { updateProviderConnection } from "@/lib/db/providers";
|
||||
|
||||
type KeyHealthLog = {
|
||||
@@ -30,38 +28,11 @@ type KeyHealthLog = {
|
||||
error?: (tag: string, message: string) => void;
|
||||
} | null;
|
||||
|
||||
const CREDENTIAL_FAILURE_PATTERNS = [
|
||||
/\b(?:invalid|incorrect|expired|missing|revoked)\s+api[\s_-]?key\b/i,
|
||||
/\bapi[\s_-]?key\s+(?:is\s+)?(?:invalid|incorrect|expired|missing|revoked|not\s+valid)\b/i,
|
||||
/\bauthentication[\s_-]+(?:failed|error|required)\b/i,
|
||||
/\b(?:invalid|expired|missing|revoked)\s+(?:token|credentials?|bearer)\b/i,
|
||||
/\bunauthorized\b/i,
|
||||
/\bnot\s+authenticated\b/i,
|
||||
/\bforbidden\b/i,
|
||||
/\baccess\s+denied\b/i,
|
||||
];
|
||||
|
||||
function isModelCapabilityFailure(status: number, failureDetail: string): boolean {
|
||||
if (!failureDetail) return false;
|
||||
const normalizedDetail = failureDetail.replace(/[_-]+/g, " ");
|
||||
// Model-family fallback already owns these phrases. Use a model-capable status for
|
||||
// classification because some aggregators misreport the same model rejection as 401.
|
||||
return isModelUnavailableError(status === 401 ? 403 : status, normalizedDetail);
|
||||
}
|
||||
|
||||
function isCredentialFailure(status: number, failureDetail: string): boolean {
|
||||
if (status !== 401 && status !== 403) return false;
|
||||
if (isModelCapabilityFailure(status, failureDetail)) return false;
|
||||
if (status === 401) return true;
|
||||
return CREDENTIAL_FAILURE_PATTERNS.some((pattern) => pattern.test(failureDetail));
|
||||
}
|
||||
|
||||
export function recordKeyHealthStatus(
|
||||
status: number,
|
||||
creds: Record<string, unknown> | null | undefined,
|
||||
log?: KeyHealthLog,
|
||||
transport?: string,
|
||||
failureDetail = ""
|
||||
transport?: string
|
||||
): void {
|
||||
// CLIProxyAPI owns a shared external credential pool. Its auth failures cannot be
|
||||
// attributed to the native OmniRoute connection selected before proxy dispatch.
|
||||
@@ -84,11 +55,11 @@ export function recordKeyHealthStatus(
|
||||
|
||||
trackConnectionExtraKeys(connId, extraKeys);
|
||||
|
||||
if (isCredentialFailure(status, failureDetail)) {
|
||||
if (status === 401) {
|
||||
const updatedHealth = recordKeyFailure(connId, currentKeyId);
|
||||
log?.warn?.(
|
||||
"AUTH",
|
||||
`${status} on connection ${connId.slice(0, 8)} - key marked as failed (failure #${updatedHealth.failures})`
|
||||
`401 on connection ${connId.slice(0, 8)} - key marked as failed (failure #${updatedHealth.failures})`
|
||||
);
|
||||
|
||||
// Persist health status to DB on every failure (not just invalid transitions)
|
||||
|
||||
@@ -24,7 +24,6 @@ import {
|
||||
isUnconfiguredLoopbackSearchProvider,
|
||||
type SearchProviderConfig,
|
||||
} from "../config/searchRegistry.ts";
|
||||
import { NIMBLE_CLIENT_SOURCE, NIMBLE_CLIENT_SOURCE_HEADER } from "../config/nimble.ts";
|
||||
import { buildPerplexityRequest, parsePerplexitySearchOptions } from "./search/perplexitySearch.ts";
|
||||
import * as fcSearch from "./search/firecrawlSearch.ts";
|
||||
import { type FirecrawlSearchEnvelope } from "./search/firecrawlSearch.ts";
|
||||
@@ -476,38 +475,6 @@ function buildTavilyRequest(
|
||||
};
|
||||
}
|
||||
|
||||
function buildNimbleRequest(
|
||||
config: SearchProviderConfig,
|
||||
params: SearchRequestParams
|
||||
): { url: string; init: RequestInit } {
|
||||
if (!params.token) throw new Error("Nimble Search requires an API key");
|
||||
const { includes, excludes } = parseDomainFilter(params.domainFilter);
|
||||
const body: Record<string, unknown> = {
|
||||
query: params.query,
|
||||
max_results: Math.min(params.maxResults, config.maxMaxResults),
|
||||
search_depth: "lite",
|
||||
output_format: "plain_text",
|
||||
focus: params.searchType === "news" ? "news" : "general",
|
||||
};
|
||||
if (params.country) body.country = params.country.toUpperCase();
|
||||
if (params.language) body.locale = params.language;
|
||||
if (params.timeRange && params.timeRange !== "any") body.time_range = params.timeRange;
|
||||
if (includes.length) body.include_domains = includes.slice(0, 50);
|
||||
if (excludes.length) body.exclude_domains = excludes.slice(0, 50);
|
||||
return {
|
||||
url: resolveSearchBaseUrl(config, params),
|
||||
init: {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${params.token}`,
|
||||
[NIMBLE_CLIENT_SOURCE_HEADER]: NIMBLE_CLIENT_SOURCE,
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function buildGooglePseRequest(
|
||||
config: SearchProviderConfig,
|
||||
params: SearchRequestParams
|
||||
@@ -740,7 +707,6 @@ const requestBuilders: Record<string, SearchRequestBuilder> = {
|
||||
"perplexity-search": buildPerplexityRequest,
|
||||
"exa-search": buildExaRequest,
|
||||
"tavily-search": buildTavilyRequest,
|
||||
"nimble-search": buildNimbleRequest,
|
||||
firecrawl: fcSearch.buildFirecrawlSearchRequest,
|
||||
"google-pse-search": buildGooglePseRequest,
|
||||
"linkup-search": buildLinkupRequest,
|
||||
@@ -862,47 +828,6 @@ function normalizeTavilyResponse(
|
||||
return { results, totalResults: results.length };
|
||||
}
|
||||
|
||||
interface NimbleSearchItem {
|
||||
title?: string;
|
||||
url?: string;
|
||||
description?: string;
|
||||
content?: string;
|
||||
}
|
||||
|
||||
interface NimbleSearchEnvelope {
|
||||
results?: NimbleSearchItem[];
|
||||
total_results?: number;
|
||||
}
|
||||
|
||||
function normalizeNimbleResponse(
|
||||
data: unknown,
|
||||
_query: string,
|
||||
_searchType: string
|
||||
): { results: SearchResult[]; totalResults: number | null } {
|
||||
const now = new Date().toISOString();
|
||||
const envelope = (data ?? {}) as NimbleSearchEnvelope;
|
||||
if (!Array.isArray(envelope.results)) return { results: [], totalResults: null };
|
||||
const results = envelope.results.map((item, idx) =>
|
||||
makeResult(
|
||||
"nimble-search",
|
||||
{
|
||||
title: item.title,
|
||||
url: item.url,
|
||||
snippet: item.description || item.content?.slice(0, 300) || "",
|
||||
full_text: item.content || undefined,
|
||||
text_format: "text",
|
||||
},
|
||||
idx,
|
||||
now
|
||||
)
|
||||
);
|
||||
return {
|
||||
results,
|
||||
totalResults:
|
||||
typeof envelope.total_results === "number" ? envelope.total_results : results.length,
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeGooglePseResponse(
|
||||
data: any,
|
||||
_query: string,
|
||||
@@ -1358,7 +1283,6 @@ const responseNormalizers: Record<string, SearchResponseNormalizer> = {
|
||||
"perplexity-search": normalizePerplexityResponse,
|
||||
"exa-search": normalizeExaResponse,
|
||||
"tavily-search": normalizeTavilyResponse,
|
||||
"nimble-search": normalizeNimbleResponse,
|
||||
firecrawl: (data: FirecrawlSearchEnvelope, _query: string, searchType: string) =>
|
||||
fcSearch.normalizeFirecrawlSearchResponse(data, searchType, makeResult),
|
||||
"google-pse-search": normalizeGooglePseResponse,
|
||||
|
||||
@@ -21,14 +21,12 @@ import { firecrawlFetch } from "../executors/firecrawl-fetch.ts";
|
||||
import { jinaReaderFetch } from "../executors/jina-reader-fetch.ts";
|
||||
import { tavilyFetch } from "../executors/tavily-fetch.ts";
|
||||
import { tinyfishFetch } from "../executors/tinyfish-fetch.ts";
|
||||
import { nimbleFetch } from "../executors/nimble-fetch.ts";
|
||||
|
||||
export type WebFetchFormat = "markdown" | "html" | "links" | "screenshot";
|
||||
|
||||
export interface WebFetchRequest {
|
||||
url: string;
|
||||
provider?:
|
||||
"firecrawl" | "jina-reader" | "tavily-search" | "tinyfish" | "context7" | "nimble-search";
|
||||
provider?: "firecrawl" | "jina-reader" | "tavily-search" | "tinyfish" | "context7";
|
||||
format?: WebFetchFormat;
|
||||
depth?: 0 | 1 | 2;
|
||||
wait_for_selector?: string;
|
||||
@@ -63,7 +61,6 @@ export const WEB_FETCH_PROVIDERS = Object.freeze([
|
||||
"tavily-search",
|
||||
"tinyfish",
|
||||
"context7",
|
||||
"nimble-search",
|
||||
] as const);
|
||||
// Derived from the array — adding a provider to WEB_FETCH_PROVIDERS
|
||||
// automatically widens the union; they cannot drift apart.
|
||||
@@ -142,14 +139,6 @@ export async function handleWebFetch(
|
||||
credentials,
|
||||
});
|
||||
|
||||
case "nimble-search":
|
||||
return await nimbleFetch({
|
||||
url: req.url,
|
||||
format,
|
||||
includeMetadata,
|
||||
credentials,
|
||||
});
|
||||
|
||||
case "context7":
|
||||
// Context7 returns llms.txt text only: html/links/screenshot formats are
|
||||
// unsupported, and the format field is validated/ignored below.
|
||||
|
||||
@@ -557,7 +557,7 @@ export const webFetchInput = z.object({
|
||||
.min(1, "URL is required")
|
||||
.describe("The URL to fetch content from"),
|
||||
provider: z
|
||||
.enum(["firecrawl", "jina-reader", "tavily-search", "tinyfish", "context7", "nimble-search"])
|
||||
.enum(["firecrawl", "jina-reader", "tavily-search", "tinyfish", "context7"])
|
||||
.optional()
|
||||
.describe(
|
||||
"Specific fetch provider to use (default: first available). " +
|
||||
|
||||
@@ -694,8 +694,7 @@ async function handleXSearch(args: {
|
||||
|
||||
async function handleWebFetch(args: {
|
||||
url: string;
|
||||
provider?:
|
||||
"firecrawl" | "jina-reader" | "tavily-search" | "tinyfish" | "context7" | "nimble-search";
|
||||
provider?: "firecrawl" | "jina-reader" | "tavily-search" | "tinyfish" | "context7";
|
||||
format?: "markdown" | "html" | "links" | "screenshot";
|
||||
include_metadata?: boolean;
|
||||
depth?: number;
|
||||
|
||||
@@ -30,16 +30,6 @@ const BOOTSTRAP_TIMEOUT_MS = 8_000;
|
||||
const ONBOARD_TIMEOUT_MS = 15_000;
|
||||
const DEFAULT_TIER_ID = "legacy-tier";
|
||||
|
||||
// onboardUser is a Long-Running Operation: Google frequently answers the
|
||||
// first call with {"done": false} (no cloudaicompanionProject field yet) and
|
||||
// expects the SAME request re-sent every couple of seconds until the
|
||||
// operation settles with {"done": true, response: {...}}. Treating the
|
||||
// first "done:false" response as "no project" (BYOP) misclassifies a normal
|
||||
// in-progress onboarding as "bring your own project" and permanently caches
|
||||
// that wrong verdict. Poll bounded, matching 9router's onboardUser().
|
||||
const ONBOARD_POLL_MAX_ATTEMPTS = 5;
|
||||
const ONBOARD_POLL_INTERVAL_MS = 2_000;
|
||||
|
||||
/** Ordered list of loadCodeAssist endpoint URLs. */
|
||||
export function getAntigravityLoadCodeAssistUrls(): string[] {
|
||||
return ANTIGRAVITY_BOOTSTRAP_BASE_URLS.map((base) => `${base}${LOAD_CODE_ASSIST_PATH}`);
|
||||
@@ -172,31 +162,10 @@ async function tryLoadCodeAssist(
|
||||
return { projectId: null, tierId: DEFAULT_TIER_ID };
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract the project id from a settled ({done:true}) onboardUser response body.
|
||||
* The documented LRO shape nests it under `response.cloudaicompanionProject`
|
||||
* (matches 9router's onboardUser and Google's Operation envelope), but some
|
||||
* observed responses put it at the top level — check both.
|
||||
*/
|
||||
function extractProjectIdFromOnboardResponse(data: Record<string, unknown> | null): string | null {
|
||||
const nested = (data?.response as Record<string, unknown> | undefined)?.cloudaicompanionProject;
|
||||
const project = nested ?? data?.cloudaicompanionProject;
|
||||
if (typeof project === "string") {
|
||||
const id = project.trim();
|
||||
return id || null;
|
||||
}
|
||||
if (project && typeof project === "object") {
|
||||
const id = (project as Record<string, unknown>).id;
|
||||
if (typeof id === "string" && id.trim()) return id.trim();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt onboardUser to create a Cloud Code project for the account.
|
||||
* Called when loadCodeAssist returns no project — the account has never
|
||||
* been onboarded. Polls the same endpoint on {done:false} responses (an
|
||||
* in-progress LRO) before concluding anything about the account.
|
||||
* been onboarded. Returns true if any endpoint reports success.
|
||||
*/
|
||||
async function tryOnboardUser(
|
||||
accessToken: string,
|
||||
@@ -207,74 +176,47 @@ async function tryOnboardUser(
|
||||
): Promise<AntigravityOnboardStatus> {
|
||||
const urls = getAntigravityOnboardUrls();
|
||||
const headers = getAntigravityContentHeaders(clientProfile, accessToken);
|
||||
const body = JSON.stringify({
|
||||
tier_id: tierId,
|
||||
metadata: getAntigravityLoadCodeAssistMetadata(),
|
||||
});
|
||||
|
||||
for (const url of urls) {
|
||||
for (let attempt = 1; attempt <= ONBOARD_POLL_MAX_ATTEMPTS; attempt++) {
|
||||
if (signal?.aborted) throw signal.reason;
|
||||
try {
|
||||
const timeoutSignal = AbortSignal.timeout(ONBOARD_TIMEOUT_MS);
|
||||
const response = await fetchImpl(url, {
|
||||
method: "POST",
|
||||
headers,
|
||||
body,
|
||||
signal: signal ? AbortSignal.any([signal, timeoutSignal]) : timeoutSignal,
|
||||
});
|
||||
if (signal?.aborted) throw signal.reason;
|
||||
try {
|
||||
const timeoutSignal = AbortSignal.timeout(ONBOARD_TIMEOUT_MS);
|
||||
const response = await fetchImpl(url, {
|
||||
method: "POST",
|
||||
headers,
|
||||
body: JSON.stringify({
|
||||
tier_id: tierId,
|
||||
metadata: getAntigravityLoadCodeAssistMetadata(),
|
||||
}),
|
||||
signal: signal ? AbortSignal.any([signal, timeoutSignal]) : timeoutSignal,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
if (response.ok) {
|
||||
// Accounts Google expects to Bring Their Own Project: onboardUser
|
||||
// returns 200 without a `cloudaicompanionProject` in the body — no
|
||||
// automatic project creation for standard-tier/personal accounts
|
||||
// (tracked in #8491). Detect that so we can fail fast with a clear
|
||||
// instruction instead of retrying forever or fabricating an id that
|
||||
// Google later rejects with a delayed 429 RESOURCE_EXHAUSTED.
|
||||
const body = await response.text().catch(() => "");
|
||||
if (body && !/cloudaicompanionProject/.test(body)) {
|
||||
console.warn(
|
||||
`[models] antigravity onboardUser failed at ${url} (${response.status}) — trying next`
|
||||
`[models] antigravity onboardUser done but no project in response at ${url} — Google BYOP (user-defined GCP project) required`
|
||||
);
|
||||
break;
|
||||
return "requires_manual_project";
|
||||
}
|
||||
|
||||
const data = (await response.json().catch(() => null)) as Record<string, unknown> | null;
|
||||
|
||||
// Only an EXPLICIT `done: false` means "in-progress LRO, poll again".
|
||||
// A proper Google Operation always carries `done` when it is one; a
|
||||
// response with `done` absent entirely (e.g. `{}`) is not an LRO in
|
||||
// progress — it's Google's immediate, settled "no project" answer for
|
||||
// BYOP accounts (#8491) and must fall through to that classification
|
||||
// on the first attempt, same as before this polling was added.
|
||||
if (data?.done === false) {
|
||||
// In-progress LRO — Google hasn't decided (project created, or
|
||||
// BYOP required) yet. Re-send the same request after a short wait.
|
||||
if (attempt < ONBOARD_POLL_MAX_ATTEMPTS) {
|
||||
console.warn(
|
||||
`[models] antigravity onboardUser at ${url} not done yet (attempt ${attempt}/${ONBOARD_POLL_MAX_ATTEMPTS}) — waiting`
|
||||
);
|
||||
await new Promise((resolve) => setTimeout(resolve, ONBOARD_POLL_INTERVAL_MS));
|
||||
continue;
|
||||
}
|
||||
console.warn(
|
||||
`[models] antigravity onboardUser at ${url} still not done after ${ONBOARD_POLL_MAX_ATTEMPTS} attempts — treating as failed`
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
// done:true — Google has settled the operation. Accounts Google
|
||||
// expects to Bring Their Own Project answer with done:true and no
|
||||
// cloudaicompanionProject — no automatic project creation for
|
||||
// standard-tier/personal accounts (tracked in #8491). Only now is it
|
||||
// safe to draw that conclusion.
|
||||
if (extractProjectIdFromOnboardResponse(data)) {
|
||||
return "onboarded";
|
||||
}
|
||||
console.warn(
|
||||
`[models] antigravity onboardUser done but no project in response at ${url} — Google BYOP (user-defined GCP project) required`
|
||||
);
|
||||
return "requires_manual_project";
|
||||
} catch (error) {
|
||||
if (signal?.aborted || (error instanceof Error && error.name === "AbortError")) {
|
||||
throw signal?.reason ?? error;
|
||||
}
|
||||
const msg = error instanceof Error ? error.message : String(error);
|
||||
console.warn(`[models] antigravity onboardUser threw for ${url}: ${msg} — trying next`);
|
||||
break;
|
||||
return "onboarded";
|
||||
}
|
||||
|
||||
console.warn(
|
||||
`[models] antigravity onboardUser failed at ${url} (${response.status}) — trying next`
|
||||
);
|
||||
} catch (error) {
|
||||
if (signal?.aborted || (error instanceof Error && error.name === "AbortError")) {
|
||||
throw signal?.reason ?? error;
|
||||
}
|
||||
const msg = error instanceof Error ? error.message : String(error);
|
||||
console.warn(`[models] antigravity onboardUser threw for ${url}: ${msg} — trying next`);
|
||||
}
|
||||
}
|
||||
return "failed";
|
||||
|
||||
@@ -31,10 +31,7 @@ const MAX_CONNECTION_EXTRA_KEYS = 500;
|
||||
*/
|
||||
export function trackConnectionExtraKeys(connectionId: string, extraKeys: string[]): void {
|
||||
const validExtras = extraKeys.filter((k) => typeof k === "string" && k.trim().length > 0);
|
||||
if (
|
||||
!_connectionExtraKeys.has(connectionId) &&
|
||||
_connectionExtraKeys.size >= MAX_CONNECTION_EXTRA_KEYS
|
||||
) {
|
||||
if (!_connectionExtraKeys.has(connectionId) && _connectionExtraKeys.size >= MAX_CONNECTION_EXTRA_KEYS) {
|
||||
const oldest = _connectionExtraKeys.keys().next().value;
|
||||
if (oldest !== undefined) _connectionExtraKeys.delete(oldest);
|
||||
}
|
||||
@@ -311,29 +308,6 @@ export function syncHealthFromDB(connectionId: string, health?: Record<string, K
|
||||
}
|
||||
}
|
||||
|
||||
/** Recover one authoritatively validated key without changing sibling-key health. */
|
||||
export function recoverKeyHealth(
|
||||
connectionId: string,
|
||||
keyId: string,
|
||||
providerSpecificData: unknown
|
||||
): Record<string, unknown> | undefined {
|
||||
const data =
|
||||
providerSpecificData && typeof providerSpecificData === "object"
|
||||
? (providerSpecificData as Record<string, unknown>)
|
||||
: {};
|
||||
const health = data.apiKeyHealth as Record<string, KeyHealth> | undefined;
|
||||
const currentHealth = health?.[keyId];
|
||||
if (!currentHealth || (currentHealth.status === "active" && currentHealth.failures === 0)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
syncHealthFromDB(connectionId, health);
|
||||
return {
|
||||
...data,
|
||||
apiKeyHealth: { ...health, [keyId]: recordKeySuccess(connectionId, keyId) },
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset the rotation index for a connection.
|
||||
* Call this when a key fails (401/403) to skip the bad key next time.
|
||||
|
||||
@@ -10,9 +10,6 @@
|
||||
* 2b. Layers 1-2 retried against the base model this id inherits quality scores
|
||||
* from, when `resolveScoresAs` resolves one (#11489). Reported as
|
||||
* `<source>:inherited`.
|
||||
* 2c. If the request id or its scoresAs base is vendor-retired (#11625), layers
|
||||
* 1–3 are skipped so a leftover arena row cannot short-circuit the layer-3
|
||||
* veto, and a dead *codex id cannot keep the coding wildcard boost.
|
||||
* 3. Models.dev tier — derived from `model_capabilities` table capability data,
|
||||
* with a vendor-lifecycle veto (#11508: a retired id never earns a tier
|
||||
* score) and the same base-model inheritance as layers 1–2
|
||||
@@ -46,7 +43,6 @@ import {
|
||||
} from "../../../src/lib/db/modelIntelligence.ts";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { resolveScoresAs } from "./scoresAs.ts";
|
||||
import { isVendorRetiredId } from "../modelLifecycle.ts";
|
||||
|
||||
// #11508 — vendor lifecycle snapshot (#11507). An id the vendor has retired
|
||||
// must never earn a capability-derived tier score: models.dev keeps listing
|
||||
@@ -467,60 +463,48 @@ export function getTaskFitness(model: string, taskType: string): number {
|
||||
return getTaskFitnessWithSource(model, taskType).score;
|
||||
}
|
||||
|
||||
function isFitnessRetired(modelId: string): boolean {
|
||||
if (isVendorRetiredId(modelId)) return true;
|
||||
const { base, via } = resolveScoresAs(modelId);
|
||||
return via !== null && isVendorRetiredId(base);
|
||||
}
|
||||
|
||||
export function getTaskFitnessWithSource(
|
||||
model: string,
|
||||
taskType: string
|
||||
): { score: number; source: string } {
|
||||
const normalizedModel = model.toLowerCase();
|
||||
const normalizedTask = taskType.toLowerCase();
|
||||
const fitnessRetired = isFitnessRetired(normalizedModel);
|
||||
|
||||
if (!fitnessRetired) {
|
||||
const userOverride = queryModelIntelligence(normalizedModel, normalizedTask, "user_override");
|
||||
if (userOverride !== null) {
|
||||
return { score: userOverride, source: "user_override" };
|
||||
}
|
||||
|
||||
const arenaElo = queryModelIntelligence(normalizedModel, normalizedTask, "arena_elo");
|
||||
if (arenaElo !== null) {
|
||||
return { score: arenaElo, source: "arena_elo" };
|
||||
}
|
||||
|
||||
// Layers 1-2, retried against the base model this id inherits quality from
|
||||
// (#11489). Every DB-backed source publishes scores for BASE models only, so
|
||||
// a variant id — an effort suffix (`gpt-5.6-sol-xhigh`), a vendor alias
|
||||
// (`gpt-5.6`), a `-free` tier marker (`mimo-v2.5-free`, #4517) — misses both
|
||||
// literal lookups and used to fall all the way to the wildcard 0.5, losing
|
||||
// every comparison against a base model that happens to be benchmarked.
|
||||
// The score is inherited VERBATIM: the 12-factor scoring already prices cost
|
||||
// and latency per variant, so there is no basis for inventing an effort
|
||||
// delta. `:inherited` keeps the indirection visible to callers.
|
||||
const inherited = lookupInheritedFitness(normalizedModel, normalizedTask);
|
||||
if (inherited !== null) {
|
||||
return inherited;
|
||||
}
|
||||
|
||||
const tierScore = getModelsDevTierFitness(normalizedModel, normalizedTask);
|
||||
if (tierScore !== null) {
|
||||
return { score: tierScore, source: "models_dev_tier" };
|
||||
}
|
||||
|
||||
const staticScore = lookupStaticFitnessTable(normalizedModel, normalizedTask);
|
||||
if (staticScore !== null) {
|
||||
return { score: staticScore, source: "fitness_table" };
|
||||
}
|
||||
|
||||
return { score: lookupWildcardBoosts(normalizedModel, normalizedTask), source: "wildcard_boost" };
|
||||
const userOverride = queryModelIntelligence(normalizedModel, normalizedTask, "user_override");
|
||||
if (userOverride !== null) {
|
||||
return { score: userOverride, source: "user_override" };
|
||||
}
|
||||
|
||||
// Retired: 0.5 is "no evidence", never a quality claim and never a *codex boost.
|
||||
return { score: 0.5, source: "wildcard_boost" };
|
||||
const arenaElo = queryModelIntelligence(normalizedModel, normalizedTask, "arena_elo");
|
||||
if (arenaElo !== null) {
|
||||
return { score: arenaElo, source: "arena_elo" };
|
||||
}
|
||||
|
||||
// Layers 1-2, retried against the base model this id inherits quality from
|
||||
// (#11489). Every DB-backed source publishes scores for BASE models only, so
|
||||
// a variant id — an effort suffix (`gpt-5.6-sol-xhigh`), a vendor alias
|
||||
// (`gpt-5.6`), a `-free` tier marker (`mimo-v2.5-free`, #4517) — misses both
|
||||
// literal lookups and used to fall all the way to the wildcard 0.5, losing
|
||||
// every comparison against a base model that happens to be benchmarked.
|
||||
// The score is inherited VERBATIM: the 12-factor scoring already prices cost
|
||||
// and latency per variant, so there is no basis for inventing an effort
|
||||
// delta. `:inherited` keeps the indirection visible to callers.
|
||||
const inherited = lookupInheritedFitness(normalizedModel, normalizedTask);
|
||||
if (inherited !== null) {
|
||||
return inherited;
|
||||
}
|
||||
|
||||
const tierHit = getModelsDevTierFitnessWithSource(normalizedModel, normalizedTask);
|
||||
if (tierHit !== null) {
|
||||
return tierHit;
|
||||
}
|
||||
|
||||
const staticScore = lookupStaticFitnessTable(normalizedModel, normalizedTask);
|
||||
if (staticScore !== null) {
|
||||
return { score: staticScore, source: "fitness_table" };
|
||||
}
|
||||
|
||||
return { score: lookupWildcardBoosts(normalizedModel, normalizedTask), source: "wildcard_boost" };
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -540,7 +524,6 @@ function lookupInheritedFitness(
|
||||
const { base, via } = resolveScoresAs(normalizedModel);
|
||||
if (via === null || base === normalizedModel) return null;
|
||||
const normalizedBase = base.toLowerCase();
|
||||
if (isVendorRetiredId(normalizedBase)) return null;
|
||||
|
||||
for (const source of ["user_override", "arena_elo"] as const) {
|
||||
const score = queryModelIntelligence(normalizedBase, normalizedTask, source);
|
||||
|
||||
@@ -70,7 +70,6 @@ import { resolveProviderId } from "../../src/shared/constants/providers.ts";
|
||||
import * as semaphore from "./rateLimitSemaphore.ts";
|
||||
import { getCircuitBreaker } from "../../src/shared/utils/circuitBreaker";
|
||||
import { parseModel } from "./model.ts";
|
||||
import { rejectRetiredAutoComboCandidates } from "./modelLifecycle.ts";
|
||||
import { createComboContext } from "./combo/context.ts";
|
||||
import { phaseComboSetup } from "./combo/comboSetup.ts";
|
||||
import { checkCredentialGate, logCredentialSkip } from "./credentialGate.ts";
|
||||
@@ -628,14 +627,11 @@ export async function buildAutoCandidates(
|
||||
})
|
||||
);
|
||||
|
||||
// Filter out candidates whose model is hidden by the user in the dashboard,
|
||||
// then drop vendor-retired ids so auto-combo cannot pick them (#11625).
|
||||
return rejectRetiredAutoComboCandidates(
|
||||
candidates.filter((c) => {
|
||||
const hiddenModels = hiddenModelsMap.get(c.provider);
|
||||
return !hiddenModels?.has(c.model);
|
||||
})
|
||||
);
|
||||
// Filter out candidates whose model is hidden by the user in the dashboard
|
||||
return candidates.filter((c) => {
|
||||
const hiddenModels = hiddenModelsMap.get(c.provider);
|
||||
return !hiddenModels?.has(c.model);
|
||||
});
|
||||
}
|
||||
|
||||
// Context-cache pin health gate — moved to combo/dispatchPrelude.ts alongside the
|
||||
|
||||
@@ -230,10 +230,7 @@ function formatMessagesForPrompt(messages: MessageLike[]): string {
|
||||
.join("\n\n");
|
||||
}
|
||||
|
||||
export function selectMessagesForSummary(
|
||||
messages: MessageLike[],
|
||||
maxMessages: number
|
||||
): MessageLike[] {
|
||||
export function selectMessagesForSummary(messages: MessageLike[], maxMessages: number): MessageLike[] {
|
||||
const validMessages = messages.filter((m) => m && typeof m === "object");
|
||||
const system = validMessages.filter(
|
||||
(m) => typeof m.role === "string" && (m.role === "system" || m.role === "developer")
|
||||
@@ -629,15 +626,6 @@ async function generateUniversalHandoffAsync(options: {
|
||||
|
||||
const summaryPrompt = HANDOFF_PROMPT_TEMPLATE.replace("{HISTORY}", historyText);
|
||||
const summaryModel = options.handoffModel || options.currModel;
|
||||
|
||||
if (options.providerAllowlist.length > 0) {
|
||||
const slashIdx = summaryModel.indexOf("/");
|
||||
const modelProvider = slashIdx > 0 ? summaryModel.slice(0, slashIdx) : "";
|
||||
if (modelProvider && !options.providerAllowlist.includes(modelProvider)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const summaryBody: Record<string, unknown> = {
|
||||
model: summaryModel,
|
||||
messages: [{ role: "user", content: summaryPrompt }],
|
||||
|
||||
@@ -4,14 +4,7 @@
|
||||
* Replacement model IDs are migration guidance only. This module never rewrites a
|
||||
* request: shutdown models are rejected, deprecated models remain callable until
|
||||
* their shutdown date, and untracked models pass through unchanged.
|
||||
*
|
||||
* Dated OpenAI rows in MODEL_LIFECYCLE_RECORDS stay provider-scoped (a warn-before-
|
||||
* shutdown date on `openai` must not leak onto an aggregator that still serves the
|
||||
* id). Snapshot `status: "retired"` ids from config/quality/model-lifecycle.json
|
||||
* are id-scoped and prefix-stripped (#11625): `openai/gpt-5.2-codex` on openrouter
|
||||
* is the same retired vendor id as `gpt-5.2-codex`.
|
||||
*/
|
||||
import { readFileSync } from "node:fs";
|
||||
|
||||
export const OPENAI_MODEL_DEPRECATIONS_URL = "https://developers.openai.com/api/docs/deprecations";
|
||||
|
||||
@@ -135,67 +128,6 @@ function shutdownTimestamp(shutdownAt: string): number {
|
||||
return Date.parse(`${shutdownAt}T00:00:00.000Z`);
|
||||
}
|
||||
|
||||
const SNAPSHOT_URL = new URL("../../config/quality/model-lifecycle.json", import.meta.url);
|
||||
const SNAPSHOT_SOURCE = "config/quality/model-lifecycle.json";
|
||||
|
||||
type VendorRetiredEntry = {
|
||||
vendor?: string;
|
||||
status?: string;
|
||||
retiredOn?: string | null;
|
||||
replacement?: string | null;
|
||||
};
|
||||
|
||||
let _retiredIds: Set<string> | null = null;
|
||||
let _retiredEntries: Map<string, VendorRetiredEntry> | null = null;
|
||||
|
||||
function loadVendorRetiredSnapshot(): {
|
||||
ids: Set<string>;
|
||||
entries: Map<string, VendorRetiredEntry>;
|
||||
} {
|
||||
if (_retiredIds && _retiredEntries) return { ids: _retiredIds, entries: _retiredEntries };
|
||||
const ids = new Set<string>();
|
||||
const entries = new Map<string, VendorRetiredEntry>();
|
||||
try {
|
||||
const parsed = JSON.parse(readFileSync(SNAPSHOT_URL, "utf8")) as {
|
||||
retired?: Record<string, VendorRetiredEntry>;
|
||||
};
|
||||
for (const [id, entry] of Object.entries(parsed.retired ?? {})) {
|
||||
if (entry?.status !== "retired") continue;
|
||||
const key = id.toLowerCase();
|
||||
ids.add(key);
|
||||
entries.set(key, entry);
|
||||
}
|
||||
} catch {
|
||||
// Snapshot missing → no id-scoped veto. Dated OpenAI rows still apply.
|
||||
}
|
||||
_retiredIds = ids;
|
||||
_retiredEntries = entries;
|
||||
return { ids, entries };
|
||||
}
|
||||
|
||||
/** True when `modelId` or its last `vendor/` path segment is `status: "retired"` in the snapshot. */
|
||||
export function isVendorRetiredId(modelId: string | null | undefined): boolean {
|
||||
if (typeof modelId !== "string" || modelId.length === 0) return false;
|
||||
const lower = modelId.toLowerCase();
|
||||
const { ids } = loadVendorRetiredSnapshot();
|
||||
if (ids.has(lower)) return true;
|
||||
const slash = lower.lastIndexOf("/");
|
||||
return slash !== -1 && ids.has(lower.slice(slash + 1));
|
||||
}
|
||||
|
||||
function lookupVendorRetiredEntry(modelId: string): VendorRetiredEntry | null {
|
||||
const lower = modelId.toLowerCase();
|
||||
const { entries } = loadVendorRetiredSnapshot();
|
||||
return entries.get(lower) ?? entries.get(lower.slice(lower.lastIndexOf("/") + 1)) ?? null;
|
||||
}
|
||||
|
||||
/** Drop auto-combo candidates whose model id the vendor has retired (#11625). */
|
||||
export function rejectRetiredAutoComboCandidates<T extends { model: string }>(
|
||||
candidates: readonly T[]
|
||||
): T[] {
|
||||
return candidates.filter((candidate) => !isVendorRetiredId(candidate.model));
|
||||
}
|
||||
|
||||
export function getModelLifecycleDecision(
|
||||
provider: string | null | undefined,
|
||||
model: string | null | undefined,
|
||||
@@ -206,24 +138,6 @@ export function getModelLifecycleDecision(
|
||||
const record = RECORDS_BY_KEY.get(lifecycleKey(normalizedProvider, normalizedModel));
|
||||
|
||||
if (!record) {
|
||||
if (isVendorRetiredId(normalizedModel)) {
|
||||
const entry = lookupVendorRetiredEntry(normalizedModel);
|
||||
const replacementId =
|
||||
typeof entry?.replacement === "string" && entry.replacement.length > 0
|
||||
? entry.replacement
|
||||
: null;
|
||||
return {
|
||||
provider: normalizedProvider,
|
||||
model: normalizedModel,
|
||||
status: "shutdown",
|
||||
action: "reject",
|
||||
shutdownAt: typeof entry?.retiredOn === "string" ? entry.retiredOn : null,
|
||||
replacement: replacementId
|
||||
? { provider: entry?.vendor ?? "", model: replacementId }
|
||||
: null,
|
||||
source: SNAPSHOT_SOURCE,
|
||||
};
|
||||
}
|
||||
return {
|
||||
provider: normalizedProvider,
|
||||
model: normalizedModel,
|
||||
@@ -256,8 +170,7 @@ export function formatModelLifecycleMessage(decision: ModelLifecycleDecision): s
|
||||
? ` Use "${decision.replacement.provider}/${decision.replacement.model}" instead.`
|
||||
: "";
|
||||
if (decision.status === "shutdown") {
|
||||
const when = decision.shutdownAt ? ` was shut down on ${decision.shutdownAt}` : " has been retired by its vendor";
|
||||
return `Model "${modelRef}"${when} and cannot be routed automatically.${replacement}`;
|
||||
return `Model "${modelRef}" was shut down on ${decision.shutdownAt} and cannot be routed automatically.${replacement}`;
|
||||
}
|
||||
return `Model "${modelRef}" is deprecated and is scheduled to shut down on ${decision.shutdownAt}.${replacement}`;
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { register } from "../registry.ts";
|
||||
import { FORMATS } from "../formats.ts";
|
||||
import {
|
||||
DEFAULT_SAFETY_SETTINGS,
|
||||
tryParseJSON,
|
||||
cleanJSONSchemaForAntigravity,
|
||||
} from "../helpers/geminiHelper.ts";
|
||||
import { buildGeminiTools, sanitizeGeminiToolName } from "../helpers/geminiToolsSanitizer.ts";
|
||||
@@ -185,6 +186,13 @@ export function claudeToGeminiRequest(model, body, stream, credentials = null) {
|
||||
.map((c) => (c.type === "text" ? c.text : JSON.stringify(c)))
|
||||
.join("\n");
|
||||
}
|
||||
let parsedContent = tryParseJSON(content);
|
||||
if (parsedContent === null) {
|
||||
parsedContent = { result: content };
|
||||
} else if (typeof parsedContent !== "object") {
|
||||
parsedContent = { result: parsedContent };
|
||||
}
|
||||
|
||||
const toolUseId = block.tool_use_id;
|
||||
const name = toolUseNames[toolUseId] || "unknown";
|
||||
|
||||
@@ -202,7 +210,7 @@ export function claudeToGeminiRequest(model, body, stream, credentials = null) {
|
||||
functionResponse: {
|
||||
...(stripFunctionCallId ? {} : { id: toolUseId }),
|
||||
name,
|
||||
response: { result: content },
|
||||
response: { result: parsedContent },
|
||||
},
|
||||
});
|
||||
break;
|
||||
|
||||
@@ -511,12 +511,18 @@ function openaiToGeminiBase(
|
||||
name = sanitizeToolName(name);
|
||||
|
||||
const resp = toolResponses[fid];
|
||||
let parsedResp = tryParseJSON(resp);
|
||||
if (parsedResp === null) {
|
||||
parsedResp = { result: resp };
|
||||
} else if (typeof parsedResp !== "object") {
|
||||
parsedResp = { result: parsedResp };
|
||||
}
|
||||
|
||||
toolParts.push({
|
||||
functionResponse: {
|
||||
...(toolNameOptions.stripFunctionCallId ? {} : { id: fid }),
|
||||
name: name,
|
||||
response: { result: resp },
|
||||
response: { result: parsedResp },
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -15,7 +15,6 @@ interface ErrorResponseBody {
|
||||
message: string;
|
||||
type?: string;
|
||||
code?: string;
|
||||
reason?: string;
|
||||
};
|
||||
upstream_details?: Record<string, unknown> | null; // sanitized upstream provider body
|
||||
}
|
||||
@@ -109,7 +108,6 @@ export function sanitizeUpstreamDetails(value: unknown, depth = 0): unknown {
|
||||
export type ErrorBodyClassification = {
|
||||
type?: string;
|
||||
code?: string;
|
||||
reason?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -134,7 +132,6 @@ export function buildErrorBody(
|
||||
message: safeMessage,
|
||||
type: classification?.type ?? errorInfo.type,
|
||||
code: classification?.code ?? errorInfo.code,
|
||||
reason: classification?.reason,
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
121
package-lock.json
generated
@@ -86,6 +86,7 @@
|
||||
"undici": "^8.10.0",
|
||||
"update-notifier": "^7.3.1",
|
||||
"uuid": "^14.0.0",
|
||||
"wreq-js": "3.1.0",
|
||||
"ws": "^8.21.3",
|
||||
"xxhash-wasm": "^1.1.0",
|
||||
"yazl": "^3.3.1",
|
||||
@@ -109,7 +110,7 @@
|
||||
"@testing-library/react": "^16.3.2",
|
||||
"@testing-library/user-event": "^14.6.6",
|
||||
"@types/better-sqlite3": "^9.6.0",
|
||||
"@types/bun": "latest",
|
||||
"@types/bun": "*",
|
||||
"@types/node": "^26.2.0",
|
||||
"@types/react": "^19.2.18",
|
||||
"@types/react-dom": "^19.2.4",
|
||||
@@ -160,7 +161,7 @@
|
||||
"better-sqlite3": "^13.0.2",
|
||||
"js-tiktoken": "^1.0.20",
|
||||
"keytar": "^7.9.0",
|
||||
"onnxruntime-node": "1.24.3",
|
||||
"onnxruntime-node": "1.27.0",
|
||||
"sqlite-vec": "^0.1.9",
|
||||
"tls-client-node": "^0.2.0",
|
||||
"wreq-js": "^3.1.0"
|
||||
@@ -15120,14 +15121,6 @@
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/boolean": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/boolean/-/boolean-3.2.0.tgz",
|
||||
"integrity": "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==",
|
||||
"deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.",
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/bottleneck": {
|
||||
"version": "2.19.5",
|
||||
"resolved": "https://registry.npmjs.org/bottleneck/-/bottleneck-2.19.5.tgz",
|
||||
@@ -18213,13 +18206,6 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/detect-node": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz",
|
||||
"integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==",
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/detect-node-es": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz",
|
||||
@@ -19065,13 +19051,6 @@
|
||||
"benchmarks"
|
||||
]
|
||||
},
|
||||
"node_modules/es6-error": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz",
|
||||
"integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==",
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/es6-promisify": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/es6-promisify/-/es6-promisify-7.0.0.tgz",
|
||||
@@ -21878,18 +21857,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/global-agent": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/global-agent/-/global-agent-3.0.0.tgz",
|
||||
"integrity": "sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==",
|
||||
"version": "4.1.3",
|
||||
"resolved": "https://registry.npmjs.org/global-agent/-/global-agent-4.1.3.tgz",
|
||||
"integrity": "sha512-KUJEViiuFT3I97t+GYMikLPJS2Lfo/S2F+DQuBWzuzaMPnvt5yyZePzArx36fBzpGTxZjIpDbXLeySLgh+k76g==",
|
||||
"license": "BSD-3-Clause",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"boolean": "^3.0.1",
|
||||
"es6-error": "^4.1.1",
|
||||
"matcher": "^3.0.0",
|
||||
"roarr": "^2.15.3",
|
||||
"semver": "^7.3.2",
|
||||
"serialize-error": "^7.0.1"
|
||||
"globalthis": "^1.0.2",
|
||||
"matcher": "^4.0.0",
|
||||
"semver": "^7.3.5",
|
||||
"serialize-error": "^8.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.0"
|
||||
@@ -25447,13 +25424,6 @@
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/json-stringify-safe": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz",
|
||||
"integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==",
|
||||
"license": "ISC",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/json5": {
|
||||
"version": "2.2.3",
|
||||
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
|
||||
@@ -27322,9 +27292,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/matcher": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz",
|
||||
"integrity": "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==",
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/matcher/-/matcher-4.0.0.tgz",
|
||||
"integrity": "sha512-S6x5wmcDmsDRRU/c2dkccDwQPXoFczc5+HpQ2lON8pnvHlnvHAHj5WlLVvw6n6vNyHuVugYrFohYxbS+pvFpKQ==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
@@ -27332,6 +27302,9 @@
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/material-symbols": {
|
||||
@@ -30304,16 +30277,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/onnxruntime-common": {
|
||||
"version": "1.24.3",
|
||||
"resolved": "https://registry.npmjs.org/onnxruntime-common/-/onnxruntime-common-1.24.3.tgz",
|
||||
"integrity": "sha512-GeuPZO6U/LBJXvwdaqHbuUmoXiEdeCjWi/EG7Y1HNnDwJYuk6WUbNXpF6luSUY8yASul3cmUlLGrCCL1ZgVXqA==",
|
||||
"version": "1.27.0",
|
||||
"resolved": "https://registry.npmjs.org/onnxruntime-common/-/onnxruntime-common-1.27.0.tgz",
|
||||
"integrity": "sha512-3KxL5wIVqa8Ex08jxSzncm9CMgw8CjOFyOQ7SxvG9o0cVLlhTNKXyIQuTbtX4tGPJEf73OER2xrjt4HJSBL4ow==",
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/onnxruntime-node": {
|
||||
"version": "1.24.3",
|
||||
"resolved": "https://registry.npmjs.org/onnxruntime-node/-/onnxruntime-node-1.24.3.tgz",
|
||||
"integrity": "sha512-JH7+czbc8ALA819vlTgcV+Q214/+VjGeBHDjX81+ZCD0PCVCIFGFNtT0V4sXG/1JXypKPgScQcB3ij/hk3YnTg==",
|
||||
"version": "1.27.0",
|
||||
"resolved": "https://registry.npmjs.org/onnxruntime-node/-/onnxruntime-node-1.27.0.tgz",
|
||||
"integrity": "sha512-QEzGwrvNBgv4uPVdnbHsOGG4G6T96mdlcFI8aAKPjMU8wOPpVocPXb6k3QGkaZagVTv2G9Bnnbo6Z3JdXr1fQw==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
@@ -30324,8 +30297,8 @@
|
||||
],
|
||||
"dependencies": {
|
||||
"adm-zip": "^0.5.16",
|
||||
"global-agent": "^3.0.0",
|
||||
"onnxruntime-common": "1.24.3"
|
||||
"global-agent": "^4.1.3",
|
||||
"onnxruntime-common": "1.27.0"
|
||||
}
|
||||
},
|
||||
"node_modules/onnxruntime-web": {
|
||||
@@ -34002,24 +33975,6 @@
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/roarr": {
|
||||
"version": "2.15.4",
|
||||
"resolved": "https://registry.npmjs.org/roarr/-/roarr-2.15.4.tgz",
|
||||
"integrity": "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==",
|
||||
"license": "BSD-3-Clause",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"boolean": "^3.0.1",
|
||||
"detect-node": "^2.0.4",
|
||||
"globalthis": "^1.0.1",
|
||||
"json-stringify-safe": "^5.0.1",
|
||||
"semver-compare": "^1.0.0",
|
||||
"sprintf-js": "^1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/robot3": {
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmjs.org/robot3/-/robot3-0.4.1.tgz",
|
||||
@@ -34379,13 +34334,6 @@
|
||||
"semver": "bin/semver.js"
|
||||
}
|
||||
},
|
||||
"node_modules/semver-compare": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz",
|
||||
"integrity": "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==",
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/send": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz",
|
||||
@@ -34413,13 +34361,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/serialize-error": {
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-7.0.1.tgz",
|
||||
"integrity": "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==",
|
||||
"version": "8.1.0",
|
||||
"resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-8.1.0.tgz",
|
||||
"integrity": "sha512-3NnuWfM6vBYoy5gZFvHiYsVbafvI9vZv/+jlIigFn4oP4zjNPK3LhcY0xSCgeb1a5L8jO71Mit9LlNoi2UfDDQ==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"type-fest": "^0.13.1"
|
||||
"type-fest": "^0.20.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
@@ -34429,9 +34377,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/serialize-error/node_modules/type-fest": {
|
||||
"version": "0.13.1",
|
||||
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz",
|
||||
"integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==",
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz",
|
||||
"integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==",
|
||||
"license": "(MIT OR CC0-1.0)",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
@@ -35201,13 +35149,6 @@
|
||||
"node": ">= 10.x"
|
||||
}
|
||||
},
|
||||
"node_modules/sprintf-js": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz",
|
||||
"integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==",
|
||||
"license": "BSD-3-Clause",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/sql.js": {
|
||||
"version": "1.14.2",
|
||||
"resolved": "https://registry.npmjs.org/sql.js/-/sql.js-1.14.2.tgz",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "omniroute",
|
||||
"version": "3.8.51",
|
||||
"description": "Unified AI router with 354 providers, RTK+Caveman compression, auto fallback, MCP/A2A, desktop, PWA, and OpenAI-compatible APIs.",
|
||||
"description": "Unified AI router with 353 providers, RTK+Caveman compression, auto fallback, MCP/A2A, desktop, PWA, and OpenAI-compatible APIs.",
|
||||
"type": "module",
|
||||
"bin": {
|
||||
"omniroute": "bin/omniroute.mjs",
|
||||
@@ -252,7 +252,7 @@
|
||||
"postinstall": "node scripts/build/postinstall.mjs",
|
||||
"uninstall": "node scripts/build/uninstall.mjs",
|
||||
"uninstall:full": "node scripts/build/uninstall.mjs --full",
|
||||
"prepare": "node -e \"try{require.resolve('husky')}catch(e){process.exit(0)};require('child_process').execSync('husky',{stdio:'inherit'})\"",
|
||||
"prepare": "husky",
|
||||
"system-info": "node scripts/dev/system-info.mjs",
|
||||
"build:cli-api": "node --import tsx/esm scripts/cli/generate-api-commands.mjs",
|
||||
"postbuild": "node scripts/build/colocate-standalone.mjs",
|
||||
@@ -347,7 +347,7 @@
|
||||
"better-sqlite3": "^13.0.2",
|
||||
"js-tiktoken": "^1.0.20",
|
||||
"keytar": "^7.9.0",
|
||||
"onnxruntime-node": "1.24.3",
|
||||
"onnxruntime-node": "1.27.0",
|
||||
"sqlite-vec": "^0.1.9",
|
||||
"tls-client-node": "^0.2.0",
|
||||
"wreq-js": "^3.1.0"
|
||||
@@ -436,7 +436,7 @@
|
||||
"unrs-resolver": true
|
||||
},
|
||||
"overrides": {
|
||||
"onnxruntime-node": "1.24.3",
|
||||
"onnxruntime-node": "1.27.0",
|
||||
"fast-xml-parser": "^5.10.1",
|
||||
"sharp": "^0.35.3",
|
||||
"postcss": "^8.5.18",
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" role="img" aria-label="Nimble">
|
||||
<rect width="24" height="24" rx="5" fill="#6D28D9" />
|
||||
<path
|
||||
d="M7 17V7h2.1l5.8 6.6V7H17v10h-2.1L9.1 10.4V17H7z"
|
||||
fill="#fff"
|
||||
/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 240 B |
@@ -19,20 +19,12 @@
|
||||
*/
|
||||
|
||||
const KEY = process.env.NVIDIA_API_KEY ?? "";
|
||||
const BASE_URL =
|
||||
process.env.NVIDIA_BASE_URL || "https://integrate.api.nvidia.com/v1/chat/completions";
|
||||
const BASE_URL = process.env.NVIDIA_BASE_URL || "https://integrate.api.nvidia.com/v1/chat/completions";
|
||||
const MODEL = process.env.NVIDIA_MODEL || "openai/gpt-oss-120b";
|
||||
|
||||
// Neutralize CR/LF before logging so env-derived values (NVIDIA_MODEL, etc.)
|
||||
// cannot forge extra log lines (S5145 log injection). Also strip any raw
|
||||
// occurrence of the API key so an upstream error/response that echoes it
|
||||
// back (e.g. inside err.stack or a validation result) never reaches the
|
||||
// terminal in clear text (js/clear-text-logging, CWE-312/532).
|
||||
const line = (s = "") => {
|
||||
let out = String(s).replace(/[\r\n]+/g, " ");
|
||||
if (KEY) out = out.split(KEY).join("[REDACTED]");
|
||||
console.log(out);
|
||||
};
|
||||
// cannot forge extra log lines (S5145 log injection).
|
||||
const line = (s = "") => console.log(String(s).replace(/[\r\n]+/g, " "));
|
||||
const hr = () => line("─".repeat(72));
|
||||
|
||||
function show(label: string, value: unknown) {
|
||||
@@ -60,13 +52,8 @@ async function partA() {
|
||||
});
|
||||
line(" ✅ validateProviderApiKey retornou (sem crash):");
|
||||
show("resultado", result);
|
||||
if (
|
||||
typeof (result as any)?.error === "string" &&
|
||||
(result as any).error.includes("startsWith")
|
||||
) {
|
||||
line(
|
||||
" ⚠️ A mensagem de erro contém 'startsWith' → crash CAPTURADO dentro do try/catch da validação."
|
||||
);
|
||||
if (typeof (result as any)?.error === "string" && (result as any).error.includes("startsWith")) {
|
||||
line(" ⚠️ A mensagem de erro contém 'startsWith' → crash CAPTURADO dentro do try/catch da validação.");
|
||||
}
|
||||
} catch (err: any) {
|
||||
line(" ❌ validateProviderApiKey LANÇOU (crash não tratado):");
|
||||
|
||||
@@ -23,7 +23,7 @@ import {
|
||||
statSync,
|
||||
chmodSync,
|
||||
} from "node:fs";
|
||||
import { join, dirname, relative } from "node:path";
|
||||
import { join, dirname } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
import { assembleStandalone } from "./assembleStandalone.mjs";
|
||||
@@ -35,12 +35,6 @@ import {
|
||||
APP_STAGING_REMOVAL_PATHS,
|
||||
findUnexpectedArtifactPaths,
|
||||
} from "./pack-artifact-policy.ts";
|
||||
import {
|
||||
collectWorkspaceVersions,
|
||||
findPackageJsonFiles,
|
||||
hasWorkspaceProtocol,
|
||||
resolvePackageJsonWorkspaceProtocols,
|
||||
} from "./resolveWorkspaceProtocols.ts";
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
@@ -713,33 +707,6 @@ if (remainingUnexpectedFiles.length > 0) {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// -- Step 11: Resolve workspace: protocol dependencies -----------------
|
||||
// npm/pnpm workspace protocol specifiers (workspace:*, workspace:^, ...)
|
||||
// are meaningless to the npm registry and make `npm install -g omniroute`
|
||||
// fail with EUNSUPPORTEDPROTOCOL. Rewrite any that leaked into published
|
||||
// package.json files to the concrete workspace package version.
|
||||
// Only touch files inside the staged dist/ tree; workspace member source
|
||||
// package.json files must never be mutated by the publish step.
|
||||
const workspaceVersions = collectWorkspaceVersions(ROOT);
|
||||
const publishablePackageJsonDirs = [DIST_DIR];
|
||||
const publishablePackageJsonPaths = publishablePackageJsonDirs
|
||||
.flatMap((dir) => (existsSync(dir) ? findPackageJsonFiles(dir) : []))
|
||||
.filter((filePath) => existsSync(filePath));
|
||||
|
||||
for (const pkgJsonPath of publishablePackageJsonPaths) {
|
||||
let pkg: Record<string, unknown>;
|
||||
try {
|
||||
pkg = JSON.parse(readFileSync(pkgJsonPath, "utf8")) as Record<string, unknown>;
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
if (!hasWorkspaceProtocol(pkg)) continue;
|
||||
|
||||
const resolved = resolvePackageJsonWorkspaceProtocols(pkg, workspaceVersions);
|
||||
writeFileSync(pkgJsonPath, JSON.stringify(resolved, null, 2) + "\n");
|
||||
console.log(` [resolved] Resolved workspace: protocols in ${relative(ROOT, pkgJsonPath)}`);
|
||||
}
|
||||
|
||||
// ── Done ───────────────────────────────────────────────────
|
||||
const distPkg = join(DIST_DIR, "package.json");
|
||||
if (existsSync(distPkg)) {
|
||||
|
||||
@@ -1,228 +0,0 @@
|
||||
/**
|
||||
* Resolve pnpm/npm workspace protocol dependencies to concrete semver versions.
|
||||
*
|
||||
* The npm registry clients cannot parse `workspace:` specifiers. During prepublish
|
||||
* we rewrite any `workspace:*`, `workspace:^`, `workspace:~` (or explicit
|
||||
* `workspace:<range>`) dependency declarations to the matching workspace package's
|
||||
* actual version before npm pack/publish sees them.
|
||||
*/
|
||||
|
||||
import { readFileSync, readdirSync, statSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import * as yaml from "js-yaml";
|
||||
|
||||
const WORKSPACE_PROTOCOL_RE = /^workspace:/;
|
||||
|
||||
const DEPENDENCY_FIELDS = [
|
||||
"dependencies",
|
||||
"devDependencies",
|
||||
"peerDependencies",
|
||||
"optionalDependencies",
|
||||
] as const;
|
||||
|
||||
/**
|
||||
* Parse a simple workspace glob entry into concrete directories relative to a root.
|
||||
* Supports entries like "packages/*" and literal directory names like "open-sse".
|
||||
*/
|
||||
function expandWorkspaceEntry(root: string, entry: string): string[] {
|
||||
const trimmed = entry.trim();
|
||||
if (!trimmed) return [];
|
||||
if (!trimmed.endsWith("/*")) {
|
||||
const dir = join(root, trimmed);
|
||||
try {
|
||||
return statSync(dir).isDirectory() ? [dir] : [];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
const parent = join(root, trimmed.slice(0, -2));
|
||||
let entries: string[] = [];
|
||||
try {
|
||||
entries = readdirSync(parent);
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
return entries
|
||||
.map((name) => join(parent, name))
|
||||
.filter((dir) => {
|
||||
try {
|
||||
return statSync(dir).isDirectory();
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the root package.json and, if present, pnpm-workspace.yaml to discover
|
||||
* workspace member directories. Returns a map of package name -> version.
|
||||
*/
|
||||
export function collectWorkspaceVersions(projectRoot: string): Map<string, string> {
|
||||
const versions = new Map<string, string>();
|
||||
|
||||
const rootPkgPath = join(projectRoot, "package.json");
|
||||
let workspaceEntries: string[] = [];
|
||||
try {
|
||||
const rootPkg = JSON.parse(readFileSync(rootPkgPath, "utf8")) as {
|
||||
workspaces?: string[];
|
||||
};
|
||||
if (Array.isArray(rootPkg.workspaces)) {
|
||||
workspaceEntries.push(...rootPkg.workspaces);
|
||||
}
|
||||
} catch {
|
||||
// ignore unreadable root package.json
|
||||
}
|
||||
|
||||
const pnpmWorkspacePath = join(projectRoot, "pnpm-workspace.yaml");
|
||||
try {
|
||||
const yamlContent = readFileSync(pnpmWorkspacePath, "utf8");
|
||||
const doc = yaml.load(yamlContent) as { packages?: unknown } | null | undefined;
|
||||
if (doc && Array.isArray(doc.packages)) {
|
||||
for (const entry of doc.packages) {
|
||||
if (typeof entry === "string" && entry) {
|
||||
workspaceEntries.push(entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// ignore missing or malformed pnpm-workspace.yaml
|
||||
}
|
||||
|
||||
const seenDirs = new Set<string>();
|
||||
for (const entry of workspaceEntries) {
|
||||
for (const dir of expandWorkspaceEntry(projectRoot, entry)) {
|
||||
if (seenDirs.has(dir)) continue;
|
||||
seenDirs.add(dir);
|
||||
try {
|
||||
const pkg = JSON.parse(readFileSync(join(dir, "package.json"), "utf8")) as {
|
||||
name?: string;
|
||||
version?: string;
|
||||
};
|
||||
if (pkg.name && pkg.version) {
|
||||
versions.set(pkg.name, pkg.version);
|
||||
}
|
||||
} catch {
|
||||
// skip unreadable workspace member package.json
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return versions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve workspace protocol dependencies inside a package.json object.
|
||||
*
|
||||
* Replaces `workspace:*`, `workspace:^`, `workspace:~`, `workspace:<range>`,
|
||||
* and `workspace:<packageName>` with the concrete version of the referenced
|
||||
* workspace package. Throws if a workspace specifier cannot be resolved.
|
||||
*/
|
||||
export function resolvePackageJsonWorkspaceProtocols(
|
||||
pkg: Record<string, unknown>,
|
||||
workspaceVersions: Map<string, string>
|
||||
): Record<string, unknown> {
|
||||
const resolved: Record<string, unknown> = { ...pkg };
|
||||
|
||||
for (const field of DEPENDENCY_FIELDS) {
|
||||
const deps = pkg[field];
|
||||
if (!deps || typeof deps !== "object" || Array.isArray(deps)) continue;
|
||||
|
||||
const resolvedDeps: Record<string, string> = {};
|
||||
let changed = false;
|
||||
for (const [depName, versionSpec] of Object.entries(deps as Record<string, unknown>)) {
|
||||
if (typeof versionSpec !== "string") {
|
||||
resolvedDeps[depName] = String(versionSpec ?? "");
|
||||
continue;
|
||||
}
|
||||
if (!WORKSPACE_PROTOCOL_RE.test(versionSpec)) {
|
||||
resolvedDeps[depName] = versionSpec;
|
||||
continue;
|
||||
}
|
||||
|
||||
const body = versionSpec.slice("workspace:".length);
|
||||
let concrete: string | undefined;
|
||||
|
||||
if (body === "*") {
|
||||
concrete = workspaceVersions.get(depName);
|
||||
} else if (body === "^") {
|
||||
const version = workspaceVersions.get(depName);
|
||||
concrete = version ? `^${version}` : undefined;
|
||||
} else if (body === "~") {
|
||||
const version = workspaceVersions.get(depName);
|
||||
concrete = version ? `~${version}` : undefined;
|
||||
} else if (body.startsWith("^") || body.startsWith("~") || /^[\d<>=]/.test(body)) {
|
||||
// Explicit range inside workspace: protocol - strip the protocol prefix.
|
||||
concrete = body;
|
||||
} else {
|
||||
// workspace:<packageName> - resolve to that package's version.
|
||||
concrete = workspaceVersions.get(body);
|
||||
}
|
||||
|
||||
if (concrete) {
|
||||
resolvedDeps[depName] = concrete;
|
||||
changed = true;
|
||||
} else {
|
||||
throw new Error(
|
||||
`Cannot resolve workspace protocol "${versionSpec}" for dependency "${depName}". ` +
|
||||
"Make sure the referenced package is a declared workspace member with a version."
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (changed) {
|
||||
resolved[field] = resolvedDeps;
|
||||
}
|
||||
}
|
||||
|
||||
return resolved;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if any dependency field in the package contains a workspace: specifier.
|
||||
*/
|
||||
export function hasWorkspaceProtocol(pkg: Record<string, unknown>): boolean {
|
||||
for (const field of DEPENDENCY_FIELDS) {
|
||||
const deps = pkg[field];
|
||||
if (!deps || typeof deps !== "object" || Array.isArray(deps)) continue;
|
||||
for (const versionSpec of Object.values(deps as Record<string, unknown>)) {
|
||||
if (typeof versionSpec === "string" && WORKSPACE_PROTOCOL_RE.test(versionSpec)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively walk a directory and return every package.json path found.
|
||||
* Stops descending after maxDepth to avoid runaway recursion on deep trees.
|
||||
*/
|
||||
export function findPackageJsonFiles(dir: string, maxDepth = 10): string[] {
|
||||
const results: string[] = [];
|
||||
if (maxDepth < 0) return results;
|
||||
|
||||
let entries: string[] = [];
|
||||
try {
|
||||
entries = readdirSync(dir);
|
||||
} catch {
|
||||
return results;
|
||||
}
|
||||
|
||||
for (const entry of entries) {
|
||||
if (entry === "node_modules") continue;
|
||||
const fullPath = join(dir, entry);
|
||||
let stat;
|
||||
try {
|
||||
stat = statSync(fullPath);
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
if (stat.isDirectory()) {
|
||||
results.push(...findPackageJsonFiles(fullPath, maxDepth - 1));
|
||||
} else if (entry === "package.json") {
|
||||
results.push(fullPath);
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
@@ -22,52 +22,6 @@ interface SearchStats {
|
||||
avgDurationMs: number;
|
||||
}
|
||||
|
||||
function isFiniteNumber(value: unknown): value is number {
|
||||
return typeof value === "number" && Number.isFinite(value);
|
||||
}
|
||||
|
||||
function isSearchStats(value: unknown): value is SearchStats {
|
||||
if (!value || typeof value !== "object") return false;
|
||||
|
||||
const candidate = value as Partial<SearchStats>;
|
||||
return (
|
||||
isFiniteNumber(candidate.total) &&
|
||||
isFiniteNumber(candidate.today) &&
|
||||
isFiniteNumber(candidate.cached) &&
|
||||
isFiniteNumber(candidate.errors) &&
|
||||
isFiniteNumber(candidate.totalCostUsd) &&
|
||||
isFiniteNumber(candidate.cacheHitRate) &&
|
||||
isFiniteNumber(candidate.avgDurationMs) &&
|
||||
!!candidate.byProvider &&
|
||||
typeof candidate.byProvider === "object" &&
|
||||
!Array.isArray(candidate.byProvider) &&
|
||||
Object.values(candidate.byProvider).every(
|
||||
(provider) =>
|
||||
!!provider &&
|
||||
typeof provider === "object" &&
|
||||
isFiniteNumber(provider.count) &&
|
||||
isFiniteNumber(provider.costUsd)
|
||||
) &&
|
||||
Array.isArray(candidate.last24h) &&
|
||||
candidate.last24h.every(
|
||||
(point) => typeof point.hour === "string" && isFiniteNumber(point.count)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
async function readSearchStats(response: Response): Promise<SearchStats> {
|
||||
const body: unknown = await response.json().catch(() => null);
|
||||
if (!response.ok) {
|
||||
const message =
|
||||
body && typeof body === "object" && "error" in body && typeof body.error === "string"
|
||||
? body.error
|
||||
: null;
|
||||
throw new Error(message ?? "searchAnalyticsNoData");
|
||||
}
|
||||
if (!isSearchStats(body)) throw new Error("searchAnalyticsNoData");
|
||||
return body;
|
||||
}
|
||||
|
||||
function StatCard({
|
||||
icon,
|
||||
label,
|
||||
@@ -131,25 +85,16 @@ export default function SearchAnalyticsTab() {
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
const controller = new AbortController();
|
||||
async function loadStats() {
|
||||
try {
|
||||
const response = await fetch("/api/v1/search/analytics", { signal: controller.signal });
|
||||
const nextStats = await readSearchStats(response);
|
||||
if (!cancelled) setStats(nextStats);
|
||||
} catch (cause) {
|
||||
const message = cause instanceof Error ? cause.message : "searchAnalyticsNoData";
|
||||
if (!cancelled) setError(message);
|
||||
} finally {
|
||||
if (!cancelled) setLoading(false);
|
||||
}
|
||||
}
|
||||
void loadStats();
|
||||
return () => {
|
||||
cancelled = true;
|
||||
controller.abort();
|
||||
};
|
||||
fetch("/api/v1/search/analytics")
|
||||
.then((r) => r.json())
|
||||
.then((d) => {
|
||||
setStats(d);
|
||||
setLoading(false);
|
||||
})
|
||||
.catch((e) => {
|
||||
setError(e.message);
|
||||
setLoading(false);
|
||||
});
|
||||
}, []);
|
||||
|
||||
if (loading) {
|
||||
|
||||
@@ -249,23 +249,21 @@ export default function ProfilePage() {
|
||||
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-4">
|
||||
{allBadges.map((badge) => {
|
||||
const isEarned = earnedIds.has(badge.id);
|
||||
const isHiddenAndLocked = Boolean(badge.hidden) && !isEarned;
|
||||
const earnedInfo = earnedBadges.find((b) => b.badgeId === badge.id);
|
||||
const rarityColor = RARITY_COLORS[badge.rarity] || RARITY_COLORS.common;
|
||||
|
||||
return (
|
||||
<button
|
||||
key={badge.id}
|
||||
onClick={() => !isHiddenAndLocked && setSelectedBadge(badge)}
|
||||
disabled={isHiddenAndLocked}
|
||||
onClick={() => setSelectedBadge(badge)}
|
||||
className={`relative p-4 rounded-xl border transition-all text-left ${
|
||||
isEarned
|
||||
? `${rarityColor} bg-surface hover:shadow-md`
|
||||
: "border-border/50 bg-surface/50 opacity-50 grayscale enabled:hover:opacity-70 disabled:cursor-default"
|
||||
: "border-border/50 bg-surface/50 opacity-50 grayscale hover:opacity-70"
|
||||
}`}
|
||||
>
|
||||
<div className="text-3xl mb-2">
|
||||
<BadgeIcon icon={isHiddenAndLocked ? null : badge.icon} earned={isEarned} />
|
||||
<BadgeIcon icon={badge.icon} earned={isEarned} />
|
||||
</div>
|
||||
<p className="font-semibold text-sm truncate">
|
||||
{badge.hidden && !isEarned ? "???" : translateBadge(badge, "name")}
|
||||
|
||||
@@ -1,247 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { Button, Input, Select, Toggle } from "@/shared/components";
|
||||
import {
|
||||
PEAK_HOUR_PROTECTION_DAYS,
|
||||
type PeakHourProtectionConfig,
|
||||
type PeakHourProtectionDay,
|
||||
type PeakHourProtectionMode,
|
||||
type PeakHourWindow,
|
||||
} from "@/lib/providers/peakHourProtection";
|
||||
import { providerText, type ProviderMessageTranslator } from "../providerPageHelpers";
|
||||
|
||||
export const EMPTY_PEAK_HOUR_PROTECTION: PeakHourProtectionConfig = {
|
||||
enabled: false,
|
||||
mode: "block",
|
||||
windows: [],
|
||||
};
|
||||
|
||||
const DAY_LABELS: Record<PeakHourProtectionDay, string> = {
|
||||
mon: "Mon",
|
||||
tue: "Tue",
|
||||
wed: "Wed",
|
||||
thu: "Thu",
|
||||
fri: "Fri",
|
||||
sat: "Sat",
|
||||
sun: "Sun",
|
||||
};
|
||||
|
||||
function cloneConfig(value: unknown): PeakHourProtectionConfig {
|
||||
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
||||
return { ...EMPTY_PEAK_HOUR_PROTECTION, windows: [] };
|
||||
}
|
||||
const record = value as Record<string, unknown>;
|
||||
const mode: PeakHourProtectionMode = record.mode === "avoid" ? "avoid" : "block";
|
||||
const windows = Array.isArray(record.windows)
|
||||
? record.windows
|
||||
.filter((entry): entry is PeakHourWindow => !!entry && typeof entry === "object")
|
||||
.map((entry) => ({
|
||||
id: typeof entry.id === "string" ? entry.id : crypto.randomUUID(),
|
||||
name: typeof entry.name === "string" ? entry.name : "",
|
||||
days: Array.isArray(entry.days)
|
||||
? entry.days.filter((day): day is PeakHourProtectionDay =>
|
||||
(PEAK_HOUR_PROTECTION_DAYS as readonly string[]).includes(day)
|
||||
)
|
||||
: [],
|
||||
startUtc: typeof entry.startUtc === "string" ? entry.startUtc : "06:00",
|
||||
endUtc: typeof entry.endUtc === "string" ? entry.endUtc : "10:00",
|
||||
}))
|
||||
: [];
|
||||
return { enabled: record.enabled === true, mode, windows };
|
||||
}
|
||||
|
||||
function newWindow(): PeakHourWindow {
|
||||
return { id: crypto.randomUUID(), name: "", days: [], startUtc: "06:00", endUtc: "10:00" };
|
||||
}
|
||||
|
||||
function weekdayWindow(startUtc: string, endUtc: string): PeakHourWindow {
|
||||
return {
|
||||
id: crypto.randomUUID(),
|
||||
name: "Weekday peak",
|
||||
days: ["mon", "tue", "wed", "thu", "fri"],
|
||||
startUtc,
|
||||
endUtc,
|
||||
};
|
||||
}
|
||||
|
||||
export function normalizePeakHourProtectionForSave(
|
||||
value: PeakHourProtectionConfig
|
||||
): PeakHourProtectionConfig | null {
|
||||
const windows = value.windows
|
||||
.map((window) => ({
|
||||
...(window.name?.trim() ? { name: window.name.trim() } : {}),
|
||||
...(window.days && window.days.length > 0 ? { days: window.days } : {}),
|
||||
startUtc: window.startUtc,
|
||||
endUtc: window.endUtc,
|
||||
}))
|
||||
.filter(
|
||||
(window) => /^\d{2}:\d{2}$/.test(window.startUtc) && /^\d{2}:\d{2}$/.test(window.endUtc)
|
||||
);
|
||||
if (!value.enabled && windows.length === 0) return null;
|
||||
return { enabled: value.enabled, mode: value.mode, windows };
|
||||
}
|
||||
|
||||
export function formatPeakHourSummary(value: unknown): string | null {
|
||||
const config = cloneConfig(value);
|
||||
if (!config.enabled || config.windows.length === 0) return null;
|
||||
const mode = config.mode === "avoid" ? "Avoid" : "Block";
|
||||
return `${mode} during ${config.windows.length} peak window${config.windows.length === 1 ? "" : "s"}`;
|
||||
}
|
||||
|
||||
export default function PeakHourProtectionEditor({
|
||||
value,
|
||||
onChange,
|
||||
t,
|
||||
}: {
|
||||
value: PeakHourProtectionConfig;
|
||||
onChange: (next: PeakHourProtectionConfig) => void;
|
||||
t: ProviderMessageTranslator;
|
||||
}) {
|
||||
const updateWindow = (id: string | undefined, patch: Partial<PeakHourWindow>) => {
|
||||
onChange({
|
||||
...value,
|
||||
windows: value.windows.map((window) => (window.id === id ? { ...window, ...patch } : window)),
|
||||
});
|
||||
};
|
||||
|
||||
const toggleDay = (window: PeakHourWindow, day: PeakHourProtectionDay) => {
|
||||
const days = new Set(window.days || []);
|
||||
if (days.has(day)) days.delete(day);
|
||||
else days.add(day);
|
||||
updateWindow(window.id, { days: Array.from(days) });
|
||||
};
|
||||
|
||||
const applyPreset = (provider: "deepseek" | "zai") => {
|
||||
const windows =
|
||||
provider === "deepseek"
|
||||
? [weekdayWindow("01:00", "04:00"), weekdayWindow("06:00", "10:00")]
|
||||
: [{ ...newWindow(), name: "Daily peak", startUtc: "06:00", endUtc: "10:00" }];
|
||||
onChange({ enabled: true, mode: value.mode, windows });
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4 rounded-lg border border-amber-500/30 bg-amber-500/5 p-4">
|
||||
<Toggle
|
||||
checked={value.enabled}
|
||||
onChange={(enabled) => onChange({ ...value, enabled })}
|
||||
label={providerText(t, "peakHourProtectionLabel", "Peak-hour protection")}
|
||||
description={providerText(
|
||||
t,
|
||||
"peakHourProtectionDescription",
|
||||
"Block this connection during configured UTC peak-hour windows. This avoids uncertain peak multipliers instead of trying to price them."
|
||||
)}
|
||||
/>
|
||||
<Select
|
||||
label={providerText(t, "peakHourProtectionModeLabel", "Protection mode")}
|
||||
value={value.mode}
|
||||
options={[
|
||||
{
|
||||
value: "block",
|
||||
label: providerText(t, "peakHourProtectionModeBlock", "Block requests"),
|
||||
},
|
||||
{
|
||||
value: "avoid",
|
||||
label: providerText(t, "peakHourProtectionModeAvoid", "Avoid in routing"),
|
||||
},
|
||||
]}
|
||||
onChange={(event) =>
|
||||
onChange({ ...value, mode: event.target.value === "avoid" ? "avoid" : "block" })
|
||||
}
|
||||
hint={providerText(
|
||||
t,
|
||||
"peakHourProtectionModeHint",
|
||||
"Direct requests fail while active; combo/auto routing skips protected connections when alternatives exist."
|
||||
)}
|
||||
/>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Button size="sm" variant="secondary" onClick={() => applyPreset("deepseek")}>
|
||||
{providerText(t, "peakHourDeepSeekPreset", "Use DeepSeek preset")}
|
||||
</Button>
|
||||
<Button size="sm" variant="secondary" onClick={() => applyPreset("zai")}>
|
||||
{providerText(t, "peakHourZaiPreset", "Use Z.ai preset")}
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
icon="add"
|
||||
onClick={() => onChange({ ...value, windows: [...value.windows, newWindow()] })}
|
||||
>
|
||||
{providerText(t, "peakHourAddWindow", "Add window")}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex flex-col gap-3">
|
||||
{value.windows.length === 0 ? (
|
||||
<p className="text-xs text-text-muted">
|
||||
{providerText(t, "peakHourNoWindows", "No peak-hour windows configured.")}
|
||||
</p>
|
||||
) : (
|
||||
value.windows.map((window) => (
|
||||
<div key={window.id} className="rounded-lg border border-border/70 bg-surface/50 p-3">
|
||||
<div className="mb-3 flex items-center justify-between gap-2">
|
||||
<Input
|
||||
label={providerText(t, "peakHourWindowName", "Window name")}
|
||||
value={window.name || ""}
|
||||
onChange={(event) => updateWindow(window.id, { name: event.target.value })}
|
||||
placeholder={providerText(
|
||||
t,
|
||||
"peakHourWindowNamePlaceholder",
|
||||
"e.g. weekday peak"
|
||||
)}
|
||||
/>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
icon="delete"
|
||||
onClick={() =>
|
||||
onChange({
|
||||
...value,
|
||||
windows: value.windows.filter((entry) => entry.id !== window.id),
|
||||
})
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
|
||||
<Input
|
||||
label={providerText(t, "peakHourStartUtc", "Start UTC")}
|
||||
type="time"
|
||||
value={window.startUtc}
|
||||
onChange={(event) => updateWindow(window.id, { startUtc: event.target.value })}
|
||||
/>
|
||||
<Input
|
||||
label={providerText(t, "peakHourEndUtc", "End UTC")}
|
||||
type="time"
|
||||
value={window.endUtc}
|
||||
onChange={(event) => updateWindow(window.id, { endUtc: event.target.value })}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-3">
|
||||
<p className="mb-2 text-xs font-medium text-text-muted">
|
||||
{providerText(t, "peakHourDays", "Days (empty = every day)")}
|
||||
</p>
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{PEAK_HOUR_PROTECTION_DAYS.map((day) => {
|
||||
const active = (window.days || []).includes(day);
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
key={day}
|
||||
onClick={() => toggleDay(window, day)}
|
||||
className={`rounded-full px-2 py-1 text-xs font-medium transition-colors ${
|
||||
active
|
||||
? "bg-amber-500 text-white"
|
||||
: "bg-muted/60 text-text-muted hover:bg-muted"
|
||||
}`}
|
||||
>
|
||||
{DAY_LABELS[day]}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -59,12 +59,6 @@ import AgentrouterConsoleFields from "./AgentrouterConsoleFields";
|
||||
import QuotaScrapingFields, { EMPTY_QUOTA_SCRAPING_FIELDS } from "./QuotaScrapingFields";
|
||||
import GlmTeamQuotaFields, { EMPTY_GLM_TEAM_QUOTA_FIELDS } from "./GlmTeamQuotaFields";
|
||||
import ProviderRegionField, { getProviderRegionConfig } from "./AlibabaProviderRegionField";
|
||||
import PeakHourProtectionEditor, {
|
||||
EMPTY_PEAK_HOUR_PROTECTION,
|
||||
formatPeakHourSummary,
|
||||
normalizePeakHourProtectionForSave,
|
||||
} from "../PeakHourProtectionEditor";
|
||||
import type { PeakHourProtectionConfig } from "@/lib/providers/peakHourProtection";
|
||||
export interface EditConnectionModalConnection {
|
||||
id?: string;
|
||||
name?: string;
|
||||
@@ -160,7 +154,6 @@ export default function EditConnectionModal({
|
||||
runtimeKey: "",
|
||||
connectorName: stringField(connectionProviderSpecificData?.connectorName) || "OmniRoute Codex",
|
||||
m365Tier: normalizeM365TierValue(connectionProviderSpecificData?.tier) as M365TierValue,
|
||||
peakHourProtection: { ...EMPTY_PEAK_HOUR_PROTECTION, windows: [] } as PeakHourProtectionConfig,
|
||||
});
|
||||
const [testing, setTesting] = useState(false);
|
||||
const [testResult, setTestResult] = useState(null);
|
||||
@@ -398,22 +391,6 @@ export default function EditConnectionModal({
|
||||
connectorName:
|
||||
stringField(connection.providerSpecificData?.connectorName) || "OmniRoute Codex",
|
||||
m365Tier: normalizeM365TierValue(connection.providerSpecificData?.tier) as M365TierValue,
|
||||
peakHourProtection: {
|
||||
...EMPTY_PEAK_HOUR_PROTECTION,
|
||||
...((connection.providerSpecificData?.peakHourProtection as PeakHourProtectionConfig) ||
|
||||
{}),
|
||||
windows: Array.isArray(
|
||||
(
|
||||
connection.providerSpecificData?.peakHourProtection as
|
||||
PeakHourProtectionConfig | undefined
|
||||
)?.windows
|
||||
)
|
||||
? [
|
||||
...(connection.providerSpecificData?.peakHourProtection as PeakHourProtectionConfig)
|
||||
.windows,
|
||||
]
|
||||
: [],
|
||||
},
|
||||
});
|
||||
const existing = connection.providerSpecificData?.extraApiKeys;
|
||||
setExtraApiKeys(Array.isArray(existing) ? existing : []);
|
||||
@@ -722,9 +699,6 @@ export default function EditConnectionModal({
|
||||
}
|
||||
if (updates.providerSpecificData) {
|
||||
updates.providerSpecificData.disableCooling = formData.disableCooling ? true : undefined;
|
||||
updates.providerSpecificData.peakHourProtection = normalizePeakHourProtectionForSave(
|
||||
formData.peakHourProtection
|
||||
);
|
||||
// Explicit `null`, not `undefined`: the PUT route merges
|
||||
// { ...existing, ...incoming }, so omitting the key would keep the previous
|
||||
// choice and switching back to the default would never take effect.
|
||||
@@ -870,16 +844,6 @@ export default function EditConnectionModal({
|
||||
label={t("disableCoolingLabel")}
|
||||
description={t("disableCoolingDescription")}
|
||||
/>
|
||||
<PeakHourProtectionEditor
|
||||
value={formData.peakHourProtection}
|
||||
onChange={(peakHourProtection) => setFormData({ ...formData, peakHourProtection })}
|
||||
t={t}
|
||||
/>
|
||||
{formatPeakHourSummary(formData.peakHourProtection) && (
|
||||
<p className="text-xs text-text-muted">
|
||||
{formatPeakHourSummary(formData.peakHourProtection)}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<QuotaScrapingFields
|
||||
provider={provider}
|
||||
|
||||
@@ -2,7 +2,6 @@ import { NextResponse } from "next/server";
|
||||
import { getCachedSettings, updateSettings } from "@/lib/localDb";
|
||||
import { SignJWT, jwtVerify, createRemoteJWKSet } from "jose";
|
||||
import { cookies } from "next/headers";
|
||||
import { timingSafeCompare } from "@/shared/utils/timingSafeCompare";
|
||||
// Test seam (static) — allows tests to inject a cookie store and capture the minted auth_token.
|
||||
// Mirrors the pattern in src/app/api/auth/login/route.ts
|
||||
export const oidcCallbackInternals = {
|
||||
@@ -55,10 +54,7 @@ export async function GET(request: Request) {
|
||||
// Validate state from cookie (via seam so tests can capture)
|
||||
const cookieStore = await oidcCallbackInternals.getCookieStore();
|
||||
const storedState = cookieStore.get("oidc_state")?.value;
|
||||
// Constant-time: `!==` short-circuits on the first differing byte, so
|
||||
// rejection time correlates with matching-prefix length (GHSA-7434-6q4c-33fh).
|
||||
// The sibling OAuth callback already compares `state` this way.
|
||||
if (!storedState || !timingSafeCompare(storedState, returnedState)) {
|
||||
if (!storedState || storedState !== returnedState) {
|
||||
return NextResponse.redirect(new URL("/login?oidc_error=invalid_state", originEarly));
|
||||
}
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ import {
|
||||
import { providerAllowsOptionalApiKey } from "@/shared/constants/providers";
|
||||
import { shouldUseApiKeyConnectionTest } from "./webSessionTestDispatch";
|
||||
import { testCodexAppServerConnection, makeDiagnosis } from "./codexAppServerHealth";
|
||||
import { recoverKeyHealth } from "@omniroute/open-sse/services/apiKeyRotator.ts";
|
||||
import { removeConnectionHealth } from "@omniroute/open-sse/services/apiKeyRotator.ts";
|
||||
import { shouldClearErrorStateOnValidProbe } from "@/lib/usage/providerLimits";
|
||||
import { isConnectionUnavailableToAuxiliaryActivity } from "@/lib/exclusiveLeaseIsolation";
|
||||
import { classifyAmbiguousOrAuthError, type ClassifyFailureArgs } from "./mistralAmbiguousAuth";
|
||||
@@ -1125,11 +1125,7 @@ export async function testSingleConnection(connectionId: string, validationModel
|
||||
lastError: clearErrorState ? null : result.valid ? connection.lastError : result.error,
|
||||
lastErrorAt: clearErrorState ? null : result.valid ? connection.lastErrorAt : now,
|
||||
lastTested: now,
|
||||
lastErrorType: clearErrorState
|
||||
? null
|
||||
: result.valid
|
||||
? connection.lastErrorType
|
||||
: diagnosis.type,
|
||||
lastErrorType: clearErrorState ? null : result.valid ? connection.lastErrorType : diagnosis.type,
|
||||
lastErrorSource: clearErrorState
|
||||
? null
|
||||
: result.valid
|
||||
@@ -1151,11 +1147,16 @@ export async function testSingleConnection(connectionId: string, validationModel
|
||||
|
||||
if (clearErrorState) {
|
||||
updateData.backoffLevel = 0;
|
||||
}
|
||||
|
||||
if (result.valid && (connection.apiKey || connection.accessToken)) {
|
||||
const recovered = recoverKeyHealth(connectionId, "primary", connection.providerSpecificData);
|
||||
if (recovered) updateData.providerSpecificData = recovered;
|
||||
const psd = connection?.providerSpecificData as Record<string, unknown> | undefined;
|
||||
updateData.providerSpecificData = {
|
||||
...(psd || {}),
|
||||
apiKeyHealth: {},
|
||||
};
|
||||
|
||||
try {
|
||||
removeConnectionHealth(connectionId);
|
||||
} catch {}
|
||||
}
|
||||
|
||||
// If token was refreshed, update tokens in DB
|
||||
|
||||
@@ -54,13 +54,6 @@ const FETCH_PROVIDERS: FetchProviderDef[] = [
|
||||
freeMonthlyQuota: 0,
|
||||
fetchFormats: ["markdown", "html"],
|
||||
},
|
||||
{
|
||||
id: "nimble-search",
|
||||
name: "Nimble Extract",
|
||||
costPerQuery: 0.005,
|
||||
freeMonthlyQuota: 0,
|
||||
fetchFormats: ["markdown", "html", "links", "screenshot"],
|
||||
},
|
||||
];
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||