@mupt-ai/dari-router 0.1.0 → 0.2.1
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 +60 -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 +121 -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/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
|
-
}
|