@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,613 +0,0 @@
|
|
|
1
|
-
import { RouterFrameworkError } from "./framework_error.js";
|
|
2
|
-
import {
|
|
3
|
-
anthropicMessageStart,
|
|
4
|
-
anthropicStopReason,
|
|
5
|
-
anthropicUsage,
|
|
6
|
-
} from "./protocol_anthropic.js";
|
|
7
|
-
import {
|
|
8
|
-
openAIChatChunk,
|
|
9
|
-
openAIStreamDelta,
|
|
10
|
-
openAIUsage,
|
|
11
|
-
} from "./protocol_openai_chat.js";
|
|
12
|
-
import { encodeProviderContinuationState } from "./continuation_state.js";
|
|
13
|
-
import { encodePortableReasoningState } from "./portable_reasoning_state.js";
|
|
14
|
-
import {
|
|
15
|
-
cancelled,
|
|
16
|
-
closeAsyncIterator,
|
|
17
|
-
errorMessage,
|
|
18
|
-
streamContract,
|
|
19
|
-
throwIfAborted,
|
|
20
|
-
} from "./framework_lifecycle.js";
|
|
21
|
-
import {
|
|
22
|
-
validateStreamEvent,
|
|
23
|
-
validateStreamSequence,
|
|
24
|
-
} from "./framework_validation.js";
|
|
25
|
-
import type {
|
|
26
|
-
RouterCompletion,
|
|
27
|
-
RouterProtocol,
|
|
28
|
-
RouterSelection,
|
|
29
|
-
RouterStreamEvent,
|
|
30
|
-
} from "./framework_types.js";
|
|
31
|
-
|
|
32
|
-
export type PrimedStream = {
|
|
33
|
-
first: RouterStreamEvent;
|
|
34
|
-
iterator: AsyncIterator<RouterStreamEvent>;
|
|
35
|
-
};
|
|
36
|
-
|
|
37
|
-
// An executor result that already passed mode and shape validation and, for
|
|
38
|
-
// streams, priming — i.e. everything covered by the fallback/lease-drop
|
|
39
|
-
// region.
|
|
40
|
-
export type StartedExecution =
|
|
41
|
-
| { type: "stream"; primed: PrimedStream }
|
|
42
|
-
| { type: "complete"; output: RouterCompletion };
|
|
43
|
-
|
|
44
|
-
export async function primeStream(
|
|
45
|
-
events: AsyncIterable<RouterStreamEvent>,
|
|
46
|
-
controller: AbortController,
|
|
47
|
-
): Promise<PrimedStream> {
|
|
48
|
-
let iterator: AsyncIterator<RouterStreamEvent> | undefined;
|
|
49
|
-
let first: IteratorResult<RouterStreamEvent>;
|
|
50
|
-
try {
|
|
51
|
-
iterator = events[Symbol.asyncIterator]();
|
|
52
|
-
throwIfAborted(controller.signal);
|
|
53
|
-
first = await iterator.next();
|
|
54
|
-
} catch (error) {
|
|
55
|
-
if (iterator !== undefined) await closeAsyncIterator(iterator);
|
|
56
|
-
if (controller.signal.aborted) throw cancelled(controller.signal.reason);
|
|
57
|
-
if (error instanceof RouterFrameworkError) throw error;
|
|
58
|
-
throw new RouterFrameworkError(
|
|
59
|
-
"executor",
|
|
60
|
-
errorMessage(error, "Executor stream failed before its first event."),
|
|
61
|
-
"stream_setup_failed",
|
|
62
|
-
undefined,
|
|
63
|
-
{ cause: error },
|
|
64
|
-
);
|
|
65
|
-
}
|
|
66
|
-
if (controller.signal.aborted) {
|
|
67
|
-
await closeAsyncIterator(iterator);
|
|
68
|
-
throw cancelled(controller.signal.reason);
|
|
69
|
-
}
|
|
70
|
-
if (first.done) {
|
|
71
|
-
// Do not abort the shared controller: that would poison a fallback
|
|
72
|
-
// attempt and misreport this contract failure as a cancellation.
|
|
73
|
-
await closeAsyncIterator(iterator);
|
|
74
|
-
throw new RouterFrameworkError(
|
|
75
|
-
"executor",
|
|
76
|
-
"Executor stream ended without a terminal event.",
|
|
77
|
-
"stream_empty",
|
|
78
|
-
);
|
|
79
|
-
}
|
|
80
|
-
try {
|
|
81
|
-
validateStreamEvent(first.value);
|
|
82
|
-
if (first.value.type === "tool_call_delta" || first.value.type === "tool_call_end") {
|
|
83
|
-
throw streamContract("Executor stream must start with text, reasoning, a tool call, or finish.");
|
|
84
|
-
}
|
|
85
|
-
} catch (error) {
|
|
86
|
-
await closeAsyncIterator(iterator);
|
|
87
|
-
throw error;
|
|
88
|
-
}
|
|
89
|
-
return { first: first.value, iterator };
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
export function streamResponse<Metadata>(args: {
|
|
93
|
-
protocol: RouterProtocol;
|
|
94
|
-
id: string;
|
|
95
|
-
model: string;
|
|
96
|
-
requestedModel: string;
|
|
97
|
-
selection: RouterSelection<Metadata>;
|
|
98
|
-
primed: PrimedStream;
|
|
99
|
-
controller: AbortController;
|
|
100
|
-
headers: HeadersInit;
|
|
101
|
-
// Fired once, at failure-classification time, when the stream dies from a
|
|
102
|
-
// genuine provider/contract failure (never on client abort or reader
|
|
103
|
-
// cancel). Must not throw and must not need awaiting.
|
|
104
|
-
onStreamFailure?: () => void;
|
|
105
|
-
// Fired exactly once during teardown, after the executor iterator is
|
|
106
|
-
// closed and before the wire stream closes. Must not throw.
|
|
107
|
-
onFinalize: (
|
|
108
|
-
completion: RouterCompletion | null,
|
|
109
|
-
error: RouterFrameworkError | null,
|
|
110
|
-
) => void;
|
|
111
|
-
}): Response {
|
|
112
|
-
const encoder = new TextEncoder();
|
|
113
|
-
const created = Math.floor(Date.now() / 1000);
|
|
114
|
-
let cancelledByReader = false;
|
|
115
|
-
let finalized = false;
|
|
116
|
-
let emitted = false;
|
|
117
|
-
let firstEvent: RouterStreamEvent | undefined = args.primed.first;
|
|
118
|
-
let streamCompletion: RouterCompletion | null = null;
|
|
119
|
-
let streamError: RouterFrameworkError | null = null;
|
|
120
|
-
let streamController: ReadableStreamDefaultController<Uint8Array>;
|
|
121
|
-
|
|
122
|
-
const sequenceIndexes = new Map<number, "text" | "tool" | "reasoning">();
|
|
123
|
-
const anthropicState: AnthropicStreamState = {
|
|
124
|
-
openBlocks: new Map(),
|
|
125
|
-
pendingTools: new Map(),
|
|
126
|
-
pendingToolOrder: [],
|
|
127
|
-
nextWireIndex: 0,
|
|
128
|
-
};
|
|
129
|
-
const openAIToolIndexes = new Map<number, number>();
|
|
130
|
-
let nextOpenAIToolIndex = 0;
|
|
131
|
-
let nextOpenAIReasoningIndex = 0;
|
|
132
|
-
let enqueuedFrames = 0;
|
|
133
|
-
const enqueueFrame = (frame: string) => {
|
|
134
|
-
if (cancelledByReader) return;
|
|
135
|
-
streamController.enqueue(encoder.encode(frame));
|
|
136
|
-
enqueuedFrames += 1;
|
|
137
|
-
};
|
|
138
|
-
const sendOpenAI = (value: Record<string, unknown> | "[DONE]") => {
|
|
139
|
-
enqueueFrame(`data: ${value === "[DONE]" ? value : JSON.stringify(value)}\n\n`);
|
|
140
|
-
};
|
|
141
|
-
const sendAnthropic = (event: string, value: Record<string, unknown>) => {
|
|
142
|
-
enqueueFrame(`event: ${event}\ndata: ${JSON.stringify(value)}\n\n`);
|
|
143
|
-
};
|
|
144
|
-
const sendOpenAIChunk = (delta: Record<string, unknown>, finishReason: string | null = null) => {
|
|
145
|
-
sendOpenAI(openAIChatChunk({ id: args.id, created, model: args.model, delta, finishReason }));
|
|
146
|
-
};
|
|
147
|
-
|
|
148
|
-
const processEvent = (event: RouterStreamEvent) => {
|
|
149
|
-
validateStreamEvent(event);
|
|
150
|
-
if (streamCompletion !== null) throw streamContract("Executor emitted an event after finish.");
|
|
151
|
-
if (event.type === "finish") {
|
|
152
|
-
if ([...sequenceIndexes.values()].includes("tool")) {
|
|
153
|
-
throw streamContract("Executor finished with an open tool call.");
|
|
154
|
-
}
|
|
155
|
-
if ([...sequenceIndexes.values()].includes("reasoning")) {
|
|
156
|
-
throw streamContract("Executor finished with an open reasoning block.");
|
|
157
|
-
}
|
|
158
|
-
streamCompletion = {
|
|
159
|
-
content: [],
|
|
160
|
-
finishReason: event.finishReason,
|
|
161
|
-
...(event.usage === undefined ? {} : { usage: event.usage }),
|
|
162
|
-
};
|
|
163
|
-
closeAnthropicTextBlocks(anthropicState, sendAnthropic);
|
|
164
|
-
if (args.protocol === "openai_chat_completions") {
|
|
165
|
-
sendOpenAIChunk({}, event.finishReason);
|
|
166
|
-
if (event.usage !== undefined) {
|
|
167
|
-
// Match OpenAI: usage arrives in a separate final chunk with an
|
|
168
|
-
// empty choices array.
|
|
169
|
-
sendOpenAI({
|
|
170
|
-
...openAIChatChunk({
|
|
171
|
-
id: args.id,
|
|
172
|
-
created,
|
|
173
|
-
model: args.model,
|
|
174
|
-
delta: {},
|
|
175
|
-
finishReason: null,
|
|
176
|
-
}),
|
|
177
|
-
choices: [],
|
|
178
|
-
usage: openAIUsage(event.usage),
|
|
179
|
-
});
|
|
180
|
-
}
|
|
181
|
-
} else {
|
|
182
|
-
sendAnthropic("message_delta", {
|
|
183
|
-
type: "message_delta",
|
|
184
|
-
delta: {
|
|
185
|
-
stop_reason: anthropicStopReason(event.finishReason),
|
|
186
|
-
stop_sequence: null,
|
|
187
|
-
},
|
|
188
|
-
usage: anthropicUsage(event.usage),
|
|
189
|
-
});
|
|
190
|
-
}
|
|
191
|
-
return;
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
emitted = true;
|
|
195
|
-
validateStreamSequence(event, sequenceIndexes);
|
|
196
|
-
if (args.protocol !== "openai_chat_completions") {
|
|
197
|
-
sendAnthropicEvent(event, anthropicState, sendAnthropic);
|
|
198
|
-
return;
|
|
199
|
-
}
|
|
200
|
-
if (event.type === "tool_call_start" || event.type === "hosted_tool_call") {
|
|
201
|
-
openAIToolIndexes.set(event.index, nextOpenAIToolIndex++);
|
|
202
|
-
}
|
|
203
|
-
const delta = openAIStreamDelta(
|
|
204
|
-
event,
|
|
205
|
-
event.type === "tool_call_start" ||
|
|
206
|
-
event.type === "tool_call_delta" ||
|
|
207
|
-
event.type === "hosted_tool_call"
|
|
208
|
-
? openAIToolIndexes.get(event.index)
|
|
209
|
-
: undefined,
|
|
210
|
-
// openAIStreamDelta emits a detail for every reasoning_end, so the index always advances.
|
|
211
|
-
event.type === "reasoning_end" ? nextOpenAIReasoningIndex++ : undefined,
|
|
212
|
-
);
|
|
213
|
-
if (delta !== null) sendOpenAIChunk(delta);
|
|
214
|
-
};
|
|
215
|
-
|
|
216
|
-
const finalize = async () => {
|
|
217
|
-
if (finalized) return;
|
|
218
|
-
finalized = true;
|
|
219
|
-
await closeAsyncIterator(args.primed.iterator);
|
|
220
|
-
args.onFinalize(streamCompletion, streamError);
|
|
221
|
-
if (cancelledByReader) return;
|
|
222
|
-
if (streamCompletion !== null && streamError === null) {
|
|
223
|
-
if (args.protocol === "openai_chat_completions") sendOpenAI("[DONE]");
|
|
224
|
-
else sendAnthropic("message_stop", { type: "message_stop" });
|
|
225
|
-
}
|
|
226
|
-
streamController.close();
|
|
227
|
-
};
|
|
228
|
-
|
|
229
|
-
// Pull-based pump: one executor event per consumer pull, so slow readers
|
|
230
|
-
// exert backpressure on the upstream provider instead of buffering the
|
|
231
|
-
// whole response.
|
|
232
|
-
const body = new ReadableStream<Uint8Array>({
|
|
233
|
-
start(controller) {
|
|
234
|
-
streamController = controller;
|
|
235
|
-
if (args.protocol === "openai_chat_completions") {
|
|
236
|
-
sendOpenAI(openAIChatChunk({
|
|
237
|
-
id: args.id,
|
|
238
|
-
created,
|
|
239
|
-
model: args.model,
|
|
240
|
-
delta: { role: "assistant" },
|
|
241
|
-
finishReason: null,
|
|
242
|
-
routing: { requestedModel: args.requestedModel, selection: args.selection },
|
|
243
|
-
}));
|
|
244
|
-
} else {
|
|
245
|
-
sendAnthropic("message_start", anthropicMessageStart({
|
|
246
|
-
id: args.id,
|
|
247
|
-
model: args.model,
|
|
248
|
-
requestedModel: args.requestedModel,
|
|
249
|
-
selection: args.selection,
|
|
250
|
-
}));
|
|
251
|
-
}
|
|
252
|
-
},
|
|
253
|
-
async pull() {
|
|
254
|
-
if (finalized) return;
|
|
255
|
-
// Advance the executor until at least one SSE frame is enqueued: some
|
|
256
|
-
// events (tool_call_end, buffered pending tools) emit nothing, and a
|
|
257
|
-
// pull that enqueues nothing would stall the stream.
|
|
258
|
-
const target = enqueuedFrames + 1;
|
|
259
|
-
try {
|
|
260
|
-
while (!finalized && enqueuedFrames < target) {
|
|
261
|
-
if (args.controller.signal.aborted) {
|
|
262
|
-
await finalize();
|
|
263
|
-
return;
|
|
264
|
-
}
|
|
265
|
-
let event: RouterStreamEvent;
|
|
266
|
-
if (firstEvent !== undefined) {
|
|
267
|
-
event = firstEvent;
|
|
268
|
-
firstEvent = undefined;
|
|
269
|
-
} else {
|
|
270
|
-
const next = await args.primed.iterator.next();
|
|
271
|
-
if (finalized) return;
|
|
272
|
-
if (args.controller.signal.aborted) {
|
|
273
|
-
await finalize();
|
|
274
|
-
return;
|
|
275
|
-
}
|
|
276
|
-
if (next.done) {
|
|
277
|
-
if (streamCompletion === null) {
|
|
278
|
-
throw streamContract(
|
|
279
|
-
emitted
|
|
280
|
-
? "Executor stream ended without a finish event."
|
|
281
|
-
: "Executor stream ended without output or a finish event.",
|
|
282
|
-
);
|
|
283
|
-
}
|
|
284
|
-
await finalize();
|
|
285
|
-
return;
|
|
286
|
-
}
|
|
287
|
-
event = next.value;
|
|
288
|
-
}
|
|
289
|
-
processEvent(event);
|
|
290
|
-
}
|
|
291
|
-
} catch (error) {
|
|
292
|
-
if (finalized) return;
|
|
293
|
-
streamError = error instanceof RouterFrameworkError
|
|
294
|
-
? error
|
|
295
|
-
: streamContract(errorMessage(error, "Executor stream failed."));
|
|
296
|
-
if (!args.controller.signal.aborted) {
|
|
297
|
-
// Client aborts and reader cancels abort the controller before the
|
|
298
|
-
// pump observes them, so un-aborted here means a genuine failure.
|
|
299
|
-
args.onStreamFailure?.();
|
|
300
|
-
const message = errorMessage(error, "Executor stream failed.");
|
|
301
|
-
if (args.protocol === "openai_chat_completions") {
|
|
302
|
-
sendOpenAI({
|
|
303
|
-
error: {
|
|
304
|
-
message,
|
|
305
|
-
type: "server_error",
|
|
306
|
-
code: error instanceof RouterFrameworkError ? error.code : "stream_failed",
|
|
307
|
-
},
|
|
308
|
-
});
|
|
309
|
-
} else {
|
|
310
|
-
sendAnthropic("error", {
|
|
311
|
-
type: "error",
|
|
312
|
-
error: { type: "api_error", message },
|
|
313
|
-
});
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
|
-
// Stop the provider request from burning tokens once the stream is
|
|
317
|
-
// dead; client aborts and reader cancels already do this.
|
|
318
|
-
args.controller.abort(streamError);
|
|
319
|
-
await finalize();
|
|
320
|
-
}
|
|
321
|
-
},
|
|
322
|
-
async cancel(reason) {
|
|
323
|
-
cancelledByReader = true;
|
|
324
|
-
args.controller.abort(reason);
|
|
325
|
-
await finalize();
|
|
326
|
-
},
|
|
327
|
-
});
|
|
328
|
-
|
|
329
|
-
return new Response(body, {
|
|
330
|
-
headers: {
|
|
331
|
-
...Object.fromEntries(new Headers(args.headers)),
|
|
332
|
-
"Content-Type": "text/event-stream",
|
|
333
|
-
"Cache-Control": "no-cache",
|
|
334
|
-
Connection: "keep-alive",
|
|
335
|
-
},
|
|
336
|
-
});
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
type AnthropicStreamBlock = {
|
|
340
|
-
kind: "text" | "tool" | "reasoning";
|
|
341
|
-
wireIndex: number;
|
|
342
|
-
};
|
|
343
|
-
|
|
344
|
-
type AnthropicPendingTool = {
|
|
345
|
-
id: string;
|
|
346
|
-
name: string;
|
|
347
|
-
deltas: string[];
|
|
348
|
-
ended: boolean;
|
|
349
|
-
};
|
|
350
|
-
|
|
351
|
-
type AnthropicStreamState = {
|
|
352
|
-
openBlocks: Map<number, AnthropicStreamBlock>;
|
|
353
|
-
pendingTools: Map<number, AnthropicPendingTool>;
|
|
354
|
-
pendingToolOrder: Array<{
|
|
355
|
-
normalizedIndex: number;
|
|
356
|
-
tool: AnthropicPendingTool;
|
|
357
|
-
}>;
|
|
358
|
-
nextWireIndex: number;
|
|
359
|
-
};
|
|
360
|
-
|
|
361
|
-
function sendAnthropicEvent(
|
|
362
|
-
event: Exclude<RouterStreamEvent, { type: "finish" }>,
|
|
363
|
-
state: AnthropicStreamState,
|
|
364
|
-
send: (event: string, value: Record<string, unknown>) => void,
|
|
365
|
-
): void {
|
|
366
|
-
if (event.type === "hosted_tool_call") {
|
|
367
|
-
throw new RouterFrameworkError(
|
|
368
|
-
"configuration",
|
|
369
|
-
"Hosted tool calls are not representable on the anthropic_messages protocol; route hosted-tool models over the OpenAI protocol.",
|
|
370
|
-
"hosted_tool_call_unrepresentable",
|
|
371
|
-
);
|
|
372
|
-
}
|
|
373
|
-
if (event.type === "text_delta") {
|
|
374
|
-
closeAnthropicTextBlocks(state, send, event.index);
|
|
375
|
-
if (hasOpenAnthropicBlock(state, "tool")) {
|
|
376
|
-
throw streamContract("Anthropic text cannot interleave with an open tool call.");
|
|
377
|
-
}
|
|
378
|
-
if (hasOpenAnthropicBlock(state, "reasoning", event.index)) {
|
|
379
|
-
throw streamContract("Anthropic text cannot interleave with an open thinking block.");
|
|
380
|
-
}
|
|
381
|
-
let block = state.openBlocks.get(event.index);
|
|
382
|
-
if (block === undefined) {
|
|
383
|
-
block = { kind: "text", wireIndex: state.nextWireIndex++ };
|
|
384
|
-
state.openBlocks.set(event.index, block);
|
|
385
|
-
send("content_block_start", {
|
|
386
|
-
type: "content_block_start",
|
|
387
|
-
index: block.wireIndex,
|
|
388
|
-
content_block: { type: "text", text: "" },
|
|
389
|
-
});
|
|
390
|
-
}
|
|
391
|
-
send("content_block_delta", {
|
|
392
|
-
type: "content_block_delta",
|
|
393
|
-
index: block.wireIndex,
|
|
394
|
-
delta: { type: "text_delta", text: event.delta },
|
|
395
|
-
});
|
|
396
|
-
return;
|
|
397
|
-
}
|
|
398
|
-
if (event.type === "reasoning_delta") {
|
|
399
|
-
closeAnthropicTextBlocks(state, send, event.index);
|
|
400
|
-
if (hasOpenAnthropicBlock(state, "tool")) {
|
|
401
|
-
throw streamContract("Anthropic reasoning cannot interleave with an open tool call.");
|
|
402
|
-
}
|
|
403
|
-
if (hasOpenAnthropicBlock(state, "reasoning", event.index)) {
|
|
404
|
-
throw streamContract("Anthropic thinking blocks cannot interleave with each other.");
|
|
405
|
-
}
|
|
406
|
-
let block = state.openBlocks.get(event.index);
|
|
407
|
-
if (block === undefined) {
|
|
408
|
-
block = { kind: "reasoning", wireIndex: state.nextWireIndex++ };
|
|
409
|
-
state.openBlocks.set(event.index, block);
|
|
410
|
-
send("content_block_start", {
|
|
411
|
-
type: "content_block_start",
|
|
412
|
-
index: block.wireIndex,
|
|
413
|
-
content_block: { type: "thinking", thinking: "", signature: "" },
|
|
414
|
-
});
|
|
415
|
-
}
|
|
416
|
-
send("content_block_delta", {
|
|
417
|
-
type: "content_block_delta",
|
|
418
|
-
index: block.wireIndex,
|
|
419
|
-
delta: { type: "thinking_delta", thinking: event.delta },
|
|
420
|
-
});
|
|
421
|
-
return;
|
|
422
|
-
}
|
|
423
|
-
if (event.type === "reasoning_end") {
|
|
424
|
-
const block = state.openBlocks.get(event.index);
|
|
425
|
-
if (block !== undefined && block.kind !== "reasoning") {
|
|
426
|
-
throw streamContract(`Anthropic stream index ${event.index} is a ${block.kind} block, not reasoning.`);
|
|
427
|
-
}
|
|
428
|
-
if (event.redacted === true) {
|
|
429
|
-
// Redacted thinking has no streamable content: emit the complete block
|
|
430
|
-
// at once. SDK accumulators apply signature_delta to .signature, which
|
|
431
|
-
// would leave data empty and fail replay.
|
|
432
|
-
if (block !== undefined) {
|
|
433
|
-
throw streamContract("Anthropic redacted thinking cannot follow streamed thinking deltas.");
|
|
434
|
-
}
|
|
435
|
-
const wireIndex = state.nextWireIndex++;
|
|
436
|
-
send("content_block_start", {
|
|
437
|
-
type: "content_block_start",
|
|
438
|
-
index: wireIndex,
|
|
439
|
-
content_block: {
|
|
440
|
-
type: "redacted_thinking",
|
|
441
|
-
data: reasoningEnvelope(event, wireIndex),
|
|
442
|
-
},
|
|
443
|
-
});
|
|
444
|
-
stopAnthropicBlock(send, wireIndex);
|
|
445
|
-
return;
|
|
446
|
-
}
|
|
447
|
-
let wireIndex: number;
|
|
448
|
-
if (block === undefined) {
|
|
449
|
-
// Empty thinking with no prior reasoning_delta: open and immediately
|
|
450
|
-
// close a block so the content-block shape matches non-streaming
|
|
451
|
-
// output.
|
|
452
|
-
wireIndex = state.nextWireIndex++;
|
|
453
|
-
send("content_block_start", {
|
|
454
|
-
type: "content_block_start",
|
|
455
|
-
index: wireIndex,
|
|
456
|
-
content_block: { type: "thinking", thinking: "", signature: "" },
|
|
457
|
-
});
|
|
458
|
-
} else {
|
|
459
|
-
wireIndex = block.wireIndex;
|
|
460
|
-
state.openBlocks.delete(event.index);
|
|
461
|
-
}
|
|
462
|
-
// Every thinking block gets a signature: the provider continuation when
|
|
463
|
-
// one exists, otherwise a portable envelope so the block replays through
|
|
464
|
-
// the request parser.
|
|
465
|
-
send("content_block_delta", {
|
|
466
|
-
type: "content_block_delta",
|
|
467
|
-
index: wireIndex,
|
|
468
|
-
delta: { type: "signature_delta", signature: reasoningEnvelope(event, wireIndex) },
|
|
469
|
-
});
|
|
470
|
-
stopAnthropicBlock(send, wireIndex);
|
|
471
|
-
return;
|
|
472
|
-
}
|
|
473
|
-
if (event.type === "tool_call_start") {
|
|
474
|
-
closeAnthropicTextBlocks(state, send);
|
|
475
|
-
if (hasOpenAnthropicBlock(state, "reasoning")) {
|
|
476
|
-
throw streamContract("Anthropic tool calls cannot interleave with an open thinking block.");
|
|
477
|
-
}
|
|
478
|
-
if (hasOpenAnthropicBlock(state, "tool")) {
|
|
479
|
-
const pending = {
|
|
480
|
-
id: event.id,
|
|
481
|
-
name: event.name,
|
|
482
|
-
deltas: [],
|
|
483
|
-
ended: false,
|
|
484
|
-
};
|
|
485
|
-
state.pendingTools.set(event.index, pending);
|
|
486
|
-
state.pendingToolOrder.push({ normalizedIndex: event.index, tool: pending });
|
|
487
|
-
return;
|
|
488
|
-
}
|
|
489
|
-
startAnthropicTool(state, send, event.index, event.id, event.name);
|
|
490
|
-
return;
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
const block = state.openBlocks.get(event.index);
|
|
494
|
-
if (block?.kind === "tool") {
|
|
495
|
-
if (event.type === "tool_call_delta") {
|
|
496
|
-
sendAnthropicToolDelta(send, block.wireIndex, event.delta);
|
|
497
|
-
return;
|
|
498
|
-
}
|
|
499
|
-
state.openBlocks.delete(event.index);
|
|
500
|
-
stopAnthropicBlock(send, block.wireIndex);
|
|
501
|
-
flushPendingAnthropicTools(state, send);
|
|
502
|
-
return;
|
|
503
|
-
}
|
|
504
|
-
|
|
505
|
-
const pending = state.pendingTools.get(event.index);
|
|
506
|
-
if (pending === undefined) {
|
|
507
|
-
throw streamContract(`Anthropic stream index ${event.index} has no open tool call.`);
|
|
508
|
-
}
|
|
509
|
-
if (event.type === "tool_call_delta") {
|
|
510
|
-
pending.deltas.push(event.delta);
|
|
511
|
-
} else {
|
|
512
|
-
pending.ended = true;
|
|
513
|
-
state.pendingTools.delete(event.index);
|
|
514
|
-
}
|
|
515
|
-
}
|
|
516
|
-
|
|
517
|
-
function hasOpenAnthropicBlock(
|
|
518
|
-
state: AnthropicStreamState,
|
|
519
|
-
kind: "tool" | "reasoning",
|
|
520
|
-
exceptIndex?: number,
|
|
521
|
-
): boolean {
|
|
522
|
-
for (const [index, block] of state.openBlocks) {
|
|
523
|
-
if (block.kind === kind && index !== exceptIndex) return true;
|
|
524
|
-
}
|
|
525
|
-
return false;
|
|
526
|
-
}
|
|
527
|
-
|
|
528
|
-
function reasoningEnvelope(
|
|
529
|
-
event: Extract<RouterStreamEvent, { type: "reasoning_end" }>,
|
|
530
|
-
wireIndex: number,
|
|
531
|
-
): string {
|
|
532
|
-
return event.continuation
|
|
533
|
-
? encodeProviderContinuationState(event.continuation)
|
|
534
|
-
: encodePortableReasoningState({
|
|
535
|
-
...(event.source === undefined ? {} : { source: event.source }),
|
|
536
|
-
itemId: event.itemId ?? `reasoning_${wireIndex}`,
|
|
537
|
-
});
|
|
538
|
-
}
|
|
539
|
-
|
|
540
|
-
function startAnthropicTool(
|
|
541
|
-
state: AnthropicStreamState,
|
|
542
|
-
send: (event: string, value: Record<string, unknown>) => void,
|
|
543
|
-
normalizedIndex: number,
|
|
544
|
-
id: string,
|
|
545
|
-
name: string,
|
|
546
|
-
): AnthropicStreamBlock {
|
|
547
|
-
const block = { kind: "tool" as const, wireIndex: state.nextWireIndex++ };
|
|
548
|
-
state.openBlocks.set(normalizedIndex, block);
|
|
549
|
-
send("content_block_start", {
|
|
550
|
-
type: "content_block_start",
|
|
551
|
-
index: block.wireIndex,
|
|
552
|
-
content_block: { type: "tool_use", id, name, input: {} },
|
|
553
|
-
});
|
|
554
|
-
return block;
|
|
555
|
-
}
|
|
556
|
-
|
|
557
|
-
function sendAnthropicToolDelta(
|
|
558
|
-
send: (event: string, value: Record<string, unknown>) => void,
|
|
559
|
-
wireIndex: number,
|
|
560
|
-
delta: string,
|
|
561
|
-
): void {
|
|
562
|
-
send("content_block_delta", {
|
|
563
|
-
type: "content_block_delta",
|
|
564
|
-
index: wireIndex,
|
|
565
|
-
delta: { type: "input_json_delta", partial_json: delta },
|
|
566
|
-
});
|
|
567
|
-
}
|
|
568
|
-
|
|
569
|
-
function stopAnthropicBlock(
|
|
570
|
-
send: (event: string, value: Record<string, unknown>) => void,
|
|
571
|
-
wireIndex: number,
|
|
572
|
-
): void {
|
|
573
|
-
send("content_block_stop", { type: "content_block_stop", index: wireIndex });
|
|
574
|
-
}
|
|
575
|
-
|
|
576
|
-
function flushPendingAnthropicTools(
|
|
577
|
-
state: AnthropicStreamState,
|
|
578
|
-
send: (event: string, value: Record<string, unknown>) => void,
|
|
579
|
-
): void {
|
|
580
|
-
let entry = state.pendingToolOrder.shift();
|
|
581
|
-
while (entry !== undefined) {
|
|
582
|
-
const { normalizedIndex, tool } = entry;
|
|
583
|
-
if (state.pendingTools.get(normalizedIndex) === tool) {
|
|
584
|
-
state.pendingTools.delete(normalizedIndex);
|
|
585
|
-
}
|
|
586
|
-
const block = startAnthropicTool(
|
|
587
|
-
state,
|
|
588
|
-
send,
|
|
589
|
-
normalizedIndex,
|
|
590
|
-
tool.id,
|
|
591
|
-
tool.name,
|
|
592
|
-
);
|
|
593
|
-
for (const delta of tool.deltas) {
|
|
594
|
-
sendAnthropicToolDelta(send, block.wireIndex, delta);
|
|
595
|
-
}
|
|
596
|
-
if (!tool.ended) return;
|
|
597
|
-
state.openBlocks.delete(normalizedIndex);
|
|
598
|
-
stopAnthropicBlock(send, block.wireIndex);
|
|
599
|
-
entry = state.pendingToolOrder.shift();
|
|
600
|
-
}
|
|
601
|
-
}
|
|
602
|
-
|
|
603
|
-
function closeAnthropicTextBlocks(
|
|
604
|
-
state: AnthropicStreamState,
|
|
605
|
-
send: (event: string, value: Record<string, unknown>) => void,
|
|
606
|
-
exceptIndex?: number,
|
|
607
|
-
): void {
|
|
608
|
-
for (const [normalizedIndex, block] of state.openBlocks) {
|
|
609
|
-
if (block.kind !== "text" || normalizedIndex === exceptIndex) continue;
|
|
610
|
-
stopAnthropicBlock(send, block.wireIndex);
|
|
611
|
-
state.openBlocks.delete(normalizedIndex);
|
|
612
|
-
}
|
|
613
|
-
}
|