@mupt-ai/dari-gateway 0.2.1 → 0.3.1

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 (71) 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/provider_reported_cost.js +17 -2
  30. package/dist/providers/subscription_limits.d.ts +4 -2
  31. package/dist/providers/subscription_limits.js +19 -3
  32. package/dist/runtime/explicit_model.d.ts +3 -0
  33. package/dist/runtime/explicit_model.js +10 -6
  34. package/dist/runtime/prepare_routed_completion.js +24 -5
  35. package/dist/runtime/strict_provider_access.d.ts +6 -0
  36. package/dist/runtime/strict_provider_access.js +40 -0
  37. package/dist/self_host/resolve.js +11 -15
  38. package/dist/self_host/subscriptions/oauth.d.ts +10 -6
  39. package/dist/self_host/subscriptions/oauth.js +101 -6
  40. package/dist/self_host/subscriptions/store.js +1 -0
  41. package/dist/types.d.ts +4 -1
  42. package/package.json +2 -2
  43. package/src/context/pricing.ts +5 -2
  44. package/src/http/handler.ts +7 -0
  45. package/src/index.ts +9 -1
  46. package/src/ingress/chat_completions.ts +8 -1
  47. package/src/ingress/codex.ts +4 -0
  48. package/src/ingress/models.ts +13 -1
  49. package/src/ingress/muse_code.ts +9 -0
  50. package/src/ingress/muse_models.ts +36 -0
  51. package/src/interaction/types.ts +2 -0
  52. package/src/models/catalog.ts +310 -0
  53. package/src/models/execution_provider.ts +4 -1
  54. package/src/models/provider_defaults.ts +3 -0
  55. package/src/models/runtime_overrides.ts +22 -0
  56. package/src/models/subscriptions.ts +54 -0
  57. package/src/platform/platform.ts +5 -1
  58. package/src/protocol/input/chat_completions.ts +3 -1
  59. package/src/protocol/input/chat_completions_validation.ts +2 -1
  60. package/src/providers/pi.ts +21 -6
  61. package/src/providers/pi_options.ts +17 -1
  62. package/src/providers/provider_call.ts +7 -3
  63. package/src/providers/provider_reported_cost.ts +16 -2
  64. package/src/providers/subscription_limits.ts +21 -4
  65. package/src/runtime/explicit_model.ts +15 -16
  66. package/src/runtime/prepare_routed_completion.ts +22 -3
  67. package/src/runtime/strict_provider_access.ts +42 -0
  68. package/src/self_host/resolve.ts +12 -26
  69. package/src/self_host/subscriptions/oauth.ts +139 -8
  70. package/src/self_host/subscriptions/store.ts +1 -0
  71. package/src/types.ts +20 -3
package/README.md CHANGED
@@ -64,7 +64,7 @@ docker run -p 8080:8080 \
64
64
  | `DARI_GATEWAY_DB` | SQLite database for subscriptions and conversation state. Default `dari-gateway.db`. It stores subscription tokens in plaintext; protect it like a secret. |
65
65
  | `DARI_AUTO_ROUTING_API_KEY` | Optional. Select among a router's models with Dari Auto Routing. |
66
66
  | `PORT` | Listen port. Default `8080`. |
67
- | Provider keys | Each provider's standard variable, such as `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, or `OPENROUTER_API_KEY`. |
67
+ | Provider keys | Each provider's standard variable, such as `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, `META_API_KEY`, or `OPENROUTER_API_KEY`. |
68
68
 
69
69
  ## Configure
70
70
 
@@ -88,13 +88,17 @@ providers: # endpoints outside the built-in catalog
88
88
 
89
89
  Model ids are `provider/model`. Clients send `model: "dari/routing"` to let the router choose, or `model: "dari/model/<id>"` to pin one of its models. A Claude Code subscription serves Anthropic models only for Claude Code itself; a ChatGPT subscription serves OpenAI models for every client.
90
90
 
91
+ ### Model catalog
92
+
93
+ `GET /v1/models?detail=1` (any gateway key; optionally `&provider=openrouter,meta`) and `listModelCatalog({ providers?, fetch?, signal? })` list each provider's tool-capable models as `dari/model/<id>`-ready ids with context window, max output tokens, input modalities, accepted thinking levels, and list prices in USD per million tokens (input, output, cache read, cache write, long-context tiers) with `price_as_of`. OpenRouter and Vercel AI Gateway are read live from their public models endpoints; other providers (default: OpenAI, Anthropic, Meta, xAI, Fireworks) come from the bundled Pi catalog.
94
+
91
95
  ## Coding agents
92
96
 
93
97
  ```bash
94
98
  export DARI_GATEWAY_URL=http://localhost:8080
95
99
  export DARI_GATEWAY_KEY=<one of DARI_GATEWAY_KEYS>
96
100
 
97
- dari credentials subscription connect claude-code # or codex; repeat to pool plans
101
+ dari credentials subscription connect claude-code # or codex or muse-code; repeat to pool plans
98
102
  dari claude
99
103
  ```
100
104
 
