Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -1326,6 +1326,11 @@ CODEX_OAUTH_CLIENT_ID=app_EMoamEEZ73f0CkXaXp7hrann
# ── Kimi Coding (Moonshot) ──
KIMI_CODING_OAUTH_CLIENT_ID=17e5f671-d194-4dfb-9706-5516cb48c098

# ── Muse Code (Meta) ──
# Public device-flow client id is baked into open-sse/utils/publicCreds.ts (muse_id).
# Set this only to override the Muse CLI client. Do not put the public id here.
# MUSE_CODE_OAUTH_CLIENT_ID=

# ── GitHub Copilot ──
GITHUB_OAUTH_CLIENT_ID=Iv1.b507a08c87ecfe98

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- **feat(oauth):** Muse Code (Meta) device-authorization login with CLIProxyAPI-parity credential lifecycle — RFC 8628 against auth.meta.com, mint the subscription inference key at `/muse-code/key`, persist the durable `dca:` token, remint on 401 / DCA-only records, honor `error.resets_at` quota cooldowns, and import CLIProxyAPI `type: meta` auth files. Dual-auth keeps pasted `META_API_KEY` on the same card. (#14329)
1 change: 1 addition & 0 deletions docs/reference/ENVIRONMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -608,6 +608,7 @@ Built-in credentials for **localhost development**. For remote deployments, regi
| `GEMINI_OAUTH_CLIENT_ID` | Gemini (Google) | Requires matching `_SECRET`. |
| `GEMINI_OAUTH_CLIENT_SECRET` | Gemini (Google) | — |
| `KIMI_CODING_OAUTH_CLIENT_ID` | Kimi Coding (Moonshot) | Public client. |
| `MUSE_CODE_OAUTH_CLIENT_ID` | Muse Code (Meta) | Optional override for the public Muse CLI device-flow client id. Leave unset to use the baked-in public client. |
| `ANTIGRAVITY_OAUTH_CLIENT_ID` | Antigravity (Google) | Requires matching `_SECRET`. |
| `ANTIGRAVITY_OAUTH_CLIENT_SECRET` | Antigravity (Google) | — |
| `GITHUB_OAUTH_CLIENT_ID` | GitHub Copilot | Public client. |
Expand Down
12 changes: 6 additions & 6 deletions docs/reference/PROVIDER_REFERENCE.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
---
title: "Provider Reference"
version: 3.8.51
lastUpdated: 2026-09-20
lastUpdated: 2026-09-22
---

# 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-09-20
> **Last generated:** 2026-09-22
Total providers: **360**. See category breakdown below.

Expand Down Expand Up @@ -49,7 +49,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 (24)
## OAuth Providers (25)

| ID | Alias | Name | Tags | Website | Notes |
|----|-------|------|------|---------|-------|
Expand All @@ -71,6 +71,7 @@ Use the dashboard at `/dashboard/providers` to enable, configure, and test each
| `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. |
| `muse-code` | `mc` | Muse Code (Meta) | OAuth | [link](https://ai.developer.meta.com/docs/muse-code/auth) | Sign in with the Muse Code device flow (same as `muse login` / CLIProxyAPI `-meta-login`) to use a Muse subscription, or paste a META_API_KEY. Device login keeps the durable dca token and mints the inference key; a 401 remints that key. Wire format is OpenAI Responses (POST /responses). |
| `openference` | `of` | Openference | OAuth | [link](https://openference.com) | Sign in with your Openference account to route requests through api.openference.com. Includes free-tier access to Qwen3.8 27b and Llama 3.2 3B — see openference.com/pricing for current plan terms. |
| `qoder` | `if` | Qoder | OAuth | — | — |
| `trae` | `tr` | Trae | OAuth | [link](https://trae.ai) | Trae is an AI-native IDE by ByteDance (SOLO remote agent). Authorize via trae.ai in the popup, or sign in at solo.trae.ai and paste the Cloud-IDE-JWT (sent as 'Authorization: Cloud-IDE-JWT <token>', ~14-day lifetime) as the access token; web_id/biz_user_id/user_unique_id/scope/tenant/region propagate via providerSpecificData. No headless refresh for pasted tokens — re-paste on expiry. |
Expand Down Expand Up @@ -117,7 +118,7 @@ Use the dashboard at `/dashboard/providers` to enable, configure, and test each
| `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) (243)
## API Key Providers (paid / paid-with-free-credits) (242)

