@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
@@ -0,0 +1,310 @@
1
+ import { getSupportedThinkingLevels, type Api, type Model } from "@mupt-ai/pi-ai";
2
+ import {
3
+ getBuiltinModelDataGeneratedAt,
4
+ getBuiltinModels,
5
+ getBuiltinProviders,
6
+ } from "@mupt-ai/pi-ai/providers/all";
7
+ import {
8
+ REASONING_EFFORTS,
9
+ isReasoningEffort,
10
+ isRecord,
11
+ type ReasoningEffort,
12
+ } from "@mupt-ai/dari-router/policy-engine";
13
+ import { HttpError } from "../http/errors.js";
14
+
15
+ // The models each provider can serve to a coding agent, with the limits,
16
+ // thinking levels, and list prices a caller needs to pick and price one.
17
+ // Gateways (OpenRouter, Vercel AI Gateway) are listed live from their public
18
+ // models endpoints; model providers come from the bundled Pi catalog.
19
+
20
+ export type CatalogModel = {
21
+ // Canonical `provider/native-id`; request it as `dari/model/<id>`.
22
+ id: string;
23
+ provider: string;
24
+ native_model_id: string;
25
+ name: string;
26
+ context_window: number | null;
27
+ max_output_tokens: number | null;
28
+ input_modalities: string[];
29
+ // Accepted thinking levels; ["off"] for models that do not reason.
30
+ thinking_levels: ReasoningEffort[];
31
+ // List price in USD per million tokens; null when the source publishes
32
+ // none, e.g. meta-routers whose price depends on the model they pick.
33
+ price: CatalogModelPrice | null;
34
+ // When the price was read: the fetch time for gateways, the catalog's
35
+ // generation time for model providers.
36
+ price_as_of: string;
37
+ };
38
+
39
+ export type CatalogModelPrice = {
40
+ input: number;
41
+ output: number;
42
+ cached_input: number | null;
43
+ cache_write: number | null;
44
+ // Long-context rates, each applying to prompts above its token threshold.
45
+ tiers: CatalogPriceTier[];
46
+ };
47
+
48
+ export type CatalogPriceTier = {
49
+ input_tokens_above: number;
50
+ input: number;
51
+ output: number;
52
+ cached_input: number | null;
53
+ cache_write: number | null;
54
+ };
55
+
56
+ export type ModelCatalogOptions = {
57
+ // Providers to list; defaults to DEFAULT_CATALOG_PROVIDERS. Any Pi built-in
58
+ // provider id is accepted.
59
+ providers?: readonly string[] | undefined;
60
+ fetch?: ((url: string, init?: RequestInit) => Promise<Response>) | undefined;
61
+ signal?: AbortSignal | undefined;
62
+ };
63
+
64
+ export const DEFAULT_CATALOG_PROVIDERS: readonly string[] = [
65
+ "openai",
66
+ "anthropic",
67
+ "meta",
68
+ "xai",
69
+ "fireworks",
70
+ "openrouter",
71
+ "vercel-ai-gateway",
72
+ ];
73
+
74
+ type LiveListing = {
75
+ url: string;
76
+ models: (data: unknown[], asOf: string) => CatalogModel[];
77
+ };
78
+
79
+ const LIVE_LISTINGS: Record<string, LiveListing> = {
80
+ openrouter: { url: "https://openrouter.ai/api/v1/models", models: openRouterModels },
81
+ "vercel-ai-gateway": { url: "https://ai-gateway.vercel.sh/v1/models", models: vercelModels },
82
+ };
83
+
84
+ /** List the agent-capable models of each requested provider. */
85
+ export async function listModelCatalog(options: ModelCatalogOptions = {}): Promise<CatalogModel[]> {
86
+ const providers = [...new Set(options.providers ?? DEFAULT_CATALOG_PROVIDERS)];
87
+ const builtin = new Set<string>(getBuiltinProviders());
88
+ for (const provider of providers) {
89
+ if (LIVE_LISTINGS[provider] === undefined && !builtin.has(provider)) {
90
+ throw new HttpError(400, `Unknown catalog provider '${provider}'.`, "unknown_provider", "provider");
91
+ }
92
+ }
93
+ const lists = await Promise.all(providers.map(async (provider) => {
94
+ const listing = LIVE_LISTINGS[provider];
95
+ return listing === undefined
96
+ ? piModels(provider)
97
+ : listing.models(await fetchListing(listing.url, options), new Date().toISOString());
98
+ }));
99
+ return lists.flat();
100
+ }
101
+
102
+ async function fetchListing(url: string, options: ModelCatalogOptions): Promise<unknown[]> {
103
+ let status: number | undefined;
104
+ let body: unknown = null;
105
+ // Network errors, aborts, and unparseable bodies are upstream failures
106
+ // too: all of them surface as catalog_unavailable rather than a raw 500.
107
+ try {
108
+ const response = await (options.fetch ?? fetch)(url, {
109
+ headers: { accept: "application/json" },
110
+ ...(options.signal ? { signal: options.signal } : {}),
111
+ });
112
+ status = response.status;
113
+ body = response.ok ? await response.json() : null;
114
+ } catch {
115
+ // Leaves body null.
116
+ }
117
+ if (!isRecord(body) || !Array.isArray(body.data)) {
118
+ const detail = status === undefined ? "could not be reached" : `returned no model list (HTTP ${status})`;
119
+ throw new HttpError(502, `${url} ${detail}.`, "catalog_unavailable");
120
+ }
121
+ return body.data;
122
+ }
123
+
124
+ function piModels(provider: string): CatalogModel[] {
125
+ const generatedAt = getBuiltinModelDataGeneratedAt();
126
+ const asOf = generatedAt === undefined ? "" : new Date(generatedAt).toISOString();
127
+ const models = getBuiltinModels(provider as never) as unknown as Model<Api>[];
128
+ return models.map((model) => ({
129
+ id: `${provider}/${model.id}`,
130
+ provider,
131
+ native_model_id: model.id,
132
+ name: model.name,
133
+ context_window: model.contextWindow,
134
+ max_output_tokens: model.maxTokens,
135
+ input_modalities: [...model.input],
136
+ thinking_levels: orderedEfforts(getSupportedThinkingLevels(model)),
137
+ price: piPrice(model.cost),
138
+ price_as_of: asOf,
139
+ }));
140
+ }
141
+
142
+ function piPrice(cost: Model<Api>["cost"]): CatalogModelPrice {
143
+ return {
144
+ input: cost.input,
145
+ output: cost.output,
146
+ cached_input: cost.cacheRead,
147
+ cache_write: cost.cacheWrite,
148
+ tiers: (cost.tiers ?? []).map((tier) => ({
149
+ input_tokens_above: tier.inputTokensAbove,
150
+ input: tier.input,
151
+ output: tier.output,
152
+ cached_input: tier.cacheRead,
153
+ cache_write: tier.cacheWrite,
154
+ })),
155
+ };
156
+ }
157
+
158
+ // https://openrouter.ai/docs/api/api-reference/models/get-models
159
+ function openRouterModels(data: unknown[], asOf: string): CatalogModel[] {
160
+ return data.filter(isRecord).flatMap((model) => {
161
+ const architecture = isRecord(model.architecture) ? model.architecture : {};
162
+ const parameters = stringList(model.supported_parameters);
163
+ if (typeof model.id !== "string" || !parameters.includes("tools")) return [];
164
+ if (!stringList(architecture.output_modalities).includes("text")) return [];
165
+ const reasoning = isRecord(model.reasoning) ? model.reasoning : {};
166
+ const topProvider = isRecord(model.top_provider) ? model.top_provider : {};
167
+ return [{
168
+ id: `openrouter/${model.id}`,
169
+ provider: "openrouter",
170
+ native_model_id: model.id,
171
+ name: typeof model.name === "string" ? model.name : model.id,
172
+ context_window: positiveInteger(model.context_length),
173
+ max_output_tokens: positiveInteger(topProvider.max_completion_tokens),
174
+ input_modalities: stringList(architecture.input_modalities),
175
+ thinking_levels: gatewayThinkingLevels({
176
+ reasons: parameters.includes("reasoning"),
177
+ efforts: Array.isArray(reasoning.supported_efforts) ? stringList(reasoning.supported_efforts) : null,
178
+ canDisable: reasoning.mandatory !== true,
179
+ }),
180
+ price: isRecord(model.pricing) ? openRouterPrice(model.pricing) : null,
181
+ price_as_of: asOf,
182
+ }];
183
+ });
184
+ }
185
+
186
+ function openRouterPrice(pricing: Record<string, unknown>): CatalogModelPrice | null {
187
+ const base = rates(pricing, "prompt", "completion");
188
+ if (base === null) return null;
189
+ // Overrides restate only the rates they change; time-of-day overrides are
190
+ // not context tiers.
191
+ const tiers = (Array.isArray(pricing.overrides) ? pricing.overrides : [])
192
+ .filter(isRecord)
193
+ .flatMap((override) => {
194
+ const threshold = positiveInteger(override.min_prompt_tokens);
195
+ if (threshold === null || "utc_days" in override || "utc_start" in override || "utc_end" in override) {
196
+ return [];
197
+ }
198
+ const tier = rates({ ...pricing, ...override }, "prompt", "completion", override);
199
+ return tier === null ? [] : [{ input_tokens_above: threshold, ...tier }];
200
+ });
201
+ return { ...base, tiers };
202
+ }
203
+
204
+ // https://vercel.com/docs/ai-gateway/models-and-providers
205
+ function vercelModels(data: unknown[], asOf: string): CatalogModel[] {
206
+ return data.filter(isRecord).flatMap((model) => {
207
+ const modalities = isRecord(model.modalities) ? model.modalities : {};
208
+ const parameters = stringList(model.supported_parameters);
209
+ if (typeof model.id !== "string" || model.type !== "language" || !parameters.includes("tools")) return [];
210
+ if (!stringList(modalities.output).includes("text")) return [];
211
+ const options = Array.isArray(model.reasoning_options) ? model.reasoning_options.filter(isRecord) : [];
212
+ const effort = options.find((option) => option.type === "effort");
213
+ return [{
214
+ id: `vercel-ai-gateway/${model.id}`,
215
+ provider: "vercel-ai-gateway",
216
+ native_model_id: model.id,
217
+ name: typeof model.name === "string" ? model.name : model.id,
218
+ context_window: positiveInteger(model.context_window),
219
+ max_output_tokens: positiveInteger(model.max_tokens),
220
+ input_modalities: stringList(modalities.input),
221
+ thinking_levels: gatewayThinkingLevels({
222
+ reasons: parameters.includes("reasoning") || options.length > 0,
223
+ efforts: effort === undefined ? null : stringList(effort.values),
224
+ canDisable: options.length === 0 || options.some((option) => option.type === "toggle"),
225
+ }),
226
+ price: isRecord(model.pricing) ? vercelPrice(model.pricing) : null,
227
+ price_as_of: asOf,
228
+ }];
229
+ });
230
+ }
231
+
232
+ function vercelPrice(pricing: Record<string, unknown>): CatalogModelPrice | null {
233
+ const base = rates(pricing, "input", "output");
234
+ if (base === null) return null;
235
+ // Each rate lists its own {cost, min, max} ranges; a range covers prompts of
236
+ // at least `min` tokens, so it is the tier above `min - 1`.
237
+ const ranged = new Map<number, Record<string, unknown>>();
238
+ for (const key of ["input", "output", "input_cache_read", "input_cache_write"]) {
239
+ const ranges = Array.isArray(pricing[`${key}_tiers`]) ? pricing[`${key}_tiers`] as unknown[] : [];
240
+ for (const range of ranges.filter(isRecord)) {
241
+ const start = positiveInteger(range.min);
242
+ if (start === null || range.cost == null) continue;
243
+ ranged.set(start - 1, { ...ranged.get(start - 1), [key]: range.cost });
244
+ }
245
+ }
246
+ const tiers = [...ranged.entries()]
247
+ .sort(([left], [right]) => left - right)
248
+ .flatMap(([threshold, stated]) => {
249
+ const tier = rates({ ...pricing, ...stated }, "input", "output", stated);
250
+ return tier === null ? [] : [{ input_tokens_above: threshold, ...tier }];
251
+ });
252
+ return { ...base, tiers };
253
+ }
254
+
255
+ // Base rates from per-token strings. A tier inherits input and output from
256
+ // the base price but states its own cache rates, or has none.
257
+ function rates(
258
+ pricing: Record<string, unknown>,
259
+ inputKey: string,
260
+ outputKey: string,
261
+ cacheRates: Record<string, unknown> = pricing,
262
+ ): Omit<CatalogPriceTier, "input_tokens_above"> | null {
263
+ const input = perMillion(pricing[inputKey]);
264
+ const output = perMillion(pricing[outputKey]);
265
+ // Meta-routers with variable prices list negative rates.
266
+ if (input === null || output === null) return null;
267
+ return {
268
+ input,
269
+ output,
270
+ cached_input: perMillion(cacheRates.input_cache_read),
271
+ cache_write: perMillion(cacheRates.input_cache_write),
272
+ };
273
+ }
274
+
275
+ // USD per token as a decimal string -> USD per million tokens. Shifting the
276
+ // exponent in the string keeps "0.000001" exactly 1.
277
+ function perMillion(value: unknown): number | null {
278
+ const rate = typeof value === "string" ? Number(`${value.trim()}e6`)
279
+ : typeof value === "number" ? value * 1_000_000
280
+ : Number.NaN;
281
+ return Number.isFinite(rate) && rate >= 0 ? rate : null;
282
+ }
283
+
284
+ // Gateways forward `reasoning.effort`; models that reason without a stated
285
+ // effort list take the standard low/medium/high levels.
286
+ function gatewayThinkingLevels(args: {
287
+ reasons: boolean;
288
+ efforts: string[] | null;
289
+ canDisable: boolean;
290
+ }): ReasoningEffort[] {
291
+ if (!args.reasons) return ["off"];
292
+ const efforts = args.efforts ?? ["low", "medium", "high"];
293
+ return orderedEfforts([
294
+ ...efforts.map((effort) => effort === "none" ? "off" : effort),
295
+ ...(args.canDisable ? ["off"] : []),
296
+ ]);
297
+ }
298
+
299
+ function orderedEfforts(levels: readonly string[]): ReasoningEffort[] {
300
+ const accepted = new Set(levels.filter(isReasoningEffort));
301
+ return REASONING_EFFORTS.filter((effort) => accepted.has(effort));
302
+ }
303
+
304
+ function stringList(value: unknown): string[] {
305
+ return Array.isArray(value) ? value.filter((item): item is string => typeof item === "string") : [];
306
+ }
307
+
308
+ function positiveInteger(value: unknown): number | null {
309
+ return typeof value === "number" && Number.isInteger(value) && value > 0 ? value : null;
310
+ }
@@ -11,12 +11,15 @@ export function executionProviderForModel(
11
11
  }
