@mupt-ai/dari-gateway 0.3.0 → 0.3.2

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 (110) hide show
  1. package/README.md +14 -6
  2. package/dari-gateway.example.yaml +3 -2
  3. package/dist/http/auth.js +2 -2
  4. package/dist/http/http2_server.d.ts +1 -1
  5. package/dist/http/http2_server.js +3 -3
  6. package/dist/http/serve.d.ts +1 -0
  7. package/dist/http/serve.js +5 -4
  8. package/dist/ingress/chat_completions.js +2 -2
  9. package/dist/ingress/codex.js +3 -1
  10. package/dist/ingress/codex_models.d.ts +1 -1
  11. package/dist/ingress/codex_models.js +22 -13
  12. package/dist/platform/platform.d.ts +13 -3
  13. package/dist/protocol/input/openai_responses.js +2 -4
  14. package/dist/protocol/output/anthropic_messages_output.d.ts +2 -2
  15. package/dist/protocol/output/anthropic_messages_output.js +1 -0
  16. package/dist/protocol/output/openai_responses_stream.js +19 -1
  17. package/dist/providers/execution.d.ts +0 -5
  18. package/dist/providers/execution.js +5 -8
  19. package/dist/providers/pi_output.d.ts +4 -0
  20. package/dist/providers/pi_output.js +1 -1
  21. package/dist/providers/provider_attempt_runner.d.ts +2 -3
  22. package/dist/providers/provider_attempt_runner.js +14 -24
  23. package/dist/providers/provider_call.d.ts +3 -3
  24. package/dist/providers/provider_call.js +1 -0
  25. package/dist/providers/provider_reported_cost.js +17 -2
  26. package/dist/providers/stream_finalization.d.ts +0 -1
  27. package/dist/providers/stream_finalization.js +0 -1
  28. package/dist/runtime/claude_code/auxiliary_routing.js +1 -9
  29. package/dist/runtime/explicit_model.js +1 -1
  30. package/dist/runtime/harness_identity/conversation.d.ts +5 -0
  31. package/dist/runtime/harness_identity/conversation.js +6 -0
  32. package/dist/runtime/harness_identity/extract.d.ts +1 -0
  33. package/dist/runtime/harness_identity/extract.js +13 -5
  34. package/dist/runtime/harness_identity/index.d.ts +2 -2
  35. package/dist/runtime/harness_identity/index.js +2 -2
  36. package/dist/runtime/prepare_routed_completion.d.ts +5 -1
  37. package/dist/runtime/prepare_routed_completion.js +26 -2
  38. package/dist/runtime/routed_runtime.js +33 -53
  39. package/dist/runtime/routing_lease.d.ts +4 -6
  40. package/dist/runtime/routing_lease.js +2 -47
  41. package/dist/runtime/runtime_reporter.d.ts +0 -2
  42. package/dist/runtime/runtime_reporter.js +0 -1
  43. package/dist/runtime/session_pin.d.ts +7 -0
  44. package/dist/runtime/session_pin.js +51 -0
  45. package/dist/self_host/config.d.ts +2 -3
  46. package/dist/self_host/config.js +1 -2
  47. package/dist/self_host/main.d.ts +2 -1
  48. package/dist/self_host/main.js +20 -4
  49. package/dist/self_host/platform.d.ts +4 -2
  50. package/dist/self_host/platform.js +6 -2
  51. package/dist/self_host/resolve.d.ts +8 -1
  52. package/dist/self_host/resolve.js +31 -8
  53. package/dist/self_host/session_pin_store.d.ts +14 -0
  54. package/dist/self_host/session_pin_store.js +44 -0
  55. package/dist/strategies/priority_selector.d.ts +1 -2
  56. package/dist/strategies/priority_selector.js +1 -2
  57. package/dist/strategies/route_decision.d.ts +0 -1
  58. package/dist/strategies/route_decision.js +0 -3
  59. package/dist/strategies/routing.d.ts +0 -3
  60. package/dist/strategies/routing.js +1 -14
  61. package/dist/telemetry/billing_finalization.d.ts +1 -2
  62. package/dist/telemetry/billing_finalization.js +1 -1
  63. package/dist/telemetry/prefix_state.d.ts +0 -1
  64. package/dist/telemetry/prefix_state.js +1 -3
  65. package/dist/telemetry/report.d.ts +0 -2
  66. package/dist/telemetry/report.js +5 -41
  67. package/dist/types.d.ts +5 -3
  68. package/package.json +2 -2
  69. package/src/http/auth.ts +2 -2
  70. package/src/http/http2_server.ts +7 -3
  71. package/src/http/serve.ts +6 -5
  72. package/src/ingress/chat_completions.ts +2 -1
  73. package/src/ingress/codex.ts +4 -1
  74. package/src/ingress/codex_models.ts +18 -7
  75. package/src/platform/platform.ts +16 -2
  76. package/src/protocol/input/openai_responses.ts +2 -9
  77. package/src/protocol/output/anthropic_messages_output.ts +3 -2
  78. package/src/protocol/output/openai_responses_stream.ts +17 -1
  79. package/src/providers/execution.ts +5 -15
  80. package/src/providers/pi_output.ts +1 -1
  81. package/src/providers/provider_attempt_runner.ts +10 -21
  82. package/src/providers/provider_call.ts +3 -1
  83. package/src/providers/provider_reported_cost.ts +16 -2
  84. package/src/providers/stream_finalization.ts +0 -2
  85. package/src/providers/subscription_limits.ts +1 -1
  86. package/src/runtime/claude_code/auxiliary_routing.ts +1 -8
  87. package/src/runtime/explicit_model.ts +1 -1
  88. package/src/runtime/harness_identity/conversation.ts +11 -0
  89. package/src/runtime/harness_identity/extract.ts +14 -5
  90. package/src/runtime/harness_identity/index.ts +2 -0
  91. package/src/runtime/prepare_routed_completion.ts +32 -2
  92. package/src/runtime/routed_runtime.ts +39 -59
  93. package/src/runtime/routing_lease.ts +2 -56
  94. package/src/runtime/runtime_reporter.ts +0 -3
  95. package/src/runtime/session_pin.ts +61 -0
  96. package/src/self_host/config.ts +3 -6
  97. package/src/self_host/main.ts +23 -4
  98. package/src/self_host/platform.ts +8 -2
  99. package/src/self_host/resolve.ts +37 -7
  100. package/src/self_host/session_pin_store.ts +58 -0
  101. package/src/strategies/priority_selector.ts +1 -2
  102. package/src/strategies/route_decision.ts +0 -5
  103. package/src/strategies/routing.ts +1 -17
  104. package/src/telemetry/billing_finalization.ts +4 -3
  105. package/src/telemetry/prefix_state.ts +1 -6
  106. package/src/telemetry/report.ts +4 -48
  107. package/src/types.ts +12 -6
  108. package/dist/strategies/fallback_decision.d.ts +0 -8
  109. package/dist/strategies/fallback_decision.js +0 -17
  110. package/src/strategies/fallback_decision.ts +0 -30
