Skip to content
Closed
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
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- **feat(oauth):** Muse Code subscription login via device flow — approve in the browser and OmniRoute exchanges the grant for a subscription inference key ([#14267](https://github.com/diegosouzapw/OmniRoute/pull/14267)) — thanks @caniko
66 changes: 66 additions & 0 deletions open-sse/config/providers/registry/muse-code/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,76 @@ import { buildOpenAiCompatibleRegistryEntry } from "../../shared.ts";
export const muse_codeProvider: RegistryEntry = buildOpenAiCompatibleRegistryEntry({
id: "muse-code",
alias: "mc",
// Subscription and API-key connections share this entry: inference is the
// Responses API in both cases, authenticated by the connection bearer
// (subscription key for OAuth, META_API_KEY for API-key connections).
baseUrl: "https://api.meta.ai/v1",
urlSuffix: "/responses",
// Live catalog for entitled-model discovery (subscription Spark ids for
// OAuth connections, Llama ids for API-key connections). The generic
// models route derives its fetch config from here.
modelsUrl: "https://api.meta.ai/v1/models",
passthroughModels: true,
reasoningTransport: "opaque",
defaultContextLength: 200000,
models: [
// Subscription Spark family: served only on the Responses API. Declaring
// the wire format (instead of relying on per-request apiFormat) keeps
// Chat Completions callers translated to Responses. Live discovery
// remains authoritative: ids a key is not entitled to are rejected, so
// these entries never route traffic a credential cannot serve. The model
// set and fallback windows mirror the verified subscription contract
// (five known ids; live limits win, 1048576/131072 fallback).
{
id: "muse-spark-1.1",
name: "Muse Spark 1.1",
contextLength: 1048576,
maxOutputTokens: 131072,
supportsReasoning: true,
supportsXHighEffort: true,
toolCalling: true,
targetFormat: "openai-responses",
},
{
id: "muse-spark-1.2",
name: "Muse Spark 1.2",
contextLength: 1048576,
maxOutputTokens: 131072,
supportsReasoning: true,
supportsXHighEffort: true,
toolCalling: true,
targetFormat: "openai-responses",
},
{
id: "muse-spark-1.2-contributor",
name: "Muse Spark 1.2 Contributor",
contextLength: 1048576,
maxOutputTokens: 131072,
supportsReasoning: true,
supportsXHighEffort: true,
toolCalling: true,
targetFormat: "openai-responses",
},
{
id: "muse-spark-1.3",
name: "Muse Spark 1.3",
contextLength: 1048576,
maxOutputTokens: 131072,
supportsReasoning: true,
supportsXHighEffort: true,
toolCalling: true,
targetFormat: "openai-responses",
},
{
id: "muse-spark-1.3-contributor",
name: "Muse Spark 1.3 Contributor",
contextLength: 1048576,
maxOutputTokens: 131072,
supportsReasoning: true,
supportsXHighEffort: true,
toolCalling: true,
targetFormat: "openai-responses",
},
{
id: "llama-4-maverick",
name: "Llama 4 Maverick",
Expand Down
3 changes: 3 additions & 0 deletions open-sse/utils/publicCreds.ts
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,9 @@ const EMBEDDED_DEFAULTS = {
],
// GitHub Copilot CLI — github oauth app id (public, device flow)
github_copilot_id: [38, 27, 95, 71, 16, 90, 69, 67, 4, 29, 72, 22, 90, 91, 12, 0, 75, 19, 8, 87],
// Muse Code subscription (Meta) — device authorization client id shipped in
// public Muse/CLI bundles (public, device authorization grant).
muse_id: [94, 93, 93, 88, 68, 93, 64, 77, 80, 31, 71, 65, 90, 85, 93, 85],
// Grok Build CLI (xAI) — public oauth client id (import-token flow)
grok_id: [
13, 92, 15, 89, 66, 91, 76, 70, 72, 29, 71, 70, 3, 65, 93, 84, 72, 23, 28, 87, 92, 88, 15, 95,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ export const SUPPORTED_WIZARD_OAUTH_PROVIDER_IDS = new Set([
"cursor",
"kilocode",
"cline",
"muse-code",
]);

function toProviderOption(
Expand Down
11 changes: 7 additions & 4 deletions src/app/(dashboard)/dashboard/providers/providerPageUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,12 @@ export function shouldShowProviderSection(

type ProviderRecord<TProvider = Record<string, unknown>> = Record<string, TProvider>;

const OAUTH_CARD_API_KEY_CONNECTION_PROVIDER_IDS = new Set(["kiro", "amazon-q", "kimi-coding"]);
const OAUTH_CARD_API_KEY_CONNECTION_PROVIDER_IDS = new Set([
"kiro",
"amazon-q",
"kimi-coding",
"muse-code",
]);

export function getProviderConnectionsRequestUrl(providerId: string): string {
const hasAliases = getProviderConnectionFamilyIds(providerId).length > 1;
Expand Down Expand Up @@ -500,9 +505,7 @@ export function filterConfiguredProviderEntries<TProvider>(
return connections.some(
(conn) =>
connectionBelongsToProviderPage(conn.provider, entry.providerId) &&
connectionSearchHaystacks(conn).some((haystack) =>
matchesAnyToken(haystack, searchQuery)
)
connectionSearchHaystacks(conn).some((haystack) => matchesAnyToken(haystack, searchQuery))
);
});
}
Expand Down
7 changes: 7 additions & 0 deletions src/app/api/providers/[id]/test/oauthTestConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,13 @@ export const OAUTH_TEST_CONFIG: Record<string, OAuthTestConfigEntry> = {
checkExpiry: true,
refreshable: true,
},
"muse-code": {
// Muse Code subscription has no refresh-token grant and no stable
// user-info endpoint: the exchanged inference key is validated by
// presence/expiry, and live connectivity is exercised by real
// /v1/responses traffic. Reconnect replaces the stored key.
checkExpiry: true,
},
kilocode: {
// Kilo OAuth does not expose a stable user-info endpoint in all environments.
// Validate using token presence/expiry as a lightweight auth check.
Expand Down
32 changes: 28 additions & 4 deletions src/lib/oauth/connectionPersistence.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,14 +82,31 @@ function isSameClaudeAccount(
return true;
}

/**
* Does this existing Muse Code connection represent the SAME subscription
* account as the incoming login? The key exchange returns `user_id` and only
* sometimes `user_email`, so an email match cannot be required: agreement on
* the persisted `accountId` decides, mirroring the Codex chatgptUserId rule
* (#7737). Without this, every reconnect of a user_id-only account would fork
* a duplicate connection instead of replacing the stored key.
*/
function isSameMuseAccount(
existingProviderData: Record<string, any> | null | undefined,
incomingProviderData: Record<string, any> | null | undefined
): boolean {
const incomingAccountId = incomingProviderData?.accountId;
const existingAccountId = existingProviderData?.accountId;
return Boolean(incomingAccountId) && safeEqual(existingAccountId, incomingAccountId);
}
/**
* Find the existing OAuth connection (if any) that an incoming token payload
* should be merged into, shared by every OAuth-completion call site
* (persistOAuthConnection, and the exchange/poll/poll-callback branches in
* `src/app/api/oauth/[provider]/[action]/route.ts`). Matches by explicit
* connectionId first, then by same email + auth type — with Codex requiring
* workspaceId/chatgptUserId agreement (#7737) and Claude requiring
* organizationUUID agreement, to avoid silently overwriting a different
* workspaceId/chatgptUserId agreement (#7737), Claude requiring
* organizationUUID agreement, and Muse Code requiring accountId agreement,
* to avoid silently overwriting a different
* account that merely shares an email.
*/
export function findExistingOAuthConnectionMatch(
Expand All @@ -100,6 +117,12 @@ export function findExistingOAuthConnectionMatch(
): Record<string, any> | undefined {
return existing.find((c) => {
if (c.id && safeEqual(connectionId, c.id)) return true;
if (provider === "muse-code") {
return (
c.authType === "oauth" &&
isSameMuseAccount(c.providerSpecificData, tokenData.providerSpecificData)
);
}
// Email dedup only when the payload actually carries an email. Without this
// guard `safeEqual(undefined, undefined)` is true, so an email-less payload
// would false-match the first email-less connection of the provider.
Expand Down Expand Up @@ -180,8 +203,9 @@ export async function persistOAuthConnection(
// / re-auth of a known connection); honor it even when the payload has no
// top-level email. Some providers (e.g. GitHub Copilot) keep identity under
// providerSpecificData, so gating dedup on tokenData.email alone created a
// duplicate connection on every refresh (#8059).
if (connectionId || tokenData.email) {
// duplicate connection on every refresh (#8059). Muse Code likewise keys
// identity on providerSpecificData.accountId, so it enters dedup too.
if (connectionId || tokenData.email || provider === "muse-code") {
const existing = await getProviderConnections({ provider });
const match = findExistingOAuthConnectionMatch(existing, provider, tokenData, connectionId);
const matchId = typeof match?.id === "string" ? match.id : null;
Expand Down
14 changes: 14 additions & 0 deletions src/lib/oauth/constants/oauth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,19 @@ export const KIMI_CODING_CONFIG = {
tokenUrl: "https://auth.kimi.com/api/oauth/token",
};

// Muse Code Subscription OAuth Configuration (Device Code Flow + key exchange)
// Device authorization follows the standard device grant; the granted device
// token is then exchanged for a subscription inference key at the key
// endpoint (onboard: true). There is no refresh-token grant: a revoked or
// replaced key requires reconnecting. Inference authenticates with the
// exchanged subscription key, never the device token.
export const MUSE_CODE_CONFIG = {
clientId: resolvePublicCred("muse_id", "MUSE_CODE_OAUTH_CLIENT_ID"),
deviceCodeUrl: "https://auth.meta.com/oidc/device/authorization/",
tokenUrl: "https://auth.meta.com/oidc/device/token/",
keyUrl: "https://api.meta.ai/muse-code/key",
};

// KiloCode OAuth Configuration (Custom Device Auth Flow)
export const KILOCODE_CONFIG = {
apiBaseUrl: "https://api.kilo.ai",
Expand Down Expand Up @@ -497,6 +510,7 @@ export const PROVIDERS = {
ANTIGRAVITY: "antigravity",
AGY: "agy",
KIMI_CODING: "kimi-coding",
MUSE_CODE: "muse-code",
OPENAI: "openai",
GITHUB: "github",
GHE_COPILOT: "ghe-copilot",
Expand Down
2 changes: 2 additions & 0 deletions src/lib/oauth/providers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { antigravity } from "./antigravity";
import { agy } from "./agy";
import { qoder } from "./qoder";
import { kimiCoding } from "./kimi-coding";
import { museCode } from "./muse-code";
import { github } from "./github";
import { gheCopilot } from "./ghe-copilot";
import { gitlabDuo } from "./gitlab-duo";
Expand All @@ -39,6 +40,7 @@ export const PROVIDERS = {
agy,
qoder,
"kimi-coding": kimiCoding,
"muse-code": museCode,
github,
"ghe-copilot": gheCopilot,
"gitlab-duo": gitlabDuo,
Expand Down
Loading
Loading