@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
|
@@ -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
|
-
}
|