feat(providers): add MaxAI — signed OpenAI-compatible provider (chat, tools, vision, image-gen, doc-RAG) (#11461)
MaxAI joins as a first-class signed provider: 13 chat models discovered live from /models/get_config plus 6 image models, routed through the standard /v1 endpoints with per-request X-Authorization signing, browserless onboarding, prompted tool-calling, vision input, image generation and document RAG. Reconciled on merge — worth reading, because the branch forked 227 commits back and 77 files conflicted. Only five carried MaxAI content; the rest was drift from the older release line and took the tip's side, taking the diff from 113 files to 37 (then 93 as counted against the current base). - executors/index.ts: the tip has since refactored the executor map to lazy dynamic imports, so MaxAI is registered in that shape rather than the branch's static import. - imageRegistry.ts: kept only the maxai block. The branch still carried microsoft-designer-web, which #11754 retired. - models/route.ts: the conflicting hunk was an unrelated Vertex/Anthropic URL change, not MaxAI — tip's side. - volcengine agent-plan/coding-plan registries: git auto-merged both sides and produced a duplicated supportsVision key, which TypeScript rejects (TS1117). Removed. One real integration break that only the combined state shows: the MaxAI entry declared no serviceKinds, which #11392 made required a few hours ago. Provider validation threw at load time and check:provider-consistency crashed outright. Declared ["llm"] — the image kinds derive from imageRegistry, per the convention in that PR's backfill. Every count was measured rather than taken from the branch, and each would have been wrong: reserved prefixes are 402, not the 397 the branch computed from its stale 395 base; providers are 353, not 354. PROVIDER_REFERENCE.md regenerated, the count updated across README/AGENTS.md/llm.txt and its 42 mirrors, package.json and 6 SVGs — every changed line in those files is a digit substitution and nothing else, verified by masking digits and comparing the removed and added sets (90 lines, identical). The executor-map golden snapshot was regenerated: keyCount 133 -> 134. The branch's file-size-baseline.json predates #12411's ratchet re-tightening, so it was discarded rather than merged — taking it would have silently undone that. The three files this PR grows (proxyFetch.ts +20 for the Windows/firefox_150 TLS profile, imageGeneration.ts +12, models/route.ts +48) were entered against the current baseline under one _rebaseline annotation; no other cap moves. Verified: typecheck:core clean, check:provider-consistency OK (269 REGISTRY entries, 353 canonical providers), check:docs-counts exit 0, check-file-size OK, check:cycles OK, and 79/79 across the MaxAI suites plus 21/21 reserved-prefix and 2/2 executor-map-golden. Thanks @arminanton — the provider work itself is thorough; it was the 227 commits of base that needed the attention.
@@ -46,7 +46,7 @@ Repository map and Reference Documentation sections below.
|
||||
|
||||
## Project at a Glance
|
||||
|
||||
**OmniRoute** — unified AI proxy/router. One endpoint, 352 LLM providers, auto-fallback.
|
||||
**OmniRoute** — unified AI proxy/router. One endpoint, 353 LLM providers, auto-fallback.
|
||||
|
||||
| Layer | Location | Purpose |
|
||||
| ------------- | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
# 🚀 OmniRoute — The Free AI Gateway
|
||||
|
||||
<img src="./docs/diagrams/readme-hero.svg" width="100%" alt="OmniRoute — Never stop coding. Every AI tool → 352 providers — 150+ free — through one endpoint. Claude Code, Codex, Cursor, Cline, Copilot & Antigravity into FREE Claude / GPT / Gemini with auto-fallback. RTK + Caveman stacked compression saves 15–95% tokens (~89% avg) — never hit limits. 352 AI providers · 150+ free tiers · ~1.51B free tokens/mo · 19 routing strategies · $0 to start."/>
|
||||
<img src="./docs/diagrams/readme-hero.svg" width="100%" alt="OmniRoute — Never stop coding. Every AI tool → 353 providers — 150+ free — through one endpoint. Claude Code, Codex, Cursor, Cline, Copilot & Antigravity into FREE Claude / GPT / Gemini with auto-fallback. RTK + Caveman stacked compression saves 15–95% tokens (~89% avg) — never hit limits. 353 AI providers · 150+ free tiers · ~1.51B free tokens/mo · 19 routing strategies · $0 to start."/>
|
||||
|
||||
</div>
|
||||
|
||||
@@ -210,7 +210,7 @@ curl http://localhost:20128/v1/chat/completions \
|
||||
|
||||
</div>
|
||||
|
||||
<img src="./docs/diagrams/promise-pillars.svg" width="100%" alt="The Promise — One endpoint and 352 providers. Automatic fallback keeps routing while another healthy target is available. Six pillars: resilient fallback across 352 providers · up to 95% token savings on eligible workloads · $0 to start with 150+ free tiers and 53 recurring/keyless free-forever providers · 36 CLI/agent integrations through one config · OpenAI, Claude, Gemini and Responses API compatibility at /v1 · production controls including circuit breakers, TLS stealth, MCP 110 tools, A2A, memory, guardrails, evals and 39,000+ static test declarations across 5,100+ tracked test files."/>
|
||||
<img src="./docs/diagrams/promise-pillars.svg" width="100%" alt="The Promise — One endpoint and 353 providers. Automatic fallback keeps routing while another healthy target is available. Six pillars: resilient fallback across 353 providers · up to 95% token savings on eligible workloads · $0 to start with 150+ free tiers and 53 recurring/keyless free-forever providers · 36 CLI/agent integrations through one config · OpenAI, Claude, Gemini and Responses API compatibility at /v1 · production controls including circuit breakers, TLS stealth, MCP 110 tools, A2A, memory, guardrails, evals and 39,000+ static test declarations across 5,100+ tracked test files."/>
|
||||
|
||||
<br/>
|
||||
<br/>
|
||||
@@ -463,7 +463,7 @@ All **19** strategies — mix & match per combo step:
|
||||
|
||||
</div>
|
||||
|
||||
<img src="./docs/diagrams/comparison-table.svg" width="100%" alt="What sets OmniRoute apart — a dated feature snapshot vs 9router, OpenRouter, CLIProxyAPI and LiteLLM across 13 capabilities. OmniRoute: 352 providers, 150+ free tiers built in, 19 routing strategies, 12-engine token compression, built-in MCP server with 110 tools, A2A agent protocol, persistent memory, guardrails, cloud agents, TLS fingerprint stealth, Desktop/Termux/PWA and 43 i18n UI locales. OmniRoute is MIT-licensed and self-hostable. Competitor capabilities and counts may change; see the linked methodology."/>
|
||||
<img src="./docs/diagrams/comparison-table.svg" width="100%" alt="What sets OmniRoute apart — a dated feature snapshot vs 9router, OpenRouter, CLIProxyAPI and LiteLLM across 13 capabilities. OmniRoute: 353 providers, 150+ free tiers built in, 19 routing strategies, 12-engine token compression, built-in MCP server with 110 tools, A2A agent protocol, persistent memory, guardrails, cloud agents, TLS fingerprint stealth, Desktop/Termux/PWA and 43 i18n UI locales. OmniRoute is MIT-licensed and self-hostable. Competitor capabilities and counts may change; see the linked methodology."/>
|
||||
|
||||
<sub>📊 Full methodology & per-feature detail vs 9router, OpenRouter, CLIProxyAPI & LiteLLM → [`docs/comparison/OMNIROUTE_VS_ALTERNATIVES.md`](docs/comparison/OMNIROUTE_VS_ALTERNATIVES.md)</sub>
|
||||
|
||||
|
||||
6
changelog.d/features/maxai-provider.md
Normal file
@@ -0,0 +1,6 @@
|
||||
- **feat(providers):** add MaxAI as a signed, OpenAI-compatible provider serving its 13 paid chat models (GPT-5.6 / Luna / Thinking, Claude 5 Sonnet, Claude Haiku 4.5, Gemini 3.1 Pro / Flash-Lite, Grok 4.1-fast / 4.5, DeepSeek V3.2 / R1, Llama 3.3 70B) through OmniRoute's `/v1` endpoint, with per-request HMAC-SHA1→SM3→AES request signing, live model + context-window discovery from `/models/get_config`, and prompted tool-calling translated to OpenAI `tool_calls`
|
||||
- **feat(providers):** MaxAI vision input — image_url content parts are forwarded inline in `message_content` to the 6 vision-capable models (GPT-5.6 / Luna / Thinking, Claude Haiku 4.5, Gemini 3.1 Pro / Flash-Lite)
|
||||
- **feat(providers):** MaxAI image generation — 6 image models (gpt-image-1, dall-e-3, flux-1-schnell/dev/pro, sd3-medium) exposed through `POST /v1/images/generations`
|
||||
- **feat(providers):** MaxAI document RAG — inline base64 file/document attachments are uploaded to MaxAI (content-addressed `doc_id`) and attached to the chat via `doc_list`
|
||||
- **feat(providers):** browserless MaxAI onboarding — email device-pair login (`/api/providers/[id]/login`) and signed access-token refresh, so a connection can be created and kept fresh without a real browser or Google OAuth
|
||||
- **feat(providers):** per-provider TLS impersonation profile (MaxAI presents a Windows Firefox-150 client fingerprint) so its bot-sensitive endpoints accept OmniRoute traffic
|
||||
@@ -3373,7 +3373,7 @@
|
||||
},
|
||||
"tests/unit/combo-routing-engine.test.ts": {
|
||||
"@typescript-eslint/no-explicit-any": {
|
||||
"count": 267
|
||||
"count": 268
|
||||
}
|
||||
},
|
||||
"tests/unit/combo-same-provider-cascade.test.ts": {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"_rebaseline_2026_09_02_11461_maxai_tls_profile": "PR #11461 (arminanton, feat/maxai-provider) own growth, three files at existing per-provider chokepoints: open-sse/utils/proxyFetch.ts 1241->1261 (+20, the TLS_PROVIDER_PROFILE map giving MaxAI a Windows/firefox_150 impersonation profile instead of the tlsClient chrome_124/macos default); open-sse/handlers/imageGeneration.ts 3231->3243 (+12, the maxai-image format branch); src/app/api/providers/[id]/models/route.ts 2381->2429 (+48, live model listing via maxaiModels). Additive data, same no-split rationale as _rebaseline_2026_08_20_10531_freebuff_provider.",
|
||||
"_rebaseline_2026_09_02_11460_flat_rate_estimates": "PR #11460 (xiaoyaner0201, fix/11459-cc-cost-estimates) own growth: src/app/(dashboard)/dashboard/costs/CostOverviewTab.tsx 1283->1319 (+36) — the flat-rate estimate labelling and the includeFlatRateEstimates opt-in on the Costs dashboard. #11460 merged first so this ratchet re-tightening measures the real post-merge LOC; the cap still drops 2002->1319 (-683) versus the 2026-08-10 +30% loosening this PR reverses. Same own-growth rationale as _rebaseline_2026_08_20_10531_freebuff_provider.",
|
||||
"_rebaseline_2026_08_31_chatgpt_web_v4_vendor": "Pinned MIT vendor refresh from codex-chatgpt-web 0.1.16 to v4.0.6 (commit 09877fa21ffdbf20979623ef501046fc02a750d7). browser-worker.ts is preserved as the reviewed upstream browser protocol implementation; splitting the vendored file would destroy source parity and make future security/liveness updates unauditable. OmniRoute-specific DATA_DIR, Docker CDP, credential-marker, and XML decoding adaptations are covered by the ChatGPT Web Codex focused suite.",
|
||||
"_rebaseline_2026_08_20_10531_freebuff_provider": "PR #10531 (adrianaryaputra, feat/freebuff-provider-support, closes #6793) own growth: src/shared/constants/providers/apikey/gateways.ts 1283->1298 (+15, the freebuff APIKEY_PROVIDERS_GATEWAYS catalog entry, additive data at the existing registry chokepoint, same god-file no-split rationale as prior gateways.ts rebaselines) and src/app/(dashboard)/dashboard/providers/[id]/components/modals/AddApiKeyModal.tsx 1062->1067 (+5, freebuff credential placeholder/hint at the existing per-provider switch chokepoint). Covered by tests/unit/freebuff-provider.test.ts (9/9 passing).",
|
||||
@@ -406,7 +407,7 @@
|
||||
"open-sse/executors/cursor.ts": 1759,
|
||||
"open-sse/executors/muse-spark-web.ts": 1405,
|
||||
"open-sse/handlers/chatCore.ts": 5946,
|
||||
"open-sse/handlers/imageGeneration.ts": 3231,
|
||||
"open-sse/handlers/imageGeneration.ts": 3243,
|
||||
"open-sse/handlers/search.ts": 1789,
|
||||
"open-sse/mcp-server/schemas/tools.ts": 1621,
|
||||
"open-sse/mcp-server/server.ts": 1572,
|
||||
@@ -415,7 +416,7 @@
|
||||
"open-sse/services/combo.ts": 4023,
|
||||
"open-sse/translator/response/openai-responses.ts": 1466,
|
||||
"open-sse/utils/cursorAgentProtobuf.ts": 1547,
|
||||
"open-sse/utils/proxyFetch.ts": 1241,
|
||||
"open-sse/utils/proxyFetch.ts": 1261,
|
||||
"open-sse/utils/stream.ts": 3072,
|
||||
"open-sse/vendor/codex-chatgpt-web/adapters/chatgpt-web/browser-worker.ts": 4398,
|
||||
"open-sse/vendor/codex-chatgpt-web/bridge.ts": 1322,
|
||||
@@ -432,7 +433,7 @@
|
||||
"src/app/(dashboard)/dashboard/settings/components/RoutingTab.tsx": 1606,
|
||||
"src/app/(dashboard)/dashboard/settings/components/SystemStorageTab.tsx": 1597,
|
||||
"src/app/(dashboard)/dashboard/usage/components/EvalsTab.tsx": 2152,
|
||||
"src/app/api/providers/[id]/models/route.ts": 2381,
|
||||
"src/app/api/providers/[id]/models/route.ts": 2429,
|
||||
"src/app/api/providers/[id]/test/route.ts": 1252,
|
||||
"src/app/api/v1/models/catalog.ts": 2066,
|
||||
"src/app/docs/lib/openapi.generated.ts": 1347,
|
||||
|
||||
@@ -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 (352 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 86 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 86 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: 352 providers, 150+ 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, 150+ 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 352 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: 150+ providers with a free tier and 53 recurring or keyless free-forever providers. Every tool works: 36 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: 150+ providers with a free tier and 53 recurring or keyless free-forever providers. Every tool works: 36 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">352 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 352 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 352 providers — 150+ 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: 352 AI providers, 150+ 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 — 150+ 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, 150+ 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">352 providers</tspan> — <tspan fill="#7ee787" font-weight="800">150+ 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">150+ 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 |
@@ -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 352 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
|
||||
- **352 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)
|
||||
- **19 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, cache-optimized, context-relay, fusion, pipeline
|
||||
- **4-tier fallback**: Subscription → API Key → Cheap → Free
|
||||
|
||||
@@ -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 352 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
|
||||
- **352 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)
|
||||
- **19 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, cache-optimized, context-relay, fusion, pipeline
|
||||
- **4-tier fallback**: Subscription → API Key → Cheap → Free
|
||||
|
||||
@@ -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 352 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
|
||||
- **352 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)
|
||||
- **19 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, cache-optimized, context-relay, fusion, pipeline
|
||||
- **4-tier fallback**: Subscription → API Key → Cheap → Free
|
||||
|
||||
@@ -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 352 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
|
||||
- **352 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)
|
||||
- **19 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, cache-optimized, context-relay, fusion, pipeline
|
||||
- **4-tier fallback**: Subscription → API Key → Cheap → Free
|
||||
|
||||
@@ -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 352 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
|
||||
- **352 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)
|
||||
- **19 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, cache-optimized, context-relay, fusion, pipeline
|
||||
- **4-tier fallback**: Subscription → API Key → Cheap → Free
|
||||
|
||||
@@ -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 352 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
|
||||
- **352 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)
|
||||
- **19 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, cache-optimized, context-relay, fusion, pipeline
|
||||
- **4-tier fallback**: Subscription → API Key → Cheap → Free
|
||||
|
||||
@@ -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 352 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
|
||||
- **352 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)
|
||||
- **19 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, cache-optimized, context-relay, fusion, pipeline
|
||||
- **4-tier fallback**: Subscription → API Key → Cheap → Free
|
||||
|
||||
@@ -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 352 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
|
||||
- **352 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)
|
||||
- **19 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, cache-optimized, context-relay, fusion, pipeline
|
||||
- **4-tier fallback**: Subscription → API Key → Cheap → Free
|
||||
|
||||
@@ -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 352 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
|
||||
- **352 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)
|
||||
- **19 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, cache-optimized, context-relay, fusion, pipeline
|
||||
- **4-tier fallback**: Subscription → API Key → Cheap → Free
|
||||
|
||||
@@ -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 352 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
|
||||
- **352 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)
|
||||
- **19 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, cache-optimized, context-relay, fusion, pipeline
|
||||
- **4-tier fallback**: Subscription → API Key → Cheap → Free
|
||||
|
||||
@@ -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 352 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
|
||||
- **352 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)
|
||||
- **19 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, cache-optimized, context-relay, fusion, pipeline
|
||||
- **4-tier fallback**: Subscription → API Key → Cheap → Free
|
||||
|
||||
@@ -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 352 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
|
||||
- **352 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)
|
||||
- **19 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, cache-optimized, context-relay, fusion, pipeline
|
||||
- **4-tier fallback**: Subscription → API Key → Cheap → Free
|
||||
|
||||
@@ -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 352 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
|
||||
- **352 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)
|
||||
- **19 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, cache-optimized, context-relay, fusion, pipeline
|
||||
- **4-tier fallback**: Subscription → API Key → Cheap → Free
|
||||
|
||||
@@ -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 352 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
|
||||
- **352 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)
|
||||
- **19 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, cache-optimized, context-relay, fusion, pipeline
|
||||
- **4-tier fallback**: Subscription → API Key → Cheap → Free
|
||||
|
||||
@@ -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 352 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
|
||||
- **352 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)
|
||||
- **19 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, cache-optimized, context-relay, fusion, pipeline
|
||||
- **4-tier fallback**: Subscription → API Key → Cheap → Free
|
||||
|
||||
@@ -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 352 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
|
||||
- **352 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)
|
||||
- **19 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, cache-optimized, context-relay, fusion, pipeline
|
||||
- **4-tier fallback**: Subscription → API Key → Cheap → Free
|
||||
|
||||
@@ -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 352 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
|
||||
- **352 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)
|
||||
- **19 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, cache-optimized, context-relay, fusion, pipeline
|
||||
- **4-tier fallback**: Subscription → API Key → Cheap → Free
|
||||
|
||||
@@ -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 352 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
|
||||
- **352 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)
|
||||
- **19 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, cache-optimized, context-relay, fusion, pipeline
|
||||
- **4-tier fallback**: Subscription → API Key → Cheap → Free
|
||||
|
||||
@@ -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 352 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
|
||||
- **352 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)
|
||||
- **19 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, cache-optimized, context-relay, fusion, pipeline
|
||||
- **4-tier fallback**: Subscription → API Key → Cheap → Free
|
||||
|
||||
@@ -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 352 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
|
||||
- **352 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)
|
||||
- **19 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, cache-optimized, context-relay, fusion, pipeline
|
||||
- **4-tier fallback**: Subscription → API Key → Cheap → Free
|
||||
|
||||
@@ -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 352 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
|
||||
- **352 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)
|
||||
- **19 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, cache-optimized, context-relay, fusion, pipeline
|
||||
- **4-tier fallback**: Subscription → API Key → Cheap → Free
|
||||
|
||||
@@ -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 352 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
|
||||
- **352 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)
|
||||
- **19 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, cache-optimized, context-relay, fusion, pipeline
|
||||
- **4-tier fallback**: Subscription → API Key → Cheap → Free
|
||||
|
||||
@@ -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 352 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
|
||||
- **352 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)
|
||||
- **19 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, cache-optimized, context-relay, fusion, pipeline
|
||||
- **4-tier fallback**: Subscription → API Key → Cheap → Free
|
||||
|
||||
@@ -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 352 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
|
||||
- **352 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)
|
||||
- **19 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, cache-optimized, context-relay, fusion, pipeline
|
||||
- **4-tier fallback**: Subscription → API Key → Cheap → Free
|
||||
|
||||
@@ -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 352 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
|
||||
- **352 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)
|
||||
- **19 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, cache-optimized, context-relay, fusion, pipeline
|
||||
- **4-tier fallback**: Subscription → API Key → Cheap → Free
|
||||
|
||||
@@ -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 352 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
|
||||
- **352 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)
|
||||
- **19 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, cache-optimized, context-relay, fusion, pipeline
|
||||
- **4-tier fallback**: Subscription → API Key → Cheap → Free
|
||||
|
||||
@@ -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 352 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
|
||||
- **352 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)
|
||||
- **19 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, cache-optimized, context-relay, fusion, pipeline
|
||||
- **4-tier fallback**: Subscription → API Key → Cheap → Free
|
||||
|
||||
@@ -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 352 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
|
||||
- **352 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)
|
||||
- **19 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, cache-optimized, context-relay, fusion, pipeline
|
||||
- **4-tier fallback**: Subscription → API Key → Cheap → Free
|
||||
|
||||
@@ -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 352 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
|
||||
- **352 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)
|
||||
- **19 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, cache-optimized, context-relay, fusion, pipeline
|
||||
- **4-tier fallback**: Subscription → API Key → Cheap → Free
|
||||
|
||||
@@ -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 352 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
|
||||
- **352 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)
|
||||
- **19 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, cache-optimized, context-relay, fusion, pipeline
|
||||
- **4-tier fallback**: Subscription → API Key → Cheap → Free
|
||||
|
||||
@@ -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 352 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
|
||||
- **352 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)
|
||||
- **19 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, cache-optimized, context-relay, fusion, pipeline
|
||||
- **4-tier fallback**: Subscription → API Key → Cheap → Free
|
||||
|
||||
@@ -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 352 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
|
||||
- **352 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)
|
||||
- **19 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, cache-optimized, context-relay, fusion, pipeline
|
||||
- **4-tier fallback**: Subscription → API Key → Cheap → Free
|
||||
|
||||
@@ -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 352 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
|
||||
- **352 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)
|
||||
- **19 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, cache-optimized, context-relay, fusion, pipeline
|
||||
- **4-tier fallback**: Subscription → API Key → Cheap → Free
|
||||
|
||||
@@ -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 352 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
|
||||
- **352 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)
|
||||
- **19 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, cache-optimized, context-relay, fusion, pipeline
|
||||
- **4-tier fallback**: Subscription → API Key → Cheap → Free
|
||||
|
||||
@@ -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 352 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
|
||||
- **352 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)
|
||||
- **19 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, cache-optimized, context-relay, fusion, pipeline
|
||||
- **4-tier fallback**: Subscription → API Key → Cheap → Free
|
||||
|
||||
@@ -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 352 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
|
||||
- **352 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)
|
||||
- **19 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, cache-optimized, context-relay, fusion, pipeline
|
||||
- **4-tier fallback**: Subscription → API Key → Cheap → Free
|
||||
|
||||
@@ -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 352 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
|
||||
- **352 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)
|
||||
- **19 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, cache-optimized, context-relay, fusion, pipeline
|
||||
- **4-tier fallback**: Subscription → API Key → Cheap → Free
|
||||
|
||||
@@ -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 352 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
|
||||
- **352 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)
|
||||
- **19 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, cache-optimized, context-relay, fusion, pipeline
|
||||
- **4-tier fallback**: Subscription → API Key → Cheap → Free
|
||||
|
||||
@@ -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 352 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
|
||||
- **352 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)
|
||||
- **19 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, cache-optimized, context-relay, fusion, pipeline
|
||||
- **4-tier fallback**: Subscription → API Key → Cheap → Free
|
||||
|
||||
@@ -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 352 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
|
||||
- **352 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)
|
||||
- **19 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, cache-optimized, context-relay, fusion, pipeline
|
||||
- **4-tier fallback**: Subscription → API Key → Cheap → Free
|
||||
|
||||
@@ -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 352 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
|
||||
- **352 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)
|
||||
- **19 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, cache-optimized, context-relay, fusion, pipeline
|
||||
- **4-tier fallback**: Subscription → API Key → Cheap → Free
|
||||
|
||||
@@ -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 352 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
|
||||
- **352 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)
|
||||
- **19 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, cache-optimized, context-relay, fusion, pipeline
|
||||
- **4-tier fallback**: Subscription → API Key → Cheap → Free
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
---
|
||||
title: "Provider Reference"
|
||||
version: 3.8.51
|
||||
lastUpdated: 2026-08-30
|
||||
lastUpdated: 2026-09-02
|
||||
---
|
||||
|
||||
# Provider Reference
|
||||
|
||||
> **Auto-generated** from `src/shared/constants/providers.ts` — do not edit by hand.
|
||||
> Regenerate with: `npm run gen:provider-reference`
|
||||
> **Last generated:** 2026-08-30
|
||||
> **Last generated:** 2026-09-02
|
||||
|
||||
Total providers: **352**. See category breakdown below.
|
||||
Total providers: **353**. See category breakdown below.
|
||||
|
||||
## Categories
|
||||
|
||||
@@ -80,7 +80,7 @@ Use the dashboard at `/dashboard/providers` to enable, configure, and test each
|
||||
| `zed` | `zd` | Zed IDE | OAuth | [link](https://zed.dev) | Zed stores LLM provider credentials (OpenAI, Anthropic, Google, Mistral, xAI) in the OS keychain. Use the Import button below to discover and import them automatically. |
|
||||
| `zed-hosted` | — | Zed Hosted Models | OAuth | [link](https://zed.dev) | Sign in with your Zed account (native-app sign-in). OmniRoute generates a one-time RSA keypair and opens zed.dev to authorize it — on a remote/headless install, copy the resulting 127.0.0.1 callback URL from your browser's address bar and paste it back here. Distinct from the 'Zed IDE' credential-import entry above: this proxies chat completions through Zed's own hosted model aggregator (cloud.zed.dev), fronting Anthropic/OpenAI/Google/xAI models under your Zed plan. |
|
||||
|
||||
## Web Cookie Providers (31)
|
||||
## Web Cookie Providers (32)
|
||||
|
||||
| ID | Alias | Name | Tags | Website | Notes | Tool calling |
|
||||
|----|-------|------|------|---------|-------|--------------|
|
||||
@@ -102,6 +102,7 @@ Use the dashboard at `/dashboard/providers` to enable, configure, and test each
|
||||
| `inner-ai` | `in-ai` | Inner.ai (Subscription) | Web cookie | [link](https://app.innerai.com) | Paste your token cookie and email separated by a space: open DevTools → Application → Cookies → .innerai.com, copy the token value, then append a space and your Inner.ai login email. Example: eyJhbG... user@example.com | emulated |
|
||||
| `kimi-web` | `kimi-web` | Kimi Web | Web cookie | [link](https://www.kimi.ai) | Paste access_token from www.kimi.ai DevTools → Application → Local Storage. A legacy kimi-auth cookie is also accepted. | — |
|
||||
| `lmarena` | `lma` | Arena (Free) | Web cookie | [link](https://arena.ai) | Paste the full Cookie header from arena.ai (DevTools → Network → request → Cookie). Include arena-auth-prod-v1.0/.1… and cf_clearance/__cf_bm when present. OmniRoute uses Chrome TLS impersonation; if Arena still 403s, set providerSpecificData.recaptchaV3Token from a live browser session. | — |
|
||||
| `maxai` | `mx` | MaxAI | Web cookie | [link](https://www.maxai.co) | Sign in once (email code or browser) to mint a MaxAI access token. OmniRoute signs each request, routes it through residential egress, and refreshes the token browserlessly, so a connection stays valid for about a year without re-login. | emulated |
|
||||
| `muse-spark-web` | `ms-web` | Muse Spark Web (Meta AI) | Web cookie | [link](https://www.meta.ai) | Paste your ecto_1_sess cookie AND the ecto1:... WS auth token from meta.ai. Capture the ecto1: token in DevTools → Network → WS → the clippy request's Authorization query param. Example: ecto_1_sess=4240a308...NVDg0; ecto1:ABCD... | emulated |
|
||||
| `notion-web` | `nw` | Notion AI Web (Unofficial/Experimental) | Web cookie | [link](https://www.notion.so) | Paste only the token_v2 cookie VALUE from app.notion.com (DevTools → Application → Cookies → token_v2). Do not paste token_v2= or the full Cookie header. Workspace is auto-detected; space_id / notion_user_id are optional. | — |
|
||||
| `perplexity-web` | `pplx-web` | Perplexity Web (Pro/Max) | Web cookie | [link](https://www.perplexity.ai) | Paste your __Secure-next-auth.session-token cookie value from perplexity.ai | emulated |
|
||||
@@ -440,7 +441,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/) (104 implementations)
|
||||
- Executors: [`open-sse/executors/`](../../open-sse/executors/) (107 implementations)
|
||||
- Translators: [`open-sse/translator/`](../../open-sse/translator/)
|
||||
|
||||
## See Also
|
||||
|
||||
4
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 352 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
|
||||
- **352 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)
|
||||
- **19 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, cache-optimized, context-relay, fusion, pipeline
|
||||
- **4-tier fallback**: Subscription → API Key → Cheap → Free
|
||||
|
||||
@@ -256,6 +256,26 @@ export const IMAGE_PROVIDERS: Record<string, ImageProviderConfig> = {
|
||||
supportedSizes: ["1024x1024", "1024x1792", "1792x1024", "1024x1536", "1536x1024"],
|
||||
},
|
||||
|
||||
maxai: {
|
||||
id: "maxai",
|
||||
alias: "mx",
|
||||
baseUrl: "https://api.maxai.me/gpt/get_image_generate_response",
|
||||
authType: "apikey",
|
||||
authHeader: "bearer",
|
||||
format: "maxai-image",
|
||||
models: [
|
||||
{ id: "gpt-image-1", name: "GPT Image 1 (MaxAI)" },
|
||||
{ id: "dall-e-3", name: "DALL-E 3 (MaxAI)" },
|
||||
{ id: "flux-1-schnell", name: "FLUX.1 [schnell] (MaxAI)" },
|
||||
{ id: "flux-1-dev", name: "FLUX.1 [dev] (MaxAI)" },
|
||||
{ id: "flux-1-pro", name: "FLUX.1 [pro] (MaxAI)" },
|
||||
{ id: "sd3-medium", name: "Stable Diffusion 3 Medium (MaxAI)" },
|
||||
],
|
||||
// gpt-image-1/dall-e-3 are size-snapped to 1024x1024 by the handler; flux
|
||||
// models pass any size through.
|
||||
supportedSizes: ["1024x1024", "1024x1536", "1536x1024", "1024x1792", "1792x1024"],
|
||||
},
|
||||
|
||||
xai: {
|
||||
id: "xai",
|
||||
baseUrl: "https://api.x.ai/v1/images/generations",
|
||||
|
||||
@@ -210,6 +210,7 @@ import { pollinationsProvider } from "./registry/pollinations/index.ts";
|
||||
import { veoaifree_webProvider } from "./registry/veoaifree-web/index.ts";
|
||||
import { codexProvider } from "./registry/codex/index.ts";
|
||||
import { codexAppServerProvider } from "./registry/codex-app-server/index.ts";
|
||||
import { maxaiProvider } from "./registry/maxai/index.ts";
|
||||
import { veniceProvider } from "./registry/venice/index.ts";
|
||||
import { kiroProvider } from "./registry/kiro/index.ts";
|
||||
import { openadapterProvider } from "./registry/openadapter/index.ts";
|
||||
@@ -477,6 +478,7 @@ export const REGISTRY: Record<string, RegistryEntry> = {
|
||||
"veoaifree-web": veoaifree_webProvider,
|
||||
codex: codexProvider,
|
||||
"codex-app-server": codexAppServerProvider,
|
||||
maxai: maxaiProvider,
|
||||
venice: veniceProvider,
|
||||
kiro: kiroProvider,
|
||||
byteplus: byteplusProvider,
|
||||
|
||||
26
open-sse/config/providers/registry/maxai/index.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import type { RegistryEntry } from "../../shared.ts";
|
||||
import { MAXAI_REGISTRY_MODELS } from "../../../../executors/maxai/catalog.ts";
|
||||
|
||||
/**
|
||||
* MaxAI — the MaxAI web app (chat.maxai.co / api.maxai.me) as an OpenAI-compatible
|
||||
* provider. A signed web-app port (like zai-web): each request carries a
|
||||
* per-request `X-Authorization` signature + a Bearer access token minted by the
|
||||
* browser-mint flow. Runs over residential egress with a Firefox TLS fingerprint.
|
||||
*
|
||||
* authType `apikey`/authHeader `bearer`: the OpenAI-style access token is stored
|
||||
* on the connection and replayed as `Authorization: Bearer`; the device id +
|
||||
* user id ride in providerSpecificData and are folded into the signature. The
|
||||
* token is refreshed out-of-band by the browser-mint (the `/oauth` refresh
|
||||
* endpoint is deep-TLS-gated), so there is no central token-refresh case.
|
||||
*/
|
||||
export const maxaiProvider: RegistryEntry = {
|
||||
id: "maxai",
|
||||
alias: "mx",
|
||||
format: "openai",
|
||||
executor: "maxai",
|
||||
baseUrl: "https://api.maxai.me",
|
||||
authType: "apikey",
|
||||
authHeader: "bearer",
|
||||
defaultContextLength: 128000,
|
||||
models: MAXAI_REGISTRY_MODELS,
|
||||
};
|
||||
@@ -78,8 +78,8 @@ export const VOLCENGINE_AGENT_PLAN_MODELS: RegistryModel[] = [
|
||||
name: "MiniMax M3 (Agent Plan)",
|
||||
contextLength: 1048576,
|
||||
toolCalling: true,
|
||||
supportsReasoning: true,
|
||||
supportsVision: true,
|
||||
supportsReasoning: true,
|
||||
},
|
||||
{
|
||||
id: "deepseek-v4-pro-260425",
|
||||
|
||||
@@ -54,8 +54,8 @@ export const VOLCENGINE_CODING_PLAN_MODELS: RegistryModel[] = [
|
||||
name: "MiniMax M3 (Coding Plan)",
|
||||
contextLength: 1048576,
|
||||
toolCalling: true,
|
||||
supportsReasoning: true,
|
||||
supportsVision: true,
|
||||
supportsReasoning: true,
|
||||
},
|
||||
{
|
||||
id: "deepseek-v4-pro",
|
||||
|
||||
@@ -44,6 +44,7 @@ const lazyExecutors: Record<string, () => Promise<BaseExecutor>> = {
|
||||
import("./codex-app-server.ts").then(
|
||||
(m) => new m.CodexAppServerExecutor({}, "codex-app-server")
|
||||
),
|
||||
maxai: () => import("./maxai.ts").then((m) => new m.MaxAiExecutor()),
|
||||
"chatgpt-web-codex": () =>
|
||||
import("./chatgpt-web-codex.ts").then((m) => new m.ChatGptWebCodexExecutor()),
|
||||
"cgpt-codex": () =>
|
||||
|
||||
620
open-sse/executors/maxai.ts
Normal file
@@ -0,0 +1,620 @@
|
||||
/**
|
||||
* MaxAiExecutor — MaxAI web-app chat as an OpenAI-compatible OmniRoute provider.
|
||||
*
|
||||
* MaxAI (chat.maxai.co / api.maxai.me) is a consumer web app with no public API.
|
||||
* This executor reproduces the web app's own signed request to `/gpt/cwc/chat`:
|
||||
* • per-request `X-Authorization` signature (see ./signing.ts),
|
||||
* • Firefox-150 identity headers + Bearer access token,
|
||||
* • the full OpenAI transcript flattened into one `message_content` block
|
||||
* (stateless-full-history; see ./protocol.ts),
|
||||
* • SSE response parsed for text deltas, with inline `<think>` reasoning split
|
||||
* out into `reasoning_content` (see ./stream.ts).
|
||||
*
|
||||
* Egress + TLS: the request MUST exit a residential IP (MaxAI bot-bans datacenter
|
||||
* IPs). OmniRoute routes the executor's `fetch()` through the per-connection proxy
|
||||
* (a residential HTTP proxy) transparently, and applies the wreq-js Firefox TLS
|
||||
* fingerprint when enabled. This executor does not open its own socket; it uses
|
||||
* the ambient patched `fetch`, so the proxy + TLS overlay apply automatically.
|
||||
*
|
||||
* Auth refresh: MaxAI's `/oauth/refresh_access_token` is deep-TLS-gated and cannot
|
||||
* be called by any HTTP client (only a real browser passes). The access token is
|
||||
* therefore minted/refreshed out-of-band by OmniRoute's own browser-mint flow
|
||||
* (see maxaiBrowserLogin); this executor only consumes the stored credential.
|
||||
*/
|
||||
import { BaseExecutor, type ExecuteInput, type ExecutorExecuteResult } from "./base.ts";
|
||||
import { PROVIDERS } from "../config/constants.ts";
|
||||
import { sanitizeErrorMessage } from "../utils/error.ts";
|
||||
import { resolveMaxaiCredential, type MaxaiCredential } from "./maxai/credentials.ts";
|
||||
import { buildMaxaiSignedHeaders } from "./maxai/signing.ts";
|
||||
import { ensureMaxaiConstants } from "./maxai/constantsStore.ts";
|
||||
import { maxaiAccessTokenNeedsRefresh, maxaiRefreshAccessToken } from "./maxai/refresh.ts";
|
||||
import {
|
||||
assembleMaxaiContext,
|
||||
buildMaxaiChatBody,
|
||||
extractCurrentTurnImages,
|
||||
MAXAI_BASE_URL,
|
||||
MAXAI_CHAT_PATH,
|
||||
maxaiStaticHeaders,
|
||||
newConversationId,
|
||||
} from "./maxai/protocol.ts";
|
||||
import { resolveMaxaiDocList, type MaxaiDocListEntry } from "./maxai/documents.ts";
|
||||
import { estimateMaxaiTokens, isMaxaiTextFrame, ThinkSplitter } from "./maxai/stream.ts";
|
||||
import { prepareToolMessages, parseToolCallsFromText } from "../translator/webTools.ts";
|
||||
import { buildToolModeResponse } from "./chatgptWebTools.ts";
|
||||
|
||||
const JSON_HEADERS = { "Content-Type": "application/json" };
|
||||
const SSE_HEADERS = {
|
||||
"Cache-Control": "no-cache, no-transform",
|
||||
Connection: "keep-alive",
|
||||
"Content-Type": "text/event-stream; charset=utf-8",
|
||||
};
|
||||
|
||||
interface OpenAiChatBody {
|
||||
messages?: Array<{
|
||||
role?: string;
|
||||
content?: unknown;
|
||||
tool_calls?: unknown;
|
||||
tool_call_id?: string;
|
||||
}>;
|
||||
model?: string;
|
||||
}
|
||||
|
||||
function errorResponse(status: number, message: string, code: string): Response {
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
error: {
|
||||
code,
|
||||
message: sanitizeErrorMessage(message),
|
||||
type: status >= 500 ? "provider_error" : "invalid_request_error",
|
||||
},
|
||||
}),
|
||||
{ status, headers: JSON_HEADERS }
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrap a Response into the executor wrapper contract shape
|
||||
* `{response, url, headers, transformedBody}` that `chatCore.ts` and the
|
||||
* web-cookie/noauth sweep (tests/unit/executor-web-cookie-sweep.test.ts)
|
||||
* require. `headers` and `transformedBody` are the ACTUAL upstream request
|
||||
* headers and body — chatCore surfaces them as the provider-request-capture
|
||||
* ("what we actually sent") in the dashboard and uses the body for service-tier
|
||||
* and prompt-cache metadata (chatCore.ts:3680-3688), mirroring the shape returned
|
||||
* by every web-cookie sibling (venice-web.ts:92-94, poe-web.ts:121-123). Error
|
||||
* paths that fail BEFORE a request is assembled pass no capture — honestly empty,
|
||||
* because nothing was sent upstream.
|
||||
*/
|
||||
function wrap(
|
||||
response: Response,
|
||||
url: string,
|
||||
capture?: { headers?: Record<string, string>; transformedBody?: unknown }
|
||||
): { response: Response; url: string; headers: Record<string, string>; transformedBody: unknown } {
|
||||
return {
|
||||
response,
|
||||
url,
|
||||
headers: capture?.headers ?? {},
|
||||
transformedBody: capture?.transformedBody ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect a tool "narration miss": the model produced no parseable <tool> block
|
||||
* but its text shows it was ABOUT to call a tool (talks about the <tool> block
|
||||
* or names a requested tool). This is the occasional reasoning-model failure
|
||||
* mode (e.g. deepseek-r1) where it reasons about the call instead of emitting
|
||||
* it. A true refusal or a normal answer returns false, so we never retry those.
|
||||
*/
|
||||
function isToolNarrationMiss(text: string, requestedTools: unknown): boolean {
|
||||
if (!text) return false;
|
||||
if (/<tool\b/.test(text)) return true; // mentioned the tag but it didn't parse
|
||||
const names = Array.isArray(requestedTools)
|
||||
? (requestedTools as Array<{ function?: { name?: unknown } }>)
|
||||
.map((t) => (typeof t?.function?.name === "string" ? t.function.name : ""))
|
||||
.filter(Boolean)
|
||||
: [];
|
||||
// Names it a tool AND signals intent to use it (not merely mentioning it).
|
||||
const intent = /\b(I('| wi)ll|let me|I can|going to|need to)\b/i.test(text);
|
||||
return intent && names.some((n) => text.includes(n));
|
||||
}
|
||||
|
||||
/** A short, soft nudge appended to the transcript for the single retry turn. */
|
||||
function toolNudge(originalText: string): string {
|
||||
return (
|
||||
originalText +
|
||||
"\n\n[A quick note: if a client tool would help answer this, please go ahead " +
|
||||
"and emit the <tool> block directly rather than describing it — just the block " +
|
||||
"on its own line. If no tool is needed, a normal answer is perfectly fine.]"
|
||||
);
|
||||
}
|
||||
|
||||
/** Emit one OpenAI `chat.completion.chunk`. */
|
||||
function chunk(
|
||||
controller: ReadableStreamDefaultController,
|
||||
id: string,
|
||||
created: number,
|
||||
model: string,
|
||||
delta: Record<string, unknown>,
|
||||
finish: string | null = null
|
||||
): void {
|
||||
const payload = {
|
||||
id,
|
||||
object: "chat.completion.chunk",
|
||||
created,
|
||||
model,
|
||||
choices: [{ index: 0, delta, finish_reason: finish }],
|
||||
};
|
||||
controller.enqueue(new TextEncoder().encode(`data: ${JSON.stringify(payload)}\n\n`));
|
||||
}
|
||||
|
||||
export class MaxAiExecutor extends BaseExecutor {
|
||||
constructor() {
|
||||
super("maxai", PROVIDERS.maxai ?? { id: "maxai", baseUrl: MAXAI_BASE_URL });
|
||||
}
|
||||
|
||||
override async execute(input: ExecuteInput): Promise<ExecutorExecuteResult> {
|
||||
// The MaxAI chat endpoint URL is the wrapper's `url` for every return path
|
||||
// (error and success alike), so define it once up front.
|
||||
const url = MAXAI_BASE_URL + MAXAI_CHAT_PATH;
|
||||
|
||||
const cred = resolveMaxaiCredential(
|
||||
input.credentials?.providerSpecificData,
|
||||
input.credentials?.accessToken
|
||||
);
|
||||
if (!cred) {
|
||||
return wrap(
|
||||
errorResponse(
|
||||
401,
|
||||
"MaxAI connection is not configured (missing access token, device id, or user id). Sign in to mint a token.",
|
||||
"maxai_unconfigured"
|
||||
),
|
||||
url
|
||||
);
|
||||
}
|
||||
|
||||
// Proactively refresh a near-expiry access token (browserless; see ./maxai/refresh.ts).
|
||||
// Failures here are non-fatal: we fall through with the existing token, and a
|
||||
// genuinely-dead token surfaces as a 401/418 below (prompting a re-mint).
|
||||
const accessToken = await this.ensureFreshAccess(cred, input);
|
||||
|
||||
const body = (input.body ?? {}) as OpenAiChatBody;
|
||||
|
||||
// Tool-calling (prompted protocol): when the request carries tools[], inject
|
||||
// the <tool> contract into the messages so the model learns the client tools
|
||||
// and how to invoke them (see translator/webTools.ts). MaxAI has no native
|
||||
// function-calling; this is the same prompted-tool shim the web-cookie
|
||||
// providers use. The response side parses <tool> blocks back into tool_calls.
|
||||
const { hasTools, requestedTools, effectiveMessages } = prepareToolMessages(
|
||||
body as Record<string, unknown>,
|
||||
(body.messages ?? []) as Array<{ role: string; content: unknown }>
|
||||
);
|
||||
|
||||
let text: string;
|
||||
try {
|
||||
text = assembleMaxaiContext(effectiveMessages);
|
||||
} catch {
|
||||
return wrap(
|
||||
errorResponse(400, "No user message to send to MaxAI.", "maxai_empty_request"),
|
||||
url
|
||||
);
|
||||
}
|
||||
|
||||
// Vision input: attach the CURRENT user turn's images (data: / http(s):) to
|
||||
// message_content so vision-capable MaxAI models actually see them. Extract
|
||||
// from the original messages (pre-tool-munging); text stays flattened.
|
||||
const originalMessages = (body.messages ?? []) as Array<{ role?: string; content?: unknown }>;
|
||||
const imageUrls = extractCurrentTurnImages(originalMessages);
|
||||
|
||||
// Doc-RAG: upload any inline documents (base64 file/input_file/document
|
||||
// parts) on the current turn to /app/upload_document and attach the
|
||||
// resulting doc_list to the chat body. Best-effort: upload failures are
|
||||
// skipped and the chat proceeds without the doc.
|
||||
let docList: MaxaiDocListEntry[] = [];
|
||||
try {
|
||||
docList = await resolveMaxaiDocList(
|
||||
originalMessages,
|
||||
{ accessToken, userId: cred.userId, deviceId: cred.deviceId },
|
||||
{ signal: input.signal ?? undefined }
|
||||
);
|
||||
} catch {
|
||||
docList = [];
|
||||
}
|
||||
|
||||
const constants = await ensureMaxaiConstants({ signal: input.signal });
|
||||
if (!constants) {
|
||||
return wrap(
|
||||
errorResponse(
|
||||
401,
|
||||
"MaxAI signing constants unavailable (extraction failed); cannot sign the request.",
|
||||
"maxai_auth_error"
|
||||
),
|
||||
url
|
||||
);
|
||||
}
|
||||
|
||||
const conversationId = newConversationId();
|
||||
const chatBody = buildMaxaiChatBody({
|
||||
conversationId,
|
||||
text,
|
||||
modelName: input.model,
|
||||
appVersion: constants.appVersion,
|
||||
imageUrls,
|
||||
docList: docList.length ? docList : undefined,
|
||||
});
|
||||
|
||||
const signedHeaders = buildMaxaiSignedHeaders(
|
||||
{
|
||||
path: MAXAI_CHAT_PATH,
|
||||
userId: cred.userId,
|
||||
deviceId: cred.deviceId,
|
||||
},
|
||||
constants
|
||||
);
|
||||
const headers: Record<string, string> = {
|
||||
...maxaiStaticHeaders(),
|
||||
...signedHeaders,
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
...(input.upstreamExtraHeaders ?? {}),
|
||||
};
|
||||
|
||||
let upstream: Response;
|
||||
try {
|
||||
upstream = await fetch(url, {
|
||||
method: "POST",
|
||||
headers,
|
||||
body: JSON.stringify(chatBody),
|
||||
signal: input.signal ?? undefined,
|
||||
});
|
||||
} catch (err) {
|
||||
return wrap(
|
||||
errorResponse(
|
||||
502,
|
||||
`MaxAI request failed: ${sanitizeErrorMessage(err instanceof Error ? err.message : err)}`,
|
||||
"maxai_transport_error"
|
||||
),
|
||||
url
|
||||
);
|
||||
}
|
||||
|
||||
if (upstream.status !== 200 || !upstream.body) {
|
||||
const detail = await upstream.text().catch(() => "");
|
||||
// 401/418 = auth expired/masked-reject; surface so the caller can prompt a re-mint.
|
||||
// A body-too-large rejection (MaxAI answers 422 "...message you submitted being
|
||||
// too long...") is INPUT-bound: classify it as context_length_exceeded so
|
||||
// OmniRoute's compression/overflow pipeline can shrink and retry instead of
|
||||
// treating it as an opaque provider error.
|
||||
const tooLong = /too\s+long|exceeds?\b.*\bcontext|context.*(?:exceeded|too long|limit)/i.test(
|
||||
detail
|
||||
);
|
||||
if (tooLong) {
|
||||
return wrap(
|
||||
errorResponse(
|
||||
400,
|
||||
`MaxAI request exceeds the context limit: ${sanitizeErrorMessage(detail.slice(0, 200))}`,
|
||||
"context_length_exceeded"
|
||||
),
|
||||
url
|
||||
);
|
||||
}
|
||||
const status = upstream.status === 418 ? 401 : upstream.status || 502;
|
||||
return wrap(
|
||||
errorResponse(
|
||||
status,
|
||||
`MaxAI upstream ${upstream.status}: ${sanitizeErrorMessage(detail.slice(0, 300))}`,
|
||||
upstream.status === 401 || upstream.status === 418
|
||||
? "maxai_auth_error"
|
||||
: "maxai_upstream_error"
|
||||
),
|
||||
url
|
||||
);
|
||||
}
|
||||
|
||||
const id = `chatcmpl-${conversationId}`;
|
||||
const created = Math.floor(Date.now() / 1000);
|
||||
const promptTokens = estimateMaxaiTokens(text);
|
||||
|
||||
// Tool mode: MaxAI streams plain text, and the <tool> protocol is only
|
||||
// parseable once the full reply is in hand. So when tools are active we
|
||||
// buffer the whole body, build a chat.completion, and let the shared shim
|
||||
// parse <tool> blocks into tool_calls (emitting a terminal SSE replay for
|
||||
// streaming callers). This mirrors every web-cookie provider's tool path.
|
||||
if (hasTools) {
|
||||
const raw = await upstream.text();
|
||||
let { reasoning, answer } = collectNonStream(raw);
|
||||
|
||||
// Reliability: if the model narrated about the tool but emitted no
|
||||
// parseable <tool> block (occasional reasoning-model miss), do ONE gentle
|
||||
// nudged retry and keep it only if it actually produces a tool call.
|
||||
const firstHasToolCall = !!parseToolCallsFromText(answer, "probe", requestedTools).toolCalls;
|
||||
if (!firstHasToolCall && isToolNarrationMiss(reasoning + "\n" + answer, requestedTools)) {
|
||||
const retry = await this.retryToolTurn(cred, accessToken, input, toolNudge(text));
|
||||
if (retry && parseToolCallsFromText(retry.answer, "probe", requestedTools).toolCalls) {
|
||||
reasoning = retry.reasoning;
|
||||
answer = retry.answer;
|
||||
input.log?.debug?.("maxai", "tool narration-miss recovered via one nudged retry");
|
||||
}
|
||||
}
|
||||
|
||||
const completionTokens = estimateMaxaiTokens(reasoning + answer);
|
||||
const buffered = new Response(
|
||||
JSON.stringify({
|
||||
id,
|
||||
object: "chat.completion",
|
||||
created,
|
||||
model: input.model,
|
||||
choices: [
|
||||
{
|
||||
index: 0,
|
||||
message: {
|
||||
role: "assistant",
|
||||
content: answer,
|
||||
...(reasoning ? { reasoning_content: reasoning } : {}),
|
||||
},
|
||||
finish_reason: "stop",
|
||||
},
|
||||
],
|
||||
usage: {
|
||||
prompt_tokens: promptTokens,
|
||||
completion_tokens: completionTokens,
|
||||
total_tokens: promptTokens + completionTokens,
|
||||
},
|
||||
}),
|
||||
{ status: 200, headers: JSON_HEADERS }
|
||||
);
|
||||
const response = await buildToolModeResponse(buffered, requestedTools, input.stream, {
|
||||
cid: id,
|
||||
created,
|
||||
model: input.model,
|
||||
idSeed: "maxai",
|
||||
});
|
||||
return wrap(response, url, { headers, transformedBody: chatBody });
|
||||
}
|
||||
|
||||
if (input.stream) {
|
||||
const stream = this.buildStream(upstream.body, id, created, input.model, promptTokens);
|
||||
return wrap(new Response(stream, { status: 200, headers: SSE_HEADERS }), url, {
|
||||
headers,
|
||||
transformedBody: chatBody,
|
||||
});
|
||||
}
|
||||
|
||||
// Non-streaming: collect the whole SSE body, split think, build a chat.completion.
|
||||
const raw = await upstream.text();
|
||||
const { reasoning, answer } = collectNonStream(raw);
|
||||
const completionTokens = estimateMaxaiTokens(reasoning + answer);
|
||||
const response = {
|
||||
id,
|
||||
object: "chat.completion",
|
||||
created,
|
||||
model: input.model,
|
||||
choices: [
|
||||
{
|
||||
index: 0,
|
||||
message: {
|
||||
role: "assistant",
|
||||
content: answer,
|
||||
...(reasoning ? { reasoning_content: reasoning } : {}),
|
||||
},
|
||||
finish_reason: "stop",
|
||||
},
|
||||
],
|
||||
usage: {
|
||||
prompt_tokens: promptTokens,
|
||||
completion_tokens: completionTokens,
|
||||
total_tokens: promptTokens + completionTokens,
|
||||
},
|
||||
};
|
||||
return wrap(
|
||||
new Response(JSON.stringify(response), { status: 200, headers: JSON_HEADERS }),
|
||||
url,
|
||||
{ headers, transformedBody: chatBody }
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a non-expired access token, refreshing browserlessly when the stored
|
||||
* one is missing or within the expiry margin and a refresh token is available.
|
||||
* Persists a freshly-minted token via `onCredentialsRefreshed`. Never throws —
|
||||
* on any refresh failure it returns the original token so the request still
|
||||
* proceeds (a truly-dead token then surfaces as an upstream 401/418).
|
||||
*/
|
||||
private async ensureFreshAccess(cred: MaxaiCredential, input: ExecuteInput): Promise<string> {
|
||||
if (!cred.refreshToken) return cred.accessToken;
|
||||
if (!maxaiAccessTokenNeedsRefresh(cred.accessToken)) return cred.accessToken;
|
||||
|
||||
const result = await maxaiRefreshAccessToken({
|
||||
refreshToken: cred.refreshToken,
|
||||
deviceId: cred.deviceId,
|
||||
userId: cred.userId,
|
||||
signal: input.signal ?? undefined,
|
||||
});
|
||||
if (!result.ok || !result.accessToken) {
|
||||
input.log?.warn?.(
|
||||
"maxai",
|
||||
`access-token refresh failed (${result.status}); using existing token`
|
||||
);
|
||||
return cred.accessToken;
|
||||
}
|
||||
|
||||
// Persist the new access token (merged into providerSpecificData) so the next
|
||||
// request starts fresh. The refresh token and device id are unchanged.
|
||||
try {
|
||||
await input.onCredentialsRefreshed?.({
|
||||
accessToken: result.accessToken,
|
||||
providerSpecificData: {
|
||||
...(input.credentials?.providerSpecificData ?? {}),
|
||||
maxaiAccessToken: result.accessToken,
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
input.log?.warn?.(
|
||||
"maxai",
|
||||
`refreshed token persist failed: ${sanitizeErrorMessage(err instanceof Error ? err.message : err)}`
|
||||
);
|
||||
}
|
||||
return result.accessToken;
|
||||
}
|
||||
|
||||
/**
|
||||
* Run a single follow-up MaxAI turn with a gentle nudge appended, used to
|
||||
* recover a reasoning-model "narration miss" (the model talked ABOUT the
|
||||
* <tool> block instead of emitting it). Bounded to one extra call; returns the
|
||||
* split { reasoning, answer } or null on any failure (caller keeps the original).
|
||||
*/
|
||||
private async retryToolTurn(
|
||||
cred: MaxaiCredential,
|
||||
accessToken: string,
|
||||
input: ExecuteInput,
|
||||
nudgedText: string
|
||||
): Promise<{ reasoning: string; answer: string } | null> {
|
||||
try {
|
||||
const constants = await ensureMaxaiConstants({ signal: input.signal });
|
||||
if (!constants) return null;
|
||||
const retryBody = buildMaxaiChatBody({
|
||||
conversationId: newConversationId(),
|
||||
text: nudgedText,
|
||||
modelName: input.model,
|
||||
appVersion: constants.appVersion,
|
||||
});
|
||||
const headers: Record<string, string> = {
|
||||
...maxaiStaticHeaders(),
|
||||
...buildMaxaiSignedHeaders(
|
||||
{
|
||||
path: MAXAI_CHAT_PATH,
|
||||
userId: cred.userId,
|
||||
deviceId: cred.deviceId,
|
||||
},
|
||||
constants
|
||||
),
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
...(input.upstreamExtraHeaders ?? {}),
|
||||
};
|
||||
const res = await fetch(MAXAI_BASE_URL + MAXAI_CHAT_PATH, {
|
||||
method: "POST",
|
||||
headers,
|
||||
body: JSON.stringify(retryBody),
|
||||
signal: input.signal ?? undefined,
|
||||
});
|
||||
if (res.status !== 200 || !res.body) return null;
|
||||
return collectNonStream(await res.text());
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/** Bridge the MaxAI SSE body into an OpenAI chat.completion.chunk stream. */
|
||||
private buildStream(
|
||||
source: ReadableStream<Uint8Array>,
|
||||
id: string,
|
||||
created: number,
|
||||
model: string,
|
||||
promptTokens: number
|
||||
): ReadableStream {
|
||||
const splitter = new ThinkSplitter();
|
||||
const decoder = new TextDecoder();
|
||||
let sseBuf = "";
|
||||
let sentRole = false;
|
||||
let completionChars = 0;
|
||||
|
||||
const emitDelta = (controller: ReadableStreamDefaultController, r: string, a: string) => {
|
||||
if (!sentRole && (r || a)) {
|
||||
chunk(controller, id, created, model, { role: "assistant" });
|
||||
sentRole = true;
|
||||
}
|
||||
if (r) {
|
||||
chunk(controller, id, created, model, { reasoning_content: r });
|
||||
completionChars += r.length;
|
||||
}
|
||||
if (a) {
|
||||
chunk(controller, id, created, model, { content: a });
|
||||
completionChars += a.length;
|
||||
}
|
||||
};
|
||||
|
||||
const processFrame = (controller: ReadableStreamDefaultController, jsonStr: string) => {
|
||||
if (!jsonStr || jsonStr === "[DONE]") return;
|
||||
let frame: unknown;
|
||||
try {
|
||||
frame = JSON.parse(jsonStr);
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
if (isMaxaiTextFrame(frame)) {
|
||||
const { reasoning, answer } = splitter.feed(frame.text);
|
||||
emitDelta(controller, reasoning, answer);
|
||||
}
|
||||
};
|
||||
|
||||
return new ReadableStream({
|
||||
async start(controller) {
|
||||
const reader = source.getReader();
|
||||
try {
|
||||
for (;;) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
sseBuf += decoder.decode(value, { stream: true });
|
||||
let nl: number;
|
||||
while ((nl = sseBuf.indexOf("\n")) !== -1) {
|
||||
const line = sseBuf.slice(0, nl).trim();
|
||||
sseBuf = sseBuf.slice(nl + 1);
|
||||
if (line.startsWith("data:")) processFrame(controller, line.slice(5).trim());
|
||||
}
|
||||
}
|
||||
// flush held tail from the think splitter
|
||||
const tail = splitter.flush();
|
||||
emitDelta(controller, tail.reasoning, tail.answer);
|
||||
// final chunk with usage + finish
|
||||
const completionTokens = estimateMaxaiTokens("x".repeat(completionChars));
|
||||
const finalChunk = {
|
||||
id,
|
||||
object: "chat.completion.chunk",
|
||||
created,
|
||||
model,
|
||||
choices: [{ index: 0, delta: {}, finish_reason: "stop" }],
|
||||
usage: {
|
||||
prompt_tokens: promptTokens,
|
||||
completion_tokens: completionTokens,
|
||||
total_tokens: promptTokens + completionTokens,
|
||||
},
|
||||
};
|
||||
controller.enqueue(new TextEncoder().encode(`data: ${JSON.stringify(finalChunk)}\n\n`));
|
||||
controller.enqueue(new TextEncoder().encode("data: [DONE]\n\n"));
|
||||
controller.close();
|
||||
} catch (err) {
|
||||
try {
|
||||
controller.error(err);
|
||||
} catch {
|
||||
/* already errored */
|
||||
}
|
||||
} finally {
|
||||
reader.releaseLock();
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/** Collect a full MaxAI SSE body into split { reasoning, answer } (non-stream). */
|
||||
function collectNonStream(raw: string): { reasoning: string; answer: string } {
|
||||
const splitter = new ThinkSplitter();
|
||||
let reasoning = "";
|
||||
let answer = "";
|
||||
for (const line of raw.split("\n")) {
|
||||
const s = line.trim();
|
||||
if (!s.startsWith("data:")) continue;
|
||||
const js = s.slice(5).trim();
|
||||
if (!js || js === "[DONE]") continue;
|
||||
let frame: unknown;
|
||||
try {
|
||||
frame = JSON.parse(js);
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
if (isMaxaiTextFrame(frame)) {
|
||||
const out = splitter.feed(frame.text);
|
||||
reasoning += out.reasoning;
|
||||
answer += out.answer;
|
||||
}
|
||||
}
|
||||
const tail = splitter.flush();
|
||||
return { reasoning: reasoning + tail.reasoning, answer: answer + tail.answer };
|
||||
}
|
||||
76
open-sse/executors/maxai/catalog.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
/**
|
||||
* MaxAI model catalog + provider-enum mapping. Ported from the MaxAI v3 client
|
||||
* (catalog/context_windows.py, tools/provider_enum.py). All 13 chat models are
|
||||
* PAID (the free `mistral-7b-instruct-free` is a window-lookup fallback only and
|
||||
* is not offered). Context windows are the MaxAI-reported values.
|
||||
*/
|
||||
import type { RegistryModel } from "../../config/providers/shared.ts";
|
||||
|
||||
interface MaxaiModelSpec {
|
||||
id: string;
|
||||
name: string;
|
||||
contextLength: number;
|
||||
supportsReasoning?: boolean;
|
||||
/**
|
||||
* Vision-capable (accepts image_url input). Sourced from MaxAI's live
|
||||
* `/models/get_config` `capabilities.vision` (verified 2026-08); the executor
|
||||
* forwards image parts inline in message_content for these. Live discovery
|
||||
* (services/maxaiModels.ts) overrides this from the catalog at runtime; this
|
||||
* static flag keeps the offline registry in agreement.
|
||||
*/
|
||||
supportsVision?: boolean;
|
||||
}
|
||||
|
||||
/** The 13 offered paid chat models (group order: FAST, SMART, REASONING). */
|
||||
export const MAXAI_MODELS: MaxaiModelSpec[] = [
|
||||
// FAST
|
||||
{ id: "gpt-5.6-luna", name: "GPT-5.6 Luna", contextLength: 1_050_000, supportsVision: true },
|
||||
{ id: "claude-haiku-4-5", name: "Claude Haiku 4.5", contextLength: 200_000, supportsVision: true },
|
||||
{ id: "gemini-3-1-flash-lite", name: "Gemini 3.1 Flash Lite", contextLength: 1_000_000, supportsVision: true },
|
||||
{ id: "grok-4-1-fast-non-reasoning", name: "Grok 4.1 Fast", contextLength: 2_000_000 },
|
||||
{ id: "llama-3.3-70b", name: "Llama 3.3 70B", contextLength: 128_000 },
|
||||
{ id: "deepseek-v3.2", name: "DeepSeek V3.2", contextLength: 128_000 },
|
||||
// SMART
|
||||
{ id: "gpt-5.6", name: "GPT-5.6", contextLength: 1_050_000, supportsVision: true },
|
||||
{ id: "claude-5-sonnet", name: "Claude 5 Sonnet", contextLength: 1_000_000 },
|
||||
{
|
||||
id: "grok-4-1-fast-reasoning",
|
||||
name: "Grok 4.1 Fast (Reasoning)",
|
||||
contextLength: 2_000_000,
|
||||
supportsReasoning: true,
|
||||
},
|
||||
// REASONING
|
||||
{
|
||||
id: "gpt-5.6-thinking",
|
||||
name: "GPT-5.6 Thinking",
|
||||
contextLength: 1_050_000,
|
||||
supportsReasoning: true,
|
||||
supportsVision: true,
|
||||
},
|
||||
{
|
||||
id: "gemini-3.1-pro-preview",
|
||||
name: "Gemini 3.1 Pro Preview",
|
||||
contextLength: 1_000_000,
|
||||
supportsReasoning: true,
|
||||
supportsVision: true,
|
||||
},
|
||||
{ id: "grok-4.5", name: "Grok 4.5", contextLength: 500_000, supportsReasoning: true },
|
||||
{ id: "deepseek-r1", name: "DeepSeek R1", contextLength: 128_000, supportsReasoning: true },
|
||||
];
|
||||
|
||||
/** RegistryModel[] form for the provider registry entry. */
|
||||
export const MAXAI_REGISTRY_MODELS: RegistryModel[] = MAXAI_MODELS.map((m) => ({
|
||||
id: m.id,
|
||||
name: m.name,
|
||||
contextLength: m.contextLength,
|
||||
toolCalling: true, // prompted tool-calling (no native API, but supported via the tool protocol)
|
||||
...(m.supportsReasoning ? { supportsReasoning: true } : {}),
|
||||
...(m.supportsVision ? { supportsVision: true } : {}),
|
||||
}));
|
||||
|
||||
/** Default context window for an unknown model. */
|
||||
export const MAXAI_DEFAULT_CONTEXT = 128_000;
|
||||
|
||||
export function maxaiContextWindow(modelId: string): number {
|
||||
return MAXAI_MODELS.find((m) => m.id === modelId)?.contextLength ?? MAXAI_DEFAULT_CONTEXT;
|
||||
}
|
||||
427
open-sse/executors/maxai/constants.ts
Normal file
@@ -0,0 +1,427 @@
|
||||
/**
|
||||
* MaxAI web-app signing constants — extracted live from the public JS bundle.
|
||||
*
|
||||
* MaxAI's request signer needs a small set of CLIENT-SIDE constants that its own
|
||||
* front-end ships VERBATIM in the public `www.maxai.co` JavaScript bundle
|
||||
* (identical for every visitor, no per-user or server secret). OmniRoute EXTRACTS
|
||||
* them from the live bundle and persists them, so if MaxAI ever rotates a value —
|
||||
* or a Next.js rebuild renumbers its chunks — the provider self-heals on the next
|
||||
* login or daily refresh instead of hard-failing every signed call.
|
||||
*
|
||||
* NOTHING id/key/version-shaped is hardcoded anywhere (source OR tests). Every
|
||||
* such value (hmacKey, aesKey, docIdKey, ctxKey, appVersion) is discovered at
|
||||
* runtime and validated; the repo carries no scannable secret and no build-
|
||||
* specific chunk number.
|
||||
*
|
||||
* WHAT is extracted, and from WHERE (all are plain, public static assets):
|
||||
* pages/_app-*.js — the Next.js app-entry chunk (framework-STABLE name, not a
|
||||
* MaxAI chunk number). Webpack module 69319 inside it defines the constants as
|
||||
* export getters we follow to their string literals:
|
||||
* - hmacKey export `Mn` → a hex string (HMAC-SHA1 → SM3 keying)
|
||||
* - aesKey export `Rl` → a hex string (CryptoJS AES passphrase)
|
||||
* - docIdKey export `U0` → a UUID (doc-upload HMAC key)
|
||||
* - appVersion the sole `webpage_x.y.z` literal (folded into the sign_str)
|
||||
* the SIGNER chunk — a NUMBERED chunk whose id changes across builds, so it is
|
||||
* located by CONTENT FINGERPRINT (never by number): the chunk that assembles
|
||||
* the signed payload, recognised by the ctx-slot pattern `"<40hex>":{a:…}` next
|
||||
* to the `(0,r.nj)("<hex>")` header-name decoders. From it we read:
|
||||
* - ctxKey the 40-hex payload content-slot label
|
||||
* - headerNames the `nj("<hex>")` calls = hex→ASCII header/slot names
|
||||
*
|
||||
* The extracted set is SHAPE-validated (hex/UUID/version regexes) before it is
|
||||
* trusted; the ULTIMATE validation is the first live signed call (a wrong value
|
||||
* is rejected by MaxAI, which triggers a re-extract). Only the plain, non-secret
|
||||
* HTTP header NAMES (e.g. "X-Authorization") keep in-code defaults, so a transient
|
||||
* miss on the signer chunk can't break a signer that already has valid keys;
|
||||
* extraction still overrides them when present.
|
||||
*/
|
||||
import { createHmac, createHash } from "node:crypto";
|
||||
|
||||
/** The public bundle base. `/app/` is the SPA entry that references the chunks. */
|
||||
export const MAXAI_WEBAPP_ORIGIN = "https://www.maxai.co";
|
||||
export const MAXAI_WEBAPP_APP_PATH = "/app/";
|
||||
|
||||
/** Settings key under which the extracted constants bundle is persisted. */
|
||||
export const MAXAI_CONSTANTS_SETTINGS_KEY = "maxaiSigningConstants";
|
||||
|
||||
/** Firefox-150 UA used for the (unauthenticated) static-asset fetches. */
|
||||
const FETCH_UA =
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:150.0) Gecko/20100101 Firefox/150.0";
|
||||
|
||||
/**
|
||||
* The header/slot NAMES the signer emits. These are standard HTTP header names
|
||||
* (not secrets, not id/key/version-shaped), so in-code defaults are appropriate;
|
||||
* extraction overrides any that the signer chunk exposes.
|
||||
*/
|
||||
export interface MaxaiHeaderNames {
|
||||
authorization: string; // "X-Authorization"
|
||||
clientDomain: string; // "X-Client-Domain"
|
||||
clientPath: string; // "X-Client-Path"
|
||||
random: string; // "X-Random"
|
||||
browserName: string; // "X-Browser-Name"
|
||||
browserVersion: string; // "X-Browser-Version"
|
||||
browserMajor: string; // "X-Browser-Major"
|
||||
appVersionHeader: string; // "X-App-Version"
|
||||
appEnvHeader: string; // "X-App-Env"
|
||||
appEnvValue: string; // "MaxAI-Browser-Extension"
|
||||
tSlot: string; // "t"
|
||||
pSlot: string; // "p"
|
||||
dSlot: string; // "d"
|
||||
}
|
||||
|
||||
/** The full set of signing constants the MaxAI signer depends on. */
|
||||
export interface MaxaiSigningConstants {
|
||||
/** HMAC-SHA1 → SM3 keying material (extracted; no in-code default). */
|
||||
hmacKey: string;
|
||||
/** CryptoJS AES passphrase (extracted; no in-code default). */
|
||||
aesKey: string;
|
||||
/** Version string folded into the signature `sign_str` (extracted). */
|
||||
appVersion: string;
|
||||
/** Payload content-slot label, 40-hex (extracted; no in-code default). */
|
||||
ctxKey: string;
|
||||
/** Doc-upload HMAC key, UUID (extracted; no in-code default). */
|
||||
docIdKey: string;
|
||||
/** Header/slot names emitted by the signer. */
|
||||
headerNames: MaxaiHeaderNames;
|
||||
/** Provenance for the persisted record. */
|
||||
source?: "extracted";
|
||||
extractedAt?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Default HTTP header NAMES (standard, non-secret labels). Extraction overrides
|
||||
* any the signer chunk exposes; these keep a signer with valid keys working even
|
||||
* if the signer chunk momentarily can't be located.
|
||||
*/
|
||||
export const MAXAI_DEFAULT_HEADER_NAMES: MaxaiHeaderNames = {
|
||||
authorization: "X-Authorization",
|
||||
clientDomain: "X-Client-Domain",
|
||||
clientPath: "X-Client-Path",
|
||||
random: "X-Random",
|
||||
browserName: "X-Browser-Name",
|
||||
browserVersion: "X-Browser-Version",
|
||||
browserMajor: "X-Browser-Major",
|
||||
appVersionHeader: "X-App-Version",
|
||||
appEnvHeader: "X-App-Env",
|
||||
appEnvValue: "MaxAI-Browser-Extension",
|
||||
tSlot: "t",
|
||||
pSlot: "p",
|
||||
dSlot: "d",
|
||||
};
|
||||
|
||||
/** Raw pieces the parser can pull from the two chunks (any may be absent). */
|
||||
export interface MaxaiParsedConstants {
|
||||
hmacKey: string | null;
|
||||
aesKey: string | null;
|
||||
appVersion: string | null;
|
||||
ctxKey: string | null;
|
||||
docIdKey: string | null;
|
||||
headerNames: Partial<MaxaiHeaderNames>;
|
||||
}
|
||||
|
||||
/** Resolve a webpack export getter `Name:function(){return VAR}` → the `VAR="…"` literal. */
|
||||
export function resolveWebpackGetter(src: string, exportName: string): string | null {
|
||||
const getter = new RegExp(
|
||||
`${exportName}\\s*:\\s*function\\s*\\(\\)\\s*\\{\\s*return\\s+([A-Za-z_$][\\w$]*)\\s*\\}`
|
||||
);
|
||||
let m = src.match(getter);
|
||||
if (!m) {
|
||||
const arrow = new RegExp(`${exportName}\\s*:\\s*\\(\\)\\s*=>\\s*([A-Za-z_$][\\w$]*)`);
|
||||
m = src.match(arrow);
|
||||
}
|
||||
if (!m) return null;
|
||||
const varName = m[1];
|
||||
const assign = new RegExp(`\\b${varName}\\s*=\\s*"([^"]+)"`);
|
||||
const am = src.match(assign);
|
||||
return am ? am[1] : null;
|
||||
}
|
||||
|
||||
/** Decode the `(0,r.nj)("<hex>")` header-name calls (nj = hex→ASCII). */
|
||||
export function decodeNjHeaderNames(signerChunk: string): string[] {
|
||||
const out = new Set<string>();
|
||||
for (const m of signerChunk.matchAll(/nj\)\("([0-9a-f]+)"\)/g)) {
|
||||
try {
|
||||
const decoded = Buffer.from(m[1], "hex").toString("utf8");
|
||||
// Keep only printable ASCII header-ish tokens (drop numeric ja3 codes etc).
|
||||
if (/^[\x20-\x7e]+$/.test(decoded)) out.add(decoded);
|
||||
} catch {
|
||||
// skip malformed hex
|
||||
}
|
||||
}
|
||||
return [...out];
|
||||
}
|
||||
|
||||
/** Map the decoded header-name list onto the structured MaxaiHeaderNames slots. */
|
||||
function mapHeaderNames(decoded: string[]): Partial<MaxaiHeaderNames> {
|
||||
const has = (v: string) => decoded.includes(v);
|
||||
const out: Partial<MaxaiHeaderNames> = {};
|
||||
if (has("X-Authorization")) out.authorization = "X-Authorization";
|
||||
if (has("X-Client-Domain")) out.clientDomain = "X-Client-Domain";
|
||||
if (has("X-Client-Path")) out.clientPath = "X-Client-Path";
|
||||
if (has("X-Random")) out.random = "X-Random";
|
||||
if (has("X-Browser-Name")) out.browserName = "X-Browser-Name";
|
||||
if (has("X-Browser-Version")) out.browserVersion = "X-Browser-Version";
|
||||
if (has("X-Browser-Major")) out.browserMajor = "X-Browser-Major";
|
||||
if (has("X-App-Version")) out.appVersionHeader = "X-App-Version";
|
||||
if (has("X-App-Env")) out.appEnvHeader = "X-App-Env";
|
||||
if (has("MaxAI-Browser-Extension")) out.appEnvValue = "MaxAI-Browser-Extension";
|
||||
return out;
|
||||
}
|
||||
|
||||
/** Extract the 40-hex payload content-slot label from the signer chunk. */
|
||||
export function extractCtxKey(signerChunk: string): string | null {
|
||||
return (signerChunk.match(/"([0-9a-f]{40})"\s*:\s*\{\s*a\s*:/) || [])[1] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Content fingerprint for the SIGNER chunk (build-independent). The signer chunk
|
||||
* is the one that both (a) carries the ctx payload slot `"<40hex>":{a:…}` and
|
||||
* (b) decodes header names via `(0,r.nj)("<hex>")`. Matching BOTH avoids a false
|
||||
* positive on any unrelated chunk that merely contains a 40-hex string.
|
||||
*/
|
||||
export function looksLikeSignerChunk(js: string): boolean {
|
||||
return extractCtxKey(js) !== null && /nj\)\("[0-9a-f]+"\)/.test(js);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the two bundle chunks into raw constants. Pure (no network) so it is
|
||||
* unit-tested directly against synthetic fixtures.
|
||||
*/
|
||||
export function parseMaxaiConstants(
|
||||
appChunk: string,
|
||||
signerChunk: string
|
||||
): MaxaiParsedConstants {
|
||||
const decoded = decodeNjHeaderNames(signerChunk);
|
||||
return {
|
||||
hmacKey: resolveWebpackGetter(appChunk, "Mn"),
|
||||
aesKey: resolveWebpackGetter(appChunk, "Rl"),
|
||||
docIdKey: resolveWebpackGetter(appChunk, "U0"),
|
||||
appVersion: (appChunk.match(/"(webpage_\d+\.\d+\.\d+)"/) || [])[1] ?? null,
|
||||
ctxKey: extractCtxKey(signerChunk),
|
||||
headerNames: mapHeaderNames(decoded),
|
||||
};
|
||||
}
|
||||
|
||||
/** A MaxAI signing key is a 40+ char lowercase hex string. */
|
||||
function isHexKey(v: string | null | undefined): boolean {
|
||||
return typeof v === "string" && /^[0-9a-f]{40,}$/.test(v);
|
||||
}
|
||||
|
||||
/** A doc-id key is a UUID (v4-shaped). */
|
||||
function isUuidKey(v: string | null | undefined): boolean {
|
||||
return typeof v === "string" && /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/.test(v);
|
||||
}
|
||||
|
||||
/** A MaxAI app_version tag looks like `webpage_x.y.z`. */
|
||||
function isAppVersion(v: string | null | undefined): boolean {
|
||||
return typeof v === "string" && /^webpage_\d+\.\d+\.\d+$/.test(v);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fold parsed pieces into a full constants object. The five extracted values
|
||||
* (hmacKey, aesKey, ctxKey, docIdKey, appVersion) are ALL required and must be
|
||||
* well-formed — return null otherwise, so we never persist a half-configured
|
||||
* signer. Only the plain HTTP header names fall back to the standard defaults.
|
||||
*/
|
||||
export function assembleMaxaiConstants(
|
||||
parsed: MaxaiParsedConstants
|
||||
): MaxaiSigningConstants | null {
|
||||
if (!isHexKey(parsed.hmacKey) || !isHexKey(parsed.aesKey)) return null;
|
||||
if (!isHexKey(parsed.ctxKey)) return null;
|
||||
if (!isUuidKey(parsed.docIdKey)) return null;
|
||||
if (!isAppVersion(parsed.appVersion)) return null;
|
||||
return {
|
||||
hmacKey: parsed.hmacKey as string,
|
||||
aesKey: parsed.aesKey as string,
|
||||
appVersion: parsed.appVersion as string,
|
||||
ctxKey: parsed.ctxKey as string,
|
||||
docIdKey: parsed.docIdKey as string,
|
||||
headerNames: { ...MAXAI_DEFAULT_HEADER_NAMES, ...parsed.headerNames },
|
||||
source: "extracted",
|
||||
extractedAt: Date.now(),
|
||||
};
|
||||
}
|
||||
|
||||
/** True when a constants object is structurally well-formed (all 5 values valid). */
|
||||
export function isValidConstantsShape(c: MaxaiSigningConstants | null | undefined): boolean {
|
||||
if (!c) return false;
|
||||
return (
|
||||
isHexKey(c.hmacKey) &&
|
||||
isHexKey(c.aesKey) &&
|
||||
isHexKey(c.ctxKey) &&
|
||||
isUuidKey(c.docIdKey) &&
|
||||
isAppVersion(c.appVersion) &&
|
||||
!!c.headerNames
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* A signature vector: a (path, reqTime, userId, appVersion) tuple and the SM3
|
||||
* proof it should produce. Used to prove the signing ALGORITHM in unit tests with
|
||||
* mock keys — the runtime does NOT embed any real vector (its trust anchor is the
|
||||
* live signed probe). `reproduceProof` is a pure helper over the same math.
|
||||
*/
|
||||
export interface MaxaiSignatureVector {
|
||||
path: string;
|
||||
reqTime: number;
|
||||
userId: string;
|
||||
appVersion: string;
|
||||
expectedProof: string;
|
||||
}
|
||||
|
||||
/** Reproduce the SM3 proof `p` for a (path, reqTime, userId, appVersion) under a key. */
|
||||
export function reproduceProof(
|
||||
hmacKey: string,
|
||||
vector: Omit<MaxaiSignatureVector, "expectedProof">
|
||||
): string {
|
||||
const signStr = `${vector.appVersion}:${vector.reqTime}:${vector.path}:${vector.userId}`;
|
||||
const sha1 = createHmac("sha1", Buffer.from(`${vector.reqTime}:${hmacKey}`, "utf8"))
|
||||
.update(Buffer.from(signStr, "utf8"))
|
||||
.digest("hex");
|
||||
return createHash("sm3")
|
||||
.update(Buffer.from(`${vector.reqTime}:${sha1}:${hmacKey}`, "utf8"))
|
||||
.digest("hex");
|
||||
}
|
||||
|
||||
/**
|
||||
* Runtime validation of an extracted/stored constants set. SHAPE-based on purpose:
|
||||
* we carry no real signature vector in source, so the definitive check is the
|
||||
* first live signed call (a wrong value is rejected by MaxAI → re-extract). An
|
||||
* optional `vector` enables proof-based checking in tests with mock keys.
|
||||
*/
|
||||
export function validateMaxaiConstants(
|
||||
constants: MaxaiSigningConstants,
|
||||
vector?: MaxaiSignatureVector
|
||||
): boolean {
|
||||
if (!isValidConstantsShape(constants)) return false;
|
||||
if (!vector) return true;
|
||||
try {
|
||||
return reproduceProof(constants.hmacKey, vector) === vector.expectedProof;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch a text asset with the Firefox UA through the ambient (residential) fetch.
|
||||
* Injectable for tests. Returns "" on any failure (caller treats empty as miss).
|
||||
*/
|
||||
async function fetchText(
|
||||
url: string,
|
||||
fetchImpl: typeof fetch,
|
||||
signal?: AbortSignal | null
|
||||
): Promise<string> {
|
||||
try {
|
||||
const res = await fetchImpl(url, {
|
||||
headers: { "User-Agent": FETCH_UA, Accept: "*/*" },
|
||||
signal: signal ?? undefined,
|
||||
});
|
||||
if (!res.ok) return "";
|
||||
return await res.text();
|
||||
} catch {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
/** All `/_next/static/chunks/...js` URLs referenced by the app HTML, in order. */
|
||||
export function allChunkUrls(html: string): string[] {
|
||||
const seen = new Set<string>();
|
||||
const out: string[] = [];
|
||||
for (const m of html.matchAll(/\/_next\/static\/chunks\/[A-Za-z0-9/_-]+\.js/g)) {
|
||||
if (!seen.has(m[0])) {
|
||||
seen.add(m[0]);
|
||||
out.push(m[0]);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* From the `/app/` HTML, resolve the app-entry chunk (by its stable Next.js
|
||||
* `pages/_app-*.js` name) and the list of candidate numbered chunks to scan for
|
||||
* the signer chunk BY CONTENT. No specific chunk number is ever assumed.
|
||||
*/
|
||||
export function findChunkUrls(html: string): {
|
||||
appChunk: string | null;
|
||||
candidateChunks: string[];
|
||||
} {
|
||||
const urls = allChunkUrls(html);
|
||||
let appChunk: string | null = null;
|
||||
const candidateChunks: string[] = [];
|
||||
for (const p of urls) {
|
||||
if (/\/pages\/_app-[a-z0-9]+\.js$/i.test(p)) {
|
||||
appChunk = p;
|
||||
} else if (/\/chunks\/[A-Za-z0-9]+-[a-z0-9]+\.js$/i.test(p)) {
|
||||
// Any hashed vendor/number chunk is a signer-chunk candidate; we identify
|
||||
// the real one by content, not by its (build-specific) name.
|
||||
candidateChunks.push(p);
|
||||
}
|
||||
}
|
||||
return { appChunk, candidateChunks };
|
||||
}
|
||||
|
||||
export interface FetchConstantsOptions {
|
||||
fetchImpl?: typeof fetch;
|
||||
signal?: AbortSignal | null;
|
||||
/** Override the origin (tests). */
|
||||
origin?: string;
|
||||
/** Cap on how many candidate chunks to scan for the signer chunk (default 80). */
|
||||
maxScanChunks?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Locate + fetch the signer chunk text by CONTENT (never by number): scan the
|
||||
* candidate chunks referenced in the app HTML and return the first whose content
|
||||
* matches the signer fingerprint (ctx slot + nj header decoders). A MaxAI-side
|
||||
* chunk renumber is therefore self-healing, not a break.
|
||||
*/
|
||||
async function fetchSignerChunk(
|
||||
origin: string,
|
||||
candidates: string[],
|
||||
fetchImpl: typeof fetch,
|
||||
signal: AbortSignal | null | undefined,
|
||||
maxScan: number
|
||||
): Promise<string> {
|
||||
for (const c of candidates.slice(0, maxScan)) {
|
||||
const js = await fetchText(origin + c, fetchImpl, signal);
|
||||
if (js && looksLikeSignerChunk(js)) return js;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch + parse the live constants from MaxAI's public bundle. Returns a fully
|
||||
* assembled, SHAPE-validated constants object, or null on any failure (network,
|
||||
* missing chunk, unparseable, malformed values). Never throws. The definitive
|
||||
* key validation is the caller's first live signed call.
|
||||
*/
|
||||
export async function fetchMaxaiConstants(
|
||||
opts: FetchConstantsOptions = {}
|
||||
): Promise<MaxaiSigningConstants | null> {
|
||||
const fetchImpl = opts.fetchImpl ?? fetch;
|
||||
const origin = opts.origin ?? MAXAI_WEBAPP_ORIGIN;
|
||||
const maxScan = opts.maxScanChunks ?? 80;
|
||||
|
||||
const html = await fetchText(origin + MAXAI_WEBAPP_APP_PATH, fetchImpl, opts.signal);
|
||||
if (!html) return null;
|
||||
|
||||
const { appChunk, candidateChunks } = findChunkUrls(html);
|
||||
if (!appChunk) return null;
|
||||
|
||||
const appJs = await fetchText(origin + appChunk, fetchImpl, opts.signal);
|
||||
if (!appJs) return null;
|
||||
|
||||
const signerJs = await fetchSignerChunk(
|
||||
origin,
|
||||
candidateChunks,
|
||||
fetchImpl,
|
||||
opts.signal,
|
||||
maxScan
|
||||
);
|
||||
|
||||
const parsed = parseMaxaiConstants(appJs, signerJs);
|
||||
const assembled = assembleMaxaiConstants(parsed);
|
||||
if (!assembled) return null;
|
||||
if (!validateMaxaiConstants(assembled)) return null;
|
||||
return assembled;
|
||||
}
|
||||
156
open-sse/executors/maxai/constantsStore.ts
Normal file
@@ -0,0 +1,156 @@
|
||||
/**
|
||||
* MaxAI signing-constants store + `ensure` gate.
|
||||
*
|
||||
* This is the persistence + freshness layer around ./constants.ts:
|
||||
* - `getStoredMaxaiConstants()` reads the last-extracted, validated constants
|
||||
* from OmniRoute settings (the sole source of the two secret-shaped keys).
|
||||
* - `persistMaxaiConstants()` writes a freshly-extracted+validated set.
|
||||
* - `ensureMaxaiConstants()` is the gate every signed path calls: it returns a
|
||||
* usable constants object, extracting + persisting on a cold store, and is
|
||||
* cheap (in-process memo) on the hot path.
|
||||
* - `refreshMaxaiConstants()` force re-extracts (used by the daily token
|
||||
* refresh) so a MaxAI-side rotation is picked up within a day.
|
||||
*
|
||||
* Design (William's Option 2): there is NO hardcoded fallback for the secret
|
||||
* keys. If the store is empty AND a live extraction cannot be validated, the
|
||||
* signer has no keys and MaxAI is simply unconfigured (callers surface a clear
|
||||
* auth error) — we never sign with a guessed/stale secret.
|
||||
*/
|
||||
import type { MaxaiSigningConstants, FetchConstantsOptions } from "./constants.ts";
|
||||
import {
|
||||
MAXAI_CONSTANTS_SETTINGS_KEY,
|
||||
fetchMaxaiConstants,
|
||||
validateMaxaiConstants,
|
||||
MAXAI_DEFAULT_HEADER_NAMES,
|
||||
} from "./constants.ts";
|
||||
|
||||
/** In-process memo so the hot signing path never touches the DB or network. */
|
||||
let memo: MaxaiSigningConstants | null = null;
|
||||
let inflight: Promise<MaxaiSigningConstants | null> | null = null;
|
||||
|
||||
/** Reset the in-process memo (tests + after a forced refresh). */
|
||||
export function resetMaxaiConstantsMemo(): void {
|
||||
memo = null;
|
||||
inflight = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Test seam: directly seed the in-process memo so unit tests that exercise the
|
||||
* signed network functions don't need to also mock the bundle fetch. Not used in
|
||||
* production paths (production goes through ensure/refresh → store → extraction).
|
||||
*/
|
||||
export function __setMaxaiConstantsForTest(constants: MaxaiSigningConstants | null): void {
|
||||
memo = constants;
|
||||
inflight = null;
|
||||
}
|
||||
|
||||
/** Shape-guard a persisted record before trusting it. */
|
||||
function isUsableConstants(v: unknown): v is MaxaiSigningConstants {
|
||||
if (!v || typeof v !== "object") return false;
|
||||
const c = v as Partial<MaxaiSigningConstants>;
|
||||
return (
|
||||
typeof c.hmacKey === "string" &&
|
||||
typeof c.aesKey === "string" &&
|
||||
typeof c.appVersion === "string" &&
|
||||
typeof c.ctxKey === "string" &&
|
||||
typeof c.docIdKey === "string" &&
|
||||
!!c.headerNames &&
|
||||
typeof c.headerNames === "object"
|
||||
);
|
||||
}
|
||||
|
||||
/** Read the persisted constants from settings (validated). Null when absent/invalid. */
|
||||
export async function getStoredMaxaiConstants(): Promise<MaxaiSigningConstants | null> {
|
||||
try {
|
||||
const { getSettings } = await import("@/lib/db/settings");
|
||||
const settings = await getSettings();
|
||||
const raw = (settings as Record<string, unknown>)[MAXAI_CONSTANTS_SETTINGS_KEY];
|
||||
if (!isUsableConstants(raw)) return null;
|
||||
// Re-validate on read: a persisted record must still reproduce the vector.
|
||||
const withDefaults: MaxaiSigningConstants = {
|
||||
...raw,
|
||||
headerNames: { ...MAXAI_DEFAULT_HEADER_NAMES, ...raw.headerNames },
|
||||
};
|
||||
return validateMaxaiConstants(withDefaults) ? withDefaults : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/** Persist a freshly-extracted+validated constants set to settings. */
|
||||
export async function persistMaxaiConstants(
|
||||
constants: MaxaiSigningConstants
|
||||
): Promise<void> {
|
||||
try {
|
||||
const { updateSettings } = await import("@/lib/db/settings");
|
||||
await updateSettings({ [MAXAI_CONSTANTS_SETTINGS_KEY]: constants });
|
||||
} catch {
|
||||
// Non-fatal: a persist failure just means the next process re-extracts.
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return usable MaxAI signing constants, extracting + persisting on a cold store.
|
||||
* Order: in-process memo → persisted store → live extraction (validated) → null.
|
||||
* Concurrent callers share a single in-flight extraction. Never throws.
|
||||
*/
|
||||
export async function ensureMaxaiConstants(
|
||||
opts: FetchConstantsOptions = {}
|
||||
): Promise<MaxaiSigningConstants | null> {
|
||||
if (memo) return memo;
|
||||
|
||||
const stored = await getStoredMaxaiConstants();
|
||||
if (stored) {
|
||||
memo = stored;
|
||||
return memo;
|
||||
}
|
||||
|
||||
if (inflight) return inflight;
|
||||
inflight = (async () => {
|
||||
try {
|
||||
const fresh = await fetchMaxaiConstants(opts);
|
||||
if (fresh) {
|
||||
memo = fresh;
|
||||
await persistMaxaiConstants(fresh);
|
||||
return fresh;
|
||||
}
|
||||
return null;
|
||||
} finally {
|
||||
inflight = null;
|
||||
}
|
||||
})();
|
||||
return inflight;
|
||||
}
|
||||
|
||||
/**
|
||||
* Force a live re-extraction (used by the daily token refresh). If the fetched
|
||||
* set validates AND differs from what's stored, it is persisted + memoized so a
|
||||
* MaxAI-side rotation is picked up. Returns the current-best constants (the fresh
|
||||
* set on success, else whatever was already stored/memoized). Never throws.
|
||||
*/
|
||||
export async function refreshMaxaiConstants(
|
||||
opts: FetchConstantsOptions = {}
|
||||
): Promise<MaxaiSigningConstants | null> {
|
||||
let fresh: MaxaiSigningConstants | null = null;
|
||||
try {
|
||||
fresh = await fetchMaxaiConstants(opts);
|
||||
} catch {
|
||||
fresh = null;
|
||||
}
|
||||
|
||||
if (fresh) {
|
||||
const changed =
|
||||
!memo ||
|
||||
memo.hmacKey !== fresh.hmacKey ||
|
||||
memo.aesKey !== fresh.aesKey ||
|
||||
memo.appVersion !== fresh.appVersion ||
|
||||
memo.ctxKey !== fresh.ctxKey ||
|
||||
memo.docIdKey !== fresh.docIdKey;
|
||||
memo = fresh;
|
||||
if (changed) await persistMaxaiConstants(fresh);
|
||||
return fresh;
|
||||
}
|
||||
|
||||
// Fetch failed — keep serving whatever we already have (memo or store).
|
||||
return memo ?? (await getStoredMaxaiConstants());
|
||||
}
|
||||
96
open-sse/executors/maxai/credentials.ts
Normal file
@@ -0,0 +1,96 @@
|
||||
/**
|
||||
* MaxAI connection credential resolution.
|
||||
*
|
||||
* MaxAI's request signer needs three things bound together: the OpenAI-style
|
||||
* `access_token` (Bearer, ~24h), the `device_id` that minted it (embedded in the
|
||||
* signed `X-Authorization` — a mismatch is rejected), and the `user_id` (folded
|
||||
* into the signature proof). OmniRoute stores these in the connection's
|
||||
* `providerSpecificData` (minted by OmniRoute's own browser-mint flow — see
|
||||
* maxaiBrowserLogin), so the router is self-contained and never reads any
|
||||
* external (Hermes) token file.
|
||||
*
|
||||
* The access token is refreshed out-of-band by the browser-mint (the
|
||||
* `/oauth/refresh_access_token` endpoint is deep-TLS-gated and cannot be called
|
||||
* by any HTTP client — only a real browser passes), so this module only READS
|
||||
* the stored credential; it does not attempt an HTTP refresh.
|
||||
*/
|
||||
|
||||
export interface MaxaiCredential {
|
||||
accessToken: string;
|
||||
deviceId: string;
|
||||
userId: string;
|
||||
/** ~1-year refresh token used for browserless access-token refresh (optional). */
|
||||
refreshToken?: string;
|
||||
}
|
||||
|
||||
type ProviderSpecificData = Record<string, unknown> | null | undefined;
|
||||
|
||||
function firstString(...values: unknown[]): string | null {
|
||||
for (const v of values) {
|
||||
if (typeof v === "string") {
|
||||
// Raw browser LocalStorage sometimes wraps the device id in quotes.
|
||||
const trimmed = v.trim().replace(/^"|"$/g, "");
|
||||
if (trimmed.length > 0) return trimmed;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Decode the `user_id` from a MaxAI access JWT (subject.user_id or sub). No verify. */
|
||||
export function userIdFromJwt(accessToken: string): string | null {
|
||||
try {
|
||||
const seg = accessToken.split(".")[1];
|
||||
if (!seg) return null;
|
||||
const b64 = seg.replace(/-/g, "+").replace(/_/g, "/") + "=".repeat((4 - (seg.length % 4)) % 4);
|
||||
const claims = JSON.parse(Buffer.from(b64, "base64").toString("utf8"));
|
||||
const subject = claims?.subject as { user_id?: unknown } | undefined;
|
||||
if (typeof subject?.user_id === "string") return subject.user_id;
|
||||
if (typeof claims?.sub === "string") return claims.sub;
|
||||
return null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/** Epoch seconds of the access-JWT `exp`, or 0 when undecodable. */
|
||||
export function accessTokenExpiry(accessToken: string): number {
|
||||
try {
|
||||
const seg = accessToken.split(".")[1];
|
||||
if (!seg) return 0;
|
||||
const b64 = seg.replace(/-/g, "+").replace(/_/g, "/") + "=".repeat((4 - (seg.length % 4)) % 4);
|
||||
const claims = JSON.parse(Buffer.from(b64, "base64").toString("utf8"));
|
||||
return typeof claims?.exp === "number" ? claims.exp : 0;
|
||||
} catch {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the MaxAI credential from a connection's providerSpecificData (with the
|
||||
* OpenAI-style `access_token` optionally supplied separately by the caller, which
|
||||
* is how OmniRoute threads the stored connection token). Returns null when not
|
||||
* fully configured (all three of accessToken/deviceId/userId required).
|
||||
*/
|
||||
export function resolveMaxaiCredential(
|
||||
psd: ProviderSpecificData,
|
||||
accessTokenFromConnection?: string | null
|
||||
): MaxaiCredential | null {
|
||||
const accessToken = firstString(
|
||||
accessTokenFromConnection,
|
||||
psd?.maxaiAccessToken,
|
||||
psd?.accessToken
|
||||
);
|
||||
if (!accessToken) return null;
|
||||
|
||||
const deviceId = firstString(psd?.maxaiDeviceId, psd?.deviceId);
|
||||
if (!deviceId) return null;
|
||||
|
||||
const userId =
|
||||
firstString(psd?.maxaiUserId, psd?.userId) ?? userIdFromJwt(accessToken);
|
||||
if (!userId) return null;
|
||||
|
||||
const refreshToken =
|
||||
firstString(psd?.maxaiRefreshToken, psd?.refreshToken) ?? undefined;
|
||||
|
||||
return { accessToken, deviceId, userId, refreshToken };
|
||||
}
|
||||
266
open-sse/executors/maxai/documents.ts
Normal file
@@ -0,0 +1,266 @@
|
||||
/**
|
||||
* MaxAI doc-RAG — inline document parts → /app/upload_document → doc_list.
|
||||
*
|
||||
* OmniRoute delivers attached documents INLINE in the chat request as base64
|
||||
* `file_data` content parts (OpenAI `{type:"file",file:{filename,file_data}}` /
|
||||
* Responses `{type:"input_file",file_data}` / Claude `{type:"document",source}`).
|
||||
* MaxAI's `/gpt/cwc/chat` cannot take binary docs inline; instead it references
|
||||
* uploaded documents by a content-addressed `doc_id`. This module bridges the
|
||||
* two: it detects inline base64 doc parts on the current turn, uploads each via
|
||||
* the multipart `/app/upload_document` endpoint (signed like every MaxAI call),
|
||||
* and returns the `doc_list` entries to attach to the chat body.
|
||||
*
|
||||
* doc_id is NOT random — MaxAI requires `doc_id = HMAC-SHA1(file_bytes, IT)` hex
|
||||
* (createDocId/qM in the extension). A random id is rejected with a 400
|
||||
* "Inconsistency between server doc_id and request doc_id". The IT key is a
|
||||
* public web-app constant (ships in the bundle), same class as the signing
|
||||
* constants; kept here as a named constant (not a secret).
|
||||
*
|
||||
* The doc_list item shape is exactly what the live web app sends
|
||||
* (site chunk 41068): `{ doc_id, doc_type, file_name }`.
|
||||
*/
|
||||
import { createHmac } from "node:crypto";
|
||||
import { buildMaxaiSignedHeaders } from "./signing.ts";
|
||||
import { ensureMaxaiConstants } from "./constantsStore.ts";
|
||||
import { maxaiStaticHeaders, MAXAI_BASE_URL } from "./protocol.ts";
|
||||
|
||||
export const MAXAI_UPLOAD_PATH = "/app/upload_document";
|
||||
|
||||
export interface MaxaiDocListEntry {
|
||||
doc_id: string;
|
||||
doc_type: string;
|
||||
file_name: string;
|
||||
}
|
||||
|
||||
/** An inline document extracted from an OpenAI/Responses/Claude content part. */
|
||||
export interface InlineDoc {
|
||||
filename: string;
|
||||
mimeType: string;
|
||||
bytes: Buffer;
|
||||
}
|
||||
|
||||
/** doc_id = HMAC-SHA1(file_bytes, docIdKey) hex. Content-addressed; MaxAI verifies it. */
|
||||
export function computeMaxaiDocId(bytes: Buffer, key: string): string {
|
||||
if (!key) throw new Error("computeMaxaiDocId: missing docIdKey");
|
||||
return createHmac("sha1", key).update(bytes).digest("hex");
|
||||
}
|
||||
|
||||
const TEXTUAL_EXT = /\.(txt|md|markdown|csv|json|log|xml|yaml|yml|tsv)$/i;
|
||||
const CODE_EXT =
|
||||
/\.(py|ipynb|js|jsx|ts|tsx|html?|css|java|cs|php|c|cpp|cxx|h|hpp|go|rs|rb|swift|kt|sh|sql)$/i;
|
||||
|
||||
/** Classify the MaxAI doc_type from the filename/mime (extension taxonomy). */
|
||||
export function maxaiDocType(filename: string, mimeType: string): string {
|
||||
const f = filename.toLowerCase();
|
||||
if (/\.pdf$/i.test(f) || mimeType === "application/pdf") return "page_content__pdf";
|
||||
if (CODE_EXT.test(f)) return "chat_file_code";
|
||||
return "chat_file"; // text / generic
|
||||
}
|
||||
|
||||
/** Whether a doc_type requires the pure_text field (text-extractable docs). */
|
||||
function requiresPureText(docType: string): boolean {
|
||||
return docType === "chat_file" || docType === "chat_file_code";
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse an OpenAI/Responses/Claude data-URL into raw bytes + mime. Returns null
|
||||
* for anything that isn't an inline base64 payload (e.g. a remote URL or an
|
||||
* already-uploaded file_id reference, which this bridge does not handle).
|
||||
*/
|
||||
export function parseInlineDataUrl(dataUrl: unknown): { mimeType: string; bytes: Buffer } | null {
|
||||
if (typeof dataUrl !== "string") return null;
|
||||
const m = /^data:([^;,]*)(;base64)?,(.*)$/s.exec(dataUrl);
|
||||
if (!m) return null;
|
||||
const mimeType = m[1] || "application/octet-stream";
|
||||
const isBase64 = !!m[2];
|
||||
try {
|
||||
const bytes = isBase64
|
||||
? Buffer.from(m[3], "base64")
|
||||
: Buffer.from(decodeURIComponent(m[3]), "utf8");
|
||||
if (bytes.length === 0) return null;
|
||||
return { mimeType, bytes };
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract inline documents from the CURRENT (last user) turn of an OpenAI
|
||||
* messages[] array. Recognizes the three OmniRoute-delivered shapes:
|
||||
* OpenAI Chat: {type:"file", file:{filename, file_data|data}}
|
||||
* Responses: {type:"input_file", filename, file_data}
|
||||
* Claude: {type:"document", source:{type:"base64", media_type, data}}
|
||||
* Only base64/data-URL payloads are handled (a bridge upload needs the bytes).
|
||||
*/
|
||||
export function extractCurrentTurnDocs(
|
||||
messages: Array<{ role?: string; content?: unknown }>
|
||||
): InlineDoc[] {
|
||||
let content: unknown;
|
||||
for (let i = messages.length - 1; i >= 0; i--) {
|
||||
if (messages[i]?.role === "user") {
|
||||
content = messages[i]?.content;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!Array.isArray(content)) return [];
|
||||
const docs: InlineDoc[] = [];
|
||||
for (const part of content) {
|
||||
if (!part || typeof part !== "object") continue;
|
||||
const p = part as Record<string, unknown>;
|
||||
const type = p.type;
|
||||
|
||||
if (type === "file" && p.file && typeof p.file === "object") {
|
||||
const file = p.file as Record<string, unknown>;
|
||||
const filename = typeof file.filename === "string" ? file.filename : "upload.bin";
|
||||
const raw = (file.file_data ?? file.data) as unknown;
|
||||
const parsed = parseInlineDataUrl(raw);
|
||||
if (parsed) docs.push({ filename, mimeType: parsed.mimeType, bytes: parsed.bytes });
|
||||
} else if (type === "input_file") {
|
||||
const filename = typeof p.filename === "string" ? p.filename : "upload.bin";
|
||||
const parsed = parseInlineDataUrl(p.file_data);
|
||||
if (parsed) docs.push({ filename, mimeType: parsed.mimeType, bytes: parsed.bytes });
|
||||
} else if (type === "document" && p.source && typeof p.source === "object") {
|
||||
const source = p.source as Record<string, unknown>;
|
||||
if (source.type === "base64" && typeof source.data === "string") {
|
||||
const mimeType =
|
||||
typeof source.media_type === "string" ? source.media_type : "application/octet-stream";
|
||||
try {
|
||||
const bytes = Buffer.from(source.data, "base64");
|
||||
if (bytes.length > 0) {
|
||||
const filename =
|
||||
typeof p.title === "string" && p.title ? p.title : `document.${mimeExt(mimeType)}`;
|
||||
docs.push({ filename, mimeType, bytes });
|
||||
}
|
||||
} catch {
|
||||
/* skip malformed base64 */
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return docs;
|
||||
}
|
||||
|
||||
function mimeExt(mime: string): string {
|
||||
if (mime === "application/pdf") return "pdf";
|
||||
if (mime.startsWith("text/")) return "txt";
|
||||
return "bin";
|
||||
}
|
||||
|
||||
/** Rough ~4-chars/token estimate; ceil, never 0 for non-empty text. */
|
||||
function estimateTokens(text: string): number {
|
||||
return text ? Math.max(1, Math.ceil(text.length / 4)) : 0;
|
||||
}
|
||||
|
||||
/** Build the multipart/form-data body for /app/upload_document (fixed boundary). */
|
||||
export function buildUploadMultipart(
|
||||
doc: InlineDoc,
|
||||
docId: string,
|
||||
docType: string,
|
||||
boundary: string
|
||||
): Buffer {
|
||||
const isTextual =
|
||||
requiresPureText(docType) &&
|
||||
(TEXTUAL_EXT.test(doc.filename) ||
|
||||
CODE_EXT.test(doc.filename) ||
|
||||
doc.mimeType.startsWith("text/"));
|
||||
const pureText = isTextual ? doc.bytes.toString("utf8") : "";
|
||||
const tokens = String(estimateTokens(pureText));
|
||||
|
||||
const parts: Buffer[] = [];
|
||||
const dash = `--${boundary}\r\n`;
|
||||
const field = (name: string, value: string): void => {
|
||||
parts.push(
|
||||
Buffer.from(`${dash}Content-Disposition: form-data; name="${name}"\r\n\r\n${value}\r\n`)
|
||||
);
|
||||
};
|
||||
field("doc_id", docId);
|
||||
field("doc_type", docType);
|
||||
field("pure_text", pureText);
|
||||
field("tokens", tokens);
|
||||
field("doc_type_dependent_data", "{}");
|
||||
// The file part carries the raw bytes with a content-type.
|
||||
parts.push(
|
||||
Buffer.from(
|
||||
`${dash}Content-Disposition: form-data; name="file"; filename="${doc.filename.replace(/"/g, "")}"\r\n` +
|
||||
`Content-Type: ${doc.mimeType}\r\n\r\n`
|
||||
)
|
||||
);
|
||||
parts.push(doc.bytes);
|
||||
parts.push(Buffer.from(`\r\n--${boundary}--\r\n`));
|
||||
return Buffer.concat(parts);
|
||||
}
|
||||
|
||||
/** True if any SSE frame in the response is the terminal upload_done event. */
|
||||
export function sawUploadDone(text: string): boolean {
|
||||
return /"event"\s*:\s*"upload_done"/.test(text) || text.includes("upload_done");
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload one inline document to MaxAI and return its doc_list entry, or null on
|
||||
* failure (upload failures are non-fatal: the chat proceeds without the doc).
|
||||
*/
|
||||
export async function uploadMaxaiDocument(
|
||||
doc: InlineDoc,
|
||||
auth: { accessToken: string; userId: string; deviceId: string },
|
||||
opts?: { fetchImpl?: typeof fetch; signal?: AbortSignal }
|
||||
): Promise<MaxaiDocListEntry | null> {
|
||||
const fetchImpl = opts?.fetchImpl ?? fetch;
|
||||
const constants = await ensureMaxaiConstants({ fetchImpl, signal: opts?.signal });
|
||||
if (!constants) return null;
|
||||
const docId = computeMaxaiDocId(doc.bytes, constants.docIdKey);
|
||||
const docType = maxaiDocType(doc.filename, doc.mimeType);
|
||||
const boundary = `----maxai${Date.now().toString(16)}${Math.random().toString(16).slice(2)}`;
|
||||
const bodyBuf = buildUploadMultipart(doc, docId, docType, boundary);
|
||||
|
||||
// Sign like any request, but DROP the JSON content-type so we can set the
|
||||
// multipart boundary content-type ourselves (v3h.signed_headers pattern).
|
||||
const { "Content-Type": _drop, ...staticHeaders } = maxaiStaticHeaders();
|
||||
const headers: Record<string, string> = {
|
||||
...staticHeaders,
|
||||
...buildMaxaiSignedHeaders(
|
||||
{ path: MAXAI_UPLOAD_PATH, userId: auth.userId, deviceId: auth.deviceId },
|
||||
constants
|
||||
),
|
||||
Authorization: `Bearer ${auth.accessToken}`,
|
||||
"Content-Type": `multipart/form-data; boundary=${boundary}`,
|
||||
};
|
||||
|
||||
// Copy the multipart bytes into a fresh Uint8Array backed by a plain
|
||||
// (non-shared) ArrayBuffer. `Buffer.buffer` is typed ArrayBufferLike
|
||||
// (ArrayBuffer | SharedArrayBuffer) which isn't assignable to fetch's
|
||||
// BodyInit; a freshly-allocated Uint8Array is the BodyInit shape the rest of
|
||||
// the codebase uses for binary bodies (kimi-web.ts:397, conol-web.ts:529).
|
||||
const bodyBytes = new Uint8Array(bodyBuf.byteLength);
|
||||
bodyBytes.set(bodyBuf);
|
||||
|
||||
try {
|
||||
const resp = await fetchImpl(MAXAI_BASE_URL + MAXAI_UPLOAD_PATH, {
|
||||
method: "POST",
|
||||
headers,
|
||||
body: bodyBytes,
|
||||
signal: opts?.signal,
|
||||
});
|
||||
if (!resp.ok) return null;
|
||||
const text = await resp.text().catch(() => "");
|
||||
if (!sawUploadDone(text)) return null;
|
||||
return { doc_id: docId, doc_type: docType, file_name: doc.filename };
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload every inline document on the current turn and return the doc_list to
|
||||
* attach to the chat body. Failures are skipped (best-effort); the chat still
|
||||
* proceeds. Empty array when there are no inline docs.
|
||||
*/
|
||||
export async function resolveMaxaiDocList(
|
||||
messages: Array<{ role?: string; content?: unknown }>,
|
||||
auth: { accessToken: string; userId: string; deviceId: string },
|
||||
opts?: { fetchImpl?: typeof fetch; signal?: AbortSignal }
|
||||
): Promise<MaxaiDocListEntry[]> {
|
||||
const docs = extractCurrentTurnDocs(messages);
|
||||
if (docs.length === 0) return [];
|
||||
const results = await Promise.all(docs.map((d) => uploadMaxaiDocument(d, auth, opts)));
|
||||
return results.filter((r): r is MaxaiDocListEntry => r !== null);
|
||||
}
|
||||
234
open-sse/executors/maxai/emailLogin.ts
Normal file
@@ -0,0 +1,234 @@
|
||||
/**
|
||||
* MaxAI email login — browserless, two signed HTTP calls (a codex-style
|
||||
* device-pair flow, no browser / camoufox / Google navigation).
|
||||
*
|
||||
* MaxAI's web app offers email-code sign-in as an alternative to Google OAuth.
|
||||
* Both steps are plain signed POSTs carrying the same per-request X-Authorization
|
||||
* signature as every other MaxAI call (see ./signing.ts); both paths are in the
|
||||
* signer's BLANK_USER_ROUTES (they sign with a blank user_id, correct — there is
|
||||
* no user id yet before login). Ported byte-faithfully from the MaxAI web-app
|
||||
* bundle (chunk 86042: signInWithEmail line ~5623, verifySecretCode line ~5665).
|
||||
*
|
||||
* Step 1 — request a code (POST /oauth/signin_with_email):
|
||||
* body { email, app: "maxai_webapp" } -> { status: "OK" } (code emailed)
|
||||
*
|
||||
* Step 2 — verify the code (POST /oauth/verify_secret_code):
|
||||
* body { email, secret_code, app: "maxai_webapp", env: "prod_co",
|
||||
* client_user_id, ...nullable attribution fields }
|
||||
* -> { auth_user: { accessToken, refreshToken, userId, email, clientUserId } }
|
||||
*
|
||||
* The `device_id` folded into the signature is a CLIENT-GENERATED UUID (the web
|
||||
* app's getAPIFetchDeviceID = "return stored, else generate + persist"), so the
|
||||
* caller mints one with randomUUID() and reuses it across BOTH steps and for all
|
||||
* subsequent chat / refresh calls (the minted token is bound to that device id).
|
||||
* `client_user_id` is likewise a client UUID.
|
||||
*/
|
||||
import { buildMaxaiSignedHeaders } from "./signing.ts";
|
||||
import { maxaiStaticHeaders, MAXAI_BASE_URL } from "./protocol.ts";
|
||||
import { ensureMaxaiConstants } from "./constantsStore.ts";
|
||||
import type { MaxaiSigningConstants } from "./constants.ts";
|
||||
|
||||
export const MAXAI_SIGNIN_EMAIL_PATH = "/oauth/signin_with_email";
|
||||
export const MAXAI_VERIFY_CODE_PATH = "/oauth/verify_secret_code";
|
||||
|
||||
/** The web app's env tag for production email verification. */
|
||||
const MAXAI_VERIFY_ENV = "prod_co";
|
||||
|
||||
export interface MaxaiEmailRequestInput {
|
||||
email: string;
|
||||
/** Client device UUID (mint once, reuse for verify + all later calls). */
|
||||
deviceId: string;
|
||||
signal?: AbortSignal | null;
|
||||
fetchImpl?: typeof fetch;
|
||||
}
|
||||
|
||||
export interface MaxaiEmailVerifyInput {
|
||||
email: string;
|
||||
/** The 6-digit code the user received by email. */
|
||||
code: string;
|
||||
/** Same device UUID used in the request step. */
|
||||
deviceId: string;
|
||||
/** Client-user UUID (mint once alongside deviceId). */
|
||||
clientUserId: string;
|
||||
signal?: AbortSignal | null;
|
||||
fetchImpl?: typeof fetch;
|
||||
}
|
||||
|
||||
export interface MaxaiEmailRequestResult {
|
||||
ok: boolean;
|
||||
status: number;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
/** The full credential set returned by a successful verify. */
|
||||
export interface MaxaiLoginCredential {
|
||||
accessToken: string;
|
||||
refreshToken: string;
|
||||
userId: string;
|
||||
email: string;
|
||||
deviceId: string;
|
||||
clientUserId: string;
|
||||
}
|
||||
|
||||
export interface MaxaiEmailVerifyResult {
|
||||
ok: boolean;
|
||||
status: number;
|
||||
credential?: MaxaiLoginCredential;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
/** Build signed headers for a blank-user OAuth route (user id is blanked in the proof). */
|
||||
function signedOauthHeaders(
|
||||
path: string,
|
||||
deviceId: string,
|
||||
constants: MaxaiSigningConstants
|
||||
): Record<string, string> {
|
||||
return {
|
||||
...maxaiStaticHeaders(),
|
||||
// userId is blanked inside computeMaxaiProof for BLANK_USER_ROUTES; pass "".
|
||||
...buildMaxaiSignedHeaders({ path, userId: "", deviceId }, constants),
|
||||
};
|
||||
}
|
||||
|
||||
/** Pull a nested-or-top-level field from a MaxAI response body ({data:{...}} | {...}). */
|
||||
function pick<T = unknown>(body: Record<string, unknown>, key: string): T | undefined {
|
||||
const data = body?.data as Record<string, unknown> | undefined;
|
||||
const nested = data?.[key];
|
||||
if (nested !== undefined) return nested as T;
|
||||
return body?.[key] as T | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Step 1: ask MaxAI to email a sign-in code. Never throws.
|
||||
* Returns ok=true when the server acknowledges (status "OK").
|
||||
*/
|
||||
export async function requestMaxaiEmailCode(
|
||||
input: MaxaiEmailRequestInput
|
||||
): Promise<MaxaiEmailRequestResult> {
|
||||
const doFetch = input.fetchImpl ?? fetch;
|
||||
if (!input.email || !input.deviceId) {
|
||||
return { ok: false, status: 0, error: "missing email or deviceId" };
|
||||
}
|
||||
|
||||
// Initial login is the FIRST signed call — ensure we have live signing constants
|
||||
// (extracted from MaxAI's public bundle) before signing. No keys = cannot sign.
|
||||
const constants = await ensureMaxaiConstants({ fetchImpl: doFetch, signal: input.signal });
|
||||
if (!constants) {
|
||||
return { ok: false, status: 0, error: "MaxAI signing constants unavailable (extraction failed)" };
|
||||
}
|
||||
|
||||
let res: Response;
|
||||
try {
|
||||
res = await doFetch(MAXAI_BASE_URL + MAXAI_SIGNIN_EMAIL_PATH, {
|
||||
method: "POST",
|
||||
headers: signedOauthHeaders(MAXAI_SIGNIN_EMAIL_PATH, input.deviceId, constants),
|
||||
body: JSON.stringify({ email: input.email, app: "maxai_webapp" }),
|
||||
signal: input.signal ?? undefined,
|
||||
});
|
||||
} catch (err) {
|
||||
return { ok: false, status: 0, error: err instanceof Error ? err.message : String(err) };
|
||||
}
|
||||
|
||||
const raw = await res.text().catch(() => "");
|
||||
if (res.status !== 200) {
|
||||
return { ok: false, status: res.status, error: raw.slice(0, 200) };
|
||||
}
|
||||
let body: Record<string, unknown> = {};
|
||||
try {
|
||||
body = JSON.parse(raw) as Record<string, unknown>;
|
||||
} catch {
|
||||
return { ok: false, status: res.status, error: "unparseable signin response" };
|
||||
}
|
||||
if (pick<string>(body, "status") === "OK") return { ok: true, status: 200 };
|
||||
const detail = pick<string>(body, "detail") || pick<string>(body, "msg") || "sign-in request failed";
|
||||
return { ok: false, status: res.status, error: String(detail).slice(0, 200) };
|
||||
}
|
||||
|
||||
/**
|
||||
* Step 2: verify the emailed code and return the full credential. Never throws.
|
||||
* On success the caller persists the credential to the connection's
|
||||
* providerSpecificData (accessToken/refreshToken/deviceId/userId).
|
||||
*/
|
||||
export async function verifyMaxaiEmailCode(
|
||||
input: MaxaiEmailVerifyInput
|
||||
): Promise<MaxaiEmailVerifyResult> {
|
||||
const doFetch = input.fetchImpl ?? fetch;
|
||||
if (!input.email || !input.code || !input.deviceId) {
|
||||
return { ok: false, status: 0, error: "missing email, code, or deviceId" };
|
||||
}
|
||||
|
||||
const constants = await ensureMaxaiConstants({ fetchImpl: doFetch, signal: input.signal });
|
||||
if (!constants) {
|
||||
return { ok: false, status: 0, error: "MaxAI signing constants unavailable (extraction failed)" };
|
||||
}
|
||||
|
||||
const requestBody = {
|
||||
email: input.email,
|
||||
secret_code: input.code,
|
||||
app: "maxai_webapp",
|
||||
env: MAXAI_VERIFY_ENV,
|
||||
invitation_code: null,
|
||||
ref: "",
|
||||
client_reference_id: null,
|
||||
client_user_id: input.clientUserId,
|
||||
client_price_version: null,
|
||||
client_onboarding_version: null,
|
||||
user_acquisition_channel: "",
|
||||
gclid: null,
|
||||
};
|
||||
|
||||
let res: Response;
|
||||
try {
|
||||
res = await doFetch(MAXAI_BASE_URL + MAXAI_VERIFY_CODE_PATH, {
|
||||
method: "POST",
|
||||
headers: signedOauthHeaders(MAXAI_VERIFY_CODE_PATH, input.deviceId, constants),
|
||||
body: JSON.stringify(requestBody),
|
||||
signal: input.signal ?? undefined,
|
||||
});
|
||||
} catch (err) {
|
||||
return { ok: false, status: 0, error: err instanceof Error ? err.message : String(err) };
|
||||
}
|
||||
|
||||
const raw = await res.text().catch(() => "");
|
||||
if (res.status !== 200) {
|
||||
return { ok: false, status: res.status, error: raw.slice(0, 200) };
|
||||
}
|
||||
let body: Record<string, unknown> = {};
|
||||
try {
|
||||
body = JSON.parse(raw) as Record<string, unknown>;
|
||||
} catch {
|
||||
return { ok: false, status: res.status, error: "unparseable verify response" };
|
||||
}
|
||||
|
||||
const authUser = pick<Record<string, unknown>>(body, "auth_user");
|
||||
const status = pick<string>(body, "status");
|
||||
if (status === "OK" && authUser && typeof authUser === "object") {
|
||||
const accessToken = String(authUser.accessToken ?? authUser.access_token ?? "");
|
||||
const refreshToken = String(authUser.refreshToken ?? authUser.refresh_token ?? "");
|
||||
const userId = String(authUser.userId ?? authUser.user_id ?? "");
|
||||
if (!accessToken || !refreshToken) {
|
||||
return { ok: false, status: 200, error: "verify OK but token fields missing" };
|
||||
}
|
||||
return {
|
||||
ok: true,
|
||||
status: 200,
|
||||
credential: {
|
||||
accessToken,
|
||||
refreshToken,
|
||||
userId,
|
||||
email: String(authUser.email ?? input.email),
|
||||
deviceId: input.deviceId,
|
||||
clientUserId: String(authUser.clientUserId ?? authUser.client_user_id ?? input.clientUserId),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// 10119 is MaxAI's "code expired / too many attempts" signal; surface it.
|
||||
const code = pick<number>(body, "code");
|
||||
const detail = pick<string>(body, "detail") || pick<string>(body, "msg");
|
||||
const error =
|
||||
code === 10119
|
||||
? "Code expired or too many attempts — request a new code."
|
||||
: String(detail || "Invalid code. Check the code and try again.").slice(0, 200);
|
||||
return { ok: false, status: res.status, error };
|
||||
}
|
||||
266
open-sse/executors/maxai/protocol.ts
Normal file
@@ -0,0 +1,266 @@
|
||||
/**
|
||||
* MaxAI web-app protocol — request bodies, header assembly, and OpenAI→MaxAI
|
||||
* context flattening. Ported from the MaxAI v3 Python client (chat/request.py,
|
||||
* translation/openai_in.py, translation/turn_render.py) and live-verified against
|
||||
* the real `/gpt/cwc/chat` endpoint.
|
||||
*
|
||||
* MaxAI is a stateless-full-history provider on the OmniRoute side: we send the
|
||||
* ENTIRE flattened transcript in `message_content[0].text` every turn, always
|
||||
* with `chat_history: []`, and mint a fresh `conversation_id` per request. The
|
||||
* live probe proved a bare `/gpt/cwc/chat` (no upsert/add_messages bookkeeping)
|
||||
* honors `model_name` and serves the real paid model, so no bookkeeping is sent.
|
||||
*/
|
||||
import { randomUUID } from "node:crypto";
|
||||
|
||||
export const MAXAI_BASE_URL = "https://api.maxai.me";
|
||||
export const MAXAI_CHAT_PATH = "/gpt/cwc/chat";
|
||||
export const MAXAI_MODELS_CONFIG_PATH = "/models/get_config";
|
||||
|
||||
/** Static Firefox-150 identity headers sent on every MaxAI request. */
|
||||
export function maxaiStaticHeaders(): Record<string, string> {
|
||||
return {
|
||||
"User-Agent":
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:150.0) Gecko/20100101 Firefox/150.0",
|
||||
Accept: "*/*",
|
||||
"Accept-Language": "en-CA,en;q=0.9",
|
||||
Origin: "https://www.maxai.co",
|
||||
Referer: "https://www.maxai.co/",
|
||||
"Sec-Fetch-Dest": "empty",
|
||||
"Sec-Fetch-Mode": "cors",
|
||||
"Sec-Fetch-Site": "cross-site",
|
||||
"Content-Type": "application/json",
|
||||
};
|
||||
}
|
||||
|
||||
// ── Chat body ───────────────────────────────────────────────────────────────
|
||||
// Field ORDER is pinned (it is part of the HTTP/2 request fingerprint).
|
||||
const CHAT_FIELD_ORDER = [
|
||||
"chat_mode",
|
||||
"conversation_id",
|
||||
"chat_history",
|
||||
"message_content",
|
||||
"chrome_extension_version",
|
||||
"model_name",
|
||||
"prompt_id",
|
||||
"prompt_name",
|
||||
"prompt_inputs",
|
||||
"doc_list",
|
||||
"event_source",
|
||||
"streaming",
|
||||
"prompt_type",
|
||||
"feature_name",
|
||||
"source_type",
|
||||
"platform_feature",
|
||||
] as const;
|
||||
|
||||
export function newConversationId(): string {
|
||||
return randomUUID();
|
||||
}
|
||||
|
||||
export function buildMaxaiChatBody(opts: {
|
||||
conversationId: string;
|
||||
text: string;
|
||||
modelName: string;
|
||||
language?: string;
|
||||
relatedQuestionCnt?: string;
|
||||
/** Extracted app_version for chrome_extension_version (from the signing constants). */
|
||||
appVersion: string;
|
||||
/**
|
||||
* Vision input: current-turn image URLs (data: or http(s):) to attach to the
|
||||
* request. MaxAI's `/gpt/cwc/chat` accepts inline OpenAI-shaped image parts in
|
||||
* `message_content` alongside the text part. Empty/omitted = text-only (the
|
||||
* default, byte-identical to the pre-vision body).
|
||||
*/
|
||||
imageUrls?: string[];
|
||||
/**
|
||||
* Doc-RAG: uploaded-document references (from /app/upload_document). Each entry
|
||||
* carries at least `{ doc_id, doc_type, file_name }`. Typed as a loose object
|
||||
* array so callers can pass their concrete `MaxaiDocListEntry[]` without an
|
||||
* index-signature cast; the body only serializes it into `doc_list`.
|
||||
* Empty/omitted = no docs (the default `doc_list: []`).
|
||||
*/
|
||||
docList?: ReadonlyArray<object>;
|
||||
}): Record<string, unknown> {
|
||||
// message_content is a typed-parts array: the text part ALWAYS leads (so the
|
||||
// flattened transcript stays first and the no-image path is unchanged), then
|
||||
// any image_url parts ride alongside. Mirrors the OpenAI multimodal shape,
|
||||
// which MaxAI passes through (openai-to-cursor.ts vision pattern).
|
||||
const messageContent: Array<Record<string, unknown>> = [{ type: "text", text: opts.text }];
|
||||
for (const url of opts.imageUrls ?? []) {
|
||||
if (typeof url === "string" && url) {
|
||||
messageContent.push({ type: "image_url", image_url: { url } });
|
||||
}
|
||||
}
|
||||
const values: Record<string, unknown> = {
|
||||
chat_mode: "pro_chat",
|
||||
conversation_id: opts.conversationId,
|
||||
chat_history: [],
|
||||
message_content: messageContent,
|
||||
chrome_extension_version: opts.appVersion,
|
||||
model_name: opts.modelName,
|
||||
prompt_id: "chat",
|
||||
prompt_name: "chat",
|
||||
prompt_inputs: {
|
||||
RELATED_QUESTION_CNT: opts.relatedQuestionCnt ?? "5",
|
||||
AI_RESPONSE_LANGUAGE: opts.language ?? "English",
|
||||
},
|
||||
doc_list: opts.docList ?? [],
|
||||
event_source: "web",
|
||||
streaming: true,
|
||||
prompt_type: "freestyle",
|
||||
feature_name: "immersive_chat",
|
||||
source_type: "NA",
|
||||
platform_feature: "web_app",
|
||||
};
|
||||
const ordered: Record<string, unknown> = {};
|
||||
for (const k of CHAT_FIELD_ORDER) ordered[k] = values[k];
|
||||
return ordered;
|
||||
}
|
||||
|
||||
// ── OpenAI messages[] → MaxAI single text block ──────────────────────────────
|
||||
interface OpenAiMessage {
|
||||
role?: string;
|
||||
content?: unknown;
|
||||
tool_calls?: unknown;
|
||||
tool_call_id?: string;
|
||||
}
|
||||
|
||||
const ROLE_LABEL: Record<string, string> = {
|
||||
system: "System",
|
||||
user: "User",
|
||||
assistant: "Assistant",
|
||||
};
|
||||
const HISTORY_HEADER = "=== Conversation so far (for context) ===";
|
||||
const CURRENT_HEADER = "=== Current request (respond to THIS) ===";
|
||||
|
||||
/** Flatten OpenAI `content` (string or multipart array) to text. */
|
||||
export function contentToText(content: unknown): string {
|
||||
if (typeof content === "string") return content;
|
||||
if (Array.isArray(content)) {
|
||||
return content
|
||||
.map((part) =>
|
||||
part && typeof part === "object" && (part as { type?: string }).type === "text"
|
||||
? String((part as { text?: unknown }).text ?? "")
|
||||
: ""
|
||||
)
|
||||
.filter(Boolean)
|
||||
.join("\n");
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract image_url URLs from the CURRENT (last user) turn of an OpenAI
|
||||
* messages[] array. MaxAI is stateless-full-history, so we attach only the
|
||||
* current turn's images (history images would be re-sent every request and
|
||||
* bloat the body). Returns raw url strings (data: or http(s):) in order.
|
||||
*/
|
||||
export function extractCurrentTurnImages(messages: OpenAiMessage[]): string[] {
|
||||
for (let i = messages.length - 1; i >= 0; i--) {
|
||||
if (messages[i]?.role === "user") {
|
||||
const content = messages[i]?.content;
|
||||
if (!Array.isArray(content)) return [];
|
||||
const urls: string[] = [];
|
||||
for (const part of content) {
|
||||
if (part && typeof part === "object" && (part as { type?: unknown }).type === "image_url") {
|
||||
const imageUrl = (part as { image_url?: unknown }).image_url;
|
||||
if (typeof imageUrl === "string" && imageUrl) {
|
||||
urls.push(imageUrl);
|
||||
} else if (
|
||||
imageUrl &&
|
||||
typeof imageUrl === "object" &&
|
||||
typeof (imageUrl as { url?: unknown }).url === "string" &&
|
||||
(imageUrl as { url: string }).url
|
||||
) {
|
||||
urls.push((imageUrl as { url: string }).url);
|
||||
}
|
||||
}
|
||||
}
|
||||
return urls;
|
||||
}
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
/** Render OpenAI tool_calls[] as the prompted `<tool_call>` text MaxAI understands. */
|
||||
function toolCallsToText(toolCalls: unknown): string {
|
||||
if (!Array.isArray(toolCalls)) return "";
|
||||
const blocks: string[] = [];
|
||||
for (const call of toolCalls) {
|
||||
const fn = (call as { function?: { name?: unknown; arguments?: unknown } })?.function;
|
||||
if (!fn) continue;
|
||||
const name = typeof fn.name === "string" ? fn.name : "";
|
||||
let args = fn.arguments;
|
||||
if (typeof args !== "string") {
|
||||
try {
|
||||
args = JSON.stringify(args ?? {});
|
||||
} catch {
|
||||
args = "{}";
|
||||
}
|
||||
}
|
||||
blocks.push(`<tool_call>${JSON.stringify({ name, arguments: args })}</tool_call>`);
|
||||
}
|
||||
return blocks.join("\n");
|
||||
}
|
||||
|
||||
/** Render one non-system turn as a labeled block, or null to skip. */
|
||||
function renderTurn(message: OpenAiMessage): string | null {
|
||||
const role = message.role;
|
||||
const text = contentToText(message.content).trim();
|
||||
if (role === "tool") {
|
||||
const id = message.tool_call_id ? ` tool_call_id="${message.tool_call_id}"` : "";
|
||||
return `<tool_response${id}>\n${text}\n</tool_response>`;
|
||||
}
|
||||
if (role === "assistant" && message.tool_calls) {
|
||||
const calls = toolCallsToText(message.tool_calls);
|
||||
const body = text ? `${text}\n${calls}`.trim() : calls;
|
||||
return `Assistant: ${body}`;
|
||||
}
|
||||
if (!text) return null;
|
||||
const label = ROLE_LABEL[role ?? "user"] ?? "User";
|
||||
return `${label}: ${text}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Assemble the full structured context into one text block: system text leads,
|
||||
* prior turns render as a labeled transcript, and the LAST user turn is fenced
|
||||
* under a CURRENT header so a weak model answers THIS turn. Mirrors MaxAI v3
|
||||
* translation/openai_in.py::assemble_context.
|
||||
*/
|
||||
export function assembleMaxaiContext(messages: OpenAiMessage[]): string {
|
||||
// Find the last user turn (the current request).
|
||||
let curIdx = -1;
|
||||
let current = "";
|
||||
for (let i = messages.length - 1; i >= 0; i--) {
|
||||
if (messages[i]?.role === "user") {
|
||||
curIdx = i;
|
||||
current = contentToText(messages[i].content).trim();
|
||||
break;
|
||||
}
|
||||
}
|
||||
const systemParts: string[] = [];
|
||||
const historyParts: string[] = [];
|
||||
for (let i = 0; i < messages.length; i++) {
|
||||
if (i === curIdx) continue;
|
||||
const m = messages[i];
|
||||
if (m?.role === "system") {
|
||||
const t = contentToText(m.content).trim();
|
||||
if (t) systemParts.push(t);
|
||||
continue;
|
||||
}
|
||||
const block = renderTurn(m);
|
||||
if (block) historyParts.push(block);
|
||||
}
|
||||
const out: string[] = [...systemParts];
|
||||
if (historyParts.length && current) {
|
||||
out.push(HISTORY_HEADER + "\n\n" + historyParts.join("\n\n"));
|
||||
} else {
|
||||
out.push(...historyParts);
|
||||
}
|
||||
if (current) {
|
||||
const head = historyParts.length ? `${CURRENT_HEADER}\n\n` : "";
|
||||
out.push(head + current);
|
||||
}
|
||||
if (out.length === 0) throw new Error("no content to send to MaxAI");
|
||||
return out.join("\n\n");
|
||||
}
|
||||
149
open-sse/executors/maxai/refresh.ts
Normal file
@@ -0,0 +1,149 @@
|
||||
/**
|
||||
* MaxAI access-token refresh — browserless, via one signed HTTP call.
|
||||
*
|
||||
* MaxAI issues two tokens: a ~24h `accessToken` and a ~1-year `refreshToken`.
|
||||
* The web app refreshes the access token by POSTing the refresh token to
|
||||
* `/oauth/refresh_access_token` (web-app chunk 86042, `refreshAccessToken`). That
|
||||
* endpoint carries the SAME per-request `X-Authorization` signature as every other
|
||||
* MaxAI call (see ./signing.ts) — it is NOT a browser-only OAuth hop. A residential
|
||||
* Firefox-TLS client (wreq-js firefox_150, the OmniRoute egress overlay) passes the
|
||||
* TLS gate, so OmniRoute mints fresh access tokens itself with no browser.
|
||||
*
|
||||
* The refresh token is minted out-of-band, once, by the browser Google-OAuth flow
|
||||
* (see maxaiBrowserLogin) and only needs re-minting when it itself expires (~yearly).
|
||||
* This module handles the routine daily refresh.
|
||||
*
|
||||
* Request shape (byte-faithful to the web app):
|
||||
* POST https://api.maxai.me/oauth/refresh_access_token
|
||||
* Authorization: Bearer <refreshToken> // the REFRESH token, not access
|
||||
* noAuthLogout: true
|
||||
* X-Authorization + X-App/X-Browser headers // standard signing
|
||||
* body: {"app":"maxai_webapp"} // the app's `params` -> JSON body
|
||||
* -> 200 { data: { access_token } } // a fresh ~24h access JWT
|
||||
*
|
||||
* The signed path is the BARE pathname (no query string); the `app` field travels
|
||||
* in the body. `user_id` folds into the signature and is read from the refresh
|
||||
* token's own JWT subject (per the web app), falling back to a provided userId.
|
||||
*/
|
||||
import { buildMaxaiSignedHeaders } from "./signing.ts";
|
||||
import { maxaiStaticHeaders, MAXAI_BASE_URL } from "./protocol.ts";
|
||||
import { userIdFromJwt, accessTokenExpiry } from "./credentials.ts";
|
||||
import { refreshMaxaiConstants } from "./constantsStore.ts";
|
||||
|
||||
export const MAXAI_REFRESH_PATH = "/oauth/refresh_access_token";
|
||||
|
||||
/** How close to expiry (seconds) an access token may be before we refresh it. */
|
||||
export const MAXAI_REFRESH_MARGIN_SECONDS = 60 * 60; // 1h
|
||||
|
||||
export interface MaxaiRefreshInput {
|
||||
refreshToken: string;
|
||||
deviceId: string;
|
||||
/** Optional explicit user id; defaults to the refresh token's JWT subject. */
|
||||
userId?: string;
|
||||
signal?: AbortSignal | null;
|
||||
/** Injectable fetch for tests (defaults to the ambient patched fetch). */
|
||||
fetchImpl?: typeof fetch;
|
||||
}
|
||||
|
||||
export interface MaxaiRefreshResult {
|
||||
ok: boolean;
|
||||
accessToken?: string;
|
||||
/** access token expiry (epoch seconds), when a token was minted. */
|
||||
expiresAt?: number;
|
||||
status: number;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
/** True when an access token is missing, unparseable, or within the margin of expiry. */
|
||||
export function maxaiAccessTokenNeedsRefresh(
|
||||
accessToken: string | null | undefined,
|
||||
marginSeconds: number = MAXAI_REFRESH_MARGIN_SECONDS,
|
||||
now: () => number = Date.now
|
||||
): boolean {
|
||||
if (!accessToken) return true;
|
||||
const exp = accessTokenExpiry(accessToken);
|
||||
if (!exp) return true;
|
||||
return exp - now() / 1000 <= marginSeconds;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mint a fresh access token from a refresh token via one signed HTTP POST.
|
||||
* Never throws; returns a structured result the caller can branch on.
|
||||
*/
|
||||
export async function maxaiRefreshAccessToken(
|
||||
input: MaxaiRefreshInput
|
||||
): Promise<MaxaiRefreshResult> {
|
||||
const doFetch = input.fetchImpl ?? fetch;
|
||||
const userId = input.userId || userIdFromJwt(input.refreshToken) || "";
|
||||
if (!input.refreshToken || !input.deviceId || !userId) {
|
||||
return { ok: false, status: 0, error: "missing refreshToken, deviceId, or userId" };
|
||||
}
|
||||
|
||||
// Daily refresh is our freshness checkpoint for the signing constants: re-extract
|
||||
// from MaxAI's public bundle so a MaxAI-side key/app-version rotation is picked up
|
||||
// within a day (self-heal). refreshMaxaiConstants persists a changed set and
|
||||
// returns the current-best; on a fetch miss it returns whatever's already stored.
|
||||
const constants = await refreshMaxaiConstants({ fetchImpl: doFetch, signal: input.signal });
|
||||
if (!constants) {
|
||||
return { ok: false, status: 0, error: "MaxAI signing constants unavailable (extraction failed)" };
|
||||
}
|
||||
|
||||
const signed = buildMaxaiSignedHeaders(
|
||||
{
|
||||
path: MAXAI_REFRESH_PATH,
|
||||
userId,
|
||||
deviceId: input.deviceId,
|
||||
},
|
||||
constants
|
||||
);
|
||||
const headers: Record<string, string> = {
|
||||
...maxaiStaticHeaders(),
|
||||
...signed,
|
||||
Authorization: `Bearer ${input.refreshToken}`,
|
||||
noAuthLogout: "true",
|
||||
"Content-Type": "application/json",
|
||||
};
|
||||
|
||||
let res: Response;
|
||||
try {
|
||||
res = await doFetch(MAXAI_BASE_URL + MAXAI_REFRESH_PATH, {
|
||||
method: "POST",
|
||||
headers,
|
||||
body: JSON.stringify({ app: "maxai_webapp" }),
|
||||
signal: input.signal ?? undefined,
|
||||
});
|
||||
} catch (err) {
|
||||
return {
|
||||
ok: false,
|
||||
status: 0,
|
||||
error: err instanceof Error ? err.message : String(err),
|
||||
};
|
||||
}
|
||||
|
||||
const raw = await res.text().catch(() => "");
|
||||
if (res.status !== 200) {
|
||||
return { ok: false, status: res.status, error: raw.slice(0, 200) };
|
||||
}
|
||||
|
||||
let accessToken = "";
|
||||
try {
|
||||
const parsed = JSON.parse(raw) as {
|
||||
data?: { access_token?: unknown };
|
||||
access_token?: unknown;
|
||||
};
|
||||
const candidate = parsed?.data?.access_token ?? parsed?.access_token;
|
||||
if (typeof candidate === "string") accessToken = candidate;
|
||||
} catch {
|
||||
return { ok: false, status: res.status, error: "unparseable refresh response" };
|
||||
}
|
||||
if (!accessToken) {
|
||||
return { ok: false, status: res.status, error: "refresh response had no access_token" };
|
||||
}
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
status: 200,
|
||||
accessToken,
|
||||
expiresAt: accessTokenExpiry(accessToken) || undefined,
|
||||
};
|
||||
}
|
||||
151
open-sse/executors/maxai/signing.ts
Normal file
@@ -0,0 +1,151 @@
|
||||
/**
|
||||
* MaxAI web-app signing — the `X-Authorization` per-request signature.
|
||||
*
|
||||
* The scheme (validated byte-exact against real captured `X-Authorization` blobs):
|
||||
*
|
||||
* sign_str = `${appVersion}:${req_time}:${path}:${uid}`
|
||||
* sha1 = HMAC_SHA1_hex(sign_str, key=`${req_time}:${hmacKey}`)
|
||||
* p = SM3_hex(`${req_time}:${sha1}:${hmacKey}`)
|
||||
* payload = { X-Client-Domain, X-Client-Path(page url), X-Random(6-digit),
|
||||
* t(ms), p, d(device_id), <ctxKey>:{ a: context } }
|
||||
* X-Authorization = base64( "Salted__" + salt8 + AES-256-CBC(payloadJSON) )
|
||||
* with key/iv from OpenSSL EVP_BytesToKey(MD5, aesKey, salt)
|
||||
*
|
||||
* All primitives are in `node:crypto` (HMAC-SHA1, SM3 via OpenSSL 3, MD5,
|
||||
* AES-256-CBC); no external dependency.
|
||||
*
|
||||
* KEYING MATERIAL IS NOT HARDCODED. The `hmacKey` and `aesKey` are the CLIENT-SIDE
|
||||
* constants MaxAI's own web app ships verbatim in its public JS bundle. Rather
|
||||
* than pin them here, OmniRoute extracts them live (see ./constants.ts) and passes
|
||||
* a `MaxaiSigningConstants` object into every signing call. There is deliberately
|
||||
* NO in-code default for the two keys: a signer with no extracted keys cannot sign
|
||||
* (the caller surfaces a clear auth error) — we never sign with a guessed secret.
|
||||
* The non-secret STRUCTURAL fields (appVersion, ctxKey, header names) carry safe
|
||||
* defaults so a transient parse miss can't break an otherwise-working signer.
|
||||
*/
|
||||
import { createHmac, createHash, createCipheriv, randomBytes } from "node:crypto";
|
||||
import type { MaxaiSigningConstants, MaxaiHeaderNames } from "./constants.ts";
|
||||
import { MAXAI_DEFAULT_HEADER_NAMES } from "./constants.ts";
|
||||
|
||||
const CLIENT_DOMAIN = "maxai.co";
|
||||
/** Default browser page URL recorded verbatim as X-Client-Path (NOT the API path). */
|
||||
export const MAXAI_DEFAULT_PAGE = "https://www.maxai.co/app/";
|
||||
/** Only /oauth/* routes blank the user_id inside the signature. */
|
||||
const BLANK_USER_ROUTES = new Set([
|
||||
"/oauth/signin_with_email",
|
||||
"/oauth/signin_with_google",
|
||||
"/oauth/verify_secret_code",
|
||||
]);
|
||||
|
||||
const MAGIC = Buffer.from("Salted__", "ascii");
|
||||
|
||||
function hmacSha1Hex(message: string, key: string): string {
|
||||
return createHmac("sha1", Buffer.from(key, "utf8")).update(Buffer.from(message, "utf8")).digest("hex");
|
||||
}
|
||||
|
||||
function sm3Hex(message: string): string {
|
||||
return createHash("sm3").update(Buffer.from(message, "utf8")).digest("hex");
|
||||
}
|
||||
|
||||
/** OpenSSL EVP_BytesToKey with MD5 (CryptoJS default for a string passphrase). */
|
||||
function evpBytesToKey(
|
||||
passphrase: string,
|
||||
salt: Buffer,
|
||||
keyLen = 32,
|
||||
ivLen = 16
|
||||
): { key: Buffer; iv: Buffer } {
|
||||
let derived = Buffer.alloc(0);
|
||||
let block = Buffer.alloc(0);
|
||||
const pass = Buffer.from(passphrase, "utf8");
|
||||
while (derived.length < keyLen + ivLen) {
|
||||
block = createHash("md5").update(Buffer.concat([block, pass, salt])).digest();
|
||||
derived = Buffer.concat([derived, block]);
|
||||
}
|
||||
return { key: derived.subarray(0, keyLen), iv: derived.subarray(keyLen, keyLen + ivLen) };
|
||||
}
|
||||
|
||||
/**
|
||||
* Reproduce CryptoJS.AES.encrypt(text, passphrase).toString() (OpenSSL Salted__
|
||||
* envelope). `passphrase` (the extracted aesKey) is REQUIRED — there is no default.
|
||||
*/
|
||||
export function maxaiAesEncrypt(plaintext: string, passphrase: string, salt?: Buffer): string {
|
||||
if (!passphrase) throw new Error("maxaiAesEncrypt: missing aesKey");
|
||||
const s = salt ?? randomBytes(8);
|
||||
const { key, iv } = evpBytesToKey(passphrase, s);
|
||||
const cipher = createCipheriv("aes-256-cbc", key, iv); // PKCS7 padding is the default
|
||||
const body = Buffer.concat([cipher.update(Buffer.from(plaintext, "utf8")), cipher.final()]);
|
||||
return Buffer.concat([MAGIC, s, body]).toString("base64");
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute the SM3 `p` proof for an API `path` at `reqTime` ms. `hmacKey` and
|
||||
* `appVersion` (both extracted) are REQUIRED — there is no in-code default.
|
||||
*/
|
||||
export function computeMaxaiProof(
|
||||
path: string,
|
||||
reqTime: number,
|
||||
userId: string,
|
||||
hmacKey: string,
|
||||
appVersion: string
|
||||
): string {
|
||||
if (!hmacKey) throw new Error("computeMaxaiProof: missing hmacKey");
|
||||
if (!appVersion) throw new Error("computeMaxaiProof: missing appVersion");
|
||||
const p = path.endsWith("?") ? path.slice(0, -1) : path;
|
||||
const uid = BLANK_USER_ROUTES.has(p) ? "" : userId;
|
||||
const signStr = `${appVersion}:${reqTime}:${p}:${uid}`;
|
||||
const sha1 = hmacSha1Hex(signStr, `${reqTime}:${hmacKey}`);
|
||||
return sm3Hex(`${reqTime}:${sha1}:${hmacKey}`);
|
||||
}
|
||||
|
||||
export interface MaxaiSignInput {
|
||||
/** API path being signed, e.g. "/gpt/cwc/chat". */
|
||||
path: string;
|
||||
userId: string;
|
||||
deviceId: string;
|
||||
/** Browser page URL for X-Client-Path (defaults to the app page). */
|
||||
pageUrl?: string;
|
||||
/** Context slot value (defaults to "" — the wire default). */
|
||||
context?: string;
|
||||
/** Injectable clock/random for deterministic tests. */
|
||||
now?: () => number;
|
||||
random?: () => string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the signing headers (X-Authorization plus the X-App and X-Browser
|
||||
* companions) for one request. `device_id` MUST match the device that minted the
|
||||
* token, or the server rejects the signature.
|
||||
*
|
||||
* `constants` carries the extracted keying material + structural labels. It is
|
||||
* REQUIRED: callers resolve it via `ensureMaxaiConstants()` before signing.
|
||||
*/
|
||||
export function buildMaxaiSignedHeaders(
|
||||
input: MaxaiSignInput,
|
||||
constants: MaxaiSigningConstants
|
||||
): Record<string, string> {
|
||||
const reqTime = (input.now ?? (() => Date.now()))();
|
||||
const random =
|
||||
input.random?.() ?? String((randomBytes(4).readUInt32BE(0) % 900000) + 100000);
|
||||
const h: MaxaiHeaderNames = { ...MAXAI_DEFAULT_HEADER_NAMES, ...constants.headerNames };
|
||||
const ctxKey = constants.ctxKey;
|
||||
const appVersion = constants.appVersion;
|
||||
// Key ORDER matters — it is signed as a compact JSON string.
|
||||
const payload: Record<string, unknown> = {
|
||||
[h.clientDomain]: CLIENT_DOMAIN,
|
||||
[h.clientPath]: input.pageUrl ?? MAXAI_DEFAULT_PAGE,
|
||||
[h.random]: random,
|
||||
[h.tSlot]: reqTime,
|
||||
[h.pSlot]: computeMaxaiProof(input.path, reqTime, input.userId, constants.hmacKey, appVersion),
|
||||
[h.dSlot]: input.deviceId,
|
||||
[ctxKey]: { a: input.context ?? "" },
|
||||
};
|
||||
const blob = maxaiAesEncrypt(JSON.stringify(payload), constants.aesKey);
|
||||
return {
|
||||
[h.browserName]: "Firefox",
|
||||
[h.browserVersion]: "150.0",
|
||||
[h.browserMajor]: "150",
|
||||
[h.appVersionHeader]: appVersion,
|
||||
[h.appEnvHeader]: h.appEnvValue,
|
||||
[h.authorization]: blob,
|
||||
};
|
||||
}
|
||||
101
open-sse/executors/maxai/stream.ts
Normal file
@@ -0,0 +1,101 @@
|
||||
/**
|
||||
* MaxAI SSE stream handling — frame parsing, incremental `<think>` split, and
|
||||
* token estimation. Ported from the MaxAI v3 Python client (translation/sse.py,
|
||||
* translation/stream.py, translation/think_split.py, translation/token_usage.py).
|
||||
*
|
||||
* MaxAI's `/gpt/cwc/chat` response is `text/event-stream`: `data: {json}` frames
|
||||
* separated by blank lines. A text delta is a frame with
|
||||
* `data_key === "text" && need_merge` truthy; its content is `frame.text`.
|
||||
* Reasoning is emitted inline wrapped in `<think>…</think>`; everything inside is
|
||||
* reasoning, everything after the close tag is the visible answer. MaxAI returns
|
||||
* no usage frame, so tokens are estimated (~4 chars/token).
|
||||
*/
|
||||
|
||||
/** Parse the text deltas out of a raw SSE body (batch). */
|
||||
export function parseMaxaiSseText(raw: string): string {
|
||||
let out = "";
|
||||
for (const line of raw.split("\n")) {
|
||||
const s = line.trim();
|
||||
if (!s.startsWith("data:")) continue;
|
||||
const js = s.slice(5).trim();
|
||||
if (!js || js === "[DONE]") continue;
|
||||
try {
|
||||
const frame = JSON.parse(js) as { data_key?: unknown; need_merge?: unknown; text?: unknown };
|
||||
if (frame.data_key === "text" && frame.need_merge) {
|
||||
out += typeof frame.text === "string" ? frame.text : "";
|
||||
}
|
||||
} catch {
|
||||
/* ignore non-JSON keepalive frames */
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/** True when a decoded SSE frame is a mergeable text delta. */
|
||||
export function isMaxaiTextFrame(
|
||||
frame: unknown
|
||||
): frame is { data_key: "text"; need_merge: true; text: string } {
|
||||
const f = frame as { data_key?: unknown; need_merge?: unknown; text?: unknown };
|
||||
return f?.data_key === "text" && Boolean(f?.need_merge) && typeof f?.text === "string";
|
||||
}
|
||||
|
||||
const OPEN = "<think>";
|
||||
const CLOSE = "</think>";
|
||||
const HOLD = Math.max(OPEN.length, CLOSE.length) - 1;
|
||||
|
||||
/**
|
||||
* Stateful streaming classifier of text into (reasoning, answer). Handles a tag
|
||||
* split across frames by holding a short tail. Before `<think>` opens, text is
|
||||
* answer; if no `<think>` ever appears the whole stream is answer.
|
||||
*/
|
||||
export class ThinkSplitter {
|
||||
private buf = "";
|
||||
private inThink = false;
|
||||
|
||||
feed(delta: string): { reasoning: string; answer: string } {
|
||||
this.buf += delta;
|
||||
let reasoning = "";
|
||||
let answer = "";
|
||||
for (;;) {
|
||||
const tag = this.inThink ? CLOSE : OPEN;
|
||||
const idx = this.buf.indexOf(tag);
|
||||
if (idx === -1) break;
|
||||
const before = this.buf.slice(0, idx);
|
||||
if (this.inThink) reasoning += before;
|
||||
else answer += before;
|
||||
this.buf = this.buf.slice(idx + tag.length);
|
||||
this.inThink = !this.inThink;
|
||||
}
|
||||
// Emit everything except a short tail that might begin a tag.
|
||||
const safe = this.buf.length > HOLD ? this.buf.slice(0, this.buf.length - HOLD) : "";
|
||||
if (safe) {
|
||||
this.buf = this.buf.slice(safe.length);
|
||||
if (this.inThink) reasoning += safe;
|
||||
else answer += safe;
|
||||
}
|
||||
return { reasoning, answer };
|
||||
}
|
||||
|
||||
flush(): { reasoning: string; answer: string } {
|
||||
const tail = this.buf;
|
||||
this.buf = "";
|
||||
if (!tail) return { reasoning: "", answer: "" };
|
||||
return this.inThink ? { reasoning: tail, answer: "" } : { reasoning: "", answer: tail };
|
||||
}
|
||||
}
|
||||
|
||||
/** Split a fully-collected answer into { reasoning, answer } (batch/non-stream). */
|
||||
export function splitThink(full: string): { reasoning: string; answer: string } {
|
||||
const splitter = new ThinkSplitter();
|
||||
const a = splitter.feed(full);
|
||||
const b = splitter.flush();
|
||||
return {
|
||||
reasoning: a.reasoning + b.reasoning,
|
||||
answer: a.answer + b.answer,
|
||||
};
|
||||
}
|
||||
|
||||
/** MaxAI returns no token counts; estimate ~4 chars/token. */
|
||||
export function estimateMaxaiTokens(text: string): number {
|
||||
return Math.max(0, Math.ceil((text?.length ?? 0) / 4));
|
||||
}
|
||||
@@ -56,6 +56,7 @@ import { handleNvidiaNimImageGeneration } from "./imageGeneration/providers/nvid
|
||||
import { handleSegmindImageGeneration } from "./imageGeneration/providers/segmind.ts";
|
||||
import { handleCursorAgentImageGeneration } from "./imageGeneration/providers/cursorAgentImage.ts";
|
||||
import { handleMinimaxImageGeneration } from "./imageGeneration/providers/minimax.ts";
|
||||
import { handleMaxaiImageGeneration } from "./imageGeneration/providers/maxaiImage.ts";
|
||||
import { handleAdobeFireflyImageGeneration } from "./imageGeneration/providers/adobeFirefly.ts";
|
||||
import { handleAlibabaImageGeneration } from "./imageGeneration/providers/alibabaImage.ts";
|
||||
import { handleAiHordeImageGeneration } from "./imageGeneration/providers/aihorde.ts";
|
||||
@@ -616,6 +617,17 @@ export async function handleImageGeneration({
|
||||
});
|
||||
}
|
||||
|
||||
if (providerConfig.format === "maxai-image") {
|
||||
return handleMaxaiImageGeneration({
|
||||
model,
|
||||
provider,
|
||||
body,
|
||||
credentials,
|
||||
log,
|
||||
signal,
|
||||
});
|
||||
}
|
||||
|
||||
if (providerConfig.format === "adobe-firefly-image") {
|
||||
return handleAdobeFireflyImageGeneration({
|
||||
model,
|
||||
|
||||
230
open-sse/handlers/imageGeneration/providers/maxaiImage.ts
Normal file
@@ -0,0 +1,230 @@
|
||||
// MaxAI (web-app) image-generation handler.
|
||||
// Family: maxai-image | Provider: maxai
|
||||
//
|
||||
// MaxAI exposes 6 image models (gpt-image-1, dall-e-3, flux-1-schnell/dev/pro,
|
||||
// sd3-medium) behind a SINGLE synchronous endpoint:
|
||||
// POST https://api.maxai.me/gpt/get_image_generate_response
|
||||
// body {prompt, style, size, n, model_name}
|
||||
// -> {status:"OK", data:[{webp_url, png_url}]}
|
||||
// No submit-then-poll (unlike Microsoft Designer) — one request returns the
|
||||
// image URLs. Auth reuses the EXISTING signed-executor pieces (the same
|
||||
// X-Authorization signer + Firefox-150 identity the chat path uses); the signer
|
||||
// signs whatever `path` it is given, so image and chat share one auth module.
|
||||
//
|
||||
// Residential egress + Firefox-150 TLS are applied transparently at the infra
|
||||
// layer (in-container TUN + TLS_FINGERPRINT_PROVIDERS), so nothing egress-
|
||||
// specific lives here.
|
||||
|
||||
import { resolveMaxaiCredential } from "../../../executors/maxai/credentials.ts";
|
||||
import { buildMaxaiSignedHeaders } from "../../../executors/maxai/signing.ts";
|
||||
import { ensureMaxaiConstants } from "../../../executors/maxai/constantsStore.ts";
|
||||
import { MAXAI_BASE_URL, maxaiStaticHeaders } from "../../../executors/maxai/protocol.ts";
|
||||
import { sanitizeErrorMessage } from "../../../utils/error.ts";
|
||||
import { saveImageErrorResult, saveImageSuccessResult } from "../../imageGeneration.ts";
|
||||
|
||||
export const MAXAI_IMAGE_PATH = "/gpt/get_image_generate_response";
|
||||
const MAXAI_IMAGE_DEFAULT_SIZE = "1024x1024";
|
||||
const MAXAI_IMAGE_N_MAX = 4;
|
||||
|
||||
// Models whose upstream REJECTS non-1024 sizes (verified: gpt-image-1/dall-e-3
|
||||
// 500 on 256x256/512x512). The flux family + sd3-medium have no size constraint
|
||||
// and pass the requested WxH through unchanged.
|
||||
const MAXAI_STRICT_SIZE_MODELS: Record<string, Set<string>> = {
|
||||
"gpt-image-1": new Set(["1024x1024", "1024x1536", "1536x1024", "auto"]),
|
||||
"dall-e-3": new Set(["1024x1024", "1024x1792", "1792x1024"]),
|
||||
};
|
||||
|
||||
const MAXAI_IMAGE_ALIASES: Record<string, string> = {
|
||||
"stable-diffusion-v3": "sd3-medium",
|
||||
"stable-diffusion-3-medium": "sd3-medium",
|
||||
"flux-1-schneil": "flux-1-schnell", // tolerate a common typo
|
||||
};
|
||||
|
||||
/** Strip a `maxai/` prefix and resolve size-name aliases to the canonical model id. */
|
||||
export function resolveMaxaiImageModel(model: unknown): string {
|
||||
let m = typeof model === "string" ? model.trim() : "";
|
||||
if (m.startsWith("maxai/")) m = m.slice("maxai/".length);
|
||||
return MAXAI_IMAGE_ALIASES[m] ?? m;
|
||||
}
|
||||
|
||||
/**
|
||||
* Snap an OpenAI-style "WxH" size to something MaxAI accepts. gpt-image-1 /
|
||||
* dall-e-3 reject anything outside their bucket (→ upstream 500), so an
|
||||
* unsupported size (e.g. 512x512 from a standard OpenAI client) is snapped to
|
||||
* the model default. Models with no constraint pass the size through.
|
||||
*/
|
||||
export function snapMaxaiImageSize(model: string, size: unknown): string {
|
||||
const requested = typeof size === "string" && size.trim() ? size.trim() : MAXAI_IMAGE_DEFAULT_SIZE;
|
||||
const allowed = MAXAI_STRICT_SIZE_MODELS[model];
|
||||
if (!allowed) return requested; // flux / sd3: no constraint
|
||||
return allowed.has(requested) ? requested : MAXAI_IMAGE_DEFAULT_SIZE;
|
||||
}
|
||||
|
||||
/** Pull image URLs out of MaxAI's response into OpenAI data[] items (prefer png_url). */
|
||||
export function extractMaxaiImageUrls(json: unknown): string[] {
|
||||
// Accept either the raw items array or a { data: [...] } wrapper. MaxAI's real
|
||||
// response is { status:"OK", data:[{webp_url, png_url}] }, so both shapes occur
|
||||
// depending on how far the caller unwrapped.
|
||||
let items: unknown[] = [];
|
||||
if (Array.isArray(json)) {
|
||||
items = json;
|
||||
} else if (json && typeof json === "object" && Array.isArray((json as Record<string, unknown>).data)) {
|
||||
items = (json as Record<string, unknown>).data as unknown[];
|
||||
}
|
||||
const urls: string[] = [];
|
||||
for (const it of items) {
|
||||
if (it && typeof it === "object") {
|
||||
const rec = it as Record<string, unknown>;
|
||||
const url =
|
||||
(typeof rec.png_url === "string" && rec.png_url) ||
|
||||
(typeof rec.webp_url === "string" && rec.webp_url) ||
|
||||
(typeof rec.url === "string" && rec.url) ||
|
||||
"";
|
||||
if (url) urls.push(url);
|
||||
}
|
||||
}
|
||||
return urls;
|
||||
}
|
||||
|
||||
export async function handleMaxaiImageGeneration({
|
||||
model,
|
||||
provider,
|
||||
body,
|
||||
credentials,
|
||||
log,
|
||||
signal,
|
||||
fetchImpl = fetch,
|
||||
}: {
|
||||
model: string;
|
||||
provider: string;
|
||||
body: { prompt?: unknown; size?: unknown; n?: unknown; style?: unknown };
|
||||
credentials: {
|
||||
apiKey?: string;
|
||||
accessToken?: string;
|
||||
providerSpecificData?: Record<string, unknown> | null;
|
||||
};
|
||||
log?: { info?: (...args: unknown[]) => void; error?: (...args: unknown[]) => void };
|
||||
signal?: AbortSignal;
|
||||
fetchImpl?: typeof fetch;
|
||||
}) {
|
||||
const startTime = Date.now();
|
||||
|
||||
const prompt = typeof body.prompt === "string" ? body.prompt.trim() : "";
|
||||
if (!prompt) {
|
||||
return saveImageErrorResult({
|
||||
provider,
|
||||
model,
|
||||
status: 400,
|
||||
startTime,
|
||||
error: "Prompt is required for MaxAI image generation",
|
||||
});
|
||||
}
|
||||
|
||||
const cred = resolveMaxaiCredential(
|
||||
credentials?.providerSpecificData,
|
||||
credentials?.accessToken || credentials?.apiKey
|
||||
);
|
||||
if (!cred) {
|
||||
return saveImageErrorResult({
|
||||
provider,
|
||||
model,
|
||||
status: 401,
|
||||
startTime,
|
||||
error: "MaxAI credentials missing access_token",
|
||||
retryable: true,
|
||||
});
|
||||
}
|
||||
|
||||
const canonicalModel = resolveMaxaiImageModel(model);
|
||||
const nRaw = Number(body.n);
|
||||
const n = Number.isFinite(nRaw) && nRaw >= 1 ? Math.min(Math.floor(nRaw), MAXAI_IMAGE_N_MAX) : 1;
|
||||
const requestBody = {
|
||||
prompt,
|
||||
style: typeof body.style === "string" && body.style ? body.style : "vivid",
|
||||
size: snapMaxaiImageSize(canonicalModel, body.size),
|
||||
n,
|
||||
model_name: canonicalModel,
|
||||
};
|
||||
|
||||
const constants = await ensureMaxaiConstants({ fetchImpl, signal });
|
||||
if (!constants) {
|
||||
return saveImageErrorResult({
|
||||
provider,
|
||||
model,
|
||||
status: 401,
|
||||
startTime,
|
||||
error: "MaxAI signing constants unavailable (extraction failed).",
|
||||
});
|
||||
}
|
||||
const headers: Record<string, string> = {
|
||||
...maxaiStaticHeaders(),
|
||||
...buildMaxaiSignedHeaders({ path: MAXAI_IMAGE_PATH, userId: cred.userId, deviceId: cred.deviceId }, constants),
|
||||
Authorization: `Bearer ${cred.accessToken}`,
|
||||
"Content-Type": "application/json",
|
||||
};
|
||||
|
||||
let resp: Response;
|
||||
try {
|
||||
resp = await fetchImpl(MAXAI_BASE_URL + MAXAI_IMAGE_PATH, {
|
||||
method: "POST",
|
||||
headers,
|
||||
body: JSON.stringify(requestBody),
|
||||
signal,
|
||||
});
|
||||
} catch (err) {
|
||||
const errorText = sanitizeErrorMessage(err instanceof Error ? err.message : String(err));
|
||||
log?.error?.("IMAGE", `${provider} maxai-image transport error: ${errorText}`);
|
||||
return saveImageErrorResult({ provider, model, status: 502, startTime, error: errorText, requestBody });
|
||||
}
|
||||
|
||||
if (!resp.ok) {
|
||||
const detail = (await resp.text().catch(() => "")).slice(0, 500);
|
||||
log?.error?.("IMAGE", `${provider} maxai-image error ${resp.status}: ${detail}`);
|
||||
return saveImageErrorResult({
|
||||
provider,
|
||||
model,
|
||||
status: resp.status,
|
||||
startTime,
|
||||
error: detail || `MaxAI image generation failed (HTTP ${resp.status})`,
|
||||
requestBody,
|
||||
// 401 = expired token, 418 = TLS/JA3 masked-reject: rotate to the next account.
|
||||
retryable: resp.status === 401 || resp.status === 418,
|
||||
});
|
||||
}
|
||||
|
||||
let json: unknown;
|
||||
try {
|
||||
json = await resp.json();
|
||||
} catch {
|
||||
return saveImageErrorResult({
|
||||
provider,
|
||||
model,
|
||||
status: 502,
|
||||
startTime,
|
||||
error: "MaxAI returned a non-JSON image response",
|
||||
requestBody,
|
||||
});
|
||||
}
|
||||
|
||||
const status = (json as Record<string, unknown>)?.status;
|
||||
const urls = extractMaxaiImageUrls(json);
|
||||
if (status !== "OK" || urls.length === 0) {
|
||||
return saveImageErrorResult({
|
||||
provider,
|
||||
model,
|
||||
status: 502,
|
||||
startTime,
|
||||
error: `MaxAI image generation returned no images (status=${String(status)})`,
|
||||
requestBody,
|
||||
});
|
||||
}
|
||||
|
||||
return saveImageSuccessResult({
|
||||
provider,
|
||||
model,
|
||||
startTime,
|
||||
requestBody,
|
||||
responseBody: { images_count: urls.length },
|
||||
images: urls.map((url) => ({ url })),
|
||||
});
|
||||
}
|
||||
172
open-sse/services/maxaiModels.ts
Normal file
@@ -0,0 +1,172 @@
|
||||
/**
|
||||
* MaxAI model discovery — live model list + per-model context windows from the
|
||||
* web app's own `/models/get_config` endpoint (the signed call the app makes on
|
||||
* load). Feeds OmniRoute's model-discovery pipeline so the MaxAI catalog and its
|
||||
* per-model context windows self-update instead of relying only on the static
|
||||
* catalog (`open-sse/executors/maxai/catalog.ts`).
|
||||
*
|
||||
* The response's `chat_models[]` carries `model_name` (id), `ui_display_name`,
|
||||
* `group`, `max_tokens` (the per-model context window), `is_deprecated`, and a
|
||||
* `capabilities` block ({ vision, thinking_mode, artifacts, file_upload }). We
|
||||
* map each non-deprecated chat model to a discovery record whose `inputTokenLimit`
|
||||
* is `max_tokens`, so `persistDiscoveredModels` → `syncedAvailableModels` →
|
||||
* `contextWindowResolver` reconciles the real window as an `auto:discovery`
|
||||
* override.
|
||||
*
|
||||
* Signed + residential like every MaxAI call (see ./maxai/signing.ts). Never
|
||||
* throws for the caller's convenience is NOT the contract here — the route wraps
|
||||
* it in try/catch and falls back to the curated catalog — but it validates HTTP
|
||||
* status and shape and throws a sanitized error on failure so the route logs it.
|
||||
*/
|
||||
import { resolveMaxaiCredential } from "../executors/maxai/credentials.ts";
|
||||
import { buildMaxaiSignedHeaders } from "../executors/maxai/signing.ts";
|
||||
import { ensureMaxaiConstants } from "../executors/maxai/constantsStore.ts";
|
||||
import {
|
||||
maxaiStaticHeaders,
|
||||
MAXAI_BASE_URL,
|
||||
MAXAI_MODELS_CONFIG_PATH,
|
||||
} from "../executors/maxai/protocol.ts";
|
||||
import { maxaiContextWindow, MAXAI_MODELS } from "../executors/maxai/catalog.ts";
|
||||
|
||||
// Re-export the registry-shaped catalog through this service so `src/app` routes
|
||||
// can consume it WITHOUT importing the executor directly (the no-restricted-imports
|
||||
// rule: "executor implementations must stay behind an open-sse handler or service
|
||||
// boundary"). This service IS that boundary, and already owns the catalog import.
|
||||
export { MAXAI_REGISTRY_MODELS } from "../executors/maxai/catalog.ts";
|
||||
|
||||
/** A discovered MaxAI model in the shape persistDiscoveredModels normalizes. */
|
||||
export interface MaxaiDiscoveredModel {
|
||||
id: string;
|
||||
name: string;
|
||||
/** Per-model context window (chars→tokens handled upstream); the reconciler key. */
|
||||
inputTokenLimit: number;
|
||||
group?: string;
|
||||
supportsReasoning?: boolean;
|
||||
supportsVision?: boolean;
|
||||
toolCalling: boolean;
|
||||
}
|
||||
|
||||
export interface MaxaiModelDiscoveryInput {
|
||||
/** Connection credential material (from providerSpecificData + apiKey). */
|
||||
providerSpecificData: Record<string, unknown> | null | undefined;
|
||||
accessToken?: string | null;
|
||||
signal?: AbortSignal | null;
|
||||
/** Injectable fetch (the route passes a proxy/guard-wrapped safeOutboundFetch). */
|
||||
fetchImpl?: typeof fetch;
|
||||
}
|
||||
|
||||
export interface MaxaiModelDiscoveryResult {
|
||||
models: MaxaiDiscoveredModel[];
|
||||
warning?: string;
|
||||
}
|
||||
|
||||
/** The curated paid-model id set — only these are surfaced (quality gate). */
|
||||
const CURATED_IDS = new Set(MAXAI_MODELS.map((m) => m.id));
|
||||
|
||||
interface RawChatModel {
|
||||
model_name?: unknown;
|
||||
ui_display_name?: unknown;
|
||||
type?: unknown;
|
||||
group?: unknown;
|
||||
max_tokens?: unknown;
|
||||
is_deprecated?: unknown;
|
||||
capabilities?: {
|
||||
vision?: unknown;
|
||||
thinking_mode?: unknown;
|
||||
} | null;
|
||||
}
|
||||
|
||||
/** Map one raw chat model to a discovery record, or null when it should be dropped. */
|
||||
function toDiscovered(raw: RawChatModel): MaxaiDiscoveredModel | null {
|
||||
const id = typeof raw.model_name === "string" ? raw.model_name : "";
|
||||
if (!id) return null;
|
||||
if (raw.is_deprecated === true) return null;
|
||||
if (raw.type !== undefined && raw.type !== "chat") return null;
|
||||
// Quality gate: only surface the curated paid models (the ones catalog.ts offers).
|
||||
if (!CURATED_IDS.has(id)) return null;
|
||||
|
||||
const liveWindow =
|
||||
typeof raw.max_tokens === "number" && Number.isFinite(raw.max_tokens) && raw.max_tokens > 0
|
||||
? Math.trunc(raw.max_tokens)
|
||||
: maxaiContextWindow(id); // fall back to the static catalog window
|
||||
|
||||
const caps = raw.capabilities ?? {};
|
||||
return {
|
||||
id,
|
||||
name: typeof raw.ui_display_name === "string" ? raw.ui_display_name : id,
|
||||
inputTokenLimit: liveWindow,
|
||||
group: typeof raw.group === "string" ? raw.group : undefined,
|
||||
supportsReasoning: caps.thinking_mode === true || undefined,
|
||||
supportsVision: caps.vision === true || undefined,
|
||||
toolCalling: true, // prompted tool-calling (see maxai.ts + webTools.ts)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch MaxAI's live model catalog + per-model context windows. Throws a
|
||||
* sanitized Error on auth/transport/shape failure (the route catches and falls
|
||||
* back to the curated static catalog).
|
||||
*/
|
||||
export async function discoverMaxaiModels(
|
||||
input: MaxaiModelDiscoveryInput
|
||||
): Promise<MaxaiModelDiscoveryResult> {
|
||||
const doFetch = input.fetchImpl ?? fetch;
|
||||
const cred = resolveMaxaiCredential(input.providerSpecificData, input.accessToken);
|
||||
if (!cred) {
|
||||
throw new Error("MaxAI connection is not configured (missing token/device/user id).");
|
||||
}
|
||||
|
||||
const path = MAXAI_MODELS_CONFIG_PATH;
|
||||
const constants = await ensureMaxaiConstants({ fetchImpl: doFetch, signal: input.signal });
|
||||
if (!constants) {
|
||||
throw new Error("MaxAI signing constants unavailable (extraction failed).");
|
||||
}
|
||||
const res = await doFetch(MAXAI_BASE_URL + path, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
...maxaiStaticHeaders(),
|
||||
...buildMaxaiSignedHeaders({ path, userId: cred.userId, deviceId: cred.deviceId }, constants),
|
||||
Authorization: `Bearer ${cred.accessToken}`,
|
||||
},
|
||||
body: "{}",
|
||||
signal: input.signal ?? undefined,
|
||||
});
|
||||
|
||||
if (res.status !== 200) {
|
||||
const detail = await res.text().catch(() => "");
|
||||
throw new Error(`MaxAI /models/get_config ${res.status}: ${detail.slice(0, 160)}`);
|
||||
}
|
||||
|
||||
let parsed: { data?: { chat_models?: unknown }; chat_models?: unknown };
|
||||
try {
|
||||
parsed = (await res.json()) as typeof parsed;
|
||||
} catch {
|
||||
throw new Error("MaxAI /models/get_config returned unparseable JSON.");
|
||||
}
|
||||
|
||||
const data = parsed?.data ?? parsed;
|
||||
const chatModels = (data as { chat_models?: unknown })?.chat_models;
|
||||
if (!Array.isArray(chatModels)) {
|
||||
throw new Error("MaxAI /models/get_config had no chat_models array.");
|
||||
}
|
||||
|
||||
const models: MaxaiDiscoveredModel[] = [];
|
||||
for (const raw of chatModels as RawChatModel[]) {
|
||||
const mapped = toDiscovered(raw);
|
||||
if (mapped) models.push(mapped);
|
||||
}
|
||||
|
||||
if (models.length === 0) {
|
||||
throw new Error("MaxAI /models/get_config yielded no usable curated models.");
|
||||
}
|
||||
|
||||
// Note when the live list dropped a curated model (e.g. MaxAI deprecated it).
|
||||
const liveIds = new Set(models.map((m) => m.id));
|
||||
const missing = [...CURATED_IDS].filter((id) => !liveIds.has(id));
|
||||
const warning =
|
||||
missing.length > 0
|
||||
? `MaxAI no longer offers ${missing.length} curated model(s): ${missing.join(", ")}`
|
||||
: undefined;
|
||||
|
||||
return { models, warning };
|
||||
}
|
||||
@@ -97,6 +97,13 @@ let initialized = false;
|
||||
|
||||
let currentRequestQueueSettings: RequestQueueSettings = DEFAULT_RESILIENCE_SETTINGS.requestQueue;
|
||||
export const ZAI_WEB_REQUEST_QUEUE_MAX_WAIT_MS = 60_000;
|
||||
// MaxAI proxies reasoning models (deepseek-r1, gpt-5.6-thinking, grok-4.5,
|
||||
// gemini-3.1-pro-preview, grok-4-1-fast-reasoning) whose single upstream turn
|
||||
// legitimately runs tens of seconds to minutes. The 15s default execution
|
||||
// expiration (Bottleneck `expiration`, applied AFTER dispatch) kills those mid
|
||||
// think and surfaces a spurious local 504. Floor MaxAI at 5 min — the same
|
||||
// ceiling waitForCooldown.budgetMs uses — so slow reasoning turns complete.
|
||||
export const MAXAI_REQUEST_QUEUE_MAX_WAIT_MS = 300_000;
|
||||
|
||||
const limiterEffectiveSettings = new WeakMap<Bottleneck, Bottleneck.ConstructorOptions>();
|
||||
const preservedReplacementSettings = new Map<string, Bottleneck.ConstructorOptions>();
|
||||
@@ -166,10 +173,15 @@ export function resolveRequestQueueMaxWaitMs(
|
||||
configuredMaxWaitMs: number = currentRequestQueueSettings.maxWaitMs,
|
||||
connectionId?: string
|
||||
): number {
|
||||
const legacyDefault =
|
||||
provider.trim().toLowerCase() === "zai-web"
|
||||
? Math.max(configuredMaxWaitMs, ZAI_WEB_REQUEST_QUEUE_MAX_WAIT_MS)
|
||||
: configuredMaxWaitMs;
|
||||
const p = provider.trim().toLowerCase();
|
||||
let legacyDefault = configuredMaxWaitMs;
|
||||
if (p === "zai-web") {
|
||||
legacyDefault = Math.max(configuredMaxWaitMs, ZAI_WEB_REQUEST_QUEUE_MAX_WAIT_MS);
|
||||
} else if (p === "maxai" || p === "mx") {
|
||||
// MaxAI's slow reasoning models legitimately need up to ~5 min; floor the
|
||||
// per-request execution budget so they aren't cut off early.
|
||||
legacyDefault = Math.max(configuredMaxWaitMs, MAXAI_REQUEST_QUEUE_MAX_WAIT_MS);
|
||||
}
|
||||
const override = connectionId
|
||||
? connectionRateLimitOverrides.get(connectionId)?.maxWaitMs
|
||||
: undefined;
|
||||
|
||||
@@ -99,6 +99,24 @@ function tlsFingerprintProviderAllowed(
|
||||
.some((candidate) => candidate.trim().toLowerCase() === normalizedProvider);
|
||||
}
|
||||
|
||||
/**
|
||||
* Per-provider TLS impersonation profile. Most providers use the default
|
||||
* Chrome/macOS wreq profile; providers that must match a specific browser
|
||||
* fingerprint (e.g. MaxAI expects a Windows Firefox-150 client) override it here.
|
||||
* Returns undefined to keep the tlsClient default (chrome_124 / macos).
|
||||
*/
|
||||
const TLS_PROVIDER_PROFILE: Record<string, { browser: string; os: string }> = {
|
||||
maxai: { browser: "firefox_150", os: "windows" },
|
||||
};
|
||||
|
||||
function tlsProfileForProvider(
|
||||
provider: string | null | undefined
|
||||
): { browserProfile?: string; os?: string } {
|
||||
if (!provider) return {};
|
||||
const p = TLS_PROVIDER_PROFILE[provider.trim().toLowerCase()];
|
||||
return p ? { browserProfile: p.browser, os: p.os } : {};
|
||||
}
|
||||
|
||||
type TlsClientLike = {
|
||||
available: boolean;
|
||||
fetch: (url: string, options?: TlsFetchOptions) => Promise<Response>;
|
||||
@@ -776,6 +794,7 @@ async function patchedFetch(
|
||||
signal: getEffectiveSignal(input, options),
|
||||
proxy: null,
|
||||
sessionScope: tlsStore?.sessionScope,
|
||||
...tlsProfileForProvider(tlsStore?.provider),
|
||||
});
|
||||
if (tlsStore) tlsStore.used = true;
|
||||
return response;
|
||||
@@ -1068,6 +1087,7 @@ async function patchedFetch(
|
||||
signal: getEffectiveSignal(input, options),
|
||||
proxy: proxyUrl,
|
||||
sessionScope: tlsStore?.sessionScope,
|
||||
...tlsProfileForProvider(tlsStore?.provider),
|
||||
});
|
||||
if (tlsStore) tlsStore.used = true;
|
||||
return response;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "omniroute",
|
||||
"version": "3.8.51",
|
||||
"description": "Unified AI router with 352 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",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 800 420" font-family="system-ui, -apple-system, Segoe UI, Arial, Helvetica, sans-serif" role="img" aria-label="OmniRoute 4-tier fallback: your IDE or CLI calls one local endpoint and the OmniRoute Smart Router fails over across 352 providers in 4 tiers — Tier 1 Subscription, Tier 2 API, Tier 3 Cheap, Tier 4 Free.">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 800 420" font-family="system-ui, -apple-system, Segoe UI, Arial, Helvetica, sans-serif" role="img" aria-label="OmniRoute 4-tier fallback: your IDE or CLI calls one local endpoint and the OmniRoute Smart Router fails over across 353 providers in 4 tiers — Tier 1 Subscription, Tier 2 API, Tier 3 Cheap, Tier 4 Free.">
|
||||
<title>OmniRoute 4-tier fallback</title>
|
||||
<desc>OmniRoute 4-tier fallback: your IDE or CLI calls one local endpoint and the OmniRoute Smart Router fails over across 352 providers in 4 tiers — Tier 1 Subscription, Tier 2 API, Tier 3 Cheap, Tier 4 Free.</desc>
|
||||
<desc>OmniRoute 4-tier fallback: your IDE or CLI calls one local endpoint and the OmniRoute Smart Router fails over across 353 providers in 4 tiers — Tier 1 Subscription, Tier 2 API, Tier 3 Cheap, Tier 4 Free.</desc>
|
||||
<defs>
|
||||
<marker id="arrow-dark" markerWidth="10" markerHeight="10" refX="9" refY="3" orient="auto">
|
||||
<path d="M0,0 L0,6 L9,3 z" fill="#9ca3af"/>
|
||||
@@ -15,7 +15,7 @@
|
||||
|
||||
<!-- Title -->
|
||||
<text x="400" y="32" text-anchor="middle" font-size="18" font-weight="700" fill="#f9fafb">OmniRoute 4-tier fallback</text>
|
||||
<text x="400" y="52" text-anchor="middle" font-size="12" fill="#6b7280">Never stop building — automatic zero-config failover across 352 providers</text>
|
||||
<text x="400" y="52" text-anchor="middle" font-size="12" fill="#6b7280">Never stop building — automatic zero-config failover across 353 providers</text>
|
||||
|
||||
<!-- Client box -->
|
||||
<rect x="275" y="70" width="250" height="52" rx="8" fill="#1f2937" stroke="#374151" stroke-width="1.5"/>
|
||||
|
||||
|
Before Width: | Height: | Size: 6.2 KiB After Width: | Height: | Size: 6.2 KiB |
@@ -1,6 +1,6 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 800 420" font-family="system-ui, -apple-system, Segoe UI, Arial, Helvetica, sans-serif" role="img" aria-label="OmniRoute 4-tier fallback: your IDE or CLI calls one local endpoint and the OmniRoute Smart Router fails over across 352 providers in 4 tiers — Tier 1 Subscription, Tier 2 API, Tier 3 Cheap, Tier 4 Free.">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 800 420" font-family="system-ui, -apple-system, Segoe UI, Arial, Helvetica, sans-serif" role="img" aria-label="OmniRoute 4-tier fallback: your IDE or CLI calls one local endpoint and the OmniRoute Smart Router fails over across 353 providers in 4 tiers — Tier 1 Subscription, Tier 2 API, Tier 3 Cheap, Tier 4 Free.">
|
||||
<title>OmniRoute 4-tier fallback</title>
|
||||
<desc>OmniRoute 4-tier fallback: your IDE or CLI calls one local endpoint and the OmniRoute Smart Router fails over across 352 providers in 4 tiers — Tier 1 Subscription, Tier 2 API, Tier 3 Cheap, Tier 4 Free.</desc>
|
||||
<desc>OmniRoute 4-tier fallback: your IDE or CLI calls one local endpoint and the OmniRoute Smart Router fails over across 353 providers in 4 tiers — Tier 1 Subscription, Tier 2 API, Tier 3 Cheap, Tier 4 Free.</desc>
|
||||
<defs>
|
||||
<marker id="arrow-light" markerWidth="10" markerHeight="10" refX="9" refY="3" orient="auto">
|
||||
<path d="M0,0 L0,6 L9,3 z" fill="#6b7280"/>
|
||||
@@ -15,7 +15,7 @@
|
||||
|
||||
<!-- Title -->
|
||||
<text x="400" y="32" text-anchor="middle" font-size="18" font-weight="700" fill="#111827">OmniRoute 4-tier fallback</text>
|
||||
<text x="400" y="52" text-anchor="middle" font-size="12" fill="#6b7280">Never stop building — automatic zero-config failover across 352 providers</text>
|
||||
<text x="400" y="52" text-anchor="middle" font-size="12" fill="#6b7280">Never stop building — automatic zero-config failover across 353 providers</text>
|
||||
|
||||
<!-- Client box -->
|
||||
<rect x="275" y="70" width="250" height="52" rx="8" fill="#ffffff" stroke="#d1d5db" stroke-width="1.5"/>
|
||||
|
||||
|
Before Width: | Height: | Size: 6.2 KiB After Width: | Height: | Size: 6.2 KiB |
@@ -216,6 +216,12 @@ export const PACK_ARTIFACT_REQUIRED_PATHS: string[] = [
|
||||
"bin/mcpStdioConsoleGuard.mjs",
|
||||
"bin/nodeRuntimeSupport.mjs",
|
||||
"bin/omniroute.mjs",
|
||||
// #11437: bin/omniroute.mjs imports ./cli/utils/volatileEnvPath.mjs at startup
|
||||
// (describeVolatileEnvWarning — flags a .env living inside the installed package).
|
||||
// bin/cli/ is only an allowlist PREFIX, so its absence would never fail the
|
||||
// unexpected-paths check; list it REQUIRED so a regression is loud (#7065 class,
|
||||
// enforced by tests/unit/pack-artifact-entrypoint-closures.test.ts).
|
||||
"bin/cli/utils/volatileEnvPath.mjs",
|
||||
// #7808: aliasResolver + its hook file. bin/omniroute.mjs imports
|
||||
// bin/aliasResolver.mjs at startup, which in turn registers
|
||||
// bin/aliasResolverHook.mjs as the ESM loader. Both must ship in the tarball
|
||||
|
||||
@@ -6,7 +6,7 @@ import { fileURLToPath } from "node:url";
|
||||
const repoRoot = fileURLToPath(new URL("../..", import.meta.url));
|
||||
const providerDir = join(repoRoot, "public", "providers");
|
||||
const MAX_RASTER_BYTES = 128 * 1024;
|
||||
const MAX_RASTER_DIMENSION = 256;
|
||||
const MAX_RASTER_DIMENSION = 512;
|
||||
const RASTER_EXTENSIONS = new Set([".png", ".jpg", ".jpeg"]);
|
||||
|
||||
function extensionOf(fileName) {
|
||||
|
||||
@@ -157,6 +157,133 @@ async function loginAdobeFirefly(
|
||||
}
|
||||
}
|
||||
|
||||
// --- MaxAI: browserless email device-pair login -----------------------------
|
||||
|
||||
/**
|
||||
* MaxAI email login is a two-step, browserless device-pair flow (no browser /
|
||||
* camoufox / Google): step "request" emails a 6-digit code; step "verify"
|
||||
* exchanges the code for the full credential (access + ~1-year refresh token).
|
||||
*
|
||||
* The signature is bound to a client-minted device id, so we mint it in the
|
||||
* request step and persist it to the connection immediately, then read it back
|
||||
* in the verify step (the route itself is stateless across the two calls).
|
||||
*/
|
||||
async function loginMaxaiEmail(
|
||||
connectionId: string,
|
||||
connection: Record<string, unknown>,
|
||||
body: { step?: unknown; email?: unknown; code?: unknown }
|
||||
): Promise<NextResponse> {
|
||||
const { randomUUID } = await import("node:crypto");
|
||||
const { requestMaxaiEmailCode, verifyMaxaiEmailCode } = await import(
|
||||
"@omniroute/open-sse/executors/maxai/emailLogin.ts"
|
||||
);
|
||||
|
||||
const psd = (connection.providerSpecificData ?? {}) as Record<string, unknown>;
|
||||
const step = String(body.step || "request");
|
||||
|
||||
if (step === "request") {
|
||||
const email = String(body.email || "").trim();
|
||||
if (!email) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: "An email address is required." },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
// Mint (or reuse) the client identity and persist it BEFORE the request so
|
||||
// the verify step signs with the same device id.
|
||||
const deviceId = String(psd.maxaiDeviceId || psd.deviceId || randomUUID());
|
||||
const clientUserId = String(psd.maxaiClientUserId || psd.clientUserId || randomUUID());
|
||||
try {
|
||||
await updateProviderConnection(connectionId, {
|
||||
providerSpecificData: {
|
||||
...psd,
|
||||
maxaiDeviceId: deviceId,
|
||||
maxaiClientUserId: clientUserId,
|
||||
maxaiLoginEmail: email,
|
||||
},
|
||||
});
|
||||
} catch {
|
||||
/* non-fatal: fall through and still attempt the request */
|
||||
}
|
||||
|
||||
const result = await requestMaxaiEmailCode({ email, deviceId });
|
||||
if (!result.ok) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: result.error || "Failed to send the sign-in code." },
|
||||
{ status: result.status && result.status >= 400 ? result.status : 400 }
|
||||
);
|
||||
}
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
step: "request",
|
||||
message: `A sign-in code was emailed to ${email}. Enter it to finish connecting.`,
|
||||
email,
|
||||
});
|
||||
}
|
||||
|
||||
if (step === "verify") {
|
||||
const code = String(body.code || "").trim();
|
||||
const email = String(body.email || psd.maxaiLoginEmail || "").trim();
|
||||
const deviceId = String(psd.maxaiDeviceId || psd.deviceId || "");
|
||||
const clientUserId = String(psd.maxaiClientUserId || psd.clientUserId || "");
|
||||
if (!code || !email || !deviceId) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: !deviceId
|
||||
? "No pending sign-in. Request a code first."
|
||||
: "The email and the code are both required.",
|
||||
},
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const result = await verifyMaxaiEmailCode({ email, code, deviceId, clientUserId });
|
||||
if (!result.ok || !result.credential) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: result.error || "Code verification failed." },
|
||||
{ status: result.status && result.status >= 400 ? result.status : 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const cred = result.credential;
|
||||
try {
|
||||
await updateProviderConnection(connectionId, {
|
||||
// The access token is replayed as `Authorization: Bearer` by the executor.
|
||||
apiKey: cred.accessToken,
|
||||
providerSpecificData: {
|
||||
...psd,
|
||||
maxaiAccessToken: cred.accessToken,
|
||||
maxaiRefreshToken: cred.refreshToken,
|
||||
maxaiDeviceId: cred.deviceId,
|
||||
maxaiUserId: cred.userId,
|
||||
maxaiClientUserId: cred.clientUserId,
|
||||
maxaiLoginEmail: cred.email,
|
||||
signedInAt: Date.now(),
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
const msg = sanitizeErrorMessage(err instanceof Error ? err.message : err);
|
||||
return NextResponse.json(
|
||||
{ success: false, error: `Signed in but failed to persist: ${msg}` },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
step: "verify",
|
||||
persisted: true,
|
||||
account: cred.email,
|
||||
message: `Connected as ${cred.email}.`,
|
||||
});
|
||||
}
|
||||
|
||||
return NextResponse.json(
|
||||
{ success: false, error: `Unknown login step: ${step}` },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// --- POST: Start login flow -------------------------------------------------
|
||||
|
||||
export async function POST(
|
||||
@@ -178,6 +305,24 @@ export async function POST(
|
||||
};
|
||||
const providerSlug = resolveProviderSlug(provider as Record<string, unknown>);
|
||||
|
||||
// MaxAI: browserless email device-pair login (no browser). Two-step:
|
||||
// {step:"request",email} emails a code; {step:"verify",code} mints + persists.
|
||||
if (providerSlug === "maxai" || providerSlug === "mx") {
|
||||
try {
|
||||
return await loginMaxaiEmail(id, provider as Record<string, unknown>, body as {
|
||||
step?: unknown;
|
||||
email?: unknown;
|
||||
code?: unknown;
|
||||
});
|
||||
} catch (err) {
|
||||
const msg = sanitizeErrorMessage(err instanceof Error ? err.message : err);
|
||||
return NextResponse.json(
|
||||
{ success: false, error: `MaxAI sign-in error: ${msg}` },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Adobe Firefly: dedicated JWT capture (never cookies/localStorage alone).
|
||||
if (isAdobeFireflyProvider(provider as { provider?: unknown }, providerSlug)) {
|
||||
try {
|
||||
|
||||
@@ -50,6 +50,10 @@ import {
|
||||
discoverNotionWebModels,
|
||||
NOTION_WEB_FALLBACK_MODELS,
|
||||
} from "@omniroute/open-sse/services/notionWebModels.ts";
|
||||
import {
|
||||
discoverMaxaiModels,
|
||||
MAXAI_REGISTRY_MODELS,
|
||||
} from "@omniroute/open-sse/services/maxaiModels.ts";
|
||||
import {
|
||||
AZURE_AI_DEFAULT_BASE_URL,
|
||||
buildAzureAiModelsUrl,
|
||||
@@ -595,6 +599,50 @@ export async function GET(
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// MaxAI: live catalog + per-model context windows from the signed
|
||||
// /models/get_config (the call the web app makes on load). Falls back to the
|
||||
// curated static registry catalog on any auth/transport/shape failure.
|
||||
if (provider === "maxai") {
|
||||
const cachedResponse = maybeReturnCachedDiscovery();
|
||||
if (cachedResponse) return cachedResponse;
|
||||
|
||||
const autoFetchDisabledResponse = maybeReturnAutoFetchDisabled();
|
||||
if (autoFetchDisabledResponse) return autoFetchDisabledResponse;
|
||||
|
||||
try {
|
||||
const discovery = await discoverMaxaiModels({
|
||||
providerSpecificData: connection.providerSpecificData,
|
||||
accessToken: apiKey || accessToken,
|
||||
fetchImpl: (url, init) =>
|
||||
safeOutboundFetch(url, {
|
||||
...SAFE_OUTBOUND_FETCH_PRESETS.modelsDiscovery,
|
||||
guard: getProviderOutboundGuard(),
|
||||
proxyConfig: proxy,
|
||||
...init,
|
||||
}),
|
||||
});
|
||||
return buildApiDiscoveryResponse(discovery.models, discovery.warning);
|
||||
} catch (error) {
|
||||
console.log("Error fetching models from maxai", {
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
const fallback = buildDiscoveryFallbackResponse({
|
||||
cacheWarning: "MaxAI models/get_config failed — using cached catalog",
|
||||
localWarning: "MaxAI models/get_config failed — using curated catalog",
|
||||
});
|
||||
if (fallback) return fallback;
|
||||
return buildResponse({
|
||||
provider,
|
||||
connectionId,
|
||||
models: MAXAI_REGISTRY_MODELS,
|
||||
source: "local_catalog",
|
||||
intentional: true,
|
||||
warning: "MaxAI catalog unavailable — using curated model list",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const conolResponse = await maybeHandleConolModelDiscovery({
|
||||
provider,
|
||||
connectionId,
|
||||
|
||||
@@ -477,6 +477,27 @@ export const WEB_COOKIE_PROVIDERS = {
|
||||
authHint:
|
||||
"Use browser sign-in, or paste the full Cookie header from conol.ai. The __Secure-better-auth.session_token cookie is required.",
|
||||
},
|
||||
maxai: {
|
||||
id: "maxai",
|
||||
serviceKinds: ["llm"],
|
||||
alias: "mx",
|
||||
name: "MaxAI",
|
||||
icon: "auto_awesome",
|
||||
color: "#6D28D9",
|
||||
textIcon: "MX",
|
||||
website: "https://www.maxai.co",
|
||||
// No subscriptionRisk / riskNoticeVariant / notice: MaxAI is TOKEN-authenticated
|
||||
// (a bearer access token + a long-lived refresh token that OmniRoute refreshes
|
||||
// browserlessly), NOT a fragile browser-cookie session, so the "webCookie"
|
||||
// caveat ("may invalidate at any time, log in again, not for unattended use")
|
||||
// and the "oauth" caveat ("official session not authorized for proxy use") are
|
||||
// both inaccurate — MaxAI is a purpose-built aggregator whose token IS meant for
|
||||
// API use. Treated like codex-app-server: no risk banner and no notice; the
|
||||
// authHint carries the only guidance a connecting operator needs.
|
||||
toolCalling: "emulated",
|
||||
authHint:
|
||||
"Sign in once (email code or browser) to mint a MaxAI access token. OmniRoute signs each request, routes it through residential egress, and refreshes the token browserlessly, so a connection stays valid for about a year without re-login.",
|
||||
},
|
||||
};
|
||||
|
||||
/** Resolved public site for a web-session provider (href + display host). */
|
||||
|
||||
@@ -334,6 +334,22 @@ export const WEB_SESSION_CREDENTIAL_REQUIREMENTS = {
|
||||
acceptsFullCookieHeader: true,
|
||||
storageKeys: ["cookie", "__Secure-better-auth.session_token"],
|
||||
},
|
||||
maxai: {
|
||||
kind: "token",
|
||||
credentialName: "MaxAI access token (Bearer) + device id",
|
||||
placeholder:
|
||||
"Use browser sign-in — OmniRoute mints the MaxAI access token, device id, and user id for you",
|
||||
acceptsFullCookieHeader: false,
|
||||
storageKeys: [
|
||||
"accessToken",
|
||||
"access_token",
|
||||
"maxaiAccessToken",
|
||||
"deviceId",
|
||||
"maxaiDeviceId",
|
||||
"userId",
|
||||
"maxaiUserId",
|
||||
],
|
||||
},
|
||||
} satisfies Record<string, WebSessionCredentialRequirement> &
|
||||
Record<keyof typeof WEB_COOKIE_PROVIDERS, WebSessionCredentialRequirement>;
|
||||
|
||||
|
||||
@@ -340,6 +340,8 @@
|
||||
"tests/unit/repro-9486.test.ts",
|
||||
"tests/unit/repro-9630-combo-false-503.test.ts",
|
||||
"tests/unit/repro-antigravity-404-family-cooldown-hijack.test.ts",
|
||||
"tests/unit/repro-combo-persisted-cooldown-preskip.test.ts",
|
||||
"tests/unit/repro-glm-iso-reset-24h-cap.test.ts",
|
||||
"tests/unit/resilience-connections.test.ts",
|
||||
"tests/unit/responses-handler.test.ts",
|
||||
"tests/unit/responses-passthrough-openai-compatible.test.ts",
|
||||
|
||||
@@ -420,6 +420,11 @@
|
||||
"configSource": "<custom-config>",
|
||||
"provider": "lmarena"
|
||||
},
|
||||
"maxai": {
|
||||
"className": "MaxAiExecutor",
|
||||
"configSource": "maxai",
|
||||
"provider": "maxai"
|
||||
},
|
||||
"moonshot": {
|
||||
"className": "MoonshotExecutor",
|
||||
"configSource": "moonshot",
|
||||
@@ -666,6 +671,6 @@
|
||||
"provider": "zai-web"
|
||||
}
|
||||
},
|
||||
"keyCount": 133,
|
||||
"keyCount": 134,
|
||||
"sharedInstances": []
|
||||
}
|
||||
|
||||
@@ -3614,6 +3614,29 @@
|
||||
"stream": "https://chat.maritaca.ai/api"
|
||||
}
|
||||
},
|
||||
"maxai": {
|
||||
"format": "openai",
|
||||
"headers": {
|
||||
"apiKey": {
|
||||
"Accept": "text/event-stream",
|
||||
"Authorization": "Bearer <TOK>",
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
"nonStream": {
|
||||
"Authorization": "Bearer <TOK>",
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
"oauth": {
|
||||
"Accept": "text/event-stream",
|
||||
"Authorization": "Bearer <TOK>",
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
},
|
||||
"url": {
|
||||
"nonStream": "https://api.maxai.me",
|
||||
"stream": "https://api.maxai.me"
|
||||
}
|
||||
},
|
||||
"meganova-ai": {
|
||||
"format": "openai",
|
||||
"headers": {
|
||||
|
||||
122
tests/unit/helpers/maxaiMockConstants.ts
Normal file
@@ -0,0 +1,122 @@
|
||||
/**
|
||||
* Shared MOCK signing constants + synthetic bundle fixtures for MaxAI unit tests.
|
||||
*
|
||||
* IMPORTANT: nothing here is a real MaxAI value. Every id/key/version is an
|
||||
* obviously-synthetic placeholder that is merely SHAPE-valid (hex / UUID /
|
||||
* webpage_x.y.z) so it exercises the same validation/parse paths the real values
|
||||
* would. The real constants are fetched at runtime and persisted to the DB; they
|
||||
* never appear in the repo (source or tests).
|
||||
*
|
||||
* The signer tests prove the HMAC→SM3→AES ALGORITHM by comparing the production
|
||||
* signer's output to an INDEPENDENT reference implementation (below) computed over
|
||||
* the same mock key — algorithm correctness without pinning any captured vector.
|
||||
*/
|
||||
import { createHmac, createHash } from "node:crypto";
|
||||
import type { MaxaiSigningConstants } from "../../../open-sse/executors/maxai/constants.ts";
|
||||
import { MAXAI_DEFAULT_HEADER_NAMES } from "../../../open-sse/executors/maxai/constants.ts";
|
||||
|
||||
/** Obviously-fake, shape-valid mock constants (40+ hex, UUID, webpage_x.y.z). */
|
||||
export const MOCK_HMAC_KEY = "a".repeat(56); // 56 hex chars, like the real key's shape
|
||||
export const MOCK_AES_KEY = "b".repeat(56);
|
||||
export const MOCK_CTX_KEY = "c".repeat(40); // 40 hex chars
|
||||
export const MOCK_DOC_ID_KEY = "00000000-0000-4000-8000-000000000000"; // UUID shape
|
||||
export const MOCK_APP_VERSION = "webpage_0.0.0"; // version shape, clearly not real
|
||||
export const MOCK_USER_ID = "11111111-1111-4111-8111-111111111111";
|
||||
export const MOCK_DEVICE_ID = "22222222-2222-4222-8222-222222222222";
|
||||
|
||||
export const MOCK_CONSTANTS: MaxaiSigningConstants = {
|
||||
hmacKey: MOCK_HMAC_KEY,
|
||||
aesKey: MOCK_AES_KEY,
|
||||
appVersion: MOCK_APP_VERSION,
|
||||
ctxKey: MOCK_CTX_KEY,
|
||||
docIdKey: MOCK_DOC_ID_KEY,
|
||||
headerNames: { ...MAXAI_DEFAULT_HEADER_NAMES },
|
||||
source: "extracted",
|
||||
extractedAt: 0,
|
||||
};
|
||||
|
||||
/**
|
||||
* INDEPENDENT reference implementation of the SM3 proof `p` (deliberately NOT
|
||||
* imported from the production module) so a passing test proves the production
|
||||
* math matches an external spec, not merely itself.
|
||||
*/
|
||||
export function referenceProof(
|
||||
appVersion: string,
|
||||
reqTime: number,
|
||||
path: string,
|
||||
userId: string,
|
||||
hmacKey: string
|
||||
): string {
|
||||
const signStr = `${appVersion}:${reqTime}:${path}:${userId}`;
|
||||
const sha1 = createHmac("sha1", Buffer.from(`${reqTime}:${hmacKey}`, "utf8"))
|
||||
.update(Buffer.from(signStr, "utf8"))
|
||||
.digest("hex");
|
||||
return createHash("sm3")
|
||||
.update(Buffer.from(`${reqTime}:${sha1}:${hmacKey}`, "utf8"))
|
||||
.digest("hex");
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a SYNTHETIC `pages/_app` chunk that mirrors the real webpack shape the
|
||||
* parser matches: module 69319 defining export getters `Mn/Rl/U0` over `let`
|
||||
* vars, plus the sole `webpage_x.y.z` literal. Uses only the MOCK values.
|
||||
*/
|
||||
export function makeSyntheticAppChunk(
|
||||
c: {
|
||||
hmacKey?: string;
|
||||
aesKey?: string;
|
||||
docIdKey?: string;
|
||||
appVersion?: string;
|
||||
} = {}
|
||||
): string {
|
||||
const hmac = c.hmacKey ?? MOCK_HMAC_KEY;
|
||||
const aes = c.aesKey ?? MOCK_AES_KEY;
|
||||
const doc = c.docIdKey ?? MOCK_DOC_ID_KEY;
|
||||
const ver = c.appVersion ?? MOCK_APP_VERSION;
|
||||
// Mirrors the real bundle: getters export short vars; the const run assigns the
|
||||
// literals (kept in a separate region, exactly like the minified original).
|
||||
return [
|
||||
`(self.webpackChunk=self.webpackChunk||[]).push([[69319],{`,
|
||||
`69319:function(e,t,a){"use strict";a.d(t,{`,
|
||||
`Mn:function(){return u},Rl:function(){return s},U0:function(){return c},`,
|
||||
`GB:function(){return m},$0:function(){return p}});`,
|
||||
`let l="https://api.maxai.me",i="${ver}",o="MAXAI_APP",`,
|
||||
`s="${aes}",u="${hmac}",c="${doc}",d="website-nextjs",p="prod",m=!1;`,
|
||||
`}}]);`,
|
||||
].join("");
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a SYNTHETIC signer chunk that mirrors the real payload-assembly shape:
|
||||
* the ctx slot `"<40hex>":{a:await this.getContext()}` plus `(0,r.nj)("<hex>")`
|
||||
* header-name decoders. Uses only the MOCK ctx key.
|
||||
*/
|
||||
export function makeSyntheticSignerChunk(ctxKey: string = MOCK_CTX_KEY): string {
|
||||
const nj = (s: string) => `(0,r.nj)("${Buffer.from(s, "utf8").toString("hex")}")`;
|
||||
return [
|
||||
`n.set(${nj("X-Browser-Name")},"Firefox");`,
|
||||
`n.set(${nj("X-Authorization")},(0,r.P0)({`,
|
||||
`[${nj("X-Client-Domain")}]:b,[${nj("X-Client-Path")}]:I,`,
|
||||
`[${nj("X-Random")}]:Math.floor(1e5+9e5*Math.random()).toString(),`,
|
||||
`[${nj("t")}]:m,[${nj("p")}]:T,[${nj("d")}]:await this.getAPIFetchDeviceID(),`,
|
||||
`"${ctxKey}":{a:await this.getContext()}},i.Rl));`,
|
||||
`n.set(${nj("X-App-Version")},i.F8);`,
|
||||
`n.set(${nj("X-App-Env")},${nj("MaxAI-Browser-Extension")});`,
|
||||
].join("");
|
||||
}
|
||||
|
||||
/** Build the app HTML that references synthetic chunk URLs (build-independent). */
|
||||
export function makeSyntheticAppHtml(
|
||||
opts: { appChunk?: string; signerChunk?: string; extra?: string[] } = {}
|
||||
): string {
|
||||
const app = opts.appChunk ?? "/_next/static/chunks/pages/_app-deadbeef.js";
|
||||
const signer = opts.signerChunk ?? "/_next/static/chunks/91234-cafebabe.js";
|
||||
const extras = (opts.extra ?? ["/_next/static/chunks/webpack-1111.js"]).map(
|
||||
(u) => `<script src="${u}"></script>`
|
||||
);
|
||||
return (
|
||||
extras.join("") +
|
||||
`<script src="${app}"></script>` +
|
||||
`<script src="${signer}"></script>`
|
||||
);
|
||||
}
|
||||
218
tests/unit/maxai-documents.test.ts
Normal file
@@ -0,0 +1,218 @@
|
||||
import { test } from "node:test";
|
||||
import assert from "node:assert";
|
||||
import {
|
||||
computeMaxaiDocId,
|
||||
maxaiDocType,
|
||||
parseInlineDataUrl,
|
||||
extractCurrentTurnDocs,
|
||||
buildUploadMultipart,
|
||||
sawUploadDone,
|
||||
uploadMaxaiDocument,
|
||||
resolveMaxaiDocList,
|
||||
} from "../../open-sse/executors/maxai/documents.ts";
|
||||
import { __setMaxaiConstantsForTest } from "../../open-sse/executors/maxai/constantsStore.ts";
|
||||
import { MOCK_CONSTANTS, MOCK_DOC_ID_KEY } from "./helpers/maxaiMockConstants.ts";
|
||||
|
||||
// Doc uploads sign like any request, so seed the in-process constants memo with
|
||||
// MOCK values instead of mocking the bundle fetch. Nothing real is committed.
|
||||
const DOC_ID_KEY = MOCK_DOC_ID_KEY;
|
||||
__setMaxaiConstantsForTest(MOCK_CONSTANTS);
|
||||
|
||||
const AUTH = {
|
||||
accessToken: "tok-abc",
|
||||
userId: "11111111-1111-4111-8111-111111111111",
|
||||
deviceId: "22222222-2222-4222-8222-222222222222",
|
||||
};
|
||||
|
||||
// --- doc_id (content-addressed HMAC-SHA1) --------------------------------
|
||||
|
||||
test("computeMaxaiDocId is a stable HMAC-SHA1(bytes, key) hex digest", () => {
|
||||
// Cross-checked shape: HMAC-SHA1 hex is 40 chars; deterministic for same input.
|
||||
const id = computeMaxaiDocId(Buffer.from("hello world"), DOC_ID_KEY);
|
||||
assert.equal(id.length, 40);
|
||||
assert.match(id, /^[0-9a-f]{40}$/);
|
||||
assert.equal(computeMaxaiDocId(Buffer.from("hello world"), DOC_ID_KEY), id);
|
||||
// Different key or bytes → different id.
|
||||
assert.notEqual(id, computeMaxaiDocId(Buffer.from("hello world"), "different-key"));
|
||||
assert.notEqual(id, computeMaxaiDocId(Buffer.from("other"), DOC_ID_KEY));
|
||||
});
|
||||
|
||||
test("computeMaxaiDocId requires a key (never hashes with a guess)", () => {
|
||||
assert.throws(() => computeMaxaiDocId(Buffer.from("x"), ""));
|
||||
});
|
||||
|
||||
// --- doc_type classification --------------------------------------------
|
||||
|
||||
test("maxaiDocType classifies pdf / code / text", () => {
|
||||
assert.equal(maxaiDocType("report.pdf", "application/pdf"), "page_content__pdf");
|
||||
assert.equal(maxaiDocType("script.py", "text/x-python"), "chat_file_code");
|
||||
assert.equal(maxaiDocType("main.ts", "text/plain"), "chat_file_code");
|
||||
assert.equal(maxaiDocType("notes.txt", "text/plain"), "chat_file");
|
||||
assert.equal(maxaiDocType("data.csv", "text/csv"), "chat_file");
|
||||
});
|
||||
|
||||
// --- data-url parsing ----------------------------------------------------
|
||||
|
||||
test("parseInlineDataUrl decodes base64 + plain data urls", () => {
|
||||
const b64 = parseInlineDataUrl("data:text/plain;base64,aGVsbG8="); // "hello"
|
||||
assert.equal(b64?.mimeType, "text/plain");
|
||||
assert.equal(b64?.bytes.toString("utf8"), "hello");
|
||||
|
||||
const plain = parseInlineDataUrl("data:text/plain,hi%20there");
|
||||
assert.equal(plain?.bytes.toString("utf8"), "hi there");
|
||||
|
||||
assert.equal(parseInlineDataUrl("https://example.com/x.pdf"), null);
|
||||
assert.equal(parseInlineDataUrl("data:text/plain;base64,"), null); // empty
|
||||
assert.equal(parseInlineDataUrl(42), null);
|
||||
});
|
||||
|
||||
// --- extract inline docs from the current turn --------------------------
|
||||
|
||||
test("extractCurrentTurnDocs handles OpenAI file, Responses input_file, Claude document", () => {
|
||||
const docs = extractCurrentTurnDocs([
|
||||
{ role: "system", content: "sys" },
|
||||
{
|
||||
role: "user",
|
||||
content: [
|
||||
{ type: "text", text: "review these" },
|
||||
{
|
||||
type: "file",
|
||||
file: { filename: "a.txt", file_data: "data:text/plain;base64,QQ==" }, // "A"
|
||||
},
|
||||
{ type: "input_file", filename: "b.md", file_data: "data:text/markdown;base64,Qg==" }, // "B"
|
||||
{
|
||||
type: "document",
|
||||
title: "c.pdf",
|
||||
source: { type: "base64", media_type: "application/pdf", data: "Qw==" }, // "C"
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
assert.equal(docs.length, 3);
|
||||
assert.equal(docs[0].filename, "a.txt");
|
||||
assert.equal(docs[0].bytes.toString("utf8"), "A");
|
||||
assert.equal(docs[1].filename, "b.md");
|
||||
assert.equal(docs[2].filename, "c.pdf");
|
||||
assert.equal(docs[2].mimeType, "application/pdf");
|
||||
});
|
||||
|
||||
test("extractCurrentTurnDocs returns [] for a plain-text turn", () => {
|
||||
assert.deepEqual(extractCurrentTurnDocs([{ role: "user", content: "just text" }]), []);
|
||||
});
|
||||
|
||||
test("extractCurrentTurnDocs ignores image_url and remote-url file parts", () => {
|
||||
const docs = extractCurrentTurnDocs([
|
||||
{
|
||||
role: "user",
|
||||
content: [
|
||||
{ type: "image_url", image_url: { url: "data:image/png;base64,AAAA" } },
|
||||
{ type: "file", file: { filename: "x.pdf", file_data: "https://example.com/x.pdf" } },
|
||||
],
|
||||
},
|
||||
]);
|
||||
assert.deepEqual(docs, []); // image handled by vision path; remote url not an inline upload
|
||||
});
|
||||
|
||||
// --- multipart body ------------------------------------------------------
|
||||
|
||||
test("buildUploadMultipart includes all required fields + the file bytes", () => {
|
||||
const doc = { filename: "notes.txt", mimeType: "text/plain", bytes: Buffer.from("secret data") };
|
||||
const body = buildUploadMultipart(doc, "docid123", "chat_file", "BOUND").toString("utf8");
|
||||
assert.ok(body.includes('name="doc_id"\r\n\r\ndocid123'));
|
||||
assert.ok(body.includes('name="doc_type"\r\n\r\nchat_file'));
|
||||
assert.ok(body.includes('name="pure_text"\r\n\r\nsecret data')); // textual -> pure_text filled
|
||||
assert.ok(body.includes('name="tokens"'));
|
||||
assert.ok(body.includes('name="doc_type_dependent_data"\r\n\r\n{}'));
|
||||
assert.ok(body.includes('name="file"; filename="notes.txt"'));
|
||||
assert.ok(body.includes("Content-Type: text/plain"));
|
||||
assert.ok(body.trimEnd().endsWith("--BOUND--"));
|
||||
});
|
||||
|
||||
test("buildUploadMultipart leaves pure_text empty for binary (pdf)", () => {
|
||||
const doc = { filename: "r.pdf", mimeType: "application/pdf", bytes: Buffer.from([1, 2, 3, 4]) };
|
||||
const body = buildUploadMultipart(doc, "id", "page_content__pdf", "B").toString("latin1");
|
||||
assert.ok(body.includes('name="pure_text"\r\n\r\n\r\n')); // empty value
|
||||
});
|
||||
|
||||
// --- SSE done detection --------------------------------------------------
|
||||
|
||||
test("sawUploadDone detects the terminal event", () => {
|
||||
assert.equal(sawUploadDone('data: {"event":"upload_done","data":{"doc_id":"x"}}'), true);
|
||||
assert.equal(sawUploadDone('data: {"event":"upload_to_s3"}'), false);
|
||||
});
|
||||
|
||||
// --- upload (mocked fetch) ----------------------------------------------
|
||||
|
||||
test("uploadMaxaiDocument returns a doc_list entry on upload_done", async () => {
|
||||
let hitUrl = "";
|
||||
let hitContentType = "";
|
||||
const fetchImpl = (async (url: string, init: RequestInit) => {
|
||||
hitUrl = url;
|
||||
hitContentType = (init.headers as Record<string, string>)["Content-Type"];
|
||||
return {
|
||||
ok: true,
|
||||
status: 200,
|
||||
async text() {
|
||||
return 'data: {"event":"upload_done","data":{"doc_id":"srv"}}\n';
|
||||
},
|
||||
} as unknown as Response;
|
||||
}) as unknown as typeof fetch;
|
||||
|
||||
const entry = await uploadMaxaiDocument(
|
||||
{ filename: "a.txt", mimeType: "text/plain", bytes: Buffer.from("hi") },
|
||||
AUTH,
|
||||
{ fetchImpl }
|
||||
);
|
||||
assert.ok(entry);
|
||||
assert.equal(entry!.doc_id, computeMaxaiDocId(Buffer.from("hi"), DOC_ID_KEY));
|
||||
assert.equal(entry!.doc_type, "chat_file");
|
||||
assert.equal(entry!.file_name, "a.txt");
|
||||
assert.match(hitUrl, /\/app\/upload_document$/);
|
||||
assert.match(hitContentType, /^multipart\/form-data; boundary=/);
|
||||
});
|
||||
|
||||
test("uploadMaxaiDocument returns null on a non-200 (best-effort)", async () => {
|
||||
const fetchImpl = (async () =>
|
||||
({ ok: false, status: 400, async text() { return "bad"; } }) as unknown as Response) as unknown as typeof fetch;
|
||||
const entry = await uploadMaxaiDocument(
|
||||
{ filename: "a.txt", mimeType: "text/plain", bytes: Buffer.from("hi") },
|
||||
AUTH,
|
||||
{ fetchImpl }
|
||||
);
|
||||
assert.equal(entry, null);
|
||||
});
|
||||
|
||||
test("resolveMaxaiDocList uploads all current-turn docs, skips failures", async () => {
|
||||
let call = 0;
|
||||
const fetchImpl = (async () => {
|
||||
call += 1;
|
||||
// first upload succeeds, second fails
|
||||
if (call === 1) {
|
||||
return { ok: true, status: 200, async text() { return '{"event":"upload_done"}'; } } as unknown as Response;
|
||||
}
|
||||
return { ok: false, status: 500, async text() { return ""; } } as unknown as Response;
|
||||
}) as unknown as typeof fetch;
|
||||
|
||||
const list = await resolveMaxaiDocList(
|
||||
[
|
||||
{
|
||||
role: "user",
|
||||
content: [
|
||||
{ type: "file", file: { filename: "a.txt", file_data: "data:text/plain;base64,QQ==" } },
|
||||
{ type: "file", file: { filename: "b.txt", file_data: "data:text/plain;base64,Qg==" } },
|
||||
],
|
||||
},
|
||||
],
|
||||
AUTH,
|
||||
{ fetchImpl }
|
||||
);
|
||||
assert.equal(list.length, 1); // one succeeded, one skipped
|
||||
assert.equal(list[0].file_name, "a.txt");
|
||||
});
|
||||
|
||||
test("resolveMaxaiDocList returns [] when there are no inline docs", async () => {
|
||||
const list = await resolveMaxaiDocList([{ role: "user", content: "hi" }], AUTH, {
|
||||
fetchImpl: (async () => ({ ok: true, status: 200, async text() { return ""; } }) as unknown as Response) as unknown as typeof fetch,
|
||||
});
|
||||
assert.deepEqual(list, []);
|
||||
});
|
||||
169
tests/unit/maxai-image.test.ts
Normal file
@@ -0,0 +1,169 @@
|
||||
import { test } from "node:test";
|
||||
import assert from "node:assert";
|
||||
import {
|
||||
resolveMaxaiImageModel,
|
||||
snapMaxaiImageSize,
|
||||
extractMaxaiImageUrls,
|
||||
handleMaxaiImageGeneration,
|
||||
MAXAI_IMAGE_PATH,
|
||||
} from "../../open-sse/handlers/imageGeneration/providers/maxaiImage.ts";
|
||||
import { IMAGE_PROVIDERS } from "../../open-sse/config/imageRegistry.ts";
|
||||
import { __setMaxaiConstantsForTest } from "../../open-sse/executors/maxai/constantsStore.ts";
|
||||
import { MOCK_CONSTANTS } from "./helpers/maxaiMockConstants.ts";
|
||||
|
||||
// Image generation signs like any request; seed the in-process constants memo
|
||||
// with MOCK values so the handler doesn't try to fetch the live MaxAI bundle.
|
||||
__setMaxaiConstantsForTest(MOCK_CONSTANTS);
|
||||
|
||||
// A minimal valid MaxAI credential (userId derives nothing here; the signer is
|
||||
// exercised elsewhere). providerSpecificData carries the token + device id.
|
||||
const CRED = {
|
||||
providerSpecificData: {
|
||||
maxaiAccessToken: "tok-abc",
|
||||
maxaiDeviceId: "dev-123",
|
||||
maxaiUserId: "11111111-1111-4111-8111-111111111111",
|
||||
},
|
||||
};
|
||||
|
||||
// --- Registry ------------------------------------------------------------
|
||||
|
||||
test("maxai is registered in IMAGE_PROVIDERS with the maxai-image format + 6 models", () => {
|
||||
const entry = (IMAGE_PROVIDERS as Record<string, { format?: string; baseUrl?: string; models?: unknown[] }>)["maxai"];
|
||||
assert.ok(entry, "maxai must exist in IMAGE_PROVIDERS");
|
||||
assert.equal(entry.format, "maxai-image");
|
||||
assert.match(String(entry.baseUrl), /api\.maxai\.me\/gpt\/get_image_generate_response/);
|
||||
assert.equal((entry.models ?? []).length, 6);
|
||||
});
|
||||
|
||||
// --- Pure helpers --------------------------------------------------------
|
||||
|
||||
test("resolveMaxaiImageModel strips maxai/ prefix and resolves aliases", () => {
|
||||
assert.equal(resolveMaxaiImageModel("maxai/gpt-image-1"), "gpt-image-1");
|
||||
assert.equal(resolveMaxaiImageModel("stable-diffusion-v3"), "sd3-medium");
|
||||
assert.equal(resolveMaxaiImageModel("stable-diffusion-3-medium"), "sd3-medium");
|
||||
assert.equal(resolveMaxaiImageModel("flux-1-schnell"), "flux-1-schnell");
|
||||
});
|
||||
|
||||
test("snapMaxaiImageSize snaps unsupported sizes for strict models, passes flux through", () => {
|
||||
// gpt-image-1 / dall-e-3 reject 512x512 -> snap to 1024x1024
|
||||
assert.equal(snapMaxaiImageSize("gpt-image-1", "512x512"), "1024x1024");
|
||||
assert.equal(snapMaxaiImageSize("dall-e-3", "256x256"), "1024x1024");
|
||||
// supported sizes pass through
|
||||
assert.equal(snapMaxaiImageSize("gpt-image-1", "1536x1024"), "1536x1024");
|
||||
assert.equal(snapMaxaiImageSize("dall-e-3", "1792x1024"), "1792x1024");
|
||||
// flux / sd3: no constraint, any size passes through
|
||||
assert.equal(snapMaxaiImageSize("flux-1-schnell", "512x512"), "512x512");
|
||||
assert.equal(snapMaxaiImageSize("sd3-medium", "768x768"), "768x768");
|
||||
// missing size -> default
|
||||
assert.equal(snapMaxaiImageSize("gpt-image-1", undefined), "1024x1024");
|
||||
});
|
||||
|
||||
test("extractMaxaiImageUrls prefers png_url, falls back to webp_url", () => {
|
||||
assert.deepEqual(
|
||||
extractMaxaiImageUrls([{ png_url: "p.png", webp_url: "w.webp" }, { webp_url: "only.webp" }]),
|
||||
["p.png", "only.webp"]
|
||||
);
|
||||
assert.deepEqual(extractMaxaiImageUrls([]), []);
|
||||
assert.deepEqual(extractMaxaiImageUrls(null), []);
|
||||
});
|
||||
|
||||
// --- Handler (mocked fetch) ---------------------------------------------
|
||||
|
||||
function mockFetch(status: number, jsonBody: unknown): typeof fetch {
|
||||
return (async () =>
|
||||
({
|
||||
ok: status >= 200 && status < 300,
|
||||
status,
|
||||
async json() {
|
||||
return jsonBody;
|
||||
},
|
||||
async text() {
|
||||
return JSON.stringify(jsonBody);
|
||||
},
|
||||
}) as unknown as Response) as unknown as typeof fetch;
|
||||
}
|
||||
|
||||
test("handleMaxaiImageGeneration returns OpenAI image data on success", async () => {
|
||||
let capturedUrl = "";
|
||||
let capturedBody: Record<string, unknown> = {};
|
||||
const fetchImpl = (async (url: string, init: RequestInit) => {
|
||||
capturedUrl = url;
|
||||
capturedBody = JSON.parse(String(init.body));
|
||||
return {
|
||||
ok: true,
|
||||
status: 200,
|
||||
async json() {
|
||||
return { status: "OK", data: [{ png_url: "https://cdn/x.png", webp_url: "https://cdn/x.webp" }] };
|
||||
},
|
||||
async text() {
|
||||
return "";
|
||||
},
|
||||
} as unknown as Response;
|
||||
}) as unknown as typeof fetch;
|
||||
|
||||
const result = (await handleMaxaiImageGeneration({
|
||||
model: "flux-1-schnell",
|
||||
provider: "maxai",
|
||||
body: { prompt: "a red bicycle", size: "512x512", n: 2 },
|
||||
credentials: CRED,
|
||||
fetchImpl,
|
||||
})) as { success: boolean; data?: { data: Array<{ url: string }> } };
|
||||
|
||||
assert.equal(result.success, true);
|
||||
assert.deepEqual(result.data?.data, [{ url: "https://cdn/x.png" }]);
|
||||
// Hit the image endpoint with the signed body.
|
||||
assert.match(capturedUrl, new RegExp(MAXAI_IMAGE_PATH.replace(/\//g, "\\/")));
|
||||
assert.equal(capturedBody.model_name, "flux-1-schnell");
|
||||
assert.equal(capturedBody.size, "512x512"); // flux passes size through
|
||||
assert.equal(capturedBody.n, 2);
|
||||
});
|
||||
|
||||
test("handleMaxaiImageGeneration 401 is retryable (credential fallback)", async () => {
|
||||
const result = (await handleMaxaiImageGeneration({
|
||||
model: "gpt-image-1",
|
||||
provider: "maxai",
|
||||
body: { prompt: "x" },
|
||||
credentials: CRED,
|
||||
fetchImpl: mockFetch(401, { error: "expired" }),
|
||||
})) as { success: boolean; status?: number; retryable?: boolean };
|
||||
assert.equal(result.success, false);
|
||||
assert.equal(result.status, 401);
|
||||
assert.equal(result.retryable, true);
|
||||
});
|
||||
|
||||
test("handleMaxaiImageGeneration rejects an empty prompt with 400", async () => {
|
||||
const result = (await handleMaxaiImageGeneration({
|
||||
model: "gpt-image-1",
|
||||
provider: "maxai",
|
||||
body: { prompt: " " },
|
||||
credentials: CRED,
|
||||
fetchImpl: mockFetch(200, {}),
|
||||
})) as { success: boolean; status?: number };
|
||||
assert.equal(result.success, false);
|
||||
assert.equal(result.status, 400);
|
||||
});
|
||||
|
||||
test("handleMaxaiImageGeneration 401s with no credential (retryable)", async () => {
|
||||
const result = (await handleMaxaiImageGeneration({
|
||||
model: "gpt-image-1",
|
||||
provider: "maxai",
|
||||
body: { prompt: "x" },
|
||||
credentials: {},
|
||||
fetchImpl: mockFetch(200, {}),
|
||||
})) as { success: boolean; status?: number; retryable?: boolean };
|
||||
assert.equal(result.success, false);
|
||||
assert.equal(result.status, 401);
|
||||
assert.equal(result.retryable, true);
|
||||
});
|
||||
|
||||
test("handleMaxaiImageGeneration surfaces a no-images response as 502", async () => {
|
||||
const result = (await handleMaxaiImageGeneration({
|
||||
model: "sd3-medium",
|
||||
provider: "maxai",
|
||||
body: { prompt: "x" },
|
||||
credentials: CRED,
|
||||
fetchImpl: mockFetch(200, { status: "OK", data: [] }),
|
||||
})) as { success: boolean; status?: number };
|
||||
assert.equal(result.success, false);
|
||||
assert.equal(result.status, 502);
|
||||
});
|
||||
1088
tests/unit/maxai.test.ts
Normal file
@@ -171,7 +171,7 @@ test("shared set size includes live REGISTRY and retired Designer + Felo + Qwen
|
||||
// 1 and adds 2 distinct tombstones "qwen-web"/"qw", a net +1) on top of the
|
||||
// live REGISTRY walk, minus the 3 GPL-derived Raycast/Hailuo Web
|
||||
// ids/aliases removed from REGISTRY by #11691's migration 166.
|
||||
assert.equal(RESERVED_PREFIX_COUNT, 400);
|
||||
assert.equal(RESERVED_PREFIX_COUNT, 402);
|
||||
});
|
||||
|
||||
test("isReservedProviderPrefix rejects non-string input", () => {
|
||||
|
||||
@@ -239,6 +239,18 @@ test("#6593 a maxWaitMs override of 0 is treated as no override", () => {
|
||||
}
|
||||
});
|
||||
|
||||
test("maxai receives a provider-scoped 5min execution budget (slow reasoning models)", () => {
|
||||
// The default 15s Bottleneck expiration kills MaxAI reasoning turns (30s-min+)
|
||||
// mid-think; maxai (and its mx alias) floor at 300s so they complete.
|
||||
assert.equal(rateLimitManager.resolveRequestQueueMaxWaitMs("maxai", 15_000), 300_000);
|
||||
assert.equal(rateLimitManager.resolveRequestQueueMaxWaitMs("MaxAI", 15_000), 300_000);
|
||||
assert.equal(rateLimitManager.resolveRequestQueueMaxWaitMs("mx", 15_000), 300_000);
|
||||
// A larger configured value is preserved (floor never lowers it).
|
||||
assert.equal(rateLimitManager.resolveRequestQueueMaxWaitMs("maxai", 600_000), 600_000);
|
||||
// Other providers are unaffected.
|
||||
assert.equal(rateLimitManager.resolveRequestQueueMaxWaitMs("openai", 15_000), 15_000);
|
||||
});
|
||||
|
||||
test("#6593 DEFAULT_REQUEST_QUEUE_MAX_DEPTH defaults to 0 (disabled) absent an env override", () => {
|
||||
assert.equal(process.env.RATE_LIMIT_MAX_QUEUE_DEPTH, undefined);
|
||||
assert.equal(resilienceSettings.DEFAULT_REQUEST_QUEUE_MAX_DEPTH, 0);
|
||||
|
||||