@mupt-ai/dari-gateway 0.2.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/README.md +8 -4
  2. package/dist/context/pricing.js +5 -1
  3. package/dist/http/handler.js +7 -0
  4. package/dist/index.d.ts +2 -1
  5. package/dist/index.js +1 -0
  6. package/dist/ingress/chat_completions.js +8 -1
  7. package/dist/ingress/codex.js +4 -0
  8. package/dist/ingress/models.js +13 -1
  9. package/dist/ingress/muse_code.d.ts +2 -0
  10. package/dist/ingress/muse_code.js +8 -0
  11. package/dist/ingress/muse_models.d.ts +9 -0
  12. package/dist/ingress/muse_models.js +32 -0
  13. package/dist/interaction/types.d.ts +1 -0
  14. package/dist/models/catalog.d.ts +35 -0
  15. package/dist/models/catalog.js +232 -0
  16. package/dist/models/execution_provider.d.ts +2 -1
  17. package/dist/models/execution_provider.js +5 -1
  18. package/dist/models/provider_defaults.js +3 -0
  19. package/dist/models/runtime_overrides.d.ts +3 -0
  20. package/dist/models/runtime_overrides.js +17 -0
  21. package/dist/models/subscriptions.d.ts +8 -0
  22. package/dist/models/subscriptions.js +37 -0
  23. package/dist/platform/platform.d.ts +5 -1
  24. package/dist/protocol/input/chat_completions.js +3 -1
  25. package/dist/protocol/input/chat_completions_validation.js +2 -1
  26. package/dist/providers/pi.js +12 -6
  27. package/dist/providers/pi_options.js +13 -1
  28. package/dist/providers/provider_call.js +7 -3
  29. package/dist/providers/subscription_limits.d.ts +4 -2
  30. package/dist/providers/subscription_limits.js +19 -3
  31. package/dist/runtime/explicit_model.d.ts +3 -0
  32. package/dist/runtime/explicit_model.js +10 -6
  33. package/dist/runtime/prepare_routed_completion.js +24 -5
  34. package/dist/runtime/strict_provider_access.d.ts +6 -0
  35. package/dist/runtime/strict_provider_access.js +40 -0
  36. package/dist/self_host/resolve.js +11 -15
  37. package/dist/self_host/subscriptions/oauth.d.ts +10 -6
  38. package/dist/self_host/subscriptions/oauth.js +101 -6
  39. package/dist/self_host/subscriptions/store.js +1 -0
  40. package/dist/types.d.ts +4 -1
  41. package/package.json +2 -2
  42. package/src/context/pricing.ts +5 -2
  43. package/src/http/handler.ts +7 -0
  44. package/src/index.ts +9 -1
  45. package/src/ingress/chat_completions.ts +8 -1
  46. package/src/ingress/codex.ts +4 -0
  47. package/src/ingress/models.ts +13 -1
  48. package/src/ingress/muse_code.ts +9 -0
  49. package/src/ingress/muse_models.ts +36 -0
  50. package/src/interaction/types.ts +2 -0
  51. package/src/models/catalog.ts +310 -0
  52. package/src/models/execution_provider.ts +4 -1
  53. package/src/models/provider_defaults.ts +3 -0
  54. package/src/models/runtime_overrides.ts +22 -0
  55. package/src/models/subscriptions.ts +54 -0
  56. package/src/platform/platform.ts +5 -1
  57. package/src/protocol/input/chat_completions.ts +3 -1
  58. package/src/protocol/input/chat_completions_validation.ts +2 -1
  59. package/src/providers/pi.ts +21 -6
  60. package/src/providers/pi_options.ts +17 -1
  61. package/src/providers/provider_call.ts +7 -3
  62. package/src/providers/subscription_limits.ts +21 -4
  63. package/src/runtime/explicit_model.ts +15 -16
  64. package/src/runtime/prepare_routed_completion.ts +22 -3
  65. package/src/runtime/strict_provider_access.ts +42 -0
  66. package/src/self_host/resolve.ts +12 -26
  67. package/src/self_host/subscriptions/oauth.ts +139 -8
  68. package/src/self_host/subscriptions/store.ts +1 -0
  69. package/src/types.ts +20 -3
