@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
package/src/pi_context.ts DELETED
@@ -1,251 +0,0 @@
1
- import type { AssistantMessage, Context, Tool } from "@mupt-ai/pi-ai";
2
-
3
- import { RouterFrameworkError } from "./framework_error.js";
4
- import { encodeOpenAIReasoningSignature } from "./pi_reasoning_signature.js";
5
- import { isRecord } from "./types.js";
6
- import type {
7
- RouterContent,
8
- RouterProviderIdentity,
9
- RouterRequest,
10
- } from "./framework_types.js";
11
- import {
12
- compatibleProviderContinuation,
13
- isOpenAIResponsesApi,
14
- type ProviderContinuationState,
15
- } from "./continuation_state.js";
16
- import type { PiModel } from "./pi_types.js";
17
-
18
- export function piContext(request: RouterRequest, model: PiModel): Context {
19
- const system: string[] = [];
20
- const messages: Context["messages"] = [];
21
- const calls = new Map<string, string>();
22
- const timestamp = Date.now();
23
- let assistant: Extract<Context["messages"][number], { role: "assistant" }> | undefined;
24
-
25
- const flushAssistant = () => {
26
- if (assistant === undefined) return;
27
- if (assistant.content.length === 0) assistant.content.push({ type: "text", text: "" });
28
- assistant.stopReason = assistant.content.some((part) => part.type === "toolCall")
29
- ? "toolUse"
30
- : "stop";
31
- messages.push(assistant);
32
- assistant = undefined;
33
- };
34
- const currentAssistant = () => {
35
- assistant ??= {
36
- role: "assistant",
37
- content: [],
38
- api: model.api,
39
- provider: model.provider,
40
- model: model.id,
41
- usage: zeroPiUsage(),
42
- stopReason: "stop",
43
- timestamp,
44
- };
45
- return assistant;
46
- };
47
-
48
- for (const item of request.items) {
49
- if (item.type === "message" && (item.role === "system" || item.role === "developer")) {
50
- flushAssistant();
51
- system.push(systemText(item.content));
52
- continue;
53
- }
54
- if (item.type === "message" && item.role === "user") {
55
- flushAssistant();
56
- messages.push({
57
- role: "user",
58
- content: piContent(item.content, "user message"),
59
- timestamp,
60
- });
61
- continue;
62
- }
63
- if (item.type === "message") {
64
- const pending = currentAssistant();
65
- pending.content.push(...item.content.map((part) => {
66
- if (part.type !== "text") {
67
- throw new RouterFrameworkError(
68
- "invalid_request",
69
- "Pi does not support image content in assistant messages.",
70
- "pi_assistant_image_unsupported",
71
- );
72
- }
73
- return { type: "text" as const, text: part.text };
74
- }));
75
- continue;
76
- }
77
- if (item.type === "tool_call") {
78
- const argumentsValue = toolArguments(item.arguments, item.id);
79
- currentAssistant().content.push({
80
- type: "toolCall",
81
- id: item.id,
82
- name: item.name,
83
- arguments: argumentsValue,
84
- });
85
- calls.set(item.id, item.name);
86
- continue;
87
- }
88
- if (item.type === "reasoning") {
89
- const target: RouterProviderIdentity = {
90
- provider: model.provider,
91
- api: model.api,
92
- model: model.id,
93
- };
94
- const continuation = compatibleProviderContinuation(item.continuation, target);
95
- if (continuation) {
96
- currentAssistant().content.push(thinkingContent(item, continuation));
97
- } else {
98
- const readable = [...(item.summary ?? []), ...item.content].join("\n");
99
- if (readable) {
100
- // Cross-provider: the encrypted blob is not replayable, so carry the
101
- // readable reasoning as plain text instead of dropping the prior
102
- // turn's chain-of-thought entirely.
103
- currentAssistant().content.push({ type: "text", text: readable });
104
- }
105
- }
106
- continue;
107
- }
108
- if (item.type === "hosted_tool_call") {
109
- if (!isOpenAIResponsesApi(model.api)) {
110
- // Dropping the item would silently lose a conversation turn.
111
- throw new RouterFrameworkError(
112
- "configuration",
113
- `Hosted tool calls are not representable on the '${model.api}' API; route hosted-tool conversations to a Responses-API model.`,
114
- "hosted_tool_call_unrepresentable",
115
- );
116
- }
117
- (currentAssistant().content as unknown as Array<Record<string, unknown>>).push({
118
- type: "providerItem",
119
- item: item.payload,
120
- });
121
- continue;
122
- }
123
- flushAssistant();
124
- messages.push({
125
- role: "toolResult",
126
- toolCallId: item.toolCallId,
127
- toolName: calls.get(item.toolCallId) ?? "tool",
128
- content: piContent(item.content, `tool result '${item.toolCallId}'`),
129
- isError: item.isError ?? false,
130
- timestamp,
131
- });
132
- }
133
- flushAssistant();
134
-
135
- const tools: Tool[] = request.tools.map((tool) => ({
136
- name: tool.name,
137
- description: tool.description ?? "",
138
- parameters: tool.inputSchema as Tool["parameters"],
139
- }));
140
- return {
141
- ...(system.length === 0 ? {} : { systemPrompt: system.join("\n\n") }),
142
- messages,
143
- ...(tools.length === 0 ? {} : { tools }),
144
- };
145
- }
146
-
147
- function thinkingContent(
148
- item: Extract<RouterRequest["items"][number], { type: "reasoning" }>,
149
- continuation: ProviderContinuationState,
150
- ): { type: "thinking"; thinking: string; thinkingSignature?: string; redacted?: boolean } {
151
- if (continuation.kind === "openai_reasoning") {
152
- const providerItemId = continuation.providerItemId ?? item.id;
153
- return {
154
- type: "thinking",
155
- thinking: [...(item.summary ?? []), ...item.content].join("\n"),
156
- thinkingSignature: encodeOpenAIReasoningSignature({
157
- ...(providerItemId ? { id: providerItemId } : {}),
158
- summary: item.summary ?? [],
159
- content: item.content,
160
- encryptedContent: continuation.encryptedContent,
161
- }),
162
- };
163
- }
164
- if (continuation.kind === "anthropic_thinking") {
165
- return {
166
- type: "thinking",
167
- thinking: continuation.thinking,
168
- thinkingSignature: continuation.signature,
169
- };
170
- }
171
- return {
172
- type: "thinking",
173
- thinking: "",
174
- thinkingSignature: continuation.data,
175
- redacted: true,
176
- };
177
- }
178
-
179
- function systemText(content: readonly RouterContent[]): string {
180
- return content.map((part) => {
181
- if (part.type !== "text") {
182
- throw new RouterFrameworkError(
183
- "invalid_request",
184
- "Pi does not support image content in system or developer messages.",
185
- "pi_system_image_unsupported",
186
- );
187
- }
188
- return part.text;
189
- }).join("");
190
- }
191
-
192
- function piContent(
193
- content: readonly RouterContent[],
194
- location: string,
195
- ): Array<{ type: "text"; text: string } | { type: "image"; data: string; mimeType: string }> {
196
- return content.map((part) => {
197
- if (part.type === "text") return { type: "text", text: part.text };
198
- const image = dataImage(part.url);
199
- if (image === null) {
200
- throw new RouterFrameworkError(
201
- "invalid_request",
202
- `Pi ${location} images must use base64 data URLs.`,
203
- "pi_remote_image_unsupported",
204
- );
205
- }
206
- return { type: "image", data: image.data, mimeType: image.mimeType };
207
- });
208
- }
209
-
210
- function dataImage(url: string): { data: string; mimeType: string } | null {
211
- const match = /^data:(image\/[a-z0-9.+-]+);base64,([a-z0-9+/=\s]+)$/i.exec(url);
212
- if (match === null) return null;
213
- return { mimeType: match[1]!, data: match[2]!.replace(/\s/g, "") };
214
- }
215
-
216
- function toolArguments(value: string | Record<string, unknown>, id: string): Record<string, unknown> {
217
- if (typeof value !== "string") return value;
218
- // Providers emit "" for zero-argument tool calls; replay it as {}.
219
- if (value === "") return {};
220
- let parsed: unknown;
221
- try {
222
- parsed = JSON.parse(value);
223
- } catch (error) {
224
- throw new RouterFrameworkError(
225
- "invalid_request",
226
- `Tool call '${id}' arguments must be valid JSON.`,
227
- "pi_tool_arguments_invalid",
228
- undefined,
229
- { cause: error },
230
- );
231
- }
232
- if (!isRecord(parsed)) {
233
- throw new RouterFrameworkError(
234
- "invalid_request",
235
- `Tool call '${id}' arguments must be a JSON object.`,
236
- "pi_tool_arguments_invalid",
237
- );
238
- }
239
- return parsed;
240
- }
241
-
242
- function zeroPiUsage(): AssistantMessage["usage"] {
243
- return {
244
- input: 0,
245
- output: 0,
246
- cacheRead: 0,
247
- cacheWrite: 0,
248
- totalTokens: 0,
249
- cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
250
- };
251
- }
package/src/pi_options.ts DELETED
@@ -1,295 +0,0 @@
1
- import type { SimpleStreamOptions } from "@mupt-ai/pi-ai";
2
-
3
- import { isOpenAIResponsesApi } from "./continuation_state.js";
4
- import { RouterFrameworkError } from "./framework_error.js";
5
- import { isRecord } from "./types.js";
6
- import type { RouterContent, RouterRequest, RouterToolChoice } from "./framework_types.js";
7
- import type {
8
- CreatePiRuntimeOptions,
9
- PiExecution,
10
- PiModel,
11
- } from "./pi_types.js";
12
-
13
- const PAYLOAD_PATCH_APIS = new Set([
14
- "anthropic-messages",
15
- "azure-openai-responses",
16
- "openai-completions",
17
- "openai-responses",
18
- ]);
19
-
20
- export async function piOptions(
21
- runtime: CreatePiRuntimeOptions,
22
- execution: PiExecution,
23
- model: PiModel,
24
- ): Promise<SimpleStreamOptions> {
25
- rejectUnsupportedOptions(execution.request, model);
26
- const apiKey = typeof runtime.apiKey === "string"
27
- ? runtime.apiKey
28
- : await runtime.apiKey({
29
- provider: model.provider,
30
- model: execution.candidate.id,
31
- api: model.api,
32
- purpose: execution.purpose,
33
- });
34
- if (typeof apiKey !== "string" || !apiKey.trim()) {
35
- throw new RouterFrameworkError(
36
- "configuration",
37
- `No API key was provided for Pi model '${execution.candidate.id}'.`,
38
- "pi_api_key_missing",
39
- );
40
- }
41
-
42
- const request = execution.request;
43
- const options: SimpleStreamOptions = {
44
- apiKey,
45
- signal: execution.signal,
46
- ...(runtime.timeoutMs === undefined ? {} : { timeoutMs: runtime.timeoutMs }),
47
- ...(runtime.maxRetries === undefined ? {} : { maxRetries: runtime.maxRetries }),
48
- ...(request.generation.temperature === undefined
49
- ? {}
50
- : { temperature: request.generation.temperature }),
51
- ...(request.generation.topP === undefined ? {} : { topP: request.generation.topP }),
52
- ...(request.generation.maxOutputTokens === undefined
53
- ? {}
54
- : { maxTokens: request.generation.maxOutputTokens }),
55
- ...(request.generation.stop === undefined ? {} : { stop: request.generation.stop }),
56
- ...(request.cacheKey === undefined ? {} : { sessionId: request.cacheKey }),
57
- // Only anthropic-messages reads metadata (and only user_id); no other
58
- // fork API consumes metadata fields.
59
- ...(request.user !== undefined && model.api === "anthropic-messages"
60
- ? { metadata: { user_id: request.user } }
61
- : {}),
62
- ...(execution.reasoningEffort === "off" ? {} : { reasoning: execution.reasoningEffort }),
63
- ...(request.responseFormat === undefined
64
- ? {}
65
- : { responseFormat: piResponseFormat(request.responseFormat, model.api) }),
66
- ...(request.toolChoice === undefined ? {} : { toolChoice: piToolChoice(request.toolChoice) }),
67
- };
68
-
69
- const budget = request.reasoning?.budgetTokens;
70
- if (
71
- budget !== undefined &&
72
- execution.reasoningEffort !== "off" &&
73
- execution.reasoningEffort !== "xhigh" &&
74
- execution.reasoningEffort !== "max"
75
- ) {
76
- options.thinkingBudgets = { [execution.reasoningEffort]: budget };
77
- }
78
-
79
- const patch = payloadPatch(request, model);
80
- if (patch !== undefined) options.onPayload = patch;
81
- return options;
82
- }
83
-
84
- // The fork hard-throws for these (api, option) pairs inside streamSimple;
85
- // reject them up front as client errors instead of surfacing 502s.
86
- function rejectUnsupportedOptions(request: RouterRequest, model: PiModel): void {
87
- if (request.generation.stop !== undefined && isOpenAIResponsesApi(model.api)) {
88
- throw unsupportedPiOption("stop sequences", model.api);
89
- }
90
- if (request.generation.topP !== undefined && model.api === "anthropic-messages") {
91
- throw unsupportedPiOption("top_p", model.api);
92
- }
93
- if (
94
- typeof request.toolChoice === "object" &&
95
- (model.api === "google-generative-ai" || model.api === "google-vertex")
96
- ) {
97
- throw unsupportedPiOption("forcing a specific tool with tool_choice", model.api);
98
- }
99
- if (request.responseFormat?.type === "json_object" && model.api === "bedrock-converse-stream") {
100
- throw unsupportedPiOption("response_format json_object", model.api);
101
- }
102
- }
103
-
104
- function unsupportedPiOption(option: string, api: string): RouterFrameworkError {
105
- return new RouterFrameworkError(
106
- "invalid_request",
107
- `Pi API '${api}' does not support ${option}.`,
108
- "pi_option_unsupported",
109
- );
110
- }
111
-
112
- function piResponseFormat(
113
- format: NonNullable<RouterRequest["responseFormat"]>,
114
- api: string,
115
- ): NonNullable<SimpleStreamOptions["responseFormat"]> {
116
- if (format.type === "json_object" && api === "anthropic-messages") {
117
- // Anthropic has no json_object mode; degrade to a permissive object
118
- // schema so the request still produces JSON output.
119
- return {
120
- type: "json_schema",
121
- jsonSchema: { name: "response", schema: { type: "object" } },
122
- };
123
- }
124
- if (format.type !== "json_schema") return format;
125
- return {
126
- type: "json_schema",
127
- jsonSchema: {
128
- name: format.name,
129
- schema: format.schema,
130
- ...(format.description === undefined ? {} : { description: format.description }),
131
- ...(format.strict === undefined ? {} : { strict: format.strict }),
132
- },
133
- };
134
- }
135
-
136
- function piToolChoice(choice: RouterToolChoice): NonNullable<SimpleStreamOptions["toolChoice"]> {
137
- if (choice === "required") return "any";
138
- if (typeof choice === "string") return choice;
139
- return { type: "function", function: { name: choice.name } };
140
- }
141
-
142
- function payloadPatch(
143
- request: RouterRequest,
144
- model: PiModel,
145
- ): SimpleStreamOptions["onPayload"] | undefined {
146
- const strictTools = request.tools.some((tool) => tool.strict !== undefined);
147
- const parallel = request.parallelToolCalls;
148
- const imageDetails = imageDetailQueues(request);
149
- const thinkingPatch = model.api === "anthropic-messages" && (
150
- request.reasoning?.enabled === false || request.reasoning?.budgetTokens !== undefined
151
- );
152
- const imageDetailPatch = imageDetails.size > 0 &&
153
- (isOpenAIResponsesApi(model.api) || model.api === "openai-completions");
154
- if (
155
- imageDetails.size > 0 &&
156
- !imageDetailPatch &&
157
- [...imageDetails.values()].some((queue) => queue.some((detail) => detail !== "auto"))
158
- ) {
159
- throw new RouterFrameworkError(
160
- "invalid_request",
161
- `Pi API '${model.api}' cannot preserve image detail controls.`,
162
- "pi_image_detail_unsupported",
163
- );
164
- }
165
- if (!strictTools && parallel === undefined && !thinkingPatch && !imageDetailPatch) {
166
- return undefined;
167
- }
168
- if ((strictTools || parallel !== undefined) && !PAYLOAD_PATCH_APIS.has(model.api)) {
169
- throw new RouterFrameworkError(
170
- "invalid_request",
171
- `Pi API '${model.api}' cannot preserve strict or parallel tool controls.`,
172
- "pi_tool_control_unsupported",
173
- );
174
- }
175
-
176
- return (value) => {
177
- if (!isRecord(value)) return undefined;
178
- let payload = value;
179
- if (strictTools) payload = patchStrictTools(payload, request, model.api);
180
- if (parallel !== undefined) payload = patchParallelTools(payload, parallel, model.api);
181
- if (imageDetailPatch) payload = patchImageDetails(payload, imageDetails);
182
- if (thinkingPatch) payload = patchAnthropicThinking(payload, request);
183
- return payload;
184
- };
185
- }
186
-
187
- function patchStrictTools(
188
- payload: Record<string, unknown>,
189
- request: RouterRequest,
190
- api: string,
191
- ): Record<string, unknown> {
192
- if (!Array.isArray(payload.tools)) return payload;
193
- const tools = new Map(request.tools.map((tool) => [tool.name, tool]));
194
- return {
195
- ...payload,
196
- tools: payload.tools.map((entry) => {
197
- if (!isRecord(entry)) return entry;
198
- if (api === "anthropic-messages") {
199
- const tool = typeof entry.name === "string" ? tools.get(entry.name) : undefined;
200
- return tool?.strict === undefined ? entry : { ...entry, strict: tool.strict };
201
- }
202
- const definition = api === "openai-completions" && isRecord(entry.function)
203
- ? entry.function
204
- : entry;
205
- const tool = typeof definition.name === "string" ? tools.get(definition.name) : undefined;
206
- if (tool?.strict === undefined) return entry;
207
- return api === "openai-completions"
208
- ? { ...entry, function: { ...definition, strict: tool.strict } }
209
- : { ...entry, strict: tool.strict };
210
- }),
211
- };
212
- }
213
-
214
- function patchParallelTools(
215
- payload: Record<string, unknown>,
216
- enabled: boolean,
217
- api: string,
218
- ): Record<string, unknown> {
219
- if (api !== "anthropic-messages") return { ...payload, parallel_tool_calls: enabled };
220
- if (enabled) return payload;
221
- const current = isRecord(payload.tool_choice) ? payload.tool_choice : {};
222
- return {
223
- ...payload,
224
- tool_choice: {
225
- type: typeof current.type === "string" ? current.type : "auto",
226
- ...current,
227
- disable_parallel_tool_use: true,
228
- },
229
- };
230
- }
231
-
232
- function patchImageDetails(
233
- payload: Record<string, unknown>,
234
- details: Map<string, Array<"auto" | "low" | "high">>,
235
- ): Record<string, unknown> {
236
- const queues = new Map([...details].map(([url, queue]) => [url, [...queue]]));
237
- const patch = (value: unknown): unknown => {
238
- if (Array.isArray(value)) return value.map(patch);
239
- if (!isRecord(value)) return value;
240
- const mapped = Object.fromEntries(
241
- Object.entries(value).map(([key, entry]) => [key, patch(entry)]),
242
- );
243
- const responseUrl = mapped.type === "input_image" && typeof mapped.image_url === "string"
244
- ? mapped.image_url
245
- : undefined;
246
- const chatImage = mapped.type === "image_url" && isRecord(mapped.image_url)
247
- ? mapped.image_url
248
- : undefined;
249
- const chatUrl = typeof chatImage?.url === "string" ? chatImage.url : undefined;
250
- const detail = queues.get(responseUrl ?? chatUrl ?? "")?.shift();
251
- if (detail === undefined) return mapped;
252
- return responseUrl === undefined
253
- ? { ...mapped, image_url: { ...chatImage, detail } }
254
- : { ...mapped, detail };
255
- };
256
- const patched = patch(payload);
257
- return isRecord(patched) ? patched : payload;
258
- }
259
-
260
- function imageDetailQueues(
261
- request: RouterRequest,
262
- ): Map<string, Array<"auto" | "low" | "high">> {
263
- const details = new Map<string, Array<"auto" | "low" | "high">>();
264
- const add = (content: readonly RouterContent[]) => {
265
- for (const part of content) {
266
- if (part.type !== "image" || part.detail === undefined) continue;
267
- const queue = details.get(part.url) ?? [];
268
- queue.push(part.detail);
269
- details.set(part.url, queue);
270
- }
271
- };
272
- for (const item of request.items) {
273
- if (item.type === "message" || item.type === "tool_result") add(item.content);
274
- }
275
- return details;
276
- }
277
-
278
- function patchAnthropicThinking(
279
- payload: Record<string, unknown>,
280
- request: RouterRequest,
281
- ): Record<string, unknown> {
282
- if (request.reasoning?.enabled === false) {
283
- return { ...payload, thinking: { type: "disabled" } };
284
- }
285
- const budget = request.reasoning?.budgetTokens;
286
- if (budget === undefined) return payload;
287
- return {
288
- ...payload,
289
- thinking: {
290
- ...(isRecord(payload.thinking) ? payload.thinking : {}),
291
- type: "enabled",
292
- budget_tokens: budget,
293
- },
294
- };
295
- }