chore(providers): retire GPL-derived Raycast and Hailuo ports

This commit is contained in:
Xiangzhe
2026-08-26 15:45:51 -03:00
committed by Markus Hartung
parent 825f8fe425
commit cd701c1eed
62 changed files with 405 additions and 3403 deletions

View File

@@ -1256,17 +1256,6 @@ GITHUB_OAUTH_CLIENT_ID=Iv1.b507a08c87ecfe98
# VISION_BRIDGE_BASE_URL=
# VISION_BRIDGE_API_KEY=
# ── Raycast Pro (local auto-import) ──
# Raycast Pro AI is a reverse-engineered, unofficial API — local/personal use
# only (no OAuth client_id/secret; token is captured via macOS Auto-Import
# from the Keychain + local Raycast SQLite DB, or pasted manually). These
# vars are optional manual overrides used by open-sse/services/raycast.ts
# and the direct-probe benchmark script scripts/raycast/usage-benchmark.mjs.
# RAYCAST_BEARER_TOKEN=
# RAYCAST_DEVICE_ID=
# RAYCAST_AID=
# RAYCAST_SIG_SECRET=
# ─────────────────────────────────────────────────────────────────────────────
# ⚠️ GOOGLE OAUTH (Antigravity) & OTHER PROVIDERS — REMOTE SERVERS
# ─────────────────────────────────────────────────────────────────────────────

View File