@@ -115,8 +119,8 @@ The root entry needs no Bun APIs, so any runtime with web-standard `fetch`, `Req
115
119
 
116
120
  Platforms can also:
117
121
 
118
- - implement `resolveProviderCredential({ provider, model, routerId, organizationId, apiKey })` to supply a per-request `{ apiKey, baseUrl?, headers? }`, such as a tenant's own key, in place of the router's resolved key;
119
- - resolve routers with `strict: true` to serve only the requested `dari/model/<id>` at the requested thinking level, with no selector, lease, fallback, or personal subscription;
122
+ - implement `resolveProviderCredential({ provider, model, routerId, organizationId, apiKey, clientHarness? })` to supply a per-request `{ apiKey, baseUrl?, headers?, subscription? }`, such as a tenant's own key, in place of the router's resolved key. With `subscription` (`"openai_codex"`, `"anthropic_claude_code"`, or `"meta_muse"`), `apiKey` is that plan's access token: Codex's `auth.json` `tokens.access_token`, the token `claude setup-token` prints, or the Model API key a Meta sign-in mints. The platform keeps it fresh. Strict routers serve these supplied plans, but never subscriptions the gateway resolved itself. A plan serves only its own provider's models; a Claude Code plan serves only Claude Code clients, and a Muse Code plan only Muse Code clients (recognized by their `x-client-id`);
123
+ - resolve routers with `strict: true` to serve only the requested `dari/model/<id>` at the requested thinking level, with no selector, lease, fallback, or personal subscription. Set `strict_providers: ["openai"]` on the resolved router to permit any bundled-catalog model from that provider without enumerating `enabled_models`. Use `dari/model/openai/gpt-5.5`; bare IDs and unknown models remain refused. This grant is ignored on non-strict routers and does not discover uncatalogued models;
120
124
  - read exact usage from `recordRequest`: the summary carries input, output, cache-read, and cache-write tokens and `total_cost_usd`, preferring the provider's billed cost (`cost_source: "provider"`, from OpenRouter and Vercel AI Gateway). Failed and aborted requests are recorded too;
121
125
  - attribute cost with `X-Dari-Tags: run=r1,trial=3`, which is parsed into `RequestRecord.tags` and the `recordUsage` payload's `tags`.
122
126
 
@@ -1,8 +1,12 @@
1
+ import { registeredModelPrice } from "../providers/pi.js";
1
2
  export function pricingLookupForRouter(credentials) {
2
3
  return (model) => modelPricingForRouter(credentials, model);
3
4
  }
4
5
  export function modelPricingForRouter(credentials, model) {
5
- const price = credentials.model_prices?.[model];
6
+ // A model the platform neither prices nor configures is served from Pi's
7
+ // catalog (see buildModel), so its catalog price is the one it is billed at.
8
+ const price = credentials.model_prices?.[model]
9
+ ?? (credentials.model_configs?.[model] === undefined ? registeredModelPrice(model) : null);
6
10
  if (!price || typeof price.input !== "number" || typeof price.output !== "number")
7
11
  return null;
8
12
  // Null cached rates mean the provider publishes none: bill cached prompt
@@ -1,5 +1,6 @@
1
1
  import { handleResponses, handleResponsesWebSocketFallback, } from "../ingress/codex.js";
2
2
  import { handleModels } from "../ingress/models.js";
3
+ import { handleMuseCodeModels } from "../ingress/muse_models.js";
3
4
  import { handleChatCompletions } from "../ingress/chat_completions.js";
4
5
  import { handleAnthropicMessages } from "../ingress/anthropic_messages.js";
5
6
  import { bindGatewayPlatform } from "../platform/platform.js";
@@ -19,6 +20,12 @@ export function createRequestHandler(options) {
19
20
  (url.pathname === "/healthz" || url.pathname === "/health")) {
20
21
  return Response.json({ status: "ok" });
21
22
  }
23
+ // Muse Code reads its model list from its base URL's origin; `dari muse`
24
+ // forwards it to the router's own path.
25
+ const museModels = url.pathname.match(/^(?:\/([^/]+))?\/muse-code\/models$/);
26
+ if (request.method === "GET" && museModels !== null) {
27
+ return await handleMuseCodeModels(request, museModels[1] ?? DEFAULT_ROUTER_ALIAS);
28
+ }
22
29
  // Claude Code probes this first-party endpoint before its Messages call
23
30
  // when ANTHROPIC_BASE_URL points at a compatible gateway.
24
31
  if (request.method === "HEAD" && url.pathname === "/api/hello") {
package/dist/index.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  export { createRequestHandler, type GatewayHandlerOptions } from "./http/handler.js";
2
2
  export type { GatewayPlatform, OperationalErrorContext, PendingLeaseWrite, PrefixStateWrite, ProviderCredentialRequest, RequestErrorContext, RequestRecord, RequestSummary, RequestTags, ResolveRouterRequest, SubscriptionHold, } from "./platform/platform.js";
3
+ export { DEFAULT_CATALOG_PROVIDERS, listModelCatalog, type CatalogModel, type CatalogModelPrice, type CatalogPriceTier, type ModelCatalogOptions, } from "./models/catalog.js";
3
4
  export { createAutoRoutingSelectorBackend } from "./strategies/auto_routing_selector.js";
4
5
  export { createPrioritySelectorBackend } from "./strategies/priority_selector.js";
5
6
  export { createLlmSelectorBackend, type SelectorBackend } from "./strategies/routing.js";
6
- export type { ProviderCredential, ResolvedRouter } from "./types.js";
7
+ export type { ProviderCredential, ResolvedRouter, SubscriptionKind } from "./types.js";
package/dist/index.js CHANGED
@@ -3,6 +3,7 @@
3
3
  // gateway with their own platform. This entry needs no Bun APIs; the Bun
4
4
  // server and the SQLite-backed self-hosted gateway live in `./bun`.
5
5
  export { createRequestHandler } from "./http/handler.js";
6
+ export { DEFAULT_CATALOG_PROVIDERS, listModelCatalog, } from "./models/catalog.js";
6
7
  export { createAutoRoutingSelectorBackend } from "./strategies/auto_routing_selector.js";
7
8
  export { createPrioritySelectorBackend } from "./strategies/priority_selector.js";
8
9
  export { createLlmSelectorBackend } from "./strategies/routing.js";
@@ -6,21 +6,28 @@ import { chatCompletionsToInteraction } from "../protocol/input/chat_completions
6
6
  import { handleRoutedCompletion } from "../runtime/routed_completion.js";
7
7
  import { chatCompletionsConversationIdentity, openCodeRequestContext, } from "../runtime/harness_identity/index.js";
8
8
  import { parseJsonRequest } from "./json.js";
9
+ import { museCodeClientId } from "./muse_code.js";
9
10
  export async function handleChatCompletions(request, routerId, selectorBackend) {
10
11
  try {
11
12
  const apiKey = bearerToken(request.headers);
12
13
  const payload = await parseJsonRequest(request);
13
14
  const interaction = chatCompletionsToInteraction({ ...payload, source_protocol: "openai_chat" });
14
15
  const agentContext = openCodeRequestContext(request.headers);
16
+ const museClientId = museCodeClientId(request.headers);
15
17
  return await handleRoutedCompletion({
16
18
  request,
17
19
  routerId,
18
20
  apiKey,
19
- payload: agentContext === undefined ? interaction : { ...interaction, agentContext },
21
+ payload: {
22
+ ...interaction,
23
+ ...(agentContext === undefined ? {} : { agentContext }),
24
+ ...(museClientId === undefined ? {} : { museCodeClientId: museClientId }),
25
+ },
20
26
  responseBody: chatCompletionResponse,
21
27
  streamBody: chatCompletionsSseStream,
22
28
  errorResponse,
23
29
  selectorBackend,
30
+ clientHarness: museClientId === undefined ? undefined : "muse_code",
24
31
  harnessConversationIdentity: chatCompletionsConversationIdentity(request.headers, payload.prompt_cache_key),
25
32
  });
26
33
  }
@@ -5,6 +5,7 @@ import { openAIResponsesResponse } from "../protocol/output/openai_responses.js"
5
5
  import { openAIResponsesSseStream } from "../protocol/output/openai_responses_stream.js";
6
6
  import { openAIResponsesToInteraction } from "../protocol/input/openai_responses.js";
7
7
  import { parseJsonRequest } from "./json.js";
8
+ import { museCodeClientId } from "./muse_code.js";
8
9
  import { handleRoutedCompletion } from "../runtime/routed_completion.js";
9
10
  import { codexConversationIdentity, codexRequestPurpose, codexRequestContext, } from "../runtime/harness_identity/index.js";
10
11
  export async function handleResponses(request, routerId, selectorBackend) {
@@ -14,10 +15,12 @@ export async function handleResponses(request, routerId, selectorBackend) {
14
15
  const interaction = openAIResponsesToInteraction(rawPayload);
15
16
  const requestPurpose = codexRequestPurpose(request.headers, rawPayload);
16
17
  const agentContext = codexRequestContext(request.headers, rawPayload);
18
+ const museClientId = museCodeClientId(request.headers);
17
19
  const payload = {
18
20
  ...interaction,
19
21
  ...(requestPurpose === undefined ? {} : { requestPurpose }),
20
22
  ...(agentContext === undefined ? {} : { agentContext }),
23
+ ...(museClientId === undefined ? {} : { museCodeClientId: museClientId }),
21
24
  };
22
25
  const harnessConversationIdentity = codexConversationIdentity(request.headers, rawPayload);
23
26
  // Codex applies its global model_reasoning_effort setting to custom model
@@ -35,6 +38,7 @@ export async function handleResponses(request, routerId, selectorBackend) {
35
38
  streamBody: openAIResponsesSseStream,
36
39
  errorResponse,
37
40
  selectorBackend,
41
+ clientHarness: museClientId === undefined ? undefined : "muse_code",
38
42
  harnessConversationIdentity,
39
43
  });
40
44
  }
@@ -2,11 +2,14 @@
2
2
  Codex reads the Codex catalog; Anthropic-SDK clients (Claude Desktop /
3
3
  Cowork gateway probes, Claude Code) read the Anthropic list shape. The
4
4
  handler detects the caller, authenticates in that protocol's auth shape,
5
- and selects the matching catalog adapter. */
5
+ and selects the matching catalog adapter. `?detail=1` instead returns the
6
+ model catalog (models/catalog.ts), optionally narrowed with
7
+ `provider=<id>[,<id>...]`. */
6
8
  import { anthropicToken, bearerToken } from "../http/auth.js";
7
9
  import { errorResponse } from "../http/errors.js";
8
10
  import { captureRequestError } from "../observability/errors.js";
9
11
  import { anthropicErrorResponse } from "./anthropic_messages.js";
12
+ import { listModelCatalog } from "../models/catalog.js";
10
13
  import { modelSupportsImageInput } from "../providers/model_capabilities.js";
11
14
  import { modelMetadataLookup } from "../providers/pi.js";
12
15
  import { resolveRouter } from "../strategies/routing.js";
@@ -23,6 +26,15 @@ export async function handleModels(request, routerId) {
23
26
  // Resolution authenticates the key and confirms the router exists even
24
27
  // when the response body (Anthropic shape) does not depend on it.
25
28
  const router = await resolveRouter({ routerId, apiKey });
29
+ const query = new URL(request.url).searchParams;
30
+ if (query.get("detail") === "1") {
31
+ const providers = query.get("provider")?.split(",").map((id) => id.trim()).filter(Boolean);
32
+ const data = await listModelCatalog({
33
+ ...(providers?.length ? { providers } : {}),
34
+ signal: request.signal,
35
+ });
36
+ return Response.json({ object: "list", data }, { headers: { "Cache-Control": "no-store" } });
37
+ }
26
38
  if (anthropicClient) {
27
39
  return Response.json(anthropicModelsResponse(), {
28
40
  headers: {
@@ -0,0 +1,2 @@
1
+ /** Muse Code's x-client-id when the request comes from Muse Code. */
2
+ export declare function museCodeClientId(headers: Headers): string | undefined;
@@ -0,0 +1,8 @@
1
+ // Muse Code names its surface (TUI, `muse exec`, desktop, `muse serve`) in
2
+ // x-client-id on every model request.
3
+ const MUSE_CODE_CLIENT_IDS = new Set(["tbh:tui", "tbh:exec", "tbh:desktop", "tbh:serve"]);
4
+ /** Muse Code's x-client-id when the request comes from Muse Code. */
5
+ export function museCodeClientId(headers) {
6
+ const clientId = headers.get("x-client-id")?.trim().toLowerCase();
7
+ return clientId !== undefined && MUSE_CODE_CLIENT_IDS.has(clientId) ? clientId : undefined;
8
+ }
@@ -0,0 +1,9 @@
1
+ import type { ResolvedRouter } from "../types.js";
2
+ /**
3
+ * Muse Code fetches its model list from its base URL's origin
4
+ * (`/muse-code/models`) before its first request. A list of only the router's
5
+ * models lets Muse Code pick from them and start on the first; `dari/routing`
6
+ * leads it only on the Autorouting plan.
7
+ */
8
+ export declare function museCodeModelsResponse(router: Pick<ResolvedRouter, "enabled_models" | "autorouting_enabled">): Record<string, unknown>;
9
+ export declare function handleMuseCodeModels(request: Request, routerId: string): Promise<Response>;
@@ -0,0 +1,32 @@
1
+ import { bearerToken } from "../http/auth.js";
2
+ import { errorResponse } from "../http/errors.js";
3
+ import { resolveRouter } from "../strategies/routing.js";
4
+ /**
5
+ * Muse Code fetches its model list from its base URL's origin
6
+ * (`/muse-code/models`) before its first request. A list of only the router's
7
+ * models lets Muse Code pick from them and start on the first; `dari/routing`
8
+ * leads it only on the Autorouting plan.
9
+ */
10
+ export function museCodeModelsResponse(router) {
11
+ const ids = [
12
+ ...(router.autorouting_enabled === false ? [] : ["dari/routing"]),
13
+ ...router.enabled_models.map((model) => `dari/model/${model}`),
14
+ ];
15
+ return {
16
+ data: ids.map((id, index) => ({
17
+ id,
18
+ display_label: id === "dari/routing" ? "Dari Routing" : id.slice("dari/model/".length),
19
+ is_default: index === 0,
20
+ })),
21
+ };
22
+ }
23
+ export async function handleMuseCodeModels(request, routerId) {
24
+ try {
25
+ // Only Muse Code fetches this list, so its plans apply even without a Meta key.
26
+ const router = await resolveRouter({ routerId, apiKey: bearerToken(request.headers), clientHarness: "muse_code" });
27
+ return Response.json(museCodeModelsResponse(router), { headers: { "Cache-Control": "no-store" } });
28
+ }
29
+ catch (error) {
30
+ return errorResponse(error);
31
+ }
32
+ }
@@ -161,6 +161,7 @@ export type InteractionRequest = {
161
161
  requestPurpose?: InteractionRequestPurpose;
162
162
  agentContext?: AgentRequestContext;
163
163
  claudeCodeUserAgent?: string;
164
+ museCodeClientId?: string;
164
165
  anthropicSafeguards?: AnthropicSafeguards;
165
166
  model?: string;
166
167
  instructions?: string;
@@ -0,0 +1,35 @@
1
+ import { type ReasoningEffort } from "@mupt-ai/dari-router/policy-engine";
2
+ export type CatalogModel = {
3
+ id: string;
4
+ provider: string;
5
+ native_model_id: string;
6
+ name: string;
7
+ context_window: number | null;
8
+ max_output_tokens: number | null;
9
+ input_modalities: string[];
10
+ thinking_levels: ReasoningEffort[];
11
+ price: CatalogModelPrice | null;
12
+ price_as_of: string;
13
+ };
14
+ export type CatalogModelPrice = {
15
+ input: number;
16
+ output: number;
17
+ cached_input: number | null;
18
+ cache_write: number | null;
19
+ tiers: CatalogPriceTier[];
20
+ };
21
+ export type CatalogPriceTier = {
22
+ input_tokens_above: number;
23
+ input: number;
24
+ output: number;
25
+ cached_input: number | null;
26
+ cache_write: number | null;
27
+ };
28
+ export type ModelCatalogOptions = {
29
+ providers?: readonly string[] | undefined;
30
+ fetch?: ((url: string, init?: RequestInit) => Promise<Response>) | undefined;
31
+ signal?: AbortSignal | undefined;
32
+ };
33
+ export declare const DEFAULT_CATALOG_PROVIDERS: readonly string[];
34
+ /** List the agent-capable models of each requested provider. */
35
+ export declare function listModelCatalog(options?: ModelCatalogOptions): Promise<CatalogModel[]>;
@@ -0,0 +1,232 @@
1
+ import { getSupportedThinkingLevels } from "@mupt-ai/pi-ai";
2
+ import { getBuiltinModelDataGeneratedAt, getBuiltinModels, getBuiltinProviders, } from "@mupt-ai/pi-ai/providers/all";
3
+ import { REASONING_EFFORTS, isReasoningEffort, isRecord, } from "@mupt-ai/dari-router/policy-engine";
4
+ import { HttpError } from "../http/errors.js";
5
+ export const DEFAULT_CATALOG_PROVIDERS = [
6
+ "openai",
7
+ "anthropic",
8
+ "meta",
9
+ "xai",
10
+ "fireworks",
11
+ "openrouter",
12
+ "vercel-ai-gateway",
13
+ ];
14
+ const LIVE_LISTINGS = {
15
+ openrouter: { url: "https://openrouter.ai/api/v1/models", models: openRouterModels },
16
+ "vercel-ai-gateway": { url: "https://ai-gateway.vercel.sh/v1/models", models: vercelModels },
17
+ };
18
+ /** List the agent-capable models of each requested provider. */
19
+ export async function listModelCatalog(options = {}) {
20
+ const providers = [...new Set(options.providers ?? DEFAULT_CATALOG_PROVIDERS)];
21
+ const builtin = new Set(getBuiltinProviders());
22
+ for (const provider of providers) {
23
+ if (LIVE_LISTINGS[provider] === undefined && !builtin.has(provider)) {
24
+ throw new HttpError(400, `Unknown catalog provider '${provider}'.`, "unknown_provider", "provider");
25
+ }
26
+ }
27
+ const lists = await Promise.all(providers.map(async (provider) => {
28
+ const listing = LIVE_LISTINGS[provider];
29
+ return listing === undefined
30
+ ? piModels(provider)
31
+ : listing.models(await fetchListing(listing.url, options), new Date().toISOString());
32
+ }));
33
+ return lists.flat();
34
+ }
35
+ async function fetchListing(url, options) {
36
+ let status;
37
+ let body = null;
38
+ // Network errors, aborts, and unparseable bodies are upstream failures
39
+ // too: all of them surface as catalog_unavailable rather than a raw 500.
40
+ try {
41
+ const response = await (options.fetch ?? fetch)(url, {
42
+ headers: { accept: "application/json" },
43
+ ...(options.signal ? { signal: options.signal } : {}),
44
+ });
45
+ status = response.status;
46
+ body = response.ok ? await response.json() : null;
47
+ }
48
+ catch {
49
+ // Leaves body null.
50
+ }
51
+ if (!isRecord(body) || !Array.isArray(body.data)) {
52
+ const detail = status === undefined ? "could not be reached" : `returned no model list (HTTP ${status})`;
53
+ throw new HttpError(502, `${url} ${detail}.`, "catalog_unavailable");
54
+ }
55
+ return body.data;
56
+ }
57
+ function piModels(provider) {
58
+ const generatedAt = getBuiltinModelDataGeneratedAt();
59
+ const asOf = generatedAt === undefined ? "" : new Date(generatedAt).toISOString();
60
+ const models = getBuiltinModels(provider);
61
+ return models.map((model) => ({
62
+ id: `${provider}/${model.id}`,
63
+ provider,
64
+ native_model_id: model.id,
65
+ name: model.name,
66
+ context_window: model.contextWindow,
67
+ max_output_tokens: model.maxTokens,
68
+ input_modalities: [...model.input],
69
+ thinking_levels: orderedEfforts(getSupportedThinkingLevels(model)),
70
+ price: piPrice(model.cost),
71
+ price_as_of: asOf,
72
+ }));
73
+ }
74
+ function piPrice(cost) {
75
+ return {
76
+ input: cost.input,
77
+ output: cost.output,
78
+ cached_input: cost.cacheRead,
79
+ cache_write: cost.cacheWrite,
80
+ tiers: (cost.tiers ?? []).map((tier) => ({
81
+ input_tokens_above: tier.inputTokensAbove,
82
+ input: tier.input,
83
+ output: tier.output,
84
+ cached_input: tier.cacheRead,
85
+ cache_write: tier.cacheWrite,
86
+ })),
87
+ };
88
+ }
89
+ // https://openrouter.ai/docs/api/api-reference/models/get-models
90
+ function openRouterModels(data, asOf) {
91
+ return data.filter(isRecord).flatMap((model) => {
92
+ const architecture = isRecord(model.architecture) ? model.architecture : {};
93
+ const parameters = stringList(model.supported_parameters);
94
+ if (typeof model.id !== "string" || !parameters.includes("tools"))
95
+ return [];
96
+ if (!stringList(architecture.output_modalities).includes("text"))
97
+ return [];
98
+ const reasoning = isRecord(model.reasoning) ? model.reasoning : {};
99
+ const topProvider = isRecord(model.top_provider) ? model.top_provider : {};
100
+ return [{
101
+ id: `openrouter/${model.id}`,
102
+ provider: "openrouter",
103
+ native_model_id: model.id,
104
+ name: typeof model.name === "string" ? model.name : model.id,
105
+ context_window: positiveInteger(model.context_length),
106
+ max_output_tokens: positiveInteger(topProvider.max_completion_tokens),
107
+ input_modalities: stringList(architecture.input_modalities),
108
+ thinking_levels: gatewayThinkingLevels({
109
+ reasons: parameters.includes("reasoning"),
110
+ efforts: Array.isArray(reasoning.supported_efforts) ? stringList(reasoning.supported_efforts) : null,
111
+ canDisable: reasoning.mandatory !== true,
112
+ }),
113
+ price: isRecord(model.pricing) ? openRouterPrice(model.pricing) : null,
114
+ price_as_of: asOf,
115
+ }];
116
+ });
117
+ }
118
+ function openRouterPrice(pricing) {
119
+ const base = rates(pricing, "prompt", "completion");
120
+ if (base === null)
121
+ return null;
122
+ // Overrides restate only the rates they change; time-of-day overrides are
123
+ // not context tiers.
124
+ const tiers = (Array.isArray(pricing.overrides) ? pricing.overrides : [])
125
+ .filter(isRecord)
126
+ .flatMap((override) => {
127
+ const threshold = positiveInteger(override.min_prompt_tokens);
128
+ if (threshold === null || "utc_days" in override || "utc_start" in override || "utc_end" in override) {
129
+ return [];
130
+ }
131
+ const tier = rates({ ...pricing, ...override }, "prompt", "completion", override);
132
+ return tier === null ? [] : [{ input_tokens_above: threshold, ...tier }];
133
+ });
134
+ return { ...base, tiers };
135
+ }
136
+ // https://vercel.com/docs/ai-gateway/models-and-providers
137
+ function vercelModels(data, asOf) {
138
+ return data.filter(isRecord).flatMap((model) => {
139
+ const modalities = isRecord(model.modalities) ? model.modalities : {};
140
+ const parameters = stringList(model.supported_parameters);
141
+ if (typeof model.id !== "string" || model.type !== "language" || !parameters.includes("tools"))
142
+ return [];
143
+ if (!stringList(modalities.output).includes("text"))
144
+ return [];
145
+ const options = Array.isArray(model.reasoning_options) ? model.reasoning_options.filter(isRecord) : [];
146
+ const effort = options.find((option) => option.type === "effort");
147
+ return [{
148
+ id: `vercel-ai-gateway/${model.id}`,
149
+ provider: "vercel-ai-gateway",
150
+ native_model_id: model.id,
151
+ name: typeof model.name === "string" ? model.name : model.id,
152
+ context_window: positiveInteger(model.context_window),
153
+ max_output_tokens: positiveInteger(model.max_tokens),
154
+ input_modalities: stringList(modalities.input),
155
+ thinking_levels: gatewayThinkingLevels({
156
+ reasons: parameters.includes("reasoning") || options.length > 0,
157
+ efforts: effort === undefined ? null : stringList(effort.values),
158
+ canDisable: options.length === 0 || options.some((option) => option.type === "toggle"),
159
+ }),
160
+ price: isRecord(model.pricing) ? vercelPrice(model.pricing) : null,
161
+ price_as_of: asOf,
162
+ }];
163
+ });
164
+ }
165
+ function vercelPrice(pricing) {
166
+ const base = rates(pricing, "input", "output");
167
+ if (base === null)
168
+ return null;
169
+ // Each rate lists its own {cost, min, max} ranges; a range covers prompts of
170
+ // at least `min` tokens, so it is the tier above `min - 1`.
171
+ const ranged = new Map();
172
+ for (const key of ["input", "output", "input_cache_read", "input_cache_write"]) {
173
+ const ranges = Array.isArray(pricing[`${key}_tiers`]) ? pricing[`${key}_tiers`] : [];
174
+ for (const range of ranges.filter(isRecord)) {
175
+ const start = positiveInteger(range.min);
176
+ if (start === null || range.cost == null)
177
+ continue;
178
+ ranged.set(start - 1, { ...ranged.get(start - 1), [key]: range.cost });
179
+ }
180
+ }
181
+ const tiers = [...ranged.entries()]
182
+ .sort(([left], [right]) => left - right)
183
+ .flatMap(([threshold, stated]) => {
184
+ const tier = rates({ ...pricing, ...stated }, "input", "output", stated);
185
+ return tier === null ? [] : [{ input_tokens_above: threshold, ...tier }];
186
+ });
187
+ return { ...base, tiers };
188
+ }
189
+ // Base rates from per-token strings. A tier inherits input and output from
190
+ // the base price but states its own cache rates, or has none.
191
+ function rates(pricing, inputKey, outputKey, cacheRates = pricing) {
192
+ const input = perMillion(pricing[inputKey]);
193
+ const output = perMillion(pricing[outputKey]);
194
+ // Meta-routers with variable prices list negative rates.
195
+ if (input === null || output === null)
196
+ return null;
197
+ return {
198
+ input,
199
+ output,
200
+ cached_input: perMillion(cacheRates.input_cache_read),
201
+ cache_write: perMillion(cacheRates.input_cache_write),
202
+ };
203
+ }
204
+ // USD per token as a decimal string -> USD per million tokens. Shifting the
205
+ // exponent in the string keeps "0.000001" exactly 1.
206
+ function perMillion(value) {
207
+ const rate = typeof value === "string" ? Number(`${value.trim()}e6`)
208
+ : typeof value === "number" ? value * 1_000_000
209
+ : Number.NaN;
210
+ return Number.isFinite(rate) && rate >= 0 ? rate : null;
211
+ }
212
+ // Gateways forward `reasoning.effort`; models that reason without a stated
213
+ // effort list take the standard low/medium/high levels.
214
+ function gatewayThinkingLevels(args) {
215
+ if (!args.reasons)
216
+ return ["off"];
217
+ const efforts = args.efforts ?? ["low", "medium", "high"];
218
+ return orderedEfforts([
219
+ ...efforts.map((effort) => effort === "none" ? "off" : effort),
220
+ ...(args.canDisable ? ["off"] : []),
221
+ ]);
222
+ }
223
+ function orderedEfforts(levels) {
224
+ const accepted = new Set(levels.filter(isReasoningEffort));
225
+ return REASONING_EFFORTS.filter((effort) => accepted.has(effort));
226
+ }
227
+ function stringList(value) {
228
+ return Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
229
+ }
230
+ function positiveInteger(value) {
231
+ return typeof value === "number" && Number.isInteger(value) && value > 0 ? value : null;
232
+ }
@@ -2,7 +2,8 @@ import type { ProviderKeySource, ResolvedRouter } from "../types.js";
2
2
  /** Actual serving provider for a canonical model on a resolved managed router. */
3
3
  export declare function executionProviderForModel(credentials: ResolvedRouter, model: string): string;
4
4
  /**
5
- * The key that serves a model on a resolved router. Missing declarations are
5
+ * The key that serves a model on a resolved router: a platform-supplied
6
+ * credential first, then the resolved provider key. Missing declarations are
6
7
  * user-owned so unknown platform data cannot trigger managed-key billing.
7
8
  */
8
9
  export declare function keySourceForModel(credentials: ResolvedRouter, model: string): ProviderKeySource;
@@ -5,9 +5,13 @@ export function executionProviderForModel(credentials, model) {
5
5
  return configured || providerForModel(model);
6
6
  }
7
7
  /**
8
- * The key that serves a model on a resolved router. Missing declarations are
8
+ * The key that serves a model on a resolved router: a platform-supplied
9
+ * credential first, then the resolved provider key. Missing declarations are
9
10
  * user-owned so unknown platform data cannot trigger managed-key billing.
10
11
  */
11
12
  export function keySourceForModel(credentials, model) {
13
+ const supplied = credentials.provider_credentials?.[model];
14
+ if (supplied)
15
+ return supplied.subscription ? "subscription" : "user";
12
16
  return credentials.provider_key_sources?.[executionProviderForModel(credentials, model)] ?? "user";
13
17
  }
@@ -28,6 +28,9 @@ const PROVIDER_BASE_URLS = {
28
28
  anthropic: "https://api.anthropic.com",
29
29
  fireworks: "https://api.fireworks.ai/inference/v1",
30
30
  xai: "https://api.x.ai/v1",
31
+ // Meta's Model API serves OpenAI Responses and Chat Completions here, and
32
+ // Anthropic Messages at https://api.meta.ai.
33
+ meta: "https://api.meta.ai/v1",
31
34
  // Pi-ai derives the effective AWS region from the endpoint unless the
32
35
  // model config overrides base_url or the native id is an inference profile
33
36
  // ARN, so this default only pins us-east-1 when nothing else is configured.
@@ -1,3 +1,6 @@
1
+ export declare function providerThinkingLevelOverrides(provider: string): Record<string, string> | undefined;
2
+ /** Registered Pi metadata with the serving provider's thinking level overrides. */
3
+ export declare function withThinkingLevelOverrides(provider: string, registered: Record<string, unknown> | undefined): Record<string, unknown> | undefined;
1
4
  export declare function offRequiresWireNone(modelId: string): boolean;
2
5
  export declare function contextWindowOverride(modelId: string): number | undefined;
3
6
  export declare function inputModalitiesOverride(modelId: string): string[] | undefined;
@@ -161,6 +161,23 @@ const MODEL_OFF_WIRE_NONE = new Set([
161
161
  "deepseek-ai/DeepSeek-V4-Flash-0731",
162
162
  "deepseek-ai/DeepSeek-V4.1-Flash-1017",
163
163
  ]);
164
+ // Wire values a provider's API accepts for thinking levels the Pi registry
165
+ // leaves unmapped. Meta's Model API takes reasoning effort "none" on every
166
+ // Muse model (dev.meta.ai/docs/coding-agents).
167
+ const PROVIDER_THINKING_LEVEL_OVERRIDES = {
168
+ meta: { off: "none" },
169
+ };
170
+ export function providerThinkingLevelOverrides(provider) {
171
+ return PROVIDER_THINKING_LEVEL_OVERRIDES[provider];
172
+ }
173
+ /** Registered Pi metadata with the serving provider's thinking level overrides. */
174
+ export function withThinkingLevelOverrides(provider, registered) {
175
+ const override = providerThinkingLevelOverrides(provider);
176
+ if (registered === undefined || override === undefined)
177
+ return registered;
178
+ const map = isRecord(registered.thinkingLevelMap) ? registered.thinkingLevelMap : {};
179
+ return { ...registered, thinkingLevelMap: { ...map, ...override } };
180
+ }
164
181
  export function offRequiresWireNone(modelId) {
165
182
  return MODEL_OFF_WIRE_NONE.has(canonicalModelId(modelId));
166
183
  }
@@ -0,0 +1,8 @@
1
+ import { type RouterModelPrice } from "@mupt-ai/dari-router/policy-engine";
2
+ import type { ClientHarness, ResolvedRouterModelConfig, SubscriptionKind } from "../types.js";
3
+ /**
4
+ * Whether a plan may serve a model for this client: each plan serves only its
5
+ * own provider's models, and only to the client its provider allows.
6
+ */
7
+ export declare function subscriptionServes(kind: SubscriptionKind, model: string, clientHarness: ClientHarness | undefined): boolean;
8
+ export declare function codexModelConfig(modelId: string, price: RouterModelPrice | null): ResolvedRouterModelConfig;
@@ -0,0 +1,37 @@
1
+ import { nativeModelId, providerForModel, } from "@mupt-ai/dari-router/policy-engine";
2
+ const CODEX_BACKEND_URL = "https://chatgpt.com/backend-api";
3
+ // The model provider whose models each kind of plan serves.
4
+ const SUBSCRIPTION_MODEL_PROVIDERS = {
5
+ openai_codex: "openai",
6
+ anthropic_claude_code: "anthropic",
7
+ meta_muse: "meta",
8
+ };
9
+ // The client each plan is limited to, as its provider limits it: a Claude Code
10
+ // plan serves only Claude Code, and a Muse Code plan only Muse Code.
11
+ const SUBSCRIPTION_CLIENTS = {
12
+ anthropic_claude_code: "claude_code",
13
+ meta_muse: "muse_code",
14
+ };
15
+ /**
16
+ * Whether a plan may serve a model for this client: each plan serves only its
17
+ * own provider's models, and only to the client its provider allows.
18
+ */
19
+ export function subscriptionServes(kind, model, clientHarness) {
20
+ if (SUBSCRIPTION_MODEL_PROVIDERS[kind] !== providerForModel(model))
21
+ return false;
22
+ const client = SUBSCRIPTION_CLIENTS[kind];
23
+ return client === undefined || clientHarness === client;
24
+ }
25
+ // A ChatGPT plan serves OpenAI models as the `openai-codex` provider through
26
+ // the Codex backend.
27
+ export function codexModelConfig(modelId, price) {
28
+ return {
29
+ provider: "openai-codex",
30
+ native_model_id: nativeModelId(modelId),
31
+ api: "openai-codex-responses",
32
+ base_url: CODEX_BACKEND_URL,
33
+ // A plan bills no tokens; the catalog price only feeds cost estimates.
34
+ cost: price ?? { input: 0, output: 0 },
35
+ fast_mode: false,
36
+ };
37
+ }