@@ -14,14 +14,7 @@ export function explicitModelDecision(
14
14
  ): RoutedDecision | null {
15
15
  const modelId = explicitModelId(prepared.interaction.model);
16
16
  if (modelId === null) return null;
17
- if (!prepared.credentials.enabled_models.includes(modelId)) {
18
- throw new HttpError(
19
- 400,
20
- `Model '${modelId}' is not enabled on this router.`,
21
- "model_not_enabled",
22
- "model",
23
- );
24
- }
17
+ if (!prepared.credentials.enabled_models.includes(modelId)) throw modelNotEnabled(modelId);
25
18
  const candidates = prepared.routing.candidateResolution.candidates.filter(
26
19
  (candidate) => candidate.model === modelId,
27
20
  );
@@ -63,14 +56,7 @@ function pickCandidate(
63
56
  refuses everything else instead of substituting. */
64
57
  export function strictModelDecision(prepared: PreparedRoutedCompletion): RoutedDecision {
65
58
  const modelId = explicitModelId(prepared.interaction.model);
66
- if (modelId === null) {
67
- throw new HttpError(
68
- 400,
69
- "This router is strict: request a pinned model as 'dari/model/<id>'.",
70
- "strict_model_required",
71
- "model",
72
- );
73
- }
59
+ if (modelId === null) throw strictModelRequired();
74
60
  const { decision } = explicitModelDecision(prepared)!;
75
61
  const requested = prepared.interaction.reasoning?.effort
76
62
  ?? prepared.credentials.model_configs?.[modelId]?.default_thinking_level;
@@ -84,3 +70,16 @@ export function strictModelDecision(prepared: PreparedRoutedCompletion): RoutedD
84
70
  }
85
71
  return { decision };
86
72
  }
73
+
74
+ export function modelNotEnabled(modelId: string): HttpError {
75
+ return new HttpError(400, `Model '${modelId}' is not enabled on this router.`, "model_not_enabled", "model");
76
+ }
77
+
78
+ export function strictModelRequired(): HttpError {
79
+ return new HttpError(
80
+ 400,
81
+ "This router is strict: request a pinned model as 'dari/model/<id>'.",
82
+ "strict_model_required",
83
+ "model",
84
+ );
85
+ }
@@ -9,12 +9,14 @@ import {
9
9
  } from "../context/routing_context.js";
10
10
  import { isBackgroundRequest, type InteractionRequest } from "../interaction/types.js";
11
11
  import { executionProviderForModel } from "../models/execution_provider.js";
12
+ import { codexModelConfig, subscriptionServes } from "../models/subscriptions.js";
12
13
  import { gatewayPlatform, type RequestTags } from "../platform/platform.js";
13
14
  import { resolveRouter } from "../strategies/routing.js";
14
15
  import { RequestReporter, type ReportedCandidatePruning } from "../telemetry/report.js";
15
16
  import { RoutingBillingFinalization } from "../telemetry/billing_finalization.js";
16
17
  import type { ClientHarness, ProviderCredential, ResolvedRouter } from "../types.js";
17
18
  import { requestTags } from "./request_tags.js";
19
+ import { strictProviderAccess } from "./strict_provider_access.js";
18
20
  import type { RoutedRuntimeReporter } from "./runtime_reporter.js";
19
21
  import type { RoutedProtocol } from "./routed_protocol.js";
20
22
  import {
@@ -125,7 +127,10 @@ export async function prepareRoutedCompletion(args: {
125
127
  }
126
128
  credentials = conversationCredentials;
127
129
  }
128
- credentials = await withProviderCredentials(credentials, args.apiKey);
130
+ const access = strictProviderAccess(credentials, args.interaction);
131
+ credentials = access.credentials;
132
+ args = { ...args, interaction: access.interaction };
133
+ credentials = await withProviderCredentials(credentials, args.apiKey, args.clientHarness);
129
134
  const billable = true;
130
135
  const routing = await buildRoutingContext({
131
136
  routerId: prefixFingerprintRouterId,
@@ -188,6 +193,7 @@ export async function prepareRoutedCompletion(args: {
188
193
  async function withProviderCredentials(
189
194
  credentials: ResolvedRouter,
190
195
  apiKey: string,
196
+ clientHarness: ClientHarness | undefined,
191
197
  ): Promise<ResolvedRouter> {
192
198
  const platform = gatewayPlatform();
193
199
  if (platform.resolveProviderCredential === undefined) return credentials;
@@ -200,14 +206,27 @@ async function withProviderCredentials(
200
206
  routerId: credentials.router_id,
201
207
  organizationId: credentials.organization_id,
202
208
  apiKey,
209
+ ...(clientHarness ? { clientHarness } : {}),
203
210
  }),
204
211
  ] as const),
205
212
  );
206
213
  const provider_credentials: Record<string, ProviderCredential> = {};
214
+ const model_configs = { ...credentials.model_configs };
207
215
  for (const [model, credential] of resolved) {
208
- if (credential?.apiKey.trim()) provider_credentials[model] = credential;
216
+ if (!credential?.apiKey.trim()) continue;
217
+ const plan = credential.subscription;
218
+ if (plan !== undefined && !subscriptionServes(plan, model, clientHarness)) continue;
219
+ provider_credentials[model] = credential;
220
+ if (plan === "openai_codex") {
221
+ // Keep the catalog's limits and thinking levels; only the serving
222
+ // endpoint changes.
223
+ model_configs[model] = {
224
+ ...model_configs[model],
225
+ ...codexModelConfig(model, credentials.model_prices?.[model] ?? null),
226
+ };
227
+ }
209
228
  }
210
- return { ...credentials, provider_credentials };
229
+ return { ...credentials, provider_credentials, model_configs };
211
230
  }
212
231
 
213
232
  // Core's cache-aware pruning audit, extended with the context layer's image
@@ -0,0 +1,42 @@
1
+ import { getBuiltinModels, getBuiltinProviders } from "@mupt-ai/pi-ai/providers/all";
2
+ import { HttpError } from "../http/errors.js";
3
+ import type { InteractionRequest } from "../interaction/types.js";
4
+ import { explicitModelId } from "../protocol/input/router_model.js";
5
+ import type { ResolvedRouter } from "../types.js";
6
+ import { modelNotEnabled, strictModelRequired } from "./explicit_model.js";
7
+
8
+ // Resolve only the requested catalog model, never turn a provider grant into
9
+ // selector candidates. Unknown IDs fail closed; a grant-only router has no
10
+ // candidates to route among, so it refuses them here with the strict errors.
11
+ export function strictProviderAccess(
12
+ credentials: ResolvedRouter,
13
+ interaction: InteractionRequest,
14
+ ): { credentials: ResolvedRouter; interaction: InteractionRequest } {
15
+ const providers = credentials.strict_providers;
16
+ if (credentials.strict !== true || !providers?.length) return { credentials, interaction };
17
+ const requested = explicitModelId(interaction.model);
18
+ const grantOnly = credentials.enabled_models.length === 0;
19
+ if (requested === null) {
20
+ if (grantOnly) throw strictModelRequired();
21
+ return { credentials, interaction };
22
+ }
23
+ const matches = new Set<string>();
24
+ for (const provider of providers) {
25
+ if (!(getBuiltinProviders() as readonly string[]).includes(provider)) {
26
+ throw new HttpError(400, `Unknown strict provider '${provider}'.`, "unknown_provider");
27
+ }
28
+ for (const model of getBuiltinModels(provider as never)) {
29
+ const id = `${provider}/${model.id}`;
30
+ if (requested === id) matches.add(id);
31
+ }
32
+ }
33
+ const model = [...matches][0];
34
+ if (model === undefined) {
35
+ if (grantOnly) throw modelNotEnabled(requested);
36
+ return { credentials, interaction };
37
+ }
38
+ return {
39
+ credentials: { ...credentials, enabled_models: [...new Set([...credentials.enabled_models, model])] },
40
+ interaction: { ...interaction, model: `dari/model/${model}` },
41
+ };
42
+ }
@@ -4,17 +4,13 @@ import {
4
4
  nativeModelId,
5
5
  providerForModel,
6
6
  type ReasoningEffort,
7
- type RouterModelPrice,
8
7
  type RouterPrefixHit,
9
8
  } from "@mupt-ai/dari-router/policy-engine";
10
9
  import { HttpError } from "../http/errors.js";
11
10
  import type { ResolveRouterRequest } from "../platform/platform.js";
11
+ import { codexModelConfig } from "../models/subscriptions.js";
12
12
  import { registeredModelPrice } from "../providers/pi.js";
13
- import type {
14
- ProviderKeySource,
15
- ResolvedRouter,
16
- ResolvedRouterModelConfig,
17
- } from "../types.js";
13
+ import type { ProviderKeySource, ResolvedRouter } from "../types.js";
18
14
  import type { GatewayConfig, RouterConfig } from "./config.js";
19
15
  import {
20
16
  freshAccessToken,
@@ -29,8 +25,6 @@ import {
29
25
  export const SELF_HOSTED_ORGANIZATION_ID = "self-hosted";
30
26
  export const SELF_HOSTED_USER_ID = "self-hosted";
31
27
 
32
- const CODEX_BACKEND_URL = "https://chatgpt.com/backend-api";
33
-
34
28
  export type SelfHostedResolverDeps = {
35
29
  config: GatewayConfig;
36
30
  gatewayKeys: readonly string[];
@@ -176,8 +170,8 @@ export function gatewayKeyIndex(keys: readonly string[], presented: string): num
176
170
  return null;
177
171
  }
178
172
 
179
- // Claude Code plans serve only Claude Code itself; ChatGPT plans serve any
180
- // client through the Codex backend.
173
+ // Claude Code plans serve only Claude Code itself and Muse Code plans only
174
+ // Muse Code; ChatGPT plans serve any client through the Codex backend.
181
175
  function subscriptionProviderFor(
182
176
  provider: string,
183
177
  clientHarness: ResolveRouterRequest["clientHarness"],
@@ -185,6 +179,7 @@ function subscriptionProviderFor(
185
179
  if (provider === "anthropic") {
186
180
  return clientHarness === "claude_code" ? "anthropic_claude_code" : null;
187
181
  }
182
+ if (provider === "meta") return clientHarness === "muse_code" ? "meta_muse" : null;
188
183
  if (provider === "openai") return "openai_codex";
189
184
  return null;
190
185
  }
@@ -197,34 +192,25 @@ function servingProviders(provider: string, credential: ProviderCredential): str
197
192
  : [provider];
198
193
  }
199
194
 
200
- function codexModelConfig(
201
- modelId: string,
202
- price: RouterModelPrice | null,
203
- ): ResolvedRouterModelConfig {
204
- return {
205
- provider: "openai-codex",
206
- native_model_id: nativeModelId(modelId),
207
- api: "openai-codex-responses",
208
- base_url: CODEX_BACKEND_URL,
209
- // A plan bills no tokens; the catalog price only feeds cost estimates.
210
- cost: price ?? { input: 0, output: 0 },
211
- fast_mode: false,
212
- };
213
- }
214
-
215
195
  function thinkingLevels(router: RouterConfig): Record<string, ReasoningEffort[]> {
216
196
  return Object.fromEntries(
217
197
  router.models.flatMap((model) => model.thinking ? [[model.id, model.thinking]] : []),
218
198
  );
219
199
  }
220
200
 
201
+ const SUBSCRIPTION_PLAN_NAMES: Record<SubscriptionProvider, string> = {
202
+ anthropic_claude_code: "Claude Code",
203
+ openai_codex: "ChatGPT",
204
+ meta_muse: "Muse Code",
205
+ };
206
+
221
207
  function unavailableRouterError(
222
208
  routerId: string,
223
209
  exhausted: ReadonlySet<SubscriptionProvider>,
224
210
  ): HttpError {
225
211
  if (exhausted.size > 0) {
226
212
  const plans = [...exhausted]
227
- .map((provider) => provider === "anthropic_claude_code" ? "Claude Code" : "ChatGPT")
213
+ .map((provider) => SUBSCRIPTION_PLAN_NAMES[provider])
228
214
  .join(" and ");
229
215
  return new HttpError(
230
216
  424,
@@ -2,16 +2,18 @@ import { createHash, randomBytes } from "node:crypto";
2
2
 
3
3
  /**
4
4
  * OAuth protocols for personal subscriptions: Claude Code (PKCE, the user
5
- * pastes back the `code#state` Anthropic shows) and ChatGPT/Codex (device
6
- * code, the gateway polls for approval). Every network call goes through the
7
- * injected `fetch`.
5
+ * pastes back the `code#state` Anthropic shows), ChatGPT/Codex (device code,
6
+ * the gateway polls for approval), and Muse Code (Meta's device code, then the
7
+ * subscription's Model API key minted from the sign-in token). Every network
8
+ * call goes through the injected `fetch`.
8
9
  */
9
10
 
10
- export type SubscriptionProvider = "anthropic_claude_code" | "openai_codex";
11
+ export type SubscriptionProvider = "anthropic_claude_code" | "openai_codex" | "meta_muse";
11
12
 
12
13
  export const SUBSCRIPTION_PROVIDERS: readonly SubscriptionProvider[] = [
13
14
  "anthropic_claude_code",
14
15
  "openai_codex",
16
+ "meta_muse",
15
17
  ];
16
18
 
17
19
  export type OAuthFetch = (url: string, init: RequestInit) => Promise<Response>;
@@ -20,7 +22,8 @@ export type OAuthOptions = { fetch: OAuthFetch; now?: Date };
20
22
 
21
23
  /** The provider refused or could not be reached (HTTP 502 at the edge). */
22
24
  export class SubscriptionOAuthProviderError extends Error {
23
- constructor(message: string, readonly status?: number) {
25
+ // `errorCode` is the provider's OAuth `error` code, such as `authorization_pending`.
26
+ constructor(message: string, readonly status?: number, readonly errorCode?: string) {
24
27
  super(message);
25
28
  this.name = "SubscriptionOAuthProviderError";
26
29
  }
@@ -43,6 +46,8 @@ export type OAuthAuthorization = {
43
46
  flow: Record<string, string>;
44
47
  };
45
48
 
49
+ // A Muse Code plan's access token is the Model API key minted for it, and its
50
+ // refresh token the Meta sign-in token the key is minted from.
46
51
  export type OAuthTokens = {
47
52
  provider: SubscriptionProvider;
48
53
  accessToken: string;
@@ -76,6 +81,23 @@ const CODEX_ACCOUNT_CLAIM = "https://api.openai.com/auth";
76
81
  export const CODEX_APPROVAL_PENDING =
77
82
  "OpenAI approval is still pending. Enter the code at OpenAI, finish approval, then try again.";
78
83
 
84
+ // Muse Code's own device sign-in (`muse login`), as its public launcher runs it.
85
+ export const MUSE_CODE_CLIENT_ID = "1031625952748946";
86
+ const META_AUTH_BASE_URL = "https://auth.meta.com";
87
+ const META_DEVICE_AUTHORIZATION_URL = `${META_AUTH_BASE_URL}/oidc/device/authorization/`;
88
+ const META_TOKEN_URL = `${META_AUTH_BASE_URL}/oidc/device/token/`;
89
+ // Muse Code mints its subscription's Model API key from the sign-in token here.
90
+ const MUSE_MINT_URL = "https://api.meta.ai/muse-code/key";
91
+ // Meta states no lifetime for a minted key, and Muse Code mints a fresh one at
92
+ // every start, so the key is minted again on this cadence.
93
+ const MUSE_KEY_LIFETIME_MS = 12 * 60 * 60_000;
94
+ // Marks a stored sign-in refresh token, which is exchanged for a fresh sign-in
95
+ // token before each mint. Without it the stored value is the sign-in token.
96
+ const MUSE_SIGN_IN_REFRESH_PREFIX = "refresh:";
97
+
98
+ export const META_APPROVAL_PENDING =
99
+ "Meta approval is still pending. Enter the code at Meta, finish approval, then try again.";
100
+
79
101
  export async function startOAuth(
80
102
  provider: SubscriptionProvider,
81
103
  options: OAuthOptions,
@@ -103,6 +125,23 @@ export async function startOAuth(
103
125
  flow: { code_verifier: verifier, state },
104
126
  };
105
127
  }
128
+ if (provider === "meta_muse") {
129
+ const payload = await postForm(
130
+ options.fetch,
131
+ META_DEVICE_AUTHORIZATION_URL,
132
+ { client_id: MUSE_CODE_CLIENT_ID },
133
+ "Meta device code request",
134
+ );
135
+ const complete = payload.verification_uri_complete;
136
+ return {
137
+ authorizationUrl: typeof complete === "string" && complete.startsWith("https://")
138
+ ? complete
139
+ : requiredString(payload, "verification_uri"),
140
+ expiresAt,
141
+ userCode: requiredString(payload, "user_code"),
142
+ flow: { device_code: requiredString(payload, "device_code") },
143
+ };
144
+ }
106
145
  const payload = await postJson(
107
146
  options.fetch,
108
147
  `${CODEX_DEVICE_AUTH_URL}/usercode`,
@@ -120,7 +159,7 @@ export async function startOAuth(
120
159
 
121
160
  /**
122
161
  * Finish a flow. Claude Code needs the pasted authorization response; the
123
- * device-code flow ignores it and asks OpenAI whether the user approved.
162
+ * device-code flows ignore it and ask the provider whether the user approved.
124
163
  */
125
164
  export async function completeOAuth(
126
165
  provider: SubscriptionProvider,
@@ -154,6 +193,35 @@ export async function completeOAuth(
154
193
  );
155
194
  return claudeTokens(token, now, null);
156
195
  }
196
+ if (provider === "meta_muse") {
197
+ let token: Record<string, unknown>;
198
+ try {
199
+ token = await postForm(
200
+ options.fetch,
201
+ META_TOKEN_URL,
202
+ {
203
+ grant_type: "urn:ietf:params:oauth:grant-type:device_code",
204
+ device_code: requiredString(flow, "device_code"),
205
+ client_id: MUSE_CODE_CLIENT_ID,
206
+ },
207
+ "Meta device approval check",
208
+ );
209
+ } catch (error) {
210
+ if (error instanceof SubscriptionOAuthProviderError) {
211
+ if (error.errorCode === "authorization_pending" || error.errorCode === "slow_down") {
212
+ throw new SubscriptionOAuthInputError(META_APPROVAL_PENDING);
213
+ }
214
+ if (error.errorCode === "access_denied") {
215
+ throw new SubscriptionOAuthInputError("The sign-in was declined on Meta. Start again to retry.");
216
+ }
217
+ if (error.errorCode === "expired_token") {
218
+ throw new SubscriptionOAuthInputError("The Meta sign-in code expired. Start again.");
219
+ }
220
+ }
221
+ throw error;
222
+ }
223
+ return await museTokens(requiredString(token, "access_token"), museRefreshToken(token), options.fetch, now);
224
+ }
157
225
 
158
226
  // The provider, not user-supplied input, supplies the authorization code.
159
227
  let approval: Record<string, unknown>;
@@ -207,6 +275,28 @@ export async function refreshOAuth(
207
275
  );
208
276
  return claudeTokens(token, now, credential.refreshToken);
209
277
  }
278
+ if (credential.provider === "meta_muse") {
279
+ let stored = credential.refreshToken;
280
+ let signInToken = stored;
281
+ if (stored.startsWith(MUSE_SIGN_IN_REFRESH_PREFIX)) {
282
+ const token = await postForm(
283
+ options.fetch,
284
+ META_TOKEN_URL,
285
+ {
286
+ grant_type: "refresh_token",
287
+ refresh_token: stored.slice(MUSE_SIGN_IN_REFRESH_PREFIX.length),
288
+ client_id: MUSE_CODE_CLIENT_ID,
289
+ },
290
+ "Meta sign-in refresh",
291
+ );
292
+ signInToken = requiredString(token, "access_token");
293
+ stored = museRefreshToken(token) ?? stored;
294
+ }
295
+ // A lapsed plan fails routing like any other refresh failure.
296
+ return await museTokens(signInToken, stored, options.fetch, now).catch((error: unknown) => {
297
+ throw error instanceof SubscriptionOAuthInputError ? new SubscriptionOAuthProviderError(error.message) : error;
298
+ });
299
+ }
210
300
  const token = await postJson(
211
301
  options.fetch,
212
302
  CODEX_TOKEN_URL,
@@ -258,6 +348,43 @@ function claudeTokens(
258
348
  };
259
349
  }
260
350
 
351
+ function museRefreshToken(token: Record<string, unknown>): string | null {
352
+ const value = token.refresh_token;
353
+ return typeof value === "string" && value ? MUSE_SIGN_IN_REFRESH_PREFIX + value : null;
354
+ }
355
+
356
+ // Mints the plan's Model API key from the Meta sign-in token.
357
+ async function museTokens(
358
+ signInToken: string,
359
+ refreshToken: string | null,
360
+ fetch: OAuthFetch,
361
+ now: Date,
362
+ ): Promise<OAuthTokens> {
363
+ const minted = await requestJson(
364
+ fetch,
365
+ MUSE_MINT_URL,
366
+ "{}",
367
+ "application/json",
368
+ "Muse Code subscription key mint",
369
+ { Authorization: `Bearer ${signInToken}`, "x-api-version": "1.0.0" },
370
+ );
371
+ // A key minted without an active plan bills Model API pay-as-you-go.
372
+ if (minted.is_subs_active !== true) {
373
+ throw new SubscriptionOAuthInputError(
374
+ "This Meta account has no active Muse Code subscription. Subscribe at https://accountscenter.meta.com/muse_code/, then connect again.",
375
+ );
376
+ }
377
+ const email = minted.user_email;
378
+ return {
379
+ provider: "meta_muse",
380
+ accessToken: requiredString(minted, "api_key"),
381
+ refreshToken: refreshToken ?? signInToken,
382
+ expiresAt: new Date(now.getTime() + MUSE_KEY_LIFETIME_MS),
383
+ accountId: null,
384
+ accountEmail: typeof email === "string" && email ? email : null,
385
+ };
386
+ }
387
+
261
388
  function codexTokens(
262
389
  payload: Record<string, unknown>,
263
390
  now: Date,
@@ -353,12 +480,13 @@ async function requestJson(
353
480
  body: string,
354
481
  contentType: string,
355
482
  operation: string,
483
+ headers: Record<string, string> = {},
356
484
  ): Promise<Record<string, unknown>> {
357
485
  let response: Response;
358
486
  try {
359
487
  response = await fetch(url, {
360
488
  method: "POST",
361
- headers: { Accept: "application/json", "Content-Type": contentType, "User-Agent": "Dari/1.0" },
489
+ headers: { Accept: "application/json", "Content-Type": contentType, "User-Agent": "Dari/1.0", ...headers },
362
490
  body,
363
491
  signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
364
492
  });
@@ -366,10 +494,13 @@ async function requestJson(
366
494
  throw new SubscriptionOAuthProviderError(`${operation} could not reach the provider.`);
367
495
  }
368
496
  if (!response.ok) {
369
- await response.body?.cancel();
497
+ // Only the RFC 6749 `error` code is kept; the rest may carry provider detail.
498
+ const failure: unknown = await response.json().catch(() => undefined);
499
+ const errorCode = isRecord(failure) && typeof failure.error === "string" ? failure.error : undefined;
370
500
  throw new SubscriptionOAuthProviderError(
371
501
  `${operation} failed with provider status ${response.status}.`,
372
502
  response.status,
503
+ errorCode,
373
504
  );
374
505
  }
375
506
  let decoded: unknown;
@@ -47,6 +47,7 @@ export type PendingOAuthSession = {
47
47
  const BASE_NAMES: Record<SubscriptionProvider, string> = {
48
48
  anthropic_claude_code: "ANTHROPIC_CLAUDE_CODE_OAUTH_TOKEN",
49
49
  openai_codex: "OPENAI_CODEX_OAUTH_TOKEN",
50
+ meta_muse: "META_MUSE_OAUTH_TOKEN",
50
51
  };
51
52
 
52
53
  type CredentialRow = {
package/src/types.ts CHANGED
@@ -42,12 +42,25 @@ export type AwsAssumeRoleProviderAuth = {
42
42
  // Authentication a provider call can use directly.
43
43
  export type ResolvedProviderAuth = BearerProviderAuth | AwsSigV4ProviderAuth;
44
44
 
45
+ // A personal plan whose access token serves its provider's models in place of
46
+ // an API key:
47
+ // - "openai_codex": a ChatGPT plan; the token is Codex's auth.json
48
+ // `tokens.access_token`, served through the Codex backend.
49
+ // - "anthropic_claude_code": a Claude Code plan; the token is the OAuth token
50
+ // `claude setup-token` prints. It serves Claude Code clients only.
51
+ // - "meta_muse": a Meta (Muse) sign-in; the token is the Model API key the
52
+ // sign-in mints (Pi's Meta OAuth `access`).
53
+ export type SubscriptionKind = "openai_codex" | "anthropic_claude_code" | "meta_muse";
54
+
45
55
  // A bearer credential an embedding platform supplies for one request through
46
56
  // GatewayPlatform.resolveProviderCredential, optionally with its own endpoint.
57
+ // With `subscription`, `apiKey` is that plan's access token; the platform keeps
58
+ // it fresh.
47
59
  export type ProviderCredential = {
48
60
  apiKey: string;
49
61
  baseUrl?: string;
50
62
  headers?: Record<string, string>;
63
+ subscription?: SubscriptionKind;
51
64
  };
52
65
 
53
66
  export type ProviderAuth = ResolvedProviderAuth | AwsAssumeRoleProviderAuth;
@@ -70,7 +83,7 @@ export type PublicEvalCatalogCard = Omit<RouterEval, "scores"> & {
70
83
  scores: PublicEvalCatalogScore[];
71
84
  };
72
85
 
73
- export type ClientHarness = "claude_code";
86
+ export type ClientHarness = "claude_code" | "muse_code";
74
87
 
75
88
  export type ResolvedRouterModelConfig = {
76
89
  provider: string;
@@ -111,9 +124,13 @@ export type ResolvedRouter = {
111
124
  speculative_routing?: boolean;
112
125
  primary_retries?: number;
113
126
  // Serve only the requested `dari/model/<id>` at the requested thinking
114
- // level: no selector, lease, fallback, or personal subscription, and an
115
- // error for anything else, whatever the client sends.
127
+ // level: no selector, lease, fallback, or personal subscription the
128
+ // platform did not supply through resolveProviderCredential, and an error
129
+ // for anything else, whatever the client sends.
116
130
  strict?: boolean;
131
+ // On strict routers only, allow requested catalog models of these providers.
132
+ // Requires dari/model/<provider>/<id>; never enables fallback.
133
+ strict_providers?: string[];
117
134
  model_fallback_enabled?: boolean;
118
135
  fallback_requires_different_provider?: boolean;
119
136
  evals?: RouterEval[];