@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,571 +0,0 @@
1
- import {
2
- decodeUntrustedProviderContinuationState,
3
- encodeProviderContinuationState,
4
- type ProviderContinuationState,
5
- } from "./continuation_state.js";
6
- import {
7
- decodeUntrustedPortableReasoningState,
8
- encodePortableReasoningState,
9
- } from "./portable_reasoning_state.js";
10
- import { RouterFrameworkError } from "./framework_error.js";
11
- import {
12
- dariRoutingPayload,
13
- invalidProtocolRequest as invalid,
14
- isAllEmptyText,
15
- optionalBoolean,
16
- optionalCacheKey,
17
- optionalNumberInRange,
18
- optionalRecord,
19
- optionalString,
20
- portableTool,
21
- positiveInteger,
22
- prefixedWireId,
23
- rejectUnknownFields,
24
- requiredString,
25
- stopSequenceValues,
26
- validateToolChoice,
27
- } from "./protocol_validation.js";
28
- import { isReasoningEffort, isRecord, type ReasoningEffort } from "./types.js";
29
- import type {
30
- RouterCompletion,
31
- RouterContent,
32
- RouterRequest,
33
- RouterResponseFormat,
34
- RouterSelection,
35
- RouterTool,
36
- RouterToolChoice,
37
- } from "./framework_types.js";
38
-
39
- export function anthropicRequest(payload: unknown): RouterRequest {
40
- if (!isRecord(payload)) throw invalid("Request body must be a JSON object.");
41
- rejectUnknownFields(payload, [
42
- "model",
43
- "max_tokens",
44
- "messages",
45
- "system",
46
- "tools",
47
- "tool_choice",
48
- "thinking",
49
- "output_config",
50
- "temperature",
51
- "top_p",
52
- "stop_sequences",
53
- "stream",
54
- "metadata",
55
- // Dari extension: optional conversation key for lease correlation.
56
- "prompt_cache_key",
57
- ]);
58
-
59
- const requestedModel = requiredString(payload.model, "model");
60
- const maxOutputTokens = positiveInteger(payload.max_tokens, "max_tokens");
61
- if (!Array.isArray(payload.messages) || payload.messages.length === 0) {
62
- throw invalid("messages must be a non-empty array.", "messages");
63
- }
64
-
65
- const items: RouterRequest["items"] = [];
66
- const system = anthropicSystem(payload.system);
67
- if (system.length > 0) {
68
- items.push({ type: "message", role: "system", content: system });
69
- }
70
- for (const [messageIndex, message] of payload.messages.entries()) {
71
- const param = `messages.${messageIndex}`;
72
- if (!isRecord(message)) throw invalid(`${param} must be an object.`, param);
73
- rejectUnknownFields(message, ["role", "content"], param);
74
- const role = requiredString(message.role, `${param}.role`);
75
- if (role !== "user" && role !== "assistant") {
76
- throw invalid(`${param}.role must be 'user' or 'assistant'.`, `${param}.role`);
77
- }
78
- appendAnthropicContent(items, role, message.content, param);
79
- }
80
-
81
- const tools = anthropicTools(payload.tools);
82
- const toolChoice = anthropicToolChoice(payload.tool_choice);
83
- validateToolChoice(toolChoice.choice, tools);
84
- if (toolChoice.parallel !== undefined && tools.length === 0) {
85
- throw invalid("tool_choice requires at least one tool.", "tool_choice");
86
- }
87
- const temperature = optionalNumberInRange(payload.temperature, "temperature", 0, 1);
88
- const topP = optionalNumberInRange(payload.top_p, "top_p", 0, 1);
89
- const stop = stopSequenceValues(payload.stop_sequences, "stop_sequences", false);
90
- const outputConfig = anthropicOutputConfig(payload.output_config);
91
- const reasoning = anthropicReasoning(payload.thinking, outputConfig.effort);
92
- const responseFormat = outputConfig.format;
93
- const stream = optionalBoolean(payload.stream, "stream") ?? false;
94
- const cacheKey = optionalCacheKey(payload.prompt_cache_key, "prompt_cache_key");
95
- const metadata = optionalRecord(payload.metadata, "metadata");
96
- const user = metadata === undefined
97
- ? undefined
98
- : optionalString(metadata.user_id, "metadata.user_id");
99
-
100
- return {
101
- protocol: "anthropic_messages",
102
- requestedModel,
103
- items,
104
- tools,
105
- ...(toolChoice.choice === undefined ? {} : { toolChoice: toolChoice.choice }),
106
- ...(toolChoice.parallel === undefined ? {} : { parallelToolCalls: toolChoice.parallel }),
107
- generation: {
108
- maxOutputTokens,
109
- ...(temperature === undefined ? {} : { temperature }),
110
- ...(topP === undefined ? {} : { topP }),
111
- ...(stop === undefined ? {} : { stop }),
112
- },
113
- ...(reasoning === undefined ? {} : { reasoning }),
114
- ...(responseFormat === undefined ? {} : { responseFormat }),
115
- stream,
116
- ...(cacheKey === undefined ? {} : { cacheKey }),
117
- ...(metadata === undefined ? {} : { metadata }),
118
- ...(user === undefined ? {} : { user }),
119
- };
120
- }
121
-
122
- export function anthropicResponse<Metadata>(args: {
123
- id: string;
124
- model: string;
125
- requestedModel: string;
126
- output: RouterCompletion;
127
- selection: RouterSelection<Metadata>;
128
- }): Record<string, unknown> {
129
- return {
130
- id: prefixedWireId(args.output.id ?? args.id, "msg_"),
131
- type: "message",
132
- role: "assistant",
133
- model: args.model,
134
- content: anthropicOutputContent(args.output),
135
- stop_reason: anthropicStopReason(args.output.finishReason),
136
- stop_sequence: null,
137
- usage: anthropicUsage(args.output.usage),
138
- dari_routing: dariRoutingPayload(args.requestedModel, args.selection),
139
- };
140
- }
141
-
142
- export function anthropicOutputContent(output: RouterCompletion): Array<Record<string, unknown>> {
143
- const content: Array<Record<string, unknown>> = [];
144
- for (const [index, item] of output.content.entries()) {
145
- if (item.type === "text") {
146
- content.push({ type: "text", text: item.text });
147
- continue;
148
- }
149
- if (item.type === "reasoning") {
150
- const encoded = item.continuation
151
- ? encodeProviderContinuationState(item.continuation)
152
- : encodePortableReasoningState({ source: item.source, itemId: `reasoning_${index}` });
153
- if (item.redacted || item.continuation?.kind === "anthropic_redacted_thinking") {
154
- content.push({ type: "redacted_thinking", data: encoded });
155
- } else {
156
- content.push({ type: "thinking", thinking: item.text, signature: encoded });
157
- }
158
- continue;
159
- }
160
- if (item.type === "hosted_tool_call") {
161
- throw new RouterFrameworkError(
162
- "configuration",
163
- "Hosted tool calls are not representable on the anthropic_messages protocol; route hosted-tool models over the OpenAI protocol.",
164
- "hosted_tool_call_unrepresentable",
165
- );
166
- }
167
- content.push({
168
- type: "tool_use",
169
- id: item.id,
170
- name: item.name,
171
- input: toolArguments(item.arguments),
172
- });
173
- }
174
- return content.length > 0 ? content : [{ type: "text", text: "" }];
175
- }
176
-
177
- export function anthropicStopReason(reason: RouterCompletion["finishReason"]): string {
178
- if (reason === "length") return "max_tokens";
179
- if (reason === "tool_calls") return "tool_use";
180
- return "end_turn";
181
- }
182
-
183
- // Anthropic semantics: input_tokens excludes cache reads/writes, which are
184
- // reported as the separate cache_read_input_tokens and
185
- // cache_creation_input_tokens fields — matching RouterUsage, where
186
- // inputTokens is likewise cache-exclusive.
187
- export function anthropicUsage(usage: RouterCompletion["usage"]): Record<string, unknown> {
188
- return {
189
- input_tokens: usage?.inputTokens ?? 0,
190
- output_tokens: usage?.outputTokens ?? 0,
191
- ...(usage?.cacheReadTokens === undefined
192
- ? {}
193
- : { cache_read_input_tokens: usage.cacheReadTokens }),
194
- ...(usage?.cacheWriteTokens === undefined
195
- ? {}
196
- : { cache_creation_input_tokens: usage.cacheWriteTokens }),
197
- };
198
- }
199
-
200
- export function anthropicMessageStart<Metadata>(args: {
201
- id: string;
202
- model: string;
203
- requestedModel: string;
204
- selection: RouterSelection<Metadata>;
205
- }): Record<string, unknown> {
206
- return {
207
- type: "message_start",
208
- message: {
209
- id: prefixedWireId(args.id, "msg_"),
210
- type: "message",
211
- role: "assistant",
212
- model: args.model,
213
- content: [],
214
- stop_reason: null,
215
- stop_sequence: null,
216
- usage: { input_tokens: 0, output_tokens: 0 },
217
- dari_routing: dariRoutingPayload(args.requestedModel, args.selection),
218
- },
219
- };
220
- }
221
-
222
- function appendAnthropicContent(
223
- items: RouterRequest["items"],
224
- role: "user" | "assistant",
225
- value: unknown,
226
- messageParam: string,
227
- ): void {
228
- if (typeof value === "string") {
229
- items.push({ type: "message", role, content: [{ type: "text", text: value }] });
230
- return;
231
- }
232
- if (!Array.isArray(value) || value.length === 0) {
233
- throw invalid(`${messageParam}.content must be a string or non-empty array.`, `${messageParam}.content`);
234
- }
235
-
236
- const hasToolUse = role === "assistant" && value.some(
237
- (block) => isRecord(block) && block.type === "tool_use",
238
- );
239
- let buffered: RouterContent[] = [];
240
- const flush = () => {
241
- if (buffered.length === 0) return;
242
- if (!hasToolUse || !isAllEmptyText(buffered)) {
243
- items.push({ type: "message", role, content: buffered });
244
- }
245
- buffered = [];
246
- };
247
- for (const [blockIndex, block] of value.entries()) {
248
- const param = `${messageParam}.content.${blockIndex}`;
249
- if (!isRecord(block)) throw invalid(`${param} must be an object.`, param);
250
- if (block.type === "text") {
251
- rejectUnknownFields(block, ["type", "text"], param);
252
- if (typeof block.text !== "string") throw invalid(`${param}.text must be a string.`, `${param}.text`);
253
- buffered.push({ type: "text", text: block.text });
254
- continue;
255
- }
256
- if (block.type === "image") {
257
- rejectUnknownFields(block, ["type", "source"], param);
258
- if (role !== "user") throw invalid("Image blocks require the user role.", param);
259
- buffered.push(anthropicImage(block.source, `${param}.source`));
260
- continue;
261
- }
262
- if (block.type === "tool_use") {
263
- rejectUnknownFields(block, ["type", "id", "name", "input"], param);
264
- if (role !== "assistant" || !isRecord(block.input)) {
265
- throw invalid("tool_use blocks require the assistant role and object input.", param);
266
- }
267
- flush();
268
- items.push({
269
- type: "tool_call",
270
- id: requiredString(block.id, `${param}.id`),
271
- name: requiredString(block.name, `${param}.name`),
272
- arguments: block.input,
273
- });
274
- continue;
275
- }
276
- if (block.type === "tool_result") {
277
- rejectUnknownFields(
278
- block,
279
- ["type", "tool_use_id", "content", "is_error"],
280
- param,
281
- );
282
- if (role !== "user") throw invalid("tool_result blocks require the user role.", param);
283
- const isError = optionalBoolean(block.is_error, `${param}.is_error`);
284
- flush();
285
- items.push({
286
- type: "tool_result",
287
- toolCallId: requiredString(block.tool_use_id, `${param}.tool_use_id`),
288
- content: anthropicResultContent(block.content, `${param}.content`),
289
- ...(isError === undefined ? {} : { isError }),
290
- });
291
- continue;
292
- }
293
- if (block.type === "thinking" || block.type === "redacted_thinking") {
294
- if (role !== "assistant") {
295
- throw invalid("Thinking blocks require the assistant role.", param);
296
- }
297
- flush();
298
- items.push(anthropicReasoningItem(block, param));
299
- continue;
300
- }
301
- throw new RouterFrameworkError(
302
- "invalid_request",
303
- `Anthropic content block '${String(block.type ?? "unknown")}' is not supported.`,
304
- "unsupported_content_block",
305
- `${param}.type`,
306
- );
307
- }
308
- flush();
309
- }
310
-
311
- function anthropicReasoningItem(
312
- block: Record<string, unknown>,
313
- param: string,
314
- ): Extract<RouterRequest["items"][number], { type: "reasoning" }> {
315
- const redacted = block.type === "redacted_thinking";
316
- rejectUnknownFields(
317
- block,
318
- redacted ? ["type", "data"] : ["type", "thinking", "signature"],
319
- param,
320
- );
321
- const field = redacted ? "data" : "signature";
322
- const encoded = requiredString(block[field], `${param}.${field}`);
323
- const continuation = decodeUntrustedProviderContinuationState(encoded);
324
- const portable = continuation ? null : decodeUntrustedPortableReasoningState(encoded);
325
- if (!continuation && !portable) {
326
- throw new RouterFrameworkError(
327
- "invalid_request",
328
- `${param}.${field} must be a Dari reasoning-state envelope; raw provider state has no recoverable provider/model provenance.`,
329
- "invalid_provider_continuation_state",
330
- `${param}.${field}`,
331
- );
332
- }
333
- const thinking = redacted ? "" : stringAllowEmpty(block.thinking, `${param}.thinking`);
334
- return {
335
- type: "reasoning",
336
- summary: thinking ? [thinking] : [],
337
- content: [],
338
- ...(continuation
339
- ? { source: continuation.source, continuation }
340
- : portable?.source
341
- ? { source: portable.source }
342
- : {}),
343
- ...(portable?.itemId ? { id: portable.itemId } : {}),
344
- };
345
- }
346
-
347
- function stringAllowEmpty(value: unknown, param: string): string {
348
- if (typeof value !== "string") throw invalid(`${param} must be a string.`, param);
349
- return value;
350
- }
351
-
352
- function anthropicSystem(value: unknown): RouterContent[] {
353
- if (value === undefined || value === null) return [];
354
- if (typeof value === "string") return [{ type: "text", text: value }];
355
- if (!Array.isArray(value)) throw invalid("system must be a string or array.", "system");
356
- return value.map((block, index) => {
357
- if (!isRecord(block) || block.type !== "text" || typeof block.text !== "string") {
358
- throw new RouterFrameworkError(
359
- "invalid_request",
360
- "Only text system blocks are supported.",
361
- "unsupported_content_block",
362
- `system.${index}`,
363
- );
364
- }
365
- rejectUnknownFields(block, ["type", "text"], `system.${index}`);
366
- return { type: "text" as const, text: block.text };
367
- });
368
- }
369
-
370
- function anthropicImage(value: unknown, param: string): RouterContent {
371
- if (!isRecord(value)) throw invalid(`${param} must be an object.`, param);
372
- if (value.type === "base64") {
373
- rejectUnknownFields(value, ["type", "media_type", "data"], param);
374
- const mediaType = requiredString(value.media_type, `${param}.media_type`);
375
- if (!mediaType.startsWith("image/")) {
376
- throw invalid(
377
- `${param}.media_type must be an image MIME type.`,
378
- `${param}.media_type`,
379
- );
380
- }
381
- return {
382
- type: "image",
383
- url: `data:${mediaType};base64,${requiredString(value.data, `${param}.data`)}`,
384
- };
385
- }
386
- if (value.type === "url") {
387
- rejectUnknownFields(value, ["type", "url"], param);
388
- return { type: "image", url: requiredString(value.url, `${param}.url`) };
389
- }
390
- throw new RouterFrameworkError(
391
- "invalid_request",
392
- `Anthropic image source '${String(value.type ?? "unknown")}' is not supported.`,
393
- "unsupported_image_source",
394
- `${param}.type`,
395
- );
396
- }
397
-
398
- function anthropicResultContent(value: unknown, param: string): RouterContent[] {
399
- if (value === undefined || value === null) return [];
400
- if (typeof value === "string") return [{ type: "text", text: value }];
401
- if (!Array.isArray(value)) throw invalid(`${param} must be a string or array.`, param);
402
- return value.map((block, index) => {
403
- if (!isRecord(block)) throw invalid(`${param}.${index} must be an object.`, `${param}.${index}`);
404
- if (block.type === "text" && typeof block.text === "string") {
405
- rejectUnknownFields(block, ["type", "text"], `${param}.${index}`);
406
- return { type: "text" as const, text: block.text };
407
- }
408
- if (block.type === "image") {
409
- rejectUnknownFields(block, ["type", "source"], `${param}.${index}`);
410
- return anthropicImage(block.source, `${param}.${index}.source`);
411
- }
412
- throw new RouterFrameworkError(
413
- "invalid_request",
414
- `Anthropic tool result block '${String(block.type ?? "unknown")}' is not supported.`,
415
- "unsupported_content_block",
416
- `${param}.${index}.type`,
417
- );
418
- });
419
- }
420
-
421
- function anthropicTools(value: unknown): RouterTool[] {
422
- if (value === undefined || value === null) return [];
423
- if (!Array.isArray(value)) throw invalid("tools must be an array.", "tools");
424
- const names = new Set<string>();
425
- return value.map((tool, index) => {
426
- const param = `tools.${index}`;
427
- if (!isRecord(tool) || (tool.type !== undefined && tool.type !== "custom")) {
428
- throw new RouterFrameworkError(
429
- "invalid_request",
430
- `${param} must be an Anthropic custom tool.`,
431
- "unsupported_tool",
432
- param,
433
- );
434
- }
435
- rejectUnknownFields(
436
- tool,
437
- ["type", "name", "description", "input_schema", "strict"],
438
- param,
439
- );
440
- return portableTool(tool, {
441
- names,
442
- param,
443
- schemaField: "input_schema",
444
- schemaRequired: true,
445
- });
446
- });
447
- }
448
-
449
- function anthropicToolChoice(value: unknown): {
450
- choice?: RouterToolChoice;
451
- parallel?: boolean;
452
- } {
453
- if (value === undefined || value === null) return {};
454
- if (!isRecord(value)) throw invalid("tool_choice must be an object.", "tool_choice");
455
- const type = requiredString(value.type, "tool_choice.type");
456
- rejectUnknownFields(
457
- value,
458
- type === "tool"
459
- ? ["type", "name", "disable_parallel_tool_use"]
460
- : ["type", "disable_parallel_tool_use"],
461
- "tool_choice",
462
- );
463
- let choice: RouterToolChoice;
464
- if (type === "auto" || type === "none") choice = type;
465
- else if (type === "any") choice = "required";
466
- else if (type === "tool") choice = { type: "tool", name: requiredString(value.name, "tool_choice.name") };
467
- else throw invalid(`tool_choice.type '${type}' is not supported.`, "tool_choice.type");
468
- const disabled = optionalBoolean(value.disable_parallel_tool_use, "tool_choice.disable_parallel_tool_use");
469
- return { choice, ...(disabled === undefined ? {} : { parallel: !disabled }) };
470
- }
471
-
472
- function anthropicReasoning(
473
- thinkingValue: unknown,
474
- effort: ReasoningEffort | undefined,
475
- ): RouterRequest["reasoning"] {
476
- if (thinkingValue === undefined || thinkingValue === null) {
477
- return effort === undefined ? undefined : { effort, enabled: effort !== "off" };
478
- }
479
- if (!isRecord(thinkingValue)) throw invalid("thinking must be an object.", "thinking");
480
- const type = requiredString(thinkingValue.type, "thinking.type");
481
- rejectUnknownFields(
482
- thinkingValue,
483
- type === "enabled" ? ["type", "budget_tokens"] : ["type"],
484
- "thinking",
485
- );
486
- if (type === "disabled") {
487
- if (effort !== undefined && effort !== "off") {
488
- throw invalid("Disabled thinking requires output_config.effort 'off'.", "output_config.effort");
489
- }
490
- return { effort: "off", enabled: false };
491
- }
492
- if (type !== "enabled" && type !== "adaptive") {
493
- throw invalid(`thinking.type '${type}' is not supported.`, "thinking.type");
494
- }
495
- if (effort === "off") {
496
- throw invalid("Enabled thinking cannot use output_config.effort 'off'.", "output_config.effort");
497
- }
498
- return {
499
- ...(effort === undefined ? {} : { effort }),
500
- enabled: true,
501
- ...(type === "enabled"
502
- ? { budgetTokens: positiveInteger(thinkingValue.budget_tokens, "thinking.budget_tokens") }
503
- : {}),
504
- };
505
- }
506
-
507
- function anthropicOutputConfig(value: unknown): {
508
- effort?: ReasoningEffort;
509
- format?: RouterResponseFormat;
510
- } {
511
- if (value === undefined || value === null) return {};
512
- if (!isRecord(value)) throw invalid("output_config must be an object.", "output_config");
513
- rejectUnknownFields(value, ["effort", "format"], "output_config");
514
- let effort: ReasoningEffort | undefined;
515
- if (value.effort !== undefined && value.effort !== null) {
516
- if (!isReasoningEffort(value.effort)) {
517
- throw invalid("output_config.effort is not supported.", "output_config.effort");
518
- }
519
- effort = value.effort;
520
- }
521
- const formatValue = value.format;
522
- if (formatValue === undefined || formatValue === null) {
523
- return effort === undefined ? {} : { effort };
524
- }
525
- if (!isRecord(formatValue) || formatValue.type !== "json_schema" || !isRecord(formatValue.schema)) {
526
- throw new RouterFrameworkError(
527
- "invalid_request",
528
- "Only Anthropic json_schema output formats are supported.",
529
- "unsupported_response_format",
530
- "output_config.format",
531
- );
532
- }
533
- rejectUnknownFields(
534
- formatValue,
535
- ["type", "name", "schema"],
536
- "output_config.format",
537
- );
538
- return {
539
- ...(effort === undefined ? {} : { effort }),
540
- format: {
541
- type: "json_schema",
542
- name: optionalString(formatValue.name, "output_config.format.name") ?? "response",
543
- schema: formatValue.schema,
544
- },
545
- };
546
- }
547
-
548
- function toolArguments(value: string | Record<string, unknown>): Record<string, unknown> {
549
- if (isRecord(value)) return value;
550
- let parsed: unknown;
551
- try {
552
- parsed = JSON.parse(value);
553
- } catch (error) {
554
- throw new RouterFrameworkError(
555
- "executor",
556
- "Executor returned tool arguments that are not valid JSON.",
557
- "executor_output_invalid",
558
- undefined,
559
- { cause: error },
560
- );
561
- }
562
- if (!isRecord(parsed)) {
563
- throw new RouterFrameworkError(
564
- "executor",
565
- "Executor returned tool arguments that are not a JSON object.",
566
- "executor_output_invalid",
567
- );
568
- }
569
- return parsed;
570
- }
571
-