@mupt-ai/dari-router 0.1.0 → 0.2.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 (118) hide show
  1. package/README.md +89 -60
  2. package/dist/anonymous_actions.js +32 -10
  3. package/dist/anonymous_actions.js.map +1 -1
  4. package/dist/auto_router.d.ts +4 -1
  5. package/dist/auto_router.js +3 -3
  6. package/dist/auto_router.js.map +1 -1
  7. package/dist/cache_behavior.d.ts +8 -6
  8. package/dist/cache_behavior.js +56 -30
  9. package/dist/cache_behavior.js.map +1 -1
  10. package/dist/cache_scope.d.ts +2 -2
  11. package/dist/cache_scope.js +23 -18
  12. package/dist/cache_scope.js.map +1 -1
  13. package/dist/cost.d.ts +3 -2
  14. package/dist/cost.js +32 -23
  15. package/dist/cost.js.map +1 -1
  16. package/dist/dari_policy.d.ts +8 -2
  17. package/dist/dari_policy.js +39 -1
  18. package/dist/dari_policy.js.map +1 -1
  19. package/dist/eval_score_imputation.d.ts +26 -0
  20. package/dist/eval_score_imputation.js +83 -0
  21. package/dist/eval_score_imputation.js.map +1 -0
  22. package/dist/framework_lifecycle.d.ts +1 -1
  23. package/dist/framework_lifecycle.js +4 -1
  24. package/dist/framework_lifecycle.js.map +1 -1
  25. package/dist/framework_models.js +6 -0
  26. package/dist/framework_models.js.map +1 -1
  27. package/dist/framework_router.js +22 -11
  28. package/dist/framework_router.js.map +1 -1
  29. package/dist/framework_types.d.ts +9 -5
  30. package/dist/index.d.ts +9 -35
  31. package/dist/index.js +6 -37
  32. package/dist/index.js.map +1 -1
  33. package/dist/model_ids.js +4 -0
  34. package/dist/model_ids.js.map +1 -1
  35. package/dist/pi_runtime.js +19 -11
  36. package/dist/pi_runtime.js.map +1 -1
  37. package/dist/pi_types.d.ts +2 -2
  38. package/{src/index.ts → dist/policy-engine.d.ts} +2 -14
  39. package/dist/policy-engine.js +25 -0
  40. package/dist/policy-engine.js.map +1 -0
  41. package/dist/prompts.d.ts +1 -1
  42. package/dist/prompts.js +3 -3
  43. package/dist/prompts.js.map +1 -1
  44. package/dist/protocols.d.ts +5 -0
  45. package/dist/protocols.js +6 -0
  46. package/dist/protocols.js.map +1 -0
  47. package/dist/route.d.ts +3 -4
  48. package/dist/route.js +17 -10
  49. package/dist/route.js.map +1 -1
  50. package/dist/selector_input.d.ts +1 -0
  51. package/dist/selector_input.js +66 -78
  52. package/dist/selector_input.js.map +1 -1
  53. package/dist/selector_parse.d.ts +1 -0
  54. package/dist/selector_parse.js +2 -1
  55. package/dist/selector_parse.js.map +1 -1
  56. package/dist/selector_request.d.ts +1 -0
  57. package/dist/selector_request.js +1 -0
  58. package/dist/selector_request.js.map +1 -1
  59. package/dist/types.d.ts +1 -1
  60. package/examples/auto_router.ts +40 -0
  61. package/examples/basic_route.ts +130 -0
  62. package/examples/create_router.ts +114 -0
  63. package/examples/dari_policy.ts +86 -0
  64. package/examples/lease_stores.ts +159 -0
  65. package/examples/managed/README.md +50 -0
  66. package/examples/managed/byok-router/README.md +13 -0
  67. package/examples/managed/byok-router/router.yml +13 -0
  68. package/examples/managed/custom-rules-router/README.md +12 -0
  69. package/examples/managed/custom-rules-router/router.yml +24 -0
  70. package/examples/managed/eval-router/README.md +18 -0
  71. package/examples/managed/eval-router/router.yml +11 -0
  72. package/examples/managed/slm-router/README.md +12 -0
  73. package/examples/managed/slm-router/router.yml +8 -0
  74. package/examples/pi_runtime.ts +55 -0
  75. package/package.json +26 -7
  76. package/src/anonymous_actions.ts +0 -434
  77. package/src/auto_router.ts +0 -95
  78. package/src/cache_behavior.ts +0 -130
  79. package/src/cache_scope.ts +0 -58
  80. package/src/cache_switch_decision.ts +0 -53
  81. package/src/candidate_pruning.ts +0 -257
  82. package/src/candidate_resolution.ts +0 -208
  83. package/src/compatibility.ts +0 -162
  84. package/src/continuation_state.ts +0 -368
  85. package/src/cost.ts +0 -455
  86. package/src/dari_policy.ts +0 -360
  87. package/src/errors.ts +0 -27
  88. package/src/fingerprint.ts +0 -148
  89. package/src/fixed_turn_cost_config.ts +0 -24
  90. package/src/framework_error.ts +0 -38
  91. package/src/framework_lifecycle.ts +0 -74
  92. package/src/framework_models.ts +0 -186
  93. package/src/framework_router.ts +0 -490
  94. package/src/framework_streaming.ts +0 -613
  95. package/src/framework_types.ts +0 -372
  96. package/src/framework_validation.ts +0 -235
  97. package/src/json.ts +0 -43
  98. package/src/model_ids.ts +0 -29
  99. package/src/pi_context.ts +0 -251
  100. package/src/pi_options.ts +0 -295
  101. package/src/pi_output.ts +0 -387
  102. package/src/pi_reasoning_signature.ts +0 -41
  103. package/src/pi_runtime.ts +0 -238
  104. package/src/pi_types.ts +0 -71
  105. package/src/portable_reasoning_state.ts +0 -129
  106. package/src/previous_decision.ts +0 -109
  107. package/src/prompts.ts +0 -69
  108. package/src/protocol_anthropic.ts +0 -571
  109. package/src/protocol_openai_chat.ts +0 -645
  110. package/src/protocol_validation.ts +0 -202
  111. package/src/route.ts +0 -490
  112. package/src/routing_history.ts +0 -157
  113. package/src/selector_input.ts +0 -246
  114. package/src/selector_parse.ts +0 -138
  115. package/src/selector_request.ts +0 -130
  116. package/src/selector_truncation.ts +0 -48
  117. package/src/selector_wire.ts +0 -167
  118. package/src/types.ts +0 -231