| ID | Alias | Name | Tags | Website | Notes |
|----|-------|------|------|---------|-------|
Expand Down Expand Up @@ -259,7 +260,6 @@ Use the dashboard at `/dashboard/providers` to enable, configure, and test each
| `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) | — |
Expand Down Expand Up @@ -448,7 +448,7 @@ Use the dashboard at `/dashboard/providers` to enable, configure, and test each

- Catalog: [`src/shared/constants/providers.ts`](../../src/shared/constants/providers.ts)
- Registry (per-model details): [`open-sse/config/providerRegistry.ts`](../../open-sse/config/providerRegistry.ts)
- Executors: [`open-sse/executors/`](../../open-sse/executors/) (121 implementations)
- Executors: [`open-sse/executors/`](../../open-sse/executors/) (124 implementations)
- Translators: [`open-sse/translator/`](../../open-sse/translator/)

## See Also
Expand Down
40 changes: 40 additions & 0 deletions open-sse/config/museCode.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/**
* Muse Code (Meta) public endpoints and CLI identity.
*
* Parity with CLIProxyAPI's Meta provider (`internal/auth/meta`,
* `internal/runtime/executor/meta_executor*`): RFC 8628 device grant against
* auth.meta.com, then mint of the subscription inference key at
* api.meta.ai/muse-code/key. The Muse CLI User-Agent is required by Meta.
*/

export const MUSE_CODE_USER_AGENT = "muse-code/1.0.2";
/** Inference UA used by CLIProxyAPI's MetaExecutor (auth still uses muse-code/1.0.2). */
export const MUSE_CODE_INFERENCE_USER_AGENT =
"muse-build/1.3.0 (interactive; macos-aarch64; build ac7280f2aca67769d1455a8847bb502b617d50f6)";
export const MUSE_CODE_AUTH_HOST = "https://auth.meta.com";
export const MUSE_CODE_DEVICE_CODE_URL = `${MUSE_CODE_AUTH_HOST}/oidc/device/authorization/`;
export const MUSE_CODE_TOKEN_URL = `${MUSE_CODE_AUTH_HOST}/oidc/device/token/`;
export const MUSE_CODE_MINT_URL = "https://api.meta.ai/muse-code/key";
export const MUSE_CODE_API_BASE_URL = "https://api.meta.ai/v1";
export const MUSE_CODE_RESPONSES_URL = `${MUSE_CODE_API_BASE_URL}/responses`;
export const MUSE_CODE_DEVICE_GRANT = "urn:ietf:params:oauth:grant-type:device_code";
export const MUSE_CODE_DEFAULT_POLL_INTERVAL_SEC = 5;
export const MUSE_CODE_MAX_POLL_MS = 15 * 60 * 1000;

export function museCodeHeaders(extra?: Record<string, string>): Record<string, string> {
return {
Accept: "application/json",
"User-Agent": MUSE_CODE_USER_AGENT,
...extra,
};
}

export function isMuseDcaToken(token: string | null | undefined): boolean {
return typeof token === "string" && token.trim().startsWith("dca:");
}

export function normalizeMuseBaseUrl(raw: string | null | undefined): string {
const trimmed = typeof raw === "string" ? raw.trim() : "";
if (!trimmed) return MUSE_CODE_API_BASE_URL;
return trimmed.replace(/\/+$/, "");
}
43 changes: 42 additions & 1 deletion open-sse/config/providers/registry/muse-code/index.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,63 @@
import {
MUSE_CODE_INFERENCE_USER_AGENT,
MUSE_CODE_RESPONSES_URL,
} from "../../../museCode.ts";
import type { RegistryEntry } from "../../shared.ts";
import { buildOpenAiCompatibleRegistryEntry } from "../../shared.ts";

const MUSE_SPARK_THINKING = ["minimal", "low", "medium", "high", "xhigh", "max"] as const;

function museSparkModel(
id: string,
name: string,
options: { vision?: boolean; thinking?: readonly string[] } = {}
) {
return {
id,
name,
contextLength: 1048576,
maxOutputTokens: 65536,
supportsReasoning: true,
supportsXHighEffort: true,
supportedThinkingEfforts: options.thinking ?? MUSE_SPARK_THINKING,
toolCalling: true,
supportsVision: options.vision !== false,
targetFormat: "openai-responses" as const,
unsupportedParams: ["logprobs", "topLogprobs", "logitBias"],
};
}

/**
* Muse Code CLI — Meta's agentic coding tool.
*
* Wire format: OpenAI Responses API (POST /responses).
* Auth: Bearer token from META_API_KEY env var.
* Auth: dual — META_API_KEY / pasted Bearer key, or RFC 8628 device OAuth
* (CLIProxyAPI-parity: persist dca token, mint inference key, remint on 401).
* Reasoning efforts: xhigh/ultra -> high (handled generically).
*
* @see https://github.com/joymadhu49/muse-openrouter-shim
*/
export const muse_codeProvider: RegistryEntry = buildOpenAiCompatibleRegistryEntry({
id: "muse-code",
alias: "mc",
baseUrl: MUSE_CODE_RESPONSES_URL,
extraHeaders: { "User-Agent": MUSE_CODE_INFERENCE_USER_AGENT },
passthroughModels: true,
reasoningTransport: "opaque",
defaultContextLength: 200000,
models: [
museSparkModel("muse-spark-1.3", "Muse Spark 1.3"),
museSparkModel("muse-spark-1.3-contributor", "Muse Spark 1.3 Contributor"),
museSparkModel("muse-spark-1.2", "Muse Spark 1.2", {
thinking: ["minimal", "low", "medium", "high", "xhigh"],
}),
museSparkModel("muse-spark-1.2-contributor", "Muse Spark 1.2 Contributor", {
thinking: ["minimal", "low", "medium", "high", "xhigh"],
}),
museSparkModel("muse-spark-1.1", "Muse Spark 1.1", {
vision: false,
thinking: ["low", "medium", "high", "xhigh"],
}),
{
id: "llama-4-maverick",
name: "Llama 4 Maverick",
Expand Down
1 change: 1 addition & 0 deletions open-sse/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ export {
refreshQoderToken,
refreshGitHubToken,
refreshCopilotToken,
refreshMuseCodeToken,
getAccessToken,
refreshTokenByProvider,
} from "./services/tokenRefresh.ts";
Expand Down
80 changes: 80 additions & 0 deletions open-sse/services/museCodeAuth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import {
MUSE_CODE_MINT_URL,
museCodeHeaders,
normalizeMuseBaseUrl,
} from "../config/museCode.ts";

export type MuseMintedKey = {
apiKey: string;
baseUrl: string;
email?: string;
name?: string;
subsTierName?: string;
subsTierId?: string;
isSubsActive?: boolean;
hasPaymentMethod?: boolean;
requirePayment?: boolean;
canSubscribe?: boolean;
};

/**
* Exchange a Meta device-client access token (`dca:…`) for the subscription
* inference key. Matches CLIProxyAPI MintAPIKey.
*/
export async function mintMuseApiKey(
dcaToken: string,
mintUrl = MUSE_CODE_MINT_URL
): Promise<MuseMintedKey> {
const token = dcaToken.trim();
if (!token) {
throw new Error("Muse Code mint requires a device access token.");
}
const response = await fetch(mintUrl, {
method: "POST",
headers: museCodeHeaders({
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
}),
body: JSON.stringify({ dca_token: token }),
});
const text = await response.text();
if (!response.ok) {
throw new Error(`Muse Code key mint failed (HTTP ${response.status}).`);
}
let data: Record<string, unknown>;
try {
data = text ? (JSON.parse(text) as Record<string, unknown>) : {};
} catch {
throw new Error("Muse Code key mint response was not JSON.");
}
const apiKey =
(typeof data.api_key === "string" && data.api_key.trim()) ||
(typeof data.apiKey === "string" && data.apiKey.trim()) ||
"";
if (!apiKey) {
throw new Error("Muse Code key mint response missing api_key.");
}
return {
apiKey,
baseUrl: normalizeMuseBaseUrl(
(typeof data.base_url === "string" && data.base_url) ||
(typeof data.baseUrl === "string" && data.baseUrl) ||
""
),
email:
(typeof data.user_email === "string" && data.user_email.trim()) ||
(typeof data.email === "string" && data.email.trim()) ||
undefined,
name:
(typeof data.user_full_name === "string" && data.user_full_name.trim()) ||
(typeof data.name === "string" && data.name.trim()) ||
undefined,
subsTierName: typeof data.subs_tier_name === "string" ? data.subs_tier_name : undefined,
subsTierId: typeof data.subs_tier_id === "string" ? data.subs_tier_id : undefined,
isSubsActive: typeof data.is_subs_active === "boolean" ? data.is_subs_active : undefined,
hasPaymentMethod:
typeof data.has_payment_method === "boolean" ? data.has_payment_method : undefined,
requirePayment: typeof data.require_payment === "boolean" ? data.require_payment : undefined,
canSubscribe: typeof data.can_subscribe === "boolean" ? data.can_subscribe : undefined,
};
}
22 changes: 22 additions & 0 deletions open-sse/services/retryAfterJson.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,22 @@ function futureTimestampMs(value: unknown, maxMs: number): number | null {
return waitMs > 0 ? Math.min(waitMs, maxMs) : null;
}

/**
* Meta Muse / CLIProxyAPI subscription quota uses `error.resets_at` as a unix
* epoch (seconds). Accept seconds, milliseconds, numeric strings, and ISO.
*/
function futureResetAtMs(value: unknown, maxMs: number): number | null {
if (typeof value === "number" && Number.isFinite(value) && value > 0) {
const ms = value < 1e12 ? value * 1000 : value;
const waitMs = ms - Date.now();
return waitMs > 0 ? Math.min(waitMs, maxMs) : null;
}
if (typeof value === "string" && /^\d+(?:\.\d+)?$/.test(value.trim())) {
return futureResetAtMs(Number(value), maxMs);
}
return futureTimestampMs(value, maxMs);
}

// RetryInfo.retryDelay / "please retry in Ns" are short per-request throttling
// hints (Gemini free-tier RPM/TPM), not long-lived quota resets like Antigravity's
// "Resets in 160h" — cap them independently of the caller's maxMs so a malformed or
Expand Down Expand Up @@ -90,6 +106,12 @@ export function parseDetailedRetryHintFromJsonBody(
return { retryAfterMs: retryInfoMs, provenance: "google_rpc_retry_info" };
}

const resetAtHint = futureResetAtMs(
errorObj.resets_at ?? root.resets_at ?? errorObj.resetsAt ?? root.resetsAt,
maxMs
);
if (resetAtHint !== null) return { retryAfterMs: resetAtHint, provenance: "body" };

const isoHint = futureTimestampMs(errorObj.retryAfter ?? root.retryAfter, maxMs);
if (isoHint !== null) return { retryAfterMs: isoHint, provenance: "body" };

Expand Down
11 changes: 11 additions & 0 deletions open-sse/services/tokenRefresh.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import {
import { refreshCodebuddyCnToken } from "./tokenRefresh/providers/codebuddyCn.ts";
import { refreshClineToken } from "./tokenRefresh/providers/cline.ts";
import { refreshKimiCodingToken } from "./tokenRefresh/providers/kimiCoding.ts";
import { refreshMuseCodeToken } from "./tokenRefresh/providers/museCode.ts";
import { refreshGitLabDuoToken } from "./tokenRefresh/providers/gitlabDuo.ts";
import { refreshClaudeOAuthToken } from "./tokenRefresh/providers/claudeOAuth.ts";
import { refreshGoogleToken } from "./tokenRefresh/providers/google.ts";
Expand All @@ -63,6 +64,7 @@ export {
refreshCodebuddyCnToken,
refreshClineToken,
refreshKimiCodingToken,
refreshMuseCodeToken,
refreshGitLabDuoToken,
refreshClaudeOAuthToken,
refreshGoogleToken,
Expand Down Expand Up @@ -436,6 +438,14 @@ async function _getAccessTokenInternal(provider, credentials, log, proxyConfig:
proxyConfig
);

case "muse-code":
return await refreshMuseCodeToken(
credentials.refreshToken,
credentials.providerSpecificData,
log,
proxyConfig
);

case "gitlab-duo":
return await refreshGitLabDuoToken(
credentials.refreshToken,
Expand Down Expand Up @@ -470,6 +480,7 @@ export function supportsTokenRefresh(provider) {
"amazon-q",
"cline",
"kimi-coding",
"muse-code",
// Devin auth is not refreshable here: devin-desktop accepts an imported API
// key (#8228), while devin-cli is local-CLI owned via `devin auth login`
// (#8407). Neither connection carries a refresh token, so listing either
Expand Down
Loading
Loading