@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,645 +0,0 @@
1
- import {
2
- decodeUntrustedProviderContinuationState,
3
- encodeProviderContinuationState,
4
- type ProviderContinuationState,
5
- type ProviderIdentity,
6
- } from "./continuation_state.js";
7
- import {
8
- decodeUntrustedPortableReasoningState,
9
- encodePortableReasoningState,
10
- } from "./portable_reasoning_state.js";
11
- import { RouterFrameworkError } from "./framework_error.js";
12
- import {
13
- dariRoutingPayload,
14
- invalidProtocolRequest as invalid,
15
- isAllEmptyText,
16
- optionalBoolean,
17
- optionalCacheKey,
18
- optionalNumberInRange,
19
- optionalPositiveInteger,
20
- optionalRecord,
21
- optionalString,
22
- portableTool,
23
- prefixedWireId,
24
- rejectUnknownFields,
25
- requiredString,
26
- stopSequenceValues,
27
- validateToolChoice,
28
- } from "./protocol_validation.js";
29
- import { isReasoningEffort, isRecord } from "./types.js";
30
- import {
31
- isRouterHostedToolCallStatus,
32
- type RouterCompletion,
33
- type RouterContent,
34
- type RouterHostedToolCallStatus,
35
- type RouterOutputHostedToolCall,
36
- type RouterRequest,
37
- type RouterResponseFormat,
38
- type RouterSelection,
39
- type RouterStreamEvent,
40
- type RouterTool,
41
- type RouterToolChoice,
42
- } from "./framework_types.js";
43
-
44
- export function openAIChatRequest(payload: unknown): RouterRequest {
45
- if (!isRecord(payload)) throw invalid("Request body must be a JSON object.");
46
- rejectUnknownFields(payload, [
47
- "model",
48
- "messages",
49
- "stream",
50
- "stream_options",
51
- "n",
52
- "tools",
53
- "tool_choice",
54
- "parallel_tool_calls",
55
- "temperature",
56
- "top_p",
57
- "max_completion_tokens",
58
- "max_tokens",
59
- "stop",
60
- "reasoning_effort",
61
- "response_format",
62
- "prompt_cache_key",
63
- "metadata",
64
- "user",
65
- ]);
66
-
67
- const requestedModel = requiredString(payload.model, "model");
68
- if (!Array.isArray(payload.messages) || payload.messages.length === 0) {
69
- throw invalid("messages must be a non-empty array.", "messages");
70
- }
71
- if (payload.n !== undefined && payload.n !== 1) {
72
- throw invalid("Only n=1 is supported.", "n");
73
- }
74
- if (payload.max_completion_tokens != null && payload.max_tokens != null) {
75
- throw invalid(
76
- "max_completion_tokens and max_tokens cannot both be provided.",
77
- "max_completion_tokens",
78
- );
79
- }
80
-
81
- const items: RouterRequest["items"] = [];
82
- for (const [messageIndex, value] of payload.messages.entries()) {
83
- const param = `messages.${messageIndex}`;
84
- if (!isRecord(value)) throw invalid(`${param} must be an object.`, param);
85
- const role = requiredString(value.role, `${param}.role`);
86
- if (role === "tool") {
87
- rejectUnknownFields(value, ["role", "content", "tool_call_id"], param);
88
- items.push({
89
- type: "tool_result",
90
- toolCallId: requiredString(value.tool_call_id, `${param}.tool_call_id`),
91
- content: openAIContent(value.content, `${param}.content`),
92
- });
93
- continue;
94
- }
95
- if (role !== "system" && role !== "developer" && role !== "user" && role !== "assistant") {
96
- throw invalid(`${param}.role is not supported.`, `${param}.role`);
97
- }
98
- rejectUnknownFields(
99
- value,
100
- role === "assistant"
101
- ? [
102
- "role",
103
- "content",
104
- "tool_calls",
105
- "reasoning_content",
106
- "reasoning",
107
- "reasoning_text",
108
- "reasoning_details",
109
- ]
110
- : ["role", "content"],
111
- param,
112
- );
113
- let reasoningItemCount = 0;
114
- if (role === "assistant") {
115
- const reasoningItems = openAIReasoningItems(value, messageIndex);
116
- reasoningItemCount = reasoningItems.length;
117
- items.push(...reasoningItems);
118
- }
119
- const toolCalls = value.tool_calls;
120
- if (toolCalls !== undefined && !Array.isArray(toolCalls)) {
121
- throw invalid(`${param}.tool_calls must be an array.`, `${param}.tool_calls`);
122
- }
123
- const hasToolCalls = toolCalls !== undefined && toolCalls.length > 0;
124
- let content = openAIContent(value.content, `${param}.content`);
125
- if (hasToolCalls && isAllEmptyText(content)) {
126
- content = [];
127
- }
128
- if (
129
- role === "assistant" &&
130
- content.length === 0 &&
131
- !hasToolCalls &&
132
- reasoningItemCount === 0
133
- ) {
134
- throw invalid(`${param} is an empty assistant message with no tool calls.`, param);
135
- }
136
- if (content.length > 0 || (!hasToolCalls && reasoningItemCount === 0)) {
137
- items.push({ type: "message", role, content });
138
- }
139
- if (toolCalls === undefined) continue;
140
- for (const [callIndex, callValue] of toolCalls.entries()) {
141
- const callParam = `${param}.tool_calls.${callIndex}`;
142
- if (!isRecord(callValue) || callValue.type !== "function") {
143
- throw invalid(`${callParam} must be a function tool call.`, callParam);
144
- }
145
- rejectUnknownFields(callValue, ["id", "type", "function"], callParam);
146
- if (!isRecord(callValue.function)) {
147
- throw invalid(`${callParam}.function must be an object.`, `${callParam}.function`);
148
- }
149
- rejectUnknownFields(
150
- callValue.function,
151
- ["name", "arguments"],
152
- `${callParam}.function`,
153
- );
154
- const argumentsValue = callValue.function.arguments;
155
- if (typeof argumentsValue !== "string" && !isRecord(argumentsValue)) {
156
- throw invalid(
157
- `${callParam}.function.arguments must be a JSON string or object.`,
158
- `${callParam}.function.arguments`,
159
- );
160
- }
161
- const id = requiredString(callValue.id, `${callParam}.id`);
162
- const name = requiredString(callValue.function.name, `${callParam}.function.name`);
163
- const hosted = openAIHostedToolCall(
164
- id,
165
- name,
166
- argumentsValue,
167
- items.filter((item) => item.type === "reasoning"),
168
- `${callParam}.function.arguments`,
169
- );
170
- if (hosted !== null) items.push(hosted);
171
- else items.push({
172
- type: "tool_call",
173
- id,
174
- name,
175
- arguments: argumentsValue,
176
- });
177
- }
178
- }
179
-
180
- const tools = openAITools(payload.tools);
181
- const toolChoice = openAIToolChoice(payload.tool_choice);
182
- validateToolChoice(toolChoice, tools);
183
- const parallelToolCalls = optionalBoolean(
184
- payload.parallel_tool_calls,
185
- "parallel_tool_calls",
186
- );
187
- if (parallelToolCalls !== undefined && tools.length === 0) {
188
- throw invalid("parallel_tool_calls requires at least one tool.", "parallel_tool_calls");
189
- }
190
- const temperature = optionalNumberInRange(payload.temperature, "temperature", 0, 2);
191
- const topP = optionalNumberInRange(payload.top_p, "top_p", 0, 1);
192
- const maxOutputTokens = optionalPositiveInteger(
193
- payload.max_completion_tokens ?? payload.max_tokens,
194
- payload.max_completion_tokens == null ? "max_tokens" : "max_completion_tokens",
195
- );
196
- const stop = stopSequenceValues(payload.stop, "stop", true);
197
- const reasoning = openAIReasoning(payload.reasoning_effort);
198
- const responseFormat = openAIResponseFormat(payload.response_format);
199
- const cacheKey = optionalCacheKey(payload.prompt_cache_key, "prompt_cache_key");
200
- const stream = optionalBoolean(payload.stream, "stream") ?? false;
201
- // Accepted for ecosystem compatibility (Vercel AI SDK, LiteLLM) and ignored:
202
- // usage is always emitted by the streaming layer.
203
- optionalRecord(payload.stream_options, "stream_options");
204
- const metadata = optionalRecord(payload.metadata, "metadata");
205
- const user = optionalString(payload.user, "user");
206
-
207
- return {
208
- protocol: "openai_chat_completions",
209
- requestedModel,
210
- items,
211
- tools,
212
- ...(toolChoice === undefined ? {} : { toolChoice }),
213
- ...(parallelToolCalls === undefined ? {} : { parallelToolCalls }),
214
- generation: {
215
- ...(temperature === undefined ? {} : { temperature }),
216
- ...(topP === undefined ? {} : { topP }),
217
- ...(maxOutputTokens === undefined ? {} : { maxOutputTokens }),
218
- ...(stop === undefined ? {} : { stop }),
219
- },
220
- ...(reasoning === undefined ? {} : { reasoning }),
221
- ...(responseFormat === undefined ? {} : { responseFormat }),
222
- ...(cacheKey === undefined ? {} : { cacheKey }),
223
- stream,
224
- ...(metadata === undefined ? {} : { metadata }),
225
- ...(user === undefined ? {} : { user }),
226
- };
227
- }
228
-
229
- export function openAIChatResponse<Metadata>(args: {
230
- id: string;
231
- model: string;
232
- requestedModel: string;
233
- output: RouterCompletion;
234
- selection: RouterSelection<Metadata>;
235
- }): Record<string, unknown> {
236
- const text = args.output.content
237
- .filter((item) => item.type === "text")
238
- .map((item) => item.text)
239
- .join("");
240
- const toolCalls = args.output.content.flatMap((item) => {
241
- if (item.type === "hosted_tool_call") return [openAIHostedToolCallWire(item)];
242
- return item.type === "tool_call"
243
- ? [{
244
- id: item.id,
245
- type: "function",
246
- function: {
247
- name: item.name,
248
- arguments: typeof item.arguments === "string"
249
- ? item.arguments
250
- : JSON.stringify(item.arguments),
251
- },
252
- }]
253
- : [];
254
- });
255
- const reasoning = args.output.content.filter((item) => item.type === "reasoning");
256
- const reasoningDetails = reasoning.map((item, index) =>
257
- item.continuation
258
- ? encryptedReasoningDetail(index, item.continuation)
259
- : portableReasoningDetail(index, item.source),
260
- );
261
- return {
262
- id: prefixedWireId(args.output.id ?? args.id, "chatcmpl-"),
263
- object: "chat.completion",
264
- created: Math.floor((args.output.createdAtMs ?? Date.now()) / 1000),
265
- model: args.model,
266
- choices: [{
267
- index: 0,
268
- message: {
269
- role: "assistant",
270
- content: toolCalls.length > 0 && text.length === 0 ? null : text,
271
- ...(reasoning.some((item) => item.text.length > 0)
272
- ? { reasoning_content: reasoning.map((item) => item.text).filter(Boolean).join("\n") }
273
- : {}),
274
- ...(reasoningDetails.length === 0 ? {} : { reasoning_details: reasoningDetails }),
275
- ...(toolCalls.length === 0 ? {} : { tool_calls: toolCalls }),
276
- },
277
- finish_reason: args.output.finishReason,
278
- }],
279
- usage: openAIUsage(args.output.usage ?? { inputTokens: 0, outputTokens: 0 }),
280
- dari_routing: dariRoutingPayload(args.requestedModel, args.selection),
281
- };
282
- }
283
-
284
- export function openAIChatChunk<Metadata>(args: {
285
- id: string;
286
- created: number;
287
- model: string;
288
- delta: Record<string, unknown>;
289
- finishReason: string | null;
290
- routing?: { requestedModel: string; selection: RouterSelection<Metadata> };
291
- usage?: Record<string, unknown>;
292
- }): Record<string, unknown> {
293
- return {
294
- id: prefixedWireId(args.id, "chatcmpl-"),
295
- object: "chat.completion.chunk",
296
- created: args.created,
297
- model: args.model,
298
- choices: [{ index: 0, delta: args.delta, finish_reason: args.finishReason }],
299
- ...(args.routing === undefined
300
- ? {}
301
- : { dari_routing: dariRoutingPayload(args.routing.requestedModel, args.routing.selection) }),
302
- ...(args.usage === undefined ? {} : { usage: args.usage }),
303
- };
304
- }
305
-
306
- export function openAIStreamDelta(
307
- event: Exclude<RouterStreamEvent, { type: "finish" }>,
308
- toolCallIndex: number = event.index,
309
- reasoningDetailIndex?: number,
310
- ): Record<string, unknown> | null {
311
- if (event.type === "text_delta") return { content: event.delta };
312
- if (event.type === "reasoning_delta") return { reasoning_content: event.delta };
313
- if (event.type === "reasoning_end") {
314
- const detailIndex = reasoningDetailIndex ?? 0;
315
- return {
316
- reasoning_details: [
317
- event.continuation === undefined
318
- ? portableReasoningDetail(detailIndex, event.source, event.itemId)
319
- : encryptedReasoningDetail(detailIndex, event.continuation),
320
- ],
321
- };
322
- }
323
- if (event.type === "tool_call_start") {
324
- return {
325
- tool_calls: [{
326
- index: toolCallIndex,
327
- id: event.id,
328
- type: "function",
329
- function: { name: event.name, arguments: "" },
330
- }],
331
- };
332
- }
333
- if (event.type === "tool_call_delta") {
334
- return { tool_calls: [{ index: toolCallIndex, function: { arguments: event.delta } }] };
335
- }
336
- if (event.type === "hosted_tool_call") {
337
- return { tool_calls: [{ index: toolCallIndex, ...openAIHostedToolCallWire(event) }] };
338
- }
339
- return null;
340
- }
341
-
342
- function openAIHostedToolCallWire(
343
- item: Omit<RouterOutputHostedToolCall, "type">,
344
- ): Record<string, unknown> {
345
- const replayPayload: Record<string, unknown> = {
346
- ...item.payload,
347
- type: item.providerType,
348
- id: item.id,
349
- };
350
- if (item.status) replayPayload.status = item.status;
351
- else delete replayPayload.status;
352
- return {
353
- id: item.id,
354
- type: "function",
355
- function: {
356
- name: item.tool,
357
- arguments: JSON.stringify(replayPayload),
358
- },
359
- };
360
- }
361
-
362
- function openAIContent(value: unknown, param: string): RouterContent[] {
363
- if (value === undefined || value === null) return [];
364
- if (typeof value === "string") return [{ type: "text", text: value }];
365
- if (!Array.isArray(value)) throw invalid(`${param} must be a string, array, or null.`, param);
366
- return value.map((part, index) => {
367
- const partParam = `${param}.${index}`;
368
- if (!isRecord(part)) throw invalid(`${partParam} must be an object.`, partParam);
369
- if (part.type === "text") {
370
- rejectUnknownFields(part, ["type", "text"], partParam);
371
- if (typeof part.text !== "string") throw invalid(`${partParam}.text must be a string.`, `${partParam}.text`);
372
- return { type: "text" as const, text: part.text };
373
- }
374
- if (part.type === "image_url") {
375
- if (!isRecord(part.image_url)) {
376
- throw invalid(`${partParam}.image_url must be an object.`, `${partParam}.image_url`);
377
- }
378
- rejectUnknownFields(part, ["type", "image_url"], partParam);
379
- rejectUnknownFields(part.image_url, ["url", "detail"], `${partParam}.image_url`);
380
- const url = requiredString(part.image_url.url, `${partParam}.image_url.url`);
381
- const detail = part.image_url.detail;
382
- if (detail !== undefined && detail !== "auto" && detail !== "low" && detail !== "high") {
383
- throw invalid(
384
- `${partParam}.image_url.detail must be 'auto', 'low', or 'high'.`,
385
- `${partParam}.image_url.detail`,
386
- );
387
- }
388
- return {
389
- type: "image" as const,
390
- url,
391
- ...(detail === undefined ? {} : { detail }),
392
- };
393
- }
394
- throw new RouterFrameworkError(
395
- "invalid_request",
396
- `${partParam}.type is not supported.`,
397
- "unsupported_content_block",
398
- `${partParam}.type`,
399
- );
400
- });
401
- }
402
-
403
- function openAITools(value: unknown): RouterTool[] {
404
- if (value === undefined || value === null) return [];
405
- if (!Array.isArray(value)) throw invalid("tools must be an array.", "tools");
406
- const names = new Set<string>();
407
- return value.map((tool, index) => {
408
- const param = `tools.${index}`;
409
- if (!isRecord(tool) || tool.type !== "function" || !isRecord(tool.function)) {
410
- throw new RouterFrameworkError(
411
- "invalid_request",
412
- `${param} must be an OpenAI function tool.`,
413
- "unsupported_tool",
414
- param,
415
- );
416
- }
417
- rejectUnknownFields(tool, ["type", "function"], param);
418
- rejectUnknownFields(
419
- tool.function,
420
- ["name", "description", "parameters", "strict"],
421
- `${param}.function`,
422
- );
423
- return portableTool(tool.function, {
424
- names,
425
- param: `${param}.function`,
426
- schemaField: "parameters",
427
- schemaRequired: false,
428
- });
429
- });
430
- }
431
-
432
- function openAIToolChoice(value: unknown): RouterToolChoice | undefined {
433
- if (value === undefined || value === null) return undefined;
434
- if (value === "none" || value === "auto" || value === "required") return value;
435
- if (isRecord(value) && value.type === "function" && isRecord(value.function)) {
436
- rejectUnknownFields(value, ["type", "function"], "tool_choice");
437
- rejectUnknownFields(value.function, ["name"], "tool_choice.function");
438
- return { type: "tool", name: requiredString(value.function.name, "tool_choice.function.name") };
439
- }
440
- throw invalid("tool_choice is not supported.", "tool_choice");
441
- }
442
-
443
- function openAIReasoningItems(
444
- message: Record<string, unknown>,
445
- messageIndex: number,
446
- ): Array<Extract<RouterRequest["items"][number], { type: "reasoning" }>> {
447
- const readable = [message.reasoning_content, message.reasoning, message.reasoning_text]
448
- .find((value): value is string => typeof value === "string" && value.length > 0);
449
- const details = message.reasoning_details;
450
- if (details !== undefined && !Array.isArray(details)) {
451
- throw invalid(
452
- `messages.${messageIndex}.reasoning_details must be an array.`,
453
- `messages.${messageIndex}.reasoning_details`,
454
- );
455
- }
456
- const entries = (details ?? []).map((detail, detailIndex) => {
457
- const param = `messages.${messageIndex}.reasoning_details.${detailIndex}`;
458
- if (!isRecord(detail)) throw invalid(`${param} must be an object.`, param);
459
- rejectUnknownFields(detail, ["type", "id", "data"], param);
460
- if (detail.type !== "reasoning.encrypted" || typeof detail.data !== "string") {
461
- throw invalid(
462
- `${param}.data must be a Dari reasoning-state envelope.`,
463
- `${param}.data`,
464
- );
465
- }
466
- const continuation = decodeUntrustedProviderContinuationState(detail.data);
467
- const portable = continuation ? null : decodeUntrustedPortableReasoningState(detail.data);
468
- if (!continuation && !portable) {
469
- throw new RouterFrameworkError(
470
- "invalid_request",
471
- `${param}.data must be a Dari reasoning-state envelope; raw provider state has no recoverable provider/model provenance.`,
472
- "invalid_provider_continuation_state",
473
- `${param}.data`,
474
- );
475
- }
476
- return {
477
- id: typeof detail.id === "string" && detail.id.length > 0
478
- ? detail.id
479
- : portable?.itemId,
480
- continuation,
481
- portable,
482
- };
483
- });
484
- if (entries.length === 0) {
485
- return readable
486
- ? [{ type: "reasoning", summary: [readable], content: [] }]
487
- : [];
488
- }
489
- return entries.map(({ id, continuation, portable }, index) => ({
490
- type: "reasoning" as const,
491
- ...(id ? { id } : {}),
492
- summary: index === 0 && readable ? [readable] : [],
493
- content: [],
494
- ...(continuation
495
- ? { source: continuation.source, continuation }
496
- : portable?.source
497
- ? { source: portable.source }
498
- : {}),
499
- }));
500
- }
501
-
502
- function openAIHostedToolCall(
503
- callId: string,
504
- name: string,
505
- argumentsValue: unknown,
506
- reasoningItems: Array<Extract<RouterRequest["items"][number], { type: "reasoning" }>>,
507
- param: string,
508
- ): Extract<RouterRequest["items"][number], { type: "hosted_tool_call" }> | null {
509
- if (name !== "web_search") return null;
510
- const payload = openAIToolArgumentsRecord(argumentsValue);
511
- if (payload === null || payload.type !== "web_search_call" || payload.id !== callId) return null;
512
- const status = hostedToolCallStatus(payload.status, param);
513
- const source = reasoningItems.flatMap((item) =>
514
- item.continuation?.kind === "openai_reasoning" &&
515
- item.continuation.hostedToolCallIds?.includes(callId)
516
- ? [item.continuation.source]
517
- : [],
518
- )[0];
519
- return {
520
- type: "hosted_tool_call",
521
- id: callId,
522
- tool: "web_search",
523
- providerType: "web_search_call",
524
- ...(status ? { status } : {}),
525
- payload: { ...payload },
526
- ...(source ? { source } : {}),
527
- };
528
- }
529
-
530
- function openAIToolArgumentsRecord(value: unknown): Record<string, unknown> | null {
531
- if (isRecord(value)) return value;
532
- if (typeof value !== "string") return null;
533
- try {
534
- const parsed: unknown = JSON.parse(value);
535
- return isRecord(parsed) ? parsed : null;
536
- } catch {
537
- return null;
538
- }
539
- }
540
-
541
- function hostedToolCallStatus(
542
- value: unknown,
543
- param: string,
544
- ): RouterHostedToolCallStatus | undefined {
545
- if (value === undefined || isRouterHostedToolCallStatus(value)) return value;
546
- throw invalid(`${param} is not supported.`, param);
547
- }
548
-
549
- function openAIReasoning(value: unknown): RouterRequest["reasoning"] {
550
- if (value === undefined || value === null) return undefined;
551
- if (!isReasoningEffort(value)) throw invalid("reasoning_effort is not supported.", "reasoning_effort");
552
- return { effort: value, enabled: value !== "off" };
553
- }
554
-
555
- function openAIResponseFormat(value: unknown): RouterResponseFormat | undefined {
556
- if (value === undefined || value === null) return undefined;
557
- if (!isRecord(value)) throw invalid("response_format must be an object.", "response_format");
558
- if (value.type === "text" || value.type === "json_object") {
559
- rejectUnknownFields(value, ["type"], "response_format");
560
- return { type: value.type };
561
- }
562
- if (value.type !== "json_schema" || !isRecord(value.json_schema)) {
563
- throw new RouterFrameworkError(
564
- "invalid_request",
565
- "response_format.type is not supported.",
566
- "unsupported_response_format",
567
- "response_format.type",
568
- );
569
- }
570
- rejectUnknownFields(value, ["type", "json_schema"], "response_format");
571
- rejectUnknownFields(
572
- value.json_schema,
573
- ["name", "schema", "description", "strict"],
574
- "response_format.json_schema",
575
- );
576
- if (!isRecord(value.json_schema.schema)) {
577
- throw invalid("response_format.json_schema.schema must be an object.", "response_format.json_schema.schema");
578
- }
579
- const description = optionalString(
580
- value.json_schema.description,
581
- "response_format.json_schema.description",
582
- );
583
- const strict = optionalBoolean(
584
- value.json_schema.strict,
585
- "response_format.json_schema.strict",
586
- );
587
- return {
588
- type: "json_schema",
589
- name: requiredString(value.json_schema.name, "response_format.json_schema.name"),
590
- schema: value.json_schema.schema,
591
- ...(description === undefined ? {} : { description }),
592
- ...(strict === undefined ? {} : { strict }),
593
- };
594
- }
595
-
596
- export function openAIUsage(
597
- usage: NonNullable<RouterCompletion["usage"]>,
598
- ): Record<string, unknown> {
599
- const cacheTokens = (usage.cacheReadTokens ?? 0) + (usage.cacheWriteTokens ?? 0);
600
- const promptTokens = usage.inputTokens + cacheTokens;
601
- return {
602
- prompt_tokens: promptTokens,
603
- completion_tokens: usage.outputTokens,
604
- // OpenAI counts cached tokens inside prompt_tokens; once cache tokens are
605
- // folded in, recompute the total rather than trusting upstream totalTokens.
606
- total_tokens: cacheTokens > 0
607
- ? promptTokens + usage.outputTokens
608
- : usage.totalTokens ?? promptTokens + usage.outputTokens,
609
- ...(cacheTokens > 0
610
- ? {
611
- prompt_tokens_details: {
612
- cached_tokens: usage.cacheReadTokens ?? 0,
613
- cache_write_tokens: usage.cacheWriteTokens ?? 0,
614
- },
615
- }
616
- : {}),
617
- };
618
- }
619
-
620
- function encryptedReasoningDetail(
621
- index: number,
622
- continuation: ProviderContinuationState,
623
- ): Record<string, unknown> {
624
- return {
625
- type: "reasoning.encrypted",
626
- id: `reasoning_${index}`,
627
- data: encodeProviderContinuationState(continuation),
628
- };
629
- }
630
-
631
- function portableReasoningDetail(
632
- index: number,
633
- source: ProviderIdentity | undefined,
634
- itemId?: string,
635
- ): Record<string, unknown> {
636
- const id = itemId ?? `reasoning_${index}`;
637
- return {
638
- type: "reasoning.encrypted",
639
- id,
640
- data: encodePortableReasoningState({
641
- ...(source === undefined ? {} : { source }),
642
- itemId: id,
643
- }),
644
- };
645
- }