@@ -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 (~25K100K 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

View File

@@ -0,0 +1 @@
- **chore(providers):** retire the distributed Raycast Relay and Hailuo Web integrations after a provenance audit confirmed substantial derivation from GPL-3.0 sources; the official GitHub and MiniMax API providers remain available. Migration 163 disables legacy connections and invalidates their active leases while preserving connection IDs, audit/history rows, quota snapshots, usage records, and fail-closed API-key connection policies.

View File

@@ -194,11 +194,6 @@
"count": 7
}
},
"open-sse/executors/raycast.ts": {
"@typescript-eslint/no-unused-vars": {
"count": 1
}
},
"open-sse/executors/t3-chat-web.ts": {
"@typescript-eslint/no-explicit-any": {
"count": 11

View File

@@ -25,6 +25,36 @@
"tests/unit/providers-page-utils.test.ts": "v3.8.47 #6675: remoção dos providers obsoletos glhf/kluster/cablyai/inclusionai — os 2 asserts que citavam providers removidos do catálogo foram removidos junto (net 276→274). Superfície aposentada, não mascaramento. Verificado legítimo. Prune após v3.8.47 mergear para main.",
"_deletedWithReplacement": {
"_comment": "Deleções de arquivo de teste com SUBSTITUTO verificado (o gate exige que o replacement exista no HEAD e seja arquivo de teste). Uso restrito ao caso 'reescrito em outro path sem rename detectável pelo -M do git'. Cada entrada precisa de reason com PR ref e passa por revisão humana no release PR. Prune após o release mergear para main.",
"tests/unit/executor-hailuo-web.test.ts": {
"replacement": "tests/unit/gpl-derived-provider-removals.test.ts",
"sourceRemoved": [
"open-sse/executors/hailuo-web.ts",
"open-sse/config/providers/registry/minimax/web/index.ts"
],
"reason": "v3.8.50 provenance remediation: the GPL-derived Hailuo Web executor and registry were removed. The replacement proves runtime/source absence while preserving both official MiniMax API providers."
},
"tests/unit/raycast-auth.test.ts": {
"replacement": "tests/unit/gpl-derived-provider-removals.test.ts",
"sourceRemoved": [
"open-sse/services/raycast.ts",
"src/lib/oauth/providers/raycast.ts",
"src/lib/oauth/services/raycast.ts"
],
"reason": "v3.8.50 provenance remediation: the GPL-derived Raycast protocol and OAuth service were removed. The replacement proves raycast/rc runtime and source absence while preserving the independent GitHub provider."
},
"tests/unit/raycast-local-extract.test.ts": {
"replacement": "tests/unit/gpl-derived-provider-removals.test.ts",
"sourceRemoved": ["src/lib/oauth/services/raycastLocal.ts"],
"reason": "v3.8.50 provenance remediation: the retired Raycast integration no longer ships local credential extraction. The replacement checks exact source closure and the negative runtime contract."
},
"tests/unit/web-cookie-hailuo-web-11000.test.ts": {
"replacement": "tests/unit/gpl-derived-provider-removals.test.ts",
"sourceRemoved": [
"open-sse/executors/hailuo-web.ts",
"open-sse/config/providers/registry/minimax/web/index.ts"
],
"reason": "v3.8.50 provenance remediation: the Hailuo Web cookie transport was removed with its GPL-derived executor. The replacement proves absence while preserving MiniMax and MiniMax China."
},
"open-sse/services/combo/__tests__/targetExhaustion.test.ts": {
"replacement": "tests/unit/combo/combo-target-exhaustion.test.ts",
"reason": "v3.8.44 #5976: os testes de exaustão eram flake-prone (delays Math.random, timeouts 30s, >3min no CI) e foram REESCRITOS como unit determinístico com MAIS cobertura (21 casos/52 asserts vs 13 casos/37 asserts). Documentado no commit 5fe225850. Revisão humana: apresentado ao operador no STOP #1 do release v3.8.44."

View File

@@ -324,7 +324,7 @@ Domain layer modules:
OAuth provider modules (22 individual files under `src/lib/oauth/providers/`):
- Registry index: `src/lib/oauth/providers/index.ts`
- Individual providers: `agy.ts`, `antigravity.ts`, `claude.ts`, `cline.ts`, `codebuddy-cn.ts`, `codex.ts`, `cursor.ts`, `devin-desktop.ts`, `ghe-copilot.ts`, `github.ts`, `gitlab-duo.ts`, `grok-cli-oauth.ts`, `grok-cli.ts`, `kilocode.ts`, `kimi-coding.ts`, `kiro.ts`, `qoder.ts`, `raycast.ts`, `trae.ts`, `xai-oauth.ts`, `zed-hosted.ts`, `zed.ts`
- Individual providers: `agy.ts`, `antigravity.ts`, `claude.ts`, `cline.ts`, `codebuddy-cn.ts`, `codex.ts`, `cursor.ts`, `devin-desktop.ts`, `ghe-copilot.ts`, `github.ts`, `gitlab-duo.ts`, `grok-cli-oauth.ts`, `grok-cli.ts`, `kilocode.ts`, `kimi-coding.ts`, `kiro.ts`, `openference.ts`, `qoder.ts`, `trae.ts`, `xai-oauth.ts`, `zed-hosted.ts`, `zed.ts`
- Thin wrapper: `src/lib/oauth/providers.ts` — re-exports from individual modules
## 5) Embedded Services (v3.8.4)
@@ -980,7 +980,7 @@ All other providers (including custom compatible nodes) use the `DefaultExecutor
| KIE | openai | API Key | ✅ | ✅ | ❌ | ❌ |
| Devin Desktop | openai | Imported API key | ✅ (Connect→SSE) | ✅ | ❌ | ⚠️ Per request |
| GitLab Duo | openai | OAuth (GitLab) | ✅ | ✅ | ✅ | ❌ |
| Devin CLI | openai | Local CLI login | ✅ | ✅ | ❌ | ✅ Task API |
| Devin CLI | openai | Local CLI login | ✅ | ✅ | ❌ | ✅ Task API |
| Codex Cloud | openai-responses | OAuth | ✅ | ❌ | ✅ | ✅ Rate limits |
| Jules | openai | OAuth | ✅ | ✅ | ✅ | ✅ Task API |
| AgentRouter | openai | API Key | ✅ | ✅ | ❌ | ❌ |

View File

@@ -292,7 +292,7 @@ table groups the actual directories and notable top-level files.
| `jobs/` | Background jobs (`autoUpdate.ts`, …) |
| `memory/` | Persistent memory: `store.ts`, `cache.ts`, `retrieval.ts`, `summarization.ts`, `extraction.ts`, `injection.ts`, `qdrant.ts`, `settings.ts`, `verify.ts`, `schemas.ts`, `types.ts` |
| `monitoring/` | `observability.ts` |
| `oauth/` | OAuth/import provider modules (22): `agy`, `antigravity`, `claude`, `cline`, `codebuddy-cn`, `codex`, `cursor`, `devin-desktop`, `ghe-copilot`, `github`, `gitlab-duo`, `grok-cli-oauth`, `grok-cli`, `kilocode`, `kimi-coding`, `kiro`, `qoder`, `raycast`, `trae`, `xai-oauth`, `zed-hosted`, `zed`, plus `services/`, `utils/`, and `constants/oauth.ts` |
| `oauth/` | OAuth/import provider modules (22): `agy`, `antigravity`, `claude`, `cline`, `codebuddy-cn`, `codex`, `cursor`, `devin-desktop`, `ghe-copilot`, `github`, `gitlab-duo`, `grok-cli-oauth`, `grok-cli`, `kilocode`, `kimi-coding`, `kiro`, `openference`, `qoder`, `trae`, `xai-oauth`, `zed-hosted`, `zed`, plus `services/`, `utils/`, and `constants/oauth.ts` |
| `plugins/` | Plugin loader (`index.ts`) |
| `promptCache/` | `prefixAnalyzer.ts`, `index.ts` |
| `providerModels/` | Managed model lifecycle: `modelDiscovery.ts`, `managedModelImport.ts`, `managedAvailableModels.ts`, `cursorAgent.ts` |

View File

@@ -195,7 +195,7 @@ src/
| `memory/vectorStore.ts` | sqlite-vec v0.1.9 wrapper — KNN brute-force + hybrid RRF (FTS5 + vector, k=60). Lazy-init, degrades gracefully when sqlite-vec unavailable. (plan 21) |
| `memory/reindex.ts` | `runReindexBatch()` — processes memories with `needs_reindex=1` in background; called by `POST /api/memory/reindex` and lazy-backfill path. (plan 21) |
| `monitoring/` | Health checks, metrics emission |
| `oauth/` | OAuth/import flows for 22 provider modules (agy, antigravity, claude, cline, codebuddy-cn, codex, cursor, devin-desktop, ghe-copilot, github, gitlab-duo, grok-cli-oauth, grok-cli, kilocode, kimi-coding, kiro, qoder, raycast, trae, xai-oauth, zed-hosted, zed) |
| `oauth/` | OAuth/import flows for 22 provider modules (agy, antigravity, claude, cline, codebuddy-cn, codex, cursor, devin-desktop, ghe-copilot, github, gitlab-duo, grok-cli-oauth, grok-cli, kilocode, kimi-coding, kiro, openference, qoder, trae, xai-oauth, zed-hosted, zed) |
| `plugins/` | Plugin registry |
| `promptCache/` | Anthropic-style prompt cache breakpoints |
| `skills/` | Skills framework (built-in + marketplace + SkillsSH) — see `docs/frameworks/SKILLS.md` |

View File

@@ -324,7 +324,7 @@ Moduły warstwy domenowej:
Moduły dostawców OAuth (22 osobne pliki w `src/lib/oauth/providers/`):
- Indeks rejestru: `src/lib/oauth/providers/index.ts`
- Poszczególni dostawcy: `agy.ts`, `antigravity.ts`, `claude.ts`, `cline.ts`, `codebuddy-cn.ts`, `codex.ts`, `cursor.ts`, `devin-desktop.ts`, `ghe-copilot.ts`, `github.ts`, `gitlab-duo.ts`, `grok-cli-oauth.ts`, `grok-cli.ts`, `kilocode.ts`, `kimi-coding.ts`, `kiro.ts`, `qoder.ts`, `raycast.ts`, `trae.ts`, `xai-oauth.ts`, `zed-hosted.ts`, `zed.ts`
- Poszczególni dostawcy: `agy.ts`, `antigravity.ts`, `claude.ts`, `cline.ts`, `codebuddy-cn.ts`, `codex.ts`, `cursor.ts`, `devin-desktop.ts`, `ghe-copilot.ts`, `github.ts`, `gitlab-duo.ts`, `grok-cli-oauth.ts`, `grok-cli.ts`, `kilocode.ts`, `kimi-coding.ts`, `kiro.ts`, `openference.ts`, `qoder.ts`, `trae.ts`, `xai-oauth.ts`, `zed-hosted.ts`, `zed.ts`
- Cienki wrapper: `src/lib/oauth/providers.ts` — re-eksport z poszczególnych modułów
## 5) Osadzone usługi (v3.8.4)
@@ -926,7 +926,7 @@ Każdy dostawca ma wyspecjalizowany executor rozszerzający `BaseExecutor` (w `o
| `PollinationsExecutor` | Pollinations AI | Klucz API niewymagany, żądania z rate limitem |
| `QoderExecutor` | Qoder AI | Wsparcie PAT i OAuth, darmowy tier multi-model |
| `VertexExecutor` | Google Vertex AI | Auth service account, endpointy oparte na regionie |
| `DevinDesktopExecutor` | Devin Desktop | Importowany klucz API + strumieniowanie czatu Connect-protobuf |
| `DevinDesktopExecutor` | Devin Desktop | Importowany klucz API + strumieniowanie czatu Connect-protobuf |
Wszystkie pozostałe dostawcy (w tym niestandardowe węzły kompatybilne) używają `DefaultExecutor`.
@@ -978,9 +978,9 @@ Wszystkie pozostałe dostawcy (w tym niestandardowe węzły kompatybilne) używa
| GLMT (preset) | claude | API Key | ✅ | ✅ | ❌ | ⚠️ Per żądanie |
| Kimi Coding | openai | OAuth / API Key | ✅ | ✅ | ✅ | ❌ |
| KIE | openai | API Key | ✅ | ✅ | ❌ | ❌ |
| Devin Desktop | openai | Importowany klucz API | ✅ (Connect→SSE) | ✅ | ❌ | ⚠️ Na żądanie |
| Devin Desktop | openai | Importowany klucz API | ✅ (Connect→SSE) | ✅ | ❌ | ⚠️ Na żądanie |
| GitLab Duo | openai | OAuth (GitLab) | ✅ | ✅ | ✅ | ❌ |
| Devin CLI | openai | Lokalne logowanie CLI | ✅ | ✅ | ❌ | ✅ Task API |
| Devin CLI | openai | Lokalne logowanie CLI | ✅ | ✅ | ❌ | ✅ Task API |
| Codex Cloud | openai-responses | OAuth | ✅ | ❌ | ✅ | ✅ Rate limity |
| Jules | openai | OAuth | ✅ | ✅ | ✅ | ✅ Task API |
| AgentRouter | openai | API Key | ✅ | ✅ | ❌ | ❌ |

View File

@@ -332,7 +332,7 @@ Módulos da camada de domínio:
Módulos do provedor OAuth (22 arquivos individuais em `src/lib/oauth/providers/`):
- Índice do registro: `src/lib/oauth/providers/index.ts`
- Provedores individuais: `agy.ts`, `antigravity.ts`, `claude.ts`, `cline.ts`, `codebuddy-cn.ts`, `codex.ts`, `cursor.ts`, `devin-desktop.ts`, `ghe-copilot.ts`, `github.ts`, `gitlab-duo.ts`, `grok-cli-oauth.ts`, `grok-cli.ts`, `kilocode.ts`, `kimi-coding.ts`, `kiro.ts`, `qoder.ts`, `raycast.ts`, `trae.ts`, `xai-oauth.ts`, `zed-hosted.ts`, `zed.ts`
- Provedores individuais: `agy.ts`, `antigravity.ts`, `claude.ts`, `cline.ts`, `codebuddy-cn.ts`, `codex.ts`, `cursor.ts`, `devin-desktop.ts`, `ghe-copilot.ts`, `github.ts`, `gitlab-duo.ts`, `grok-cli-oauth.ts`, `grok-cli.ts`, `kilocode.ts`, `kimi-coding.ts`, `kiro.ts`, `openference.ts`, `qoder.ts`, `trae.ts`, `xai-oauth.ts`, `zed-hosted.ts`, `zed.ts`
- Wrapper fino: `src/lib/oauth/providers.ts` — re-exportações de módulos individuais
## Subsistemas Principais (v3.8.0)
@@ -907,7 +907,7 @@ Cada provedor tem um executor especializado que estende `BaseExecutor` (em `open
| `PollinationsExecutor` | Pollinations AI | Nenhuma chave de API necessária, requisições limitadas por taxa |
| `QoderExecutor` | Qoder AI | Suporte a PAT e OAuth, nível gratuito multi-modelo |
| `VertexExecutor` | Google Vertex AI | Autenticação de conta de serviço, endpoints baseados em região |
| `DevinDesktopExecutor` | Devin Desktop | Chave de API importada + streaming de chat Connect-protobuf |
| `DevinDesktopExecutor` | Devin Desktop | Chave de API importada + streaming de chat Connect-protobuf |
Todos os outros provedores (incluindo nós compatíveis personalizados) usam o `DefaultExecutor`.
@@ -960,9 +960,9 @@ Todos os outros provedores (incluindo nós compatíveis personalizados) usam o `
| GLMT (preset) | claude | Chave de API | ✅ | ✅ | ❌ | ⚠️ Por solicitação |
| Kimi Coding | openai | OAuth / Chave de API | ✅ | ✅ | ✅ | ❌ |
| KIE | openai | Chave de API | ✅ | ✅ | ❌ | ❌ |
| Devin Desktop | openai | Chave de API importada | ✅ (Connect→SSE) | ✅ | ❌ | ⚠️ Por solicitação |
| Devin Desktop | openai | Chave de API importada | ✅ (Connect→SSE) | ✅ | ❌ | ⚠️ Por solicitação |
| GitLab Duo | openai | OAuth (GitLab) | ✅ | ✅ | ✅ | ❌ |
| Devin CLI | openai | Login local da CLI | ✅ | ✅ | ❌ | ✅ API de Tarefas |
| Devin CLI | openai | Login local da CLI | ✅ | ✅ | ❌ | ✅ API de Tarefas |
| Codex Cloud | openai-responses | OAuth | ✅ | ❌ | ✅ | ✅ Limites de taxa |
| Jules | openai | OAuth | ✅ | ✅ | ✅ | ✅ API de Tarefas |
| AgentRouter | openai | Chave de API | ✅ | ✅ | ❌ | ❌ |

View File

@@ -326,7 +326,7 @@ flowchart LR
OAuth 服务商模块(`src/lib/oauth/providers/` 下 22 个独立文件):
- 注册表索引:`src/lib/oauth/providers/index.ts`
- 独立服务商:`agy.ts`, `antigravity.ts`, `claude.ts`, `cline.ts`, `codebuddy-cn.ts`, `codex.ts`, `cursor.ts`, `devin-desktop.ts`, `ghe-copilot.ts`, `github.ts`, `gitlab-duo.ts`, `grok-cli-oauth.ts`, `grok-cli.ts`, `kilocode.ts`, `kimi-coding.ts`, `kiro.ts`, `qoder.ts`, `raycast.ts`, `trae.ts`, `xai-oauth.ts`, `zed-hosted.ts`, `zed.ts`
- 独立服务商:`agy.ts`, `antigravity.ts`, `claude.ts`, `cline.ts`, `codebuddy-cn.ts`, `codex.ts`, `cursor.ts`, `devin-desktop.ts`, `ghe-copilot.ts`, `github.ts`, `gitlab-duo.ts`, `grok-cli-oauth.ts`, `grok-cli.ts`, `kilocode.ts`, `kimi-coding.ts`, `kiro.ts`, `openference.ts`, `qoder.ts`, `trae.ts`, `xai-oauth.ts`, `zed-hosted.ts`, `zed.ts`
- 薄封装层:`src/lib/oauth/providers.ts` — 从独立模块重新导出
## 5) 嵌入式服务v3.8.4
@@ -896,35 +896,35 @@ flowchart LR
每个服务商都有一个继承 `BaseExecutor`(在 `open-sse/executors/base.ts` 中)的专用执行器,该基类提供了 URL 构建、Header 构造、带指数退避的重试、凭据刷新钩子以及 `execute()` 编排方法。
| 执行器 | 服务商 | 特殊处理 |
| ------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------- |
| `DefaultExecutor` | OpenAI, Claude, Gemini, Qwen, OpenRouter, GLM, Kimi, MiniMax, DeepSeek, Groq, xAI, Mistral, Perplexity, Together, Fireworks, Cerebras, Cohere, NVIDIA 等 | 每服务商动态 URL/Header 配置 |
| `AntigravityExecutor` | Google Antigravity | 自定义项目/会话 ID、Retry-After 解析、429 混淆 |
| `AzureOpenAIExecutor` | Azure OpenAI | 基于部署的路由、api-version 查询参数强制执行 |
| `BlackboxWebExecutor` | Blackbox AI (web-mode) | Web 会话反向 + TLS 指纹模拟 |
| `ChatGPTWebExecutor` | ChatGPT web | TLS 客户端 + 会话 Cookie 管理(`chatgptTlsClient.ts`|
| `ClaudeIdentityExecutor` | Claude.ai (CCH 通道) | 约束 + Tool 重映射管线、指纹塑造 |
| `CliProxyApiExecutor` | CLIProxyAPI 兼容服务商 | 自定义认证和协议处理 |
| `CloudflareAiExecutor` | Cloudflare Workers AI | 帐户 ID 注入、基于 Neurons 的用量追踪 |
| `CodexExecutor` | OpenAI Codex | 注入系统指令、强制推理力度 |
| `CommandCodeExecutor` | Command Code | OAuth + 每会话 Header 轮换 |
| `CursorExecutor` | Cursor IDE | ConnectRPC 协议、Protobuf 编码、基于校验和的请求签名|
| `DevinCliExecutor` | Devin CLI | Devin 任务生命周期桥接(通过云代理模块) |
| `GithubExecutor` | GitHub Copilot | Copilot Token 刷新、VSCode 模仿 Header |
| `GitlabExecutor` | GitLab Duo | GitLab OAuth + 项目级路由 |
| `GlmExecutor` | Z.AI GLM`glmt` 预设) | Thinking Budget 感知、GLMT 预设常量 |
| `GrokWebExecutor` | xAI Grok web | Web 会话反向、模式选择think/standard |
| `KieExecutor` | KIE | 自定义 Token 签发 + 轮换会话锚点 |
| `KiroExecutor` | AWS CodeWhisperer/Kiro | AWS EventStream 二进制格式 → SSE 转换 |
| `MuseSparkWebExecutor` | Muse Spark (web) | Web 会话反向 + 图片消息桥接 |
| `NlpCloudExecutor` | NLP Cloud | 服务商专属请求体形状 |
| `OpenCodeExecutor` | OpenCode | AI SDK 兼容服务商初始化 |
| `PerplexityWebExecutor` | Perplexity web | Web 会话反向,用于聊延续 |
| `PetalsExecutor` | Petals distributed inference | 去中心化集群路由 |
| `PollinationsExecutor` | Pollinations AI | 无需 API Key、带速率限制的请求 |
| `QoderExecutor` | Qoder AI | PAT 和 OAuth 支持、多模型免费层 |
| `VertexExecutor` | Google Vertex AI | 服务帐户认证、基于区域的端点 |
| `DevinDesktopExecutor` | Devin Desktop | 导入的 API 密钥 + Connect-protobuf 聊天流 |
| 执行器 | 服务商 | 特殊处理 |
| ------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------ |
| `DefaultExecutor` | OpenAI, Claude, Gemini, Qwen, OpenRouter, GLM, Kimi, MiniMax, DeepSeek, Groq, xAI, Mistral, Perplexity, Together, Fireworks, Cerebras, Cohere, NVIDIA 等 | 每服务商动态 URL/Header 配置 |
| `AntigravityExecutor` | Google Antigravity | 自定义项目/会话 ID、Retry-After 解析、429 混淆 |
| `AzureOpenAIExecutor` | Azure OpenAI | 基于部署的路由、api-version 查询参数强制执行 |
| `BlackboxWebExecutor` | Blackbox AI (web-mode) | Web 会话反向 + TLS 指纹模拟 |
| `ChatGPTWebExecutor` | ChatGPT web | TLS 客户端 + 会话 Cookie 管理(`chatgptTlsClient.ts` |
| `ClaudeIdentityExecutor` | Claude.ai (CCH 通道) | 约束 + Tool 重映射管线、指纹塑造 |
| `CliProxyApiExecutor` | CLIProxyAPI 兼容服务商 | 自定义认证和协议处理 |
| `CloudflareAiExecutor` | Cloudflare Workers AI | 帐户 ID 注入、基于 Neurons 的用量追踪 |
| `CodexExecutor` | OpenAI Codex | 注入系统指令、强制推理力度 |
| `CommandCodeExecutor` | Command Code | OAuth + 每会话 Header 轮换 |
| `CursorExecutor` | Cursor IDE | ConnectRPC 协议、Protobuf 编码、基于校验和的请求签名 |
| `DevinCliExecutor` | Devin CLI | Devin 任务生命周期桥接(通过云代理模块) |
| `GithubExecutor` | GitHub Copilot | Copilot Token 刷新、VSCode 模仿 Header |
| `GitlabExecutor` | GitLab Duo | GitLab OAuth + 项目级路由 |
| `GlmExecutor` | Z.AI GLM`glmt` 预设) | Thinking Budget 感知、GLMT 预设常量 |
| `GrokWebExecutor` | xAI Grok web | Web 会话反向、模式选择think/standard |
| `KieExecutor` | KIE | 自定义 Token 签发 + 轮换会话锚点 |
| `KiroExecutor` | AWS CodeWhisperer/Kiro | AWS EventStream 二进制格式 → SSE 转换 |
| `MuseSparkWebExecutor` | Muse Spark (web) | Web 会话反向 + 图片消息桥接 |
| `NlpCloudExecutor` | NLP Cloud | 服务商专属请求体形状 |
| `OpenCodeExecutor` | OpenCode | AI SDK 兼容服务商初始化 |
| `PerplexityWebExecutor` | Perplexity web | Web 会话反向,用于聊延续 |
| `PetalsExecutor` | Petals distributed inference | 去中心化集群路由 |
| `PollinationsExecutor` | Pollinations AI | 无需 API Key、带速率限制的请求 |
| `QoderExecutor` | Qoder AI | PAT 和 OAuth 支持、多模型免费层 |
| `VertexExecutor` | Google Vertex AI | 服务帐户认证、基于区域的端点 |
| `DevinDesktopExecutor` | Devin Desktop | 导入的 API 密钥 + Connect-protobuf 聊天流 |
其余所有服务商(含自定义兼容节点)使用 `DefaultExecutor`
@@ -935,65 +935,65 @@ flowchart LR
> [`docs/reference/PROVIDER_REFERENCE.md`](../reference/PROVIDER_REFERENCE.md)(自动生成)或数据源头
> `src/shared/constants/providers.ts`(加载时通过 Zod 校验)。
| 服务商 | 格式 | 认证 | 流式 | 非流式 | Token 刷新 | 用量 API |
| ----------------- | ---------------- | --------------------- | -------------- | ---------- | ----------- | ----------------- |
| Claude | claude | API Key / OAuth | ✅ | ✅ | ✅ | ⚠️ 仅管理员 |
| Gemini | gemini | API Key / OAuth | ✅ | ✅ | ✅ | ⚠️ Cloud Console |
| Antigravity | antigravity | OAuth | ✅ | ✅ | ✅ | ✅ 完整配额 API |
| OpenAI | openai | API Key | ✅ | ✅ | ❌ | ❌ |
| Codex | openai-responses | OAuth | ✅ 强制 | ❌ | ✅ | ✅ 速率限制 |
| GitHub Copilot | openai | OAuth + Copilot Token | ✅ | ✅ | ✅ | ✅ 配额快照 |
| Cursor | cursor | 自定义校验和 | ✅ | ✅ | ❌ | ❌ |
| Kiro | kiro | AWS SSO OIDC | ✅ (EventStream)| ❌ | ✅ | ✅ 用量限制 |
| Qwen | openai | OAuth | ✅ | ✅ | ✅ | ⚠️ 每请求 |
| Qoder | openai | OAuth / PAT | ✅ | ✅ | ✅ | ⚠️ 每请求 |
| Kilo Code | openai | OAuth | ✅ | ✅ | ✅ | ❌ |
| Cline | openai | OAuth | ✅ | ✅ | ✅ | ❌ |
| Kimi Coding | openai | OAuth | ✅ | ✅ | ✅ | ❌ |
| OpenRouter | openai | API Key | ✅ | ✅ | ❌ | ❌ |
| GLM/Kimi/MiniMax | claude | API Key | ✅ | ✅ | ❌ | ❌ |
| DeepSeek | openai | API Key | ✅ | ✅ | ❌ | ❌ |
| Groq | openai | API Key | ✅ | ✅ | ❌ | ❌ |
| xAI (Grok) | openai | API Key | ✅ | ✅ | ❌ | ❌ |
| Mistral | openai | API Key | ✅ | ✅ | ❌ | ❌ |
| Perplexity | openai | API Key | ✅ | ✅ | ❌ | ❌ |
| Together AI | openai | API Key | ✅ | ✅ | ❌ | ❌ |
| Fireworks AI | openai | API Key | ✅ | ✅ | ❌ | ❌ |
| Cerebras | openai | API Key | ✅ | ✅ | ❌ | ❌ |
| Cohere | openai | API Key | ✅ | ✅ | ❌ | ❌ |
| NVIDIA NIM | openai | API Key | ✅ | ✅ | ❌ | ❌ |
| Cloudflare AI | openai | API Token + 帐户 ID | ✅ | ✅ | ❌ | ❌ |
| Pollinations | openai | 无需密钥 | ✅ | ✅ | ❌ | ❌ |
| Scaleway AI | openai | API Key | ✅ | ✅ | ❌ | ❌ |
| LongCat | openai | API Key | ✅ | ✅ | ❌ | ❌ |
| Ollama Cloud | openai | API Key可选 | ✅ | ✅ | ❌ | ❌ |
| HuggingFace | openai | API Key | ✅ | ✅ | ❌ | ❌ |
| Nebius | openai | API Key | ✅ | ✅ | ❌ | ❌ |
| SiliconFlow | openai | API Key | ✅ | ✅ | ❌ | ❌ |
| Hyperbolic | openai | API Key | ✅ | ✅ | ❌ | ❌ |
| Vertex AI | gemini | 服务帐户 | ✅ | ✅ | ✅ | ⚠️ Cloud Console |
| Command Code | openai | OAuth | ✅ | ✅ | ✅ | ⚠️ 每请求 |
| Z.AI / GLM | openai | API Key / OAuth | ✅ | ✅ | ❌ | ❌ |
| GLMT (preset) | claude | API Key | ✅ | ✅ | ❌ | ⚠️ 每请求 |
| Kimi Coding | openai | OAuth / API Key | ✅ | ✅ | ✅ | ❌ |
| KIE | openai | API Key | ✅ | ✅ | ❌ | ❌ |
| Devin Desktop | openai | 导入的 API 密钥 | ✅ (Connect→SSE) | ✅ | ❌ | ⚠️ 每请求 |
| GitLab Duo | openai | OAuth (GitLab) | ✅ | ✅ | ✅ | ❌ |
| Devin CLI | openai | 本地 CLI 登录 | ✅ | ✅ | ❌ | ✅ 任务 API |
| Codex Cloud | openai-responses | OAuth | ✅ | ❌ | ✅ | ✅ 速率限制 |
| Jules | openai | OAuth | ✅ | ✅ | ✅ | ✅ 任务 API |
| AgentRouter | openai | API Key | ✅ | ✅ | ❌ | ❌ |
| ChatGPT-Web | openai | 会话 Cookie + TLS | ✅ | ✅ | ❌ | ❌ |
| Grok-Web | openai | 会话 Cookie | ✅ | ✅ | ❌ | ❌ |
| Perplexity-Web | openai | 会话 Cookie | ✅ | ✅ | ❌ | ❌ |
| BlackBox-Web | openai | 会话 Cookie + TLS | ✅ | ✅ | ❌ | ❌ |
| Muse-Spark-Web | openai | 会话 Cookie | ✅ | ✅ | ❌ | ❌ |
| ModelScope | openai | API Key | ✅ | ✅ | ❌ | ⚠️ 配额策略 |
| BazaarLink | openai | API Key | ✅ | ✅ | ❌ | ❌ |
| Petals | openai | 无需密钥 | ✅ | ✅ | ❌ | ❌ |
| Qoder | openai | OAuth / PAT | ✅ | ✅ | ✅ | ⚠️ 每请求 |
| OpenCode (Go/Zen) | openai | OAuth | ✅ | ✅ | ✅ | ❌ |
| CLIProxyAPI | openai | 自定义 | ✅ | ✅ | ❌ | ❌ |
| 服务商 | 格式 | 认证 | 流式 | 非流式 | Token 刷新 | 用量 API |
| ----------------- | ---------------- | --------------------- | ---------------- | ------ | ---------- | ---------------- |
| Claude | claude | API Key / OAuth | ✅ | ✅ | ✅ | ⚠️ 仅管理员 |
| Gemini | gemini | API Key / OAuth | ✅ | ✅ | ✅ | ⚠️ Cloud Console |
| Antigravity | antigravity | OAuth | ✅ | ✅ | ✅ | ✅ 完整配额 API |
| OpenAI | openai | API Key | ✅ | ✅ | ❌ | ❌ |
| Codex | openai-responses | OAuth | ✅ 强制 | ❌ | ✅ | ✅ 速率限制 |
| GitHub Copilot | openai | OAuth + Copilot Token | ✅ | ✅ | ✅ | ✅ 配额快照 |
| Cursor | cursor | 自定义校验和 | ✅ | ✅ | ❌ | ❌ |
| Kiro | kiro | AWS SSO OIDC | ✅ (EventStream) | ❌ | ✅ | ✅ 用量限制 |
| Qwen | openai | OAuth | ✅ | ✅ | ✅ | ⚠️ 每请求 |
| Qoder | openai | OAuth / PAT | ✅ | ✅ | ✅ | ⚠️ 每请求 |
| Kilo Code | openai | OAuth | ✅ | ✅ | ✅ | ❌ |
| Cline | openai | OAuth | ✅ | ✅ | ✅ | ❌ |
| Kimi Coding | openai | OAuth | ✅ | ✅ | ✅ | ❌ |
| OpenRouter | openai | API Key | ✅ | ✅ | ❌ | ❌ |
| GLM/Kimi/MiniMax | claude | API Key | ✅ | ✅ | ❌ | ❌ |
| DeepSeek | openai | API Key | ✅ | ✅ | ❌ | ❌ |
| Groq | openai | API Key | ✅ | ✅ | ❌ | ❌ |
| xAI (Grok) | openai | API Key | ✅ | ✅ | ❌ | ❌ |
| Mistral | openai | API Key | ✅ | ✅ | ❌ | ❌ |
| Perplexity | openai | API Key | ✅ | ✅ | ❌ | ❌ |
| Together AI | openai | API Key | ✅ | ✅ | ❌ | ❌ |
| Fireworks AI | openai | API Key | ✅ | ✅ | ❌ | ❌ |
| Cerebras | openai | API Key | ✅ | ✅ | ❌ | ❌ |
| Cohere | openai | API Key | ✅ | ✅ | ❌ | ❌ |
| NVIDIA NIM | openai | API Key | ✅ | ✅ | ❌ | ❌ |
| Cloudflare AI | openai | API Token + 帐户 ID | ✅ | ✅ | ❌ | ❌ |
| Pollinations | openai | 无需密钥 | ✅ | ✅ | ❌ | ❌ |
| Scaleway AI | openai | API Key | ✅ | ✅ | ❌ | ❌ |
| LongCat | openai | API Key | ✅ | ✅ | ❌ | ❌ |
| Ollama Cloud | openai | API Key可选 | ✅ | ✅ | ❌ | ❌ |
| HuggingFace | openai | API Key | ✅ | ✅ | ❌ | ❌ |
| Nebius | openai | API Key | ✅ | ✅ | ❌ | ❌ |
| SiliconFlow | openai | API Key | ✅ | ✅ | ❌ | ❌ |
| Hyperbolic | openai | API Key | ✅ | ✅ | ❌ | ❌ |
| Vertex AI | gemini | 服务帐户 | ✅ | ✅ | ✅ | ⚠️ Cloud Console |
| Command Code | openai | OAuth | ✅ | ✅ | ✅ | ⚠️ 每请求 |
| Z.AI / GLM | openai | API Key / OAuth | ✅ | ✅ | ❌ | ❌ |
| GLMT (preset) | claude | API Key | ✅ | ✅ | ❌ | ⚠️ 每请求 |
| Kimi Coding | openai | OAuth / API Key | ✅ | ✅ | ✅ | ❌ |
| KIE | openai | API Key | ✅ | ✅ | ❌ | ❌ |
| Devin Desktop | openai | 导入的 API 密钥 | ✅ (Connect→SSE) | ✅ | ❌ | ⚠️ 每请求 |
| GitLab Duo | openai | OAuth (GitLab) | ✅ | ✅ | ✅ | ❌ |
| Devin CLI | openai | 本地 CLI 登录 | ✅ | ✅ | ❌ | ✅ 任务 API |
| Codex Cloud | openai-responses | OAuth | ✅ | ❌ | ✅ | ✅ 速率限制 |
| Jules | openai | OAuth | ✅ | ✅ | ✅ | ✅ 任务 API |
| AgentRouter | openai | API Key | ✅ | ✅ | ❌ | ❌ |
| ChatGPT-Web | openai | 会话 Cookie + TLS | ✅ | ✅ | ❌ | ❌ |
| Grok-Web | openai | 会话 Cookie | ✅ | ✅ | ❌ | ❌ |
| Perplexity-Web | openai | 会话 Cookie | ✅ | ✅ | ❌ | ❌ |
| BlackBox-Web | openai | 会话 Cookie + TLS | ✅ | ✅ | ❌ | ❌ |
| Muse-Spark-Web | openai | 会话 Cookie | ✅ | ✅ | ❌ | ❌ |
| ModelScope | openai | API Key | ✅ | ✅ | ❌ | ⚠️ 配额策略 |
| BazaarLink | openai | API Key | ✅ | ✅ | ❌ | ❌ |
| Petals | openai | 无需密钥 | ✅ | ✅ | ❌ | ❌ |
| Qoder | openai | OAuth / PAT | ✅ | ✅ | ✅ | ⚠️ 每请求 |
| OpenCode (Go/Zen) | openai | OAuth | ✅ | ✅ | ✅ | ❌ |
| CLIProxyAPI | openai | 自定义 | ✅ | ✅ | ❌ | ❌ |
## 格式转换覆盖
@@ -1029,25 +1029,25 @@ flowchart LR
## 支持的 API 端点
| 端点 | 格式 | 处理器 |
| --------------------------------------------------- | ------------------ | ----------------------------------------------------------------- |
| `POST /v1/chat/completions` | OpenAI Chat | `src/sse/handlers/chat.ts` |
| `POST /v1/messages` | Claude Messages | 同一处理器(自动检测) |
| `POST /v1/responses` | OpenAI Responses | `open-sse/handlers/responsesHandler.ts` |
| `POST /v1/embeddings` | OpenAI Embeddings | `open-sse/handlers/embeddings.ts` |
| `GET /v1/embeddings` | 模型列表 | API 路由 |
| `POST /v1/images/generations` | OpenAI Images | `open-sse/handlers/imageGeneration.ts` |
| `GET /v1/images/generations` | 模型列表 | API 路由 |
| `POST /v1/providers/{provider}/chat/completions` | OpenAI Chat | 带模型校验的专用每服务商路由 |
| `POST /v1/providers/{provider}/embeddings` | OpenAI Embeddings | 带模型校验的专用每服务商路由 |
| `POST /v1/providers/{provider}/images/generations` | OpenAI Images | 带模型校验的专用每服务商路由 |
| `POST /v1/messages/count_tokens` | Claude Token Count | API 路由 |
| `GET /v1/models` | OpenAI Models 列表 | API 路由(聊 + 向量嵌入 + 图片 + 自定义模型) |
| `GET /api/models/catalog` | 目录 | 按服务商 + 类型分组的所有模型 |
| `POST /v1beta/models/*:streamGenerateContent` | Gemini native | API 路由 |
| `GET/PUT/DELETE /api/settings/proxy` | 代理配置 | 网络代理配置 |
| `POST /api/settings/proxy/test` | 代理连通性 | 代理健康/连通性测试端点 |
| `GET/POST/DELETE /api/provider-models` | 服务商模型 | 服务商模型元数据,支撑自定义和管理可用模型 |
| 端点 | 格式 | 处理器 |
| -------------------------------------------------- | ------------------ | --------------------------------------------- |
| `POST /v1/chat/completions` | OpenAI Chat | `src/sse/handlers/chat.ts` |
| `POST /v1/messages` | Claude Messages | 同一处理器(自动检测) |
| `POST /v1/responses` | OpenAI Responses | `open-sse/handlers/responsesHandler.ts` |
| `POST /v1/embeddings` | OpenAI Embeddings | `open-sse/handlers/embeddings.ts` |
| `GET /v1/embeddings` | 模型列表 | API 路由 |
| `POST /v1/images/generations` | OpenAI Images | `open-sse/handlers/imageGeneration.ts` |
| `GET /v1/images/generations` | 模型列表 | API 路由 |
| `POST /v1/providers/{provider}/chat/completions` | OpenAI Chat | 带模型校验的专用每服务商路由 |
| `POST /v1/providers/{provider}/embeddings` | OpenAI Embeddings | 带模型校验的专用每服务商路由 |
| `POST /v1/providers/{provider}/images/generations` | OpenAI Images | 带模型校验的专用每服务商路由 |
| `POST /v1/messages/count_tokens` | Claude Token Count | API 路由 |
| `GET /v1/models` | OpenAI Models 列表 | API 路由(聊 + 向量嵌入 + 图片 + 自定义模型) |
| `GET /api/models/catalog` | 目录 | 按服务商 + 类型分组的所有模型 |
| `POST /v1beta/models/*:streamGenerateContent` | Gemini native | API 路由 |
| `GET/PUT/DELETE /api/settings/proxy` | 代理配置 | 网络代理配置 |
| `POST /api/settings/proxy/test` | 代理连通性 | 代理健康/连通性测试端点 |
| `GET/POST/DELETE /api/provider-models` | 服务商模型 | 服务商模型元数据,支撑自定义和管理可用模型 |
## 旁路处理器

View File

@@ -322,7 +322,7 @@ flowchart LR
OAuth 提供者模組(`src/lib/oauth/providers/` 下的 22 個個別檔案):
- 註冊表索引:`src/lib/oauth/providers/index.ts`
- 個別提供者:`agy.ts`, `antigravity.ts`, `claude.ts`, `cline.ts`, `codebuddy-cn.ts`, `codex.ts`, `cursor.ts`, `devin-desktop.ts`, `ghe-copilot.ts`, `github.ts`, `gitlab-duo.ts`, `grok-cli-oauth.ts`, `grok-cli.ts`, `kilocode.ts`, `kimi-coding.ts`, `kiro.ts`, `qoder.ts`, `raycast.ts`, `trae.ts`, `xai-oauth.ts`, `zed-hosted.ts`, `zed.ts`
- 個別提供者:`agy.ts`, `antigravity.ts`, `claude.ts`, `cline.ts`, `codebuddy-cn.ts`, `codex.ts`, `cursor.ts`, `devin-desktop.ts`, `ghe-copilot.ts`, `github.ts`, `gitlab-duo.ts`, `grok-cli-oauth.ts`, `grok-cli.ts`, `kilocode.ts`, `kimi-coding.ts`, `kiro.ts`, `openference.ts`, `qoder.ts`, `trae.ts`, `xai-oauth.ts`, `zed-hosted.ts`, `zed.ts`
- 薄包裝層:`src/lib/oauth/providers.ts` — 從個別模組重新匯出
## 5) 嵌入式服務v3.8.4
@@ -901,7 +901,7 @@ flowchart LR
| `PollinationsExecutor` | Pollinations AI | 無需 API 金鑰、速率限制請求 |
| `QoderExecutor` | Qoder AI | PAT 與 OAuth 支援、多模型免費方案 |
| `VertexExecutor` | Google Vertex AI | 服務帳戶驗證、基於區域的端點 |
| `DevinDesktopExecutor` | Devin Desktop | 匯入的 API 金鑰 + Connect-protobuf 聊天串流 |
| `DevinDesktopExecutor` | Devin Desktop | 匯入的 API 金鑰 + Connect-protobuf 聊天串流 |
所有其他提供者(包括自訂相容節點)使用 `DefaultExecutor`
@@ -953,9 +953,9 @@ flowchart LR
| GLMT預設 | claude | API 金鑰 | ✅ | ✅ | ❌ | ⚠️ 每次請求 |
| Kimi Coding | openai | OAuth / API 金鑰 | ✅ | ✅ | ✅ | ❌ |
| KIE | openai | API 金鑰 | ✅ | ✅ | ❌ | ❌ |
| Devin Desktop | openai | 匯入的 API 金鑰 | ✅ (Connect→SSE) | ✅ | ❌ | ⚠️ 每次請求 |
| Devin Desktop | openai | 匯入的 API 金鑰 | ✅ (Connect→SSE) | ✅ | ❌ | ⚠️ 每次請求 |
| GitLab Duo | openai | OAuthGitLab | ✅ | ✅ | ✅ | ❌ |
| Devin CLI | openai | 本機 CLI 登入 | ✅ | ✅ | ❌ | ✅ 任務 API |
| Devin CLI | openai | 本機 CLI 登入 | ✅ | ✅ | ❌ | ✅ 任務 API |
| Codex Cloud | openai-responses | OAuth | ✅ | ❌ | ✅ | ✅ 速率限制 |
| Jules | openai | OAuth | ✅ | ✅ | ✅ | ✅ 任務 API |
| AgentRouter | openai | API 金鑰 | ✅ | ✅ | ❌ | ❌ |

View File

@@ -598,10 +598,6 @@ Built-in credentials for **localhost development**. For remote deployments, regi
| `BLACKBOX_WEB_VALIDATED_TOKEN` | Blackbox Web | Frontend `tk` token to send as `validated` on `/api/chat`. Required when Blackbox enforces token matching; otherwise OmniRoute falls back to a random UUID. See issue #2252. |
| `VISION_BRIDGE_BASE_URL` | Vision Bridge guardrail | OpenAI-compatible base URL for non-Anthropic vision-bridge calls. Defaults to the legacy OpenAI URL env or api.openai.com. Point at OmniRoute's `/v1` self-loop or any OpenAI-compat endpoint (Gemini OpenAI-compat, OpenRouter). Issue #2232. When the URL is OmniRoute's own `/v1`, the describe sub-request sends `x-omniroute-admission-bypass: internal` and authenticates with the resolved self-loop credential (`sk_omniroute` sentinel in local mode, or `OMNIROUTE_API_KEY` / `ROUTER_API_KEY`#1350) so `REQUIRE_API_KEY=true` deployments work. |
| `VISION_BRIDGE_API_KEY` | Vision Bridge guardrail | API key for the URL above. Overrides per-provider OpenAI / Google env vars for non-Anthropic vision-bridge calls. Anthropic models keep their dedicated Anthropic key path. Issue #2232. |
| `RAYCAST_BEARER_TOKEN` | Raycast Pro | Optional manual override for the Raycast access token (normally captured via macOS Auto-Import). No OAuth client_id/secret — reverse-engineered, local/personal use only. |
| `RAYCAST_DEVICE_ID` | Raycast Pro | Optional manual override for the Raycast device ID used to sign requests. |
| `RAYCAST_AID` | Raycast Pro | Optional manual override for the Raycast account/app ID; falls back to the device ID when unset. |
| `RAYCAST_SIG_SECRET` | Raycast Pro | Optional override for the request-signing HMAC secret. Defaults to a community-extracted value in `open-sse/services/raycast.ts`. |
> [!WARNING]
>

View File

@@ -52,7 +52,7 @@ Use the dashboard at `/dashboard/providers` to enable, configure, and test each
| `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)
## OAuth Providers (24)
| ID | Alias | Name | Tags | Website | Notes |
|----|-------|------|------|---------|-------|
@@ -76,13 +76,12 @@ Use the dashboard at `/dashboard/providers` to enable, configure, and test each
| `kiro` | `kr` | Kiro AI | OAuth | — | Free tier: 50 credits/month (~25K100K 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)
## Web Cookie Providers (34)
| ID | Alias | Name | Tags | Website | Notes | Tool calling |
|----|-------|------|------|---------|-------|--------------|
@@ -100,7 +99,6 @@ Use the dashboard at `/dashboard/providers` to enable, configure, and test each
| `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 the full cookie header, the __Secure-1PSID value, or the JSON export containing cookies from gemini.google.com. Include __Secure-1PSIDTS and __Secure-1PSIDCC when available. | 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://chat.minimax.io) | 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 |

View File

@@ -24,7 +24,6 @@ import { glmProvider } from "./registry/glm/index.ts";
import { glmtProvider } from "./registry/glm/t/index.ts";
import { glm_cnProvider } from "./registry/glm/cn/index.ts";
import { traeProvider } from "./registry/trae/index.ts";
import { raycastProvider } from "./registry/raycast/index.ts";
import { muse_spark_webProvider } from "./registry/muse-spark-web/index.ts";
import { lmarenaProvider } from "./registry/lmarena/index.ts";
import { kilocodeProvider } from "./registry/kilocode/index.ts";
@@ -116,7 +115,6 @@ import { perplexityProvider } from "./registry/perplexity/index.ts";
import { perplexity_webProvider } from "./registry/perplexity/web/index.ts";
import { minimaxProvider } from "./registry/minimax/index.ts";
import { minimax_cnProvider } from "./registry/minimax/cn/index.ts";
import { hailuo_webProvider } from "./registry/minimax/web/index.ts";
import { haiperProvider } from "./registry/haiper/index.ts";
import { bytezProvider } from "./registry/bytez/index.ts";
import { blackboxProvider } from "./registry/blackbox/index.ts";
@@ -294,7 +292,6 @@ export const REGISTRY: Record<string, RegistryEntry> = {
glmt: glmtProvider,
"glm-cn": glm_cnProvider,
trae: traeProvider,
raycast: raycastProvider,
"muse-spark-web": muse_spark_webProvider,
lmarena: lmarenaProvider,
kilocode: kilocodeProvider,
@@ -386,7 +383,6 @@ export const REGISTRY: Record<string, RegistryEntry> = {
"perplexity-web": perplexity_webProvider,
minimax: minimaxProvider,
"minimax-cn": minimax_cnProvider,
"hailuo-web": hailuo_webProvider,
haiper: haiperProvider,
bytez: bytezProvider,
blackbox: blackboxProvider,

View File

@@ -1,23 +0,0 @@
import type { RegistryEntry } from "../../../shared.ts";
export const HAILUO_WEB_STATIC_MODELS = [
// The Hailuo web client does not expose a model selector in its chat API —
// one default assistant persona (characterID) handles every request. See
// open-sse/executors/hailuo-web.ts for the ported g4f protocol details.
{ id: "hailuo", name: "Hailuo (MiniMax)" },
];
export const hailuo_webProvider: RegistryEntry = {
id: "hailuo-web",
// Distinct alias: the paid API-key "minimax"/"minimax-cn" providers
// (../../minimax/index.ts) keep their own short alias; this free web/cookie
// variant is addressed by its own id, per the established kimi-web/qwen-web
// secondary-variant convention (tests/unit/provider-alias-uniqueness.test.ts).
alias: "hailuo-web",
format: "openai",
executor: "hailuo-web",
baseUrl: "https://chat.minimax.io",
authType: "apikey",
authHeader: "bearer",
models: HAILUO_WEB_STATIC_MODELS,
};

View File

@@ -1,61 +0,0 @@
/**
* @file index.ts
* @description Raycast Pro AI provider registry entry (reverse-engineered, unofficial API).
*
* @changes
* - [2026-07-28] [Composer] - Initial Raycast provider registry module
*/
import type { RegistryEntry } from "../../shared.ts";
/** Seed catalog — full list synced from Raycast /api/v1/ai/models on connect/import. */
export const raycastProvider: RegistryEntry = {
id: "raycast",
alias: "rc",
format: "openai",
executor: "raycast",
baseUrl: "https://backend.raycast.com/api/v1/ai",
authType: "oauth",
authHeader: "bearer",
defaultContextLength: 128000,
models: [
//GPT
{ id: "openai-gpt-5.6-sol", name: "GPT-5.6 Sol" },
{ id: "openai-gpt-5.6-terra", name: "GPT-5.6 Terra" },
{ id: "openai-gpt-5.6-luna", name: "GPT-5.6 Luna" },
//Claude
{ id: "anthropic-claude-opus-5", name: "Claude Opus 5" },
{ id: "anthropic-claude-sonnet-5", name: "Claude Sonnet 5" },
{ id: "anthropic-claude-4-5-haiku-reasoning", name: "Claude 4.5 Haiku Reasoning" },
{ id: "anthropic-claude-4-5-haiku", name: "Claude 4.5 Haiku" },
//Gemini
{ id: "google-gemini-3.1-pro", name: "Gemini 3.1 Pro" },
{ id: "google-gemini-3.7-flash", name: "Gemini 3.7 Flash" },
{ id: "google-gemini-3.5-flash-lite", name: "Gemini 3.5 Flash Lite" },
//Perplexity
{ id: "perplexity-sonar-reasoning-pro", name: "Sonar Reasoning Pro" },
{ id: "perplexity-sonar-pro", name: "Sonar Pro" },
{ id: "perplexity-sonar", name: "Sonar" },
//Mistral
{ id: "mistral-mistral-large-latest", name: "Mistral Large" },
{ id: "mistral-mistral-medium-latest", name: "Mistral Medium" },
{ id: "mistral-mistral-small-latest", name: "Mistral Small" },
{ id: "mistral-codestral-latest", name: "Codestral" },
{ id: "mistral-open-mistral-nemo", name: "Mistral Nemo" },
//Grok
{ id: "xai-grok-4.6", name: "Grok 4.6" },
//Opensource
{ id: "gateway-alibaba/qwen3.8-max", name: "Qwen 3.8 Max" },
{ id: "gateway-moonshotai/kimi-k3", name: "Kimi K3" },
{ id: "baseten-deepseek-ai/DeepSeek-V4-Pro", name: "DeepSeek V4 Pro" },
{ id: "gateway-deepseek/deepseek-v4-flash", name: "DeepSeek V4 Flash" },
{ id: "baseten-zai-org/GLM-5.2", name: "GLM 5.2" },
{ id: "gateway-thinkingmachines/inkling-1.0", name: "Inkling 1.0" },
{ id: "gateway-google/gemma-4-31b-it", name: "Gemma 4 31B" },
{ id: "groq-openai/gpt-oss-120b", name: "GPT-OSS 120B" },
{ id: "groq-openai/gpt-oss-20b", name: "GPT-OSS 20B" },
{ id: "groq-qwen/qwen3-32b", name: "Qwen 3 32B" },
{ id: "groq-llama-3.3-70b-versatile", name: "LLaMA 3.3 70B" },
{ id: "groq-llama-3.1-8b-instant", name: "LLaMA 3.1 8B" },
],
};

View File

@@ -1,546 +0,0 @@
/**
* HailuoWebExecutor — Hailuo AI (MiniMax) web chat via chat.minimax.io.
*
* Distinct from the paid API-key `minimax`/`minimax-cn` providers
* (open-sse/config/providers/registry/minimax/) — this targets the free
* consumer chat product at chat.minimax.io.
*
* Endpoint: POST https://chat.minimax.io/v4/api/chat/msg?<fingerprint query>
* Auth: `token` header — value read from the site's `_token` localStorage
* entry, plus a per-request `yy` signature header.
* Body: multipart/form-data — characterID, msgContent, chatID, searchMode.
* Response: text/event-stream lines (`event:` / `data:`) carrying
* `send_result` (chat title + chatID, once) and `message_result`
* (cumulative — not delta — `content` field per event) until a
* `close_chunk` event ends the stream.
*
* Ported from the g4f reference implementation
* (g4f/Provider/needs_auth/mini_max/{HailuoAI,crypt}.py) — request signing
* (`generate_yy_header`/`get_body_to_yy`) and the SSE event shape are ported
* 1:1. The device-fingerprint fields (device_id, uuid, os/browser name,
* screen dims) are normally generated by the browser and stored in
* localStorage; when the user hasn't captured them, this executor derives
* stable per-connection values from the token via MD5 so the signature stays
* consistent across requests without server-side state.
*
* ⚠️ Not yet validated against a live hailuo.ai session — see PR description
* for the exact VPS live-check command that must be run before this is
* treated as fully verified. The host, API path, header shape, and signing
* scheme are ported directly from the (actively maintained) g4f source, but
* upstream reverse-engineered protocols can change without notice.
*/
import { createHash } from "node:crypto";
import { BaseExecutor, type ExecuteInput } from "./base.ts";
import { makeExecutorErrorResult as makeErrorResult, sanitizeErrorMessage } from "../utils/error.ts";
const BASE_URL = "https://chat.minimax.io";
const API_PATH = "/v4/api/chat/msg";
const USER_AGENT =
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/149.0.0.0 Safari/537.36";
const DEFAULT_CHARACTER_ID = "1";
const DEFAULT_CHAT_ID = "0";
type JsonRecord = Record<string, unknown>;
function asRecord(value: unknown): JsonRecord {
return value && typeof value === "object" && !Array.isArray(value) ? (value as JsonRecord) : {};
}
function toStringOrEmpty(value: unknown): string {
return typeof value === "string" ? value.trim() : "";
}
function md5(input: string): string {
return createHash("md5").update(input, "utf8").digest("hex");
}
/**
* Percent-encode matching Python's `urllib.parse.quote(s, safe="")` — encode
* every byte except the always-safe RFC 3986 unreserved set (letters,
* digits, `_.-~`). `encodeURIComponent` leaves a few extra characters
* (`!*'()`) unescaped, so it is not a drop-in replacement for the upstream
* signature to match byte-for-byte.
*/
export function pyQuote(input: string): string {
const bytes = new TextEncoder().encode(input);
let out = "";
for (const byte of bytes) {
const ch = String.fromCharCode(byte);
if (/[A-Za-z0-9_.\-~]/.test(ch)) {
out += ch;
} else {
out += `%${byte.toString(16).toUpperCase().padStart(2, "0")}`;
}
}
return out;
}
/** Port of `get_body_to_yy()` from crypt.py. */
export function getBodyToYy(characterID: string, msgContent: string, chatID: string): string {
const normalized = msgContent.replace(/\r\n/g, "").replace(/\n/g, "").replace(/\r/g, "");
return md5(characterID) + md5(normalized) + md5(chatID) + md5("");
}
/** Port of `generate_yy_header()` from crypt.py. */
export function generateYyHeader(
pathAndQuery: string,
bodyToYy: string,
timestampMs: number
): string {
const encodedPath = pyQuote(pathAndQuery);
const timeHash = md5(String(timestampMs));
const combined = `${encodedPath}_${bodyToYy}${timeHash}ooui`;
return md5(combined);
}
/**
* Derive a stable per-connection fingerprint id from the token when the user
* hasn't captured the real browser-generated value from localStorage. Pure
* function of the token, so it stays identical across requests without
* needing to persist any new state.
*/
function deriveFingerprintId(token: string, salt: string): string {
return md5(`${token}:${salt}`);
}
export function buildHailuoPathAndQuery(
token: string,
providerSpecificData: unknown,
unixMs: number
): string {
const data = asRecord(providerSpecificData);
const deviceId =
toStringOrEmpty(data.device_id) ||
toStringOrEmpty(data.deviceId) ||
deriveFingerprintId(token, "device_id");
const uuid = toStringOrEmpty(data.uuid) || deriveFingerprintId(token, "uuid");
const params = new URLSearchParams({
device_platform: "web",
biz_id: "2",
app_id: "3001",
version_code: "22201",
lang: "en",
uuid,
device_id: deviceId,
os_name: toStringOrEmpty(data.os_name) || "Windows",
browser_name: toStringOrEmpty(data.browser_name) || "chrome",
cpu_core_num: toStringOrEmpty(data.cpu_core_num) || "8",
browser_language: toStringOrEmpty(data.browser_language) || "en-US",
browser_platform: toStringOrEmpty(data.browser_platform) || "Win32",
screen_width: toStringOrEmpty(data.screen_width) || "1920",
screen_height: toStringOrEmpty(data.screen_height) || "1080",
unix: String(unixMs),
});
return `${API_PATH}?${params.toString()}`;
}
type HailuoInputMessage = {
role: string;
content: unknown;
tool_calls?: unknown;
};
function textFromContent(content: unknown): string {
if (typeof content === "string") return content;
if (!Array.isArray(content)) {
throw new Error("Hailuo Web only supports text message content");
}
return content
.map((part) => {
if (!part || typeof part !== "object" || Array.isArray(part)) {
throw new Error("Hailuo Web only supports text message content");
}
const record = part as Record<string, unknown>;
if (
(record.type === "text" || record.type === "input_text") &&
typeof record.text === "string"
) {
return record.text;
}
throw new Error("Hailuo Web does not support image, audio, file, or tool content");
})
.join("");
}
/** Fold text-only OpenAI history into the single msgContent field Hailuo accepts. */
export function foldHailuoMessages(messages: HailuoInputMessage[]): string {
const parts: string[] = [];
for (const message of messages) {
if (message.role === "tool" || message.role === "function") {
throw new Error("Hailuo Web does not support tool result messages");
}
if (message.tool_calls !== undefined) {
throw new Error("Hailuo Web does not support assistant tool calls");
}
const text = textFromContent(message.content);
if (!text) continue;
if (message.role === "system" || message.role === "developer") {
parts.push(`System: ${text}`);
} else if (message.role === "user") {
parts.push(parts.length > 0 ? `User: ${text}` : text);
} else if (message.role === "assistant") {
parts.push(`Assistant: ${text}`);
} else {
throw new Error(`Hailuo Web does not support message role ${message.role}`);
}
}
return parts.join("\n\n").trim();
}
export interface HailuoStreamState {
emittedLen: number;
}
/** `message_result.content` is a cumulative snapshot, not a delta — diff it. */
export function extractHailuoMessageDelta(content: string, state: HailuoStreamState): string {
if (typeof content !== "string" || content.length <= state.emittedLen) return "";
const delta = content.slice(state.emittedLen);
state.emittedLen = content.length;
return delta;
}
export type HailuoSseLine =
| { type: "event"; value: string }
| { type: "data"; value: unknown }
| null;
/** Parse a single raw SSE line. Malformed/truncated `data:` lines are swallowed, not thrown. */
export function parseHailuoLine(line: string): HailuoSseLine {
if (line.startsWith("event:")) {
return { type: "event", value: line.slice(6).trim() };
}
if (line.startsWith("data:")) {
const raw = line.slice(5).trim();
try {
return { type: "data", value: JSON.parse(raw) };
} catch {
return null;
}
}
return null;
}
export function extractHailuoMessageResultContent(data: unknown): string | null {
const root = asRecord(data);
const payload = asRecord(root.data);
const messageResult = asRecord(payload.messageResult);
return typeof messageResult.content === "string" ? messageResult.content : null;
}
function openAiChunk(id: string, created: number, modelId: string, content: string): JsonRecord {
return {
id,
object: "chat.completion.chunk",
created,
model: modelId,
choices: [{ index: 0, delta: { content }, finish_reason: null }],
};
}
function openAiCompletion(id: string, created: number, modelId: string, content: string): JsonRecord {
return {
id,
object: "chat.completion",
created,
model: modelId,
choices: [{ index: 0, message: { role: "assistant", content }, finish_reason: "stop" }],
};
}
export class HailuoWebExecutor extends BaseExecutor {
constructor() {
super("hailuo-web", { id: "hailuo-web", baseUrl: BASE_URL });
}
private buildStreamHeaders(token: string, yy: string): Record<string, string> {
return {
Accept: "text/event-stream",
"User-Agent": USER_AGENT,
Origin: BASE_URL,
Referer: `${BASE_URL}/`,
token,
yy,
};
}
private async streamToText(
upstream: Response,
onDelta: (delta: string) => void
): Promise<{ ok: boolean; errorMessage?: string }> {
const reader = upstream.body?.getReader();
if (!reader) return { ok: true };
const decoder = new TextDecoder();
const state: HailuoStreamState = { emittedLen: 0 };
let currentEvent = "";
let buffer = "";
const processLine = (line: string): "continue" | "close" => {
const parsed = parseHailuoLine(line);
if (!parsed) return "continue";
if (parsed.type === "event") {
currentEvent = parsed.value;
if (currentEvent === "close_chunk") return "close";
return "continue";
}
if (currentEvent === "message_result") {
const content = extractHailuoMessageResultContent(parsed.value);
if (content !== null) {
const delta = extractHailuoMessageDelta(content, state);
if (delta) onDelta(delta);
}
}
return "continue";
};
try {
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split(/\r?\n/);
buffer = lines.pop() || "";
for (const line of lines) {
if (processLine(line) === "close") return { ok: true };
}
}
if (buffer) processLine(buffer);
return { ok: true };
} catch (error) {
return {
ok: false,
errorMessage: error instanceof Error ? error.message : "Hailuo stream read failed",
};
}
}
/** Validate tool/function-call fields and fold messages into a single msgContent string. */
private prepareMsgContent(bodyObj: JsonRecord): { msgContent: string } | { error: string } {
const tools = bodyObj.tools;
const functions = bodyObj.functions;
if (tools != null && (!Array.isArray(tools) || tools.length > 0)) {
return { error: "Hailuo Web does not support OpenAI function tools" };
}
if (functions != null && (!Array.isArray(functions) || functions.length > 0)) {
return { error: "Hailuo Web does not support legacy function tools" };
}
try {
const messages = Array.isArray(bodyObj.messages)
? (bodyObj.messages as HailuoInputMessage[])
: [];
const msgContent = foldHailuoMessages(messages);
if (!msgContent) throw new Error("Hailuo Web requires a non-empty user message");
return { msgContent };
} catch (error) {
return { error: error instanceof Error ? error.message : "Invalid Hailuo Web request" };
}
}
/** Build the signed request: URL, headers, and the multipart form body. */
private buildSignedRequest(
token: string,
providerSpecificData: unknown,
msgContent: string
): { url: string; headers: Record<string, string>; form: FormData } {
const now = Date.now();
const pathAndQuery = buildHailuoPathAndQuery(token, providerSpecificData, now);
const psd = asRecord(providerSpecificData);
const characterID = toStringOrEmpty(psd.characterID) || DEFAULT_CHARACTER_ID;
const chatID = toStringOrEmpty(psd.chatID) || DEFAULT_CHAT_ID;
const bodyToYy = getBodyToYy(characterID, msgContent, chatID);
const yy = generateYyHeader(pathAndQuery, bodyToYy, now);
const form = new FormData();
form.set("characterID", characterID);
form.set("msgContent", msgContent);
form.set("chatID", chatID);
form.set("searchMode", "0");
return { url: `${BASE_URL}${pathAndQuery}`, headers: this.buildStreamHeaders(token, yy), form };
}
/** POST the signed multipart request and normalize both network + upstream-status errors. */
private async dispatch(
url: string,
reqHeaders: Record<string, string>,
form: FormData,
signal: AbortSignal | null | undefined,
body: unknown,
bodyObj: JsonRecord
): Promise<{ upstream: Response } | { errorResult: ReturnType<typeof makeErrorResult> }> {
let upstream: Response;
try {
upstream = await fetch(url, { method: "POST", headers: reqHeaders, body: form, signal });
} catch (err) {
return {
errorResult: {
...makeErrorResult(
502,
`Hailuo fetch failed: ${err instanceof Error ? err.message : "unknown"}`,
body,
url
),
headers: reqHeaders,
transformedBody: bodyObj,
},
};
}
if (!upstream.ok) {
const errText = await upstream.text().catch(() => "");
return {
errorResult: {
...makeErrorResult(
upstream.status,
`Hailuo error: ${sanitizeErrorMessage(errText)}`,
body,
url
),
headers: reqHeaders,
transformedBody: bodyObj,
},
};
}
return { upstream };
}
/** Buffer the SSE stream into a single OpenAI-shaped chat.completion response. */
private async buildNonStreamingResponse(
upstream: Response,
id: string,
created: number,
modelId: string,
url: string,
reqHeaders: Record<string, string>,
body: unknown,
bodyObj: JsonRecord
) {
let answer = "";
const result = await this.streamToText(upstream, (delta) => {
answer += delta;
});
if (!result.ok) {
return {
...makeErrorResult(
502,
`Hailuo protocol error: ${sanitizeErrorMessage(result.errorMessage || "unknown")}`,
body,
url
),
headers: reqHeaders,
transformedBody: bodyObj,
};
}
return {
response: new Response(JSON.stringify(openAiCompletion(id, created, modelId, answer)), {
headers: { "Content-Type": "application/json" },
}),
url,
headers: reqHeaders,
transformedBody: bodyObj,
};
}
private buildStreamingResponse(
upstream: Response,
id: string,
created: number,
modelId: string,
signal?: AbortSignal | null
): ReadableStream {
const encoder = new TextEncoder();
return new ReadableStream({
start: async (controller) => {
let emittedRole = false;
const result = await this.streamToText(upstream, (delta) => {
if (!emittedRole) {
emittedRole = true;
controller.enqueue(
encoder.encode(`data: ${JSON.stringify(openAiChunk(id, created, modelId, ""))}\n\n`)
);
}
controller.enqueue(
encoder.encode(`data: ${JSON.stringify(openAiChunk(id, created, modelId, delta))}\n\n`)
);
});
if (!result.ok) {
if (!signal?.aborted) {
controller.error(new Error(result.errorMessage || "Hailuo stream error"));
} else {
try {
controller.close();
} catch {
/* already closed */
}
}
return;
}
controller.enqueue(
encoder.encode(
`data: ${JSON.stringify({
id,
object: "chat.completion.chunk",
created,
model: modelId,
choices: [{ index: 0, delta: {}, finish_reason: "stop" }],
})}\n\n`
)
);
controller.enqueue(encoder.encode("data: [DONE]\n\n"));
controller.close();
},
});
}
async execute(input: ExecuteInput) {
const { body, credentials, signal, stream: wantStream } = input;
const bodyObj = asRecord(body);
const token = toStringOrEmpty(credentials?.apiKey) || toStringOrEmpty(credentials?.accessToken);
if (!token) {
return makeErrorResult(
401,
"Missing Hailuo _token — log in at hailuo.ai and capture _token from localStorage.",
body,
`${BASE_URL}${API_PATH}`
);
}
const prepared = this.prepareMsgContent(bodyObj);
if ("error" in prepared) {
return makeErrorResult(400, prepared.error, body, BASE_URL);
}
const { url, headers: reqHeaders, form } = this.buildSignedRequest(
token,
credentials?.providerSpecificData,
prepared.msgContent
);
const dispatched = await this.dispatch(url, reqHeaders, form, signal, body, bodyObj);
if ("errorResult" in dispatched) return dispatched.errorResult;
const { upstream } = dispatched;
const id = `chatcmpl-hailuo-${Date.now()}`;
const created = Math.floor(Date.now() / 1000);
const modelId = input.model || "hailuo";
if (wantStream) {
const outStream = this.buildStreamingResponse(upstream, id, created, modelId, signal);
return {
response: new Response(outStream, {
headers: {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
Connection: "keep-alive",
},
}),
url,
headers: reqHeaders,
transformedBody: bodyObj,
};
}
return this.buildNonStreamingResponse(upstream, id, created, modelId, url, reqHeaders, body, bodyObj);
}
}

View File

@@ -176,9 +176,6 @@ const lazyExecutors: Record<string, () => Promise<BaseExecutor>> = {
"doubao-web": () => import("./doubao-web.ts").then((m) => new m.DoubaoWebExecutor()),
db: () => import("./doubao-web.ts").then((m) => new m.DoubaoWebExecutor()), // Alias
"qwen-web": () => import("./qwen-web.ts").then((m) => new m.QwenWebExecutor()),
raycast: () => import("./raycast.ts").then((m) => new m.RaycastExecutor()),
rc: () => import("./raycast.ts").then((m) => new m.RaycastExecutor()), // Alias
"hailuo-web": () => import("./hailuo-web.ts").then((m) => new m.HailuoWebExecutor()),
"zai-web": () => import("./zai-web.ts").then((m) => new m.ZaiWebExecutor()),
zw: () => import("./zai-web.ts").then((m) => new m.ZaiWebExecutor()), // Alias
theoldllm: () => import("./theoldllm.ts").then((m) => new m.TheOldLlmExecutor()),

View File

@@ -1,235 +0,0 @@
/**
* @file raycast.ts
* @description Executor for Raycast Pro AI (reverse-engineered backend.raycast.com API).
*
* @changes
* - [2026-07-27] [Composer] - Initial Raycast Pro local-dev executor
*/
import { BaseExecutor, mergeUpstreamExtraHeaders, type ProviderCredentials } from "./base.ts";
import { PROVIDERS } from "../config/constants.ts";
import { sanitizeErrorMessage } from "../utils/error.ts";
import {
RAYCAST_CHAT_URL,
buildRaycastChatBody,
buildRaycastHeaders,
parseRaycastSseText,
} from "../services/raycast.ts";
type JsonRecord = Record<string, unknown>;
type ChatMessage = { role?: string; content?: unknown };
export class RaycastExecutor extends BaseExecutor {
constructor() {
super("raycast", PROVIDERS.raycast);
}
buildUrl(): string {
return RAYCAST_CHAT_URL;
}
// Not a BaseExecutor.buildHeaders override: Raycast signs headers over the exact
// request payload (2nd param is the body string, not the base's `stream` boolean),
// and execute() below is fully custom — keep it as a distinct helper so a
// polymorphic buildHeaders(credentials, true) call can never land here.
private buildRaycastRequestHeaders(
credentials: ProviderCredentials,
payload?: string
): Record<string, string> {
const body = payload || "{}";
return buildRaycastHeaders(body, credentials as JsonRecord);
}
async execute({ model, body, stream, credentials, signal, upstreamExtraHeaders }) {
const reqBody = body as { messages?: ChatMessage[]; temperature?: number };
let payload: string;
try {
payload = buildRaycastChatBody(model as string, reqBody.messages || [], reqBody.temperature);
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
return {
response: new Response(
JSON.stringify({
error: {
message: sanitizeErrorMessage(message),
type: "invalid_request_error",
code: "",
},
}),
{ status: 400, headers: { "Content-Type": "application/json" } }
),
url: RAYCAST_CHAT_URL,
headers: {},
transformedBody: body,
};
}
const headers = this.buildRaycastRequestHeaders(credentials as ProviderCredentials, payload);
mergeUpstreamExtraHeaders(headers, upstreamExtraHeaders as Record<string, string> | null);
let raycastResponse: Response;
try {
raycastResponse = await fetch(RAYCAST_CHAT_URL, {
method: "POST",
headers,
body: payload,
signal: signal || undefined,
});
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
return {
response: new Response(
JSON.stringify({
error: { message: sanitizeErrorMessage(message), type: "api_error", code: "" },
}),
{ status: 502, headers: { "Content-Type": "application/json" } }
),
url: RAYCAST_CHAT_URL,
headers,
transformedBody: payload,
};
}
if (!raycastResponse.ok) {
const errorText = await raycastResponse.text();
return {
response: new Response(
JSON.stringify({
error: {
message: sanitizeErrorMessage(`Raycast API error (${raycastResponse.status})`),
type: "api_error",
code: String(raycastResponse.status),
},
}),
{ status: raycastResponse.status, headers: { "Content-Type": "application/json" } }
),
url: RAYCAST_CHAT_URL,
headers,
transformedBody: payload,
};
}
const responseId = `chatcmpl-raycast-${Date.now()}`;
const created = Math.floor(Date.now() / 1000);
const modelId = model as string;
if (stream !== false) {
const raycastBody = raycastResponse.body;
if (!raycastBody) {
return {
response: new Response(
JSON.stringify({
error: { message: "Raycast returned empty stream body", type: "api_error", code: "" },
}),
{ status: 502, headers: { "Content-Type": "application/json" } }
),
url: RAYCAST_CHAT_URL,
headers,
transformedBody: payload,
};
}
const sseStream = new ReadableStream({
async start(controller) {
const encoder = new TextEncoder();
const reader = raycastBody.getReader();
const decoder = new TextDecoder();
let buffer = "";
try {
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
let newlineIndex: number;
while ((newlineIndex = buffer.indexOf("\n")) >= 0) {
const line = buffer.slice(0, newlineIndex).trim();
buffer = buffer.slice(newlineIndex + 1);
if (!line.startsWith("data:")) continue;
try {
const data = JSON.parse(line.slice(5).trim()) as {
text?: string;
finish_reason?: string | null;
complete?: boolean;
};
const hasContent = typeof data.text === "string" && data.text.length > 0;
const hasFinishReason =
data.finish_reason !== undefined && data.finish_reason !== null;
if (data.complete || (!hasContent && !hasFinishReason)) continue;
const chunk = {
id: responseId,
object: "chat.completion.chunk",
created,
model: modelId,
choices: [
{
index: 0,
delta: { content: data.text || "" },
finish_reason: hasFinishReason ? data.finish_reason : null,
},
],
};
controller.enqueue(encoder.encode(`data: ${JSON.stringify(chunk)}\n\n`));
} catch {
// Ignore malformed SSE data.
}
}
}
controller.enqueue(encoder.encode("data: [DONE]\n\n"));
controller.close();
} catch (err) {
controller.error(err);
}
},
});
return {
response: new Response(sseStream, {
status: 200,
headers: {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
Connection: "keep-alive",
},
}),
url: RAYCAST_CHAT_URL,
headers,
transformedBody: payload,
};
}
const responseText = await raycastResponse.text();
const content = parseRaycastSseText(responseText);
return {
response: new Response(
JSON.stringify({
id: responseId,
object: "chat.completion",
created,
model: modelId,
choices: [
{
index: 0,
message: { role: "assistant", content, refusal: null },
finish_reason: "stop",
},
],
usage: {
prompt_tokens: 0,
completion_tokens: 0,
total_tokens: 0,
},
}),
{ status: 200, headers: { "Content-Type": "application/json" } }
),
url: RAYCAST_CHAT_URL,
headers,
transformedBody: payload,
};
}
}

View File

@@ -1,280 +0,0 @@
/**
* @file raycast.ts
* @description Raycast Pro AI reverse-engineered protocol (backend.raycast.com).
* Ported from szcharlesji/raycast-relay (Node, 2026-06) — V2 HMAC + V1 JWT signatures.
*
* @changes
* - [2026-07-27] [Composer] - Initial Raycast Pro local-dev provider protocol
*/
import { createHmac, createHash, randomUUID } from "node:crypto";
import { resolvePublicCred } from "../utils/publicCreds.ts";
export const RAYCAST_CHAT_URL = "https://backend.raycast.com/api/v1/ai/chat_completions";
export const RAYCAST_MODELS_URL = "https://backend.raycast.com/api/v1/ai/models";
export const RAYCAST_DEFAULT_USER_AGENT = "Raycast/1.104.20 (macOS Version 26.5.1 (Build 25F80))";
export const RAYCAST_DEFAULT_EXPERIMENTAL = "chatBranching, mcpHTTPServer";
/**
* Community-extracted default; override via providerSpecificData.sigSecret or
* RAYCAST_SIG_SECRET. Embedded through resolvePublicCred() per Hard Rule #11 —
* a public upstream credential must never be a string literal in the source
* (see docs/security/PUBLIC_CREDS.md).
*/
export const RAYCAST_DEFAULT_SIG_SECRET = resolvePublicCred(
"raycast_sig_secret",
"RAYCAST_SIG_SECRET"
);
export type RaycastCredentials = {
accessToken?: string;
providerSpecificData?: {
deviceId?: string;
aid?: string;
sigSecret?: string;
userAgent?: string;
experimental?: string;
};
};
export type RaycastModelEntry = {
id: string;
model: string;
name: string;
provider: string;
requires_better_ai?: boolean;
availability?: string;
};
type ChatMessage = { role?: string; content?: unknown };
export function rot13rot5(input: string): string {
return input.replace(/[A-Za-z0-9]/g, (char) => {
const code = char.charCodeAt(0);
if (code >= 65 && code <= 90) return String.fromCharCode(((code - 65 + 13) % 26) + 65);
if (code >= 97 && code <= 122) return String.fromCharCode(((code - 97 + 13) % 26) + 97);
return String.fromCharCode(((code - 48 + 5) % 10) + 48);
});
}
export function signatureV2(
timestamp: string,
deviceId: string,
payload: string,
secret: string
): string {
const bodyHash = createHash("sha256").update(payload).digest("hex");
const message = [timestamp, deviceId, bodyHash].map(rot13rot5).join(".");
return createHmac("sha256", secret).update(message).digest("hex");
}
function base64UrlJson(value: Record<string, unknown>): string {
return Buffer.from(JSON.stringify(value)).toString("base64url");
}
export function raycastJwt(aid: string, secret: string): string {
const iat = Date.now() / 1000;
const header = base64UrlJson({ typ: "JWT", alg: "HS256" });
const payload = base64UrlJson({ aid, exp: iat + 60, iat });
const signature = createHmac("sha256", secret).update(`${header}.${payload}`).digest("base64url");
return `${header}.${payload}.${signature}`;
}
export function decodeAidFromRaycastJwt(jwt: string): string | null {
const parts = jwt.trim().split(".");
if (parts.length < 2) return null;
try {
const payload = JSON.parse(Buffer.from(parts[1], "base64url").toString("utf8")) as {
aid?: string;
};
return payload.aid || null;
} catch {
return null;
}
}
export function resolveRaycastSecrets(credentials: RaycastCredentials): {
bearerToken: string;
deviceId: string;
aid: string;
sigSecret: string;
} {
const psd = credentials.providerSpecificData || {};
const bearerToken = (credentials.accessToken || "").trim();
const deviceId = (psd.deviceId || "").trim();
const aid = (psd.aid || deviceId || "").trim();
const sigSecret = (
psd.sigSecret ||
process.env.RAYCAST_SIG_SECRET ||
RAYCAST_DEFAULT_SIG_SECRET
).trim();
if (!bearerToken) throw new Error("Raycast bearer token is required");
if (!deviceId) throw new Error("Raycast device ID is required");
if (!sigSecret) throw new Error("Raycast signature secret is required");
return { bearerToken, deviceId, aid, sigSecret };
}
export function buildRaycastHeaders(
payload: string,
credentials: RaycastCredentials
): Record<string, string> {
const { bearerToken, deviceId, aid, sigSecret } = resolveRaycastSecrets(credentials);
const psd = credentials.providerSpecificData || {};
const timestamp = Math.floor(Date.now() / 1000).toString();
return {
Accept: "application/json",
Authorization: `Bearer ${bearerToken}`,
"X-Raycast-Timestamp": timestamp,
"Accept-Language": "en-US,en;q=0.9",
"X-Raycast-DeviceId": deviceId,
"Content-Type": "application/json",
"X-Raycast-Signature-v2": signatureV2(timestamp, deviceId, payload, sigSecret),
"X-Raycast-Experimental": psd.experimental || RAYCAST_DEFAULT_EXPERIMENTAL,
"X-Raycast-Signature": raycastJwt(aid, sigSecret),
"User-Agent": psd.userAgent || RAYCAST_DEFAULT_USER_AGENT,
};
}
export function contentToText(content: unknown): string {
if (typeof content === "string") return content;
if (!Array.isArray(content)) return JSON.stringify(content ?? "");
return content
.map((part) => {
if (typeof part === "string") return part;
if (
part &&
typeof part === "object" &&
"type" in part &&
(part as { type?: string }).type === "text"
) {
return String((part as { text?: string }).text || "");
}
return "";
})
.filter(Boolean)
.join("\n");
}
export function convertOpenAiMessages(messages: ChatMessage[]): {
raycastMessages: Array<{ author: string; content: { text: string } }>;
systemInstruction: string;
} {
let systemInstruction = "markdown";
const raycastMessages: Array<{ author: string; content: { text: string } }> = [];
for (const [index, message] of messages.entries()) {
if (message.role === "system" && index === 0) {
systemInstruction = contentToText(message.content);
continue;
}
if (message.role === "user" || message.role === "assistant") {
raycastMessages.push({
author: message.role,
content: { text: contentToText(message.content) },
});
}
}
return { raycastMessages, systemInstruction };
}
export function inferProviderInfo(modelId: string): { provider: string; model: string } {
if (modelId.startsWith("openai_o1-")) {
return { provider: "openai", model: modelId.slice("openai_o1-".length) };
}
const providers = [
"anthropic",
"baseten",
"google",
"groq",
"mistral",
"openai",
"perplexity",
"raycast",
"together",
"xai",
];
for (const provider of providers) {
const prefix = `${provider}-`;
if (modelId.startsWith(prefix)) {
return { provider, model: modelId.slice(prefix.length) };
}
}
if (modelId.includes("/")) return { provider: "baseten", model: modelId };
return { provider: "openai", model: modelId || "gpt-5-mini" };
}
export function buildRaycastChatBody(
modelId: string,
messages: ChatMessage[],
temperature?: number
): string {
const { provider, model } = inferProviderInfo(modelId);
const { raycastMessages, systemInstruction } = convertOpenAiMessages(messages);
if (raycastMessages.length === 0) {
throw new Error("Raycast requires at least one user or assistant message");
}
return JSON.stringify({
model,
provider,
messages: raycastMessages,
system_instruction: systemInstruction,
temperature: temperature ?? 0.5,
additional_system_instructions: "",
debug: false,
locale: "en-US",
source: "ai_chat",
thread_id: randomUUID(),
tools: [],
});
}
export function parseRaycastSseText(responseText: string): string {
let fullText = "";
for (const line of responseText.split("\n")) {
if (!line.startsWith("data:")) continue;
try {
const data = JSON.parse(line.slice(5).trim()) as { text?: string };
if (data.text) fullText += data.text;
} catch {
// Ignore non-JSON SSE lines.
}
}
return fullText;
}
export async function fetchRaycastModels(
credentials: RaycastCredentials,
options?: { includePremium?: boolean; includeDeprecated?: boolean }
): Promise<RaycastModelEntry[]> {
const payload = "{}";
const headers = buildRaycastHeaders(payload, credentials);
const res = await fetch(RAYCAST_MODELS_URL, { method: "GET", headers });
if (!res.ok) {
const text = await res.text();
throw new Error(`Raycast models error [${res.status}]: ${text.slice(0, 300)}`);
}
const data = (await res.json()) as { models?: RaycastModelEntry[] };
const includePremium = options?.includePremium ?? true;
const includeDeprecated = options?.includeDeprecated ?? true;
return (data.models || []).filter((model) => {
if (!includePremium && model.requires_better_ai) return false;
if (!includeDeprecated && model.availability === "deprecated") return false;
return true;
});
}

View File

@@ -206,15 +206,6 @@ const EMBEDDED_DEFAULTS = {
// Firefly credits balance endpoint public x-api-key (`SunbreakWebUI1`) from
// GET firefly.adobe.io/v1/credits/balance browser traffic.
adobe_firefly_balance_api_key: [60, 24, 0, 11, 0, 10, 20, 31, 50, 72, 18, 32, 43, 93],
// Raycast Pro V2 request-signature secret (#8895). Community-extracted from the
// public Raycast macOS client — the SAME value ships to every install, so it is
// public by design, not a per-user credential. Overridable via RAYCAST_SIG_SECRET
// or providerSpecificData.sigSecret.
raycast_sig_secret: [
89, 15, 13, 93, 71, 90, 65, 67, 86, 24, 71, 67, 1, 9, 91, 0, 73, 64, 87, 88, 93, 90, 91, 68, 12,
20, 18, 3, 21, 70, 66, 3, 13, 11, 1, 72, 69, 87, 88, 95, 87, 88, 17, 94, 20, 67, 92, 27, 72, 68,
3, 10, 92, 6, 21, 21, 84, 95, 14, 15, 88, 70, 95, 77,
],
} as const;
export type EmbeddedDefaultKey = keyof typeof EMBEDDED_DEFAULTS;

View File

@@ -1,100 +0,0 @@
#!/usr/bin/env node
/**
* @file extract-credentials.mjs
* @description Print Raycast Pro credentials from local macOS install (redacted preview).
*
* Usage: node scripts/raycast/extract-credentials.mjs
*
* @changes
* - [2026-07-27] [Composer] - CLI credential extractor for local Raycast
*/
import { execFileSync } from "node:child_process";
import { createHash } from "node:crypto";
import { copyFileSync, existsSync, mkdtempSync, readFileSync, rmdirSync, unlinkSync } from "node:fs";
import { homedir, tmpdir } from "node:os";
import { join } from "node:path";
const RAYCAST_SALT = "yvkwWXzxPPBAqY2tmaKrB*DvYjjMaeEf";
const RAYCAST_SUPPORT = join(homedir(), "Library", "Application Support", "com.raycast.macos");
const RAYCAST_DB = join(RAYCAST_SUPPORT, "raycast-enc.sqlite");
function redact(s, keep = 8) {
if (!s || s.length <= keep * 2) return "***";
return `${s.slice(0, keep)}${s.slice(-4)}`;
}
function readKeychain(account) {
return JSON.parse(
execFileSync("security", ["find-generic-password", "-s", "Raycast", "-a", account, "-w"], {
encoding: "utf-8",
}).trim()
);
}
function dbPassphrase() {
const keyHex = execFileSync(
"security",
["find-generic-password", "-s", "Raycast", "-a", "database_key", "-w"],
{ encoding: "utf-8" }
).trim();
return createHash("sha256")
.update(keyHex + RAYCAST_SALT)
.digest("hex");
}
function queryDb(sql) {
const tmpDir = mkdtempSync(join(tmpdir(), "raycast-extract-"));
const tmpDb = join(tmpDir, "db.sqlite");
copyFileSync(RAYCAST_DB, tmpDb);
for (const ext of ["-wal", "-shm"]) {
const src = RAYCAST_DB + ext;
if (existsSync(src)) copyFileSync(src, tmpDb + ext);
}
const passphrase = dbPassphrase();
const input = `PRAGMA key = '${passphrase}';\n.mode json\n${sql}`;
const out = execFileSync("sqlcipher", [tmpDb], { input, encoding: "utf-8" });
for (const ext of ["", "-wal", "-shm"]) {
try {
unlinkSync(tmpDb + ext);
} catch {}
}
try {
rmdirSync(tmpDir);
} catch {}
const jsonStr = out.startsWith("ok\n") ? out.slice(3) : out;
return JSON.parse(jsonStr.trim() || "[]");
}
if (process.platform !== "darwin") {
console.error("macOS only");
process.exit(1);
}
const store = readKeychain("raycast-store_credentials");
const token = store?.oauth?.access_token;
if (!token) {
console.error("No Raycast bearer token in Keychain — open Raycast and sign in");
process.exit(1);
}
const users = queryDb("SELECT analyticsId, email, username, hasProFeatures, hasBetterAI FROM user LIMIT 1;");
const user = users[0] || {};
const deviceId =
user.analyticsId ||
JSON.parse(readFileSync(join(RAYCAST_SUPPORT, "posthog.distinctId"), "utf-8"))["posthog.distinctId"];
console.log(JSON.stringify({
accessTokenPreview: redact(token),
accessToken: token,
deviceId,
aid: deviceId,
email: user.email || store?.user?.email,
username: user.username || store?.user?.username,
hasProFeatures: !!user.hasProFeatures,
hasBetterAI: !!user.hasBetterAI,
sources: {
bearer: "Keychain Raycast / raycast-store_credentials",
deviceId: "raycast-enc.sqlite user.analyticsId",
},
}, null, 2));

View File

@@ -1,165 +0,0 @@
#!/usr/bin/env node
/**
* @file usage-benchmark.mjs
* @description Battle-test Raycast Pro usage via OmniRoute local endpoint.
*
* Env (required):
* OMNIROUTE_URL default http://127.0.0.1:20128/v1
* OMNIROUTE_API_KEY OmniRoute API key (if REQUIRE_API_KEY)
*
* Env (optional — direct Raycast probe without OmniRoute):
* RAYCAST_BEARER_TOKEN
* RAYCAST_DEVICE_ID
* RAYCAST_AID
* RAYCAST_SIG_SECRET
*
* Usage:
* node scripts/raycast/usage-benchmark.mjs --models 5 --rounds 3
* node scripts/raycast/usage-benchmark.mjs --model openai-gpt-5-mini --rounds 10
*
* @changes
* - [2026-07-27] [Composer] - Initial Raycast Pro usage benchmark script
*/
import { createHmac, createHash } from "node:crypto";
const args = process.argv.slice(2);
function arg(name, fallback) {
const i = args.indexOf(`--${name}`);
return i >= 0 && args[i + 1] ? args[i + 1] : fallback;
}
const rounds = Number(arg("rounds", "3"));
const model = arg("model", "");
const modelCount = Number(arg("models", "5"));
const omnirouteUrl = (process.env.OMNIROUTE_URL || "http://127.0.0.1:20128/v1").replace(/\/$/, "");
const apiKey = process.env.OMNIROUTE_API_KEY || "";
const RAYCAST_CHAT_URL = "https://backend.raycast.com/api/v1/ai/chat_completions";
const RAYCAST_MODELS_URL = "https://backend.raycast.com/api/v1/ai/models";
const SIG_SECRET =
process.env.RAYCAST_SIG_SECRET ||
"6bc455473576ce2cd6f70426caff867aabbe3f7291c1a79681af5e8ce0ca1408";
function rot13rot5(input) {
return input.replace(/[A-Za-z0-9]/g, (char) => {
const code = char.charCodeAt(0);
if (code >= 65 && code <= 90) return String.fromCharCode(((code - 65 + 13) % 26) + 65);
if (code >= 97 && code <= 122) return String.fromCharCode(((code - 97 + 13) % 26) + 97);
return String.fromCharCode(((code - 48 + 5) % 10) + 48);
});
}
function signatureV2(timestamp, deviceId, payload, secret) {
const bodyHash = createHash("sha256").update(payload).digest("hex");
const message = [timestamp, deviceId, bodyHash].map(rot13rot5).join(".");
return createHmac("sha256", secret).update(message).digest("hex");
}
function raycastJwt(aid, secret) {
const iat = Date.now() / 1000;
const header = Buffer.from(JSON.stringify({ typ: "JWT", alg: "HS256" })).toString("base64url");
const payload = Buffer.from(JSON.stringify({ aid, exp: iat + 60, iat })).toString("base64url");
const signature = createHmac("sha256", secret)
.update(`${header}.${payload}`)
.digest("base64url");
return `${header}.${payload}.${signature}`;
}
function raycastHeaders(payload) {
const bearerToken = process.env.RAYCAST_BEARER_TOKEN;
const deviceId = process.env.RAYCAST_DEVICE_ID;
const aid = process.env.RAYCAST_AID;
if (!bearerToken || !deviceId || !aid) {
throw new Error("Set RAYCAST_BEARER_TOKEN, RAYCAST_DEVICE_ID, RAYCAST_AID for direct probe");
}
const timestamp = Math.floor(Date.now() / 1000).toString();
return {
Accept: "application/json",
Authorization: `Bearer ${bearerToken}`,
"X-Raycast-Timestamp": timestamp,
"X-Raycast-DeviceId": deviceId,
"Content-Type": "application/json",
"X-Raycast-Signature-v2": signatureV2(timestamp, deviceId, payload, SIG_SECRET),
"X-Raycast-Signature": raycastJwt(aid, SIG_SECRET),
"X-Raycast-Experimental": "chatBranching, mcpHTTPServer",
"User-Agent": "Raycast/1.104.20 (macOS Version 26.5.1 (Build 25F80))",
};
}
async function fetchRaycastModels() {
const payload = "{}";
const res = await fetch(RAYCAST_MODELS_URL, { method: "GET", headers: raycastHeaders(payload) });
const text = await res.text();
if (!res.ok) throw new Error(`models [${res.status}]: ${text.slice(0, 200)}`);
const data = JSON.parse(text);
return (data.models || []).map((m) => m.id);
}
async function chatOmniroute(modelId, prompt) {
const headers = { "Content-Type": "application/json" };
if (apiKey) headers.Authorization = `Bearer ${apiKey}`;
const started = Date.now();
const res = await fetch(`${omnirouteUrl}/chat/completions`, {
method: "POST",
headers,
body: JSON.stringify({
model: `raycast/${modelId}`,
messages: [{ role: "user", content: prompt }],
stream: false,
max_tokens: 32,
}),
});
const ms = Date.now() - started;
const body = await res.text();
return { ok: res.ok, status: res.status, ms, body: body.slice(0, 300) };
}
async function main() {
console.log(`OmniRoute: ${omnirouteUrl}`);
console.log(`Rounds per model: ${rounds}`);
let models = [];
if (model) {
models = [model];
} else if (process.env.RAYCAST_BEARER_TOKEN) {
models = (await fetchRaycastModels()).slice(0, modelCount);
console.log(`Direct Raycast model probe — testing ${models.length} models via OmniRoute`);
} else {
models = ["openai-gpt-5-mini"];
console.log("No RAYCAST_* env — using default model openai-gpt-5-mini via OmniRoute combo id");
}
const results = [];
for (const modelId of models) {
let ok = 0;
let fail = 0;
const latencies = [];
for (let i = 0; i < rounds; i++) {
const prompt = `Raycast benchmark round ${i + 1} — reply with exactly: pong`;
try {
const r = await chatOmniroute(modelId, prompt);
latencies.push(r.ms);
if (r.ok) ok++;
else {
fail++;
console.error(` FAIL ${modelId} #${i + 1} [${r.status}]: ${r.body}`);
}
} catch (err) {
fail++;
console.error(` ERR ${modelId} #${i + 1}:`, err.message);
}
}
const avg = latencies.length ? Math.round(latencies.reduce((a, b) => a + b, 0) / latencies.length) : 0;
results.push({ modelId, ok, fail, avgMs: avg });
console.log(`${modelId}: ${ok}/${rounds} ok, avg ${avg}ms`);
}
console.log("\nSummary:");
console.table(results);
}
main().catch((err) => {
console.error(err);
process.exit(1);
});

View File

@@ -8,7 +8,6 @@ import {
KiroOAuthWrapper,
CursorAuthModal,
TraeAuthModal,
RaycastAuthModal,
ProxyConfigModal,
} from "@/shared/components";
import RiskNoticeModal from "../../components/RiskNoticeModal";
@@ -281,13 +280,6 @@ export default function ProviderModalsPanel({
onSuccess={handleOAuthSuccess}
onClose={() => setShowOAuthModal(false)}
/>
) : providerId === "raycast" ? (
<RaycastAuthModal
isOpen={showOAuthModal}
reauthConnection={reauthConnection}
onSuccess={handleOAuthSuccess}
onClose={() => setShowOAuthModal(false)}
/>
) : (
<OAuthModal
isOpen={showOAuthModal}

View File

@@ -1,124 +0,0 @@
/**
* @file route.ts
* @description Auto-import Raycast Pro credentials from local macOS Keychain + DB.
*
* @changes
* - [2026-07-27] [Composer] - One-click local Raycast credential extraction
*/
import { NextResponse } from "next/server";
import { createProviderConnection } from "@/models";
import { replaceSyncedAvailableModelsForConnection } from "@/lib/db/models";
import { RaycastService } from "@/lib/oauth/services/raycast";
import {
extractLocalRaycastCredentials,
isRaycastLocalExtractAvailable,
} from "@/lib/oauth/services/raycastLocal";
import { requireManagementAuth } from "@/lib/api/requireManagementAuth";
import { resolveProxyForProvider } from "@/models";
import { runWithProxyContext } from "@omniroute/open-sse/utils/proxyFetch.ts";
async function requireOAuthImportAuth(request: Request) {
// GHSA-mg76: importing a provider connection is a state-mutating admin action;
// require management scope (or a dashboard session), not any valid client key.
return requireManagementAuth(request, { invalidApiKeyStatus: 401 });
}
export async function GET(request: Request) {
const authResponse = await requireOAuthImportAuth(request);
if (authResponse) return authResponse;
return NextResponse.json({
available: isRaycastLocalExtractAvailable(),
platform: process.platform,
requires: ["macOS", "Raycast.app installed", "sqlcipher CLI (brew install sqlcipher)"],
sources: {
bearerToken: "Keychain → Raycast / raycast-store_credentials → oauth.access_token",
deviceId:
"Raycast encrypted DB user.analyticsId (same as posthog.distinctId on disk)",
},
});
}
export async function POST(request: Request) {
const authResponse = await requireOAuthImportAuth(request);
if (authResponse) return authResponse;
if (!isRaycastLocalExtractAvailable()) {
return NextResponse.json(
{
error:
"Raycast auto-import unavailable — need macOS, Raycast installed, and sqlcipher (`brew install sqlcipher`)",
},
{ status: 400 }
);
}
try {
const local = extractLocalRaycastCredentials();
const raycastService = new RaycastService();
const resolved = raycastService.validateCredentials({
accessToken: local.accessToken,
deviceId: local.deviceId,
aid: local.aid,
});
const proxy = await resolveProxyForProvider("raycast");
const models = await runWithProxyContext(proxy, () =>
raycastService.probeModels({
accessToken: local.accessToken,
deviceId: local.deviceId,
aid: resolved.aid,
})
);
const connection: any = await createProviderConnection({
provider: "raycast",
authType: "oauth",
accessToken: local.accessToken,
refreshToken: null,
email: local.email || null,
expiresAt: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString(),
providerSpecificData: {
deviceId: local.deviceId,
aid: resolved.aid,
authMethod: "auto_imported",
username: local.username,
hasProFeatures: local.hasProFeatures,
hasBetterAI: local.hasBetterAI,
extractSource: local.source,
modelCount: models.length,
premiumModelCount: models.filter((m) => m.requires_better_ai).length,
},
testStatus: "active",
});
await replaceSyncedAvailableModelsForConnection(
"raycast",
connection.id,
models.map((model) => ({
id: model.id,
name: model.name || model.id,
}))
);
return NextResponse.json({
success: true,
source: local.source,
connection: {
id: connection.id,
provider: connection.provider,
email: connection.email,
},
models: {
total: models.length,
premium: models.filter((m) => m.requires_better_ai).length,
sample: models.slice(0, 12).map((m) => m.id),
},
});
} catch (error: unknown) {
const message = error instanceof Error ? error.message : String(error);
console.error("Raycast auto-import error:", message);
return NextResponse.json({ error: message }, { status: 400 });
}
}

View File

@@ -1,143 +0,0 @@
/**
* @file route.ts
* @description Import Raycast Pro credentials captured from macOS app traffic.
*
* @changes
* - [2026-07-27] [Composer] - Initial Raycast token import route (local dev)
*/
import { NextResponse } from "next/server";
import { createProviderConnection } from "@/models";
import { RaycastService } from "@/lib/oauth/services/raycast";
import { raycastImportSchema } from "@/shared/validation/schemas";
import { isValidationFailure, validateBody } from "@/shared/validation/helpers";
import { requireManagementAuth } from "@/lib/api/requireManagementAuth";
import { resolveProxyForProvider } from "@/models";
import { runWithProxyContext } from "@omniroute/open-sse/utils/proxyFetch.ts";
async function requireOAuthImportAuth(request: Request) {
// GHSA-mg76: importing a provider connection is a state-mutating admin action;
// require management scope (or a dashboard session), not any valid client key.
return requireManagementAuth(request, { invalidApiKeyStatus: 401 });
}
export async function POST(request: Request) {
const authResponse = await requireOAuthImportAuth(request);
if (authResponse) return authResponse;
let rawBody;
try {
rawBody = await request.json();
} catch {
return NextResponse.json(
{
error: {
message: "Invalid request",
details: [{ field: "body", message: "Invalid JSON body" }],
},
},
{ status: 400 }
);
}
try {
const validation = validateBody(raycastImportSchema, rawBody);
if (isValidationFailure(validation)) {
return NextResponse.json({ error: validation.error }, { status: 400 });
}
const { accessToken, deviceId, aid, signatureJwt, sigSecret } = validation.data;
const raycastService = new RaycastService();
const resolved = raycastService.validateCredentials({
accessToken,
deviceId,
aid,
signatureJwt,
sigSecret,
});
const proxy = await resolveProxyForProvider("raycast");
const models = await runWithProxyContext(proxy, () =>
raycastService.probeModels({
accessToken: accessToken.trim(),
deviceId: deviceId.trim(),
aid: resolved.aid,
sigSecret: sigSecret?.trim(),
})
);
const connection: any = await createProviderConnection({
provider: "raycast",
authType: "oauth",
accessToken: accessToken.trim(),
refreshToken: null,
expiresAt: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString(),
providerSpecificData: {
deviceId: deviceId.trim(),
aid: resolved.aid,
sigSecret: sigSecret?.trim() || "",
authMethod: "imported",
modelCount: models.length,
premiumModelCount: models.filter((m) => m.requires_better_ai).length,
},
testStatus: "active",
});
return NextResponse.json({
success: true,
connection: {
id: connection.id,
provider: connection.provider,
},
models: {
total: models.length,
premium: models.filter((m) => m.requires_better_ai).length,
sample: models.slice(0, 8).map((m) => m.id),
},
});
} catch (error: unknown) {
const message = error instanceof Error ? error.message : String(error);
console.error("Raycast import token error:", message);
return NextResponse.json({ error: message }, { status: 400 });
}
}
export async function GET(request: Request) {
const authResponse = await requireOAuthImportAuth(request);
if (authResponse) return authResponse;
const raycastService = new RaycastService();
return NextResponse.json({
provider: "raycast",
method: "import_token",
localDevOnly: true,
instructions: raycastService.getCaptureInstructions(),
requiredFields: [
{
name: "accessToken",
label: "Bearer Token",
description: "From Authorization: Bearer header on backend.raycast.com requests",
type: "textarea",
},
{
name: "deviceId",
label: "Device ID",
description: "From X-Raycast-DeviceId header",
type: "text",
},
{
name: "signatureJwt",
label: "Signature JWT",
description: "From X-Raycast-Signature header (AID decoded automatically)",
type: "textarea",
},
{
name: "sigSecret",
label: "Signature Secret",
description: "Optional override — defaults to community-extracted SIG_SECRET",
type: "text",
},
],
});
}

View File

@@ -91,8 +91,6 @@ import { isConnectionUnavailableToAuxiliaryActivity } from "@/lib/exclusiveLease
import { fetchCursorAgentModels } from "@/lib/providerModels/cursorAgent";
import { fetchCursorAvailableModels } from "@/lib/providerModels/cursorAvailableModels";
import { ensureCursorAutoCatalogEntry } from "@/lib/providerModels/cursorAutoCatalog";
import { fetchRaycastModels } from "@omniroute/open-sse/services/raycast.ts";
import { runWithProxyContext } from "@omniroute/open-sse/utils/proxyFetch.ts";
import {
type JsonRecord,
asRecord,
@@ -1298,58 +1296,6 @@ export async function GET(
});
}
if (provider === "raycast") {
const cachedResponse = maybeReturnCachedDiscovery();
if (cachedResponse) return cachedResponse;
const autoFetchDisabledResponse = maybeReturnAutoFetchDisabled();
if (autoFetchDisabledResponse) return autoFetchDisabledResponse;
const psd = asRecord(connection.providerSpecificData);
const deviceId = toNonEmptyString(psd.deviceId);
const aid = toNonEmptyString(psd.aid) || deviceId;
if (!accessToken || !deviceId) {
const fallback = buildDiscoveryFallbackResponse({
localWarning: "Raycast credentials incomplete — using local catalog",
});
if (fallback) return fallback;
return NextResponse.json({ error: "Raycast credentials incomplete" }, { status: 400 });
}
try {
const raycastModels = await runWithProxyContext(proxy, () =>
fetchRaycastModels({
accessToken,
providerSpecificData: {
deviceId,
aid: aid || deviceId,
sigSecret: toNonEmptyString(psd.sigSecret) || undefined,
},
})
);
const models = raycastModels.map((model) => ({
id: model.id,
name: model.name || model.id,
owned_by: model.provider || provider,
...(model.requires_better_ai ? { premium: true } : {}),
...(model.availability ? { availability: model.availability } : {}),
}));
return buildApiDiscoveryResponse(models);
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
console.log("[models] raycast fetch failed:", message);
const fallback = buildDiscoveryFallbackResponse({
cacheWarning: `Raycast API unavailable (${message}) — using cached catalog`,
localWarning: `Raycast API unavailable (${message}) — using local catalog`,
});
if (fallback) return fallback;
return NextResponse.json(
{ error: `Failed to fetch Raycast models: ${message}` },
{ status: 502 }
);
}
}
if (provider === "cursor") {
const cachedResponse = maybeReturnCachedDiscovery();
if (cachedResponse) return cachedResponse;
@@ -1906,8 +1852,7 @@ export async function GET(
// ponytail: Anthropic partner models via Model Garden publisher endpoint (Bearer only)
if (bearerToken) {
const psd = asRecord(connection.providerSpecificData);
const region =
(typeof psd.region === "string" && psd.region.trim()) || "us-central1";
const region = (typeof psd.region === "string" && psd.region.trim()) || "us-central1";
// Extract project_id from SA JSON for project-scoped listing (mirrors executor URL pattern).
// Falls back to global publisher endpoint if no project available.
@@ -1917,7 +1862,9 @@ export async function GET(
try {
const sa = JSON.parse(credential);
if (sa?.project_id) projectId = sa.project_id;
} catch { /* not SA JSON, skip */ }
} catch {
/* not SA JSON, skip */
}
}
if (projectId) {
anthropicModelsUrl = `https://aiplatform.googleapis.com/v1/projects/${projectId}/locations/${region}/publishers/anthropic/models`;
@@ -1938,9 +1885,8 @@ export async function GET(
});
if (anthropicResponse.ok) {
const anthropicData = await anthropicResponse.json();
const { parseVertexAnthropicModels } = await import(
"@/lib/providerModels/vertexAnthropicModelsParser"
);
const { parseVertexAnthropicModels } =
await import("@/lib/providerModels/vertexAnthropicModelsParser");
allModels.push(...parseVertexAnthropicModels(anthropicData));
} else {
console.log("[models] Vertex Anthropic partner discovery failed", {

View File

@@ -205,14 +205,6 @@ export const OAUTH_TEST_CONFIG: Record<string, OAuthTestConfigEntry> = {
// Validate using token presence/expiry as a lightweight auth check.
checkExpiry: true,
},
raycast: {
// #8895 — Raycast Pro is an `import_token` provider: the token is imported
// from the local Raycast install, `refreshToken` is always null and the
// stored `expiresIn` defaults to 30 days. There is nothing to refresh, so
// the test is the expiry check on the imported token; without an entry here
// Test Connection persists testStatus="error" on a healthy account (#8408).
checkExpiry: true,
},
"zed-hosted": {
// Zed Hosted Models uses a long-lived native-app access token with no
// expiry or refresh token. Validate presence here; real connectivity is

View File

@@ -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. microsoft-designer-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)
);
}

View File

@@ -0,0 +1,32 @@
-- Retire the Raycast Relay and Hailuo Web integrations whose distributed
-- implementations were substantially derived from GPL-3.0 sources.
--
-- Keep connection rows and historical records for auditability. Disabling the
-- connections is deliberately fail-closed: API-key allowed_connections entries
-- continue to reference the same connection ids instead of becoming an empty
-- allowlist, which would mean unrestricted access in the policy layer.
UPDATE exclusive_connection_leases
SET state = 'INVALIDATED',
ended_at = COALESCE(ended_at, datetime('now')),
end_reason = COALESCE(end_reason, 'provider integration retired in v3.8.50')
WHERE state = 'ACTIVE'
AND (
provider IN ('raycast', 'rc', 'hailuo-web')
OR connection_id IN (
SELECT id
FROM provider_connections
WHERE provider IN ('raycast', 'rc', 'hailuo-web')
)
);
UPDATE provider_connections
SET is_active = 0,
test_status = 'unavailable',
error_code = 'PROVIDER_REMOVED',
last_error = 'Provider integration retired from OmniRoute v3.8.50',
last_error_type = 'provider_removed',
last_error_source = 'migration:163',
last_error_at = COALESCE(last_error_at, datetime('now')),
updated_at = datetime('now')
WHERE provider IN ('raycast', 'rc', 'hailuo-web');

View File

@@ -78,7 +78,7 @@ function fieldMatch(incoming: string | null, existing: string | null): boolean |
* and `incomingProfileArn` (#10815).
*
* Two independent disambiguators, either of which can prove "different
* account": `providerSpecificData.username` (Raycast-style IdP dedup) and
* account": `providerSpecificData.username` (generic username/IdP fallback) and
* `providerSpecificData.profileArn` (Kiro/AWS profile dedup — Kiro never
* sets `username`). A field only rules a match IN/OUT when both the
* incoming and existing record carry it; when neither carries either field

View File

@@ -435,17 +435,6 @@ export const TRAE_CONFIG = {
"Authorize via trae.ai in the popup, or sign in to solo.trae.ai and paste the Cloud-IDE-JWT from the Authorization header (~14-day lifetime).",
};
// Raycast Pro AI — reverse-engineered, unofficial API. LOCAL / PERSONAL USE ONLY.
// See docs/security/PUBLIC_CREDS.md pattern: no secrets in repo; credentials from user's Mac.
export const RAYCAST_CONFIG = {
apiEndpoint: "https://backend.raycast.com",
chatEndpoint: "/api/v1/ai/chat_completions",
modelsEndpoint: "/api/v1/ai/models",
clientType: "macos-app",
captureInstructions:
"macOS only: use Auto-Import (Keychain + Raycast DB) or capture Bearer, X-Raycast-DeviceId, and optional X-Raycast-Signature JWT from backend.raycast.com traffic.",
};
// Devin Desktop / Devin CLI import-token configuration.
// Public product identity is Devin. The upstream transport still identifies
// the IDE as `windsurf`; authentication itself is import-only.
@@ -509,10 +498,6 @@ export const PROVIDERS = {
KIRO: "kiro",
AMAZON_Q: "amazon-q",
CURSOR: "cursor",
// #8895 — registered in src/lib/oauth/providers/index.ts but missing here, so
// every consumer reading PROVIDERS (onboarding wizard, test-connection routing)
// did not know Raycast Pro exists as an OAuth provider.
RAYCAST: "raycast",
KILOCODE: "kilocode",
CLINE: "cline",
CLINEPASS: "clinepass",

View File

@@ -22,7 +22,6 @@ import { gitlabDuo } from "./gitlab-duo";
import { kiro } from "./kiro";
import { cursor } from "./cursor";
import { trae } from "./trae";
import { raycast } from "./raycast";
import { kilocode } from "./kilocode";
import { cline } from "./cline";
import { devinDesktop } from "./devin-desktop";
@@ -47,7 +46,6 @@ export const PROVIDERS = {
"amazon-q": kiro,
cursor,
trae,
raycast,
kilocode,
cline,
// clinepass reuses the Cline WorkOS OAuth flow 1:1 (same api.cline.bot host, same token

View File

@@ -1,37 +0,0 @@
/**
* @file raycast.ts
* @description Raycast Pro token-import OAuth provider (reverse-engineered, local dev only).
*
* @changes
* - [2026-07-27] [Composer] - Initial Raycast Pro import_token provider
*/
import { RAYCAST_CONFIG } from "../constants/oauth";
type RaycastRawTokens = {
accessToken?: string;
access_token?: string;
deviceId?: string;
device_id?: string;
aid?: string;
sigSecret?: string;
signatureSecret?: string;
signatureJwt?: string;
expiresIn?: number;
};
export const raycast = {
config: RAYCAST_CONFIG,
flowType: "import_token",
mapTokens: (tokens: RaycastRawTokens) => ({
accessToken: tokens.accessToken || tokens.access_token,
refreshToken: null,
expiresIn: tokens.expiresIn || 30 * 24 * 60 * 60,
providerSpecificData: {
deviceId: tokens.deviceId || tokens.device_id || "",
aid: tokens.aid || "",
sigSecret: tokens.sigSecret || tokens.signatureSecret || "",
authMethod: "imported",
},
}),
};

View File

@@ -1,65 +0,0 @@
/**
* @file raycast.ts
* @description Raycast Pro credential validation via live models API probe.
*
* @changes
* - [2026-07-27] [Composer] - Initial Raycast import validation service
*/
import {
decodeAidFromRaycastJwt,
fetchRaycastModels,
type RaycastModelEntry,
} from "@omniroute/open-sse/services/raycast.ts";
export class RaycastService {
validateCredentials(input: {
accessToken: string;
deviceId: string;
aid?: string;
signatureJwt?: string;
sigSecret?: string;
}): { aid: string } {
const accessToken = input.accessToken.trim();
const deviceId = input.deviceId.trim();
let aid = (input.aid || "").trim();
if (!aid && input.signatureJwt?.trim()) {
aid = decodeAidFromRaycastJwt(input.signatureJwt.trim()) || "";
}
if (!accessToken) throw new Error("Bearer token is required");
if (!deviceId) throw new Error("Device ID is required");
// AID is optional for current Raycast API — fall back to deviceId when not captured manually.
if (!aid) aid = deviceId;
return { aid };
}
async probeModels(credentials: {
accessToken: string;
deviceId: string;
aid: string;
sigSecret?: string;
}): Promise<RaycastModelEntry[]> {
return fetchRaycastModels({
accessToken: credentials.accessToken,
providerSpecificData: {
deviceId: credentials.deviceId,
aid: credentials.aid,
sigSecret: credentials.sigSecret,
},
});
}
getCaptureInstructions(): string[] {
return [
"Easiest: click Auto-Import (macOS) — reads Keychain + local Raycast DB.",
"Manual fallback: Proxyman/Charles SSL proxy on backend.raycast.com.",
"Bearer token lives in Keychain: Raycast / raycast-store_credentials.",
"Device ID = analyticsId in ~/Library/Application Support/com.raycast.macos/posthog.distinctId.",
"Signature JWT is optional with current Raycast builds.",
];
}
}

View File

@@ -1,198 +0,0 @@
/**
* @file raycastLocal.ts
* @description Extract Raycast Pro credentials from local macOS install (Keychain + encrypted DB).
*
* @changes
* - [2026-07-27] [Composer] - Auto-extract bearer token and device ID from local Raycast
*/
import { execFileSync } from "node:child_process";
import { createHash } from "node:crypto";
import { copyFileSync, existsSync, mkdtempSync, readFileSync, rmdirSync, unlinkSync } from "node:fs";
import { homedir, tmpdir } from "node:os";
import { join } from "node:path";
const RAYCAST_SALT = "yvkwWXzxPPBAqY2tmaKrB*DvYjjMaeEf";
const RAYCAST_SUPPORT = join(homedir(), "Library", "Application Support", "com.raycast.macos");
const RAYCAST_DB = join(RAYCAST_SUPPORT, "raycast-enc.sqlite");
const POSTHOG_DISTINCT = join(RAYCAST_SUPPORT, "posthog.distinctId");
export type RaycastLocalCredentials = {
accessToken: string;
deviceId: string;
aid: string;
email?: string;
username?: string;
hasProFeatures?: boolean;
hasBetterAI?: boolean;
source: "keychain+analyticsId" | "keychain+posthog";
};
function readKeychainJson(account: string): Record<string, unknown> | null {
try {
const raw = execFileSync(
"security",
["find-generic-password", "-s", "Raycast", "-a", account, "-w"],
{ encoding: "utf-8" }
).trim();
return JSON.parse(raw) as Record<string, unknown>;
} catch {
return null;
}
}
function getDatabasePassphrase(): string {
const keyHex = execFileSync(
"security",
["find-generic-password", "-s", "Raycast", "-a", "database_key", "-w"],
{ encoding: "utf-8" }
).trim();
return createHash("sha256")
.update(keyHex + RAYCAST_SALT)
.digest("hex");
}
function queryEncryptedDb(passphrase: string, sql: string): unknown[] {
if (!existsSync(RAYCAST_DB)) return [];
const tmpDir = mkdtempSync(join(tmpdir(), "omniroute-raycast-"));
const tmpDb = join(tmpDir, "raycast-enc.sqlite");
const cleanup = () => {
for (const ext of ["", "-wal", "-shm"]) {
try {
unlinkSync(tmpDb + ext);
} catch {
// ignore
}
}
try {
rmdirSync(tmpDir);
} catch {
// ignore
}
};
try {
copyFileSync(RAYCAST_DB, tmpDb);
for (const ext of ["-wal", "-shm"]) {
const src = RAYCAST_DB + ext;
if (existsSync(src)) copyFileSync(src, tmpDb + ext);
}
const input = `PRAGMA key = '${passphrase}';\n.mode json\n${sql}`;
const result = execFileSync("sqlcipher", [tmpDb], { input, encoding: "utf-8" });
const jsonStr = result.startsWith("ok\n") ? result.slice(3) : result;
return JSON.parse(jsonStr.trim() || "[]") as unknown[];
} catch {
return [];
} finally {
cleanup();
}
}
function readAnalyticsIdFromDb(): string | null {
try {
const passphrase = getDatabasePassphrase();
const rows = queryEncryptedDb(
passphrase,
"SELECT analyticsId FROM user WHERE analyticsId IS NOT NULL LIMIT 1;"
) as Array<{ analyticsId?: string }>;
const id = rows[0]?.analyticsId?.trim();
return id || null;
} catch {
return null;
}
}
function readAnalyticsIdFromPosthog(): string | null {
try {
if (!existsSync(POSTHOG_DISTINCT)) return null;
const parsed = JSON.parse(readFileSync(POSTHOG_DISTINCT, "utf-8")) as {
"posthog.distinctId"?: string;
};
const id = parsed["posthog.distinctId"]?.trim();
return id || null;
} catch {
return null;
}
}
function readUserProfile(): { email?: string; username?: string; hasProFeatures?: boolean; hasBetterAI?: boolean } {
try {
const passphrase = getDatabasePassphrase();
const rows = queryEncryptedDb(
passphrase,
"SELECT email, username, hasProFeatures, hasBetterAI FROM user LIMIT 1;"
) as Array<{
email?: string;
username?: string;
hasProFeatures?: number;
hasBetterAI?: number;
}>;
const row = rows[0];
if (!row) return {};
return {
email: row.email,
username: row.username,
hasProFeatures: !!row.hasProFeatures,
hasBetterAI: !!row.hasBetterAI,
};
} catch {
return {};
}
}
export function isRaycastLocalExtractAvailable(): boolean {
if (process.platform !== "darwin") return false;
try {
execFileSync("which", ["sqlcipher"], { encoding: "utf-8" });
} catch {
return false;
}
return existsSync(RAYCAST_DB) || existsSync(POSTHOG_DISTINCT);
}
/**
* Pull Raycast Pro credentials from the local macOS install.
* Bearer token: Keychain entry `raycast-store_credentials` → oauth.access_token
* Device ID: user.analyticsId (same as posthog.distinctId)
*/
export function extractLocalRaycastCredentials(): RaycastLocalCredentials {
if (process.platform !== "darwin") {
throw new Error("Raycast auto-import is macOS-only");
}
const store = readKeychainJson("raycast-store_credentials");
const oauth = (store?.oauth || {}) as { access_token?: string };
const accessToken = oauth.access_token?.trim();
if (!accessToken) {
throw new Error(
"Raycast bearer token not found in Keychain — open Raycast and sign in first"
);
}
const analyticsFromDb = readAnalyticsIdFromDb();
const analyticsFromPosthog = readAnalyticsIdFromPosthog();
const deviceId = analyticsFromDb || analyticsFromPosthog;
if (!deviceId) {
throw new Error(
"Raycast device/analytics ID not found — launch Raycast once so it writes local state"
);
}
const profile = readUserProfile();
const user = (store?.user || {}) as { email?: string; username?: string };
return {
accessToken,
deviceId,
// V1 JWT aid — chat works without a captured signature JWT; deviceId is a stable fallback.
aid: deviceId,
email: profile.email || user.email,
username: profile.username || user.username,
hasProFeatures: profile.hasProFeatures,
hasBetterAI: profile.hasBetterAI,
source: analyticsFromDb ? "keychain+analyticsId" : "keychain+posthog",
};
}

View File

@@ -65,7 +65,6 @@ export const LOCAL_ONLY_API_PREFIXES: ReadonlyArray<string> = [
"/api/jobs/", // sub-paths: /api/jobs/:id/{runs,enable,disable,run-now} (the bare `/api/jobs` above matches the list route; this matches children)
"/api/oauth/cursor/auto-import", // spawns execFile("which", argv-array-of-one-arg "cursor") to verify a local Cursor install before importing creds — RCE-via-tunnel surface (Hard Rules #15 + #17, found by 6A.8 route-guard gate). Specific path only: the rest of /api/oauth/ (browser redirect/callback flows) must stay remote-reachable. Note: this comment intentionally avoids a literal closing square bracket character — check-openapi-security-tiers.mjs's naive regex parser for this array stops at the first one it finds, silently truncating its view of every entry after this one.
"/api/oauth/kiro/auto-import", // reads host-local Kiro credential files (homedir kiro-cli data) — must reach the loopback-only gate, not the PUBLIC /api/oauth/ prefix (GHSA-wgwc-crjm-pmwv, GHSA-gxv4-955v-v6cm). Excluded from PUBLIC in publicApiRoutes.ts.
"/api/oauth/raycast/auto-import", // reads host-local Raycast credential files — same loopback-only rationale as the kiro and cursor auto-import routes above.
"/api/skills/collect/", // Skill Collector CLI detection: GET .../detect probes getCliRuntimeStatus() per CLI_TOOL_IDS entry, which spawns a child process to check each tool — RCE-via-tunnel surface (Hard Rules #15 + #17, PR #6294 review).
"/api/discovery/", // Discovery tool (opt-in provider scanner): the scan route makes outbound probes to provider endpoints (SSRF-adjacent) and the whole surface is an admin research tool — strict-loopback only, no manage-scope bypass (NOT in LOCAL_ONLY_MANAGE_SCOPE_BYPASS_PREFIXES). See _tasks/features-v3.8.42/gaps/DISCOVERY_TOOL_DESIGN.md.
VNC_ROUTE_PREFIX, // #7892: /api/vnc-session/* spawns Docker containers via child_process.spawn (src/lib/vncSession/service.ts) — RCE-via-tunnel surface (Hard Rules #15 + #17), same CVE class (GHSA-fhh6-4qxv-rpqj).

View File

@@ -1,213 +0,0 @@
"use client";
/**
* @file RaycastAuthModal.tsx
* @description Import Raycast Pro AI credentials (auto-detect from local macOS install).
*
* @changes
* - [2026-07-27] [Composer] - Add one-click auto-import from Keychain + Raycast DB
*/
import { useEffect, useState } from "react";
import Modal from "./Modal";
import Button from "./Button";
import Input from "./Input";
type RaycastAuthModalProps = {
isOpen: boolean;
reauthConnection?: unknown;
onSuccess?: () => void;
onClose: () => void;
};
export default function RaycastAuthModal({
isOpen,
onSuccess,
onClose,
}: RaycastAuthModalProps) {
const [accessToken, setAccessToken] = useState("");
const [deviceId, setDeviceId] = useState("");
const [signatureJwt, setSignatureJwt] = useState("");
const [sigSecret, setSigSecret] = useState("");
const [importing, setImporting] = useState(false);
const [autoAvailable, setAutoAvailable] = useState(false);
const [showManual, setShowManual] = useState(false);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
if (!isOpen) return;
fetch("/api/oauth/raycast/auto-import")
.then((r) => r.json())
.then((d) => setAutoAvailable(!!d.available))
.catch(() => setAutoAvailable(false));
}, [isOpen]);
const handleAutoImport = async () => {
setImporting(true);
setError(null);
try {
const res = await fetch("/api/oauth/raycast/auto-import", { method: "POST" });
const data = await res.json();
if (!res.ok) {
throw new Error(
typeof data.error === "string" ? data.error : data.error?.message || "Auto-import failed"
);
}
onSuccess?.();
onClose();
} catch (err) {
setError(err instanceof Error ? err.message : String(err));
} finally {
setImporting(false);
}
};
const handleImportToken = async () => {
if (!accessToken.trim() || !deviceId.trim()) {
setError("Bearer token and device ID are required.");
return;
}
setImporting(true);
setError(null);
try {
const body: Record<string, string> = {
accessToken: accessToken.trim(),
deviceId: deviceId.trim(),
};
if (signatureJwt.trim()) body.signatureJwt = signatureJwt.trim();
if (sigSecret.trim()) body.sigSecret = sigSecret.trim();
const res = await fetch("/api/oauth/raycast/import", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
});
const data = await res.json();
if (!res.ok) {
throw new Error(
typeof data.error === "string" ? data.error : data.error?.message || "Import failed"
);
}
onSuccess?.();
onClose();
} catch (err) {
setError(err instanceof Error ? err.message : String(err));
} finally {
setImporting(false);
}
};
return (
<Modal isOpen={isOpen} title="Connect Raycast Pro AI" onClose={onClose}>
<div className="flex flex-col gap-4">
<div className="bg-emerald-50 dark:bg-emerald-900/20 p-4 rounded-lg border border-emerald-200 dark:border-emerald-800">
<p className="text-sm text-emerald-900 dark:text-emerald-200 mb-3">
<strong>Auto-import (recommended on macOS):</strong> reads your local Raycast login from
Keychain + analytics device ID. No proxy needed.
</p>
<Button onClick={handleAutoImport} fullWidth disabled={importing || !autoAvailable}>
{importing
? "Importing…"
: autoAvailable
? "Auto-Import from Local Raycast"
: "Auto-Import unavailable (need macOS + sqlcipher)"}
</Button>
{!autoAvailable && (
<p className="text-xs text-emerald-800 dark:text-emerald-300 mt-2">
Install sqlcipher: <span className="font-mono">brew install sqlcipher</span>
</p>
)}
</div>
<div className="bg-amber-50 dark:bg-amber-900/20 p-4 rounded-lg border border-amber-200 dark:border-amber-800">
<p className="text-sm text-amber-900 dark:text-amber-200">
<strong>Local dev only.</strong> Uses your Raycast Pro subscription via reverse-engineered
API. Not official may break on Raycast updates.
</p>
<button
type="button"
className="text-xs text-amber-800 dark:text-amber-300 underline mt-2"
onClick={() => setShowManual((v) => !v)}
>
{showManual ? "Hide manual import" : "Manual import (proxy capture)"}
</button>
</div>
{showManual && (
<>
<div>
<label className="block text-sm font-medium mb-2">
Bearer Token <span className="text-red-500">*</span>
</label>
<textarea
value={accessToken}
onChange={(e) => setAccessToken(e.target.value)}
placeholder="rca_..."
rows={3}
className="w-full px-3 py-2 text-sm font-mono border border-border rounded-lg bg-background focus:outline-none focus:border-primary resize-none"
/>
</div>
<div>
<label className="block text-sm font-medium mb-2">
Device ID <span className="text-red-500">*</span>
</label>
<Input
value={deviceId}
onChange={(e) => setDeviceId(e.target.value)}
placeholder="analyticsId / X-Raycast-DeviceId"
className="font-mono text-sm"
/>
</div>
<div>
<label className="block text-sm font-medium mb-2">
Signature JWT <span className="text-text-muted text-xs">optional</span>
</label>
<textarea
value={signatureJwt}
onChange={(e) => setSignatureJwt(e.target.value)}
placeholder="X-Raycast-Signature (optional on current builds)"
rows={2}
className="w-full px-3 py-2 text-sm font-mono border border-border rounded-lg bg-background focus:outline-none focus:border-primary resize-none"
/>
</div>
<div>
<label className="block text-sm font-medium mb-2">
SIG_SECRET <span className="text-text-muted text-xs">optional</span>
</label>
<Input
value={sigSecret}
onChange={(e) => setSigSecret(e.target.value)}
placeholder="Override if Raycast rotated signing key"
className="font-mono text-sm"
/>
</div>
<Button
onClick={handleImportToken}
fullWidth
disabled={importing || !accessToken.trim() || !deviceId.trim()}
>
{importing ? "Importing…" : "Import Manually"}
</Button>
</>
)}
{error && (
<div className="bg-red-50 dark:bg-red-900/20 p-3 rounded-lg border border-red-200 dark:border-red-800">
<p className="text-sm text-red-600 dark:text-red-400">{error}</p>
</div>
)}
<Button onClick={onClose} variant="ghost" fullWidth>
Cancel
</Button>
</div>
</Modal>
);
}

View File

@@ -32,7 +32,6 @@ export { default as KiroOAuthWrapper } from "./KiroOAuthWrapper";
export { default as KiroSocialOAuthModal } from "./KiroSocialOAuthModal";
export { default as CursorAuthModal } from "./CursorAuthModal";
export { default as TraeAuthModal } from "./TraeAuthModal";
export { default as RaycastAuthModal } from "./RaycastAuthModal";
export { default as SegmentedControl } from "./SegmentedControl";
export { default as Breadcrumbs } from "./Breadcrumbs";
export { default as EmptyState } from "./EmptyState";

View File

@@ -75,7 +75,6 @@ export function getProviderConnectionFamilyIds(providerId: unknown): readonly st
// Web / Cookie Providers
// API Key Providers
// Sub-categories within APIKEY_PROVIDERS (used by dashboard and catalog views).
@@ -145,7 +144,6 @@ export const AGGREGATOR_PROVIDER_IDS = new Set([
"helixmind",
"tabitoken",
"logfare",
]);
export const ENTERPRISE_CLOUD_PROVIDER_IDS = new Set([
@@ -182,7 +180,7 @@ export const VIDEO_PROVIDER_IDS = new Set([
// IDE Providers: editors with built-in AI subscription (separate section in UI).
// These providers live in OAUTH_PROVIDERS but render under "IDE Providers"
// instead of "OAuth Providers" to avoid visual duplication.
export const IDE_PROVIDER_IDS = new Set(["cursor", "zed", "trae", "raycast"]);
export const IDE_PROVIDER_IDS = new Set(["cursor", "zed", "trae"]);
export const EMBEDDING_RERANK_PROVIDER_IDS = new Set(["voyage-ai", "jina-ai"]);

View File

@@ -187,19 +187,6 @@ export const OAUTH_PROVIDERS = {
authHint:
"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.",
},
raycast: {
id: "raycast",
alias: "rc",
name: "Raycast Pro AI",
icon: "terminal",
color: "#FF6363",
textIcon: "RC",
website: "https://raycast.com/ai",
subscriptionRisk: true,
riskNoticeVariant: "oauth",
authHint:
"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.",
},
"kimi-coding": {
id: "kimi-coding",
alias: "kmc",

View File

@@ -343,23 +343,6 @@ export const WEB_COOKIE_PROVIDERS = {
subscriptionRisk: true,
riskNoticeVariant: "webCookie",
},
"hailuo-web": {
id: "hailuo-web",
// Distinct alias: avoid colliding with the existing API-key "minimax"/
// "minimax-cn" providers (src/shared/constants/providers/apikey/regional.ts).
alias: "hailuo-web",
name: "Hailuo Web (MiniMax)",
icon: "auto_awesome",
color: "#5B21B6",
textIcon: "HL",
website: "https://chat.minimax.io",
authHint:
"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).",
subscriptionRisk: true,
riskNoticeVariant: "webCookie",
},
"qwen-web": {
id: "qwen-web",
// The web variant uses its own id; the retired `qw` alias is not reassigned.

View File

@@ -102,15 +102,14 @@ function isPublicCloudApiRoute(pathname: string, method: string): boolean {
);
}
// OAuth "auto-import" routes read host-local credential files (Cursor / Kiro /
// Raycast tokens). The broad `/api/oauth/` PUBLIC prefix would classify them
// OAuth "auto-import" routes read host-local credential files (Cursor / Kiro
// tokens). The broad `/api/oauth/` PUBLIC prefix would classify them
// PUBLIC, which skips the LOCAL_ONLY tier entirely (GHSA-wgwc-crjm-pmwv) and
// exposes the host credential to a remote caller (GHSA-gxv4-955v-v6cm). Exclude
// them so they fall through to MANAGEMENT and reach the loopback-only gate.
const LOCAL_ONLY_OAUTH_IMPORT_ROUTES = [
"/api/oauth/cursor/auto-import",
"/api/oauth/kiro/auto-import",
"/api/oauth/raycast/auto-import",
];
/**

View File

@@ -135,13 +135,6 @@ export const WEB_SESSION_CREDENTIAL_REQUIREMENTS = {
acceptsFullCookieHeader: true,
storageKeys: ["cookie", "ecto_1_sess", "abra_sess"],
},
"hailuo-web": {
kind: "token",
credentialName: "_token",
placeholder: '_token=... (hailuo.ai → DevTools → Local Storage → "_token")',
acceptsFullCookieHeader: false,
storageKeys: ["token", "_token"],
},
"claude-web": {
kind: "cookie",
credentialName: "sessionKey",

View File

@@ -185,14 +185,6 @@ export const traeImportSchema = z.object({
region: z.string().trim().optional(),
});
export const raycastImportSchema = z.object({
accessToken: z.string().trim().min(1, "Raycast bearer token is required"),
deviceId: z.string().trim().min(1, "Raycast device ID is required"),
aid: z.string().trim().optional(),
signatureJwt: z.string().trim().optional(),
sigSecret: z.string().trim().optional(),
});
export const kiroImportSchema = z.object({
refreshToken: z.string().trim().min(1, "Refresh token is required"),
region: z.string().trim().default("us-east-1"),

View File

@@ -380,11 +380,6 @@
"configSource": "<custom-config>",
"provider": "hyperagent"
},
"hailuo-web": {
"className": "HailuoWebExecutor",
"configSource": "<custom-config>",
"provider": "hailuo-web"
},
"hc": {
"className": "HuggingChatExecutor",
"configSource": "<custom-config>",
@@ -565,16 +560,6 @@
"configSource": "<custom-config>",
"provider": "qwen-web"
},
"raycast": {
"className": "RaycastExecutor",
"configSource": "raycast",
"provider": "raycast"
},
"rc": {
"className": "RaycastExecutor",
"configSource": "raycast",
"provider": "raycast"
},
"t3-web": {
"className": "T3ChatWebExecutor",
"configSource": "<custom-config>",
@@ -721,6 +706,6 @@
"provider": "zai-web"
}
},
"keyCount": 144,
"keyCount": 141,
"sharedInstances": []
}

View File

@@ -2813,29 +2813,6 @@
"stream": "https://api.groq.com/openai/v1/chat/completions"
}
},
"hailuo-web": {
"format": "openai",
"headers": {
"apiKey": {
"Accept": "text/event-stream",
"Authorization": "Bearer <TOK>",
"Content-Type": "application/json"
},
"nonStream": {
"Authorization": "Bearer <TOK>",
"Content-Type": "application/json"
},
"oauth": {
"Accept": "text/event-stream",
"Authorization": "Bearer <TOK>",
"Content-Type": "application/json"
}
},
"url": {
"nonStream": "https://chat.minimax.io",
"stream": "https://chat.minimax.io"
}
},
"haiper": {
"format": "openai",
"headers": {
@@ -5101,29 +5078,6 @@
"stream": "https://chat.qwen.ai/api/v2/chat/completions"
}
},
"raycast": {
"format": "openai",
"headers": {
"apiKey": {
"Accept": "text/event-stream",
"Authorization": "Bearer <TOK>",
"Content-Type": "application/json"
},
"nonStream": {
"Authorization": "Bearer <TOK>",
"Content-Type": "application/json"
},
"oauth": {
"Accept": "text/event-stream",
"Authorization": "Bearer <TOK>",
"Content-Type": "application/json"
}
},
"url": {
"nonStream": "https://backend.raycast.com/api/v1/ai",
"stream": "https://backend.raycast.com/api/v1/ai"
}
},
"regolo": {
"format": "openai",
"headers": {

View File

@@ -8,11 +8,7 @@ import { isLocalOnlyPath } from "../../../src/server/authz/routeGuard.ts";
// host-local credential files. They must NOT be PUBLIC (which skips the LOCAL_ONLY
// tier); they must classify MANAGEMENT and be loopback-gated.
const AUTO_IMPORT = [
"/api/oauth/cursor/auto-import",
"/api/oauth/kiro/auto-import",
"/api/oauth/raycast/auto-import",
];
const AUTO_IMPORT = ["/api/oauth/cursor/auto-import", "/api/oauth/kiro/auto-import"];
test("OAuth auto-import routes are excluded from PUBLIC classification", () => {
for (const p of AUTO_IMPORT) {

View File

@@ -102,11 +102,7 @@ test("CORS relaxation reason set is unchanged", () => {
});
test("LOCAL_ONLY oauth auto-import exclusions still win over the /api/oauth/ subtree", () => {
for (const route of [
"/api/oauth/cursor/auto-import",
"/api/oauth/kiro/auto-import",
"/api/oauth/raycast/auto-import",
]) {
for (const route of ["/api/oauth/cursor/auto-import", "/api/oauth/kiro/auto-import"]) {
assert.equal(isPublicApiRoute(route, "POST"), false, route);
assert.equal(classifyRoute(route, "POST").routeClass, "MANAGEMENT", route);
}

View File

@@ -1,248 +0,0 @@
import { describe, it } from "node:test";
import assert from "node:assert/strict";
const mod = await import("../../open-sse/executors/hailuo-web.ts");
describe("HailuoWebExecutor", () => {
it("can be instantiated", () => {
const executor = new mod.HailuoWebExecutor();
assert.ok(executor);
});
// Test vectors derived independently via Python's hashlib.md5 + urllib.parse.quote,
// reproducing generate_yy_header()/get_body_to_yy() from g4f's crypt.py bit-for-bit:
//
// import hashlib
// from urllib.parse import quote
// def hash_function(s): return hashlib.md5(s.encode()).hexdigest()
// def get_body_to_yy(characterID, msgContent, chatID):
// L = msgContent.replace("\r\n","").replace("\n","").replace("\r","")
// return hash_function(characterID) + hash_function(L) + hash_function(chatID) + hash_function("")
// def generate_yy_header(path, body_to_yy, t):
// encoded_path = quote(path, "")
// combined = f"{encoded_path}_{body_to_yy}{hash_function(str(t))}ooui"
// return hash_function(combined)
it("pyQuote percent-encodes exactly like Python's quote(s, safe='')", () => {
const path = "/v4/api/chat/msg?device_platform=web&biz_id=2&app_id=3001";
assert.equal(
mod.pyQuote(path),
"%2Fv4%2Fapi%2Fchat%2Fmsg%3Fdevice_platform%3Dweb%26biz_id%3D2%26app_id%3D3001"
);
// Unreserved chars (letters, digits, _.-~) pass through untouched.
assert.equal(mod.pyQuote("abcXYZ019_.-~"), "abcXYZ019_.-~");
});
it("getBodyToYy matches the independently-computed MD5 chain", () => {
const bodyToYy = mod.getBodyToYy("1", "hello world", "0");
assert.equal(
bodyToYy,
"c4ca4238a0b923820dcc509a6f75849b" +
"5eb63bbbe01eeed093cb22bb8f5acdc3" +
"cfcd208495d565ef66e7dff9f98764da" +
"d41d8cd98f00b204e9800998ecf8427e"
);
});
it("getBodyToYy normalizes CRLF/CR/LF in msgContent before hashing", () => {
const withCrlf = mod.getBodyToYy("1", "hello\r\nworld", "0");
const withLf = mod.getBodyToYy("1", "helloworld", "0");
assert.equal(withCrlf, withLf);
});
it("generateYyHeader matches the independently-computed signature", () => {
const path = "/v4/api/chat/msg?device_platform=web&biz_id=2&app_id=3001";
const bodyToYy = mod.getBodyToYy("1", "hello world", "0");
const yy = mod.generateYyHeader(path, bodyToYy, 1700000000000);
assert.equal(yy, "6893d64988ecf45b1de1808b91ae855b");
});
it("builds a stable path_and_query with derived device_id/uuid when none is supplied", () => {
const a = mod.buildHailuoPathAndQuery("token-abc", undefined, 1700000000000);
const b = mod.buildHailuoPathAndQuery("token-abc", undefined, 1700000000000);
assert.equal(a, b, "same token must derive the same fingerprint every time");
const params = new URL(`https://x${a}`).searchParams;
assert.equal(params.get("device_platform"), "web");
assert.equal(params.get("uuid")?.length, 32);
assert.equal(params.get("device_id")?.length, 32);
});
it("honors user-supplied device_id/uuid over the derived fallback", () => {
const path = mod.buildHailuoPathAndQuery(
"token-abc",
{ device_id: "real-device", uuid: "real-uuid" },
1700000000000
);
const params = new URL(`https://x${path}`).searchParams;
assert.equal(params.get("device_id"), "real-device");
assert.equal(params.get("uuid"), "real-uuid");
});
it("folds text-only OpenAI history into a single msgContent block", () => {
const folded = mod.foldHailuoMessages([
{ role: "system", content: "Be nice." },
{ role: "user", content: "hi" },
{ role: "assistant", content: "hello!" },
{ role: "user", content: "how are you?" },
]);
assert.equal(
folded,
"System: Be nice.\n\nUser: hi\n\nAssistant: hello!\n\nUser: how are you?"
);
});
it("throws on tool-call content it cannot faithfully forward", () => {
assert.throws(() =>
mod.foldHailuoMessages([{ role: "assistant", content: "", tool_calls: [{}] }])
);
assert.throws(() => mod.foldHailuoMessages([{ role: "tool", content: "result" }]));
});
it("diffs cumulative message_result content into deltas", () => {
const state = { emittedLen: 0 };
assert.equal(mod.extractHailuoMessageDelta("Hel", state), "Hel");
assert.equal(mod.extractHailuoMessageDelta("Hello", state), "lo");
assert.equal(mod.extractHailuoMessageDelta("Hello", state), "", "no growth => no delta");
});
it("parses event:/data: SSE lines and swallows malformed data without throwing", () => {
const eventLine = mod.parseHailuoLine("event: message_result");
assert.deepEqual(eventLine, { type: "event", value: "message_result" });
const dataLine = mod.parseHailuoLine('data: {"data":{"messageResult":{"content":"hi"}}}');
assert.deepEqual(dataLine, { type: "data", value: { data: { messageResult: { content: "hi" } } } });
// Truncated/malformed JSON must not throw — the stream must keep going.
assert.equal(mod.parseHailuoLine("data: {not json"), null);
assert.equal(mod.parseHailuoLine("not a recognized line"), null);
});
it("extracts message_result.content from a send_result/message_result event payload", () => {
const content = mod.extractHailuoMessageResultContent({
data: { messageResult: { content: "partial answer" } },
});
assert.equal(content, "partial answer");
assert.equal(mod.extractHailuoMessageResultContent({ data: { sendResult: { chatID: "1" } } }), null);
});
it("returns a 401 credential error when the token is missing", async () => {
const executor = new mod.HailuoWebExecutor();
const result = await executor.execute({
model: "hailuo",
body: { messages: [{ role: "user", content: "hi" }] },
stream: false,
credentials: { apiKey: "" },
signal: null,
});
const text = await result.response.text();
assert.equal(result.response.status, 401);
assert.match(text, /token/i);
});
it("maps an upstream 401 (invalid/expired token) as a terminal, non-cooldown error", async () => {
const originalFetch = globalThis.fetch;
globalThis.fetch = (async () =>
new Response(JSON.stringify({ error: "invalid token" }), { status: 401 })) as typeof fetch;
try {
const executor = new mod.HailuoWebExecutor();
const result = await executor.execute({
model: "hailuo",
body: { messages: [{ role: "user", content: "hi" }] },
stream: false,
credentials: { apiKey: "expired-token" },
signal: null,
});
assert.equal(result.response.status, 401);
} finally {
globalThis.fetch = originalFetch;
}
});
it("maps an upstream 429 as a transient (retryable) error", async () => {
const originalFetch = globalThis.fetch;
globalThis.fetch = (async () =>
new Response(JSON.stringify({ error: "rate limited" }), { status: 429 })) as typeof fetch;
try {
const executor = new mod.HailuoWebExecutor();
const result = await executor.execute({
model: "hailuo",
body: { messages: [{ role: "user", content: "hi" }] },
stream: false,
credentials: { apiKey: "some-token" },
signal: null,
});
assert.equal(result.response.status, 429);
} finally {
globalThis.fetch = originalFetch;
}
});
it("collects a non-streaming completion from send_result/message_result/close_chunk SSE events", async () => {
const sse = [
"event: send_result",
'data: {"data":{"sendResult":{"chatID":"c1","chatTitle":"hi"}}}',
"event: message_result",
'data: {"data":{"messageResult":{"content":"Hel"}}}',
"event: message_result",
'data: {"data":{"messageResult":{"content":"Hello"}}}',
"event: close_chunk",
"",
].join("\n");
const originalFetch = globalThis.fetch;
globalThis.fetch = (async () => new Response(sse, { status: 200 })) as typeof fetch;
try {
const executor = new mod.HailuoWebExecutor();
const result = await executor.execute({
model: "hailuo",
body: { messages: [{ role: "user", content: "hi" }] },
stream: false,
credentials: { apiKey: "some-token" },
signal: null,
});
const json = await result.response.json();
assert.equal(result.response.status, 200);
assert.equal(json.choices[0].message.content, "Hello");
assert.equal(new URL(result.url).pathname, "/v4/api/chat/msg");
} finally {
globalThis.fetch = originalFetch;
}
});
it("streams incremental deltas for a streaming request", async () => {
const sse = [
"event: message_result",
'data: {"data":{"messageResult":{"content":"Hi"}}}',
"event: message_result",
'data: {"data":{"messageResult":{"content":"Hi there"}}}',
"event: close_chunk",
"",
].join("\n");
const originalFetch = globalThis.fetch;
globalThis.fetch = (async () => new Response(sse, { status: 200 })) as typeof fetch;
try {
const executor = new mod.HailuoWebExecutor();
const result = await executor.execute({
model: "hailuo",
body: { messages: [{ role: "user", content: "hi" }] },
stream: true,
credentials: { apiKey: "some-token" },
signal: null,
});
const text = await result.response.text();
assert.match(text, /"content":"Hi"/);
assert.match(text, /"content":" there"/);
assert.match(text, /data: \[DONE\]/);
} finally {
globalThis.fetch = originalFetch;
}
});
});

View File

@@ -0,0 +1,48 @@
import assert from "node:assert/strict";
import fs from "node:fs";
import path from "node:path";
import test from "node:test";
import { REGISTRY } from "../../open-sse/config/providerRegistry.ts";
import { hasSpecializedExecutor } from "../../open-sse/executors/index.ts";
test("Raycast Relay GPL-derived integration is absent from runtime dispatch", () => {
assert.equal(REGISTRY.raycast, undefined);
assert.equal(REGISTRY.rc, undefined);
assert.equal(hasSpecializedExecutor("raycast"), false);
assert.equal(hasSpecializedExecutor("rc"), false);
assert.ok(REGISTRY.github, "the independent official GitHub provider must remain");
});
test("Hailuo Web GPL-derived integration is absent without removing official MiniMax", () => {
assert.equal(REGISTRY["hailuo-web"], undefined);
assert.equal(hasSpecializedExecutor("hailuo-web"), false);
assert.ok(REGISTRY.minimax, "the official MiniMax provider must remain registered");
assert.ok(REGISTRY["minimax-cn"], "the official MiniMax China provider must remain registered");
});
test("GPL-derived implementation and credential source files are absent from the shipped tree", () => {
const removedPaths = [
"open-sse/config/providers/registry/minimax/web/index.ts",
"open-sse/config/providers/registry/raycast/index.ts",
"open-sse/executors/hailuo-web.ts",
"open-sse/executors/raycast.ts",
"open-sse/services/raycast.ts",
"scripts/raycast/extract-credentials.mjs",
"scripts/raycast/usage-benchmark.mjs",
"src/app/api/oauth/raycast/auto-import/route.ts",
"src/app/api/oauth/raycast/import/route.ts",
"src/lib/oauth/providers/raycast.ts",
"src/lib/oauth/services/raycast.ts",
"src/lib/oauth/services/raycastLocal.ts",
"src/shared/components/RaycastAuthModal.tsx",
];
for (const relativePath of removedPaths) {
assert.equal(
fs.existsSync(path.join(process.cwd(), relativePath)),
false,
`${relativePath} must not ship`
);
}
});

View File

@@ -0,0 +1,130 @@
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-gpl-provider-retirement-"));
process.env.DATA_DIR = TEST_DATA_DIR;
const core = await import("../../src/lib/db/core.ts");
const apiKeysDb = await import("../../src/lib/db/apiKeys.ts");
const RETIRED_PROVIDER_IDS = ["raycast", "rc", "hailuo-web"] as const;
test.after(() => {
core.resetDbInstance();
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
});
test("migration 163 disables GPL-derived connections fail-closed and preserves audit history", async () => {
const db = core.getDbInstance();
const applied = db
.prepare("SELECT version FROM _omniroute_migrations WHERE version = 163")
.get() as { version: number } | undefined;
assert.ok(applied, "migration 163 must be recorded as applied");
// api_keys policy columns are reconciled lazily by the domain module on fresh
// databases; production upgrades already carry them from normal API-key use.
await apiKeysDb.getApiKeys();
for (const provider of [...RETIRED_PROVIDER_IDS, "minimax", "minimax-cn"]) {
const connectionId = `${provider}-connection`;
db.prepare(
"INSERT INTO provider_connections " +
"(id, provider, auth_type, name, is_active, created_at, updated_at) " +
"VALUES (?, ?, 'apikey', ?, 1, datetime('now'), datetime('now'))"
).run(connectionId, provider, `${provider}-fixture`);
}
db.prepare(
"INSERT INTO api_keys " +
"(id, name, key, key_hash, key_prefix, allowed_connections, is_active, created_at) " +
"VALUES ('restricted-key', 'restricted-key', 'restricted-secret', " +
"'restricted-hash', 'restrict', ?, 1, datetime('now'))"
).run(JSON.stringify(["raycast-connection", "minimax-connection"]));
for (const provider of RETIRED_PROVIDER_IDS) {
const connectionId = `${provider}-connection`;
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 (?, 'restricted-key', ?, ?, 1, " +
"'ACTIVE', datetime('now'), datetime('now'), datetime('now', '+1 hour'))"
).run(provider.padEnd(64, "0"), provider, connectionId);
db.prepare(
"INSERT INTO usage_history (provider, model, timestamp) " +
"VALUES (?, 'legacy-model', datetime('now'))"
).run(provider);
db.prepare(
"INSERT INTO call_logs (id, timestamp, provider, model, status) " +
"VALUES (?, datetime('now'), ?, 'legacy-model', 200)"
).run(`${provider}-call`, provider);
db.prepare(
"INSERT INTO quota_snapshots " +
"(provider, connection_id, window_key, remaining_percentage, is_exhausted, created_at) " +
"VALUES (?, ?, 'monthly', 50, 0, ?)"
).run(provider, connectionId, new Date().toISOString());
}
const sql = fs.readFileSync(
path.join(process.cwd(), "src/lib/db/migrations/163_retire_gpl_derived_providers.sql"),
"utf8"
);
db.exec(sql);
db.exec(sql);
for (const provider of RETIRED_PROVIDER_IDS) {
const connectionId = `${provider}-connection`;
const connection = db
.prepare(
"SELECT is_active, test_status, error_code, last_error_type, last_error_source " +
"FROM provider_connections WHERE id = ?"
)
.get(connectionId) as {
is_active: number;
test_status: string;
error_code: string;
last_error_type: string;
last_error_source: string;
};
assert.deepEqual(connection, {
is_active: 0,
test_status: "unavailable",
error_code: "PROVIDER_REMOVED",
last_error_type: "provider_removed",
last_error_source: "migration:163",
});
const lease = db
.prepare(
"SELECT state, ended_at, end_reason FROM exclusive_connection_leases " +
"WHERE connection_id = ?"
)
.get(connectionId) as { state: string; ended_at: string | null; end_reason: string | null };
assert.equal(lease.state, "INVALIDATED");
assert.ok(lease.ended_at);
assert.equal(lease.end_reason, "provider integration retired in v3.8.50");
assert.ok(db.prepare("SELECT id FROM usage_history WHERE provider = ?").get(provider));
assert.ok(db.prepare("SELECT id FROM call_logs WHERE provider = ?").get(provider));
assert.ok(db.prepare("SELECT id FROM quota_snapshots WHERE provider = ?").get(provider));
}
for (const provider of ["minimax", "minimax-cn"]) {
const row = db
.prepare("SELECT is_active FROM provider_connections WHERE id = ?")
.get(`${provider}-connection`) as { is_active: number };
assert.equal(row.is_active, 1, `${provider} must remain active`);
}
const apiKey = db
.prepare("SELECT is_active, allowed_connections FROM api_keys WHERE id = 'restricted-key'")
.get() as { is_active: number; allowed_connections: string };
assert.equal(apiKey.is_active, 1);
assert.deepEqual(JSON.parse(apiKey.allowed_connections), [
"raycast-connection",
"minimax-connection",
]);
});

View File

@@ -36,7 +36,7 @@ test("#9502: web-session credential spec for muse-spark-web mentions the ecto1:
assert.ok(startIdx >= 0, "muse-spark-web credential spec not found");
const museSection = webSessionCredentials.slice(
startIdx,
webSessionCredentials.indexOf('"hailuo-web"', startIdx)
webSessionCredentials.indexOf('"claude-web"', startIdx)
);
assert.match(museSection, /ecto1/, "credential spec must mention the ecto1: WS auth token");
});

View File

@@ -41,7 +41,6 @@ const {
OAUTH_TIMEOUT,
PROVIDERS: OAUTH_PROVIDER_IDS,
QODER_CONFIG,
RAYCAST_CONFIG,
TRAE_CONFIG,
XAI_OAUTH_CONFIG,
OPENFERENCE_CONFIG,
@@ -65,7 +64,6 @@ const EXPECTED_PROVIDER_KEYS = [
"amazon-q",
"cursor",
"trae",
"raycast",
"kilocode",
"cline",
"clinepass",
@@ -104,7 +102,6 @@ const EXPECTED_CONFIG_BY_PROVIDER = {
clinepass: CLINE_CONFIG, // reuses the Cline WorkOS flow (clinepass: cline in providers/index.ts)
"devin-desktop": DEVIN_DESKTOP_CONFIG,
"devin-cli": DEVIN_DESKTOP_CONFIG,
raycast: RAYCAST_CONFIG,
trae: TRAE_CONFIG,
"grok-cli": GROK_BUILD_OAUTH_CONFIG,
"xai-oauth": XAI_OAUTH_CONFIG,

View File

@@ -62,12 +62,10 @@ test("src/shared providers map resolves the same aliases unambiguously", () => {
assert.equal(getProviderAlias("kimi"), "kimi");
});
// #6673: hailuo-web must not collide with the paid API-key minimax/minimax-cn
// providers — it uses its own id as alias, per the secondary-variant convention.
test("hailuo-web resolves to its own id/alias and does not collide with minimax", () => {
assert.equal(PROVIDER_ID_TO_ALIAS["hailuo-web"], "hailuo-web");
assert.equal(resolveProviderId("hailuo-web"), "hailuo-web");
assert.equal(getProviderAlias("hailuo-web"), "hailuo-web");
test("retired Hailuo alias is absent while official MiniMax aliases remain distinct", () => {
assert.equal(PROVIDER_ID_TO_ALIAS["hailuo-web"], undefined);
assert.equal(PROVIDER_ID_TO_ALIAS.minimax, "minimax");
assert.equal(PROVIDER_ID_TO_ALIAS["minimax-cn"], "minimax-cn");
assert.equal(resolveProviderId("minimax"), "minimax");
assert.equal(resolveProviderId("minimax-cn"), "minimax-cn");
});

View File

@@ -21,22 +21,30 @@ test("cookie-kind web sessions do not use the API-key test path", () => {
});
test("token-kind web sessions WITHOUT a token-aware validator stay off the API-key test path", () => {
// hailuo-web and promptql are `kind: "token"` in WEB_SESSION_CREDENTIAL_REQUIREMENTS,
// but neither has an entry in validation.ts's SPECIALTY_VALIDATORS map (nor a
// token-aware branch like zai-web's in validateWebCookieProvider). Routing them through
// promptql and microsoft-designer-web are `kind: "token"` in
// WEB_SESSION_CREDENTIAL_REQUIREMENTS, but neither has an entry in validation.ts's
// SPECIALTY_VALIDATORS map (nor a token-aware branch like zai-web's). Routing them through
// the API-key test path would dispatch to the generic cookie probe, which sends the
// stored token as a `Cookie` header and treats most non-401/403 responses as valid —
// an invalid hailuo-web/promptql token could be reported as a healthy connection.
assert.equal(shouldUseApiKeyConnectionTest("cookie", "hailuo-web"), false);
// an invalid token could be reported as a healthy connection.
assert.equal(shouldUseApiKeyConnectionTest("cookie", "promptql"), false);
// Same reasoning applies to the other two token-kind providers with no validator.
// Same reasoning applies to the other token-kind providers with no validator.
assert.equal(shouldUseApiKeyConnectionTest("cookie", "microsoft-designer-web"), false);
assert.equal(shouldUseApiKeyConnectionTest("cookie", "t3-chat-web"), false);
// Retired provider ids also stay off every credential-test path.
assert.equal(shouldUseApiKeyConnectionTest("cookie", "hailuo-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);
}
});

View File

@@ -17,8 +17,8 @@
// sarvam+plamo in regional) to 193, then #8170 (inception/typhoon — inception in frontier-labs,
// typhoon in regional) to 195, then Firecrawl dual search+fetch under SEARCH_PROVIDERS.firecrawl
// (removed specialty-media duplicate) to 194, #8861 (Xiaomi MiMo Token Plan, regional) to 195, and
// the Cheaper Inference gateway (OSS-sponsor reseller, gateways family) to 198 (UnoRouter #9009,
// Raycast Pro #8895), then later additions to 199; retiring GitHub Models brings it to 198.
// the Cheaper Inference gateway (OSS-sponsor reseller, gateways family) to 198 (UnoRouter #9009),
// then later additions to 199; retiring GitHub Models brings it to 198.
// The v3.8.50 free-tier gateway waves (#9631 registry cycle, waves 2-5, #9210 phase 3) grew the
// gateways family to 228 measured on the tip; Puter retired (#10210) and chatanywhere restored
// (base-reds round 3, #9985) are both included in that measurement; Cursor API (specialty-media,

View File

@@ -1,75 +0,0 @@
/**
* @file raycast-auth.test.ts
* @description Unit tests for Raycast V2 signature + message conversion.
*
* @changes
* - [2026-07-27] [Composer] - Initial Raycast protocol tests
*/
import assert from "node:assert/strict";
import { describe, it } from "node:test";
import {
buildRaycastChatBody,
convertOpenAiMessages,
decodeAidFromRaycastJwt,
inferProviderInfo,
rot13rot5,
signatureV2,
} from "../../open-sse/services/raycast.ts";
describe("raycast auth protocol", () => {
it("rot13rot5 encodes alphanumerics", () => {
assert.equal(rot13rot5("ABCabc123"), "NOPnop678");
});
it("signatureV2 matches raycast-relay fixture shape", () => {
const secret = "6bc455473576ce2cd6f70426caff867aabbe3f7291c1a79681af5e8ce0ca1408";
const timestamp = "1720000000";
const deviceId = "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee";
const payload = "{}";
const sig = signatureV2(timestamp, deviceId, payload, secret);
assert.match(sig, /^[a-f0-9]{64}$/);
assert.equal(sig, signatureV2(timestamp, deviceId, payload, secret));
});
it("decodes aid from JWT payload", () => {
const header = Buffer.from(JSON.stringify({ typ: "JWT", alg: "HS256" })).toString("base64url");
const payload = Buffer.from(JSON.stringify({ aid: "test-aid-123", exp: 9999999999, iat: 1 })).toString(
"base64url"
);
const jwt = `${header}.${payload}.fake-sig`;
assert.equal(decodeAidFromRaycastJwt(jwt), "test-aid-123");
});
it("converts OpenAI messages to Raycast shape", () => {
const { raycastMessages, systemInstruction } = convertOpenAiMessages([
{ role: "system", content: "Be concise" },
{ role: "user", content: "Hello" },
]);
assert.equal(systemInstruction, "Be concise");
assert.deepEqual(raycastMessages, [{ author: "user", content: { text: "Hello" } }]);
});
it("infers provider prefixes from model ids", () => {
assert.deepEqual(inferProviderInfo("openai-gpt-5-mini"), {
provider: "openai",
model: "gpt-5-mini",
});
assert.deepEqual(inferProviderInfo("anthropic-claude-sonnet-4-6"), {
provider: "anthropic",
model: "claude-sonnet-4-6",
});
assert.deepEqual(inferProviderInfo("raycast-ray1"), { provider: "raycast", model: "ray1" });
});
it("buildRaycastChatBody includes thread_id and provider split", () => {
const body = JSON.parse(
buildRaycastChatBody("openai-gpt-5-mini", [{ role: "user", content: "ping" }], 0.7)
);
assert.equal(body.model, "gpt-5-mini");
assert.equal(body.provider, "openai");
assert.equal(body.temperature, 0.7);
assert.equal(typeof body.thread_id, "string");
assert.equal(body.messages[0].author, "user");
});
});

View File

@@ -1,18 +0,0 @@
/**
* @file raycast-local-extract.test.ts
* @description Tests for Raycast local credential extraction (mocked where needed).
*
* @changes
* - [2026-07-27] [Composer] - Raycast local extract unit tests
*/
import assert from "node:assert/strict";
import { describe, it } from "node:test";
import { isRaycastLocalExtractAvailable } from "../../src/lib/oauth/services/raycastLocal.ts";
describe("raycast local extract", () => {
it("reports availability on darwin when Raycast paths exist", () => {
const result = isRaycastLocalExtractAvailable();
assert.equal(typeof result, "boolean");
});
});

View File

@@ -5,7 +5,6 @@
* has nothing to do with `BaseExecutor.buildHeaders(credentials, stream?, clientHeaders?,
* model?, health?)`:
*
* hailuo-web (token: string, yy: string)
* lmarena (_model: string, credentials: unknown, _body: unknown)
* qwen-web (token: string, cookieHeader: string, chatId?: string)
*
@@ -23,12 +22,10 @@ import test from "node:test";
import assert from "node:assert/strict";
import { BaseExecutor } from "../../open-sse/executors/base.ts";
import { HailuoWebExecutor } from "../../open-sse/executors/hailuo-web.ts";
import { LMArenaExecutor } from "../../open-sse/executors/lmarena.ts";
import { QwenWebExecutor } from "../../open-sse/executors/qwen-web.ts";
const CASES = [
{ name: "hailuo-web", make: () => new HailuoWebExecutor(), helper: "buildStreamHeaders" },
{ name: "lmarena", make: () => new LMArenaExecutor(), helper: "buildRequestHeaders" },
{ name: "qwen-web", make: () => new QwenWebExecutor(), helper: "buildApiHeaders" },
];

View File

@@ -1,16 +0,0 @@
import { describe, it } from "node:test";
import assert from "node:assert/strict";
import { WEB_COOKIE_PROVIDERS } from "../../src/shared/constants/providers/web-cookie.ts";
import { hailuo_webProvider } from "../../open-sse/config/providers/registry/minimax/web/index.ts";
describe("hailuo-web domain updates (#11000)", () => {
it("hailuo-web website targets chat.minimax.io to avoid 301 redirect errors", () => {
const entry = WEB_COOKIE_PROVIDERS["hailuo-web"];
assert.ok(entry);
assert.equal(entry.website, "https://chat.minimax.io");
});
it("hailuo-web registry entry baseUrl targets chat.minimax.io", () => {
assert.equal(hailuo_webProvider.baseUrl, "https://chat.minimax.io");
});
});