@@ -16,7 +16,7 @@ import { routingBillingProviderAttemptFacts, } from "./billing_finalization.js";
16
16
  import { estimateBenchmarkReplay } from "./benchmark_replay.js";
17
17
  import { extractInteractionObservability } from "./requests/interaction_observability.js";
18
18
  import { costString, finiteNumber, isRecord, tokenCount, } from "./ingest_values.js";
19
- import { buildCompletedPrefixHit, leaseCountdownForPrefix, prefixEntryForIngest, reportPendingLease, reportPrefixState, } from "./prefix_state.js";
19
+ import { buildCompletedPrefixHit, leaseCountdownForPrefix, prefixEntryForIngest, reportPrefixState, } from "./prefix_state.js";
20
20
  import { subscriptionUsageLimitFromFailure } from "../providers/subscription_limits.js";
21
21
  import { scrubSecrets } from "./scrub.js";
22
22
  import { encodeRetainedInteractionRequest } from "./requests/retained_interaction_request.js";
@@ -50,7 +50,6 @@ export class RequestReporter {
50
50
  selectorOutputAccepted = false;
51
51
  decision = null;
52
52
  nextDecision = null;
53
- fallbackDecision = null;
54
53
  providerAttempts = [];
55
54
  billingProviderAttempts = [];
56
55
  decisionDetails = {};
@@ -66,7 +65,6 @@ export class RequestReporter {
66
65
  this.selectorOutputAccepted = true;
67
66
  this.decision = run.decision;
68
67
  this.nextDecision = run.decision;
69
- this.fallbackDecision = run.fallbackDecision ?? null;
70
68
  }
71
69
  attachSpeculativeSelectorRun(run, servingDecision) {
72
70
  this.selectorRun = run;
@@ -74,7 +72,6 @@ export class RequestReporter {
74
72
  this.selectorOutputAccepted = true;
75
73
  this.decision = servingDecision;
76
74
  this.nextDecision = run.decision;
77
- this.fallbackDecision = run.fallbackDecision ?? null;
78
75
  }
79
76
  // Speculative execution can begin before a selector run; its serving
80
77
  // decision drives selected-model attribution, prefix entries, and errors.
@@ -82,9 +79,6 @@ export class RequestReporter {
82
79
  this.decision = decision;
83
80
  this.nextDecision = decision;
84
81
  }
85
- attachFallbackDecision(decision) {
86
- this.fallbackDecision = decision ?? null;
87
- }
88
82
  attachServingDecision(decision) {
89
83
  this.decision = decision;
90
84
  }
@@ -96,10 +90,10 @@ export class RequestReporter {
96
90
  modelCost(attempt) {
97
91
  return modelCost(attempt ? [...this.billingProviderAttempts, this.billingFacts(attempt)] : this.billingProviderAttempts);
98
92
  }
99
- billingFacts({ execution, message, role, attempt, billingOutcome }) {
93
+ billingFacts({ execution, message, attempt, billingOutcome }) {
100
94
  return routingBillingProviderAttemptFacts({
101
95
  provider: execution.provider, model: execution.model,
102
- providerKeySource: execution.providerKeySource, role, attempt, outcome: billingOutcome,
96
+ providerKeySource: execution.providerKeySource, attempt, outcome: billingOutcome,
103
97
  providerMessage: message, usage: isRecord(message.usage) ? message.usage : null,
104
98
  providerPricingKnown: this.pricingKnown(execution.model),
105
99
  });
@@ -224,16 +218,8 @@ export class RequestReporter {
224
218
  const decision = this.decision;
225
219
  const selectedModel = outcome.providerExecution?.model ?? decision?.selectedModel ?? null;
226
220
  const reasoningBucket = decision?.reasoningEffort ?? null;
227
- // A completed fallback attempt means the intended pair failed this
228
- // turn: the next-turn recommendation and any lease countdown follow the
229
- // pair that actually served instead of re-pinning the failed one.
230
- const fallbackServed = this.providerAttempts.some((attempt) => attempt.role === "fallback" && attempt.outcome === "completed");
231
- const nextModel = fallbackServed
232
- ? selectedModel
233
- : this.nextDecision?.selectedModel ?? selectedModel;
234
- const nextReasoningEffort = fallbackServed
235
- ? reasoningBucket
236
- : this.nextDecision?.reasoningEffort ?? reasoningBucket;
221
+ const nextModel = this.nextDecision?.selectedModel ?? selectedModel;
222
+ const nextReasoningEffort = this.nextDecision?.reasoningEffort ?? reasoningBucket;
237
223
  const selectedProvider = outcome.providerExecution?.provider ?? (selectedModel
238
224
  ? executionProviderForModel(a.credentials, selectedModel)
239
225
  : null);
@@ -292,7 +278,6 @@ export class RequestReporter {
292
278
  leaseTurnsRemaining: leaseCountdownForPrefix({
293
279
  served: decision ?? null,
294
280
  next: this.nextDecision,
295
- fallbackServed,
296
281
  }),
297
282
  reason: this.nextDecision?.reason ?? decision?.reason ?? null,
298
283
  chain: selectedChain,
@@ -324,19 +309,6 @@ export class RequestReporter {
324
309
  // resolves this turn's conversation id and next-model prediction.
325
310
  // The full report below re-applies the same state idempotently, so a
326
311
  // lost or timed-out fast write only degrades to the old ordering.
327
- // An ordinary fallback breaks any lease commitment, prefetched ones
328
- // included. A lease-aware fallback transfers the active countdown to
329
- // the serving pair, so only clear pending state when no countdown was
330
- // transferred. The depth fence orders the clear safely against
331
- // in-flight prefetch writes.
332
- if (!backgroundRequest && fallbackServed && decision?.leaseTurnsRemaining === undefined) {
333
- trackBackgroundTelemetry(reportPendingLease({
334
- routerId: a.routerId,
335
- conversationId: a.conversationId,
336
- decidedAtDepth: a.leaseDepth,
337
- lease: null,
338
- }));
339
- }
340
312
  if (prefixEntry !== null || touchedHash !== null) {
341
313
  this.prefixStatePromise = reportPrefixState({
342
314
  organization_id: a.credentials.organization_id,
@@ -404,12 +376,6 @@ export class RequestReporter {
404
376
  strategy: a.strategy,
405
377
  selected_reasoning_effort: reasoningBucket,
406
378
  next_reasoning_effort: nextReasoningEffort,
407
- ...(this.fallbackDecision
408
- ? {
409
- fallback_model: this.fallbackDecision.selectedModel,
410
- fallback_reasoning_effort: this.fallbackDecision.reasoningEffort,
411
- }
412
- : {}),
413
379
  ...(this.providerAttempts.length > 0
414
380
  ? { provider_attempts: this.providerAttempts }
415
381
  : {}),
@@ -466,8 +432,6 @@ export class RequestReporter {
466
432
  selected_reasoning_effort: reasoningBucket,
467
433
  next_model: nextModel,
468
434
  next_reasoning_effort: nextReasoningEffort,
469
- fallback_model: this.fallbackDecision?.selectedModel ?? null,
470
- fallback_reasoning_effort: this.fallbackDecision?.reasoningEffort ?? null,
471
435
  provider_key_source: keySource,
472
436
  message_count: observability.messageCount,
473
437
  tool_count: observability.metadata.declared_tool_counts.total,
package/dist/types.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { CustomRouterConfig, Provider, ReasoningEffort, RouterEval, RouterEvalScore, RouterModelPrice, RouterPrefixHit, RoutingStrategy } from "@mupt-ai/dari-router/policy-engine";
1
+ import type { CustomRouterConfig, Provider, ReasoningEffort, RouterEval, RouterEvalScore, RouterModelPrice, RouterPrefixHit, RoutingDecision, RoutingStrategy } from "@mupt-ai/dari-router/policy-engine";
2
2
  export type ProviderKeySource = "user" | "managed" | "subscription";
3
3
  export type BearerProviderAuth = {
4
4
  type: "bearer";
@@ -41,6 +41,9 @@ export type PublicEvalCatalogCard = Omit<RouterEval, "scores"> & {
41
41
  updated_at: string | null;
42
42
  scores: PublicEvalCatalogScore[];
43
43
  };
44
+ export type ServedDecision = RoutingDecision & {
45
+ sessionPinned?: boolean;
46
+ };
44
47
  export type ClientHarness = "claude_code" | "muse_code";
45
48
  export type ResolvedRouterModelConfig = {
46
49
  provider: string;
@@ -74,11 +77,10 @@ export type ResolvedRouter = {
74
77
  zdr?: boolean;
75
78
  allow_long_context?: boolean;
76
79
  speculative_routing?: boolean;
80
+ session_pinning?: boolean;
77
81
  primary_retries?: number;
78
82
  strict?: boolean;
79
83
  strict_providers?: string[];
80
- model_fallback_enabled?: boolean;
81
- fallback_requires_different_provider?: boolean;
82
84
  evals?: RouterEval[];
83
85
  eval_score_imputation?: boolean;
84
86
  image_capability_filtering?: boolean;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mupt-ai/dari-gateway",
3
- "version": "0.3.0",
3
+ "version": "0.3.2",
4
4
  "description": "Self-hostable LLM gateway for coding agents and APIs: one endpoint for Anthropic Messages, OpenAI Chat Completions, and OpenAI Responses, with subscription pooling, failover, and prompt-cache-aware routing.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -50,7 +50,7 @@
50
50
  "dependencies": {
51
51
  "@aws-sdk/client-sts": "3.1063.0",
52
52
  "@mupt-ai/dari-router": "0.2.5",
53
- "@mupt-ai/pi-ai": "1.4.0-mupt.1"
53
+ "@mupt-ai/pi-ai": "1.4.0-mupt.2"
54
54
  },
55
55
  "devDependencies": {
56
56
  "@types/bun": "^1.3.4",
package/src/http/auth.ts CHANGED
@@ -6,7 +6,7 @@ export function bearerToken(headers: Headers): string {
6
6
  const value = headers.get("authorization") ?? "";
7
7
  const [scheme, token] = value.split(/\s+/, 2);
8
8
  if (scheme?.toLowerCase() !== "bearer" || !token) {
9
- throw new HttpError(401, "Missing or invalid routing API key.", "authentication_error");
9
+ throw new HttpError(401, "Missing or invalid API key.", "authentication_error");
10
10
  }
11
11
  return publicRoutingToken(token);
12
12
  }
@@ -16,7 +16,7 @@ export function anthropicToken(headers: Headers): string {
16
16
  if (authorization) return bearerToken(headers);
17
17
  const apiKey = headers.get("x-api-key");
18
18
  if (!apiKey) {
19
- throw new HttpError(401, "Missing or invalid routing API key.", "authentication_error");
19
+ throw new HttpError(401, "Missing or invalid API key.", "authentication_error");
20
20
  }
21
21
  return publicRoutingToken(apiKey);
22
22
  }
@@ -23,7 +23,11 @@ const HTTP2_FORBIDDEN_RESPONSE_HEADERS = new Set([
23
23
  "upgrade",
24
24
  ]);
25
25
 
26
- export function startHttp2Server(port: number, handler: Http2RequestHandler): Http2RoutingServer {
26
+ export function startHttp2Server(
27
+ port: number,
28
+ handler: Http2RequestHandler,
29
+ hostname = "0.0.0.0",
30
+ ): Http2RoutingServer {
27
31
  const sessions = new Set<ServerHttp2Session>();
28
32
  let closePromise: Promise<void> | undefined;
29
33
  const server = createServer((request, response) => {
@@ -33,10 +37,10 @@ export function startHttp2Server(port: number, handler: Http2RequestHandler): Ht
33
37
  sessions.add(session);
34
38
  session.once("close", () => sessions.delete(session));
35
39
  });
36
- server.listen(port, "0.0.0.0");
40
+ server.listen(port, hostname);
37
41
  const address = server.address();
38
42
  if (address === null || typeof address === "string") {
39
- throw new Error("HTTP/2 routing server did not bind to a TCP port.");
43
+ throw new Error("HTTP/2 gateway server did not bind to a TCP port.");
40
44
  }
41
45
  return {
42
46
  port: address.port,
package/src/http/serve.ts CHANGED
@@ -16,10 +16,10 @@ type StoppableServer = {
16
16
  export function startServer(
17
17
  listenPort: number,
18
18
  handler: (request: Request) => Promise<Response>,
19
- options: { http2?: boolean } = {},
19
+ options: { http2?: boolean; hostname?: string } = {},
20
20
  ) {
21
21
  if (options.http2) {
22
- return startHttp2Server(listenPort, handler);
22
+ return startHttp2Server(listenPort, handler, options.hostname);
23
23
  }
24
24
 
25
25
  // The selector runs before the first response byte, and routed models can
@@ -27,6 +27,7 @@ export function startServer(
27
27
  // clients and infrastructure still bound total request lifetime.
28
28
  return Bun.serve({
29
29
  port: listenPort,
30
+ hostname: options.hostname,
30
31
  fetch: handler,
31
32
  idleTimeout: 0,
32
33
  });
@@ -45,7 +46,7 @@ export function installShutdownHandlers(
45
46
  if (shuttingDown) return;
46
47
  shuttingDown = true;
47
48
  const workDeadlineAtMs = performance.now() + SHUTDOWN_WORK_TIMEOUT_MS;
48
- console.info("routing server shutting down", JSON.stringify({ signal }));
49
+ console.info("gateway server shutting down", JSON.stringify({ signal }));
49
50
  try {
50
51
  await stopServerForShutdown(server);
51
52
  } finally {
@@ -54,7 +55,7 @@ export function installShutdownHandlers(
54
55
  );
55
56
  if (remaining > 0) {
56
57
  console.warn(
57
- "routing server background drain timed out",
58
+ "gateway server background drain timed out",
58
59
  JSON.stringify({ remaining_task_count: remaining }),
59
60
  );
60
61
  captureOperationalError(
@@ -112,7 +113,7 @@ function stopWithin(
112
113
 
113
114
  function reportServerStopFailure(error: unknown): void {
114
115
  console.warn(
115
- "routing server shutdown failed",
116
+ "gateway server shutdown failed",
116
117
  error instanceof Error ? error.name : "UnknownError",
117
118
  );
118
119
  captureOperationalError(error, { code: "server_force_stop_failed" });
@@ -9,6 +9,7 @@ import type { SelectorBackend } from "../strategies/routing.js";
9
9
  import {
10
10
  chatCompletionsConversationIdentity,
11
11
  openCodeRequestContext,
12
+ piRequestContext,
12
13
  } from "../runtime/harness_identity/index.js";
13
14
  import { parseJsonRequest } from "./json.js";
14
15
  import { museCodeClientId } from "./muse_code.js";
@@ -22,7 +23,7 @@ export async function handleChatCompletions(
22
23
  const apiKey = bearerToken(request.headers);
23
24
  const payload = await parseJsonRequest<ChatCompletionRequest>(request);
24
25
  const interaction = chatCompletionsToInteraction({ ...payload, source_protocol: "openai_chat" });
25
- const agentContext = openCodeRequestContext(request.headers);
26
+ const agentContext = piRequestContext(request.headers) ?? openCodeRequestContext(request.headers);
26
27
  const museClientId = museCodeClientId(request.headers);
27
28
  return await handleRoutedCompletion({
28
29
  request,
@@ -1,3 +1,4 @@
1
+ import { explicitModelId } from "../protocol/input/router_model.js";
1
2
  import { bearerToken } from "../http/auth.js";
2
3
  import { errorResponse } from "../http/errors.js";
3
4
  import { captureRequestError } from "../observability/errors.js";
@@ -40,7 +41,9 @@ export async function handleResponses(
40
41
  // providers. That preference describes a direct model, not a heterogeneous
41
42
  // router, so let Dari select the reasoning effort for verified Codex turns.
42
43
  // Parsing above still validates malformed Responses reasoning controls.
43
- if (harnessConversationIdentity !== undefined) payload.reasoning = undefined;
44
+ if (harnessConversationIdentity !== undefined && explicitModelId(payload.model) === null) {
45
+ payload.reasoning = undefined;
46
+ }
44
47
  return await handleRoutedCompletion({
45
48
  request,
46
49
  routerId,
@@ -32,6 +32,8 @@ type CodexCatalogEntry = {
32
32
  contextWindow: number;
33
33
  hostedWebSearch: boolean;
34
34
  imageInput: boolean;
35
+ reasoningLevels?: string[];
36
+ defaultReasoningLevel?: string | null;
35
37
  };
36
38
 
37
39
  function codexCatalogEntry(args: CodexCatalogEntry): Record<string, unknown> {
@@ -39,8 +41,11 @@ function codexCatalogEntry(args: CodexCatalogEntry): Record<string, unknown> {
39
41
  slug: args.slug,
40
42
  display_name: args.displayName,
41
43
  description: args.description,
42
- default_reasoning_level: null,
43
- supported_reasoning_levels: [],
44
+ default_reasoning_level: args.defaultReasoningLevel ?? null,
45
+ supported_reasoning_levels: (args.reasoningLevels ?? []).map((effort) => ({
46
+ effort,
47
+ description: `${effort} thinking`,
48
+ })),
44
49
  shell_type: "shell_command",
45
50
  visibility: "list",
46
51
  supported_in_api: true,
@@ -80,15 +85,17 @@ function codexCatalogEntry(args: CodexCatalogEntry): Record<string, unknown> {
80
85
  };
81
86
  }
82
87
 
83
- /* `dari/routing` stays first (the Codex launcher patches it); each enabled
84
- model follows as `dari/model/<id>` so Codex's /model menu can switch to it. */
88
+ /* `dari/routing` comes first while the router may autoroute (the Codex
89
+ launcher patches it); each enabled model follows as `dari/model/<id>` so
90
+ Codex's /model menu can switch to it. The `dari` launchers build every
91
+ agent's model menu from this list. */
85
92
  export function codexModelsResponse(
86
- router: Pick<ResolvedRouter, "enabled_models" | "model_configs" | "allow_long_context">,
93
+ router: Pick<ResolvedRouter, "enabled_models" | "model_configs" | "allow_long_context" | "autorouting_enabled">,
87
94
  imageInputFor: (model: string) => boolean
88
95
  ): Record<string, unknown> {
89
96
  return {
90
97
  models: [
91
- codexCatalogEntry({
98
+ ...(router.autorouting_enabled === false ? [] : [codexCatalogEntry({
92
99
  slug: CANONICAL_ROUTER_MODEL,
93
100
  displayName: "Dari Routing",
94
101
  description: "Routes Codex HTTP Responses requests through the configured Dari model router.",
@@ -96,7 +103,7 @@ export function codexModelsResponse(
96
103
  contextWindow: codexRouterContextWindow(router),
97
104
  hostedWebSearch: codexRouterSupportsHostedWebSearch(router),
98
105
  imageInput: router.enabled_models.some(imageInputFor),
99
- }),
106
+ })]),
100
107
  ...router.enabled_models.map((model, index) =>
101
108
  codexCatalogEntry({
102
109
  slug: explicitModelSlug(model),
@@ -106,6 +113,10 @@ export function codexModelsResponse(
106
113
  contextWindow: codexRouterContextWindow({ ...router, enabled_models: [model] }),
107
114
  hostedWebSearch: codexRouterSupportsHostedWebSearch({ ...router, enabled_models: [model] }),
108
115
  imageInput: imageInputFor(model),
116
+ reasoningLevels: router.model_configs?.[model]?.supported_thinking_levels
117
+ ?.map((effort) => effort === "off" ? "none" : effort),
118
+ defaultReasoningLevel: router.model_configs?.[model]?.default_thinking_level === "off"
119
+ ? "none" : router.model_configs?.[model]?.default_thinking_level,
109
120
  })
110
121
  ),
111
122
  ],
@@ -45,6 +45,10 @@ export type GatewayPlatform = {
45
45
  writePrefixState(write: PrefixStateWrite): Promise<void>;
46
46
  /** Set or clear a conversation's prefetched routing lease. */
47
47
  writePendingLease(write: PendingLeaseWrite): Promise<void>;
48
+ /** The pair a session pinned on a session-pinning router, or null. */
49
+ readSessionPin(request: SessionPinKey): Promise<SessionPin | null>;
50
+ /** Pin a session to a pair, replacing any earlier pin. */
51
+ writeSessionPin(write: SessionPinKey & SessionPin): Promise<void>;
48
52
  /**
49
53
  * Hold an exhausted personal subscription until it resets. Resolves whether
50
54
  * another connected subscription for the same provider can serve a retry.
@@ -104,6 +108,17 @@ export type PendingLeaseWrite = {
104
108
  } | null;
105
109
  };
106
110
 
111
+ // A main agent and each of its subagents have their own pin key.
112
+ export type SessionPinKey = {
113
+ routerId: string;
114
+ pinKey: string;
115
+ };
116
+
117
+ export type SessionPin = {
118
+ model: string;
119
+ reasoningEffort: ReasoningEffort;
120
+ };
121
+
107
122
  export type SubscriptionHold = {
108
123
  userId: string;
109
124
  credentialId: string | undefined;
@@ -146,8 +161,6 @@ export type RequestSummary = {
146
161
  selected_reasoning_effort: ReasoningEffort | null;
147
162
  next_model: string | null;
148
163
  next_reasoning_effort: ReasoningEffort | null;
149
- fallback_model: string | null;
150
- fallback_reasoning_effort: ReasoningEffort | null;
151
164
  provider_key_source: ProviderKeySource | null;
152
165
  message_count: number;
153
166
  tool_count: number;
@@ -184,6 +197,7 @@ export type OperationalErrorCode =
184
197
  | "routing_report_assembly_failed"
185
198
  | "routing_report_barrier_failed"
186
199
  | "server_force_stop_failed"
200
+ | "session_pin_write_failed"
187
201
  | "stream_report_finalization_failed"
188
202
  | "subscription_unavailable_ingest_failed";
189
203
 
@@ -572,15 +572,8 @@ function reasoningControls(value: unknown): InteractionRequest["reasoning"] {
572
572
  if (value === undefined || value === null) return undefined;
573
573
  if (!isRecord(value)) throw new HttpError(400, "reasoning must be an object.", "invalid_request_error", "reasoning");
574
574
  const normalizedEffort = normalizedReasoningEffort(value.effort, "reasoning.effort");
575
- const summary = value.summary;
576
- if (summary !== undefined) {
577
- throw new HttpError(
578
- 400,
579
- "reasoning.summary is not supported by this router MVP.",
580
- "invalid_request_error",
581
- "reasoning.summary"
582
- );
583
- }
575
+ // Summaries are display-only: clients such as Muse Code ask for them by
576
+ // default, and the router serves the turn without them.
584
577
  return normalizedEffort === undefined ? undefined : { effort: normalizedEffort };
585
578
  }
586
579
 
@@ -2,7 +2,7 @@ import { parseDataImageUrl } from "../../interaction/image.js";
2
2
  import { encodeProviderContinuationState } from "../../interaction/provider_continuation_state.js";
3
3
  import { encodePortableReasoningState } from "../../interaction/portable_reasoning_state.js";
4
4
  import type { InteractionOutput } from "../../interaction/types.js";
5
- import type { RoutingDecision } from "@mupt-ai/dari-router/policy-engine";
5
+ import type { ServedDecision } from "../../types.js";
6
6
 
7
7
  export function anthropicMessageId(requestId: string): string {
8
8
  return `msg_${requestId.replace(/[^A-Za-z0-9_-]/g, "_")}`.slice(0, 64);
@@ -99,7 +99,7 @@ export function anthropicUsage(output: Pick<InteractionOutput, "usage">): Record
99
99
  }
100
100
 
101
101
  export function anthropicRouting(
102
- decision: RoutingDecision,
102
+ decision: ServedDecision,
103
103
  requestId: string,
104
104
  conversationId?: string
105
105
  ): Record<string, unknown> {
@@ -113,6 +113,7 @@ export function anthropicRouting(
113
113
  ...(decision.leaseTurnsRemaining === undefined
114
114
  ? {}
115
115
  : { lease_turns_remaining: decision.leaseTurnsRemaining }),
116
+ ...(decision.sessionPinned === true ? { session_pinned: true } : {}),
116
117
  };
117
118
  }
118
119
 
@@ -6,7 +6,7 @@ import {
6
6
  openAIResponsesTerminal,
7
7
  openAIResponsesUsage,
8
8
  } from "./openai_responses_output.js";
9
- import { piMessageToInteractionOutput } from "../../providers/pi_output.js";
9
+ import { piMessageToInteractionOutput, textSignature } from "../../providers/pi_output.js";
10
10
  import type {
11
11
  InteractionOutput,
12
12
  InteractionOutputItem,
@@ -119,6 +119,7 @@ class ResponsesAccumulator {
119
119
  private readonly addedIndexes = new Set<number>();
120
120
  private readonly completedIndexes = new Set<number>();
121
121
  private readonly messageIds = new Map<number, string>();
122
+ private readonly messagePhases = new Map<number, "commentary" | "final_answer">();
122
123
  private readonly reasoningIds = new Map<number, string>();
123
124
  private readonly messagePartStarted = new Set<number>();
124
125
  private readonly reasoningPartStarted = new Set<number>();
@@ -144,6 +145,7 @@ class ResponsesAccumulator {
144
145
  const index = this.indexFor(event);
145
146
  this.activate(index, send);
146
147
  if (type === "thinking_start") this.ensureReasoning(index, send);
148
+ if (type === "text_start") this.rememberMessageSignature(event, index);
147
149
  return;
148
150
  }
149
151
  if (type === "text_delta" && typeof event.delta === "string" && event.delta) {
@@ -330,17 +332,29 @@ class ResponsesAccumulator {
330
332
  return openAIResponsesUsage(this.finalOutput ?? this.baseOutput());
331
333
  }
332
334
 
335
+ // Pi signs a Responses text block with its message id and phase at
336
+ // text_start, so the item is announced with the phase it ends with.
337
+ private rememberMessageSignature(event: Record<string, unknown>, index: number): void {
338
+ const content = isRecord(event.partial) ? event.partial.content : undefined;
339
+ const block = Array.isArray(content) ? content[event.contentIndex as number] : undefined;
340
+ const signature = isRecord(block) ? textSignature(block.textSignature) : null;
341
+ if (signature?.id && !this.messageIds.has(index)) this.messageIds.set(index, signature.id);
342
+ if (signature?.phase) this.messagePhases.set(index, signature.phase);
343
+ }
344
+
333
345
  private ensureMessage(index: number, send: Send): Extract<InteractionOutputItem, { type: "message" }> {
334
346
  const existing = this.items.get(index);
335
347
  if (existing?.type === "message") return existing;
336
348
  const id = this.messageIds.get(index) ?? streamedItemId("msg", this.responseId, index);
337
349
  this.messageIds.set(index, id);
350
+ const phase = this.messagePhases.get(index);
338
351
  const message: Extract<InteractionOutputItem, { type: "message" }> = {
339
352
  type: "message",
340
353
  id,
341
354
  role: "assistant",
342
355
  content: [{ type: "text", text: "" }],
343
356
  contentShape: "parts",
357
+ ...(phase ? { phase } : {}),
344
358
  source: this.args.source,
345
359
  };
346
360
  this.items.set(index, message);
@@ -353,6 +367,7 @@ class ResponsesAccumulator {
353
367
  id,
354
368
  status: "in_progress",
355
369
  content: [],
370
+ ...(phase ? { phase } : {}),
356
371
  },
357
372
  });
358
373
  this.sendMessagePartAdded(index, id, send);
@@ -420,6 +435,7 @@ class ResponsesAccumulator {
420
435
  id: serialized.id,
421
436
  status: "in_progress",
422
437
  content: [],
438
+ ...(item.phase ? { phase: item.phase } : {}),
423
439
  },
424
440
  });
425
441
  } else {
@@ -58,11 +58,8 @@ import {
58
58
  export type ProviderChoice = {
59
59
  decision: RoutingDecision;
60
60
  execution: ProviderExecution;
61
- role: "primary" | "fallback";
62
61
  };
63
62
 
64
- export type ResolveFallbackChoice = () => Promise<ProviderChoice | null>;
65
-
66
63
  export type PreparedStreamExecution = {
67
64
  choice: ProviderChoice;
68
65
  started: StartedStream;
@@ -86,7 +83,6 @@ export async function executeNonStreaming(args: {
86
83
  interaction: InteractionRequest;
87
84
  primary: ProviderChoice;
88
85
  primaryRetries: number;
89
- resolveFallback: ResolveFallbackChoice;
90
86
  reporter: RoutedRuntimeReporter;
91
87
  protocol: RoutedProtocol;
92
88
  startedCompletion?: StartedCompletion;
@@ -111,7 +107,6 @@ export async function completeNonStreaming(args: {
111
107
  interaction: InteractionRequest;
112
108
  primary: ProviderChoice;
113
109
  primaryRetries: number;
114
- resolveFallback: ResolveFallbackChoice;
115
110
  reporter: RoutedRuntimeReporter;
116
111
  protocol: RoutedProtocol;
117
112
  startedCompletion?: StartedCompletion;
@@ -157,7 +152,7 @@ export async function completeNonStreaming(args: {
157
152
  output,
158
153
  modelCost: args.reporter.modelCost({
159
154
  model: choice.decision.selectedModel, reasoning_effort: choice.decision.reasoningEffort,
160
- role: choice.role, attempt: result.attempt, outcome: "completed", billingOutcome: "completed",
155
+ attempt: result.attempt, outcome: "completed", billingOutcome: "completed",
161
156
  execution: reportedExecution(choice.execution), message: completion.message,
162
157
  }),
163
158
  request: args.interaction,
@@ -231,7 +226,6 @@ async function completeWithRetries(args: {
231
226
  interaction: InteractionRequest;
232
227
  primary: ProviderChoice;
233
228
  primaryRetries: number;
234
- resolveFallback: ResolveFallbackChoice;
235
229
  reporter: RoutedRuntimeReporter;
236
230
  startedCompletion?: StartedCompletion;
237
231
  clientSignal?: AbortSignal;
@@ -239,7 +233,6 @@ async function completeWithRetries(args: {
239
233
  return runProviderAttemptPlan({
240
234
  primary: args.primary,
241
235
  retries: args.primaryRetries,
242
- resolveFallback: args.resolveFallback,
243
236
  primaryInitial: args.startedCompletion,
244
237
  isCustomerCanceled: () => customerAbortRequested(args.clientSignal),
245
238
  canceledFailure: customerCanceledProviderMessage,
@@ -459,7 +452,6 @@ export async function prepareStreamingExecution(args: {
459
452
  interaction: InteractionRequest;
460
453
  primary: ProviderChoice;
461
454
  primaryRetries: number;
462
- resolveFallback: ResolveFallbackChoice;
463
455
  reporter: RoutedRuntimeReporter;
464
456
  startedStream?: StartedStream;
465
457
  initialError?: unknown;
@@ -475,7 +467,6 @@ export async function prepareStreamingExecution(args: {
475
467
  const result = await runProviderAttemptPlan({
476
468
  primary: args.primary,
477
469
  retries: args.primaryRetries,
478
- resolveFallback: args.resolveFallback,
479
470
  primaryInitial,
480
471
  isCustomerCanceled: () => customerAbortRequested(args.clientSignal),
481
472
  canceledFailure: customerCanceledProviderMessage,
@@ -537,7 +528,7 @@ async function runStreamSetupAttempt(args: {
537
528
  clientSignal: args.clientSignal,
538
529
  });
539
530
  // Prime before committing so connection failures and prompt rejections
540
- // stay inside the one shared retry/fallback state machine.
531
+ // stay inside the one shared retry state machine.
541
532
  const primed = await primeProviderStream(
542
533
  started.providerStream.events,
543
534
  started.customerAbort,
@@ -611,7 +602,6 @@ export async function executeStreaming(args: {
611
602
  protocol: args.protocol,
612
603
  providerLatencyMs: () =>
613
604
  args.failedSetupLatencyMs + Date.now() - started.providerStartedMs,
614
- role: args.choice.role,
615
605
  attempt: args.attempt,
616
606
  reportBarrier: args.reportBarrier,
617
607
  delivered: observed.delivered,
@@ -630,7 +620,7 @@ export async function executeStreaming(args: {
630
620
  clientSignal: args.clientSignal,
631
621
  modelCost: (message, failed) => args.reporter.modelCost({
632
622
  model: args.choice.decision.selectedModel, reasoning_effort: args.choice.decision.reasoningEffort,
633
- role: args.choice.role, attempt: args.attempt,
623
+ attempt: args.attempt,
634
624
  outcome: failed ? "terminal_error" : "completed",
635
625
  billingOutcome: failed ? "provider_failed" : "completed",
636
626
  execution: reportedExecution(execution), message,
@@ -673,7 +663,6 @@ function recordProviderAttempt(
673
663
  reporter.recordProviderAttempt({
674
664
  model: choice.decision.selectedModel,
675
665
  reasoning_effort: choice.decision.reasoningEffort,
676
- role: choice.role,
677
666
  attempt,
678
667
  outcome,
679
668
  execution: reportedExecution(choice.execution),
@@ -720,7 +709,8 @@ async function providerFailedAttempt(
720
709
  }
721
710
  // Retrying an exhausted plan cannot succeed. The platform learns about it
722
711
  // before this request returns so the next resolve already serves the
723
- // user's next subscription, or the router's subscription fallback model.
712
+ // user's next subscription, or the model's API credential when the router
713
+ // allows subscription API fallback for it.
724
714
  const switched = execution.subscriptionUserId !== undefined
725
715
  && await holdSubscription({
726
716
  userId: execution.subscriptionUserId,
@@ -411,7 +411,7 @@ function reasoningText(value: unknown, type: "summary_text" | "reasoning_text"):
411
411
  );
412
412
  }
413
413
 
414
- function textSignature(value: unknown): { id?: string; phase?: "commentary" | "final_answer" } | null {
414
+ export function textSignature(value: unknown): { id?: string; phase?: "commentary" | "final_answer" } | null {
415
415
  if (typeof value !== "string" || !value) return null;
416
416
  try {
417
417
  const parsed: unknown = JSON.parse(value);