mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-09-07 07:22:08 +03:00
Compare commits
5 Commits
docs/opena
...
fix/v3850-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d0be77a448 | ||
|
|
f6507634f2 | ||
|
|
14451bb9c2 | ||
|
|
7cb897ade3 | ||
|
|
fba837c056 |
@@ -1941,12 +1941,6 @@ APP_LOG_TO_FILE=true
|
||||
# Base backoff after a transient 408 response (ms); five attempts maximum.
|
||||
# ADOBE_FIREFLY_SUBMIT_BASE_DELAY_MS=8000
|
||||
|
||||
# ── Microsoft Designer Web (Image Generation) ──
|
||||
# Polling config for the microsoft-designer-web submit-then-poll image job.
|
||||
# Used by: open-sse/handlers/imageGeneration/providers/designerWeb.ts
|
||||
# DESIGNER_WEB_POLL_TIMEOUT_MS=60000 # Max wait for job completion (default: 60s)
|
||||
# DESIGNER_WEB_POLL_INTERVAL_MS=2000 # Poll frequency (default: 2s)
|
||||
|
||||
# ── Adobe Firefly (Image Upscale) ──
|
||||
# Base delay (ms) for the submit-retry exponential backoff when Adobe Firefly's
|
||||
# upscale job submission is rate-limited. Used by:
|
||||
|
||||
@@ -1,447 +0,0 @@
|
||||
---
|
||||
title: "Provider Reference"
|
||||
version: 3.8.50
|
||||
lastUpdated: 2026-08-21
|
||||
---
|
||||
|
||||
# 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-21
|
||||
|
||||
Total providers: **349**. See category breakdown below.
|
||||
|
||||
## Categories
|
||||
|
||||
- **Free** — free tier with API key (configured via dashboard)
|
||||
- **No-auth** — public endpoints that require no key or sign-in at all
|
||||
- **OAuth** — sign-in flow handled by OmniRoute, no API key needed
|
||||
- **Web cookie** — wraps the provider's web app via cookie auth
|
||||
- **API key** — paid provider configured via API key (free credits may apply)
|
||||
- **Local** — runs on the user's machine (Ollama, LM Studio, vLLM, etc.)
|
||||
- **Search** — web search providers
|
||||
- **Audio** — audio-only providers (TTS/STT)
|
||||
- **Upstream proxy** — providers that proxy to other providers
|
||||
- **Cloud agent** — long-running coding agents (Codex Cloud, Devin, Jules)
|
||||
- **System** — OmniRoute-internal providers (loopback, etc.)
|
||||
|
||||
Additional tags: `image`, `video`, `aggregator`, `enterprise`, `embed/rerank`, `self-hosted`.
|
||||
|
||||
`Tool calling` (where shown): `native` — real function-calling API; `emulated` — the `tools` array is prompt-emulated via `webTools.ts` (regex-parsed `<tool>{...}</tool>` blocks); `none` — `tools` is currently silently dropped. See #7286.
|
||||
|
||||
Use the dashboard at `/dashboard/providers` to enable, configure, and test each provider.
|
||||
|
||||
---
|
||||
|
||||
## No-auth Providers (no key required) (11)
|
||||
|
||||
| ID | Alias | Name | Tags | Website | Notes | Tool calling |
|
||||
|----|-------|------|------|---------|-------|--------------|
|
||||
| `aihorde` | `horde` | AI Horde | No-auth | [link](https://aihorde.net) | No API key required — uses AI Horde's documented anonymous key. Adding a free aihorde.net key is optional and only buys higher queue priority (kudos). | — |
|
||||
| `auggie` | `aug` | Augment (Auggie CLI) | No-auth | [link](https://augmentcode.com) | No API key stored by OmniRoute. Install the Auggie CLI and run `auggie login` on this machine, then OmniRoute spawns it locally for each request. | — |
|
||||
| `chipotle` | `pepper` | Chipotle Pepper AI (Free) | No-auth | [link](https://amelia.chipotle.com) | No credentials required. Uses Chipotle's public support chatbot via reverse-engineered SockJS/STOMP protocol. | — |
|
||||
| `cloudflare-playground` | `cfp` | Cloudflare AI Playground | No-auth | [link](https://playground.ai.cloudflare.com) | No credentials required — anonymous browser sessions over a reverse-engineered cf_agent WebSocket protocol (Playwright transport). | — |
|
||||
| `devin-cli-agentic` | `dva` | Devin CLI Agentic Bridge | No-auth | [link](https://docs.devin.ai/work-with-devin/devin-cli) | Authentication is owned by the official Devin CLI in its isolated bridge volume. | emulated |
|
||||
| `duckduckgo-web` | `ddgw` | DuckDuckGo AI Chat | No-auth | [link](https://duckduckgo.com/duckchat) | No credentials required — DuckDuckGo AI Chat is anonymous and free. | emulated |
|
||||
| `felo-web` | `felo` | Felo | No-auth | [link](https://felo.ai) | No credentials required — Felo is a free, no-signup chat/search aggregator. | — |
|
||||
| `opencode` | `oc` | OpenCode Free | No-auth | [link](https://opencode.ai) | No API key required — uses OpenCode's public free endpoint. | — |
|
||||
| `theoldllm` | `tllm` | The Old LLM (Free) | No-auth | [link](https://theoldllm.vercel.app) | No credentials required. The executor auto-generates access tokens via an embedded Playwright browser instance. | — |
|
||||
| `veoaifree-web` | `veo-free` | Veo AI Free | No-auth, video | [link](https://veoaifree.com) | No auth required. Rate limited to 6 requests/hour per IP. | — |
|
||||
| `zcode` | `zc` | ZCode (GLM Coding Plan) | No-auth | [link](https://zcode.z.ai) | No API key stored by OmniRoute. The local ZCode app-server uses the existing builtin:zai-coding-plan login. | — |
|
||||
|
||||
## OAuth Providers (25)
|
||||
|
||||
| ID | Alias | Name | Tags | Website | Notes |
|
||||
|----|-------|------|------|---------|-------|
|
||||
| `agy` | `agy` | Antigravity CLI | OAuth | [link](https://antigravity.google) | Import your Antigravity CLI (`agy`) login (paste/upload its token file), auto-detect a local CLI login, or sign in with Google. Shares the Antigravity backend (incl. Claude models). |
|
||||
| `amazon-q` | `aq` | Amazon Q | OAuth | [link](https://aws.amazon.com/q/developer/) | Uses the same AWS Builder ID or imported refresh-token flow as Kiro, but keeps Amazon Q connections separate. |
|
||||
| `antigravity` | — | Antigravity | OAuth | — | — |
|
||||
| `claude` | `cc` | Claude Code | OAuth | — | — |
|
||||
| `cline` | `cl` | Cline | OAuth | — | — |
|
||||
| `clinepass` | `cp` | ClinePass | OAuth | [link](https://cline.bot/cline-pass) | ClinePass is Cline's $9.99/mo subscription bundling 10 open coding models. Sign in with your Cline account (same login as the Cline CLI/IDE), or paste a direct ClinePass API key (app.cline.bot → Settings → API Keys). A ClinePass subscription unlocks the cline-pass/* models. Reuses the Cline WorkOS OAuth flow. |
|
||||
| `codebuddy-cn` | `cbcn` | CodeBuddy CN | OAuth | [link](https://copilot.tencent.com) | Tencent CodeBuddy CN (copilot.tencent.com). Sign in via the official CLI device-code flow, or paste a direct API key (sent as Authorization: Bearer). Catalog: GLM / Kimi / MiniMax / DeepSeek / Hunyuan. |
|
||||
| `codex` | `cx` | OpenAI Codex | OAuth | — | — |
|
||||
| `cursor` | `cu` | Cursor IDE | OAuth | — | — |
|
||||
| `devin-cli` | `dv` | Devin CLI | OAuth | [link](https://cli.devin.ai) | Requires the Devin CLI binary. Run `devin auth login` to authenticate, or provide your WINDSURF_API_KEY. Install: https://cli.devin.ai |
|
||||
| `devin-desktop` | — | Devin Desktop | OAuth | [link](https://devin.ai) | Paste an existing Devin API key from an authenticated Devin session. Key export availability and steps vary by Devin version and account. |
|
||||
| `ghe-copilot` | `ghe-copilot` | GitHub Enterprise Copilot | OAuth | — | Enter your GHE instance URL (e.g., https://ghe.company.com) in provider settings, then authenticate via device flow. |
|
||||
| `github` | `gh` | GitHub Copilot | OAuth | — | — |
|
||||
| `gitlab-duo` | `gitlab-duo` | GitLab Duo | OAuth | [link](https://docs.gitlab.com/user/duo_agent_platform/code_suggestions/) | GitLab Duo OAuth is not configured. Register an OAuth application at https://gitlab.com/-/profile/applications with redirect URI http://localhost:20128/callback and scopes "ai_features read_user", then set GITLAB_DUO_OAUTH_CLIENT_ID (and optionally GITLAB_DUO_OAUTH_CLIENT_SECRET) and restart. |
|
||||
| `grok-cli` | `gc` | Grok Build | OAuth | — | Sign in with your browser, or paste your ~/.grok/auth.json (or the JWT access token) from the Grok Build CLI; refresh_token is rotated automatically either way. |
|
||||
| `kilocode` | `kc` | Kilo Code | OAuth | — | — |
|
||||
| `kimi-coding` | `kmc` | Kimi Code CLI | OAuth | [link](https://www.kimi.com/code?aff=omniroute) | Sign in with the same Kimi account used by Kimi Code CLI. OmniRoute uses the CLI OAuth flow and Kimi Coding Plan endpoints. |
|
||||
| `kiro` | `kr` | Kiro AI | OAuth | — | Free tier: 50 credits/month (~25K–100K tokens). ⚠️ Kiro ToS prohibits third-party proxy/harness use. |
|
||||
| `openference` | `of` | Openference | OAuth | [link](https://openference.com) | Sign in with your Openference account to route requests through api.openference.com. An active plan is required for inference — OAuth may authenticate but return 402 without one. |
|
||||
| `qoder` | `if` | Qoder | OAuth | — | — |
|
||||
| `raycast` | `rc` | Raycast Pro AI | OAuth | [link](https://raycast.com/ai) | Unofficial integration — uses your Raycast Pro subscription via credentials from the macOS app (Auto-Import or manual capture). May break on Raycast updates. Not for redistribution; personal use only. |
|
||||
| `trae` | `tr` | Trae | OAuth | [link](https://trae.ai) | Trae is an AI-native IDE by ByteDance (SOLO remote agent). Authorize via trae.ai in the popup, or sign in at solo.trae.ai and paste the Cloud-IDE-JWT (sent as 'Authorization: Cloud-IDE-JWT <token>', ~14-day lifetime) as the access token; web_id/biz_user_id/user_unique_id/scope/tenant/region propagate via providerSpecificData. No headless refresh for pasted tokens — re-paste on expiry. |
|
||||
| `xai-oauth` | `xao` | xAI OAuth (Grok) | OAuth | [link](https://x.ai) | Sign in with xAI to use api.x.ai models such as Grok 4.5. This is separate from Grok Build JWT sessions, which use cli-chat-proxy.grok.com and grok-build model aliases. |
|
||||
| `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 (35)
|
||||
|
||||
| ID | Alias | Name | Tags | Website | Notes | Tool calling |
|
||||
|----|-------|------|------|---------|-------|--------------|
|
||||
| `adapta-web` | `adp-web` | Adapta.org (Adapta One Web) | Web cookie | [link](https://agent.adapta.one) | Paste your __client cookie value from .clerk.agent.adapta.one (DevTools → Application → Cookies) | emulated |
|
||||
| `adobe-firefly` | `firefly` | Adobe Firefly (Image/Video) | Web cookie | [link](https://firefly.adobe.com) | RECOMMENDED: firefly.adobe.com signed-in → F12 → Network → click firefly-3p.ff.adobe.io (generate-async or models/discovery) → Request Headers → Authorization → copy the token AFTER 'Bearer ' (starts with eyJ…). Cookie-only from firefly.adobe.com mints a GUEST token → 401/403; only multi-domain IMS cookies (adobelogin.com) or that Bearer JWT work. Unofficial/experimental media + Limits. | — |
|
||||
| `blackbox-web` | `bb-web` | Blackbox Web (Subscription) | Web cookie | [link](https://app.blackbox.ai) | Paste your __Secure-authjs.session-token value or full cookie header from app.blackbox.ai | emulated |
|
||||
| `chatgpt-web` | `cgpt-web` | ChatGPT Web (Plus/Pro) | Web cookie | [link](https://chatgpt.com) | Paste your __Secure-next-auth.session-token cookie value from chatgpt.com | emulated |
|
||||
| `chatgpt-web-codex` | `cgpt-codex` | ChatGPT Web (Codex) | Web cookie | [link](https://chatgpt.com) | Paste the full ChatGPT Cookie header. OmniRoute verifies it in an isolated headless browser profile. | native |
|
||||
| `claude-web` | `cw` | Claude Web | Web cookie | [link](https://claude.ai) | Paste your session cookie from claude.ai | none |
|
||||
| `conol-web` | `cnl` | Conol (Unofficial/Experimental) | Web cookie | [link](https://conol.ai) | Use browser sign-in, or paste the full Cookie header from conol.ai. The __Secure-better-auth.session_token cookie is required. | — |
|
||||
| `copilot-m365-web` | `m365copilot` | Microsoft 365 Copilot (BizChat) | Web cookie | [link](https://m365.cloud.microsoft/chat) | Sign in at m365.cloud.microsoft/chat, then open DevTools → Network → filter 'WS' → click the Chathub WebSocket connection. Copy both the access_token query parameter AND the account-specific Chathub path segment from its request URL (wss://…/Chathub/<path>?…&access_token=…). It is NOT an Authorization: Bearer header on an XHR/Fetch request. The token is short-lived; this is an unofficial integration. Optional: store a refresh_token in providerSpecificData.refreshToken (any Microsoft device-code/refresh flow for the substrate.office.com/sydney scopes) and OmniRoute pre-flight-refreshes the access token itself — otherwise re-capture after every ~75 min expiry. | — |
|
||||
| `copilot-web` | `copilot` | Microsoft Copilot Web | Web cookie | [link](https://copilot.microsoft.com) | Paste the access_token from an authenticated copilot.microsoft.com request (DevTools → Network → Authorization), or export a HAR while logged in | — |
|
||||
| `deepseek-web` | `ds-web` | DeepSeek Web | Web cookie | [link](https://chat.deepseek.com) | Paste your userToken from chat.deepseek.com — DevTools → Application → Local Storage → userToken | emulated |
|
||||
| `doubao-web` | `db` | Dola Web (ByteDance) | Web cookie | [link](https://www.dola.com) | Paste the full Cookie header from www.dola.com. It should include sessionid, ttwid, and s_v_web_id. If s_v_web_id is unavailable, fp=verify_... from a chat/completion request URL can be used as a fallback. | — |
|
||||
| `gemini-business` | `gembiz` | Gemini Business (Enterprise) | Web cookie | [link](https://business.gemini.google) | From your enterprise account: open business.gemini.google/home/cid/{your-cid}, then copy __Secure-1PSID and __Secure-1PSIDTS cookies from DevTools → Application → Cookies. Paste as a cookie header below. | — |
|
||||
| `gemini-web` | `gweb` | Gemini Web (Free) | Web cookie | [link](https://gemini.google.com) | Paste your __Secure-1PSID cookie value from gemini.google.com. Optionally add __Secure-1PSIDTS separated by semicolon. | emulated |
|
||||
| `grok-web` | `gw` | Grok Web (Subscription) | Web cookie | [link](https://grok.com) | Paste the full grok.com cookie line from DevTools → Application → Cookies. Include both `sso` and `sso-rw` (e.g. `sso=...; sso-rw=...`) — Grok's anti-bot rejects `sso` on its own. | — |
|
||||
| `hailuo-web` | `hailuo-web` | Hailuo Web (MiniMax) | Web cookie | [link](https://hailuo.ai) | Open hailuo.ai, log in, then open DevTools → Application → Local Storage → copy the "_token" value. device_id/uuid fingerprint fields are derived automatically; if requests fail, re-capture _token (sessions can expire). | — |
|
||||
| `huggingchat` | `huggingchat` | HuggingChat (Free) | Web cookie | [link](https://huggingface.co/chat) | Paste the full Cookie header from huggingface.co/chat (DevTools → Network → /chat/conversation → Request Headers → Cookie). It should include hf-chat and may also include token / aws-waf-token. | — |
|
||||
| `hyperagent` | `ha` | HyperAgent (Unofficial/Experimental) | Web cookie | [link](https://hyperagent.com) | Paste the full Cookie header from hyperagent.com (DevTools → Network → any request → Request Headers → Cookie). Session cookies power chat + billing usage. | — |
|
||||
| `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.com/code?aff=omniroute) | Paste access_token from www.kimi.com 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. | — |
|
||||
| `microsoft-designer-web` | `msdesigner` | Microsoft Designer (Image Generation) | Web cookie | [link](https://designer.microsoft.com) | Sign in at designer.microsoft.com, then open DevTools → Network, generate an image, and find the request to DallE.ashx?action=GetDallEImagesCogSci. Copy the value of its Authorization: Bearer header (the access_token — no 'Bearer ' prefix). The token is short-lived; this is an unofficial, reverse-engineered integration. | — |
|
||||
| `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 |
|
||||
| `poe-web` | `poe` | Poe Web (Subscription) | Web cookie | [link](https://poe.com) | Paste your p-b cookie value from poe.com (DevTools → Application → Cookies → p-b) | — |
|
||||
| `promptql` | `pql` | PromptQL (Unofficial/Experimental) | Web cookie | [link](https://prompt.ql.app) | Paste the Bearer JWT from prompt.ql.app DevTools → Network → graphql → Authorization (token only). Optional projectId + session Cookie for refresh. | — |
|
||||
| `qwen-web` | `qwen-web` | Qwen Web (Free) | Web cookie | [link](https://chat.qwen.ai) | Open chat.qwen.ai, log in, then open DevTools → Application → Local Storage → copy the "token" value (or use tongyi_sso_ticket cookie as Bearer token). | emulated |
|
||||
| `t3-web` | `t3chat` | t3.chat (Pro/Free) | Web cookie | [link](https://t3.chat) | Open t3.chat in your browser, log in, then open DevTools → Application → Local Storage → https://t3.chat. Copy the value of 'convex-session-id'. Also open DevTools → Network, copy the Cookie header from any request. Paste both values here. See provider setup docs for a step-by-step guide. | emulated |
|
||||
| `tencent-aistudio-web` | `tasw` | Tencent AI Studio (Free) | Web cookie | [link](https://aistudio.tencent.ai) | Log in to aistudio.tencent.ai, open DevTools -> Network, copy any request Cookie header containing session tokens. | — |
|
||||
| `tinycms-web` | `tcw` | TinyCMS Web (Free/Sub) | Web cookie | [link](https://site.tinycms.xyz) | Go to site.tinycms.xyz, open DevTools → Application → Local Storage, copy the value of 'app-config-uuid' (starts with 'R'), and paste it here. | — |
|
||||
| `v0-vercel-web` | `v0-vercel-web` | v0 Vercel Web (Code Gen) | Web cookie | [link](https://v0.dev) | Paste your session cookie from v0.dev (DevTools → Application → Cookies) | — |
|
||||
| `venice-web` | `ven` | Venice Web (Privacy) | Web cookie | [link](https://venice.ai) | Paste your session cookie from venice.ai (DevTools → Application → Cookies) | — |
|
||||
| `yuanbao-web` | `ybw` | Tencent Yuanbao (Free) | Web cookie | [link](https://yuanbao.tencent.com) | Log in to yuanbao.tencent.com, then paste the full Cookie header (DevTools → Network → any /api request → Request Headers → Cookie). It must contain hy_user and hy_token. | — |
|
||||
| `zai-web` | `zw` | Z.ai Web | Web cookie | [link](https://chat.z.ai) | Copy the "token" value from chat.z.ai → DevTools → Application → Local Storage. Do not copy cookies; OmniRoute handles the per-request CAPTCHA through its browser transport. | — |
|
||||
| `zenmux-free` | `zmf` | ZenMux Free (Web) | Web cookie | [link](https://zenmux.ai) | Login at zenmux.ai, then export all cookies using EditThisCookie or Cookie-Editor and paste the full Cookie header string here. Refresh every ~30 days. | — |
|
||||
|
||||
## API Key Providers (paid / paid-with-free-credits) (233)
|
||||
|
||||
| ID | Alias | Name | Tags | Website | Notes |
|
||||
|----|-------|------|------|---------|-------|
|
||||
| `360ai` | `360ai` | 360 AI | API key | [link](https://ai.360.cn) | Get API key at ai.360.cn |
|
||||
| `agentrouter` | `agentrouter` | AgentRouter | API key, aggregator | [link](https://agentrouter.org) | $200 free credits on signup - multi-model routing gateway |
|
||||
| `agnes` | `agnes` | Agnes AI | API key, video | [link](https://agnes-ai.com) | Get API key at agnes-ai.com |
|
||||
| `ai21` | `ai21` | AI21 Labs | API key | [link](https://www.ai21.com) | $10 trial credits on signup (valid 3 months), no credit card required |
|
||||
| `aimlapi` | `aiml` | AI/ML API | API key, aggregator | [link](https://aimlapi.com) | Free tier paused (2026) — AI/ML API is now pay-as-you-go only (min $20 top-up); no recurring free credits. |
|
||||
| `ainative` | `ainative` | AINative Studio | API key | [link](https://ainative.studio) | Create a free API key at ainative.studio (no card), then paste it here as a Bearer token. |
|
||||
| `aion` | `aion` | Aion Labs | API key | [link](https://www.aionlabs.ai) | Create a free API key at aionlabs.ai (no card), then paste it here as a Bearer token. |
|
||||
| `alibaba` | `ali` | Alibaba Cloud Model Studio | API key | [link](https://bailian.console.alibabacloud.com/) | — |
|
||||
| `alibaba-cn` | `ali-cn` | Alibaba (China) | API key | [link](https://dashscope.console.aliyun.com/) | — |
|
||||
| `ant-ling` | `ling` | Ant Ling / Ring (inclusionAI) | API key | [link](https://developer.ant-ling.com/en/docs/) | Register and create an API key at the Ant Ling API console (https://chat.ant-ling.com/open), then paste it here. OmniRoute routes chat traffic to https://api.ant-ling.com/v1/chat/completions; the provider is OpenAI-compatible and also exposes an Anthropic-compatible surface. |
|
||||
| `anthropic` | `anthropic` | Anthropic | API key | [link](https://platform.claude.com) | — |
|
||||
| `anyapi` | `anyapi` | AnyAPI AI | API key, aggregator | [link](https://anyapi.ai) | Free plan: 100,000 ANY Tokens/day and 100 RPM for eligible Free/Basic models; no credit card required. |
|
||||
| `api-airforce` | `af` | Api.airforce | API key | [link](https://api.airforce) | 55 free tier models including Grok-3, Claude 3.7, Qwen3, Kimi-K2, Gemini 2.5 Flash, DeepSeek-V3 |
|
||||
| `arcee-ai` | `arcee` | Arcee AI | API key | [link](https://arcee.ai) | Get API key at arcee.ai |
|
||||
| `auriko` | `auriko` | Auriko | API key, aggregator | [link](https://www.auriko.ai) | Free plan publishes 1,000 Platform RPM and 10,000 BYOK RPM. Platform inference still passes through provider cost; this is not a free-token pool or unlimited free inference. |
|
||||
| `azure-ai` | `azure-ai` | Azure AI Foundry | API key, enterprise | [link](https://learn.microsoft.com/azure/ai-foundry) | Use your Azure AI Foundry key. Base URL can be https://<resource>.services.ai.azure.com/openai/v1/ or https://<resource>.openai.azure.com/openai/v1/. |
|
||||
| `azure-openai` | `azure` | Azure OpenAI | API key, enterprise | [link](https://azure.microsoft.com/products/ai-services/openai-service) | Use your Azure OpenAI API key. Base URL should be your resource endpoint, for example https://my-resource.openai.azure.com. |
|
||||
| `bai` | `bai` | b.ai | API key | [link](https://b.ai) | Bearer API key for the b.ai OpenAI-compatible LLM gateway (distinct from TheB.AI). Create a key at https://docs.b.ai, then use https://api.b.ai/v1 as the OpenAI-compatible base URL. |
|
||||
| `baichuan` | `baichuan` | Baichuan | API key | [link](https://www.baichuan-ai.com/) | Get API key at platform.baichuan-ai.com |
|
||||
| `baidu` | `baidu` | Baidu (ERNIE) | API key | [link](https://ernie.baidu.com/) | Get API key at console.bce.baidu.com |
|
||||
| `bailian-coding-plan` | `bcp` | Alibaba Token Plan | API key | [link](https://www.alibabacloud.com/help/en/model-studio/token-plan-overview) | — |
|
||||
| `baseten` | `baseten` | Baseten | API key | [link](https://baseten.co) | $30 free trial credits for GPU inference |
|
||||
| `bazaarlink` | `bzl` | BazaarLink | API key | [link](https://bazaarlink.ai) | Use your BazaarLink API key (starts with sk-bl-) in Authorization: Bearer <key>. OpenAI SDK works with base URL https://bazaarlink.ai/api/v1. Models use provider/model-name format. |
|
||||
| `bedrock` | `bedrock` | Amazon Bedrock | API key, enterprise | [link](https://aws.amazon.com/bedrock) | Use your Amazon Bedrock API key and configure the AWS region where your models are enabled (for example eu-west-2). OmniRoute calls Bedrock's native Converse API directly. |
|
||||
| `black-forest-labs` | `bfl` | Black Forest Labs | API key, image | [link](https://blackforestlabs.ai) | — |
|
||||
| `blackbox` | `bb` | Blackbox AI | API key | [link](https://blackbox.ai) | Limited free access is available through Blackbox; model availability and account limits apply |
|
||||
| `bluesminds` | `bm` | BluesMinds | API key | [link](https://www.bluesminds.com) | Free daily pi credits — supports 200+ models including GPT-4o, GPT-4.1, Claude Sonnet 4.5, Gemini 2.0 Flash, DeepSeek V4, Qwen, Kimi K2 |
|
||||
| `byteplus` | `bpm` | BytePlus ModelArk | API key | [link](https://console.byteplus.com/ark) | — |
|
||||
| `bytez` | `bytez` | Bytez | API key | [link](https://bytez.com) | $1 free credits, refreshes every 4 weeks |
|
||||
| `cerebras` | `cerebras` | Cerebras | API key | [link](https://inference.cerebras.ai) | Free Trial: 1M tokens/day, 30K TPM, 5 RPM — no credit card. |
|
||||
| `charm-hyper` | `charm-hyper` | Charm Hyper | API key | [link](https://hyper.charm.land) | 100 free monthly Hypercredits on signup |
|
||||
| `chat-oripe` | `chat-oripe` | Chat Oripe | API key, aggregator | [link](https://api.oriper.com) | Official metadata advertises 2M tokens/month, but the public site and documentation were blocked during audit; treat the quota and brand mapping as unconfirmed. |
|
||||
| `chatanywhere` | `chatanywhere` | ChatAnywhere | API key, aggregator | [link](https://chatanywhere.tech) | Personal, educational or research use only: public documentation cites 10,000 points/day and 200 requests/day per IP/key; do not use for commercial traffic. |
|
||||
| `cheaperinference` | `cinf` | Cheaper Inference | API key | [link](https://cheaperinference.com/?utm_source=omniroute) | — |
|
||||
| `chenzk` | `chenzk` | Chenzk API | API key | [link](https://chenzk.top) | — |
|
||||
| `chutes` | `chutes` | Chutes.ai | API key, aggregator | [link](https://chutes.ai) | Bearer API key for the Chutes OpenAI-compatible gateway. |
|
||||
| `clarifai` | `clarifai` | Clarifai | API key, enterprise | [link](https://docs.clarifai.com) | Use your Clarifai PAT or app-specific API key. OmniRoute targets the OpenAI-compatible endpoint at https://api.clarifai.com/v2/ext/openai/v1 and authenticates with Authorization: Key <token>. |
|
||||
| `cloudcode-one` | `cloudcode-one` | CloudCode.ONE | API key, aggregator | [link](https://cloudcode.one) | Published free models include glm-4.7-flash and glm-4.6v-flash; no numeric quota is published, and key creation may require credit or a coupon. |
|
||||
| `cloudflare-ai` | `cf` | Cloudflare Workers AI | API key | [link](https://developers.cloudflare.com/workers-ai) | Requires API Token AND Account ID (found at dash.cloudflare.com) |
|
||||
| `clova-studio` | `clova` | Naver CLOVA Studio | API key | [link](https://api.ncloud-docs.com/docs/en/ai-naver-clovastudio-summary) | — |
|
||||
| `codestral` | `codestral` | Codestral | API key | [link](https://mistral.ai) | — |
|
||||
| `cohere` | `cohere` | Cohere | API key | [link](https://cohere.com) | Free Trial: 1,000 API calls/month for testing, no credit card required |
|
||||
| `command-code` | `cmd` | Command Code | API key | [link](https://commandcode.ai/) | Use a Command Code API key. Requests are sent to Command Code's /alpha/generate endpoint. |
|
||||
| `coze` | `coze` | Coze | API key | [link](https://coze.com) | Get API key at coze.com/open/api |
|
||||
| `crof` | `crof` | CrofAI | API key | [link](https://crof.ai) | — |
|
||||
| `cursor-api` | `cua` | Cursor API | API key | [link](https://cursor.com/dashboard/api) | Paste a Cursor user API key (crsr_...) from cursor.com/dashboard/api. OmniRoute exchanges it for a session token on demand; no IDE or cursor-agent install is needed. Usage bills to the Cursor plan that owns the key. |
|
||||
| `dahl` | `dahl` | Dahl | API key | [link](https://inference.dahl.global) | Click 'Add Account' to auto-generate a token, or add a manual API key. |
|
||||
| `databricks` | `databricks` | Databricks | API key, enterprise | [link](https://www.databricks.com) | — |
|
||||
| `datarobot` | `datarobot` | DataRobot | API key, enterprise | [link](https://docs.datarobot.com) | Use your DataRobot API token. Optional Base URL can be the account root (for LLM Gateway) or a deployment URL under /api/v2/deployments/<id>. |
|
||||
| `deepai` | `deepai` | DeepAI | API key, image | [link](https://deepai.org) | Use your DeepAI API key. Get one at deepai.org — requires a Pro subscription ($9.99/mo). |
|
||||
| `deepinfra` | `deepinfra` | DeepInfra | API key | [link](https://deepinfra.com) | Free signup credits for API testing and model exploration |
|
||||
| `deepseek` | `ds` | DeepSeek | API key | [link](https://platform.deepseek.com) | 5M free tokens on signup - no credit card required |
|
||||
| `dgrid` | `dgrid` | DGrid | API key | [link](https://dgrid.ai) | DGrid Free Models Router: 10 requests/minute and 100 requests/day. A $5 lifetime top-up unlocks up to 20 requests/minute and 1,000 requests/day. |
|
||||
| `dify` | `dify` | Dify | API key | [link](https://dify.ai) | Get API key from your Dify instance. |
|
||||
| `digitalocean` | `digitalocean` | DigitalOcean | API key | [link](https://docs.digitalocean.com/products/ai-platform/) | — |
|
||||
| `dit` | `dai` | DIT.ai | API key | [link](https://dit.ai) | Use your dit.ai API key in Authorization: Bearer <key>. Fully OpenAI-compatible — a drop-in replacement, just change the base URL to https://api.dit.ai/v1. |
|
||||
| `doubao` | `doubao` | Doubao | API key | [link](https://doubao.com) | Get API key at console.volcengine.com |
|
||||
| `dxnt` | `dxnt` | DXNT / DX Token | API key, aggregator | [link](https://www.dxnt.com) | Free accounts are documented at 100 calls/day; the quota may increase through invitations and can vary by account. |
|
||||
| `electronhub` | `electronhub` | Electron Hub | API key, aggregator | [link](https://www.electronhub.ai) | Free plan: 5 RPM, $0.25 weekly credits and 10 Neutrinos/day for :free models; family budgets also apply. |
|
||||
| `empower` | `empower` | Empower | API key, aggregator | [link](https://docs.empower.dev) | Bearer API key for the Empower OpenAI-compatible endpoint. |
|
||||
| `factory` | `factory` | Factory | API key | [link](https://factory.ai) | Bearer API key for the Factory OpenAI-compatible gateway. |
|
||||
| `fal-ai` | `fal` | Fal.ai | API key, image | [link](https://fal.ai) | — |
|
||||
| `fastrouter` | `fastrouter` | FastRouter | API key, aggregator | [link](https://fastrouter.ai) | Models with the :free suffix allow 10 requests/day per organization and model; availability may change. |
|
||||
| `featherless-ai` | `featherless` | Featherless AI | API key | [link](https://featherless.ai) | Free tier available — no credit card required |
|
||||
| `fenayai` | `fenayai` | FenayAI | API key, aggregator | [link](https://fenayai.com) | Bearer API key for the FenayAI OpenAI-compatible gateway. |
|
||||
| `fireworks` | `fireworks` | Fireworks AI | API key | [link](https://fireworks.ai) | $1 free starter credits on signup for API testing |
|
||||
| `free-ai` | `free-ai` | Free.ai | API key, aggregator | [link](https://free.ai) | 30,000 tokens/day cover self-hosted models after email verification. Usage beyond the pool can bill at raw cost, and premium external models are paid. |
|
||||
| `freeaiapikey` | `faik` | FreeAIAPIKey | API key | [link](https://freeaiapikey.com) | — |
|
||||
| `freebuff` | `freebuff` | Freebuff | API key | [link](https://freebuff.com) | Enter Freebuff / Codebuff Auth Token (obtained via CLI login or automated harvester). |
|
||||
| `freeinference` | `freeinference` | FreeInference | API key, aggregator | [link](https://freeinference.org) | Free research access without a card; non-Harvard applicants require manual approval and no numeric quota is publicly guaranteed. |
|
||||
| `freemodel-dev` | `fmd` | FreeModel.dev | API key | [link](https://freemodel.dev) | $300 free credits on signup — no credit card required. Access GPT-5.4 and GPT-5.5 (OpenAI's latest flagship models) through an OpenAI-compatible API. |
|
||||
| `freetheai` | `fta` | FreeTheAi | API key, aggregator | [link](https://freetheai.xyz) | Join the FreeTheAi Discord to get your free API key. |
|
||||
| `friendliai` | `friendli` | FriendliAI | API key | [link](https://friendli.ai) | Free tier for serverless inference — no credit card required |
|
||||
| `g4f-gemini` | `g4fgem` | g4f.space — Gemini | API key, aggregator | [link](https://g4f.space) | No auth required. Free tier is limited to 5 requests/minute — sign up at g4f.dev/members.html for higher limits. |
|
||||
| `g4f-groq` | `g4fgroq` | g4f.space — Groq | API key, aggregator | [link](https://g4f.space) | No auth required. Free tier is limited to 5 requests/minute — sign up at g4f.dev/members.html for higher limits. |
|
||||
| `g4f-nvidia` | `g4fnv` | g4f.space — NVIDIA | API key, aggregator | [link](https://g4f.space) | No auth required. Free tier is limited to 5 requests/minute — sign up at g4f.dev/members.html for higher limits. |
|
||||
| `g4f-ollama` | `g4foll` | g4f.space — Ollama | API key, aggregator | [link](https://g4f.space) | No auth required. Free tier is limited to 5 requests/minute — sign up at g4f.dev/members.html for higher limits. |
|
||||
| `g4f-pollinations` | `g4fpol` | g4f.space — Pollinations | API key, aggregator | [link](https://g4f.space) | No auth required. Free tier is limited to 5 requests/minute — sign up at g4f.dev/members.html for higher limits. |
|
||||
| `galadriel` | `galadriel` | Galadriel | API key | [link](https://galadriel.com) | ⚠️ **DEPRECATED.** api.galadriel.ai no longer resolves (sweep 2026-06-19); the inference API appears discontinued. |
|
||||
| `gemini` | `gemini` | Gemini (Google AI Studio) | API key | [link](https://aistudio.google.com) | Free tier available through Google AI Studio; current per-model quotas and regional limits apply |
|
||||
| `getgoapi` | `ggo` | GoAPI | API key, aggregator | [link](https://api.getgoapi.com) | — |
|
||||
| `gigachat` | `gigachat` | GigaChat (Sber) | API key | [link](https://developers.sber.ru) | — |
|
||||
| `gitlab` | `gitlab` | GitLab Duo PAT | API key | [link](https://docs.gitlab.com/user/duo_agent_platform/code_suggestions/) | GitLab personal access token for the public Code Suggestions API. Configure a self-hosted base URL when not using gitlab.com. |
|
||||
| `gitlawb` | `glb` | Gitlawb Opengateway (MiMo) | API key | [link](https://opengateway.gitlawb.com) | Free MiMo (xiaomi/mimo-v2.5) revoked 2026-05 — Opengateway is now a pay-as-you-go credit gateway; no recurring free model. |
|
||||
| `gitlawb-gmi` | `glb-gmi` | Gitlawb Opengateway (GMI Cloud) | API key | [link](https://opengateway.gitlawb.com) | Free Nemotron promo ended 2026-06 — the GMI Cloud route is now pay-as-you-go credit only. |
|
||||
| `glm` | `glm` | GLM Coding | API key | [link](https://z.ai/subscribe) | — |
|
||||
| `glm-cn` | `glmcn` | GLM Coding (China) | API key | [link](https://open.bigmodel.cn) | — |
|
||||
| `glmt` | `glmt` | GLM Thinking | API key | [link](https://open.bigmodel.cn) | — |
|
||||
| `groq` | `groq` | Groq | API key | [link](https://groq.com) | Free tier: 30 RPM / 14.4K RPD — no credit card |
|
||||
| `hackclub` | `hc` | Hackclub AI | API key, aggregator | [link](https://ai.hackclub.com) | Sign in with your Hack Club account at ai.hackclub.com. |
|
||||
| `haiper` | `hp` | Haiper | API key, video | [link](https://haiper.ai) | Get API key at haiper.ai/haiper-api |
|
||||
| `hcnsec` | `hcnsec` | Huancheng Public API | API key | [link](https://api.hcnsec.cn) | Get API key at api.hcnsec.cn |
|
||||
| `helixmind` | `helixmind` | HelixMind | API key, aggregator | [link](https://helixmind.online) | Previously circulated 3 RPM/50 RPD and no-card claims were not confirmed during the 2026-08-02 audit; current quota and billing require account verification. |
|
||||
| `helyxai` | `helyxai` | Helyx AI | API key, aggregator | [link](https://helyxai.space) | Operational Free plan documents 100,000 tokens/day; the site's separate 2M+ marketing claim conflicts and is not treated as a quota guarantee. |
|
||||
| `heroku` | `heroku` | Heroku AI | API key, enterprise | [link](https://www.heroku.com) | — |
|
||||
| `huggingface` | `hf` | HuggingFace | API key | [link](https://huggingface.co) | Free Inference API for thousands of models (Whisper, VITS, SDXL…) |
|
||||
| `hyperbolic` | `hyp` | Hyperbolic | API key | [link](https://hyperbolic.xyz) | $1-5 trial credits on signup for serverless inference |
|
||||
| `ideogram` | `ideo` | Ideogram | API key | [link](https://ideogram.ai) | Get API key at ideogram.ai/docs/api |
|
||||
| `iflytek` | `iflytek` | iFlytek Spark | API key | [link](https://xinghuo.xfyun.cn) | Get API key at console.xfyun.cn |
|
||||
| `inception` | `inception` | Inception | API key | [link](https://docs.inceptionlabs.ai) | 10M free tokens on signup, no credit card required. |
|
||||
| `inference-net` | `inet` | Inference.net | API key | [link](https://inference.net) | $25 free credits on signup plus research grants available |
|
||||
| `internlm` | `internlm` | InternLM (Intern-S1) | API key | [link](https://internlm.intern-ai.org.cn/) | Free monthly quota ~1M input / 3M output tokens (~10 RPM) |
|
||||
| `jina-ai` | `jina` | Jina AI (Foundation API) | API key, embed/rerank | [link](https://jina.ai) | Bearer API key for api.jina.ai — embeddings, rerank, classify, segment, and search. Dashboard keys take precedence over JINA_AI_API_KEY. This is not the Reader / r.jina.ai card and does not fetch URLs. |
|
||||
| `jina-reader` | `jr` | Jina Reader (r.jina.ai) | API key | [link](https://jina.ai/reader) | Bearer API key for r.jina.ai URL-to-markdown (/v1/web/fetch only). Does not serve /v1/embeddings or /v1/rerank. The same Jina token as Foundation API works; OmniRoute reuses a jina-ai dashboard key or JINA_AI_API_KEY when this card is empty. |
|
||||
| `kenari` | `kenari` | Kenari | API key | [link](https://kenari.id) | Use your Kenari API key (kn-...) in Authorization: Bearer <key>. Fully OpenAI-compatible. API base URL: https://kenari.id/v1. |
|
||||
| `kie` | `kie` | KIE.AI | API key | [link](https://kie.ai) | — |
|
||||
| `kilo-gateway` | `kg` | Kilo Gateway | API key, aggregator | [link](https://kilo.ai) | — |
|
||||
| `kimi` | `kimi` | Kimi (Legacy Moonshot API) | API key | [link](https://platform.kimi.ai?aff=omniroute) | — |
|
||||
| `kimi-coding-apikey` | `kmca` | Kimi Code API Key | API key | [link](https://www.kimi.com/code?aff=omniroute) | — |
|
||||
| `lambda-ai` | `lambda` | Lambda AI | API key | [link](https://lambda.ai) | — |
|
||||
| `laozhang` | `lz` | LaoZhang AI | API key, aggregator | [link](https://api.laozhang.ai) | — |
|
||||
| `leonardo` | `leo` | Leonardo AI | API key, video | [link](https://leonardo.ai) | Get API key at leonardo.ai/developer |
|
||||
| `liquid` | `liquid` | Liquid AI | API key | [link](https://liquid.ai) | Get API key at liquid.ai |
|
||||
| `literouter` | `literouter` | LiteRouter | API key, aggregator | [link](https://literouter.com) | Free model variants use the :free suffix; daily credit limits vary by model and free input is capped at 5,000 tokens. |
|
||||
| `llamagate` | `llamagate` | LlamaGate | API key | [link](https://llamagate.ai) | — |
|
||||
| `llm-kiwi` | `llmkiwi` | LLM.Kiwi | API key, aggregator | [link](https://llm.kiwi) | Free plan exposes auto and hrLLM; the published 40 requests/hour limit applies to hrLLM. |
|
||||
| `llm7` | `llm7` | LLM7.io | API key | [link](https://llm7.io) | Use any non-empty key (for example 'unused'). If older built-in models return model_unavailable, use Available Models → Import from /models or Auto-Sync; verified live model: gemini-3.1-flash-lite. |
|
||||
| `llmgateway` | `llmgateway` | LLM Gateway | API key, aggregator | [link](https://llmgateway.io) | Hosted Free plan: free-priced models are limited to 5 requests per 10 minutes when the account has no credits. |
|
||||
| `logfare` | `logfare` | Logfare | API key, aggregator | [link](https://logfare.ai) | Create a free account at https://logfare.ai/register (username/password, no email verification) to get an instant API key, then paste it here as a Bearer token. |
|
||||
| `longcat` | `lc` | LongCat AI | API key | [link](https://longcat.chat/platform/docs) | Free: one-time 10M-token grant after account signup + KYC verification (LongCat-2.0). One-time only — not a recurring daily/monthly allowance. |
|
||||
| `magnific` | `freepik` | Magnific | API key, image | [link](https://www.magnific.com) | Get an API key at magnific.com/user/api-keys (header x-magnific-api-key). Legacy Freepik developer keys still work. |
|
||||
| `maritalk` | `maritalk` | Maritalk | API key | [link](https://www.maritaca.ai) | — |
|
||||
| `meganova-ai` | `meganova-ai` | MegaNova AI | API key, aggregator | [link](https://meganova.ai) | Free signup without a card. Published Tier 1 per-model quotas total 550 requests/day; they are not a shared global pool, and paid overage can apply if enabled. |
|
||||
| `meta-llama` | `meta` | Meta Llama API | API key | [link](https://llama.developer.meta.com) | — |
|
||||
| `minimax` | `minimax` | Minimax Coding | API key, video | [link](https://www.minimax.io) | — |
|
||||
| `minimax-cn` | `minimax-cn` | Minimax (China) | API key | [link](https://www.minimaxi.com) | — |
|
||||
| `mistral` | `mistral` | Mistral | API key | [link](https://mistral.ai) | Free Experiment tier: rate-limited access to all models, no credit card required |
|
||||
| `mixedbread` | `mxbai` | Mixedbread AI | API key | [link](https://www.mixedbread.com) | Bearer API key for the Mixedbread embeddings API. |
|
||||
| `mixlayer` | `mixlayer` | Mixlayer | API key, aggregator | [link](https://www.mixlayer.com) | The qwen/qwen3.5-4b-free model is free for prototyping and rate-limited; no fixed public RPM or daily quota is confirmed. |
|
||||
| `mnn-ai` | `mnn-ai` | MNN AI | API key, aggregator | [link](https://mnnai.ru) | Free plan: $1 monthly credits, 10 RPM and access only to models marked Free. |
|
||||
| `modal` | `mdl` | Modal | API key, enterprise | [link](https://modal.com/docs) | Use the bearer token that protects your Modal deployment, if enabled. Base URL should point to your OpenAI-compatible Modal app, for example https://<workspace>--<app>.modal.run/v1. |
|
||||
| `modelscope` | `ms` | ModelScope | API key | [link](https://modelscope.cn) | Free tier via ModelScope API-Inference — Alibaba account required. |
|
||||
| `monsterapi` | `monster` | MonsterAPI | API key | [link](https://monsterapi.ai) | ⚠️ **DEPRECATED.** Monster API shuttered operations on 2026-06-30. Use alternative OpenAI-compatible providers. |
|
||||
| `moonshot` | `moonshot` | Kimi | API key | [link](https://platform.kimi.ai?aff=omniroute) | — |
|
||||
| `morph` | `morph` | Morph | API key | [link](https://morphllm.com) | Free tier: 250K credits/month, $0 |
|
||||
| `muse-code` | `mc` | Muse Code (Meta) | API key | [link](https://github.com/meta-llama/llama-stack) | Use your META_API_KEY env var as a Bearer token. Muse Code CLI uses the OpenAI Responses API wire format (POST /responses). |
|
||||
| `naga-ac` | `naga` | Naga.ac | API key, aggregator | [link](https://naga.ac) | Get API key at naga.ac — Google/GitHub/Discord signup available. |
|
||||
| `naga-ai` | `naga-ai` | Naga AI | API key, aggregator | [link](https://naga.ac) | Models marked :free are publicly listed, but no numeric quota is confirmed. Naga's policy warns that free-tier prompts and outputs may be collected or used for training. |
|
||||
| `nanogpt` | `nanogpt` | NanoGPT | API key | [link](https://nano-gpt.com) | — |
|
||||
| `nara` | `nara` | NaraRouter | API key | [link](https://bynara.id) | Get a free API key via NaraRouter's Telegram channel, then paste it here as a Bearer token. |
|
||||
| `navy` | `navy` | NavyAI | API key | [link](https://api.navy) | Create a free API key from the NavyAI dashboard, then paste it here as a Bearer token. |
|
||||
| `nebius` | `nebius` | Nebius AI | API key | [link](https://nebius.com) | ~$1 trial credits on signup for API testing |
|
||||
| `nlpcloud` | `nlpc` | NLP Cloud | API key | [link](https://docs.nlpcloud.com) | Use your NLP Cloud API key in Authorization: Token <key>. OmniRoute targets the chatbot endpoint on https://api.nlpcloud.io/v1/gpu/<model>/chatbot by default. |
|
||||
| `nomic` | `nomic` | Nomic | API key | [link](https://nomic.ai) | Get API key at atlas.nomic.ai |
|
||||
| `nous-research` | `nous` | Nous Research | API key | [link](https://portal.nousresearch.com/help) | Use your Nous Portal API key. OmniRoute targets the official OpenAI-compatible inference endpoint at https://inference-api.nousresearch.com/v1. |
|
||||
| `novita` | `novita` | Novita AI | API key, video, aggregator | [link](https://novita.ai) | $0.50 trial credits on signup (valid about 1 year) |
|
||||
| `nscale` | `nscale` | nScale | API key | [link](https://nscale.com) | $5 free credits on signup for inference testing |
|
||||
| `nube` | `nube` | Nube.sh | API key | [link](https://nube.sh) | — |
|
||||
| `nvidia` | `nvidia` | NVIDIA NIM | API key | [link](https://build.nvidia.com) | Free dev access: ~40 RPM, 70+ models (Kimi K2.5, GLM 4.7, DeepSeek V3.2...) |
|
||||
| `oci` | `oci` | OCI Generative AI | API key, enterprise | [link](https://www.oracle.com/artificial-intelligence/generative-ai) | Use your OCI Generative AI API key or IAM bearer token. Base URL can be https://inference.generativeai.<region>.oci.oraclecloud.com/openai/v1/. |
|
||||
| `ofoxai` | `ofoxai` | OfoxAI | API key, aggregator | [link](https://ofox.ai) | The current catalog advertises 10+ free models without a public numeric quota; review upstream provenance, retention and training terms before production use. |
|
||||
| `ollama-cloud` | `ollamacloud` | Ollama Cloud | API key | [link](https://ollama.com/settings/keys) | — |
|
||||
| `openadapter` | `oad` | OpenAdapter | API key | [link](https://openadapter.dev) | Use your OpenAdapter API key in Authorization: Bearer sk-cv-<key>. Fully OpenAI-compatible. API base URL: https://api.openadapter.in/v1. |
|
||||
| `openai` | `openai` | OpenAI | API key | [link](https://platform.openai.com) | — |
|
||||
| `opencode-go` | `opencode-go` | OpenCode Go | API key | [link](https://opencode.ai/go) | — |
|
||||
| `opencode-zen` | `opencode-zen` | OpenCode Zen | API key | [link](https://opencode.ai/zen) | — |
|
||||
| `openference-api` | `ofa` | Openference API | API key | [link](https://openference.com) | Free plan: 3-day trial with open-source models — no credit card required |
|
||||
| `openrouter` | `openrouter` | OpenRouter | API key, aggregator | [link](https://openrouter.ai) | Free models at $0/token with :free suffix - 20 RPM / 200 RPD |
|
||||
| `openvecta` | `openvecta` | OpenVecta | API key | [link](https://openvecta.com) | Free credits on signup for OpenAI-compatible inference across LLMs, embeddings, and reasoning models |
|
||||
| `orcarouter` | `orcarouter` | OrcaRouter | API key | [link](https://www.orcarouter.ai) | — |
|
||||
| `ovhcloud` | `ovh` | OVHcloud AI | API key | [link](https://www.ovhcloud.com) | — |
|
||||
| `perplexity` | `pplx` | Perplexity | API key | [link](https://www.perplexity.ai) | — |
|
||||
| `piapi` | `pi` | PiAPI | API key, aggregator | [link](https://piapi.ai) | — |
|
||||
| `pioneer` | `pn` | Pioneer AI | API key | [link](https://pioneer.ai) | $75 free usage credits — no credit card required |
|
||||
| `plamo` | `plamo` | PLaMo | API key | [link](https://plamo.preferredai.jp/api) | — |
|
||||
| `poe` | `poe` | Poe | API key, aggregator | [link](https://creator.poe.com/api-reference) | Bearer API key for the Poe OpenAI-compatible API. |
|
||||
| `poixe-ai` | `poixe-ai` | Poixe AI | API key, aggregator | [link](https://poixe.com) | Current public free limits are small and model-group specific: 2 RPM/5 RPD for large-cup models and 20 RPM/50 RPD for small-cup models. |
|
||||
| `pollinations` | `pol` | Pollinations AI | API key, video | [link](https://pollinations.ai) | Anonymous/keyless access to the documented free models is best-effort. Local v3.8.50 verification (2026-07-31) returned 401 via OmniRoute and Cloudflare 1010 on direct upstream probes from the same network. Premium models still require a Pollinations API key from enter.pollinations.ai. |
|
||||
| `poolside` | `poolside` | Poolside | API key | [link](https://poolside.ai) | Laguna S 2.1 and XS 2.1 are free during Preview; no public numeric quota is published. |
|
||||
| `predibase` | `predibase` | Predibase | API key | [link](https://predibase.com) | ⚠️ **DEPRECATED.** serving.app.predibase.com no longer resolves (sweep 2026-06-19); the managed serving API appears discontinued. |
|
||||
| `publicai` | `publicai` | PublicAI | API key | [link](https://publicai.co) | Requires an API key — one-time signup credit, then paid |
|
||||
| `qianfan` | `qianfan` | Baidu Qianfan | API key | [link](https://cloud.baidu.com/product-s/qianfan_home) | — |
|
||||
| `qiniu` | `qiniu` | Qiniu | API key | [link](https://www.qiniu.com) | — |
|
||||
| `qwen-cloud` | `qwc` | Qwen Cloud | API key | [link](https://www.qwencloud.com/) | — |
|
||||
| `qwen-cloud-token-plan` | `qct` | Qwen Cloud Token Plan | API key | [link](https://www.qwencloud.com/pricing/token-plan) | — |
|
||||
| `recraft` | `recraft` | Recraft | API key, image | [link](https://recraft.ai) | — |
|
||||
| `regolo` | `regolo` | Regolo AI | API key | [link](https://regolo.ai) | Get your Regolo API key from regolo.ai, then paste it here as a Bearer token. |
|
||||
| `reka` | `reka` | Reka | API key | [link](https://docs.reka.ai/chat/overview) | Use your Reka API key. OmniRoute supports the OpenAI-compatible base URL https://api.reka.ai/v1 and sends both Authorization and X-Api-Key headers for compatibility. |
|
||||
| `requesty` | `requesty` | Requesty | API key | [link](https://requesty.ai) | Free tier ~200 requests/day - multi-model routing gateway (300+ models) |
|
||||
| `routeway` | `routeway` | Routeway | API key | [link](https://routeway.ai) | Create a free API key at routeway.ai, then paste it here as a Bearer token. |
|
||||
| `runwayml` | `runway` | Runway | API key, video | [link](https://docs.dev.runwayml.com) | Use your Runway API key in Authorization: Bearer <key>. OmniRoute targets the current Runway API at https://api.dev.runwayml.com/v1 and sends the required X-Runway-Version header automatically. |
|
||||
| `sambanova` | `samba` | SambaNova | API key | [link](https://sambanova.ai) | $5 free credits on signup (30-day validity), no credit card required |
|
||||
| `sap` | `sap` | SAP Generative AI Hub | API key, enterprise | [link](https://help.sap.com/docs/sap-ai-core/sap-ai-core-service-guide/generative-ai-hub-in-sap-ai-core) | Use your SAP AI Core bearer token. Base URL can be your AI_API_URL root or a deploymentUrl from Generative AI Hub. |
|
||||
| `sarvam` | `sarvam` | Sarvam AI | API key | [link](https://docs.sarvam.ai) | ₹1,000 in free signup credits — never expire |
|
||||
| `scaleway` | `scw` | Scaleway AI | API key | [link](https://www.scaleway.com/en/docs/ai-data/generative-apis/) | 1M free tokens for new accounts — EU/GDPR compliant (Paris), Qwen3 235B & Llama 70B |
|
||||
| `sealion` | `sealion` | SEA-LION | API key | [link](https://sea-lion.ai) | Sign in at sea-lion.ai with Google (no card, no region wall), create an API key, then paste it here. |
|
||||
| `segmind` | `segmind` | Segmind | API key, image, video | [link](https://segmind.com) | Use your Segmind API key in the x-api-key header. OmniRoute targets https://api.segmind.com/v1/<model> and returns the generated image/video bytes directly. |
|
||||
| `sensenova` | `sensenova` | SenseNova | API key | [link](https://platform.sensenova.cn) | Get API key at platform.sensenova.cn |
|
||||
| `siliconflow` | `siliconflow` | SiliconFlow | API key | [link](https://cloud.siliconflow.com) | $1 free credits plus currently listed $0 models after identity verification; availability and limits may change |
|
||||
| `snowflake` | `snowflake` | Snowflake Cortex | API key, enterprise | [link](https://www.snowflake.com) | — |
|
||||
| `sparkdesk` | `sparkdesk` | SparkDesk | API key | [link](https://xinghuo.xfyun.cn) | Get API key at console.xfyun.cn |
|
||||
| `speka` | `speka` | Speka AI | API key, aggregator | [link](https://speka.me) | Free plan: $1 monthly usage, 10 RPM, one API key and access to open models and the playground; no card required. |
|
||||
| `stability-ai` | `stability` | Stability AI | API key, image | [link](https://stability.ai) | — |
|
||||
| `stepfun` | `stepfun` | StepFun | API key | [link](https://stepfun.com) | Get API key at platform.stepfun.com |
|
||||
| `sumopod` | `sumopod` | SumoPod | API key | [link](https://ai.sumopod.com) | Use your SumoPod API key (sk-...) in Authorization: Bearer <key>. Fully OpenAI-compatible. API base URL: https://ai.sumopod.com/v1. |
|
||||
| `suno` | `suno` | Suno | API key | [link](https://suno.ai) | Paste session cookie from suno.ai (Clerk auth) |
|
||||
| `synthetic` | `synthetic` | Synthetic | API key, aggregator | [link](https://synthetic.new) | — |
|
||||
| `tabitoken` | `tabitoken` | TabiToken | API key, aggregator | [link](https://tabitoken.com) | — |
|
||||
| `tencent` | `tencent` | Tencent Hunyuan | API key | [link](https://hunyuan.tencent.com) | Get API key at console.cloud.tencent.com |
|
||||
| `thebai` | `thebai` | TheB.AI | API key, aggregator | [link](https://theb.ai) | Bearer API key for the TheB.AI OpenAI-compatible gateway. |
|
||||
| `tinyfish` | `tf` | TinyFish Fetch | API key | [link](https://docs.tinyfish.ai/fetch-api) | X-API-Key from agent.tinyfish.ai/api-keys |
|
||||
| `together` | `together` | Together AI | API key, video | [link](https://www.together.ai) | — |
|
||||
| `token-kiosk` | `tk` | Token Kiosk | API key | [link](https://agent-router.gaib.ai) | Use your Token Kiosk API key in Authorization: Bearer <key>. Fully OpenAI-compatible gateway. API base URL: https://agent-router.gaib.ai/v1. |
|
||||
| `tokenreply` | `tokenreply` | TokenReply | API key, aggregator | [link](https://www.tokenreply.com) | Free-tagged models have model- and campaign-specific daily limits; no fixed global free quota is published. |
|
||||
| `tokenrouter` | `trk` | TokenRouter | API key | [link](https://tokenrouter.com) | Use your TokenRouter API key in Authorization: Bearer <key>. Fully OpenAI-compatible. API base URL: https://api.tokenrouter.com/v1. |
|
||||
| `topaz` | `topaz` | Topaz | API key, image | [link](https://topazlabs.com) | — |
|
||||
| `typhoon` | `typhoon` | Typhoon | API key | [link](https://docs.opentyphoon.ai) | Free API key with a 5 req/s and 200 req/m rate limit. |
|
||||
| `udio` | `udio` | Udio | API key | [link](https://udio.com) | Paste session cookie from udio.com (Supabase auth) |
|
||||
| `uncloseai` | `unc` | UncloseAI | API key | [link](https://uncloseai.com) | No auth required. API accepts any non-empty string as key for identification. If older built-in models return 404, use Available Models → Import from /models or Auto-Sync; verified live model: solidrust/Hermes-3-Llama-3.1-8B-AWQ. |
|
||||
| `unorouter` | `unorouter` | UnoRouter | API key, aggregator | [link](https://unorouter.ai) | Models with the :free suffix do not debit balance; limit is 1 request/minute per free model per user. |
|
||||
| `upstage` | `upstage` | Upstage | API key | [link](https://www.upstage.ai) | — |
|
||||
| `v0-vercel` | `v0` | v0 (Vercel) | API key | [link](https://v0.dev) | — |
|
||||
| `venice` | `venice` | Venice.ai | API key | [link](https://venice.ai) | — |
|
||||
| `vercel-ai-gateway` | `vag` | Vercel AI Gateway | API key, aggregator | [link](https://vercel.com/docs/ai-gateway) | — |
|
||||
| `vertex` | `vertex` | Vertex AI | API key, enterprise | [link](https://cloud.google.com/vertex-ai) | Provide Service Account JSON or OAuth access_token |
|
||||
| `vertex-partner` | `vp` | Vertex AI Partners | API key, enterprise | [link](https://cloud.google.com/vertex-ai) | Provide the same Service Account JSON used for Vertex AI partner models. |
|
||||
| `void-ai` | `void-ai` | Void AI | API key, aggregator | [link](https://voidai.app) | The public model catalog marks some models with a free plan requirement, but access is conditional and no numeric quota is confirmed. |
|
||||
| `volcengine` | `volcengine` | Volcengine | API key | [link](https://www.volcengine.com) | — |
|
||||
| `voyage-ai` | `voyage` | Voyage AI | API key, embed/rerank | [link](https://www.voyageai.com) | Bearer API key for Voyage AI embeddings and rerank APIs. |
|
||||
| `wafer` | `wafer` | Wafer AI | API key | [link](https://wafer.ai) | — |
|
||||
| `wandb` | `wandb` | Weights & Biases Inference | API key | [link](https://wandb.ai) | — |
|
||||
| `watsonx` | `watsonx` | IBM watsonx.ai Gateway | API key, enterprise | [link](https://www.ibm.com/products/watsonx-ai) | Use your watsonx bearer token. Base URL can be https://<region>.ml.cloud.ibm.com/ml/gateway/v1/ or a self-managed /ml/gateway/v1 endpoint. |
|
||||
| `writer` | `writer` | Writer | API key | [link](https://dev.writer.com) | — |
|
||||
| `x5lab` | `x5lab` | X5Lab | API key | [link](https://x5lab.dev) | Use your X5Lab API key (x5-...) in Authorization: Bearer <key>. Fully OpenAI-compatible. API base URL: https://api.x5lab.dev/v1. |
|
||||
| `xai` | `xai` | xAI (Grok) | API key | [link](https://x.ai) | Use an official xAI API key, or sign in with xAI OAuth. Grok Build JWT sessions remain a separate provider. |
|
||||
| `xiaomi-mimo` | `mimo` | Xiaomi MiMo | API key | [link](https://mimo.mi.com) | — |
|
||||
| `xiaomi-mimo-token-plan` | `mimotp` | Xiaomi MiMo Token Plan | API key | [link](https://mimo.mi.com) | — |
|
||||
| `yi` | `yi` | Yi (01.AI) | API key | [link](https://01.ai) | Get API key at platform.lingyiwanwu.com |
|
||||
| `yolo-auto` | `yolo-auto` | Yolo-Auto | API key, aggregator | [link](https://yolo-auto.com) | Free API access is request-limited and intended for testing; no numeric daily quota is published and free access is not promised indefinitely. |
|
||||
| `zai` | `zai` | Z.AI | API key | [link](https://open.bigmodel.cn) | — |
|
||||
| `zenmux` | `zm` | ZenMux | API key | [link](https://zenmux.ai) | Use your ZenMux API key in Authorization: Bearer <key>. ZenMux is fully OpenAI-compatible. Base URL: https://zenmux.ai/api/v1. |
|
||||
| `zerolimitai` | `zerolimitai` | ZeroLimitAI | API key, aggregator | [link](https://www.zerolimitai.com) | Temporary free trial is advertised, but official pages conflict between 3 and 7 days; a 100-calls/day claim is not treated as permanent. |
|
||||
| `zylo-api` | `zylo` | Zylo API | API key, aggregator | [link](https://zyloai.net) | Basic plan: 10 RPM, 7,200 requests/day and 200,000 tokens/day; limited to Basic text models. |
|
||||
|
||||
## Local Providers (14)
|
||||
|
||||
| ID | Alias | Name | Tags | Website | Notes |
|
||||
|----|-------|------|------|---------|-------|
|
||||
| `comfyui` | `comfyui` | ComfyUI | Local | [link](https://github.com/comfyanonymous/ComfyUI) | No API key required. Configure the local ComfyUI base URL (default: http://localhost:8188). |
|
||||
| `docker-model-runner` | `dmr` | Docker Model Runner | Local, self-hosted | [link](https://docs.docker.com/ai/model-runner/) | API key optional. Configure the local Docker Model Runner OpenAI-compatible base URL (default: http://localhost:12434/v1). |
|
||||
| `lemonade` | `lemonade` | Lemonade Server | Local, self-hosted | [link](https://lemonade-server.ai) | API key optional. Configure the local Lemonade OpenAI-compatible base URL (default: http://localhost:13305/api/v1). |
|
||||
| `llama-cpp` | `llamacpp` | llama.cpp | Local, self-hosted | [link](https://github.com/ggml-org/llama.cpp) | API key optional (use any value, e.g. sk-no-key-required). Configure the llama-server OpenAI-compatible base URL (default: http://127.0.0.1:8080/v1). Note: if Llamafile is also installed, both default to port 8080 — run only one at a time or override the port. |
|
||||
| `llamafile` | `llamafile` | Llamafile | Local, self-hosted | [link](https://github.com/Mozilla-Ocho/llamafile) | API key optional. Configure the local Llamafile OpenAI-compatible base URL (default: http://127.0.0.1:8080/v1). |
|
||||
| `lm-studio` | `lmstudio` | LM Studio | Local, self-hosted | [link](https://lmstudio.ai) | API key optional. Configure the local LM Studio OpenAI-compatible base URL (default: http://localhost:1234/v1). |
|
||||
| `mlx-gemma` | `mlx-gemma` | MLX Gemma 26B | Local, self-hosted | [link](https://github.com/ml-explore/mlx) | No API key required. Runs mlx-lm server locally on port 11435. Requires uv and mlx-lm installed. Model: mlx-community/gemma-4-26B-A4B-it-qat-q4_0-mlx-aligned (~15.9GB peak memory). |
|
||||
| `mlx-qwen` | `mlx-qwen` | MLX Qwen 3.8 27B | Local, self-hosted | [link](https://github.com/ml-explore/mlx) | No API key required. Runs mlx-lm server locally on port 11436. Requires uv and mlx-lm installed. Model: maglun/Qwen3.8-27B-MLX-Mixed-3.80bpw (~13.1GB peak memory). |
|
||||
| `ollama-local` | `ollama` | Ollama | Local, self-hosted | [link](https://ollama.com) | No API key required. Ollama runs locally — configure its OpenAI-compatible base URL (default: http://localhost:11434/v1) and make sure Ollama is running before connecting. |
|
||||
| `oobabooga` | `ooba` | oobabooga | Local, self-hosted | [link](https://github.com/oobabooga/text-generation-webui) | API key optional. Configure the local oobabooga OpenAI-compatible base URL (default: http://localhost:5000/v1). |
|
||||
| `sdwebui` | `sdwebui` | SD WebUI | Local | [link](https://github.com/AUTOMATIC1111/stable-diffusion-webui) | No API key required. Configure the local WebUI base URL (default: http://localhost:7860). |
|
||||
| `triton` | `triton` | NVIDIA Triton | Local, self-hosted | [link](https://developer.nvidia.com/triton-inference-server) | API key optional. Configure the Triton OpenAI-compatible base URL (default: http://localhost:8000/v1). |
|
||||
| `vllm` | `vllm` | vLLM | Local, self-hosted | [link](https://github.com/vllm-project/vllm) | API key optional. Configure the local vLLM OpenAI-compatible base URL (default: http://localhost:8000/v1). |
|
||||
| `xinference` | `xinference` | XInference | Local, self-hosted | [link](https://inference.readthedocs.io) | API key optional. Configure the local XInference OpenAI-compatible base URL (default: http://localhost:9997/v1). |
|
||||
|
||||
## Search Providers (13)
|
||||
|
||||
| ID | Alias | Name | Tags | Website | Notes |
|
||||
|----|-------|------|------|---------|-------|
|
||||
| `brave-search` | `brave-search` | Brave Search | Search | [link](https://brave.com/search/api) | Subscription token from Brave Search API dashboard |
|
||||
| `exa-search` | `exa-search` | Exa Search | Search | [link](https://exa.ai) | API key from dashboard.exa.ai |
|
||||
| `firecrawl` | `fc` | Firecrawl | Search | [link](https://firecrawl.dev) | API key from firecrawl.dev/app/api-keys (or set your self-hosted Firecrawl base URL) |
|
||||
| `google-pse-search` | `google-pse` | Google Programmable Search | Search | [link](https://developers.google.com/custom-search/v1/overview) | Requires a Google API key and your Programmable Search Engine ID (cx) |
|
||||
| `linkup-search` | `linkup` | Linkup Search | Search | [link](https://docs.linkup.so) | Bearer API key from the Linkup dashboard |
|
||||
| `ollama-search` | `ollama-search` | Ollama Search | Search | [link](https://ollama.com/settings/keys) | Same API key as Ollama Cloud (from ollama.com/settings/keys) |
|
||||
| `perplexity-search` | `pplx-search` | Perplexity Search | Search | [link](https://docs.perplexity.ai/guides/search-quickstart) | Same API key as Perplexity (pplx-...) |
|
||||
| `searchapi-search` | `searchapi` | SearchAPI | Search | [link](https://www.searchapi.io/docs/google) | API key from SearchAPI (query param or Bearer auth) |
|
||||
| `searxng-search` | `searxng` | SearXNG Search | Search | [link](https://docs.searxng.org) | API key is optional. Set your SearXNG base URL. Some instances may require a bearer token for access. |
|
||||
| `serper-search` | `serper-search` | Serper Search | Search | [link](https://serper.dev) | API key from serper.dev dashboard |
|
||||
| `tavily-search` | `tavily-search` | Tavily Search | Search | [link](https://tavily.com) | API key from app.tavily.com (format: tvly-...) |
|
||||
| `x-search` | `x_search` | X Search (Grok) | Search | [link](https://docs.x.ai/developers/tools/x-search) | SuperGrok OAuth (xai-oauth) or xAI API key. This is Grok X Search, not the X Developer MCP. |
|
||||
| `youcom-search` | `youcom-search` | You.com Search | Search | [link](https://you.com/business/api/) | X-API-Key from the You.com platform dashboard |
|
||||
|
||||
## Audio-only Providers (12)
|
||||
|
||||
| ID | Alias | Name | Tags | Website | Notes |
|
||||
|----|-------|------|------|---------|-------|
|
||||
| `assemblyai` | `aai` | AssemblyAI | Audio | [link](https://assemblyai.com) | — |
|
||||
| `aws-polly` | `polly` | AWS Polly | Audio | [link](https://aws.amazon.com/polly/) | Use AWS Secret Access Key as API key; set providerSpecificData.accessKeyId and optional region. |
|
||||
| `cartesia` | `cartesia` | Cartesia | Audio | [link](https://cartesia.ai) | — |
|
||||
| `deepgram` | `dg` | Deepgram | Audio | [link](https://deepgram.com) | — |
|
||||
| `elevenlabs` | `el` | ElevenLabs | Audio | [link](https://elevenlabs.io) | — |
|
||||
| `fishaudio` | `fishaudio` | Fish Audio | Audio | [link](https://fish.audio) | — |
|
||||
| `gladia` | `gladia` | Gladia | Audio | [link](https://gladia.io) | — |
|
||||
| `inworld` | `inworld` | Inworld | Audio | [link](https://inworld.ai) | — |
|
||||
| `playht` | `playht` | PlayHT | Audio | [link](https://play.ht) | — |
|
||||
| `rev-ai` | `revai` | Rev AI | Audio | [link](https://www.rev.ai) | — |
|
||||
| `soniox` | `sx` | Soniox | Audio | [link](https://soniox.com) | — |
|
||||
| `speechmatics` | `sm` | Speechmatics | Audio | [link](https://www.speechmatics.com) | Free tier — 8 hours/month, no credit card required. Batch (async) mode only. |
|
||||
|
||||
## Upstream Proxy Providers (2)
|
||||
|
||||
| ID | Alias | Name | Tags | Website | Notes |
|
||||
|----|-------|------|------|---------|-------|
|
||||
| `9router` | `nr` | 9router | Upstream proxy | [link](https://www.npmjs.com/package/9router) | — |
|
||||
| `cliproxyapi` | `cpa` | CLIProxyAPI | Upstream proxy | [link](https://github.com/router-for-me/CLIProxyAPI) | — |
|
||||
|
||||
## Cloud Agent Providers (3)
|
||||
|
||||
| ID | Alias | Name | Tags | Website | Notes |
|
||||
|----|-------|------|------|---------|-------|
|
||||
| `codex-cloud` | `codex-cloud` | Codex Cloud | Cloud agent | [link](https://openai.com/codex) | OpenAI API key with Codex Cloud task access. |
|
||||
| `devin` | `devin` | Devin | Cloud agent | [link](https://devin.ai) | Devin API key for cloud agent sessions. |
|
||||
| `jules` | `jules` | Google Jules | Cloud agent | [link](https://jules.google) | Jules API key for creating and managing cloud coding tasks. |
|
||||
|
||||
## System Providers (1)
|
||||
|
||||
| ID | Alias | Name | Tags | Website | Notes |
|
||||
|----|-------|------|------|---------|-------|
|
||||
| `auto` | `auto` | Auto (Zero-Config) | System | — | — |
|
||||
|
||||
## Sources of truth
|
||||
|
||||
- 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/) (106 implementations)
|
||||
- Translators: [`open-sse/translator/`](../../open-sse/translator/)
|
||||
|
||||
## See Also
|
||||
|
||||
- [FREE_TIERS.md](./FREE_TIERS.md) — curated free-tier guide
|
||||
- [USER_GUIDE.md](../guides/USER_GUIDE.md) — provider setup walkthrough
|
||||
- [ARCHITECTURE.md](../architecture/ARCHITECTURE.md) — overall architecture
|
||||
@@ -557,7 +557,7 @@ the current catalog at **[radar.omniroute.online/planos](https://radar.omniroute
|
||||
- **🧠 Memory you control** — off by default, opt-in int8 vector quantization + typed decay, per-request `x-omniroute-no-memory`. → [Memory](docs/frameworks/MEMORY.md)
|
||||
- **🛡️ Security** — prompt-injection guard on every LLM route (red-team suite), opt-in credential-masking guardrail (redacts leaked API keys/secrets in both directions), free DuckDuckGo last-resort web search, and an optional OIDC login gate for the dashboard (password login always stays available). → [Guardrails](docs/security/GUARDRAILS.md)
|
||||
- **🖼️ New endpoints** — `/v1/ocr` (Mistral OCR) and `/v1/audio/translations` (Whisper-style) round out the media surface. → [API Reference](docs/reference/API_REFERENCE.md)
|
||||
- **🎨 Image / video / audio generation** — one API for media: xAI Grok Imagine & Novita AI video, ComfyUI, Freepik, Adobe Firefly, Microsoft Designer, Segmind, and speech providers such as ElevenLabs. → [API Reference](docs/reference/API_REFERENCE.md)
|
||||
- **🎨 Image / video / audio generation** — one API for media: xAI Grok Imagine & Novita AI video, ComfyUI, Freepik, Adobe Firefly, Segmind, and speech providers such as ElevenLabs. → [API Reference](docs/reference/API_REFERENCE.md)
|
||||
- **🌍 Deployment & ops** — reverse-proxy `basePath`, browser-language auto-detect, per-key device tracking, root-less MITM trust, zh-TW localization. → [Environment](docs/reference/ENVIRONMENT.md)
|
||||
- **🤝 More providers & agents** — Cursor Cloud Agent, Grok Build (xAI) with browser + OAuth login, Ollama first-class card, Claude Opus 5 & Sonnet 5, Kimi official partnership (Code/Web/Moonshot), Zed, Requesty, SenseNova, Yuanbao, Agnes AI… and a refreshed **357-provider catalog**. → [Providers](docs/reference/PROVIDER_REFERENCE.md)
|
||||
- **📡 Routing transparency** — every response carries an `X-OmniRoute-Decision` header naming the strategy/provider/latency that served it, a new `cache-optimized` combo strategy + Auto-Combo `cacheAffinity` factor route repeat requests back to the connection holding the cached prefix, and a read-only `/v1/auto-combo/{channel}/candidates` endpoint exposes an `auto/*` channel's live candidate pool. → [Auto-Combo](docs/routing/AUTO-COMBO.md)
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
- **fix(providers):** retire `microsoft-designer-web` and its `msdesigner` alias from runtime and catalogs while preserving stored credentials, provider history and allowlists as inactive records — source dependency on gpt4free's [`MicrosoftDesigner.py`](https://github.com/xtekky/gpt4free/blob/0943d16eef757a44deb60651c07bbe5fa958cafe/g4f/Provider/needs_auth/MicrosoftDesigner.py) is proven at snapshot `0943d16eef757a44deb60651c07bbe5fa958cafe`, whose [`LICENSE`](https://github.com/xtekky/gpt4free/blob/0943d16eef757a44deb60651c07bbe5fa958cafe/LICENSE) is GPL-3.0; expressive adaptation is probable, which records provenance/licensing risk without asserting a legal conclusion ([#11720](https://github.com/diegosouzapw/OmniRoute/pull/11720)).
|
||||
@@ -92,6 +92,14 @@
|
||||
"tests/unit/edgetts-provider.test.ts": {
|
||||
"replacement": "tests/unit/edgetts-retirement.test.ts",
|
||||
"reason": "v3.8.50 #11711: the EdgeTTS executor and its positive protocol tests were retired together after a provenance/licensing HOLD. The replacement pins the new public contract across the speech registry, handler and UI-derived catalog while preserving gTTS, AWS Polly and ElevenLabs. Feature retirement, not test masking; prune after v3.8.50 merges to main."
|
||||
},
|
||||
"tests/unit/microsoft-designer-web-6672.test.ts": {
|
||||
"replacement": "tests/unit/microsoft-designer-web-retirement.test.ts",
|
||||
"reason": "v3.8.51 #11720: the Microsoft Designer Web runtime, handler, executor, catalogs and public credential were retired. The replacement pins the fail-closed runtime contract, exact-ID matching, source removal and preservation of unrelated image providers. Feature removal, not masking."
|
||||
},
|
||||
"tests/unit/designer-web-empty-response-502.test.ts": {
|
||||
"replacement": "tests/unit/microsoft-designer-web-retirement.test.ts",
|
||||
"reason": "v3.8.51 #11720: the deleted test exercised polling behavior in the removed Designer handler. The replacement verifies that the handler stays absent and both retired IDs fail closed while OpenAI DALL-E and other media providers remain available. Feature removal, not masking."
|
||||
}
|
||||
},
|
||||
"tests/unit/catalog-updates-v3x.test.ts": "v3.8.45 #6248: fix(providers) remove deprecated MiMo V2 entries — os 5 asserts removidos pinavam specs de modelos mimo-v2-* que deixaram de existir no catálogo (54→49). Asserts seguem a remoção dos modelos, não enfraquecimento. Verificado legítimo. Prune após v3.8.45 mergear para main.",
|
||||
|
||||
@@ -486,7 +486,7 @@ Radar è opt-in e usa soltanto richieste GET. Il client OmniRoute non carica pro
|
||||
- **🧠 Memoria sotto il tuo controllo** — disattivata per impostazione predefinita, quantizzazione vettoriale int8 opt-in + decadimento tipizzato, `x-omniroute-no-memory` per-request. → [Memoria](../../frameworks/MEMORY.md)
|
||||
- **🛡️ Sicurezza** — guard contro la prompt injection su ogni route LLM (suite red-team), guardrail opzionale per il masking delle credenziali (oscura API key/secret trapelati in entrambe le direzioni), web search DuckDuckGo gratuita come ultima risorsa e gate di login OIDC opzionale per la dashboard (il login con password resta sempre disponibile). → [Guardrail](../../security/GUARDRAILS.md)
|
||||
- **🖼️ Nuovi endpoint** — `/v1/ocr` (Mistral OCR) e `/v1/audio/translations` (stile Whisper) completano la superficie media. → [Riferimento API](../../reference/API_REFERENCE.md)
|
||||
- **🎨 Generazione immagini / video / audio** — una sola API per i media: xAI Grok Imagine e Novita AI video, ComfyUI, Freepik, Adobe Firefly, Microsoft Designer, Segmind e provider vocali come ElevenLabs. → [Riferimento API](../../reference/API_REFERENCE.md)
|
||||
- **🎨 Generazione immagini / video / audio** — una sola API per i media: xAI Grok Imagine e Novita AI video, ComfyUI, Freepik, Adobe Firefly, Segmind e provider vocali come ElevenLabs. → [Riferimento API](../../reference/API_REFERENCE.md)
|
||||
- **🌍 Deployment e operazioni** — `basePath` del reverse proxy, rilevamento automatico della lingua del browser, tracking dei dispositivi per chiave, trust MITM senza root, localizzazione zh-TW. → [Ambiente](../../reference/ENVIRONMENT.md)
|
||||
- **🤝 Più provider e agenti** — Cursor Cloud Agent, Grok Build (xAI) con login browser + OAuth, scheda Ollama di prima classe, Claude Opus 5 e Sonnet 5, partnership ufficiale Kimi (Code/Web/Moonshot), Zed, Requesty, SenseNova, Yuanbao, Agnes AI… e un catalogo aggiornato di **350 provider**. → [Provider](../../reference/PROVIDER_REFERENCE.md)
|
||||
- **📡 Trasparenza del routing** — ogni risposta include un header `X-OmniRoute-Decision` con strategia/provider/latenza che l'ha servita; una nuova strategia combo `cache-optimized` + il fattore `cacheAffinity` di Auto-Combo riportano le richieste ripetute alla connessione che possiede il prefisso in cache; un endpoint read-only `/v1/auto-combo/{channel}/candidates` espone il pool di candidati live di un canale `auto/*`. → [Auto-Combo](../../routing/AUTO-COMBO.md)
|
||||
|
||||
@@ -548,7 +548,7 @@ Radar isteğe bağlıdır (opt-in) ve yalnızca GET istekleri yapar. OmniRoute i
|
||||
- **🧠 Kontrol ettiğiniz bellek** — Varsayılan olarak kapalı, isteğe bağlı int8 vektör niceleme + tipli sönümleme, istek başına `x-omniroute-no-memory`. → [Bellek](docs/frameworks/MEMORY.md)
|
||||
- **🛡️ Güvenlik** — Her LLM rotasında istem enjeksiyonu koruması (red-team paketi), isteğe bağlı kimlik bilgisi maskeleme koruması (her iki yönde de sızan API anahtarlarını/gizli bilgileri sansürler), ücretsiz DuckDuckGo son çare web araması ve pano için isteğe bağlı OIDC giriş kapısı (şifreyle giriş her zaman kullanılabilir kalır). → [Güvenlik Önlemleri (Guardrails)](docs/security/GUARDRAILS.md)
|
||||
- **🖼️ Yeni uç noktalar** — `/v1/ocr` (Mistral OCR) ve `/v1/audio/translations` (Whisper tarzı) medya yüzeyini tamamlar. → [API Referansı](docs/reference/API_REFERENCE.md)
|
||||
- **🎨 Görsel / video / ses üretimi** — Medya için tek bir API: xAI Grok Imagine ve Novita AI video, ComfyUI, Freepik, Adobe Firefly, Microsoft Designer, Segmind ve ElevenLabs gibi konuşma sağlayıcıları. → [API Referansı](docs/reference/API_REFERENCE.md)
|
||||
- **🎨 Görsel / video / ses üretimi** — Medya için tek bir API: xAI Grok Imagine ve Novita AI video, ComfyUI, Freepik, Adobe Firefly, Segmind ve ElevenLabs gibi konuşma sağlayıcıları. → [API Referansı](docs/reference/API_REFERENCE.md)
|
||||
- **🌍 Dağıtım ve operasyonlar** — Ters proxy `basePath`, tarayıcı dili otomatik algılama, anahtar başına cihaz takibi, root gerektirmeyen MITM güveni, zh-TW yerelleştirmesi. → [Ortam Değişkenleri](docs/reference/ENVIRONMENT.md)
|
||||
- **🤝 Daha fazla sağlayıcı ve ajan** — Cursor Cloud Agent, tarayıcı + OAuth girişiyle Grok Build (xAI), Ollama birinci sınıf kartı, Claude Opus 5 ve Sonnet 5, Kimi resmi ortaklığı (Code/Web/Moonshot), Zed, Requesty, SenseNova, Yuanbao, Agnes AI… ve yenilenen **350 sağlayıcılı katalog**. → [Sağlayıcılar](docs/reference/PROVIDER_REFERENCE.md)
|
||||
- **📡 Yönlendirme şeffaflığı** — Her yanıt, isteğe hizmet veren stratejiyi/sağlayıcıyı/gecikmeyi belirten bir `X-OmniRoute-Decision` başlığı taşır, yeni bir `cache-optimized` kombo stratejisi + Auto-Combo `cacheAffinity` faktörü yinelenen istekleri önbelleğe alınmış öneki tutan bağlantıya geri yönlendirir ve salt okunur bir `/v1/auto-combo/{channel}/candidates` uç noktası bir `auto/*` kanalının canlı aday havuzunu gösterir. → [Auto-Combo](docs/routing/AUTO-COMBO.md)
|
||||
|
||||
@@ -1025,8 +1025,6 @@ desktop install.
|
||||
| `MODEL_CATALOG_INCLUDE_NAMES` | `true` | `src/shared/constants/featureFlagDefinitions.ts` | Include display-friendly `name` fields in `/v1/models` responses. Disable for clients that expect IDs only. |
|
||||
| `NANOBANANA_POLL_TIMEOUT_MS` | `120000` | `open-sse/handlers/imageGeneration.ts` | Max wait for NanoBanana image generation jobs. |
|
||||
| `NANOBANANA_POLL_INTERVAL_MS` | `2500` | `open-sse/handlers/imageGeneration.ts` | NanoBanana job polling frequency. |
|
||||
| `DESIGNER_WEB_POLL_TIMEOUT_MS` | `60000` | `open-sse/handlers/imageGeneration/providers/designerWeb.ts` | Max wait for microsoft-designer-web image generation jobs. |
|
||||
| `DESIGNER_WEB_POLL_INTERVAL_MS` | `2000` | `open-sse/handlers/imageGeneration/providers/designerWeb.ts` | microsoft-designer-web job polling frequency. |
|
||||
| `ADOBE_FIREFLY_SUBMIT_BASE_DELAY_MS` | `8000` | `open-sse/services/adobeFireflyUpscale.ts` | Base delay for the Adobe Firefly upscale submit-retry exponential backoff. |
|
||||
| `AWS_REGION` | _(unset)_ | `src/lib/providers/validation.ts`, `open-sse/handlers/audioSpeech.ts` | Region used to construct AWS Bedrock endpoints (Kiro, audio). |
|
||||
| `AWS_DEFAULT_REGION` | _(unset)_ | `src/lib/providers/validation.ts`, `open-sse/handlers/audioSpeech.ts` | Fallback when `AWS_REGION` is not set. |
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
---
|
||||
title: "Provider Reference"
|
||||
version: 3.8.51
|
||||
lastUpdated: 2026-08-27
|
||||
lastUpdated: 2026-08-28
|
||||
---
|
||||
|
||||
# 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-27
|
||||
> **Last generated:** 2026-08-28
|
||||
|
||||
Total providers: **357**. See category breakdown below.
|
||||
Total providers: **356**. See category breakdown below.
|
||||
|
||||
## Categories
|
||||
|
||||
@@ -82,7 +82,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 (35)
|
||||
## Web Cookie Providers (34)
|
||||
|
||||
| ID | Alias | Name | Tags | Website | Notes | Tool calling |
|
||||
|----|-------|------|------|---------|-------|--------------|
|
||||
@@ -106,7 +106,6 @@ 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. | — |
|
||||
| `microsoft-designer-web` | `msdesigner` | Microsoft Designer (Image Generation) | Web cookie | [link](https://designer.microsoft.com) | Sign in at designer.microsoft.com, then open DevTools → Network, generate an image, and find the request to DallE.ashx?action=GetDallEImagesCogSci. Copy the value of its Authorization: Bearer header (the access_token — no 'Bearer ' prefix). The token is short-lived; this is an unofficial, reverse-engineered integration. | — |
|
||||
| `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 |
|
||||
@@ -445,7 +444,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/) (112 implementations)
|
||||
- Executors: [`open-sse/executors/`](../../open-sse/executors/) (111 implementations)
|
||||
- Translators: [`open-sse/translator/`](../../open-sse/translator/)
|
||||
|
||||
## See Also
|
||||
|
||||
@@ -267,18 +267,6 @@ export const IMAGE_PROVIDERS: Record<string, ImageProviderConfig> = {
|
||||
supportedSizes: ["1024x1024", "1024x1792", "1792x1024", "1024x1536", "1536x1024"],
|
||||
},
|
||||
|
||||
"microsoft-designer-web": {
|
||||
id: "microsoft-designer-web",
|
||||
alias: "msdesigner",
|
||||
baseUrl:
|
||||
"https://designerapp.officeapps.live.com/designerapp/DallE.ashx?action=GetDallEImagesCogSci",
|
||||
authType: "apikey",
|
||||
authHeader: "bearer",
|
||||
format: "designer-web",
|
||||
models: [{ id: "dall-e-3", name: "DALL-E 3 (Microsoft Designer Web)" }],
|
||||
supportedSizes: ["1024x1024", "1792x1024", "1024x1792"],
|
||||
},
|
||||
|
||||
xai: {
|
||||
id: "xai",
|
||||
baseUrl: "https://api.x.ai/v1/images/generations",
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { SEARCH_PROVIDERS } from "../config/searchRegistry.ts";
|
||||
import { assertMicrosoftDesignerWebProviderAvailable } from "@/shared/constants/designerWebRetirement";
|
||||
import {
|
||||
registerLazyExecutor,
|
||||
loadRegisteredExecutor,
|
||||
@@ -124,10 +125,6 @@ const lazyExecutors: Record<string, () => Promise<BaseExecutor>> = {
|
||||
"copilot-m365-web": () =>
|
||||
import("./copilot-m365-web.ts").then((m) => new m.CopilotM365WebExecutor()),
|
||||
copilot: () => import("./copilot-web.ts").then((m) => new m.CopilotWebExecutor()), // Alias
|
||||
"microsoft-designer-web": () =>
|
||||
import("./microsoft-designer-web.ts").then((m) => new m.MicrosoftDesignerWebExecutor()),
|
||||
msdesigner: () =>
|
||||
import("./microsoft-designer-web.ts").then((m) => new m.MicrosoftDesignerWebExecutor()), // Alias
|
||||
"adobe-firefly": () => import("./adobe-firefly.ts").then((m) => new m.AdobeFireflyExecutor()),
|
||||
firefly: () => import("./adobe-firefly.ts").then((m) => new m.AdobeFireflyExecutor()), // Alias
|
||||
"veoaifree-web": () => import("./veoaifree-web.ts").then((m) => new m.VeoAIFreeWebExecutor()),
|
||||
@@ -242,6 +239,7 @@ const CHAT_UNSUPPORTED_CLOUD_AGENT_PROVIDERS = new Set(["jules"]);
|
||||
const CHAT_UNSUPPORTED_SEARCH_PROVIDERS = new Set(Object.keys(SEARCH_PROVIDERS));
|
||||
|
||||
export async function getExecutor(provider: string): Promise<BaseExecutor> {
|
||||
assertMicrosoftDesignerWebProviderAvailable(provider);
|
||||
const loaded = await loadRegisteredExecutor(provider);
|
||||
if (loaded) return loaded;
|
||||
if (CHAT_UNSUPPORTED_CLOUD_AGENT_PROVIDERS.has(provider)) {
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
// MicrosoftDesignerWebExecutor — chat-completions guard for the
|
||||
// microsoft-designer-web web-cookie provider (#6672).
|
||||
//
|
||||
// Microsoft Designer (designerapp.officeapps.live.com/DallE.ashx) is an
|
||||
// image-generation-only upstream: it has no chat/completions surface at all.
|
||||
// The real request/response flow lives entirely in the image-generation
|
||||
// handler (open-sse/handlers/imageGeneration/providers/designerWeb.ts),
|
||||
// dispatched from open-sse/handlers/imageGeneration.ts by
|
||||
// providerConfig.format === "designer-web" — NOT through getExecutor().
|
||||
//
|
||||
// microsoft-designer-web is still listed in WEB_COOKIE_PROVIDERS (it uses
|
||||
// the same unofficial, DevTools-sourced bearer-token credential UX and
|
||||
// subscription-risk notice as the other web-cookie providers — see
|
||||
// tests/unit/microsoft-designer-web-6672.test.ts). Without a registered
|
||||
// executor here, getExecutor("microsoft-designer-web") silently falls
|
||||
// through to DefaultExecutor's `PROVIDERS[provider] || PROVIDERS.openai`
|
||||
// fallback (open-sse/executors/index.ts:176 comment, #6699) — which would
|
||||
// send the user's real Designer bearer token to api.openai.com, mislabeled
|
||||
// as an OpenAI request, if anything ever mis-routes a chat/completions call
|
||||
// to this provider.
|
||||
//
|
||||
// This executor closes that gap cheaply: it never calls the network. Any
|
||||
// chat/completions attempt against microsoft-designer-web is rejected
|
||||
// immediately with a clean, sanitized 400 telling the caller to use
|
||||
// /v1/images/generations instead — satisfying the executor wrapper
|
||||
// contract (tests/unit/executor-web-cookie-sweep.test.ts) without ever
|
||||
// forwarding credentials anywhere.
|
||||
import { BaseExecutor, type ExecuteInput } from "./base.ts";
|
||||
import { makeExecutorErrorResult } from "../utils/error.ts";
|
||||
|
||||
const DESIGNER_WEB_BASE_URL =
|
||||
"https://designerapp.officeapps.live.com/designerapp/DallE.ashx?action=GetDallEImagesCogSci";
|
||||
|
||||
export class MicrosoftDesignerWebExecutor extends BaseExecutor {
|
||||
constructor() {
|
||||
super("microsoft-designer-web", { id: "microsoft-designer-web", baseUrl: DESIGNER_WEB_BASE_URL });
|
||||
}
|
||||
|
||||
async execute(_input: ExecuteInput) {
|
||||
return makeExecutorErrorResult(
|
||||
400,
|
||||
"microsoft-designer-web is an image-generation-only provider and does not support " +
|
||||
"chat completions. Use POST /v1/images/generations with model " +
|
||||
'"microsoft-designer-web/dall-e-3" instead.',
|
||||
_input.body,
|
||||
DESIGNER_WEB_BASE_URL
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default MicrosoftDesignerWebExecutor;
|
||||
@@ -20,6 +20,7 @@ import { getExecutor } from "../../executors/index.ts";
|
||||
import { isCliproxyapiDeepModeEnabled } from "../../executors/cliproxyapi.ts";
|
||||
import { isDarioDeepModeEnabled } from "../../executors/dario.ts";
|
||||
import { getCachedSettings } from "@/lib/db/readCache";
|
||||
import { assertMicrosoftDesignerWebProviderAvailable } from "@/shared/constants/designerWebRetirement";
|
||||
import { getUpstreamProxyConfigCached } from "./comboContextCache.ts";
|
||||
import type { FallbackBackend } from "@/lib/db/upstreamProxy";
|
||||
import { wrapExecutorWithCliproxyapiModelMapping } from "./cliproxyModelMapping.ts";
|
||||
@@ -94,6 +95,8 @@ export async function resolveExecutorWithProxy(
|
||||
log?: LoggerLike,
|
||||
providerSpecificData?: Record<string, unknown> | null
|
||||
) {
|
||||
assertMicrosoftDesignerWebProviderAvailable(prov);
|
||||
|
||||
// Per-connection routing override (#6339): the resolved connection can opt itself
|
||||
// into the CLIProxyAPI passthrough executor via providerSpecificData.cliproxyapiMode
|
||||
// === "claude-native" (UI toggle). This takes precedence over the provider-level
|
||||
@@ -173,7 +176,10 @@ export async function resolveExecutorWithProxy(
|
||||
result = await nativeExec.execute(input);
|
||||
} catch (err) {
|
||||
const errMsg = err instanceof Error ? err.message : String(err);
|
||||
log?.info?.("UPSTREAM_PROXY", `${prov} native error (${errMsg}), retrying via ${backendLabel}`);
|
||||
log?.info?.(
|
||||
"UPSTREAM_PROXY",
|
||||
`${prov} native error (${errMsg}), retrying via ${backendLabel}`
|
||||
);
|
||||
try {
|
||||
return await proxyExec.execute(input);
|
||||
} catch (proxyErr) {
|
||||
|
||||
@@ -35,6 +35,10 @@ import {
|
||||
getConfiguredTimeout,
|
||||
} from "@/shared/utils/fetchTimeout";
|
||||
import { sanitizeErrorMessage, sanitizeUpstreamDetails } from "../utils/error.ts";
|
||||
import {
|
||||
isMicrosoftDesignerWebRetiredProviderId,
|
||||
MICROSOFT_DESIGNER_WEB_RETIRED_MESSAGE,
|
||||
} from "@/shared/constants/designerWebRetirement";
|
||||
|
||||
import { handleSDWebUIImageGeneration } from "./imageGeneration/providers/sdWebUI.ts";
|
||||
import { handleHyperbolicImageGeneration } from "./imageGeneration/providers/hyperbolic.ts";
|
||||
@@ -52,7 +56,6 @@ import {
|
||||
} from "./imageGeneration/providers/chatgptWeb.ts";
|
||||
import { handleNvidiaNimImageGeneration } from "./imageGeneration/providers/nvidiaNim.ts";
|
||||
import { handleSegmindImageGeneration } from "./imageGeneration/providers/segmind.ts";
|
||||
import { handleDesignerWebImageGeneration } from "./imageGeneration/providers/designerWeb.ts";
|
||||
import { handleCursorAgentImageGeneration } from "./imageGeneration/providers/cursorAgentImage.ts";
|
||||
import { handleMinimaxImageGeneration } from "./imageGeneration/providers/minimax.ts";
|
||||
import { handleAdobeFireflyImageGeneration } from "./imageGeneration/providers/adobeFirefly.ts";
|
||||
@@ -382,6 +385,20 @@ export async function handleImageGeneration({
|
||||
clientHeaders = null,
|
||||
peerLocality = null,
|
||||
}) {
|
||||
const requestedModel = typeof body?.model === "string" ? body.model : "";
|
||||
const slash = requestedModel.indexOf("/");
|
||||
const requestedPrefix = slash > 0 ? requestedModel.slice(0, slash) : requestedModel;
|
||||
if (
|
||||
isMicrosoftDesignerWebRetiredProviderId(resolvedProvider) ||
|
||||
isMicrosoftDesignerWebRetiredProviderId(requestedPrefix)
|
||||
) {
|
||||
return {
|
||||
success: false,
|
||||
status: HTTP_STATUS.GONE,
|
||||
error: MICROSOFT_DESIGNER_WEB_RETIRED_MESSAGE,
|
||||
};
|
||||
}
|
||||
|
||||
let provider, model;
|
||||
|
||||
if (resolvedProvider) {
|
||||
@@ -590,17 +607,6 @@ export async function handleImageGeneration({
|
||||
});
|
||||
}
|
||||
|
||||
if (providerConfig.format === "designer-web") {
|
||||
return handleDesignerWebImageGeneration({
|
||||
model,
|
||||
provider,
|
||||
providerConfig,
|
||||
body,
|
||||
credentials,
|
||||
log,
|
||||
});
|
||||
}
|
||||
|
||||
if (providerConfig.format === "adobe-firefly-image") {
|
||||
return handleAdobeFireflyImageGeneration({
|
||||
model,
|
||||
|
||||
@@ -1,301 +0,0 @@
|
||||
// Microsoft Designer (unofficial, reverse-engineered web API) image handler.
|
||||
// Family: designer-web | Provider: microsoft-designer-web
|
||||
// Reference: g4f/Provider/needs_auth/MicrosoftDesigner.py (fetched + verified
|
||||
// during triage of #6672) — Bearer access_token auth against
|
||||
// designerapp.officeapps.live.com/designerapp/DallE.ashx, submit-then-poll
|
||||
// for image_urls_thumbnail[].ImageUrl.
|
||||
//
|
||||
// The upstream ClientId header is a fixed, publicly-shared value the
|
||||
// designer.microsoft.com frontend sends on every session (not a secret) —
|
||||
// routed through resolvePublicCred() per Hard Rule #11 / docs/security/PUBLIC_CREDS.md.
|
||||
|
||||
import { randomUUID, randomBytes } from "node:crypto";
|
||||
import { resolvePublicCred } from "../../../utils/publicCreds.ts";
|
||||
import { sanitizeErrorMessage } from "../../../utils/error.ts";
|
||||
import { saveImageErrorResult, saveImageSuccessResult } from "../../imageGeneration.ts";
|
||||
|
||||
const DESIGNER_WEB_POLL_TIMEOUT_MS_DEFAULT = 60000;
|
||||
const DESIGNER_WEB_POLL_INTERVAL_MS_DEFAULT = 2000;
|
||||
const DESIGNER_WEB_BATCH_SIZE = "4";
|
||||
|
||||
/** Maps an OpenAI-style "WxH" size string to the closest Designer aspect ratio bucket. */
|
||||
export function mapDesignerWebImageSize(size: unknown): "1_1" | "16_9" | "9_16" {
|
||||
if (typeof size !== "string" || !size.includes("x")) return "1_1";
|
||||
const [wRaw, hRaw] = size.split("x");
|
||||
const w = Number(wRaw);
|
||||
const h = Number(hRaw);
|
||||
if (!Number.isFinite(w) || !Number.isFinite(h) || w <= 0 || h <= 0) return "1_1";
|
||||
if (w > h * 1.2) return "16_9";
|
||||
if (h > w * 1.2) return "9_16";
|
||||
return "1_1";
|
||||
}
|
||||
|
||||
/** Builds the fixed + per-request headers Microsoft Designer expects on every call. */
|
||||
export function buildDesignerWebHeaders({
|
||||
accessToken,
|
||||
sessionId = randomUUID(),
|
||||
userId = randomBytes(16).toString("hex"),
|
||||
}: {
|
||||
accessToken: string;
|
||||
sessionId?: string;
|
||||
userId?: string;
|
||||
}): Record<string, string> {
|
||||
return {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
ClientId: resolvePublicCred("microsoft_designer_client_id"),
|
||||
SessionId: sessionId,
|
||||
UserId: userId,
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
};
|
||||
}
|
||||
|
||||
/** Builds the DallE.ashx form body from an OpenAI-shaped image-generation request. */
|
||||
export function buildDesignerWebFormBody(prompt: string, size: unknown): URLSearchParams {
|
||||
const params = new URLSearchParams();
|
||||
params.set("dalle-caption", prompt);
|
||||
params.set("dalle-image-size", mapDesignerWebImageSize(size));
|
||||
params.set("dalle-batch-size", DESIGNER_WEB_BATCH_SIZE);
|
||||
params.set("dalle-seed", String(Math.floor(Math.random() * 1_000_000_000)));
|
||||
return params;
|
||||
}
|
||||
|
||||
interface DesignerWebParsedResponse {
|
||||
status: "ready" | "pending" | "empty";
|
||||
imageUrls: string[];
|
||||
pollIntervalMs: number | null;
|
||||
}
|
||||
|
||||
/** Parses a DallE.ashx JSON body into a ready/pending/empty verdict. */
|
||||
export function parseDesignerWebResponse(json: unknown): DesignerWebParsedResponse {
|
||||
const body = (json ?? {}) as Record<string, unknown>;
|
||||
const thumbs = Array.isArray(body.image_urls_thumbnail) ? body.image_urls_thumbnail : [];
|
||||
const imageUrls = thumbs
|
||||
.map((t) => (t && typeof t === "object" ? (t as Record<string, unknown>).ImageUrl : null))
|
||||
.filter((u): u is string => typeof u === "string" && u.length > 0);
|
||||
|
||||
if (imageUrls.length > 0) {
|
||||
return { status: "ready", imageUrls, pollIntervalMs: null };
|
||||
}
|
||||
|
||||
const pollingMeta = (body.polling_response as Record<string, unknown> | undefined)
|
||||
?.polling_meta_data as Record<string, unknown> | undefined;
|
||||
const pollIntervalMs = Number.isFinite(pollingMeta?.poll_interval)
|
||||
? Number(pollingMeta?.poll_interval)
|
||||
: null;
|
||||
|
||||
if (pollIntervalMs !== null) {
|
||||
return { status: "pending", imageUrls: [], pollIntervalMs };
|
||||
}
|
||||
|
||||
return { status: "empty", imageUrls: [], pollIntervalMs: null };
|
||||
}
|
||||
|
||||
function normalizePositiveNumber(value: unknown, fallback: number): number {
|
||||
const n = Number(value);
|
||||
return Number.isFinite(n) && n > 0 ? n : fallback;
|
||||
}
|
||||
|
||||
interface DesignerWebRequestConfig {
|
||||
prompt: string;
|
||||
accessToken: string;
|
||||
headers: Record<string, string>;
|
||||
formBody: URLSearchParams;
|
||||
timeoutMs: number;
|
||||
pollIntervalMs: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Outcome of request validation. String-discriminated rather than `ok: boolean`
|
||||
* because `open-sse` compiles with `strictNullChecks: false`, where a
|
||||
* boolean-literal discriminant narrows the positive branch but leaves the
|
||||
* negative one as the full union — so `if (!resolved.ok)` would not expose
|
||||
* `status`/`error`. All three unions in this file shared that root cause.
|
||||
*/
|
||||
type DesignerWebRequestResolution =
|
||||
| { state: "resolved"; config: DesignerWebRequestConfig }
|
||||
| { state: "invalid"; status: number; error: string };
|
||||
|
||||
/** Validates the request and resolves auth + poll timing. Returns an error status/message on failure. */
|
||||
function resolveDesignerWebRequest(
|
||||
body: { prompt?: unknown; size?: unknown; timeout_ms?: unknown; poll_interval_ms?: unknown },
|
||||
credentials: { apiKey?: string; accessToken?: string }
|
||||
): DesignerWebRequestResolution {
|
||||
const prompt = typeof body.prompt === "string" ? body.prompt.trim() : "";
|
||||
if (!prompt) {
|
||||
return {
|
||||
state: "invalid",
|
||||
status: 400,
|
||||
error: "Prompt is required for Microsoft Designer image generation",
|
||||
};
|
||||
}
|
||||
|
||||
const accessToken = credentials?.apiKey || credentials?.accessToken;
|
||||
if (!accessToken) {
|
||||
return {
|
||||
state: "invalid",
|
||||
status: 401,
|
||||
error: "Microsoft Designer credentials missing access_token",
|
||||
};
|
||||
}
|
||||
|
||||
const timeoutMs = normalizePositiveNumber(
|
||||
body.timeout_ms,
|
||||
normalizePositiveNumber(
|
||||
process.env.DESIGNER_WEB_POLL_TIMEOUT_MS,
|
||||
DESIGNER_WEB_POLL_TIMEOUT_MS_DEFAULT
|
||||
)
|
||||
);
|
||||
const pollIntervalMs = normalizePositiveNumber(
|
||||
body.poll_interval_ms,
|
||||
normalizePositiveNumber(
|
||||
process.env.DESIGNER_WEB_POLL_INTERVAL_MS,
|
||||
DESIGNER_WEB_POLL_INTERVAL_MS_DEFAULT
|
||||
)
|
||||
);
|
||||
|
||||
return {
|
||||
state: "resolved",
|
||||
config: {
|
||||
prompt,
|
||||
accessToken,
|
||||
headers: buildDesignerWebHeaders({ accessToken }),
|
||||
formBody: buildDesignerWebFormBody(prompt, body.size),
|
||||
timeoutMs,
|
||||
pollIntervalMs,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
type DesignerWebPending = { state: "pending"; waitMs: number };
|
||||
type DesignerWebReady = { state: "ready"; imageUrls: string[] };
|
||||
type DesignerWebFailed = { state: "failed"; status: number; error: string };
|
||||
|
||||
/** One poll cycle: still working, finished with images, or finished with an error. */
|
||||
type DesignerWebStepResult = DesignerWebPending | DesignerWebReady | DesignerWebFailed;
|
||||
|
||||
/**
|
||||
* What the poll loop hands back. Deliberately excludes the pending arm — the
|
||||
* loop either returns a terminal step or synthesizes a 504, and never surfaces
|
||||
* `pending` to its caller. The previous signature admitted it, which is why
|
||||
* `outcome.success` did not exist on every member of that union.
|
||||
*/
|
||||
type DesignerWebOutcome = DesignerWebReady | DesignerWebFailed;
|
||||
|
||||
/** Runs one submit/poll fetch cycle and classifies the outcome. */
|
||||
async function stepDesignerWebPoll(
|
||||
baseUrl: string,
|
||||
headers: Record<string, string>,
|
||||
formBody: URLSearchParams,
|
||||
pollIntervalMs: number,
|
||||
fetchImpl: typeof fetch
|
||||
): Promise<DesignerWebStepResult> {
|
||||
const resp = await fetchImpl(baseUrl, { method: "POST", headers, body: formBody });
|
||||
|
||||
if (!resp.ok) {
|
||||
return {
|
||||
state: "failed",
|
||||
status: resp.status,
|
||||
error: sanitizeErrorMessage(await resp.text()),
|
||||
};
|
||||
}
|
||||
|
||||
const parsed = parseDesignerWebResponse(await resp.json());
|
||||
|
||||
if (parsed.status === "ready") {
|
||||
return { state: "ready", imageUrls: parsed.imageUrls };
|
||||
}
|
||||
if (parsed.status === "empty") {
|
||||
return {
|
||||
state: "failed",
|
||||
status: 502,
|
||||
error: "Microsoft Designer response did not contain image data or polling metadata",
|
||||
};
|
||||
}
|
||||
return {
|
||||
state: "pending",
|
||||
waitMs: Math.min(parsed.pollIntervalMs ?? pollIntervalMs, pollIntervalMs),
|
||||
};
|
||||
}
|
||||
|
||||
/** Drives the submit-then-poll loop to completion, timeout, or a terminal error. */
|
||||
async function runDesignerWebPollLoop(
|
||||
baseUrl: string,
|
||||
config: DesignerWebRequestConfig,
|
||||
fetchImpl: typeof fetch,
|
||||
log?: { info?: (...args: unknown[]) => void }
|
||||
): Promise<DesignerWebOutcome> {
|
||||
const deadline = Date.now() + config.timeoutMs;
|
||||
let attempt = 0;
|
||||
|
||||
while (Date.now() < deadline) {
|
||||
attempt += 1;
|
||||
const step = await stepDesignerWebPoll(
|
||||
baseUrl,
|
||||
config.headers,
|
||||
config.formBody,
|
||||
config.pollIntervalMs,
|
||||
fetchImpl
|
||||
);
|
||||
if (step.state !== "pending") return step;
|
||||
log?.info?.("IMAGE", `designer-web pending, poll #${attempt} in ${step.waitMs}ms`);
|
||||
await new Promise((resolve) => setTimeout(resolve, step.waitMs));
|
||||
}
|
||||
|
||||
return {
|
||||
state: "failed",
|
||||
status: 504,
|
||||
error: "Microsoft Designer image generation timed out waiting for a result",
|
||||
};
|
||||
}
|
||||
|
||||
export async function handleDesignerWebImageGeneration({
|
||||
model,
|
||||
provider,
|
||||
providerConfig,
|
||||
body,
|
||||
credentials,
|
||||
log,
|
||||
fetchImpl = fetch,
|
||||
}: {
|
||||
model: string;
|
||||
provider: string;
|
||||
providerConfig: { baseUrl: string };
|
||||
body: { prompt?: unknown; size?: unknown; timeout_ms?: unknown; poll_interval_ms?: unknown };
|
||||
credentials: { apiKey?: string; accessToken?: string };
|
||||
log?: { info?: (...args: unknown[]) => void; error?: (...args: unknown[]) => void };
|
||||
fetchImpl?: typeof fetch;
|
||||
}) {
|
||||
const startTime = Date.now();
|
||||
const resolved = resolveDesignerWebRequest(body, credentials);
|
||||
if (resolved.state === "invalid") {
|
||||
return saveImageErrorResult({
|
||||
provider,
|
||||
model,
|
||||
status: resolved.status,
|
||||
startTime,
|
||||
error: resolved.error,
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
const outcome = await runDesignerWebPollLoop(providerConfig.baseUrl, resolved.config, fetchImpl, log);
|
||||
if (outcome.state === "ready") {
|
||||
return saveImageSuccessResult({
|
||||
provider,
|
||||
model,
|
||||
startTime,
|
||||
images: outcome.imageUrls.map((url) => ({ url })),
|
||||
});
|
||||
}
|
||||
if (log?.error) {
|
||||
log.error("IMAGE", `${provider} designer-web error ${outcome.status}: ${outcome.error}`);
|
||||
}
|
||||
return saveImageErrorResult({ provider, model, status: outcome.status, startTime, error: outcome.error });
|
||||
} catch (err) {
|
||||
const errorText = sanitizeErrorMessage(err instanceof Error ? err.message : String(err));
|
||||
if (log?.error) {
|
||||
log.error("IMAGE", `${provider} designer-web exception: ${errorText}`);
|
||||
}
|
||||
return saveImageErrorResult({ provider, model, status: 500, startTime, error: errorText });
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import { getSettings } from "@/lib/db/settings";
|
||||
import { getProviderRegistry } from "./providerRegistryAccessor";
|
||||
import type { ConnectionFields } from "@/lib/db/encryption";
|
||||
import { NOAUTH_PROVIDERS } from "@/shared/constants/providers";
|
||||
import { isMicrosoftDesignerWebRetiredProviderId } from "@/shared/constants/designerWebRetirement";
|
||||
import { hasUsableWebSessionCredential } from "@/shared/providers/webSessionCredentials";
|
||||
import { toNumber } from "@/shared/utils/numeric";
|
||||
import { isCompatibleProviderConnectionId } from "@/shared/utils/compatibleProviderId";
|
||||
@@ -600,19 +601,22 @@ export async function prepareVirtualAutoComboInputs(
|
||||
.filter((conn) => conn.provider in NOAUTH_PROVIDERS)
|
||||
.map((conn) => conn.provider)
|
||||
);
|
||||
const runtimeConnections = connections.filter(
|
||||
(connection) => !isMicrosoftDesignerWebRetiredProviderId(connection.provider)
|
||||
);
|
||||
const hiddenModelsMap = getHiddenModelsByProvider();
|
||||
// #7622: a no-auth provider's own provider_connections row (#6557) can carry
|
||||
// `providerSpecificData.excludedModels` regardless of its isActive state (the
|
||||
// dispatch-time enforcement in auth.ts does not gate on isActive either), so
|
||||
// gather it from BOTH the active and disabled connection lists.
|
||||
const noAuthProviderSpecificData = new Map<string, Record<string, unknown> | null | undefined>();
|
||||
for (const conn of [...connections, ...disabledNoAuthConnections]) {
|
||||
for (const conn of [...runtimeConnections, ...disabledNoAuthConnections]) {
|
||||
if (conn.provider in NOAUTH_PROVIDERS) {
|
||||
noAuthProviderSpecificData.set(conn.provider, conn.providerSpecificData);
|
||||
}
|
||||
}
|
||||
|
||||
const validConnections = connections.filter(hasUsableConnectionCredential);
|
||||
const validConnections = runtimeConnections.filter(hasUsableConnectionCredential);
|
||||
|
||||
const candidatePool: VirtualAutoComboCandidate[] = [];
|
||||
const registry = getProviderRegistry();
|
||||
@@ -699,7 +703,7 @@ export async function prepareVirtualAutoComboInputs(
|
||||
// #7623: honor existing model lockouts + connection cooldown/terminal state so
|
||||
// auto/* never advertises models the dispatch path would immediately skip.
|
||||
const connectionsById = new Map<string, ConnectionResilienceView>();
|
||||
for (const conn of [...connections, ...disabledNoAuthConnections]) {
|
||||
for (const conn of [...runtimeConnections, ...disabledNoAuthConnections]) {
|
||||
connectionsById.set(conn.id, conn);
|
||||
}
|
||||
|
||||
|
||||
@@ -28,6 +28,7 @@ import type {
|
||||
ResolvedComboTarget,
|
||||
} from "./types.ts";
|
||||
import { extractSessionAffinityKey } from "@/sse/services/auth";
|
||||
import { isMicrosoftDesignerWebRetiredProviderId } from "@/shared/constants/designerWebRetirement";
|
||||
import { filterChatSelectableModels } from "../modelEndpointPolicy.ts";
|
||||
import { DEFAULT_INTENT_CONFIG, type IntentClassifierConfig } from "../intentClassifier.ts";
|
||||
import { getTaskFitness } from "../autoCombo/taskFitness.ts";
|
||||
@@ -457,7 +458,10 @@ export async function expandAutoComboCandidatePool(
|
||||
...new Set(
|
||||
(allConnections as Array<{ provider?: unknown }>)
|
||||
.map((c) => c.provider)
|
||||
.filter((p): p is string => typeof p === "string" && p.length > 0)
|
||||
.filter(
|
||||
(p): p is string =>
|
||||
typeof p === "string" && p.length > 0 && !isMicrosoftDesignerWebRetiredProviderId(p)
|
||||
)
|
||||
),
|
||||
];
|
||||
// Pre-build a Set of already-present modelStr values so candidate-pool
|
||||
|
||||
@@ -172,14 +172,6 @@ const EMBEDDED_DEFAULTS = {
|
||||
openference_id: [0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||
// Trae Cloud IDE — public oauth client id
|
||||
trae_id: [10, 3, 95, 6, 10, 22, 66, 3, 11, 90, 72, 31, 91, 2],
|
||||
// Microsoft Designer web app — public ClientId header sent by the
|
||||
// designer.microsoft.com frontend to designerapp.officeapps.live.com
|
||||
// (not a secret — every browser session sends the same fixed value;
|
||||
// reverse-engineered from the g4f MicrosoftDesigner provider reference).
|
||||
microsoft_designer_client_id: [
|
||||
13, 88, 13, 91, 68, 89, 65, 21, 72, 26, 21, 76, 0, 65, 93, 2, 26, 23, 28, 87, 14, 87, 8, 95, 12,
|
||||
17, 70, 6, 24, 66, 17, 1, 10, 95, 81, 28,
|
||||
],
|
||||
// Microsoft 365 Copilot web (m365.cloud.microsoft) — public SPA client id
|
||||
// observed in browser tokens and M365-Copilot2API. Not a per-user secret.
|
||||
m365_oauth_client_id: [
|
||||
|
||||
@@ -8,8 +8,8 @@ import { getWebSessionCredentialRequirement } from "@/shared/providers/webSessio
|
||||
* `validateWebCookieProvider` probe (`src/lib/providers/validation/webCookie.ts`).
|
||||
*
|
||||
* `WEB_SESSION_CREDENTIAL_REQUIREMENTS` currently marks more providers as `kind: "token"`
|
||||
* than have a matching validator (e.g. hailuo-web, microsoft-designer-web, t3-chat-web,
|
||||
* promptql). Those fall through to `validateWebCookieProvider`'s generic probe, which
|
||||
* than have a matching validator (e.g. hailuo-web, t3-chat-web, promptql). Those fall
|
||||
* through to `validateWebCookieProvider`'s generic probe, which
|
||||
* sends the stored credential as a `Cookie` header and treats most non-401/403 responses
|
||||
* as valid — the wrong wire format for a token-authenticated provider, so an invalid
|
||||
* token can be reported as a healthy connection. Keep this set in sync with
|
||||
@@ -30,7 +30,6 @@ export function shouldUseApiKeyConnectionTest(authType: unknown, providerId: unk
|
||||
if (authType !== "cookie") return false;
|
||||
if (getWebSessionCredentialRequirement(providerId)?.kind !== "token") return false;
|
||||
return (
|
||||
typeof providerId === "string" &&
|
||||
TOKEN_AWARE_VALIDATED_WEB_SESSION_PROVIDERS.has(providerId)
|
||||
typeof providerId === "string" && TOKEN_AWARE_VALIDATED_WEB_SESSION_PROVIDERS.has(providerId)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@ import {
|
||||
extractImageEditInputFromJson,
|
||||
validateCodexImageEditReferences,
|
||||
} from "@/lib/images/imageRouteModel";
|
||||
import { isMicrosoftDesignerWebProviderRetiredError } from "@/shared/constants/designerWebRetirement";
|
||||
import { resolveProxyForConnection } from "@/lib/localDb";
|
||||
import { runWithProxyContext } from "@omniroute/open-sse/utils/proxyFetch.ts";
|
||||
import { isCodexFreePlan } from "@omniroute/open-sse/executors/codex/tools.ts";
|
||||
@@ -363,7 +364,15 @@ async function postHandler(request: Request, _context?: unknown) {
|
||||
|
||||
// Resolve combo/alias, custom-provider prefix, and built-in ids consistently with
|
||||
// /v1/images/generations (#3215).
|
||||
const resolvedModel = await resolveImageRouteModel(fullModel);
|
||||
let resolvedModel: string;
|
||||
try {
|
||||
resolvedModel = await resolveImageRouteModel(fullModel);
|
||||
} catch (error) {
|
||||
if (isMicrosoftDesignerWebProviderRetiredError(error)) {
|
||||
return errorResponse(HTTP_STATUS.GONE, error.message);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
const parsed = parseImageModel(resolvedModel);
|
||||
const providerConfig = parsed.provider ? getImageProvider(parsed.provider) : null;
|
||||
// Firefly nano/gpt-image accept multiple reference blobs; other non-Codex stay at 1.
|
||||
|
||||
@@ -23,6 +23,11 @@ import { getComboByName } from "@/lib/db/combos";
|
||||
import { getAllCustomModels } from "@/lib/db/models";
|
||||
import { resolveProxyForConnection } from "@/lib/db/settings";
|
||||
import { resolveImageRouteModel } from "@/lib/images/imageRouteModel";
|
||||
import {
|
||||
isMicrosoftDesignerWebProviderRetiredError,
|
||||
isMicrosoftDesignerWebRetiredProviderId,
|
||||
MICROSOFT_DESIGNER_WEB_RETIRED_MESSAGE,
|
||||
} from "@/shared/constants/designerWebRetirement";
|
||||
import {
|
||||
resolveLocalSyncedEndpointRoute,
|
||||
type LocalSyncedEndpointRoute,
|
||||
@@ -122,21 +127,20 @@ async function postHandler(request, context) {
|
||||
const policy = await enforceApiKeyPolicy(request, body.model);
|
||||
if (policy.rejection) return policy.rejection;
|
||||
|
||||
const modelPrefix = body.model.includes("/")
|
||||
? body.model.slice(0, body.model.indexOf("/"))
|
||||
: body.model;
|
||||
if (isMicrosoftDesignerWebRetiredProviderId(modelPrefix)) {
|
||||
return errorResponse(HTTP_STATUS.GONE, MICROSOFT_DESIGNER_WEB_RETIRED_MESSAGE);
|
||||
}
|
||||
|
||||
// #9239: Detect combo name and divert to full image combo execution.
|
||||
// Checks before resolveImageRouteModel so we skip single-target flattening.
|
||||
if (body.model && typeof body.model === "string" && !body.model.includes("/")) {
|
||||
const combo = await getComboByName(body.model as string);
|
||||
if (combo) {
|
||||
const { executeImageCombo } = await import(
|
||||
"@omniroute/open-sse/services/imageCombo"
|
||||
);
|
||||
return executeImageCombo(
|
||||
body.model as string,
|
||||
body,
|
||||
{ request, policy },
|
||||
startTime,
|
||||
log
|
||||
);
|
||||
const { executeImageCombo } = await import("@omniroute/open-sse/services/imageCombo");
|
||||
return executeImageCombo(body.model as string, body, { request, policy }, startTime, log);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -144,7 +148,14 @@ async function postHandler(request, context) {
|
||||
// model (`myImg/gpt-image-2`) to its internal `<nodeId>/<model>` form so the
|
||||
// custom-model lookup and handler's resolvedProvider extraction resolve correctly.
|
||||
// Built-in and already-internal ids pass through unchanged. Shared with /images/edits.
|
||||
body.model = await resolveImageRouteModel(body.model);
|
||||
try {
|
||||
body.model = await resolveImageRouteModel(body.model);
|
||||
} catch (error) {
|
||||
if (isMicrosoftDesignerWebProviderRetiredError(error)) {
|
||||
return errorResponse(HTTP_STATUS.GONE, error.message);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
// Parse model to get provider
|
||||
let { provider, model: requestedModel } = parseImageModel(body.model);
|
||||
@@ -246,7 +257,8 @@ async function postHandler(request, context) {
|
||||
provider,
|
||||
null,
|
||||
syncedEndpointRoute?.connectionIds ?? null,
|
||||
requestedModel );
|
||||
requestedModel
|
||||
);
|
||||
if (!credentials) {
|
||||
return errorResponse(
|
||||
HTTP_STATUS.BAD_REQUEST,
|
||||
@@ -346,7 +358,10 @@ async function postHandler(request, context) {
|
||||
});
|
||||
}
|
||||
|
||||
const errorPayload = toJsonErrorPayload((result as any).error, "Image generation provider error") as {
|
||||
const errorPayload = toJsonErrorPayload(
|
||||
(result as any).error,
|
||||
"Image generation provider error"
|
||||
) as {
|
||||
error?: { message?: string };
|
||||
};
|
||||
const message =
|
||||
|
||||
122
src/lib/db/migrations/164_retire_microsoft_designer_web.sql
Normal file
122
src/lib/db/migrations/164_retire_microsoft_designer_web.sql
Normal file
@@ -0,0 +1,122 @@
|
||||
-- Permanently retire the Microsoft Designer Web reverse-engineered integration.
|
||||
-- Keep connection rows, encrypted credentials, allowlists, and historical usage intact.
|
||||
|
||||
UPDATE provider_connections
|
||||
SET is_active = 0,
|
||||
test_status = 'unavailable',
|
||||
last_error = 'Provider retired from OmniRoute runtime.',
|
||||
last_error_at = COALESCE(last_error_at, CURRENT_TIMESTAMP),
|
||||
last_error_type = 'provider_retired',
|
||||
last_error_source = 'migration:retire-microsoft-designer-web'
|
||||
WHERE lower(trim(provider, ' ' || char(9) || char(10) || char(11) || char(12) || char(13)))
|
||||
IN ('microsoft-designer-web', 'msdesigner');
|
||||
|
||||
UPDATE exclusive_connection_leases
|
||||
SET state = 'INVALIDATED',
|
||||
ended_at = COALESCE(ended_at, CURRENT_TIMESTAMP),
|
||||
end_reason = 'AUTHORIZATION_CHANGED'
|
||||
WHERE state = 'ACTIVE'
|
||||
AND (
|
||||
lower(trim(provider, ' ' || char(9) || char(10) || char(11) || char(12) || char(13)))
|
||||
IN ('microsoft-designer-web', 'msdesigner')
|
||||
OR connection_id IN (
|
||||
SELECT id
|
||||
FROM provider_connections
|
||||
WHERE lower(trim(provider, ' ' || char(9) || char(10) || char(11) || char(12) || char(13)))
|
||||
IN ('microsoft-designer-web', 'msdesigner')
|
||||
)
|
||||
);
|
||||
|
||||
CREATE TRIGGER IF NOT EXISTS trg_retire_microsoft_designer_web_provider_insert
|
||||
AFTER INSERT ON provider_connections
|
||||
WHEN lower(trim(NEW.provider, ' ' || char(9) || char(10) || char(11) || char(12) || char(13)))
|
||||
IN ('microsoft-designer-web', 'msdesigner')
|
||||
BEGIN
|
||||
UPDATE provider_connections
|
||||
SET is_active = 0,
|
||||
test_status = 'unavailable',
|
||||
last_error = 'Provider retired from OmniRoute runtime.',
|
||||
last_error_at = COALESCE(last_error_at, CURRENT_TIMESTAMP),
|
||||
last_error_type = 'provider_retired',
|
||||
last_error_source = 'migration:retire-microsoft-designer-web'
|
||||
WHERE id = NEW.id;
|
||||
|
||||
UPDATE exclusive_connection_leases
|
||||
SET state = 'INVALIDATED',
|
||||
ended_at = COALESCE(ended_at, CURRENT_TIMESTAMP),
|
||||
end_reason = 'AUTHORIZATION_CHANGED'
|
||||
WHERE connection_id = NEW.id AND state = 'ACTIVE';
|
||||
END;
|
||||
|
||||
CREATE TRIGGER IF NOT EXISTS trg_retire_microsoft_designer_web_provider_update
|
||||
AFTER UPDATE OF provider, is_active, test_status, last_error, last_error_type, last_error_source
|
||||
ON provider_connections
|
||||
WHEN lower(trim(NEW.provider, ' ' || char(9) || char(10) || char(11) || char(12) || char(13)))
|
||||
IN ('microsoft-designer-web', 'msdesigner')
|
||||
AND (
|
||||
COALESCE(NEW.is_active, 0) <> 0
|
||||
OR COALESCE(NEW.test_status, '') <> 'unavailable'
|
||||
OR COALESCE(NEW.last_error, '') <> 'Provider retired from OmniRoute runtime.'
|
||||
OR COALESCE(NEW.last_error_type, '') <> 'provider_retired'
|
||||
OR COALESCE(NEW.last_error_source, '') <> 'migration:retire-microsoft-designer-web'
|
||||
)
|
||||
BEGIN
|
||||
UPDATE provider_connections
|
||||
SET is_active = 0,
|
||||
test_status = 'unavailable',
|
||||
last_error = 'Provider retired from OmniRoute runtime.',
|
||||
last_error_at = COALESCE(last_error_at, CURRENT_TIMESTAMP),
|
||||
last_error_type = 'provider_retired',
|
||||
last_error_source = 'migration:retire-microsoft-designer-web'
|
||||
WHERE id = NEW.id;
|
||||
|
||||
UPDATE exclusive_connection_leases
|
||||
SET state = 'INVALIDATED',
|
||||
ended_at = COALESCE(ended_at, CURRENT_TIMESTAMP),
|
||||
end_reason = 'AUTHORIZATION_CHANGED'
|
||||
WHERE connection_id = NEW.id AND state = 'ACTIVE';
|
||||
END;
|
||||
|
||||
CREATE TRIGGER IF NOT EXISTS trg_retire_microsoft_designer_web_lease_insert
|
||||
AFTER INSERT ON exclusive_connection_leases
|
||||
WHEN NEW.state = 'ACTIVE'
|
||||
AND (
|
||||
lower(trim(NEW.provider, ' ' || char(9) || char(10) || char(11) || char(12) || char(13)))
|
||||
IN ('microsoft-designer-web', 'msdesigner')
|
||||
OR EXISTS (
|
||||
SELECT 1
|
||||
FROM provider_connections
|
||||
WHERE id = NEW.connection_id
|
||||
AND lower(trim(provider, ' ' || char(9) || char(10) || char(11) || char(12) || char(13)))
|
||||
IN ('microsoft-designer-web', 'msdesigner')
|
||||
)
|
||||
)
|
||||
BEGIN
|
||||
UPDATE exclusive_connection_leases
|
||||
SET state = 'INVALIDATED',
|
||||
ended_at = COALESCE(ended_at, CURRENT_TIMESTAMP),
|
||||
end_reason = 'AUTHORIZATION_CHANGED'
|
||||
WHERE id = NEW.id AND state = 'ACTIVE';
|
||||
END;
|
||||
|
||||
CREATE TRIGGER IF NOT EXISTS trg_retire_microsoft_designer_web_lease_update
|
||||
AFTER UPDATE OF provider, connection_id, state ON exclusive_connection_leases
|
||||
WHEN NEW.state = 'ACTIVE'
|
||||
AND (
|
||||
lower(trim(NEW.provider, ' ' || char(9) || char(10) || char(11) || char(12) || char(13)))
|
||||
IN ('microsoft-designer-web', 'msdesigner')
|
||||
OR EXISTS (
|
||||
SELECT 1
|
||||
FROM provider_connections
|
||||
WHERE id = NEW.connection_id
|
||||
AND lower(trim(provider, ' ' || char(9) || char(10) || char(11) || char(12) || char(13)))
|
||||
IN ('microsoft-designer-web', 'msdesigner')
|
||||
)
|
||||
)
|
||||
BEGIN
|
||||
UPDATE exclusive_connection_leases
|
||||
SET state = 'INVALIDATED',
|
||||
ended_at = COALESCE(ended_at, CURRENT_TIMESTAMP),
|
||||
end_reason = 'AUTHORIZATION_CHANGED'
|
||||
WHERE id = NEW.id AND state = 'ACTIVE';
|
||||
END;
|
||||
@@ -34,6 +34,7 @@ import {
|
||||
isMatchingOauthIdentity,
|
||||
} from "./webSessionDedup";
|
||||
import { pickCodexConnectionForUser } from "@/lib/oauth/utils/codexConnectionSelection";
|
||||
import { isMicrosoftDesignerWebRetiredProviderId } from "@/shared/constants/designerWebRetirement";
|
||||
import { reconcileCodexUsageHistory } from "./providers/usageIdentityReconciliation";
|
||||
|
||||
/**
|
||||
@@ -595,6 +596,10 @@ export async function createProviderConnection(data: JsonRecord) {
|
||||
_updateConnectionRow(db, existingId, encryptConnectionFields(persistence));
|
||||
})();
|
||||
backupDbFile("pre-write");
|
||||
if (isMicrosoftDesignerWebRetiredProviderId(merged.provider)) {
|
||||
invalidateDbCache("connections");
|
||||
return getProviderConnectionById(existingId);
|
||||
}
|
||||
return withNullableRateLimitOverrides(
|
||||
withNullableQuotaWindowThresholds(
|
||||
withNullableMaxConcurrent(cleanNulls(merged), merged),
|
||||
@@ -722,6 +727,10 @@ export async function createProviderConnection(data: JsonRecord) {
|
||||
backupDbFile("pre-write");
|
||||
invalidateDbCache("connections"); // Bust connections read cache
|
||||
|
||||
if (isMicrosoftDesignerWebRetiredProviderId(data.provider)) {
|
||||
return getProviderConnectionById(String(connection.id));
|
||||
}
|
||||
|
||||
return withNullableRateLimitOverrides(
|
||||
withNullableQuotaWindowThresholds(
|
||||
withNullableMaxConcurrent(cleanNulls(connection), connection),
|
||||
@@ -967,6 +976,10 @@ export async function updateProviderConnection(id: string, data: JsonRecord) {
|
||||
reorderConnections(db, providerId);
|
||||
}
|
||||
|
||||
if (isMicrosoftDesignerWebRetiredProviderId(merged.provider)) {
|
||||
return getProviderConnectionById(id);
|
||||
}
|
||||
|
||||
return withNullableRateLimitOverrides(
|
||||
withNullableQuotaWindowThresholds(
|
||||
withNullableMaxConcurrent(cleanNulls(merged), merged),
|
||||
|
||||
@@ -19,6 +19,7 @@ import { resolveComboTargets } from "@omniroute/open-sse/services/combo.ts";
|
||||
|
||||
import { getComboByName, getCombos } from "@/lib/db/combos";
|
||||
import { getCachedProviderNodes } from "@/lib/localDb";
|
||||
import { assertMicrosoftDesignerWebProviderAvailable } from "@/shared/constants/designerWebRetirement";
|
||||
|
||||
/**
|
||||
* Rewrite a `prefix/model` custom image model to its internal `<nodeId>/<model>` form.
|
||||
@@ -32,6 +33,7 @@ export async function resolveImageModelPrefix(modelStr: string): Promise<string>
|
||||
if (slash <= 0) return modelStr;
|
||||
|
||||
const prefixPart = modelStr.slice(0, slash);
|
||||
assertMicrosoftDesignerWebProviderAvailable(prefixPart);
|
||||
const rest = modelStr.slice(slash + 1);
|
||||
if (!rest) return modelStr;
|
||||
|
||||
@@ -75,6 +77,8 @@ export async function resolveSingleImageComboTarget(name: string): Promise<strin
|
||||
*/
|
||||
export async function resolveImageRouteModel(modelStr: string): Promise<string> {
|
||||
if (typeof modelStr !== "string" || !modelStr.trim()) return modelStr;
|
||||
const slash = modelStr.indexOf("/");
|
||||
assertMicrosoftDesignerWebProviderAvailable(slash > 0 ? modelStr.slice(0, slash) : modelStr);
|
||||
const parsedModel = parseImageModel(modelStr);
|
||||
const hasSlash = modelStr.includes("/");
|
||||
|
||||
|
||||
33
src/shared/constants/designerWebRetirement.ts
Normal file
33
src/shared/constants/designerWebRetirement.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
export const RETIRED_MICROSOFT_DESIGNER_WEB_PROVIDER_IDS: ReadonlySet<string> = new Set([
|
||||
"microsoft-designer-web",
|
||||
"msdesigner",
|
||||
]);
|
||||
|
||||
export const MICROSOFT_DESIGNER_WEB_RETIRED_MESSAGE =
|
||||
"Provider has been retired from OmniRoute runtime.";
|
||||
|
||||
function normalizeProviderId(providerId: unknown): string {
|
||||
return typeof providerId === "string" ? providerId.trim().toLowerCase() : "";
|
||||
}
|
||||
|
||||
export function isMicrosoftDesignerWebRetiredProviderId(providerId: unknown): boolean {
|
||||
return RETIRED_MICROSOFT_DESIGNER_WEB_PROVIDER_IDS.has(normalizeProviderId(providerId));
|
||||
}
|
||||
|
||||
export function assertMicrosoftDesignerWebProviderAvailable(providerId: unknown): void {
|
||||
if (!isMicrosoftDesignerWebRetiredProviderId(providerId)) return;
|
||||
|
||||
const error = new Error(MICROSOFT_DESIGNER_WEB_RETIRED_MESSAGE);
|
||||
(error as Error & { status?: number }).status = 410;
|
||||
throw error;
|
||||
}
|
||||
|
||||
export function isMicrosoftDesignerWebProviderRetiredError(
|
||||
error: unknown
|
||||
): error is Error & { status: 410 } {
|
||||
return (
|
||||
error instanceof Error &&
|
||||
(error as Error & { status?: number }).status === 410 &&
|
||||
error.message === MICROSOFT_DESIGNER_WEB_RETIRED_MESSAGE
|
||||
);
|
||||
}
|
||||
@@ -156,19 +156,6 @@ export const WEB_COOKIE_PROVIDERS = {
|
||||
subscriptionRisk: true,
|
||||
riskNoticeVariant: "webCookie",
|
||||
},
|
||||
"microsoft-designer-web": {
|
||||
id: "microsoft-designer-web",
|
||||
alias: "msdesigner",
|
||||
name: "Microsoft Designer (Image Generation)",
|
||||
icon: "auto_awesome",
|
||||
color: "#0078D4",
|
||||
textIcon: "MSD",
|
||||
website: "https://designer.microsoft.com",
|
||||
authHint:
|
||||
"Sign in at designer.microsoft.com, then open DevTools → Network, generate an image, and find the request to DallE.ashx?action=GetDallEImagesCogSci. Copy the value of its Authorization: Bearer header (the access_token — no 'Bearer ' prefix). The token is short-lived; this is an unofficial, reverse-engineered integration.",
|
||||
subscriptionRisk: true,
|
||||
riskNoticeVariant: "webCookie",
|
||||
},
|
||||
"t3-web": {
|
||||
id: "t3-web",
|
||||
alias: "t3chat",
|
||||
|
||||
@@ -11,15 +11,21 @@
|
||||
// provider (tokenrouter bug: "No active credentials for provider:
|
||||
// tokenrouter" despite a fully configured compatible node).
|
||||
//
|
||||
// Semantics (mirror the original inline runtime guard exactly):
|
||||
// - REGISTRY entry ids + aliases only. Manual alias ids outside REGISTRY
|
||||
// (xiaomi/llamacpp/aq) do NOT intercept nodes at runtime and are therefore
|
||||
// deliberately NOT reserved — including them would cause false-positive
|
||||
// rejections.
|
||||
// - Case-sensitive: mixed-case input like "TokenRouter" does not collide with
|
||||
// the runtime lookup (`Set.has` is exact-match), so it stays allowed.
|
||||
// Semantics:
|
||||
// - Live REGISTRY entry ids + aliases, plus exact retired provider ids that
|
||||
// must remain unavailable after their registry entries are removed. Manual
|
||||
// aliases outside REGISTRY (xiaomi/llamacpp/aq) do NOT intercept nodes at
|
||||
// runtime and are therefore deliberately NOT reserved — including them would
|
||||
// cause false-positive rejections.
|
||||
// - Live REGISTRY entries remain case-sensitive. Retired ids use their retirement
|
||||
// normalizer (trim + lowercase), so casing cannot revive a removed provider.
|
||||
import { REGISTRY } from "@omniroute/open-sse/config/providerRegistry.ts";
|
||||
|
||||
import {
|
||||
isMicrosoftDesignerWebRetiredProviderId,
|
||||
RETIRED_MICROSOFT_DESIGNER_WEB_PROVIDER_IDS,
|
||||
} from "@/shared/constants/designerWebRetirement";
|
||||
|
||||
let _reserved: Set<string> | null = null;
|
||||
|
||||
function buildReservedProviderPrefixes(): Set<string> {
|
||||
@@ -29,13 +35,16 @@ function buildReservedProviderPrefixes(): Set<string> {
|
||||
if (entry?.id) reserved.add(entry.id);
|
||||
if (entry?.alias) reserved.add(entry.alias);
|
||||
}
|
||||
for (const providerId of RETIRED_MICROSOFT_DESIGNER_WEB_PROVIDER_IDS) {
|
||||
reserved.add(providerId);
|
||||
}
|
||||
_reserved = reserved;
|
||||
return reserved;
|
||||
}
|
||||
|
||||
/**
|
||||
* All reserved provider prefixes (REGISTRY ids + aliases). Built lazily so the
|
||||
* registry is only walked once per process.
|
||||
* All canonical reserved provider prefixes (REGISTRY ids + aliases + retired ids).
|
||||
* Built lazily so the registry is only walked once per process.
|
||||
*/
|
||||
export function getReservedProviderPrefixes(): ReadonlySet<string> {
|
||||
return buildReservedProviderPrefixes();
|
||||
@@ -58,7 +67,10 @@ export const RESERVED_PROVIDER_PREFIXES: ReadonlySet<string> = getReservedProvid
|
||||
* reserved (mirrors the runtime guard's typeof check).
|
||||
*/
|
||||
export function isReservedProviderPrefix(value: unknown): boolean {
|
||||
return typeof value === "string" && buildReservedProviderPrefixes().has(value);
|
||||
return (
|
||||
(typeof value === "string" && buildReservedProviderPrefixes().has(value)) ||
|
||||
isMicrosoftDesignerWebRetiredProviderId(value)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -163,13 +163,6 @@ export const WEB_SESSION_CREDENTIAL_REQUIREMENTS = {
|
||||
acceptsFullCookieHeader: false,
|
||||
storageKeys: ["token", "access_token", "accessToken"],
|
||||
},
|
||||
"microsoft-designer-web": {
|
||||
kind: "token",
|
||||
credentialName: "access_token",
|
||||
placeholder: "access_token=... (Authorization: Bearer header from the DallE.ashx request)",
|
||||
acceptsFullCookieHeader: false,
|
||||
storageKeys: ["token", "access_token", "accessToken"],
|
||||
},
|
||||
"copilot-m365-web": {
|
||||
kind: "token",
|
||||
credentialName: "access_token + chathubPath",
|
||||
|
||||
@@ -135,6 +135,7 @@ import { generateRequestId } from "../../shared/utils/requestId";
|
||||
import { logAuditEvent } from "../../lib/compliance/index";
|
||||
import { enforceApiKeyPolicy } from "../../shared/utils/apiKeyPolicy";
|
||||
import { hasProviderQuotaBypassScope } from "../../shared/constants/apiKeyPolicyScopes";
|
||||
import { isMicrosoftDesignerWebProviderRetiredError } from "../../shared/constants/designerWebRetirement";
|
||||
import { cloneBoundedForLog } from "@omniroute/open-sse/utils/requestLogger.ts";
|
||||
import { handleInternalUsageCommand } from "@/lib/usage/internalUsageCommand";
|
||||
import {
|
||||
@@ -948,7 +949,15 @@ async function handleChatImplementation(
|
||||
// prefix may differ from the credential provider ID (e.g. model
|
||||
// "xiaomi/mimo-v2-flash" resolves to provider "xiaomi" but the combo
|
||||
// target specifies providerId: "opengate" for credential lookup).
|
||||
const modelInfo = await getModelInfo(modelString);
|
||||
let modelInfo;
|
||||
try {
|
||||
modelInfo = await getModelInfo(modelString);
|
||||
} catch (error) {
|
||||
// Persisted explicit combos may still reference the retired provider. Treat
|
||||
// that target as unavailable so priority/fallback strategies can continue.
|
||||
if (isMicrosoftDesignerWebProviderRetiredError(error)) return false;
|
||||
throw error;
|
||||
}
|
||||
// Apply the same prefix-override guard as handleSingleModelChat:
|
||||
// if providerId is just the prefix already in the model string, use
|
||||
// the fully-resolved modelInfo.provider for a precise credential check.
|
||||
|
||||
@@ -35,6 +35,7 @@ import {
|
||||
} from "../../shared/utils/circuitBreaker";
|
||||
import { classify429FromError, type FailureKind } from "../../shared/utils/classify429";
|
||||
import { resolveUseUpstream429BreakerHints } from "../../shared/utils/providerHints";
|
||||
import { isMicrosoftDesignerWebProviderRetiredError } from "../../shared/constants/designerWebRetirement";
|
||||
|
||||
import { logProxyEvent } from "../../lib/proxyLogger";
|
||||
import { logTranslationEvent } from "../../lib/translatorEvents";
|
||||
@@ -120,7 +121,15 @@ export async function resolveModelOrError(
|
||||
endpointPath: string = "",
|
||||
requestHeaders: Record<string, unknown> | null | undefined = null
|
||||
) {
|
||||
const modelInfo = await getModelInfo(modelStr);
|
||||
let modelInfo;
|
||||
try {
|
||||
modelInfo = await getModelInfo(modelStr);
|
||||
} catch (error) {
|
||||
if (isMicrosoftDesignerWebProviderRetiredError(error)) {
|
||||
return { error: errorResponse(HTTP_STATUS.GONE, error.message) };
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
const sourceFormat = detectFormatFromEndpoint(body, endpointPath);
|
||||
|
||||
if (
|
||||
|
||||
@@ -32,6 +32,7 @@ import {
|
||||
import { buildJinaEnvCredentials } from "@/lib/providers/jina";
|
||||
import { buildGeminiEnvCredentials } from "@/lib/providers/gemini";
|
||||
import { toNumber } from "@/shared/utils/numeric";
|
||||
import { isMicrosoftDesignerWebRetiredProviderId } from "@/shared/constants/designerWebRetirement";
|
||||
import {
|
||||
createLazyConnectionView,
|
||||
toProviderConnection,
|
||||
@@ -1283,6 +1284,12 @@ export async function getProviderCredentials(
|
||||
requestedModel: string | null = null,
|
||||
options: CredentialSelectionOptions = {}
|
||||
) {
|
||||
if (isMicrosoftDesignerWebRetiredProviderId(provider)) {
|
||||
invalidateManagedLease(options, "AUTHORIZATION_CHANGED");
|
||||
log.warn("AUTH", "Retired provider credential selection denied");
|
||||
return null;
|
||||
}
|
||||
|
||||
const selectionLock = options._leaseRetryWithLockHeld
|
||||
? null
|
||||
: createSelectionLock(getSelectionMutexKey(provider, options));
|
||||
|
||||
@@ -21,6 +21,7 @@ import { getLearnedReasoningEffortForModel } from "@omniroute/open-sse/services/
|
||||
import { REGISTRY } from "@omniroute/open-sse/config/providerRegistry.ts";
|
||||
import { getRegisteredProviderEffortBaseModelId } from "@omniroute/open-sse/utils/registeredEffortVariants.ts";
|
||||
import { getReservedProviderPrefixes } from "@/shared/constants/reservedProviderPrefixes";
|
||||
import { assertMicrosoftDesignerWebProviderAvailable } from "@/shared/constants/designerWebRetirement";
|
||||
|
||||
export { parseModel, stripContextWindowSuffix };
|
||||
|
||||
@@ -425,6 +426,10 @@ export async function getModelInfo(modelStr) {
|
||||
const parsed = parseModel(modelStr);
|
||||
const { extendedContext } = parsed;
|
||||
|
||||
// Fail closed before a custom compatible node or stripModelPrefix can reinterpret
|
||||
// an exact retired provider id/alias as an unrelated live provider.
|
||||
assertMicrosoftDesignerWebProviderAvailable(parsed.providerAlias || parsed.provider);
|
||||
|
||||
const attachRuntimeModelMeta = async (info: any) => {
|
||||
if (!info?.provider || !info?.model) return info;
|
||||
|
||||
|
||||
@@ -266,7 +266,6 @@
|
||||
"tests/unit/mcp-connect-scope.test.ts",
|
||||
"tests/unit/memory-embedding-remote.test.ts",
|
||||
"tests/unit/memory-embedding-transformers.test.ts",
|
||||
"tests/unit/microsoft-designer-web-6672.test.ts",
|
||||
"tests/unit/middleware-header-strip-5849.test.ts",
|
||||
"tests/unit/middleware-hooks-error-sanitization.test.ts",
|
||||
"tests/unit/model-catalog-runtime-invalidation.test.ts",
|
||||
|
||||
@@ -445,11 +445,6 @@
|
||||
"configSource": "<custom-config>",
|
||||
"provider": "lmarena"
|
||||
},
|
||||
"microsoft-designer-web": {
|
||||
"className": "MicrosoftDesignerWebExecutor",
|
||||
"configSource": "<custom-config>",
|
||||
"provider": "microsoft-designer-web"
|
||||
},
|
||||
"moonshot": {
|
||||
"className": "MoonshotExecutor",
|
||||
"configSource": "moonshot",
|
||||
@@ -460,11 +455,6 @@
|
||||
"configSource": "<custom-config>",
|
||||
"provider": "muse-spark-web"
|
||||
},
|
||||
"msdesigner": {
|
||||
"className": "MicrosoftDesignerWebExecutor",
|
||||
"configSource": "<custom-config>",
|
||||
"provider": "microsoft-designer-web"
|
||||
},
|
||||
"muse-spark-web": {
|
||||
"className": "MuseSparkWebExecutor",
|
||||
"configSource": "<custom-config>",
|
||||
@@ -721,6 +711,6 @@
|
||||
"provider": "zai-web"
|
||||
}
|
||||
},
|
||||
"keyCount": 144,
|
||||
"keyCount": 142,
|
||||
"sharedInstances": []
|
||||
}
|
||||
|
||||
@@ -17,13 +17,11 @@ process.env.DATA_DIR = testDataDir;
|
||||
// Dynamic imports AFTER DATA_DIR is set so core.ts picks up the temp path.
|
||||
const coreDb = await import("../../src/lib/db/core.ts");
|
||||
const upstreamProxyDb = await import("../../src/lib/db/upstreamProxy.ts");
|
||||
const { resolveExecutorWithProxy } = await import(
|
||||
"../../open-sse/handlers/chatCore/executorProxy.ts"
|
||||
);
|
||||
const { resolveExecutorWithProxy } =
|
||||
await import("../../open-sse/handlers/chatCore/executorProxy.ts");
|
||||
const { getExecutor } = await import("../../open-sse/executors/index.ts");
|
||||
const { clearUpstreamProxyConfigCache } = await import(
|
||||
"../../open-sse/handlers/chatCore/comboContextCache.ts"
|
||||
);
|
||||
const { clearUpstreamProxyConfigCache } =
|
||||
await import("../../open-sse/handlers/chatCore/comboContextCache.ts");
|
||||
|
||||
before(async () => {
|
||||
await coreDb.ensureDbInitialized();
|
||||
@@ -137,3 +135,22 @@ test("connection override wins over provider mode 'fallback'", async () => {
|
||||
// Connection override short-circuits to the passthrough executor, not the fallback wrapper.
|
||||
assert.equal(exec, await getExecutor("cliproxyapi"));
|
||||
});
|
||||
|
||||
test("connection proxy overrides cannot bypass Microsoft Designer retirement", async () => {
|
||||
for (const providerId of ["microsoft-designer-web", " MSDESIGNER\t"]) {
|
||||
for (const providerSpecificData of [
|
||||
{ cliproxyapiMode: "claude-native" },
|
||||
{ darioMode: "claude-native" },
|
||||
]) {
|
||||
await assert.rejects(
|
||||
() => resolveExecutorWithProxy(providerId, undefined, providerSpecificData),
|
||||
(error: unknown) => {
|
||||
assert.ok(error instanceof Error);
|
||||
assert.equal((error as Error & { status?: number }).status, 410);
|
||||
assert.equal(error.message, "Provider has been retired from OmniRoute runtime.");
|
||||
return true;
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -14,6 +14,7 @@ process.env.DATA_DIR = TEST_DATA_DIR;
|
||||
|
||||
const core = await import("../../src/lib/db/core.ts");
|
||||
const providersDb = await import("../../src/lib/db/providers.ts");
|
||||
const modelsDb = await import("../../src/lib/db/models.ts");
|
||||
const combo = await import("../../open-sse/services/combo.ts");
|
||||
const providerModels = await import("../../open-sse/config/providerModels.ts");
|
||||
|
||||
@@ -209,3 +210,36 @@ test("expandAutoComboCandidatePool does not duplicate an already-present modelSt
|
||||
assert.equal(matches[0].connectionId, "conn-1");
|
||||
assert.equal(matches[0].weight, 5);
|
||||
});
|
||||
|
||||
test("expandAutoComboCandidatePool excludes trigger-bypassed Microsoft Designer providers", async () => {
|
||||
await core.ensureDbInitialized();
|
||||
const db = core.getDbInstance();
|
||||
db.exec("DROP TRIGGER IF EXISTS trg_retire_microsoft_designer_web_provider_insert");
|
||||
db.exec("DROP TRIGGER IF EXISTS trg_retire_microsoft_designer_web_provider_update");
|
||||
|
||||
for (const provider of ["microsoft-designer-web", "msdesigner", "openai"]) {
|
||||
await providersDb.createProviderConnection({
|
||||
provider,
|
||||
authType: "apikey",
|
||||
name: `${provider}-trigger-bypass`,
|
||||
apiKey: `sk-${provider}-test`,
|
||||
defaultModel: provider === "openai" ? "gpt-4o-mini" : "dall-e-3",
|
||||
isActive: true,
|
||||
});
|
||||
if (provider !== "openai") {
|
||||
await modelsDb.addCustomModel(provider, "designer-chat-bypass", "Bypass model");
|
||||
}
|
||||
}
|
||||
|
||||
const expanded = await combo.expandAutoComboCandidatePool([], { config: {} });
|
||||
|
||||
assert.equal(
|
||||
expanded.some((target) => ["microsoft-designer-web", "msdesigner"].includes(target.provider)),
|
||||
false
|
||||
);
|
||||
assert.equal(
|
||||
expanded.some((target) => target.provider === "openai"),
|
||||
true,
|
||||
"supported active providers must remain expandable"
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,67 +0,0 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
const { handleDesignerWebImageGeneration } = await import(
|
||||
"../../open-sse/handlers/imageGeneration/providers/designerWeb.ts"
|
||||
);
|
||||
|
||||
/**
|
||||
* `stepDesignerWebPoll` classifies an unrecognized upstream body as a terminal
|
||||
* 502 — the "empty" arm of the step union, alongside pending / ready / upstream
|
||||
* failure.
|
||||
*
|
||||
* `microsoft-designer-web-6672.test.ts` covers every other arm end-to-end
|
||||
* (400 no prompt, 401 no token, immediate ready, poll-then-ready, non-OK
|
||||
* upstream, 504 timeout) but tests "empty" only at the parser level
|
||||
* (`parseDesignerWebResponse: unrecognized shape is 'empty'`) — it never drives
|
||||
* the handler with one, so the 502 the handler synthesizes from it was
|
||||
* unasserted.
|
||||
*/
|
||||
|
||||
function jsonResponse(status: number, body: unknown) {
|
||||
return {
|
||||
ok: status >= 200 && status < 300,
|
||||
status,
|
||||
json: async () => body,
|
||||
text: async () => JSON.stringify(body),
|
||||
} as Response;
|
||||
}
|
||||
|
||||
const BASE = {
|
||||
model: "dall-e-3",
|
||||
provider: "microsoft-designer-web",
|
||||
providerConfig: { baseUrl: "https://designerapp.officeapps.live.com/designerapp/DallE.ashx" },
|
||||
credentials: { apiKey: "tok-abc" },
|
||||
};
|
||||
|
||||
test("a 200 with an unrecognized body is a terminal 502, not a retry", async () => {
|
||||
let calls = 0;
|
||||
const result = await handleDesignerWebImageGeneration({
|
||||
...BASE,
|
||||
body: { prompt: "a cat astronaut", timeout_ms: 5_000, poll_interval_ms: 1 },
|
||||
fetchImpl: async () => {
|
||||
calls += 1;
|
||||
return jsonResponse(200, { unexpected: true });
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(result.success, false);
|
||||
assert.equal(result.status, 502, "an unparseable success body is a bad-gateway, not a timeout");
|
||||
assert.match(String(result.error), /did not contain image data or polling metadata/);
|
||||
assert.equal(calls, 1, "the empty classification is terminal — it must not keep polling");
|
||||
});
|
||||
|
||||
test("a 200 with neither images nor polling metadata does not fall through to 504", async () => {
|
||||
// The distinction matters: 502 says "the upstream answered with something we
|
||||
// cannot use", 504 says "the upstream never finished". A timeout_ms generous
|
||||
// enough to allow several polls proves the 502 came from classification, not
|
||||
// from the deadline.
|
||||
const result = await handleDesignerWebImageGeneration({
|
||||
...BASE,
|
||||
body: { prompt: "a cat astronaut", timeout_ms: 10_000, poll_interval_ms: 1 },
|
||||
fetchImpl: async () => jsonResponse(200, { polling_response: {} }),
|
||||
});
|
||||
|
||||
assert.equal(result.status, 502);
|
||||
assert.notEqual(result.status, 504);
|
||||
});
|
||||
@@ -1,216 +0,0 @@
|
||||
import { test } from "node:test";
|
||||
import assert from "node:assert";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { resolvePublicCred } from "../../open-sse/utils/publicCreds.ts";
|
||||
import {
|
||||
buildDesignerWebHeaders,
|
||||
buildDesignerWebFormBody,
|
||||
mapDesignerWebImageSize,
|
||||
parseDesignerWebResponse,
|
||||
handleDesignerWebImageGeneration,
|
||||
} from "../../open-sse/handlers/imageGeneration/providers/designerWeb.ts";
|
||||
import { WEB_COOKIE_PROVIDERS } from "../../src/shared/constants/providers/web-cookie.ts";
|
||||
import { IMAGE_PROVIDERS } from "../../open-sse/config/imageRegistry.ts";
|
||||
|
||||
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
|
||||
|
||||
// --- Registry entries -------------------------------------------------
|
||||
|
||||
test("microsoft-designer-web is registered in WEB_COOKIE_PROVIDERS with a webCookie risk notice", () => {
|
||||
const entry = (WEB_COOKIE_PROVIDERS as Record<string, unknown>)["microsoft-designer-web"];
|
||||
assert.ok(entry, "microsoft-designer-web must exist in WEB_COOKIE_PROVIDERS");
|
||||
assert.equal(entry.id, "microsoft-designer-web");
|
||||
assert.equal(entry.subscriptionRisk, true);
|
||||
assert.equal(entry.riskNoticeVariant, "webCookie");
|
||||
assert.match(entry.website, /designer\.microsoft\.com/);
|
||||
});
|
||||
|
||||
test("microsoft-designer-web is registered in IMAGE_PROVIDERS with the designer-web format", () => {
|
||||
const entry = (IMAGE_PROVIDERS as Record<string, unknown>)["microsoft-designer-web"];
|
||||
assert.ok(entry, "microsoft-designer-web must exist in IMAGE_PROVIDERS");
|
||||
assert.equal(entry.format, "designer-web");
|
||||
assert.match(entry.baseUrl, /designerapp\.officeapps\.live\.com/);
|
||||
assert.ok(Array.isArray(entry.models) && entry.models.length > 0);
|
||||
});
|
||||
|
||||
// --- Public credential (Hard Rule #11) ---------------------------------
|
||||
|
||||
test("microsoft_designer_client_id embedded default decodes to the public Designer ClientId", () => {
|
||||
assert.equal(resolvePublicCred("microsoft_designer_client_id"), "b5c2664a-7e9b-4a7a-8c9a-cd2c52dcf621");
|
||||
});
|
||||
|
||||
test("designerWeb.ts never embeds the raw ClientId literal (Hard Rule #11)", () => {
|
||||
const src = fs.readFileSync(
|
||||
path.join(repoRoot, "open-sse/handlers/imageGeneration/providers/designerWeb.ts"),
|
||||
"utf8"
|
||||
) as string;
|
||||
assert.ok(
|
||||
!src.includes("b5c2664a-7e9b-4a7a-8c9a-cd2c52dcf621"),
|
||||
"designerWeb.ts must resolve the ClientId via resolvePublicCred(), not a string literal"
|
||||
);
|
||||
assert.ok(
|
||||
src.includes('resolvePublicCred("microsoft_designer_client_id")'),
|
||||
"designerWeb.ts must call resolvePublicCred for the Designer ClientId"
|
||||
);
|
||||
});
|
||||
|
||||
// --- Pure helpers --------------------------------------------------------
|
||||
|
||||
test("mapDesignerWebImageSize buckets sizes into square/landscape/portrait", () => {
|
||||
assert.equal(mapDesignerWebImageSize("1024x1024"), "1_1");
|
||||
assert.equal(mapDesignerWebImageSize("1792x1024"), "16_9");
|
||||
assert.equal(mapDesignerWebImageSize("1024x1792"), "9_16");
|
||||
assert.equal(mapDesignerWebImageSize(undefined), "1_1");
|
||||
assert.equal(mapDesignerWebImageSize("garbage"), "1_1");
|
||||
});
|
||||
|
||||
test("buildDesignerWebHeaders sets Bearer auth + the public ClientId + per-request SessionId/UserId", () => {
|
||||
const headers = buildDesignerWebHeaders({ accessToken: "tok-123" });
|
||||
assert.equal(headers.Authorization, "Bearer tok-123");
|
||||
assert.equal(headers.ClientId, "b5c2664a-7e9b-4a7a-8c9a-cd2c52dcf621");
|
||||
assert.ok(headers.SessionId && headers.SessionId.length > 0);
|
||||
assert.ok(headers.UserId && headers.UserId.length > 0);
|
||||
assert.equal(headers["Content-Type"], "application/x-www-form-urlencoded");
|
||||
});
|
||||
|
||||
test("buildDesignerWebFormBody encodes prompt, mapped size, fixed batch size, and a seed", () => {
|
||||
const form = buildDesignerWebFormBody("a cat astronaut", "1792x1024");
|
||||
assert.equal(form.get("dalle-caption"), "a cat astronaut");
|
||||
assert.equal(form.get("dalle-image-size"), "16_9");
|
||||
assert.equal(form.get("dalle-batch-size"), "4");
|
||||
assert.ok(Number(form.get("dalle-seed")) >= 0);
|
||||
});
|
||||
|
||||
test("parseDesignerWebResponse: ready state extracts thumbnail image URLs", () => {
|
||||
const parsed = parseDesignerWebResponse({
|
||||
image_urls_thumbnail: [{ ImageUrl: "https://example.com/a.png" }, { ImageUrl: "https://example.com/b.png" }],
|
||||
});
|
||||
assert.equal(parsed.status, "ready");
|
||||
assert.deepEqual(parsed.imageUrls, ["https://example.com/a.png", "https://example.com/b.png"]);
|
||||
});
|
||||
|
||||
test("parseDesignerWebResponse: pending state surfaces the polling interval", () => {
|
||||
const parsed = parseDesignerWebResponse({
|
||||
polling_response: { polling_meta_data: { poll_interval: 1500 } },
|
||||
});
|
||||
assert.equal(parsed.status, "pending");
|
||||
assert.equal(parsed.pollIntervalMs, 1500);
|
||||
assert.deepEqual(parsed.imageUrls, []);
|
||||
});
|
||||
|
||||
test("parseDesignerWebResponse: unrecognized shape is 'empty'", () => {
|
||||
const parsed = parseDesignerWebResponse({ unexpected: true });
|
||||
assert.equal(parsed.status, "empty");
|
||||
});
|
||||
|
||||
// --- Handler (mocked fetch — no live Designer session required) ---------
|
||||
|
||||
function jsonResponse(status: number, body: unknown) {
|
||||
return {
|
||||
ok: status >= 200 && status < 300,
|
||||
status,
|
||||
json: async () => body,
|
||||
text: async () => JSON.stringify(body),
|
||||
} as Response;
|
||||
}
|
||||
|
||||
test("handleDesignerWebImageGeneration returns 400 when prompt is missing", async () => {
|
||||
const result = await handleDesignerWebImageGeneration({
|
||||
model: "dall-e-3",
|
||||
provider: "microsoft-designer-web",
|
||||
providerConfig: { baseUrl: "https://designerapp.officeapps.live.com/designerapp/DallE.ashx" },
|
||||
body: {},
|
||||
credentials: { apiKey: "tok" },
|
||||
fetchImpl: async () => jsonResponse(200, {}),
|
||||
});
|
||||
assert.equal(result.success, false);
|
||||
assert.equal(result.status, 400);
|
||||
});
|
||||
|
||||
test("handleDesignerWebImageGeneration returns 401 when access_token is missing", async () => {
|
||||
const result = await handleDesignerWebImageGeneration({
|
||||
model: "dall-e-3",
|
||||
provider: "microsoft-designer-web",
|
||||
providerConfig: { baseUrl: "https://designerapp.officeapps.live.com/designerapp/DallE.ashx" },
|
||||
body: { prompt: "a cat" },
|
||||
credentials: {},
|
||||
fetchImpl: async () => jsonResponse(200, {}),
|
||||
});
|
||||
assert.equal(result.success, false);
|
||||
assert.equal(result.status, 401);
|
||||
});
|
||||
|
||||
test("handleDesignerWebImageGeneration succeeds immediately when the first response is already ready", async () => {
|
||||
let calls = 0;
|
||||
const result = await handleDesignerWebImageGeneration({
|
||||
model: "dall-e-3",
|
||||
provider: "microsoft-designer-web",
|
||||
providerConfig: { baseUrl: "https://designerapp.officeapps.live.com/designerapp/DallE.ashx" },
|
||||
body: { prompt: "a cat astronaut", size: "1024x1024" },
|
||||
credentials: { apiKey: "tok-abc" },
|
||||
fetchImpl: async () => {
|
||||
calls += 1;
|
||||
return jsonResponse(200, {
|
||||
image_urls_thumbnail: [{ ImageUrl: "https://example.com/ready.png" }],
|
||||
});
|
||||
},
|
||||
});
|
||||
assert.equal(calls, 1);
|
||||
assert.equal(result.success, true);
|
||||
assert.equal(result.data.data[0].url, "https://example.com/ready.png");
|
||||
});
|
||||
|
||||
test("handleDesignerWebImageGeneration polls until ready, bounded by poll_interval_ms/timeout_ms", async () => {
|
||||
let calls = 0;
|
||||
const result = await handleDesignerWebImageGeneration({
|
||||
model: "dall-e-3",
|
||||
provider: "microsoft-designer-web",
|
||||
providerConfig: { baseUrl: "https://designerapp.officeapps.live.com/designerapp/DallE.ashx" },
|
||||
body: { prompt: "a cat astronaut", timeout_ms: 5000, poll_interval_ms: 1 },
|
||||
credentials: { apiKey: "tok-abc" },
|
||||
fetchImpl: async () => {
|
||||
calls += 1;
|
||||
if (calls < 3) {
|
||||
return jsonResponse(200, {
|
||||
polling_response: { polling_meta_data: { poll_interval: 1 } },
|
||||
});
|
||||
}
|
||||
return jsonResponse(200, {
|
||||
image_urls_thumbnail: [{ ImageUrl: "https://example.com/final.png" }],
|
||||
});
|
||||
},
|
||||
});
|
||||
assert.equal(calls, 3);
|
||||
assert.equal(result.success, true);
|
||||
assert.equal(result.data.data[0].url, "https://example.com/final.png");
|
||||
});
|
||||
|
||||
test("handleDesignerWebImageGeneration surfaces a sanitized error on a non-OK upstream response", async () => {
|
||||
const result = await handleDesignerWebImageGeneration({
|
||||
model: "dall-e-3",
|
||||
provider: "microsoft-designer-web",
|
||||
providerConfig: { baseUrl: "https://designerapp.officeapps.live.com/designerapp/DallE.ashx" },
|
||||
body: { prompt: "a cat astronaut" },
|
||||
credentials: { apiKey: "expired-token" },
|
||||
fetchImpl: async () => jsonResponse(401, { error: "invalid_token" }),
|
||||
});
|
||||
assert.equal(result.success, false);
|
||||
assert.equal(result.status, 401);
|
||||
assert.ok(!String(result.error).includes(" at "), "error must not leak a stack trace");
|
||||
});
|
||||
|
||||
test("handleDesignerWebImageGeneration times out cleanly when the upstream never becomes ready", async () => {
|
||||
const result = await handleDesignerWebImageGeneration({
|
||||
model: "dall-e-3",
|
||||
provider: "microsoft-designer-web",
|
||||
providerConfig: { baseUrl: "https://designerapp.officeapps.live.com/designerapp/DallE.ashx" },
|
||||
body: { prompt: "a cat astronaut", timeout_ms: 5, poll_interval_ms: 1 },
|
||||
credentials: { apiKey: "tok-abc" },
|
||||
fetchImpl: async () =>
|
||||
jsonResponse(200, { polling_response: { polling_meta_data: { poll_interval: 1 } } }),
|
||||
});
|
||||
assert.equal(result.success, false);
|
||||
assert.equal(result.status, 504);
|
||||
});
|
||||
@@ -0,0 +1,44 @@
|
||||
import assert from "node:assert/strict";
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import test from "node:test";
|
||||
|
||||
const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-designer-image-"));
|
||||
process.env.DATA_DIR = TEST_DATA_DIR;
|
||||
process.env.DISABLE_SQLITE_AUTO_BACKUP = "true";
|
||||
|
||||
const { handleImageGeneration } = await import("../../open-sse/handlers/imageGeneration.ts");
|
||||
|
||||
test.after(() => {
|
||||
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
test("image handler blocks exact retired providers before any upstream fetch", async () => {
|
||||
const originalFetch = globalThis.fetch;
|
||||
let fetchCalls = 0;
|
||||
globalThis.fetch = (async () => {
|
||||
fetchCalls += 1;
|
||||
throw new Error("unexpected upstream fetch");
|
||||
}) as typeof fetch;
|
||||
|
||||
try {
|
||||
for (const resolvedProvider of ["microsoft-designer-web", "MSDESIGNER"]) {
|
||||
const result = await handleImageGeneration({
|
||||
body: { model: `${resolvedProvider}/dall-e-3`, prompt: "test" },
|
||||
credentials: {
|
||||
apiKey: "test-key",
|
||||
providerSpecificData: { baseUrl: "https://compatible.invalid/v1" },
|
||||
},
|
||||
resolvedProvider,
|
||||
});
|
||||
|
||||
assert.equal(result.success, false, resolvedProvider);
|
||||
assert.equal(result.status, 410, resolvedProvider);
|
||||
assert.equal(result.error, "Provider has been retired from OmniRoute runtime.");
|
||||
}
|
||||
assert.equal(fetchCalls, 0);
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
});
|
||||
133
tests/unit/microsoft-designer-web-model-routing.test.ts
Normal file
133
tests/unit/microsoft-designer-web-model-routing.test.ts
Normal file
@@ -0,0 +1,133 @@
|
||||
import assert from "node:assert/strict";
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import test from "node:test";
|
||||
|
||||
const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-designer-routing-"));
|
||||
process.env.DATA_DIR = TEST_DATA_DIR;
|
||||
process.env.DISABLE_SQLITE_AUTO_BACKUP = "true";
|
||||
|
||||
const core = await import("../../src/lib/db/core.ts");
|
||||
const providersDb = await import("../../src/lib/db/providers.ts");
|
||||
const settingsDb = await import("../../src/lib/db/settings.ts");
|
||||
const { getModelInfo } = await import("../../src/sse/services/model.ts");
|
||||
const { resolveModelOrError } = await import("../../src/sse/handlers/chatHelpers.ts");
|
||||
const { resolveImageRouteModel } = await import("../../src/lib/images/imageRouteModel.ts");
|
||||
const { RESERVED_PROVIDER_PREFIXES, isReservedProviderPrefix } =
|
||||
await import("../../src/shared/constants/reservedProviderPrefixes.ts");
|
||||
const { createProviderNodeSchema, updateProviderNodeSchema } =
|
||||
await import("../../src/shared/validation/schemas.ts");
|
||||
|
||||
async function resetStorage(): Promise<void> {
|
||||
core.resetDbInstance();
|
||||
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
|
||||
fs.mkdirSync(TEST_DATA_DIR, { recursive: true });
|
||||
}
|
||||
|
||||
async function createCompatibleNode(id: string, prefix: string): Promise<void> {
|
||||
await providersDb.createProviderNode({
|
||||
id,
|
||||
type: "openai-compatible",
|
||||
name: `${prefix}-test-node`,
|
||||
prefix,
|
||||
apiType: "chat",
|
||||
baseUrl: "https://compatible.invalid/v1",
|
||||
chatPath: "/v1/chat/completions",
|
||||
modelsPath: "/v1/models",
|
||||
});
|
||||
}
|
||||
|
||||
function assertRetiredError(error: unknown): boolean {
|
||||
assert.ok(error instanceof Error);
|
||||
assert.equal((error as Error & { status?: number }).status, 410);
|
||||
assert.equal(error.message, "Provider has been retired from OmniRoute runtime.");
|
||||
return true;
|
||||
}
|
||||
|
||||
test.beforeEach(resetStorage);
|
||||
test.after(() => {
|
||||
core.resetDbInstance();
|
||||
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
test("retired Designer IDs remain reserved after leaving the live provider registry", () => {
|
||||
for (const providerId of [
|
||||
"microsoft-designer-web",
|
||||
"msdesigner",
|
||||
" MICROSOFT-DESIGNER-WEB\t",
|
||||
"\nMsDesigner ",
|
||||
]) {
|
||||
assert.equal(isReservedProviderPrefix(providerId), true, providerId);
|
||||
}
|
||||
assert.equal(RESERVED_PROVIDER_PREFIXES.has("microsoft-designer-web"), true);
|
||||
assert.equal(RESERVED_PROVIDER_PREFIXES.has("msdesigner"), true);
|
||||
assert.equal(isReservedProviderPrefix("microsoft-designer-web-preview"), false);
|
||||
});
|
||||
|
||||
test("provider-node schemas reject exact retired prefixes but preserve similar IDs", () => {
|
||||
for (const prefix of ["microsoft-designer-web", "MSDESIGNER"]) {
|
||||
const create = createProviderNodeSchema.safeParse({
|
||||
name: "Retired collision",
|
||||
prefix,
|
||||
apiType: "chat",
|
||||
baseUrl: "https://compatible.invalid/v1",
|
||||
});
|
||||
const update = updateProviderNodeSchema.safeParse({
|
||||
name: "Retired collision",
|
||||
prefix,
|
||||
baseUrl: "https://compatible.invalid/v1",
|
||||
});
|
||||
assert.equal(create.success, false, prefix);
|
||||
assert.equal(update.success, false, prefix);
|
||||
}
|
||||
|
||||
assert.equal(
|
||||
createProviderNodeSchema.safeParse({
|
||||
name: "Similar control",
|
||||
prefix: "microsoft-designer-web-preview",
|
||||
apiType: "chat",
|
||||
baseUrl: "https://compatible.invalid/v1",
|
||||
}).success,
|
||||
true
|
||||
);
|
||||
});
|
||||
|
||||
test("compatible-node remapping cannot erase a retired Designer prefix", async () => {
|
||||
await createCompatibleNode("openai-compatible-chat-designer-retired", "microsoft-designer-web");
|
||||
|
||||
await assert.rejects(() => getModelInfo("microsoft-designer-web/gpt-4o"), assertRetiredError);
|
||||
|
||||
const resolved = await resolveModelOrError("microsoft-designer-web/gpt-4o", {
|
||||
messages: [{ role: "user", content: "test" }],
|
||||
});
|
||||
assert.ok(resolved.error instanceof Response);
|
||||
assert.equal(resolved.error.status, 410);
|
||||
assert.deepEqual(await resolved.error.json(), {
|
||||
error: {
|
||||
message: "Provider has been retired from OmniRoute runtime.",
|
||||
type: "invalid_request_error",
|
||||
code: "model_shutdown",
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test("stripModelPrefix cannot erase the retired Designer alias", async () => {
|
||||
await settingsDb.updateSettings({ stripModelPrefix: true });
|
||||
|
||||
await assert.rejects(() => getModelInfo(" MSDESIGNER /gpt-4o"), assertRetiredError);
|
||||
});
|
||||
|
||||
test("image prefix resolution refuses retired nodes but preserves a similar control", async () => {
|
||||
await createCompatibleNode("openai-compatible-images-designer-retired", "msdesigner");
|
||||
await createCompatibleNode(
|
||||
"openai-compatible-images-designer-preview",
|
||||
"microsoft-designer-web-preview"
|
||||
);
|
||||
|
||||
await assert.rejects(() => resolveImageRouteModel("msdesigner/dall-e-3"), assertRetiredError);
|
||||
assert.equal(
|
||||
await resolveImageRouteModel("microsoft-designer-web-preview/control-image"),
|
||||
"openai-compatible-images-designer-preview/control-image"
|
||||
);
|
||||
});
|
||||
85
tests/unit/microsoft-designer-web-retirement.test.ts
Normal file
85
tests/unit/microsoft-designer-web-retirement.test.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
import assert from "node:assert/strict";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import test from "node:test";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
import { IMAGE_PROVIDERS } from "../../open-sse/config/imageRegistry.ts";
|
||||
import { getExecutor } from "../../open-sse/executors/index.ts";
|
||||
import { WEB_COOKIE_PROVIDERS } from "../../src/shared/constants/providers/web-cookie.ts";
|
||||
import { WEB_SESSION_CREDENTIAL_REQUIREMENTS } from "../../src/shared/providers/webSessionCredentials.ts";
|
||||
|
||||
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
|
||||
|
||||
const RETIRED_IDS = [
|
||||
"microsoft-designer-web",
|
||||
"msdesigner",
|
||||
" MICROSOFT-DESIGNER-WEB\t",
|
||||
"\nMsDesigner ",
|
||||
] as const;
|
||||
|
||||
test("Microsoft Designer Web runtime IDs fail closed at the executor seam", async () => {
|
||||
for (const providerId of RETIRED_IDS) {
|
||||
await assert.rejects(
|
||||
() => getExecutor(providerId),
|
||||
(error: unknown) => {
|
||||
assert.ok(error instanceof Error);
|
||||
assert.equal((error as Error & { status?: number }).status, 410);
|
||||
assert.equal(error.message, "Provider has been retired from OmniRoute runtime.");
|
||||
assert.equal(error.message.includes(providerId.trim()), false);
|
||||
return true;
|
||||
}
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test("Microsoft Designer retirement is exact and preserves supported providers", async () => {
|
||||
for (const providerId of [
|
||||
"microsoft-designer-web-preview",
|
||||
"openai",
|
||||
"azure",
|
||||
"copilot",
|
||||
"musespark-web",
|
||||
"modelscope",
|
||||
]) {
|
||||
await assert.doesNotReject(() => getExecutor(providerId));
|
||||
}
|
||||
});
|
||||
|
||||
test("Microsoft Designer Web implementation and active catalog surfaces are absent", () => {
|
||||
const imageProviders = IMAGE_PROVIDERS as Record<string, unknown>;
|
||||
const webCookieProviders = WEB_COOKIE_PROVIDERS as Record<string, unknown>;
|
||||
const webSessionRequirements = WEB_SESSION_CREDENTIAL_REQUIREMENTS as Record<string, unknown>;
|
||||
|
||||
for (const providerId of ["microsoft-designer-web", "msdesigner"]) {
|
||||
assert.equal(imageProviders[providerId], undefined);
|
||||
assert.equal(webCookieProviders[providerId], undefined);
|
||||
assert.equal(webSessionRequirements[providerId], undefined);
|
||||
}
|
||||
|
||||
for (const relativePath of [
|
||||
"open-sse/executors/microsoft-designer-web.ts",
|
||||
"open-sse/handlers/imageGeneration/providers/designerWeb.ts",
|
||||
]) {
|
||||
assert.equal(fs.existsSync(path.join(repoRoot, relativePath)), false, relativePath);
|
||||
}
|
||||
|
||||
const sourceMustNotContain: Array<[string, RegExp]> = [
|
||||
[
|
||||
"open-sse/handlers/imageGeneration.ts",
|
||||
/handleDesignerWebImageGeneration|providers\/designerWeb/,
|
||||
],
|
||||
["open-sse/executors/index.ts", /MicrosoftDesignerWebExecutor|microsoft-designer-web\.ts/],
|
||||
["open-sse/utils/publicCreds.ts", /microsoft_designer_client_id/],
|
||||
["src/app/api/providers/[id]/test/webSessionTestDispatch.ts", /microsoft-designer-web/],
|
||||
[".env.example", /DESIGNER_WEB_|Microsoft Designer Web/],
|
||||
];
|
||||
for (const [relativePath, pattern] of sourceMustNotContain) {
|
||||
assert.doesNotMatch(fs.readFileSync(path.join(repoRoot, relativePath), "utf8"), pattern);
|
||||
}
|
||||
|
||||
assert.ok(imageProviders.openai, "OpenAI/DALL-E image generation remains registered");
|
||||
assert.ok(webCookieProviders["copilot-web"], "Copilot Web remains registered");
|
||||
assert.ok(webCookieProviders["muse-spark-web"], "MuseSpark Web remains registered");
|
||||
assert.equal(fs.existsSync(path.join(repoRoot, "open-sse/executors/azure-openai.ts")), true);
|
||||
});
|
||||
138
tests/unit/microsoft-designer-web-runtime-block.test.ts
Normal file
138
tests/unit/microsoft-designer-web-runtime-block.test.ts
Normal file
@@ -0,0 +1,138 @@
|
||||
import assert from "node:assert/strict";
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import test from "node:test";
|
||||
|
||||
const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-designer-retired-"));
|
||||
process.env.DATA_DIR = TEST_DATA_DIR;
|
||||
process.env.DISABLE_SQLITE_AUTO_BACKUP = "true";
|
||||
|
||||
const core = await import("../../src/lib/db/core.ts");
|
||||
const providersDb = await import("../../src/lib/db/providers.ts");
|
||||
const leasesDb = await import("../../src/lib/db/exclusiveConnectionLeases.ts");
|
||||
const auth = await import("../../src/sse/services/auth.ts");
|
||||
|
||||
const LEASE_OWNER_ID = `vlo_${"D".repeat(43)}`;
|
||||
const API_KEY_ID = "designer-retirement-managed-key";
|
||||
|
||||
async function resetStorage(): Promise<void> {
|
||||
core.resetDbInstance();
|
||||
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
|
||||
fs.mkdirSync(TEST_DATA_DIR, { recursive: true });
|
||||
}
|
||||
|
||||
test.beforeEach(resetStorage);
|
||||
test.after(() => {
|
||||
core.resetDbInstance();
|
||||
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
test("creating a retired Microsoft Designer connection reports its persisted tombstone", async () => {
|
||||
const connection = (await providersDb.createProviderConnection({
|
||||
provider: "msdesigner",
|
||||
authType: "cookie",
|
||||
name: "retired-designer-create",
|
||||
isActive: true,
|
||||
providerSpecificData: { accessToken: "test-token" },
|
||||
})) as {
|
||||
id: string;
|
||||
isActive: boolean;
|
||||
testStatus: string | null;
|
||||
lastErrorType: string | null;
|
||||
lastErrorSource: string | null;
|
||||
};
|
||||
|
||||
assert.equal(connection.isActive, false);
|
||||
assert.equal(connection.testStatus, "unavailable");
|
||||
assert.equal(connection.lastErrorType, "provider_retired");
|
||||
assert.equal(connection.lastErrorSource, "migration:retire-microsoft-designer-web");
|
||||
|
||||
const persisted = await providersDb.getProviderConnectionById(connection.id);
|
||||
assert.equal(persisted?.isActive, false);
|
||||
assert.equal(persisted?.providerSpecificData.accessToken, "test-token");
|
||||
|
||||
const reactivated = await providersDb.updateProviderConnection(connection.id, {
|
||||
isActive: true,
|
||||
testStatus: "active",
|
||||
lastError: null,
|
||||
lastErrorType: null,
|
||||
lastErrorSource: null,
|
||||
});
|
||||
assert.equal(reactivated?.isActive, false);
|
||||
assert.equal(reactivated?.testStatus, "unavailable");
|
||||
assert.equal(reactivated?.lastErrorType, "provider_retired");
|
||||
|
||||
const upserted = await providersDb.createProviderConnection({
|
||||
provider: "msdesigner",
|
||||
authType: "cookie",
|
||||
name: "retired-designer-create",
|
||||
isActive: true,
|
||||
testStatus: "active",
|
||||
providerSpecificData: { accessToken: "replacement-token" },
|
||||
});
|
||||
assert.equal(upserted?.id, connection.id);
|
||||
assert.equal(upserted?.isActive, false);
|
||||
assert.equal(upserted?.testStatus, "unavailable");
|
||||
|
||||
const control = await providersDb.createProviderConnection({
|
||||
provider: "openai",
|
||||
authType: "apikey",
|
||||
name: "openai-control",
|
||||
apiKey: "sk-openai-control",
|
||||
isActive: true,
|
||||
});
|
||||
assert.equal(control?.isActive, true);
|
||||
});
|
||||
|
||||
test("retired Microsoft Designer credentials are rejected and an active managed lease is invalidated", async () => {
|
||||
await core.ensureDbInitialized();
|
||||
const db = core.getDbInstance();
|
||||
for (const trigger of [
|
||||
"trg_retire_microsoft_designer_web_provider_insert",
|
||||
"trg_retire_microsoft_designer_web_provider_update",
|
||||
"trg_retire_microsoft_designer_web_lease_insert",
|
||||
"trg_retire_microsoft_designer_web_lease_update",
|
||||
]) {
|
||||
db.exec(`DROP TRIGGER IF EXISTS ${trigger}`);
|
||||
}
|
||||
|
||||
const connection = (await providersDb.createProviderConnection({
|
||||
provider: "microsoft-designer-web",
|
||||
authType: "cookie",
|
||||
name: "retired-designer",
|
||||
isActive: true,
|
||||
providerSpecificData: { accessToken: "test-token" },
|
||||
})) as { id: string };
|
||||
|
||||
const acquired = leasesDb.acquireExclusiveConnectionLease({
|
||||
leaseOwnerId: LEASE_OWNER_ID,
|
||||
apiKeyId: API_KEY_ID,
|
||||
provider: "microsoft-designer-web",
|
||||
connectionId: connection.id,
|
||||
});
|
||||
assert.equal(acquired.kind, "ACQUIRED");
|
||||
if (acquired.kind !== "ACQUIRED") return;
|
||||
|
||||
const selected = await auth.getProviderCredentials(
|
||||
"microsoft-designer-web",
|
||||
null,
|
||||
[connection.id],
|
||||
"dall-e-3",
|
||||
{
|
||||
lease: {
|
||||
apiKeyId: API_KEY_ID,
|
||||
context: {
|
||||
leaseOwnerId: LEASE_OWNER_ID,
|
||||
leaseOwnerHash: leasesDb.hashLeaseOwnerId(LEASE_OWNER_ID),
|
||||
ownerDiagnostic: "designer-retirement-test",
|
||||
generation: acquired.lease.generation,
|
||||
},
|
||||
mode: "request",
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
assert.equal(selected, null);
|
||||
assert.equal(leasesDb.getActiveExclusiveConnectionLease(LEASE_OWNER_ID), null);
|
||||
});
|
||||
178
tests/unit/migration-164-retire-microsoft-designer-web.test.ts
Normal file
178
tests/unit/migration-164-retire-microsoft-designer-web.test.ts
Normal file
@@ -0,0 +1,178 @@
|
||||
import assert from "node:assert/strict";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import test from "node:test";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
import Database from "better-sqlite3";
|
||||
|
||||
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
|
||||
const migrationPath = path.join(
|
||||
repoRoot,
|
||||
"src/lib/db/migrations/164_retire_microsoft_designer_web.sql"
|
||||
);
|
||||
|
||||
type ProviderRow = {
|
||||
provider: string;
|
||||
is_active: number;
|
||||
test_status: string | null;
|
||||
last_error_type: string | null;
|
||||
last_error_source: string | null;
|
||||
api_key: string | null;
|
||||
provider_specific_data: string | null;
|
||||
created_at: string;
|
||||
};
|
||||
|
||||
type LeaseRow = { state: string; end_reason: string | null };
|
||||
|
||||
test("migration 164 permanently tombstones only Microsoft Designer Web runtime IDs", () => {
|
||||
const db = new Database(":memory:");
|
||||
test.after(() => db.close());
|
||||
|
||||
db.exec(`
|
||||
CREATE TABLE provider_connections (
|
||||
id TEXT PRIMARY KEY,
|
||||
provider TEXT NOT NULL,
|
||||
is_active INTEGER DEFAULT 1,
|
||||
test_status TEXT,
|
||||
last_error TEXT,
|
||||
last_error_at TEXT,
|
||||
last_error_type TEXT,
|
||||
last_error_source TEXT,
|
||||
api_key TEXT,
|
||||
provider_specific_data TEXT,
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL
|
||||
);
|
||||
CREATE TABLE exclusive_connection_leases (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
lease_owner_hash TEXT NOT NULL,
|
||||
api_key_id TEXT NOT NULL,
|
||||
provider TEXT NOT NULL,
|
||||
connection_id TEXT NOT NULL,
|
||||
generation INTEGER NOT NULL,
|
||||
state TEXT NOT NULL,
|
||||
acquired_at TEXT NOT NULL,
|
||||
renewed_at TEXT NOT NULL,
|
||||
expires_at TEXT NOT NULL,
|
||||
ended_at TEXT,
|
||||
end_reason TEXT
|
||||
);
|
||||
`);
|
||||
|
||||
const insertProvider = db.prepare(`
|
||||
INSERT INTO provider_connections (
|
||||
id, provider, is_active, test_status, api_key, provider_specific_data, created_at, updated_at
|
||||
) VALUES (?, ?, 1, 'active', ?, ?, '2026-01-02T03:04:05.000Z', '2026-01-02T03:04:05.000Z')
|
||||
`);
|
||||
insertProvider.run("designer-canonical", "microsoft-designer-web", "encrypted-a", '{"keep":1}');
|
||||
insertProvider.run("designer-alias", "\tMSDesigner\r", "encrypted-b", '{"keep":2}');
|
||||
|
||||
const controlProviders = [
|
||||
"microsoft-designer-web-preview",
|
||||
"openai",
|
||||
"azure",
|
||||
"copilot",
|
||||
"musespark-web",
|
||||
"modelscope",
|
||||
];
|
||||
for (const provider of controlProviders) {
|
||||
insertProvider.run(
|
||||
`control-${provider}`,
|
||||
provider,
|
||||
`encrypted-${provider}`,
|
||||
'{"control":true}'
|
||||
);
|
||||
}
|
||||
|
||||
const insertLease = db.prepare(`
|
||||
INSERT INTO exclusive_connection_leases (
|
||||
lease_owner_hash, api_key_id, provider, connection_id, generation, state,
|
||||
acquired_at, renewed_at, expires_at
|
||||
) VALUES (?, 'managed-key', ?, ?, 1, 'ACTIVE',
|
||||
'2026-01-02T03:04:05.000Z', '2026-01-02T03:04:05.000Z', '2099-01-02T03:04:05.000Z')
|
||||
`);
|
||||
insertLease.run("a".repeat(64), "microsoft-designer-web", "designer-canonical");
|
||||
insertLease.run("b".repeat(64), "MSDESIGNER", "designer-alias");
|
||||
insertLease.run("c".repeat(64), "openai", "control-openai");
|
||||
|
||||
const migrationSql = fs.readFileSync(migrationPath, "utf8");
|
||||
db.exec(migrationSql);
|
||||
db.exec(migrationSql);
|
||||
|
||||
for (const id of ["designer-canonical", "designer-alias"]) {
|
||||
const row = db
|
||||
.prepare("SELECT * FROM provider_connections WHERE id = ?")
|
||||
.get(id) as ProviderRow;
|
||||
assert.equal(row.is_active, 0);
|
||||
assert.equal(row.test_status, "unavailable");
|
||||
assert.equal(row.last_error_type, "provider_retired");
|
||||
assert.equal(row.last_error_source, "migration:retire-microsoft-designer-web");
|
||||
assert.match(row.api_key ?? "", /^encrypted-/);
|
||||
assert.match(row.provider_specific_data ?? "", /"keep":/);
|
||||
assert.equal(row.created_at, "2026-01-02T03:04:05.000Z");
|
||||
}
|
||||
|
||||
for (const provider of controlProviders) {
|
||||
const row = db
|
||||
.prepare("SELECT * FROM provider_connections WHERE id = ?")
|
||||
.get(`control-${provider}`) as ProviderRow;
|
||||
assert.equal(row.provider, provider);
|
||||
assert.equal(row.is_active, 1);
|
||||
assert.equal(row.test_status, "active");
|
||||
assert.equal(row.last_error_type, null);
|
||||
}
|
||||
|
||||
const retiredLeases = db
|
||||
.prepare("SELECT state, end_reason FROM exclusive_connection_leases ORDER BY id LIMIT 2")
|
||||
.all() as LeaseRow[];
|
||||
assert.deepEqual(retiredLeases, [
|
||||
{ state: "INVALIDATED", end_reason: "AUTHORIZATION_CHANGED" },
|
||||
{ state: "INVALIDATED", end_reason: "AUTHORIZATION_CHANGED" },
|
||||
]);
|
||||
assert.deepEqual(
|
||||
db.prepare("SELECT state, end_reason FROM exclusive_connection_leases WHERE id = 3").get(),
|
||||
{ state: "ACTIVE", end_reason: null }
|
||||
);
|
||||
|
||||
db.prepare(
|
||||
`
|
||||
INSERT OR REPLACE INTO provider_connections (
|
||||
id, provider, is_active, test_status, api_key, provider_specific_data, created_at, updated_at
|
||||
) VALUES ('designer-replaced', ' MSDESIGNER ', 1, 'active', 'encrypted-c', '{"keep":3}',
|
||||
'2026-02-03T04:05:06.000Z', '2026-02-03T04:05:06.000Z')
|
||||
`
|
||||
).run();
|
||||
const replaced = db
|
||||
.prepare("SELECT * FROM provider_connections WHERE id = 'designer-replaced'")
|
||||
.get() as ProviderRow;
|
||||
assert.equal(replaced.is_active, 0);
|
||||
assert.equal(replaced.test_status, "unavailable");
|
||||
|
||||
db.prepare(
|
||||
"UPDATE provider_connections SET provider = 'microsoft-designer-web', is_active = 1, test_status = 'active' WHERE id = 'control-openai'"
|
||||
).run();
|
||||
const converted = db
|
||||
.prepare("SELECT * FROM provider_connections WHERE id = 'control-openai'")
|
||||
.get() as ProviderRow;
|
||||
assert.equal(converted.is_active, 0);
|
||||
assert.equal(converted.test_status, "unavailable");
|
||||
assert.deepEqual(
|
||||
db.prepare("SELECT state, end_reason FROM exclusive_connection_leases WHERE id = 3").get(),
|
||||
{ state: "INVALIDATED", end_reason: "AUTHORIZATION_CHANGED" }
|
||||
);
|
||||
|
||||
insertLease.run("d".repeat(64), "openai", "designer-replaced");
|
||||
assert.deepEqual(
|
||||
db.prepare("SELECT state, end_reason FROM exclusive_connection_leases WHERE id = 4").get(),
|
||||
{ state: "INVALIDATED", end_reason: "AUTHORIZATION_CHANGED" }
|
||||
);
|
||||
|
||||
db.prepare(
|
||||
"UPDATE exclusive_connection_leases SET state = 'ACTIVE', ended_at = NULL, end_reason = NULL WHERE id = 1"
|
||||
).run();
|
||||
assert.deepEqual(
|
||||
db.prepare("SELECT state, end_reason FROM exclusive_connection_leases WHERE id = 1").get(),
|
||||
{ state: "INVALIDATED", end_reason: "AUTHORIZATION_CHANGED" }
|
||||
);
|
||||
});
|
||||
@@ -11,9 +11,10 @@
|
||||
//
|
||||
// The reserved set is shared between the runtime guard and the validation
|
||||
// schemas via src/shared/constants/reservedProviderPrefixes.ts (single source of
|
||||
// truth). Set semantics mirror the old inline guard exactly:
|
||||
// - REGISTRY entry ids + aliases only;
|
||||
// - case-sensitive (mixed-case "TokenRouter" does NOT collide at runtime);
|
||||
// truth). Live set semantics mirror the old inline guard exactly; exact retired
|
||||
// ids remain reserved after registry removal and use trim + lowercase matching:
|
||||
// - REGISTRY entry ids + aliases, plus retired ids;
|
||||
// - live ids remain case-sensitive (mixed-case "TokenRouter" does NOT collide);
|
||||
// - manual alias ids that live outside REGISTRY (xiaomi/llamacpp/aq) are NOT
|
||||
// included — verified they do not intercept nodes at runtime.
|
||||
import test from "node:test";
|
||||
@@ -106,13 +107,15 @@ test("shared set excludes manual aliases that never intercept nodes at runtime",
|
||||
assert.equal(RESERVED_PROVIDER_PREFIXES.has("aq"), false);
|
||||
});
|
||||
|
||||
test("shared set size matches full REGISTRY scan (398 unique prefixes)", () => {
|
||||
// Count measured against release/v3.8.51 tip after #11629 (opper) and
|
||||
// #11631 (1min.ai) boarded — 398 unique ids/aliases walked from the
|
||||
// provider REGISTRY on top of the 395 pinned post-#11333.
|
||||
// the assertion pins that the set is a full REGISTRY walk, not a
|
||||
// hand-maintained list.
|
||||
assert.equal(RESERVED_PREFIX_COUNT, 398);
|
||||
test("shared set size includes live REGISTRY and 2 retired Designer prefixes", () => {
|
||||
// Count measured against release/v3.8.51 tip (398 pinned post-#11333,
|
||||
// #11629, #11631 — a deduplicated Set, not a raw id+alias sum) after the
|
||||
// Microsoft Designer Web retirement drops its 2 ids/aliases from the live
|
||||
// REGISTRY walk and re-adds them explicitly via
|
||||
// RETIRED_MICROSOFT_DESIGNER_WEB_PROVIDER_IDS so they remain unavailable
|
||||
// for compatible-node shadowing. The assertion pins the actual computed
|
||||
// set size, not a hand-derived sum.
|
||||
assert.equal(RESERVED_PREFIX_COUNT, 400);
|
||||
});
|
||||
|
||||
test("isReservedProviderPrefix rejects non-string input", () => {
|
||||
|
||||
@@ -30,17 +30,27 @@ test("token-kind web sessions WITHOUT a token-aware validator stay off the API-k
|
||||
assert.equal(shouldUseApiKeyConnectionTest("cookie", "hailuo-web"), false);
|
||||
assert.equal(shouldUseApiKeyConnectionTest("cookie", "promptql"), false);
|
||||
|
||||
// Same reasoning applies to the other two token-kind providers with no validator.
|
||||
assert.equal(shouldUseApiKeyConnectionTest("cookie", "microsoft-designer-web"), false);
|
||||
// Same reasoning applies to the remaining token-kind provider with no validator.
|
||||
assert.equal(shouldUseApiKeyConnectionTest("cookie", "t3-chat-web"), false);
|
||||
});
|
||||
|
||||
test("every token-kind web session with a real token-aware validator uses the API-key test path", () => {
|
||||
for (const providerId of ["deepseek-web", "kimi-web", "tinycms-web", "copilot-m365-web", "copilot-web", "zai-web"]) {
|
||||
for (const providerId of [
|
||||
"deepseek-web",
|
||||
"kimi-web",
|
||||
"tinycms-web",
|
||||
"copilot-m365-web",
|
||||
"copilot-web",
|
||||
"zai-web",
|
||||
]) {
|
||||
assert.equal(shouldUseApiKeyConnectionTest("cookie", providerId), true, providerId);
|
||||
}
|
||||
});
|
||||
|
||||
test("retired Microsoft Designer cannot regain the positive web-session test path", () => {
|
||||
assert.equal(shouldUseApiKeyConnectionTest("cookie", "microsoft-designer-web"), false);
|
||||
});
|
||||
|
||||
test("other auth types are not broadened", () => {
|
||||
assert.equal(shouldUseApiKeyConnectionTest("oauth", "deepseek-web"), false);
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
isRegisteredImageModel,
|
||||
} from "../../open-sse/config/imageRegistry.ts";
|
||||
|
||||
test("#10832 unprefixed dall-e-3 routes to OpenAI, not Microsoft Designer Web", () => {
|
||||
test("#10832 unprefixed dall-e-3 remains routed to OpenAI after Designer retirement", () => {
|
||||
assert.deepEqual(parseImageModel("dall-e-3"), {
|
||||
provider: "openai",
|
||||
model: "dall-e-3",
|
||||
@@ -16,28 +16,19 @@ test("#10832 unprefixed dall-e-3 routes to OpenAI, not Microsoft Designer Web",
|
||||
provider: "openai",
|
||||
model: "dall-e-3",
|
||||
});
|
||||
assert.deepEqual(parseImageModel("microsoft-designer-web/dall-e-3"), {
|
||||
provider: "microsoft-designer-web",
|
||||
model: "dall-e-3",
|
||||
});
|
||||
assert.deepEqual(parseImageModel("msdesigner/dall-e-3"), {
|
||||
provider: "microsoft-designer-web",
|
||||
model: "dall-e-3",
|
||||
});
|
||||
|
||||
assert.equal(isRegisteredImageModel("openai", "dall-e-3"), true);
|
||||
assert.equal(isRegisteredImageModel("microsoft-designer-web", "dall-e-3"), true);
|
||||
assert.equal(isRegisteredImageModel("microsoft-designer-web", "dall-e-3"), false);
|
||||
assert.equal(isRegisteredImageModel("msdesigner", "dall-e-3"), false);
|
||||
|
||||
const openai = IMAGE_PROVIDERS.openai;
|
||||
assert.ok(openai.models.some((model) => model.id === "dall-e-3"));
|
||||
assert.equal((IMAGE_PROVIDERS as Record<string, unknown>)["microsoft-designer-web"], undefined);
|
||||
assert.equal((IMAGE_PROVIDERS as Record<string, unknown>).msdesigner, undefined);
|
||||
|
||||
const catalog = getAllImageModels();
|
||||
assert.ok(catalog.some((model) => model.id === "openai/dall-e-3" && model.provider === "openai"));
|
||||
assert.ok(
|
||||
catalog.some(
|
||||
(model) =>
|
||||
model.id === "microsoft-designer-web/dall-e-3" &&
|
||||
model.provider === "microsoft-designer-web"
|
||||
)
|
||||
assert.equal(
|
||||
catalog.some((model) => ["microsoft-designer-web", "msdesigner"].includes(model.provider)),
|
||||
false
|
||||
);
|
||||
});
|
||||
|
||||
@@ -245,3 +245,42 @@ test("createVirtualAutoCombo keeps credential-required providers out when discon
|
||||
"OpenAI should still require a real active connection"
|
||||
);
|
||||
});
|
||||
|
||||
test("createVirtualAutoCombo excludes trigger-bypassed Microsoft Designer connections exactly", async () => {
|
||||
await core.ensureDbInitialized();
|
||||
const db = core.getDbInstance();
|
||||
db.exec("DROP TRIGGER IF EXISTS trg_retire_microsoft_designer_web_provider_insert");
|
||||
db.exec("DROP TRIGGER IF EXISTS trg_retire_microsoft_designer_web_provider_update");
|
||||
|
||||
for (const [provider, model] of [
|
||||
["microsoft-designer-web", "dall-e-3"],
|
||||
["msdesigner", "dall-e-3"],
|
||||
["microsoft-designer-web-preview", "preview-model"],
|
||||
] as const) {
|
||||
await providersDb.createProviderConnection({
|
||||
provider,
|
||||
authType: "apikey",
|
||||
name: `${provider}-trigger-bypass`,
|
||||
apiKey: `sk-${provider}-test`,
|
||||
providerSpecificData: { accessToken: `${provider}-token` },
|
||||
defaultModel: model,
|
||||
isActive: true,
|
||||
});
|
||||
}
|
||||
|
||||
const combo: VirtualComboResult = await virtualFactory.createVirtualAutoCombo("coding");
|
||||
|
||||
assert.equal(
|
||||
combo.models.some((model) =>
|
||||
["microsoft-designer-web", "msdesigner"].includes(model.providerId)
|
||||
),
|
||||
false
|
||||
);
|
||||
assert.equal(combo.autoConfig.candidatePool.includes("microsoft-designer-web"), false);
|
||||
assert.equal(combo.autoConfig.candidatePool.includes("msdesigner"), false);
|
||||
assert.equal(
|
||||
combo.models.some((model) => model.providerId === "microsoft-designer-web-preview"),
|
||||
true,
|
||||
"a merely similar provider ID must remain eligible"
|
||||
);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user