12
12
 
13
13
  /**
14
- * The key that serves a model on a resolved router. Missing declarations are
14
+ * The key that serves a model on a resolved router: a platform-supplied
15
+ * credential first, then the resolved provider key. Missing declarations are
15
16
  * user-owned so unknown platform data cannot trigger managed-key billing.
16
17
  */
17
18
  export function keySourceForModel(
18
19
  credentials: ResolvedRouter,
19
20
  model: string,
20
21
  ): ProviderKeySource {
22
+ const supplied = credentials.provider_credentials?.[model];
23
+ if (supplied) return supplied.subscription ? "subscription" : "user";
21
24
  return credentials.provider_key_sources?.[executionProviderForModel(credentials, model)] ?? "user";
22
25
  }
@@ -33,6 +33,9 @@ const PROVIDER_BASE_URLS: Record<string, string> = {
33
33
  anthropic: "https://api.anthropic.com",
34
34
  fireworks: "https://api.fireworks.ai/inference/v1",
35
35
  xai: "https://api.x.ai/v1",
36
+ // Meta's Model API serves OpenAI Responses and Chat Completions here, and
37
+ // Anthropic Messages at https://api.meta.ai.
38
+ meta: "https://api.meta.ai/v1",
36
39
  // Pi-ai derives the effective AWS region from the endpoint unless the
37
40
  // model config overrides base_url or the native id is an inference profile
38
41
  // ARN, so this default only pins us-east-1 when nothing else is configured.
@@ -169,6 +169,28 @@ const MODEL_OFF_WIRE_NONE: ReadonlySet<string> = new Set([
169
169
  "deepseek-ai/DeepSeek-V4.1-Flash-1017",
170
170
  ]);
171
171
 
172
+ // Wire values a provider's API accepts for thinking levels the Pi registry
173
+ // leaves unmapped. Meta's Model API takes reasoning effort "none" on every
174
+ // Muse model (dev.meta.ai/docs/coding-agents).
175
+ const PROVIDER_THINKING_LEVEL_OVERRIDES: Record<string, Record<string, string>> = {
176
+ meta: { off: "none" },
177
+ };
178
+
179
+ export function providerThinkingLevelOverrides(provider: string): Record<string, string> | undefined {
180
+ return PROVIDER_THINKING_LEVEL_OVERRIDES[provider];
181
+ }
182
+
183
+ /** Registered Pi metadata with the serving provider's thinking level overrides. */
184
+ export function withThinkingLevelOverrides(
185
+ provider: string,
186
+ registered: Record<string, unknown> | undefined,
187
+ ): Record<string, unknown> | undefined {
188
+ const override = providerThinkingLevelOverrides(provider);
189
+ if (registered === undefined || override === undefined) return registered;
190
+ const map = isRecord(registered.thinkingLevelMap) ? registered.thinkingLevelMap : {};
191
+ return { ...registered, thinkingLevelMap: { ...map, ...override } };
192
+ }
193
+
172
194
  export function offRequiresWireNone(modelId: string): boolean {
173
195
  return MODEL_OFF_WIRE_NONE.has(canonicalModelId(modelId));
174
196
  }
@@ -0,0 +1,54 @@
1
+ import {
2
+ nativeModelId,
3
+ providerForModel,
4
+ type RouterModelPrice,
5
+ } from "@mupt-ai/dari-router/policy-engine";
6
+
7
+ import type { ClientHarness, ResolvedRouterModelConfig, SubscriptionKind } from "../types.js";
8
+
9
+ const CODEX_BACKEND_URL = "https://chatgpt.com/backend-api";
10
+
11
+ // The model provider whose models each kind of plan serves.
12
+ const SUBSCRIPTION_MODEL_PROVIDERS: Record<SubscriptionKind, string> = {
13
+ openai_codex: "openai",
14
+ anthropic_claude_code: "anthropic",
15
+ meta_muse: "meta",
16
+ };
17
+
18
+ // The client each plan is limited to, as its provider limits it: a Claude Code
19
+ // plan serves only Claude Code, and a Muse Code plan only Muse Code.
20
+ const SUBSCRIPTION_CLIENTS: Partial<Record<SubscriptionKind, ClientHarness>> = {
21
+ anthropic_claude_code: "claude_code",
22
+ meta_muse: "muse_code",
23
+ };
24
+
25
+ /**
26
+ * Whether a plan may serve a model for this client: each plan serves only its
27
+ * own provider's models, and only to the client its provider allows.
28
+ */
29
+ export function subscriptionServes(
30
+ kind: SubscriptionKind,
31
+ model: string,
32
+ clientHarness: ClientHarness | undefined,
33
+ ): boolean {
34
+ if (SUBSCRIPTION_MODEL_PROVIDERS[kind] !== providerForModel(model)) return false;
35
+ const client = SUBSCRIPTION_CLIENTS[kind];
36
+ return client === undefined || clientHarness === client;
37
+ }
38
+
39
+ // A ChatGPT plan serves OpenAI models as the `openai-codex` provider through
40
+ // the Codex backend.
41
+ export function codexModelConfig(
42
+ modelId: string,
43
+ price: RouterModelPrice | null,
44
+ ): ResolvedRouterModelConfig {
45
+ return {
46
+ provider: "openai-codex",
47
+ native_model_id: nativeModelId(modelId),
48
+ api: "openai-codex-responses",
49
+ base_url: CODEX_BACKEND_URL,
50
+ // A plan bills no tokens; the catalog price only feeds cost estimates.
51
+ cost: price ?? { input: 0, output: 0 },
52
+ fast_mode: false,
53
+ };
54
+ }
@@ -33,7 +33,10 @@ export type GatewayPlatform = {
33
33
  * Optional. Supply the credential for one enabled model of this request,
34
34
  * consulted after `resolveRouter` and before its provider keys (for the
35
35
  * self-hosted gateway, its environment keys), so an embedder can serve
36
- * per-tenant keys from its own store. Undefined keeps the resolved key.
36
+ * per-tenant keys from its own store. Undefined keeps the resolved key. A
37
+ * credential with `subscription` serves the model on that plan, even on a
38
+ * strict router; a plan that cannot serve the model for this client (see
39
+ * SubscriptionKind) is ignored.
37
40
  */
38
41
  resolveProviderCredential?(
39
42
  request: ProviderCredentialRequest,
@@ -74,6 +77,7 @@ export type ProviderCredentialRequest = {
74
77
  organizationId: string;
75
78
  // The gateway key the request authenticated with.
76
79
  apiKey: string;
80
+ clientHarness?: ClientHarness;
77
81
  };
78
82
 
79
83
  export type PrefixStateWrite = {
@@ -80,7 +80,9 @@ export function internalChatCompletionsToInteraction(request: ChatCompletionRequ
80
80
  const tools = chatTools(request.tools);
81
81
  toolsByWireName(tools);
82
82
  const responseFormat = toInteractionResponseFormat(request.response_format);
83
- const reasoningEffort = request.reasoning_effort;
83
+ // OpenAI spells the non-reasoning effort "none"; Dari spells it "off".
84
+ const requestedEffort: string | undefined = request.reasoning_effort;
85
+ const reasoningEffort = requestedEffort === "none" ? "off" : request.reasoning_effort;
84
86
  return {
85
87
  sourceProtocol: request.source_protocol ?? "openai_chat",
86
88
  model: request.model,
@@ -214,7 +214,8 @@ function validateReasoning(request: ChatCompletionRequest): void {
214
214
  "reasoning"
215
215
  );
216
216
  }
217
- if (request.reasoning_effort !== undefined && !isReasoningEffort(request.reasoning_effort)) {
217
+ const effort: unknown = request.reasoning_effort;
218
+ if (effort !== undefined && effort !== "none" && !isReasoningEffort(effort)) {
218
219
  throw new HttpError(
219
220
  400,
220
221
  "reasoning_effort is not supported.",
@@ -29,7 +29,13 @@ import {
29
29
  import { HttpError } from "../http/errors.js";
30
30
  import { effectiveContextWindow } from "../models/context_limits.js";
31
31
  import { defaultBaseUrl } from "../models/provider_defaults.js";
32
- import { contextWindowOverride, inputModalitiesOverride, runtimeCompat } from "../models/runtime_overrides.js";
32
+ import {
33
+ contextWindowOverride,
34
+ inputModalitiesOverride,
35
+ providerThinkingLevelOverrides,
36
+ runtimeCompat,
37
+ withThinkingLevelOverrides,
38
+ } from "../models/runtime_overrides.js";
33
39
  import { BEDROCK_API } from "./model_capabilities.js";
34
40
  import { buildContext } from "./pi_context.js";
35
41
  import { providerIdentityFromModel } from "../interaction/provider_continuation_state.js";
@@ -335,16 +341,21 @@ export function buildModel(
335
341
  // A null catalog value delegates to Pi's registry when it knows the model.
336
342
  // Custom OpenAI-compatible models are not necessarily registered, so keep
337
343
  // the public effort set permissive and let the provider validate the wire
338
- // value instead of silently treating an unknown model as off-only.
344
+ // value instead of silently treating an unknown model as off-only. A
345
+ // built-in provider with its own thinking levels (Meta) serves ids newer
346
+ // than the registry the same way, with only its key configured.
347
+ const providerThinking = providerThinkingLevelOverrides(provider);
339
348
  const fallbackThinkingLevels =
340
- config !== undefined && configuredThinkingLevels == null && registered === undefined
349
+ (config !== undefined || providerThinking !== undefined) &&
350
+ configuredThinkingLevels == null &&
351
+ registered === undefined
341
352
  ? REASONING_EFFORTS
342
353
  : undefined;
343
354
  const effectiveThinkingLevels = Array.isArray(configuredThinkingLevels)
344
355
  ? configuredThinkingLevels
345
356
  : fallbackThinkingLevels;
346
357
  const configuredThinking = effectiveThinkingLevels
347
- ? thinkingMetadata(effectiveThinkingLevels, registered)
358
+ ? thinkingMetadata(effectiveThinkingLevels, registered ?? { thinkingLevelMap: providerThinking })
348
359
  : null;
349
360
  const defaultThinkingLevel = config?.default_thinking_level ?? undefined;
350
361
  const api = config?.api ?? registered?.api ?? defaultApi(provider);
@@ -415,7 +426,10 @@ function resolveModelMetadata(
415
426
  nativeId: string
416
427
  ): Record<string, unknown> | undefined {
417
428
  const registry = registryProvider(provider, modelId);
418
- return pi.getModel?.(registry, nativeId) ?? pi.getModel?.(registry, modelId);
429
+ return withThinkingLevelOverrides(
430
+ provider,
431
+ pi.getModel?.(registry, nativeId) ?? pi.getModel?.(registry, modelId),
432
+ );
419
433
  }
420
434
 
421
435
  // Dari's "azure" provider has no Pi registry of its own. One Foundry resource
@@ -491,7 +505,8 @@ function executionPricing(
491
505
  }
492
506
 
493
507
  function defaultApi(provider: string): string {
494
- if (provider === "openai" || provider === "xai") return "openai-responses";
508
+ // Meta recommends Responses: its Chat Completions drops reasoning between turns.
509
+ if (provider === "openai" || provider === "xai" || provider === "meta") return "openai-responses";
495
510
  if (provider === "anthropic") return "anthropic-messages";
496
511
  if (provider === "amazon-bedrock") return BEDROCK_API;
497
512
  if (provider === "azure") return "azure-openai-responses";
@@ -51,8 +51,10 @@ export function buildOptions(
51
51
  const providerAuth: ResolvedProviderAuth = typeof auth === "string" ? { type: "bearer", apiKey: auth } : auth;
52
52
  const { generation, toolChoice, parallelToolCalls, responseFormat } = request;
53
53
  const headers: Record<string, string> = {};
54
+ const bearerHeader = sendsBearerToMessages(providerAuth, model);
55
+ if (bearerHeader) headers.authorization = `Bearer ${providerAuth.apiKey}`;
54
56
  const options: Record<string, unknown> = {
55
- ...authenticationOptions(providerAuth, api),
57
+ ...(bearerHeader ? {} : authenticationOptions(providerAuth, api)),
56
58
  maxRetries: 0,
57
59
  timeoutMs,
58
60
  signal,
@@ -67,6 +69,9 @@ export function buildOptions(
67
69
  if (isClaudeCodeOAuth(providerAuth, api)) {
68
70
  headers["user-agent"] = request.claudeCodeUserAgent ?? CLAUDE_CODE_USER_AGENT;
69
71
  }
72
+ if (model.provider === "meta" && request.museCodeClientId !== undefined) {
73
+ headers["x-client-id"] = request.museCodeClientId;
74
+ }
70
75
  // Pi's Anthropic transport honors compat.supportsTemperature; the OpenAI
71
76
  // transports forward temperature regardless, so drop it here for models
72
77
  // that reject it instead of failing the whole request.
@@ -207,6 +212,17 @@ function authenticationOptions(auth: ResolvedProviderAuth, api: unknown): Bedroc
207
212
  };
208
213
  }
209
214
 
215
+ // Anthropic Messages endpoints outside Anthropic that take the key as a bearer
216
+ // token (Claude Code's ANTHROPIC_AUTH_TOKEN) and reject x-api-key.
217
+ const BEARER_MESSAGES_PROVIDERS: ReadonlySet<unknown> = new Set(["meta"]);
218
+
219
+ function sendsBearerToMessages(
220
+ auth: ResolvedProviderAuth,
221
+ model: Model,
222
+ ): auth is Extract<ResolvedProviderAuth, { type: "bearer" }> {
223
+ return auth.type === "bearer" && isAnthropic(model.api) && BEARER_MESSAGES_PROVIDERS.has(model.provider);
224
+ }
225
+
210
226
  function isClaudeCodeOAuth(auth: ResolvedProviderAuth, api: unknown): boolean {
211
227
  return auth.type === "bearer" && isAnthropic(api) && auth.apiKey.includes("sk-ant-oat");
212
228
  }
@@ -115,8 +115,10 @@ export function prepareProviderExecution(args: {
115
115
  }
116
116
  const provider = executionProviderForModel(args.credentials, model);
117
117
  const supplied = args.credentials.provider_credentials?.[model];
118
- const providerKeySource = supplied ? "user" : keySourceForModel(args.credentials, model);
119
- if (args.credentials.strict === true && providerKeySource === "subscription") {
118
+ const providerKeySource = keySourceForModel(args.credentials, model);
119
+ // A strict router serves a plan only when the platform supplied it for this
120
+ // request, never a pooled or personal one it resolved.
121
+ if (args.credentials.strict === true && providerKeySource === "subscription" && !supplied) {
120
122
  throw new HttpError(
121
123
  400,
122
124
  `Strict routers do not serve '${model}' from a personal subscription.`,
@@ -148,7 +150,9 @@ export function prepareProviderExecution(args: {
148
150
  : {}),
149
151
  provider,
150
152
  providerKeySource,
151
- ...(providerKeySource === "subscription" && args.credentials.attributed_user_id
153
+ // Only resolved personal subscriptions are held at their usage limit;
154
+ // the platform manages the plans it supplies.
155
+ ...(providerKeySource === "subscription" && !supplied && args.credentials.attributed_user_id
152
156
  ? {
153
157
  subscriptionUserId: args.credentials.attributed_user_id,
154
158
  subscriptionCredentialId: args.credentials.subscription_credential_ids?.[provider],
@@ -4,7 +4,7 @@ import { providerFailureMessage } from "../runtime/provider_stream_outcome.js";
4
4
  import type { ProviderKeySource } from "../types.js";
5
5
  import type { CapturedProviderResponse } from "./response_capture.js";
6
6
 
7
- export type SubscriptionOAuthProvider = "anthropic_claude_code" | "openai_codex";
7
+ export type SubscriptionOAuthProvider = "anthropic_claude_code" | "openai_codex" | "meta_muse";
8
8
 
9
9
  export type SubscriptionUsageLimit = {
10
10
  oauthProvider: SubscriptionOAuthProvider;
@@ -25,6 +25,7 @@ const FAILURE_MARKER = "subscription_usage_limit";
25
25
  const SUBSCRIPTION_LABEL: Record<SubscriptionOAuthProvider, string> = {
26
26
  anthropic_claude_code: "Claude Code",
27
27
  openai_codex: "ChatGPT",
28
+ meta_muse: "Muse Code",
28
29
  };
29
30
 
30
31
  export function subscriptionOAuthProviderFor(
@@ -34,13 +35,16 @@ export function subscriptionOAuthProviderFor(
34
35
  if (keySource !== "subscription") return null;
35
36
  if (provider === "anthropic") return "anthropic_claude_code";
36
37
  if (provider === "openai" || provider === "openai-codex") return "openai_codex";
38
+ if (provider === "meta") return "meta_muse";
37
39
  return null;
38
40
  }
39
41
 
40
42
  /**
41
43
  * Recognize a personal subscription that has run out of quota. Claude Code
42
44
  * answers 429 with the unified rate-limit headers; the ChatGPT backend answers
43
- * 429 with a structured `error.code` and `resets_at`. The Codex WebSocket
45
+ * 429 with a structured `error.code` and `resets_at`. Meta documents no
46
+ * usage-limit signature for Muse Code plans, so any 429 on one holds it until
47
+ * the reset Meta gives, if any. The Codex WebSocket
44
48
  * transport bypasses fetch, so its usage-limit error only survives as message
45
49
  * text and is matched there without a reset time.
46
50
  */
@@ -57,6 +61,7 @@ export function detectSubscriptionUsageLimit(args: {
57
61
  if (oauthProvider === "anthropic_claude_code") {
58
62
  return anthropicUsageLimit(args.captured, now);
59
63
  }
64
+ if (oauthProvider === "meta_muse") return museUsageLimit(args.captured, now);
60
65
  return codexUsageLimit(args.captured, providerFailureMessage(args.failure), now);
61
66
  }
62
67
 
@@ -109,6 +114,18 @@ function codexUsageLimit(
109
114
  };
110
115
  }
111
116
 
117
+ function museUsageLimit(
118
+ captured: CapturedProviderResponse | undefined,
119
+ now: Date,
120
+ ): SubscriptionUsageLimit | null {
121
+ if (captured === undefined || captured.status !== 429) return null;
122
+ return {
123
+ oauthProvider: "meta_muse",
124
+ resetsAt: laterReset(codexErrorBody(captured.bodyText)?.resets_at, captured.headers, now),
125
+ reason: "Muse Code usage limit reached.",
126
+ };
127
+ }
128
+
112
129
  function codexErrorBody(bodyText: string): Record<string, unknown> | null {
113
130
  try {
114
131
  const parsed: unknown = JSON.parse(bodyText);
@@ -178,13 +195,13 @@ export function subscriptionUsageLimitFromFailure(
178
195
  const oauthProvider = marker.oauth_provider;
179
196
  const resetsAt = typeof marker.resets_at === "string" ? new Date(marker.resets_at) : null;
180
197
  if (
181
- (oauthProvider !== "anthropic_claude_code" && oauthProvider !== "openai_codex")
198
+ typeof oauthProvider !== "string" || !(oauthProvider in SUBSCRIPTION_LABEL)
182
199
  || resetsAt === null || Number.isNaN(resetsAt.getTime())
183
200
  ) {
184
201
  return null;
185
202
  }
186
203
  return {
187
- oauthProvider,
204
+ oauthProvider: oauthProvider as SubscriptionOAuthProvider,
188
205
  resetsAt,
189
206
  reason: typeof marker.reason === "string" ? marker.reason : "Subscription usage limit reached.",
190
207
  ...(marker.switched === true ? { switched: true } : {}),