@@ -1,372 +0,0 @@
1
- import type { ReasoningEffort, RoutingDecision } from "./types.js";
2
- import type {
3
- RouterContinuationState,
4
- RouterProviderIdentity,
5
- } from "./continuation_state.js";
6
- import type { RouterFrameworkError } from "./framework_error.js";
7
-
8
- export type { RouterContinuationState, RouterProviderIdentity } from "./continuation_state.js";
9
-
10
- export type RouterProtocol = "openai_chat_completions" | "anthropic_messages";
11
-
12
- export type RouterTextContent = {
13
- type: "text";
14
- text: string;
15
- };
16
-
17
- export type RouterImageContent = {
18
- type: "image";
19
- url: string;
20
- detail?: "auto" | "low" | "high";
21
- };
22
-
23
- export type RouterContent = RouterTextContent | RouterImageContent;
24
-
25
- export type RouterMessageItem = {
26
- type: "message";
27
- role: "system" | "developer" | "user" | "assistant";
28
- content: RouterContent[];
29
- };
30
-
31
- export type RouterToolCallItem = {
32
- type: "tool_call";
33
- id: string;
34
- name: string;
35
- arguments: string | Record<string, unknown>;
36
- };
37
-
38
- export type RouterToolResultItem = {
39
- type: "tool_result";
40
- toolCallId: string;
41
- content: RouterContent[];
42
- isError?: boolean;
43
- };
44
-
45
- // Provider-hosted tools (for example OpenAI/Anthropic web search). These are
46
- // replayable provider-native calls, not user-defined function tools.
47
- export type RouterHostedToolCallStatus =
48
- | "in_progress"
49
- | "searching"
50
- | "completed"
51
- | "incomplete"
52
- | "failed";
53
-
54
- export function isRouterHostedToolCallStatus(
55
- value: unknown,
56
- ): value is RouterHostedToolCallStatus {
57
- return value === "in_progress" || value === "searching" || value === "completed" ||
58
- value === "incomplete" || value === "failed";
59
- }
60
-
61
- export type RouterHostedToolCallItem = {
62
- type: "hosted_tool_call";
63
- id: string;
64
- tool: "web_search";
65
- providerType: "web_search_call";
66
- status?: RouterHostedToolCallStatus;
67
- payload: Record<string, unknown>;
68
- source?: RouterProviderIdentity;
69
- };
70
-
71
- // Prior-turn reasoning/thinking carried across turns. The framework does not
72
- // translate encrypted blobs between providers; it carries them tagged with
73
- // their source provider identity and executors drop/refuse cross-provider
74
- // continuations (see compatibleContinuation).
75
- export type RouterReasoningItem = {
76
- type: "reasoning";
77
- id?: string;
78
- summary?: string[];
79
- content: string[];
80
- source?: RouterProviderIdentity;
81
- continuation?: RouterContinuationState;
82
- };
83
-
84
- export type RouterInputItem =
85
- | RouterMessageItem
86
- | RouterToolCallItem
87
- | RouterToolResultItem
88
- | RouterReasoningItem
89
- | RouterHostedToolCallItem;
90
-
91
- export type RouterTool = {
92
- name: string;
93
- description?: string;
94
- inputSchema: Record<string, unknown>;
95
- strict?: boolean;
96
- };
97
-
98
- export type RouterToolChoice =
99
- | "none"
100
- | "auto"
101
- | "required"
102
- | { type: "tool"; name: string };
103
-
104
- export type RouterResponseFormat =
105
- | { type: "text" }
106
- | { type: "json_object" }
107
- | {
108
- type: "json_schema";
109
- name: string;
110
- schema: Record<string, unknown>;
111
- description?: string;
112
- strict?: boolean;
113
- };
114
-
115
- export type RouterRequest = {
116
- protocol: RouterProtocol;
117
- requestedModel: string;
118
- items: RouterInputItem[];
119
- tools: RouterTool[];
120
- toolChoice?: RouterToolChoice;
121
- parallelToolCalls?: boolean;
122
- generation: {
123
- temperature?: number;
124
- topP?: number;
125
- maxOutputTokens?: number;
126
- stop?: string | string[];
127
- };
128
- reasoning?: {
129
- effort?: ReasoningEffort;
130
- enabled?: boolean;
131
- budgetTokens?: number;
132
- };
133
- responseFormat?: RouterResponseFormat;
134
- cacheKey?: string;
135
- stream: boolean;
136
- metadata?: Record<string, unknown>;
137
- user?: string;
138
- };
139
-
140
- export type RouterModelCapabilities = {
141
- imageInput?: boolean;
142
- toolUse?: boolean;
143
- structuredOutput?: boolean;
144
- streaming?: boolean;
145
- };
146
-
147
- export type RouterModel<Metadata = unknown> = {
148
- id: string;
149
- executor?: string;
150
- provider?: string;
151
- api?: string;
152
- reasoningEfforts?: readonly ReasoningEffort[];
153
- defaultReasoningEffort?: ReasoningEffort;
154
- capabilities?: RouterModelCapabilities;
155
- metadata?: Metadata;
156
- };
157
-
158
- export type RouterCandidate<Metadata = unknown> = {
159
- id: string;
160
- executor: string;
161
- provider: string;
162
- api: string;
163
- reasoningEfforts: readonly ReasoningEffort[];
164
- defaultReasoningEffort: ReasoningEffort;
165
- capabilities: Required<RouterModelCapabilities>;
166
- metadata?: Metadata;
167
- };
168
-
169
- export type RoutingPolicyInput<Metadata = unknown> = {
170
- request: RouterRequest;
171
- candidates: readonly RouterCandidate<Metadata>[];
172
- signal: AbortSignal;
173
- };
174
-
175
- export type RoutingPolicyDecision = {
176
- model: string;
177
- reasoningEffort?: ReasoningEffort;
178
- reason?: string;
179
- // Turns the policy commits to this selection after the current one. When
180
- // set, createRouter serves the same model for subsequent requests sharing
181
- // the same cacheKey without calling the policy again.
182
- leaseTurnsRemaining?: number;
183
- details?: unknown;
184
- };
185
-
186
- export type RoutingPolicy<Metadata = unknown> =
187
- | ((input: RoutingPolicyInput<Metadata>) => RoutingPolicyDecision | Promise<RoutingPolicyDecision>)
188
- | {
189
- select(input: RoutingPolicyInput<Metadata>): RoutingPolicyDecision | Promise<RoutingPolicyDecision>;
190
- };
191
-
192
- export type RouterSelection<Metadata = unknown> = {
193
- decision: RoutingDecision;
194
- candidates: readonly RouterCandidate<Metadata>[];
195
- policyDetails?: unknown;
196
- };
197
-
198
- export type RouterUsage = {
199
- // Non-cached input tokens. Cache reads/writes are separate counts, not
200
- // subsets of inputTokens; protocol serializers fold them per each wire
201
- // format's own semantics.
202
- inputTokens: number;
203
- outputTokens: number;
204
- cacheReadTokens?: number;
205
- cacheWriteTokens?: number;
206
- totalTokens?: number;
207
- };
208
-
209
- export type RouterFinishReason = "stop" | "length" | "tool_calls";
210
-
211
- export type RouterOutputText = {
212
- type: "text";
213
- text: string;
214
- };
215
-
216
- export type RouterOutputToolCall = {
217
- type: "tool_call";
218
- id: string;
219
- name: string;
220
- arguments: string | Record<string, unknown>;
221
- };
222
-
223
- export type RouterOutputReasoning = {
224
- type: "reasoning";
225
- // Readable thinking text (plaintext summary or content). Empty for purely
226
- // redacted thinking, where only the encrypted continuation is available.
227
- text: string;
228
- redacted?: boolean;
229
- source?: RouterProviderIdentity;
230
- continuation?: RouterContinuationState;
231
- };
232
-
233
- export type RouterOutputHostedToolCall = {
234
- type: "hosted_tool_call";
235
- id: string;
236
- tool: "web_search";
237
- providerType: "web_search_call";
238
- status?: RouterHostedToolCallStatus;
239
- payload: Record<string, unknown>;
240
- source?: RouterProviderIdentity;
241
- };
242
-
243
- export type RouterOutputItem =
244
- | RouterOutputText
245
- | RouterOutputToolCall
246
- | RouterOutputReasoning
247
- | RouterOutputHostedToolCall;
248
-
249
- export type RouterCompletion = {
250
- id?: string;
251
- createdAtMs?: number;
252
- content: RouterOutputItem[];
253
- finishReason: RouterFinishReason;
254
- usage?: RouterUsage;
255
- };
256
-
257
- export type RouterStreamEvent =
258
- | { type: "text_delta"; index: number; delta: string }
259
- | { type: "tool_call_start"; index: number; id: string; name: string }
260
- | { type: "tool_call_delta"; index: number; delta: string }
261
- | { type: "tool_call_end"; index: number }
262
- | { type: "reasoning_delta"; index: number; delta: string }
263
- // Hosted tool executions (provider-run web search) arrive complete rather
264
- // than incrementally: one event carries the full replayable item.
265
- | (RouterOutputHostedToolCall & { index: number })
266
- | {
267
- type: "reasoning_end";
268
- index: number;
269
- redacted?: boolean;
270
- // Provider identity and stable item id for signing plain (no-
271
- // continuation) reasoning with a portable envelope on replayable wire
272
- // formats.
273
- source?: RouterProviderIdentity;
274
- itemId?: string;
275
- continuation?: import("./continuation_state.js").ProviderContinuationState;
276
- }
277
- | { type: "finish"; finishReason: RouterFinishReason; usage?: RouterUsage };
278
-
279
- export type RouterExecutorResult =
280
- | { type: "complete"; output: RouterCompletion }
281
- | { type: "stream"; events: AsyncIterable<RouterStreamEvent> };
282
-
283
- export type RouterExecutorInput<Metadata = unknown> = {
284
- request: RouterRequest;
285
- model: RouterCandidate<Metadata>;
286
- decision: RoutingDecision;
287
- signal: AbortSignal;
288
- };
289
-
290
- export type RouterExecutor<Metadata = unknown> = {
291
- execute(
292
- input: RouterExecutorInput<Metadata>,
293
- ): RouterExecutorResult | Promise<RouterExecutorResult>;
294
- };
295
-
296
- export type CreateRouterOptions<Metadata = unknown> = {
297
- models: readonly RouterModel<Metadata>[];
298
- policy: RoutingPolicy<Metadata>;
299
- runtime?: RouterExecutor<Metadata>;
300
- executors?: Readonly<Record<string, RouterExecutor<Metadata>>>;
301
- generateId?: () => string;
302
- // Pluggable lease store. When omitted, createRouter uses an in-memory
303
- // store with a 30-minute TTL. Inject a persistent store to share leases
304
- // across processes (e.g. the managed backend's DB-backed leases).
305
- leaseStore?: LeaseStore;
306
- // Lifecycle hooks for telemetry, billing, and logging. All hooks are
307
- // optional and fire-and-forget: createRouter never awaits them on the
308
- // hot path.
309
- hooks?: RouterHooks<Metadata>;
310
- // Fallback configuration. When the primary executor fails, createRouter
311
- // retries on a fallback model if one is available and eligible.
312
- fallback?: RouterFallbackConfig;
313
- };
314
-
315
- export type Router<Metadata = unknown> = {
316
- readonly models: readonly RouterCandidate<Metadata>[];
317
- select(request: RouterRequest, signal?: AbortSignal): Promise<RouterSelection<Metadata>>;
318
- fetch(request: Request): Promise<Response>;
319
- };
320
-
321
- // A lease commitment: the router serves the same model for subsequent
322
- // requests sharing the cacheKey without calling the policy again.
323
- export type RouterLease = {
324
- model: string;
325
- reasoningEffort: ReasoningEffort;
326
- turnsRemaining: number;
327
- expiresAt: number;
328
- };
329
-
330
- // Pluggable lease storage. The default in-memory implementation has a
331
- // 30-minute TTL; inject a persistent store for cross-process leases. Methods
332
- // may be synchronous or return promises (DB-backed stores). Read-modify-write
333
- // atomicity under concurrent requests is the store's responsibility. Every
334
- // call is advisory: a store that throws or rejects never fails the request.
335
- export interface LeaseStore {
336
- get(cacheKey: string): RouterLease | undefined | Promise<RouterLease | undefined>;
337
- set(cacheKey: string, lease: RouterLease): void | Promise<void>;
338
- delete(cacheKey: string): void | Promise<void>;
339
- // Remove all expired leases. Called on every cacheKey-bearing selection.
340
- pruneExpired(nowMs: number): void | Promise<void>;
341
- }
342
-
343
- // onSelection observes the policy's decision before execution. The other
344
- // hooks receive the selection actually served: after a successful fallback
345
- // its decision names the fallback model, states the fallback in its reason,
346
- // and carries no leaseTurnsRemaining (the primary's lease was released).
347
- export type RouterHooks<Metadata = unknown> = {
348
- // Fires after a successful selection (including lease short-circuits).
349
- onSelection?(selection: RouterSelection<Metadata>, request: RouterRequest): void;
350
- // Fires after a successful completion (non-streaming). For streaming
351
- // completions, use onStreamClose which fires when the stream finishes.
352
- onCompletion?(completion: RouterCompletion, selection: RouterSelection<Metadata>): void;
353
- // Fires when a stream finishes successfully or with an error. The
354
- // completion deliberately carries content: [] — the router does not buffer
355
- // streamed output; only finishReason and usage are populated.
356
- onStreamClose?(
357
- completion: RouterCompletion | null,
358
- selection: RouterSelection<Metadata>,
359
- error: RouterFrameworkError | null,
360
- ): void;
361
- // Fires when the executor or stream fails.
362
- onError?(error: RouterFrameworkError, selection: RouterSelection<Metadata>): void;
363
- };
364
-
365
- // Executor-failure fallback for createRouter. Distinct from the phased API's
366
- // RouteFallbackConfig, which defaults both flags ON; here everything defaults OFF.
367
- export type RouterFallbackConfig = {
368
- // Whether fallback is enabled (default: false).
369
- enabled?: boolean;
370
- // Require the fallback model to be from a different provider (default: false).
371
- requiresDifferentProvider?: boolean;
372
- };
@@ -1,235 +0,0 @@
1
- import { RouterFrameworkError } from "./framework_error.js";
2
- import {
3
- completionContract,
4
- streamContract,
5
- } from "./framework_lifecycle.js";
6
- import { isReasoningEffort } from "./types.js";
7
- import type {
8
- RouterCandidate,
9
- RouterCompletion,
10
- RouterFinishReason,
11
- RouterSelection,
12
- RouterStreamEvent,
13
- RoutingPolicyDecision,
14
- } from "./framework_types.js";
15
-
16
- export function validateStreamSequence(
17
- event: Exclude<RouterStreamEvent, { type: "finish" }>,
18
- openIndexes: Map<number, "text" | "tool" | "reasoning">,
19
- ): void {
20
- const current = openIndexes.get(event.index);
21
- if (event.type === "text_delta") {
22
- if (current === "tool" || current === "reasoning") throw streamContract(`Stream index ${event.index} is already a ${current} block.`);
23
- if (current === undefined) openIndexes.set(event.index, "text");
24
- return;
25
- }
26
- if (event.type === "reasoning_delta") {
27
- if (current === "tool" || current === "text") throw streamContract(`Stream index ${event.index} is already a ${current} block.`);
28
- if (current === undefined) openIndexes.set(event.index, "reasoning");
29
- return;
30
- }
31
- if (event.type === "reasoning_end") {
32
- if (current === "text" || current === "tool") throw streamContract(`Stream index ${event.index} is already a ${current} block.`);
33
- if (current === "reasoning") openIndexes.delete(event.index);
34
- // If current is undefined, reasoning_end opens and immediately closes a
35
- // reasoning block (empty or redacted thinking with no prior delta).
36
- return;
37
- }
38
- if (event.type === "tool_call_start") {
39
- if (current !== undefined) throw streamContract(`Stream index ${event.index} is already in use.`);
40
- openIndexes.set(event.index, "tool");
41
- return;
42
- }
43
- if (event.type === "hosted_tool_call") {
44
- // Hosted tool calls arrive complete: the event opens and closes its
45
- // block, so the index only needs to be unused.
46
- if (current !== undefined) throw streamContract(`Stream index ${event.index} is already in use.`);
47
- return;
48
- }
49
- if (current !== "tool") {
50
- throw streamContract(`Stream index ${event.index} has no open tool call.`);
51
- }
52
- if (event.type === "tool_call_end") openIndexes.delete(event.index);
53
- }
54
-
55
- export function validateStreamEvent(event: RouterStreamEvent): void {
56
- if (!event || typeof event !== "object" || typeof event.type !== "string") {
57
- throw streamContract("Executor emitted an invalid stream event.");
58
- }
59
- if (event.type === "finish") {
60
- validateFinishReason(event.finishReason);
61
- if (event.usage !== undefined) validateUsage(event.usage);
62
- return;
63
- }
64
- if (!Number.isSafeInteger(event.index) || event.index < 0) {
65
- throw streamContract("Executor stream event index must be a non-negative integer.");
66
- }
67
- if (event.type === "text_delta" || event.type === "tool_call_delta" || event.type === "reasoning_delta") {
68
- if (typeof event.delta !== "string" || event.delta.length === 0) {
69
- throw streamContract("Executor stream deltas must be non-empty strings.");
70
- }
71
- return;
72
- }
73
- if (event.type === "tool_call_start") {
74
- if (
75
- typeof event.id !== "string" || !event.id.trim() ||
76
- typeof event.name !== "string" || !event.name.trim()
77
- ) {
78
- throw streamContract("Executor tool-call starts require non-empty id and name.");
79
- }
80
- return;
81
- }
82
- if (event.type === "hosted_tool_call") {
83
- if (
84
- typeof event.id !== "string" || event.id.length === 0 ||
85
- event.tool !== "web_search" ||
86
- event.providerType !== "web_search_call" ||
87
- typeof event.payload !== "object" || event.payload === null || Array.isArray(event.payload)
88
- ) {
89
- throw streamContract("Executor hosted tool calls require id, tool, providerType, and payload.");
90
- }
91
- return;
92
- }
93
- if (event.type !== "tool_call_end" && event.type !== "reasoning_end") {
94
- throw streamContract(`Executor emitted unsupported stream event '${String((event as { type: unknown }).type)}'.`);
95
- }
96
- }
97
-
98
- export function validateCompletion(output: RouterCompletion): void {
99
- if (!output || typeof output !== "object" || !Array.isArray(output.content)) {
100
- throw new RouterFrameworkError(
101
- "executor",
102
- "Executor returned an invalid completion.",
103
- "executor_output_invalid",
104
- );
105
- }
106
- validateFinishReason(output.finishReason, completionContract);
107
- if (output.usage !== undefined) validateUsage(output.usage, completionContract);
108
- for (const item of output.content) {
109
- if (
110
- item && typeof item === "object" && (
111
- (item.type === "text" && typeof item.text === "string") ||
112
- (
113
- item.type === "reasoning" &&
114
- typeof item.text === "string" &&
115
- (item.redacted === undefined || typeof item.redacted === "boolean") &&
116
- (item.source === undefined || (
117
- typeof item.source.provider === "string" &&
118
- typeof item.source.api === "string" &&
119
- typeof item.source.model === "string"
120
- )) &&
121
- (item.continuation === undefined || (
122
- typeof item.continuation === "object" && item.continuation !== null
123
- ))
124
- ) ||
125
- (
126
- item.type === "tool_call" &&
127
- typeof item.id === "string" && item.id.length > 0 &&
128
- typeof item.name === "string" && item.name.length > 0 &&
129
- (typeof item.arguments === "string" || (
130
- typeof item.arguments === "object" && item.arguments !== null && !Array.isArray(item.arguments)
131
- ))
132
- ) ||
133
- (
134
- item.type === "hosted_tool_call" &&
135
- typeof item.id === "string" && item.id.length > 0 &&
136
- item.tool === "web_search" &&
137
- item.providerType === "web_search_call" &&
138
- typeof item.payload === "object" && item.payload !== null && !Array.isArray(item.payload)
139
- )
140
- )
141
- ) continue;
142
- throw new RouterFrameworkError(
143
- "executor",
144
- "Executor returned an invalid completion content item.",
145
- "executor_output_invalid",
146
- );
147
- }
148
- }
149
-
150
- function validateFinishReason(
151
- value: unknown,
152
- contractError: (message: string) => RouterFrameworkError = streamContract,
153
- ): asserts value is RouterFinishReason {
154
- if (value !== "stop" && value !== "length" && value !== "tool_calls") {
155
- throw contractError("Executor returned an invalid finish reason.");
156
- }
157
- }
158
-
159
- function validateUsage(
160
- usage: NonNullable<RouterCompletion["usage"]>,
161
- contractError: (message: string) => RouterFrameworkError = streamContract,
162
- ): void {
163
- if (!usage || typeof usage !== "object") {
164
- throw contractError("Executor usage must be an object.");
165
- }
166
- for (const [name, value, required] of [
167
- ["inputTokens", usage.inputTokens, true],
168
- ["outputTokens", usage.outputTokens, true],
169
- ["cacheReadTokens", usage.cacheReadTokens, false],
170
- ["cacheWriteTokens", usage.cacheWriteTokens, false],
171
- ["totalTokens", usage.totalTokens, false],
172
- ] as const) {
173
- if ((required && value === undefined) || (
174
- value !== undefined && (!Number.isSafeInteger(value) || value < 0)
175
- )) {
176
- throw contractError(
177
- `Executor usage ${name} must be ${required ? "a" : "an optional"} non-negative integer.`,
178
- );
179
- }
180
- }
181
- }
182
-
183
- export function validatePolicyDecision<Metadata>(
184
- value: RoutingPolicyDecision,
185
- candidates: readonly RouterCandidate<Metadata>[],
186
- ): RouterSelection<Metadata> {
187
- if (!value || typeof value !== "object" || typeof value.model !== "string") {
188
- throw new RouterFrameworkError(
189
- "policy",
190
- "Routing policy returned an invalid decision.",
191
- "policy_invalid_decision",
192
- );
193
- }
194
- const candidate = candidates.find((item) => item.id === value.model);
195
- if (candidate === undefined) {
196
- throw new RouterFrameworkError(
197
- "policy",
198
- `Routing policy selected unconfigured or ineligible model '${value.model}'.`,
199
- "policy_invalid_model",
200
- );
201
- }
202
- const reasoningEffort = value.reasoningEffort ?? candidate.defaultReasoningEffort;
203
- if (!isReasoningEffort(reasoningEffort) || !candidate.reasoningEfforts.includes(reasoningEffort)) {
204
- throw new RouterFrameworkError(
205
- "policy",
206
- `Routing policy selected unsupported reasoning effort '${String(reasoningEffort)}' for '${candidate.id}'.`,
207
- "policy_invalid_reasoning_effort",
208
- );
209
- }
210
- const reason = typeof value.reason === "string" && value.reason.trim()
211
- ? value.reason.trim()
212
- : `Routing policy selected ${candidate.id}/${reasoningEffort}.`;
213
- const leaseTurnsRemaining = value.leaseTurnsRemaining;
214
- if (leaseTurnsRemaining !== undefined && (
215
- typeof leaseTurnsRemaining !== "number" ||
216
- !Number.isInteger(leaseTurnsRemaining) ||
217
- leaseTurnsRemaining < 0
218
- )) {
219
- throw new RouterFrameworkError(
220
- "policy",
221
- `Routing policy returned invalid leaseTurnsRemaining '${String(leaseTurnsRemaining)}'.`,
222
- "policy_invalid_lease_turns",
223
- );
224
- }
225
- return {
226
- decision: {
227
- selectedModel: candidate.id,
228
- reasoningEffort,
229
- reason,
230
- ...(leaseTurnsRemaining === undefined ? {} : { leaseTurnsRemaining }),
231
- },
232
- candidates,
233
- ...(value.details === undefined ? {} : { policyDetails: value.details }),
234
- };
235
- }
package/src/json.ts DELETED
@@ -1,43 +0,0 @@
1
- // Small JSON-shape validators shared by modules that consume data across a
2
- // serialization boundary (recorded routing states, selector inputs, policy
3
- // completions). Errors read as `<label> <requirement>` so callers can pass
4
- // precise field paths.
5
-
6
- export type JsonValue =
7
- | string
8
- | number
9
- | boolean
10
- | null
11
- | JsonValue[]
12
- | { [key: string]: JsonValue };
13
-
14
- export type JsonObject = { [key: string]: JsonValue };
15
-
16
- export function requiredString(value: unknown, label: string): string {
17
- if (typeof value !== "string" || value.length === 0) {
18
- throw new Error(`${label} must be a non-empty string`);
19
- }
20
- return value;
21
- }
22
-
23
- export function jsonValue(value: unknown, label: string): JsonValue {
24
- if (value === undefined) throw new Error(`${label} is missing`);
25
- const serialized = JSON.stringify(value);
26
- if (serialized === undefined) throw new Error(`${label} is not JSON serializable`);
27
- return JSON.parse(serialized) as JsonValue;
28
- }
29
-
30
- export function jsonRecord(value: unknown, label: string): Record<string, JsonValue> {
31
- if (typeof value !== "object" || value === null || Array.isArray(value)) {
32
- throw new Error(`${label} must be an object`);
33
- }
34
- return value as Record<string, JsonValue>;
35
- }
36
-
37
- export function withoutKeys(
38
- value: Readonly<Record<string, JsonValue>>,
39
- keys: readonly string[],
40
- ): JsonObject {
41
- const omitted = new Set(keys);
42
- return Object.fromEntries(Object.entries(value).filter(([key]) => !omitted.has(key)));
43
- }
package/src/model_ids.ts DELETED
@@ -1,29 +0,0 @@
1
- import { RouterCoreError } from "./errors.js";
2
- import type { Provider } from "./types.js";
3
-
4
- const PROVIDER_PATTERN = /^[a-z][a-z0-9_.-]{0,119}$/;
5
-
6
- export function providerForModel(model: string): Provider {
7
- const slash = model.indexOf("/");
8
- if (slash <= 0) {
9
- throw new RouterCoreError("invalid_request", `Unsupported model provider for ${model}`, "unsupported_model", "model");
10
- }
11
- const provider = model.slice(0, slash).toLowerCase();
12
- if (!PROVIDER_PATTERN.test(provider)) {
13
- throw new RouterCoreError("invalid_request", `Unsupported model provider for ${model}`, "unsupported_model", "model");
14
- }
15
- return provider;
16
- }
17
-
18
- export function nativeModelId(model: string): string {
19
- return model.slice(`${providerForModel(model)}/`.length);
20
- }
21
-
22
- // providerForModel() lowercases the provider prefix, so lookups keyed on
23
- // model ids must use the same canonical spelling; the model segment stays
24
- // case-sensitive (native ids can be), matching providerForModel semantics.
25
- export function canonicalModelId(modelId: string): string {
26
- const slash = modelId.indexOf("/");
27
- if (slash <= 0) return modelId;
28
- return modelId.slice(0, slash).toLowerCase() + modelId.slice(slash);
29
- }