diff --git a/.env.example b/.env.example index 85967b6d58..5772c425fb 100644 --- a/.env.example +++ b/.env.example @@ -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 # ───────────────────────────────────────────────────────────────────────────── diff --git a/PROVIDER_REFERENCE.md b/PROVIDER_REFERENCE.md deleted file mode 100644 index 571fe0e904..0000000000 --- a/PROVIDER_REFERENCE.md +++ /dev/null @@ -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 `{...}` blocks); `none` — `tools` is currently silently dropped. See #7286. - -Use the dashboard at `/dashboard/providers` to enable, configure, and test each provider. - ---- - -## No-auth Providers (no key required) (11) - -| ID | Alias | Name | Tags | Website | Notes | Tool calling | -|----|-------|------|------|---------|-------|--------------| -| `aihorde` | `horde` | AI Horde | No-auth | [link](https://aihorde.net) | No API key required — uses AI Horde's documented anonymous key. Adding a free aihorde.net key is optional and only buys higher queue priority (kudos). | — | -| `auggie` | `aug` | Augment (Auggie CLI) | No-auth | [link](https://augmentcode.com) | No API key stored by OmniRoute. Install the Auggie CLI and run `auggie login` on this machine, then OmniRoute spawns it locally for each request. | — | -| `chipotle` | `pepper` | Chipotle Pepper AI (Free) | No-auth | [link](https://amelia.chipotle.com) | No credentials required. Uses Chipotle's public support chatbot via reverse-engineered SockJS/STOMP protocol. | — | -| `cloudflare-playground` | `cfp` | Cloudflare AI Playground | No-auth | [link](https://playground.ai.cloudflare.com) | No credentials required — anonymous browser sessions over a reverse-engineered cf_agent WebSocket protocol (Playwright transport). | — | -| `devin-cli-agentic` | `dva` | Devin CLI Agentic Bridge | No-auth | [link](https://docs.devin.ai/work-with-devin/devin-cli) | Authentication is owned by the official Devin CLI in its isolated bridge volume. | emulated | -| `duckduckgo-web` | `ddgw` | DuckDuckGo AI Chat | No-auth | [link](https://duckduckgo.com/duckchat) | No credentials required — DuckDuckGo AI Chat is anonymous and free. | emulated | -| `felo-web` | `felo` | Felo | No-auth | [link](https://felo.ai) | No credentials required — Felo is a free, no-signup chat/search aggregator. | — | -| `opencode` | `oc` | OpenCode Free | No-auth | [link](https://opencode.ai) | No API key required — uses OpenCode's public free endpoint. | — | -| `theoldllm` | `tllm` | The Old LLM (Free) | No-auth | [link](https://theoldllm.vercel.app) | No credentials required. The executor auto-generates access tokens via an embedded Playwright browser instance. | — | -| `veoaifree-web` | `veo-free` | Veo AI Free | No-auth, video | [link](https://veoaifree.com) | No auth required. Rate limited to 6 requests/hour per IP. | — | -| `zcode` | `zc` | ZCode (GLM Coding Plan) | No-auth | [link](https://zcode.z.ai) | No API key stored by OmniRoute. The local ZCode app-server uses the existing builtin:zai-coding-plan login. | — | - -## OAuth Providers (25) - -| ID | Alias | Name | Tags | Website | Notes | -|----|-------|------|------|---------|-------| -| `agy` | `agy` | Antigravity CLI | OAuth | [link](https://antigravity.google) | Import your Antigravity CLI (`agy`) login (paste/upload its token file), auto-detect a local CLI login, or sign in with Google. Shares the Antigravity backend (incl. Claude models). | -| `amazon-q` | `aq` | Amazon Q | OAuth | [link](https://aws.amazon.com/q/developer/) | Uses the same AWS Builder ID or imported refresh-token flow as Kiro, but keeps Amazon Q connections separate. | -| `antigravity` | — | Antigravity | OAuth | — | — | -| `claude` | `cc` | Claude Code | OAuth | — | — | -| `cline` | `cl` | Cline | OAuth | — | — | -| `clinepass` | `cp` | ClinePass | OAuth | [link](https://cline.bot/cline-pass) | ClinePass is Cline's $9.99/mo subscription bundling 10 open coding models. Sign in with your Cline account (same login as the Cline CLI/IDE), or paste a direct ClinePass API key (app.cline.bot → Settings → API Keys). A ClinePass subscription unlocks the cline-pass/* models. Reuses the Cline WorkOS OAuth flow. | -| `codebuddy-cn` | `cbcn` | CodeBuddy CN | OAuth | [link](https://copilot.tencent.com) | Tencent CodeBuddy CN (copilot.tencent.com). Sign in via the official CLI device-code flow, or paste a direct API key (sent as Authorization: Bearer). Catalog: GLM / Kimi / MiniMax / DeepSeek / Hunyuan. | -| `codex` | `cx` | OpenAI Codex | OAuth | — | — | -| `cursor` | `cu` | Cursor IDE | OAuth | — | — | -| `devin-cli` | `dv` | Devin CLI | OAuth | [link](https://cli.devin.ai) | Requires the Devin CLI binary. Run `devin auth login` to authenticate, or provide your WINDSURF_API_KEY. Install: https://cli.devin.ai | -| `devin-desktop` | — | Devin Desktop | OAuth | [link](https://devin.ai) | Paste an existing Devin API key from an authenticated Devin session. Key export availability and steps vary by Devin version and account. | -| `ghe-copilot` | `ghe-copilot` | GitHub Enterprise Copilot | OAuth | — | Enter your GHE instance URL (e.g., https://ghe.company.com) in provider settings, then authenticate via device flow. | -| `github` | `gh` | GitHub Copilot | OAuth | — | — | -| `gitlab-duo` | `gitlab-duo` | GitLab Duo | OAuth | [link](https://docs.gitlab.com/user/duo_agent_platform/code_suggestions/) | GitLab Duo OAuth is not configured. Register an OAuth application at https://gitlab.com/-/profile/applications with redirect URI http://localhost:20128/callback and scopes "ai_features read_user", then set GITLAB_DUO_OAUTH_CLIENT_ID (and optionally GITLAB_DUO_OAUTH_CLIENT_SECRET) and restart. | -| `grok-cli` | `gc` | Grok Build | OAuth | — | Sign in with your browser, or paste your ~/.grok/auth.json (or the JWT access token) from the Grok Build CLI; refresh_token is rotated automatically either way. | -| `kilocode` | `kc` | Kilo Code | OAuth | — | — | -| `kimi-coding` | `kmc` | Kimi Code CLI | OAuth | [link](https://www.kimi.com/code?aff=omniroute) | Sign in with the same Kimi account used by Kimi Code CLI. OmniRoute uses the CLI OAuth flow and Kimi Coding Plan endpoints. | -| `kiro` | `kr` | Kiro AI | OAuth | — | Free tier: 50 credits/month (~25K–100K tokens). ⚠️ Kiro ToS prohibits third-party proxy/harness use. | -| `openference` | `of` | Openference | OAuth | [link](https://openference.com) | Sign in with your Openference account to route requests through api.openference.com. An active plan is required for inference — OAuth may authenticate but return 402 without one. | -| `qoder` | `if` | Qoder | OAuth | — | — | -| `raycast` | `rc` | Raycast Pro AI | OAuth | [link](https://raycast.com/ai) | Unofficial integration — uses your Raycast Pro subscription via credentials from the macOS app (Auto-Import or manual capture). May break on Raycast updates. Not for redistribution; personal use only. | -| `trae` | `tr` | Trae | OAuth | [link](https://trae.ai) | Trae is an AI-native IDE by ByteDance (SOLO remote agent). Authorize via trae.ai in the popup, or sign in at solo.trae.ai and paste the Cloud-IDE-JWT (sent as 'Authorization: Cloud-IDE-JWT ', ~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/?…&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://.services.ai.azure.com/openai/v1/ or https://.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 . 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 . | -| `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/. | -| `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 . 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 . 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://--.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 . OmniRoute targets the chatbot endpoint on https://api.nlpcloud.io/v1/gpu//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..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-. 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 . 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/ 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 . 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 . 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 . 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://.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 . 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 . 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 diff --git a/changelog.d/maintenance/pending-retire-gpl-derived-integrations.md b/changelog.d/maintenance/pending-retire-gpl-derived-integrations.md new file mode 100644 index 0000000000..fca4cef4bc --- /dev/null +++ b/changelog.d/maintenance/pending-retire-gpl-derived-integrations.md @@ -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. diff --git a/config/quality/eslint-suppressions.json b/config/quality/eslint-suppressions.json index 86fc92a447..974d6af121 100644 --- a/config/quality/eslint-suppressions.json +++ b/config/quality/eslint-suppressions.json @@ -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 diff --git a/config/quality/test-masking-allowlist.json b/config/quality/test-masking-allowlist.json index fd828b6d5f..adf03e4f42 100644 --- a/config/quality/test-masking-allowlist.json +++ b/config/quality/test-masking-allowlist.json @@ -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." diff --git a/docs/architecture/ARCHITECTURE.md b/docs/architecture/ARCHITECTURE.md index 790cf35de7..0f4657f726 100644 --- a/docs/architecture/ARCHITECTURE.md +++ b/docs/architecture/ARCHITECTURE.md @@ -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 | ✅ | ✅ | ❌ | ❌ | diff --git a/docs/architecture/CODEBASE_DOCUMENTATION.md b/docs/architecture/CODEBASE_DOCUMENTATION.md index a0d9ff603e..d6437857a3 100644 --- a/docs/architecture/CODEBASE_DOCUMENTATION.md +++ b/docs/architecture/CODEBASE_DOCUMENTATION.md @@ -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` | diff --git a/docs/architecture/REPOSITORY_MAP.md b/docs/architecture/REPOSITORY_MAP.md index 4d59418054..84bf149a67 100644 --- a/docs/architecture/REPOSITORY_MAP.md +++ b/docs/architecture/REPOSITORY_MAP.md @@ -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` | diff --git a/docs/i18n/pl/docs/architecture/ARCHITECTURE.md b/docs/i18n/pl/docs/architecture/ARCHITECTURE.md index 6997bd716a..45e6c33cb1 100644 --- a/docs/i18n/pl/docs/architecture/ARCHITECTURE.md +++ b/docs/i18n/pl/docs/architecture/ARCHITECTURE.md @@ -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 | ✅ | ✅ | ❌ | ❌ | diff --git a/docs/i18n/pt-BR/docs/architecture/ARCHITECTURE.md b/docs/i18n/pt-BR/docs/architecture/ARCHITECTURE.md index ae89dd97ca..f6d6534a09 100644 --- a/docs/i18n/pt-BR/docs/architecture/ARCHITECTURE.md +++ b/docs/i18n/pt-BR/docs/architecture/ARCHITECTURE.md @@ -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 | ✅ | ✅ | ❌ | ❌ | diff --git a/docs/i18n/zh-CN/docs/architecture/ARCHITECTURE.md b/docs/i18n/zh-CN/docs/architecture/ARCHITECTURE.md index 691bfae8c1..e79474458d 100644 --- a/docs/i18n/zh-CN/docs/architecture/ARCHITECTURE.md +++ b/docs/i18n/zh-CN/docs/architecture/ARCHITECTURE.md @@ -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` | 服务商模型 | 服务商模型元数据,支撑自定义和管理可用模型 | ## 旁路处理器 diff --git a/docs/i18n/zh-TW/docs/architecture/ARCHITECTURE.md b/docs/i18n/zh-TW/docs/architecture/ARCHITECTURE.md index 756e62c76b..5c3fd5a85b 100644 --- a/docs/i18n/zh-TW/docs/architecture/ARCHITECTURE.md +++ b/docs/i18n/zh-TW/docs/architecture/ARCHITECTURE.md @@ -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 | OAuth(GitLab) | ✅ | ✅ | ✅ | ❌ | -| Devin CLI | openai | 本機 CLI 登入 | ✅ | ✅ | ❌ | ✅ 任務 API | +| Devin CLI | openai | 本機 CLI 登入 | ✅ | ✅ | ❌ | ✅ 任務 API | | Codex Cloud | openai-responses | OAuth | ✅ | ❌ | ✅ | ✅ 速率限制 | | Jules | openai | OAuth | ✅ | ✅ | ✅ | ✅ 任務 API | | AgentRouter | openai | API 金鑰 | ✅ | ✅ | ❌ | ❌ | diff --git a/docs/reference/ENVIRONMENT.md b/docs/reference/ENVIRONMENT.md index 6349719a85..7b2f9c09ac 100644 --- a/docs/reference/ENVIRONMENT.md +++ b/docs/reference/ENVIRONMENT.md @@ -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] > diff --git a/docs/reference/PROVIDER_REFERENCE.md b/docs/reference/PROVIDER_REFERENCE.md index bb1e819178..aa5322e84e 100644 --- a/docs/reference/PROVIDER_REFERENCE.md +++ b/docs/reference/PROVIDER_REFERENCE.md @@ -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 (~25K–100K tokens). ⚠️ Kiro ToS prohibits third-party proxy/harness use. | | `openference` | `of` | Openference | OAuth | [link](https://openference.com) | Sign in with your Openference account to route requests through api.openference.com. An active plan is required for inference — OAuth may authenticate but return 402 without one. | | `qoder` | `if` | Qoder | OAuth | — | — | -| `raycast` | `rc` | Raycast Pro AI | OAuth | [link](https://raycast.com/ai) | Unofficial integration — uses your Raycast Pro subscription via credentials from the macOS app (Auto-Import or manual capture). May break on Raycast updates. Not for redistribution; personal use only. | | `trae` | `tr` | Trae | OAuth | [link](https://trae.ai) | Trae is an AI-native IDE by ByteDance (SOLO remote agent). Authorize via trae.ai in the popup, or sign in at solo.trae.ai and paste the Cloud-IDE-JWT (sent as 'Authorization: Cloud-IDE-JWT ', ~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 | diff --git a/open-sse/config/providers/index.ts b/open-sse/config/providers/index.ts index c54ea48c60..24533d6972 100644 --- a/open-sse/config/providers/index.ts +++ b/open-sse/config/providers/index.ts @@ -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 = { 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 = { "perplexity-web": perplexity_webProvider, minimax: minimaxProvider, "minimax-cn": minimax_cnProvider, - "hailuo-web": hailuo_webProvider, haiper: haiperProvider, bytez: bytezProvider, blackbox: blackboxProvider, diff --git a/open-sse/config/providers/registry/minimax/web/index.ts b/open-sse/config/providers/registry/minimax/web/index.ts deleted file mode 100644 index 6c2addc043..0000000000 --- a/open-sse/config/providers/registry/minimax/web/index.ts +++ /dev/null @@ -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, -}; diff --git a/open-sse/config/providers/registry/raycast/index.ts b/open-sse/config/providers/registry/raycast/index.ts deleted file mode 100644 index aca286a6d1..0000000000 --- a/open-sse/config/providers/registry/raycast/index.ts +++ /dev/null @@ -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" }, - ], -}; diff --git a/open-sse/executors/hailuo-web.ts b/open-sse/executors/hailuo-web.ts deleted file mode 100644 index 7d1b839c26..0000000000 --- a/open-sse/executors/hailuo-web.ts +++ /dev/null @@ -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? - * 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; - -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; - 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 { - 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; 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, - form: FormData, - signal: AbortSignal | null | undefined, - body: unknown, - bodyObj: JsonRecord - ): Promise<{ upstream: Response } | { errorResult: ReturnType }> { - 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, - 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); - } -} diff --git a/open-sse/executors/index.ts b/open-sse/executors/index.ts index 20155214ba..c790f503d9 100644 --- a/open-sse/executors/index.ts +++ b/open-sse/executors/index.ts @@ -176,9 +176,6 @@ const lazyExecutors: Record Promise> = { "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()), diff --git a/open-sse/executors/raycast.ts b/open-sse/executors/raycast.ts deleted file mode 100644 index bfa8d28028..0000000000 --- a/open-sse/executors/raycast.ts +++ /dev/null @@ -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; -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 { - 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 | 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, - }; - } -} diff --git a/open-sse/services/raycast.ts b/open-sse/services/raycast.ts deleted file mode 100644 index 801f2bf77a..0000000000 --- a/open-sse/services/raycast.ts +++ /dev/null @@ -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 { - 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 { - 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 { - 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; - }); -} diff --git a/open-sse/utils/publicCreds.ts b/open-sse/utils/publicCreds.ts index ad915e4ce2..a6b1dfb0d8 100644 --- a/open-sse/utils/publicCreds.ts +++ b/open-sse/utils/publicCreds.ts @@ -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; diff --git a/scripts/raycast/extract-credentials.mjs b/scripts/raycast/extract-credentials.mjs deleted file mode 100644 index 44d3866239..0000000000 --- a/scripts/raycast/extract-credentials.mjs +++ /dev/null @@ -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)); diff --git a/scripts/raycast/usage-benchmark.mjs b/scripts/raycast/usage-benchmark.mjs deleted file mode 100644 index 85c30232d6..0000000000 --- a/scripts/raycast/usage-benchmark.mjs +++ /dev/null @@ -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); -}); diff --git a/src/app/(dashboard)/dashboard/providers/[id]/components/ProviderModalsPanel.tsx b/src/app/(dashboard)/dashboard/providers/[id]/components/ProviderModalsPanel.tsx index 45aa63ec63..a4d994eac5 100644 --- a/src/app/(dashboard)/dashboard/providers/[id]/components/ProviderModalsPanel.tsx +++ b/src/app/(dashboard)/dashboard/providers/[id]/components/ProviderModalsPanel.tsx @@ -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" ? ( - setShowOAuthModal(false)} - /> ) : ( - 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 }); - } -} diff --git a/src/app/api/oauth/raycast/import/route.ts b/src/app/api/oauth/raycast/import/route.ts deleted file mode 100644 index 266dd1aecc..0000000000 --- a/src/app/api/oauth/raycast/import/route.ts +++ /dev/null @@ -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", - }, - ], - }); -} diff --git a/src/app/api/providers/[id]/models/route.ts b/src/app/api/providers/[id]/models/route.ts index 8ccbb3e311..ee2baeb393 100755 --- a/src/app/api/providers/[id]/models/route.ts +++ b/src/app/api/providers/[id]/models/route.ts @@ -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", { diff --git a/src/app/api/providers/[id]/test/oauthTestConfig.ts b/src/app/api/providers/[id]/test/oauthTestConfig.ts index 754003e44b..354c4adef3 100644 --- a/src/app/api/providers/[id]/test/oauthTestConfig.ts +++ b/src/app/api/providers/[id]/test/oauthTestConfig.ts @@ -205,14 +205,6 @@ export const OAUTH_TEST_CONFIG: Record = { // 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 diff --git a/src/app/api/providers/[id]/test/webSessionTestDispatch.ts b/src/app/api/providers/[id]/test/webSessionTestDispatch.ts index dc2ba36a5a..fbacfe334d 100644 --- a/src/app/api/providers/[id]/test/webSessionTestDispatch.ts +++ b/src/app/api/providers/[id]/test/webSessionTestDispatch.ts @@ -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) ); } diff --git a/src/lib/db/migrations/163_retire_gpl_derived_providers.sql b/src/lib/db/migrations/163_retire_gpl_derived_providers.sql new file mode 100644 index 0000000000..1e9cab4c5e --- /dev/null +++ b/src/lib/db/migrations/163_retire_gpl_derived_providers.sql @@ -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'); diff --git a/src/lib/db/webSessionDedup.ts b/src/lib/db/webSessionDedup.ts index b68ee00122..7983c2686f 100644 --- a/src/lib/db/webSessionDedup.ts +++ b/src/lib/db/webSessionDedup.ts @@ -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 diff --git a/src/lib/oauth/constants/oauth.ts b/src/lib/oauth/constants/oauth.ts index 0318db40b3..0b8a65a75c 100644 --- a/src/lib/oauth/constants/oauth.ts +++ b/src/lib/oauth/constants/oauth.ts @@ -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", diff --git a/src/lib/oauth/providers/index.ts b/src/lib/oauth/providers/index.ts index 596890c6bd..cfd7ced721 100644 --- a/src/lib/oauth/providers/index.ts +++ b/src/lib/oauth/providers/index.ts @@ -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 diff --git a/src/lib/oauth/providers/raycast.ts b/src/lib/oauth/providers/raycast.ts deleted file mode 100644 index 85f15bd35c..0000000000 --- a/src/lib/oauth/providers/raycast.ts +++ /dev/null @@ -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", - }, - }), -}; diff --git a/src/lib/oauth/services/raycast.ts b/src/lib/oauth/services/raycast.ts deleted file mode 100644 index cadd9d82a8..0000000000 --- a/src/lib/oauth/services/raycast.ts +++ /dev/null @@ -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 { - 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.", - ]; - } -} diff --git a/src/lib/oauth/services/raycastLocal.ts b/src/lib/oauth/services/raycastLocal.ts deleted file mode 100644 index 976a023a39..0000000000 --- a/src/lib/oauth/services/raycastLocal.ts +++ /dev/null @@ -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 | null { - try { - const raw = execFileSync( - "security", - ["find-generic-password", "-s", "Raycast", "-a", account, "-w"], - { encoding: "utf-8" } - ).trim(); - return JSON.parse(raw) as Record; - } 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", - }; -} diff --git a/src/server/authz/routeGuard.ts b/src/server/authz/routeGuard.ts index ee5b5232c5..60a8d8e15a 100644 --- a/src/server/authz/routeGuard.ts +++ b/src/server/authz/routeGuard.ts @@ -65,7 +65,6 @@ export const LOCAL_ONLY_API_PREFIXES: ReadonlyArray = [ "/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). diff --git a/src/shared/components/RaycastAuthModal.tsx b/src/shared/components/RaycastAuthModal.tsx deleted file mode 100644 index 7d1461294f..0000000000 --- a/src/shared/components/RaycastAuthModal.tsx +++ /dev/null @@ -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(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 = { - 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 ( - -
-
-

- Auto-import (recommended on macOS): reads your local Raycast login from - Keychain + analytics device ID. No proxy needed. -

- - {!autoAvailable && ( -

- Install sqlcipher: brew install sqlcipher -

- )} -
- -
-

- Local dev only. Uses your Raycast Pro subscription via reverse-engineered - API. Not official — may break on Raycast updates. -

- -
- - {showManual && ( - <> -
- -