@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.
- package/README.md +89 -60
- package/dist/anonymous_actions.js +32 -10
- package/dist/anonymous_actions.js.map +1 -1
- package/dist/auto_router.d.ts +4 -1
- package/dist/auto_router.js +3 -3
- package/dist/auto_router.js.map +1 -1
- package/dist/cache_behavior.d.ts +8 -6
- package/dist/cache_behavior.js +56 -30
- package/dist/cache_behavior.js.map +1 -1
- package/dist/cache_scope.d.ts +2 -2
- package/dist/cache_scope.js +23 -18
- package/dist/cache_scope.js.map +1 -1
- package/dist/cost.d.ts +3 -2
- package/dist/cost.js +32 -23
- package/dist/cost.js.map +1 -1
- package/dist/dari_policy.d.ts +8 -2
- package/dist/dari_policy.js +39 -1
- package/dist/dari_policy.js.map +1 -1
- package/dist/eval_score_imputation.d.ts +26 -0
- package/dist/eval_score_imputation.js +83 -0
- package/dist/eval_score_imputation.js.map +1 -0
- package/dist/framework_lifecycle.d.ts +1 -1
- package/dist/framework_lifecycle.js +4 -1
- package/dist/framework_lifecycle.js.map +1 -1
- package/dist/framework_models.js +6 -0
- package/dist/framework_models.js.map +1 -1
- package/dist/framework_router.js +22 -11
- package/dist/framework_router.js.map +1 -1
- package/dist/framework_types.d.ts +9 -5
- package/dist/index.d.ts +9 -35
- package/dist/index.js +6 -37
- package/dist/index.js.map +1 -1
- package/dist/model_ids.js +4 -0
- package/dist/model_ids.js.map +1 -1
- package/dist/pi_runtime.js +19 -11
- package/dist/pi_runtime.js.map +1 -1
- package/dist/pi_types.d.ts +2 -2
- package/{src/index.ts → dist/policy-engine.d.ts} +2 -14
- package/dist/policy-engine.js +25 -0
- package/dist/policy-engine.js.map +1 -0
- package/dist/prompts.d.ts +1 -1
- package/dist/prompts.js +3 -3
- package/dist/prompts.js.map +1 -1
- package/dist/protocols.d.ts +5 -0
- package/dist/protocols.js +6 -0
- package/dist/protocols.js.map +1 -0
- package/dist/route.d.ts +3 -4
- package/dist/route.js +17 -10
- package/dist/route.js.map +1 -1
- package/dist/selector_input.d.ts +1 -0
- package/dist/selector_input.js +66 -78
- package/dist/selector_input.js.map +1 -1
- package/dist/selector_parse.d.ts +1 -0
- package/dist/selector_parse.js +2 -1
- package/dist/selector_parse.js.map +1 -1
- package/dist/selector_request.d.ts +1 -0
- package/dist/selector_request.js +1 -0
- package/dist/selector_request.js.map +1 -1
- package/dist/types.d.ts +1 -1
- package/examples/auto_router.ts +40 -0
- package/examples/basic_route.ts +130 -0
- package/examples/create_router.ts +114 -0
- package/examples/dari_policy.ts +86 -0
- package/examples/lease_stores.ts +159 -0
- package/examples/managed/README.md +50 -0
- package/examples/managed/byok-router/README.md +13 -0
- package/examples/managed/byok-router/router.yml +13 -0
- package/examples/managed/custom-rules-router/README.md +12 -0
- package/examples/managed/custom-rules-router/router.yml +24 -0
- package/examples/managed/eval-router/README.md +18 -0
- package/examples/managed/eval-router/router.yml +11 -0
- package/examples/managed/slm-router/README.md +12 -0
- package/examples/managed/slm-router/router.yml +8 -0
- package/examples/pi_runtime.ts +55 -0
- package/package.json +26 -7
- package/src/anonymous_actions.ts +0 -434
- package/src/auto_router.ts +0 -95
- package/src/cache_behavior.ts +0 -130
- package/src/cache_scope.ts +0 -58
- package/src/cache_switch_decision.ts +0 -53
- package/src/candidate_pruning.ts +0 -257
- package/src/candidate_resolution.ts +0 -208
- package/src/compatibility.ts +0 -162
- package/src/continuation_state.ts +0 -368
- package/src/cost.ts +0 -455
- package/src/dari_policy.ts +0 -360
- package/src/errors.ts +0 -27
- package/src/fingerprint.ts +0 -148
- package/src/fixed_turn_cost_config.ts +0 -24
- package/src/framework_error.ts +0 -38
- package/src/framework_lifecycle.ts +0 -74
- package/src/framework_models.ts +0 -186
- package/src/framework_router.ts +0 -490
- package/src/framework_streaming.ts +0 -613
- package/src/framework_types.ts +0 -372
- package/src/framework_validation.ts +0 -235
- package/src/json.ts +0 -43
- package/src/model_ids.ts +0 -29
- package/src/pi_context.ts +0 -251
- package/src/pi_options.ts +0 -295
- package/src/pi_output.ts +0 -387
- package/src/pi_reasoning_signature.ts +0 -41
- package/src/pi_runtime.ts +0 -238
- package/src/pi_types.ts +0 -71
- package/src/portable_reasoning_state.ts +0 -129
- package/src/previous_decision.ts +0 -109
- package/src/prompts.ts +0 -69
- package/src/protocol_anthropic.ts +0 -571
- package/src/protocol_openai_chat.ts +0 -645
- package/src/protocol_validation.ts +0 -202
- package/src/route.ts +0 -490
- package/src/routing_history.ts +0 -157
- package/src/selector_input.ts +0 -246
- package/src/selector_parse.ts +0 -138
- package/src/selector_request.ts +0 -130
- package/src/selector_truncation.ts +0 -48
- package/src/selector_wire.ts +0 -167
- package/src/types.ts +0 -231
package/src/framework_types.ts
DELETED
|
@@ -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
|
-
}
|