narraeon 0.1.1 → 0.3.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.
- package/dist/node/protocol/modelConnections.js +49 -0
- package/dist/node/protocol/modelUsage.js +39 -0
- package/dist/node/protocol/v1.js +35 -0
- package/dist/node/runtime/FileNativePhysicalClone.js +86 -0
- package/dist/node/runtime/V1Runtime.js +66 -29
- package/dist/node/runtime/model/FileNativeModelAdapters.js +730 -287
- package/dist/node/runtime/model/ModelConnectionStore.js +123 -2
- package/dist/node/runtime/model/ModelHost.js +81 -9
- package/dist/node/runtime/model/ModelHostStream.js +61 -1
- package/dist/node/runtime/model/SettingAuthorStream.js +16 -113
- package/dist/node/runtime/play/FileNativePlayAdvanceStore.js +269 -0
- package/dist/node/runtime/play/FileNativePlayTimelineStore.js +1128 -0
- package/dist/node/runtime/play/PlayCallChain.js +904 -369
- package/dist/node/runtime/play/PlayDocumentTools.js +35 -0
- package/dist/node/runtime/play/PlayFollowupRequests.js +43 -26
- package/dist/node/runtime/prompt/FileNativePromptCompiler.js +50 -35
- package/dist/node/runtime/prompt/FileNativeToolRegistry.js +55 -29
- package/dist/node/runtime/setting/DocumentCandidateSettingImprovement.js +9 -4
- package/dist/node/runtime/world/FileNativeAuthorityV3.js +971 -0
- package/dist/node/runtime/world/FileNativeHistoryProjection.js +39 -0
- package/dist/node/runtime/world/FileNativeWorldStorageMigrator.js +1067 -0
- package/dist/node/runtime/world/FileNativeWorldStore.js +615 -784
- package/dist/node/server/createServer.js +10 -2
- package/dist/web/assets/index-D5VqDvjY.js +1192 -0
- package/dist/web/assets/index-Xa0s35CJ.css +1 -0
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/dist/web/assets/index-CRGTZ9Ak.css +0 -1
- package/dist/web/assets/index-DDx1itzT.js +0 -1192
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { chmod, mkdir, open, readFile, rename, stat, unlink, } from "node:fs/promises";
|
|
3
3
|
import { join } from "node:path";
|
|
4
|
-
import { modelProviderPresets, } from "../../protocol/modelConnections.js";
|
|
4
|
+
import { hasCLIProxyThinkingSuffix, modelReasoningPolicyIssue, modelProviderPresets, } from "../../protocol/modelConnections.js";
|
|
5
5
|
const libraryFileName = "model-connections-v1.json";
|
|
6
6
|
const maxModelListBytes = 1024 * 1024;
|
|
7
7
|
export class ModelConnectionUnavailableError extends Error {
|
|
@@ -204,6 +204,26 @@ function normalizeSaveInput(input, existing) {
|
|
|
204
204
|
const provider = validateProvider(input.provider);
|
|
205
205
|
const baseUrl = normalizeBaseUrl(requiredTrimmed(input.baseUrl, "Base URL", 4096));
|
|
206
206
|
const modelId = requiredTrimmed(input.modelId, "Model ID", 512);
|
|
207
|
+
const dialect = validateDialect(input.dialect ?? existing?.dialect);
|
|
208
|
+
const reasoningEffort = validateReasoningEffort(input.reasoningEffort ?? existing?.reasoningEffort);
|
|
209
|
+
const reasoningSummary = validateReasoningSummary(input.reasoningSummary ?? existing?.reasoningSummary);
|
|
210
|
+
const thinkingMode = validateThinkingMode(input.thinkingMode ?? existing?.thinkingMode);
|
|
211
|
+
const thinkingBudgetTokens = validateThinkingBudgetTokens(input.thinkingBudgetTokens !== undefined
|
|
212
|
+
? input.thinkingBudgetTokens
|
|
213
|
+
: thinkingMode === "enabled"
|
|
214
|
+
? existing?.thinkingBudgetTokens
|
|
215
|
+
: null);
|
|
216
|
+
validateTokenLimits(input.contextWindowTokens, input.maxOutputTokens);
|
|
217
|
+
validateReasoningPolicy({
|
|
218
|
+
provider,
|
|
219
|
+
dialect,
|
|
220
|
+
modelId,
|
|
221
|
+
effort: reasoningEffort,
|
|
222
|
+
summary: reasoningSummary,
|
|
223
|
+
thinking: thinkingMode,
|
|
224
|
+
thinkingBudgetTokens,
|
|
225
|
+
maxOutputTokens: input.maxOutputTokens,
|
|
226
|
+
});
|
|
207
227
|
const explicitApiKey = normalizeOptionalApiKey(input.apiKey);
|
|
208
228
|
const apiKey = explicitApiKey ??
|
|
209
229
|
(existing?.provider === provider && existing.baseUrl === baseUrl
|
|
@@ -213,15 +233,19 @@ function normalizeSaveInput(input, existing) {
|
|
|
213
233
|
throw new Error(existing === undefined
|
|
214
234
|
? "A new model configuration requires an API key"
|
|
215
235
|
: "The endpoint or protocol changed; enter the API key again because old credentials are never forwarded to a new endpoint");
|
|
216
|
-
validateTokenLimits(input.contextWindowTokens, input.maxOutputTokens);
|
|
217
236
|
validatePresetBinding(presetId, provider, baseUrl);
|
|
218
237
|
return {
|
|
219
238
|
name,
|
|
220
239
|
presetId,
|
|
221
240
|
provider,
|
|
241
|
+
dialect,
|
|
222
242
|
baseUrl,
|
|
223
243
|
apiKey,
|
|
224
244
|
modelId,
|
|
245
|
+
reasoningEffort,
|
|
246
|
+
reasoningSummary,
|
|
247
|
+
thinkingMode,
|
|
248
|
+
thinkingBudgetTokens,
|
|
225
249
|
contextWindowTokens: input.contextWindowTokens,
|
|
226
250
|
maxOutputTokens: input.maxOutputTokens,
|
|
227
251
|
};
|
|
@@ -253,19 +277,52 @@ function validateStoredConnection(value) {
|
|
|
253
277
|
const name = requiredTrimmed(value.name, "Configuration name", 160);
|
|
254
278
|
const presetId = validatePresetId(value.presetId);
|
|
255
279
|
const provider = validateProvider(value.provider);
|
|
280
|
+
const dialect = validateDialect(value.dialect);
|
|
256
281
|
const baseUrl = normalizeBaseUrl(requiredTrimmed(value.baseUrl, "Base URL"));
|
|
257
282
|
const apiKey = requiredTrimmed(value.apiKey, "API Key", 16_384);
|
|
258
283
|
const modelId = requiredTrimmed(value.modelId, "Model ID", 512);
|
|
284
|
+
let reasoningEffort = validateReasoningEffort(value.reasoningEffort);
|
|
285
|
+
const reasoningSummary = validateReasoningSummary(value.reasoningSummary);
|
|
286
|
+
let thinkingMode = validateThinkingMode(value.thinkingMode);
|
|
287
|
+
const thinkingBudgetTokens = validateThinkingBudgetTokens(value.thinkingBudgetTokens);
|
|
288
|
+
// Before Thinking became an independent field, Anthropic effort=none meant
|
|
289
|
+
// disabled thinking and a non-default summary implicitly activated adaptive
|
|
290
|
+
// thinking. Normalize that released shape without reinterpreting new data.
|
|
291
|
+
if (value.thinkingMode === undefined && provider === "anthropic_messages") {
|
|
292
|
+
if (reasoningEffort === "none") {
|
|
293
|
+
reasoningEffort = "provider_default";
|
|
294
|
+
thinkingMode = "disabled";
|
|
295
|
+
}
|
|
296
|
+
else if (reasoningSummary !== "provider_default" &&
|
|
297
|
+
!(dialect === "cliproxyapi" && hasCLIProxyThinkingSuffix(modelId))) {
|
|
298
|
+
thinkingMode = "adaptive";
|
|
299
|
+
}
|
|
300
|
+
}
|
|
259
301
|
validateTokenLimits(value.contextWindowTokens, value.maxOutputTokens);
|
|
302
|
+
validateReasoningPolicy({
|
|
303
|
+
provider,
|
|
304
|
+
dialect,
|
|
305
|
+
modelId,
|
|
306
|
+
effort: reasoningEffort,
|
|
307
|
+
summary: reasoningSummary,
|
|
308
|
+
thinking: thinkingMode,
|
|
309
|
+
thinkingBudgetTokens,
|
|
310
|
+
maxOutputTokens: value.maxOutputTokens,
|
|
311
|
+
});
|
|
260
312
|
validatePresetBinding(presetId, provider, baseUrl);
|
|
261
313
|
return {
|
|
262
314
|
id,
|
|
263
315
|
name,
|
|
264
316
|
presetId,
|
|
265
317
|
provider,
|
|
318
|
+
dialect,
|
|
266
319
|
baseUrl,
|
|
267
320
|
apiKey,
|
|
268
321
|
modelId,
|
|
322
|
+
reasoningEffort,
|
|
323
|
+
reasoningSummary,
|
|
324
|
+
thinkingMode,
|
|
325
|
+
thinkingBudgetTokens,
|
|
269
326
|
contextWindowTokens: value.contextWindowTokens,
|
|
270
327
|
maxOutputTokens: value.maxOutputTokens,
|
|
271
328
|
};
|
|
@@ -276,8 +333,13 @@ function toView(connection) {
|
|
|
276
333
|
name: connection.name,
|
|
277
334
|
presetId: connection.presetId,
|
|
278
335
|
provider: connection.provider,
|
|
336
|
+
dialect: connection.dialect,
|
|
279
337
|
baseUrl: connection.baseUrl,
|
|
280
338
|
modelId: connection.modelId,
|
|
339
|
+
reasoningEffort: connection.reasoningEffort,
|
|
340
|
+
reasoningSummary: connection.reasoningSummary,
|
|
341
|
+
thinkingMode: connection.thinkingMode,
|
|
342
|
+
thinkingBudgetTokens: connection.thinkingBudgetTokens,
|
|
281
343
|
contextWindowTokens: connection.contextWindowTokens,
|
|
282
344
|
maxOutputTokens: connection.maxOutputTokens,
|
|
283
345
|
hasApiKey: true,
|
|
@@ -286,9 +348,14 @@ function toView(connection) {
|
|
|
286
348
|
function toBinding(connection) {
|
|
287
349
|
return structuredClone({
|
|
288
350
|
provider: connection.provider,
|
|
351
|
+
dialect: connection.dialect,
|
|
289
352
|
baseUrl: connection.baseUrl,
|
|
290
353
|
apiKey: connection.apiKey,
|
|
291
354
|
modelId: connection.modelId,
|
|
355
|
+
reasoningEffort: connection.reasoningEffort,
|
|
356
|
+
reasoningSummary: connection.reasoningSummary,
|
|
357
|
+
thinkingMode: connection.thinkingMode,
|
|
358
|
+
thinkingBudgetTokens: connection.thinkingBudgetTokens,
|
|
292
359
|
contextWindowTokens: connection.contextWindowTokens,
|
|
293
360
|
maxOutputTokens: connection.maxOutputTokens,
|
|
294
361
|
});
|
|
@@ -300,6 +367,60 @@ function validateProvider(value) {
|
|
|
300
367
|
throw new Error("Model-connection protocol is invalid");
|
|
301
368
|
return value;
|
|
302
369
|
}
|
|
370
|
+
function validateDialect(value) {
|
|
371
|
+
if (value === undefined)
|
|
372
|
+
return "standard";
|
|
373
|
+
if (value !== "standard" && value !== "cliproxyapi")
|
|
374
|
+
throw new Error("Model-provider dialect is invalid");
|
|
375
|
+
return value;
|
|
376
|
+
}
|
|
377
|
+
function validateReasoningEffort(value) {
|
|
378
|
+
if (value === undefined)
|
|
379
|
+
return "provider_default";
|
|
380
|
+
if (value !== "provider_default" &&
|
|
381
|
+
value !== "none" &&
|
|
382
|
+
value !== "minimal" &&
|
|
383
|
+
value !== "low" &&
|
|
384
|
+
value !== "medium" &&
|
|
385
|
+
value !== "high" &&
|
|
386
|
+
value !== "xhigh" &&
|
|
387
|
+
value !== "max")
|
|
388
|
+
throw new Error("Model reasoning effort is invalid");
|
|
389
|
+
return value;
|
|
390
|
+
}
|
|
391
|
+
function validateReasoningSummary(value) {
|
|
392
|
+
if (value === undefined)
|
|
393
|
+
return "provider_default";
|
|
394
|
+
if (value !== "provider_default" &&
|
|
395
|
+
value !== "auto" &&
|
|
396
|
+
value !== "concise" &&
|
|
397
|
+
value !== "detailed" &&
|
|
398
|
+
value !== "none")
|
|
399
|
+
throw new Error("Model reasoning-summary policy is invalid");
|
|
400
|
+
return value;
|
|
401
|
+
}
|
|
402
|
+
function validateThinkingMode(value) {
|
|
403
|
+
if (value === undefined)
|
|
404
|
+
return "provider_default";
|
|
405
|
+
if (value !== "provider_default" &&
|
|
406
|
+
value !== "adaptive" &&
|
|
407
|
+
value !== "enabled" &&
|
|
408
|
+
value !== "disabled")
|
|
409
|
+
throw new Error("Model thinking mode is invalid");
|
|
410
|
+
return value;
|
|
411
|
+
}
|
|
412
|
+
function validateThinkingBudgetTokens(value) {
|
|
413
|
+
if (value === undefined || value === null)
|
|
414
|
+
return null;
|
|
415
|
+
if (!Number.isSafeInteger(value) || Number(value) < 0)
|
|
416
|
+
throw new Error("Model thinking token budget is invalid");
|
|
417
|
+
return Number(value);
|
|
418
|
+
}
|
|
419
|
+
function validateReasoningPolicy(input) {
|
|
420
|
+
const issue = modelReasoningPolicyIssue(input);
|
|
421
|
+
if (issue !== null)
|
|
422
|
+
throw new Error(issue);
|
|
423
|
+
}
|
|
303
424
|
function validatePresetId(value) {
|
|
304
425
|
if (typeof value !== "string" ||
|
|
305
426
|
!modelProviderPresets.some(({ id }) => id === value))
|
|
@@ -4,7 +4,8 @@ export function equalModelHostBinding(left, right) {
|
|
|
4
4
|
left.modelId === right.modelId &&
|
|
5
5
|
left.contextWindowTokens === right.contextWindowTokens &&
|
|
6
6
|
left.maxOutputTokens === right.maxOutputTokens &&
|
|
7
|
-
left.protocolConfigFingerprint === right.protocolConfigFingerprint
|
|
7
|
+
left.protocolConfigFingerprint === right.protocolConfigFingerprint &&
|
|
8
|
+
left.cacheStrategy === right.cacheStrategy);
|
|
8
9
|
}
|
|
9
10
|
/** The persisted operation binding is unavailable in the current Runtime. */
|
|
10
11
|
export class ModelHostBindingMismatchError extends Error {
|
|
@@ -14,6 +15,14 @@ export class ModelHostBindingMismatchError extends Error {
|
|
|
14
15
|
this.name = "ModelHostBindingMismatchError";
|
|
15
16
|
}
|
|
16
17
|
}
|
|
18
|
+
/** The logical session cannot continue without its provider-native payload. */
|
|
19
|
+
export class ModelHostContinuationError extends Error {
|
|
20
|
+
kind = "continuation_unavailable";
|
|
21
|
+
constructor(message, options) {
|
|
22
|
+
super(message, options);
|
|
23
|
+
this.name = "ModelHostContinuationError";
|
|
24
|
+
}
|
|
25
|
+
}
|
|
17
26
|
/** A deterministic provider failure: the operation may be failed safely. */
|
|
18
27
|
export class ModelHostFailureError extends Error {
|
|
19
28
|
kind = "failed";
|
|
@@ -30,7 +39,19 @@ export class ModelHostOutcomeUnknownError extends Error {
|
|
|
30
39
|
this.name = "ModelHostOutcomeUnknownError";
|
|
31
40
|
}
|
|
32
41
|
}
|
|
42
|
+
/** The player explicitly stopped a dispatched request; it remains non-replayable. */
|
|
43
|
+
export class ModelHostCancelledError extends ModelHostOutcomeUnknownError {
|
|
44
|
+
constructor(message, options) {
|
|
45
|
+
super(message, options);
|
|
46
|
+
this.name = "ModelHostCancelledError";
|
|
47
|
+
}
|
|
48
|
+
}
|
|
33
49
|
/** Deterministic adapter for exercising the true external ModelHost port. */
|
|
50
|
+
/**
|
|
51
|
+
* Deterministic test double. Its synthetic provider state is fixture data, not
|
|
52
|
+
* a production continuation codec; real adapters must retain returned native
|
|
53
|
+
* state and never call scriptedProviderState.
|
|
54
|
+
*/
|
|
34
55
|
export class ScriptedModelHost {
|
|
35
56
|
requests = [];
|
|
36
57
|
#binding;
|
|
@@ -57,8 +78,8 @@ export class ScriptedModelHost {
|
|
|
57
78
|
for (const delta of deltas ?? [])
|
|
58
79
|
observer?.onDelta?.(structuredClone(delta));
|
|
59
80
|
const providerState = response.providerState ??
|
|
60
|
-
(continuation
|
|
61
|
-
? scriptedProviderState(this.#binding.provider, this.#responseIndex)
|
|
81
|
+
(continuation !== "lost"
|
|
82
|
+
? scriptedProviderState(this.#binding.provider, this.#responseIndex, response)
|
|
62
83
|
: undefined);
|
|
63
84
|
this.#responseIndex += 1;
|
|
64
85
|
return Promise.resolve({
|
|
@@ -69,21 +90,72 @@ export class ScriptedModelHost {
|
|
|
69
90
|
});
|
|
70
91
|
}
|
|
71
92
|
}
|
|
72
|
-
function scriptedProviderState(provider, responseIndex) {
|
|
73
|
-
if (provider === "openai_responses")
|
|
93
|
+
function scriptedProviderState(provider, responseIndex, response) {
|
|
94
|
+
if (provider === "openai_responses") {
|
|
95
|
+
const output = [];
|
|
96
|
+
if (response.reasoningContent !== undefined)
|
|
97
|
+
output.push({
|
|
98
|
+
type: "reasoning",
|
|
99
|
+
summary: [{ type: "summary_text", text: response.reasoningContent }],
|
|
100
|
+
});
|
|
101
|
+
if (response.text !== undefined)
|
|
102
|
+
output.push({
|
|
103
|
+
type: "message",
|
|
104
|
+
role: "assistant",
|
|
105
|
+
content: [{ type: "output_text", text: response.text }],
|
|
106
|
+
});
|
|
107
|
+
for (const call of response.toolCalls ?? [])
|
|
108
|
+
output.push({
|
|
109
|
+
type: "function_call",
|
|
110
|
+
call_id: call.id,
|
|
111
|
+
name: call.name,
|
|
112
|
+
arguments: JSON.stringify(call.arguments),
|
|
113
|
+
});
|
|
74
114
|
return {
|
|
75
115
|
protocol: "openai_responses",
|
|
76
|
-
output
|
|
116
|
+
output,
|
|
77
117
|
responseId: `scripted-${responseIndex}`,
|
|
78
118
|
};
|
|
79
|
-
|
|
119
|
+
}
|
|
120
|
+
if (provider === "anthropic_messages") {
|
|
121
|
+
const content = [];
|
|
122
|
+
if (response.reasoningContent !== undefined)
|
|
123
|
+
content.push({ type: "thinking", thinking: response.reasoningContent });
|
|
124
|
+
if (response.text !== undefined)
|
|
125
|
+
content.push({ type: "text", text: response.text });
|
|
126
|
+
for (const call of response.toolCalls ?? [])
|
|
127
|
+
content.push({
|
|
128
|
+
type: "tool_use",
|
|
129
|
+
id: call.id,
|
|
130
|
+
name: call.name,
|
|
131
|
+
input: structuredClone(call.arguments),
|
|
132
|
+
});
|
|
80
133
|
return {
|
|
81
134
|
protocol: "anthropic_messages",
|
|
82
|
-
content
|
|
135
|
+
content,
|
|
83
136
|
responseId: `scripted-${responseIndex}`,
|
|
84
137
|
};
|
|
138
|
+
}
|
|
85
139
|
return {
|
|
86
140
|
protocol: "chat_completions",
|
|
87
|
-
assistantMessage: {
|
|
141
|
+
assistantMessage: {
|
|
142
|
+
role: "assistant",
|
|
143
|
+
content: response.text ?? null,
|
|
144
|
+
...(response.reasoningContent === undefined
|
|
145
|
+
? {}
|
|
146
|
+
: { reasoning_content: response.reasoningContent }),
|
|
147
|
+
...((response.toolCalls ?? []).length === 0
|
|
148
|
+
? {}
|
|
149
|
+
: {
|
|
150
|
+
tool_calls: response.toolCalls.map((call) => ({
|
|
151
|
+
id: call.id,
|
|
152
|
+
type: "function",
|
|
153
|
+
function: {
|
|
154
|
+
name: call.name,
|
|
155
|
+
arguments: JSON.stringify(call.arguments),
|
|
156
|
+
},
|
|
157
|
+
})),
|
|
158
|
+
}),
|
|
159
|
+
},
|
|
88
160
|
};
|
|
89
161
|
}
|
|
@@ -4,8 +4,10 @@ export async function aggregateChatModelStream(body, onDelta) {
|
|
|
4
4
|
let content = "";
|
|
5
5
|
let reasoningContent = "";
|
|
6
6
|
let usage;
|
|
7
|
+
let stopReason;
|
|
7
8
|
let completed = false;
|
|
8
9
|
let sawPayload = false;
|
|
10
|
+
const providerMessage = { role: "assistant" };
|
|
9
11
|
const calls = new Map();
|
|
10
12
|
for await (const event of modelHostEvents(body)) {
|
|
11
13
|
if (event.data === "[DONE]") {
|
|
@@ -31,9 +33,12 @@ export async function aggregateChatModelStream(body, onDelta) {
|
|
|
31
33
|
choiceValue.finish_reason !== null &&
|
|
32
34
|
typeof choiceValue.finish_reason !== "string")
|
|
33
35
|
throw new Error("Chat SSE finish_reason is invalid");
|
|
36
|
+
if (typeof choiceValue.finish_reason === "string")
|
|
37
|
+
stopReason = choiceValue.finish_reason;
|
|
34
38
|
const delta = choiceValue.delta;
|
|
35
39
|
if (delta.role !== undefined && delta.role !== "assistant")
|
|
36
40
|
throw new Error("Chat SSE assistant role is invalid");
|
|
41
|
+
mergeChatProviderDelta(providerMessage, delta);
|
|
37
42
|
if (delta.content !== undefined &&
|
|
38
43
|
delta.content !== null &&
|
|
39
44
|
typeof delta.content !== "string")
|
|
@@ -113,6 +118,7 @@ export async function aggregateChatModelStream(body, onDelta) {
|
|
|
113
118
|
return { id: call.id, name: call.name, arguments: call.arguments };
|
|
114
119
|
});
|
|
115
120
|
const assistantMessage = {
|
|
121
|
+
...providerMessage,
|
|
116
122
|
role: "assistant",
|
|
117
123
|
content: content === "" ? null : content,
|
|
118
124
|
...(reasoningContent === "" ? {} : { reasoning_content: reasoningContent }),
|
|
@@ -132,8 +138,50 @@ export async function aggregateChatModelStream(body, onDelta) {
|
|
|
132
138
|
assistantMessage,
|
|
133
139
|
toolCalls,
|
|
134
140
|
usage,
|
|
141
|
+
...(stopReason === undefined ? {} : { stopReason }),
|
|
135
142
|
};
|
|
136
143
|
}
|
|
144
|
+
/**
|
|
145
|
+
* Preserve compatibility fields carried by Chat SSE deltas without pretending
|
|
146
|
+
* they are part of the Runtime projection. Known fields are rebuilt below from
|
|
147
|
+
* their protocol-defined fragment rules; unknown fields stay opaque here.
|
|
148
|
+
*/
|
|
149
|
+
function mergeChatProviderDelta(target, delta) {
|
|
150
|
+
for (const [key, value] of Object.entries(delta)) {
|
|
151
|
+
if (key === "role" ||
|
|
152
|
+
key === "content" ||
|
|
153
|
+
key === "reasoning_content" ||
|
|
154
|
+
key === "tool_calls" ||
|
|
155
|
+
value === undefined)
|
|
156
|
+
continue;
|
|
157
|
+
target[key] = mergeOpaqueDelta(target[key], value);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
function mergeOpaqueDelta(current, fragment) {
|
|
161
|
+
if (fragment === null)
|
|
162
|
+
return current ?? null;
|
|
163
|
+
if (typeof fragment === "string")
|
|
164
|
+
return typeof current === "string" ? current + fragment : fragment;
|
|
165
|
+
if (Array.isArray(fragment)) {
|
|
166
|
+
const result = Array.isArray(current) ? structuredClone(current) : [];
|
|
167
|
+
for (const item of fragment) {
|
|
168
|
+
if (isRecord(item) && Number.isSafeInteger(item.index)) {
|
|
169
|
+
const index = Number(item.index);
|
|
170
|
+
result[index] = mergeOpaqueDelta(result[index], item);
|
|
171
|
+
}
|
|
172
|
+
else
|
|
173
|
+
result.push(structuredClone(item));
|
|
174
|
+
}
|
|
175
|
+
return result;
|
|
176
|
+
}
|
|
177
|
+
if (isRecord(fragment)) {
|
|
178
|
+
const result = isRecord(current) ? structuredClone(current) : {};
|
|
179
|
+
for (const [key, value] of Object.entries(fragment))
|
|
180
|
+
result[key] = mergeOpaqueDelta(result[key], value);
|
|
181
|
+
return result;
|
|
182
|
+
}
|
|
183
|
+
return structuredClone(fragment);
|
|
184
|
+
}
|
|
137
185
|
export async function aggregateAnthropicModelStream(body, onDelta) {
|
|
138
186
|
const blocks = new Map();
|
|
139
187
|
const usage = {};
|
|
@@ -153,7 +201,8 @@ export async function aggregateAnthropicModelStream(body, onDelta) {
|
|
|
153
201
|
if (started || !isRecord(payload.message))
|
|
154
202
|
throw new Error("Anthropic SSE message_start is invalid");
|
|
155
203
|
if (payload.message.role !== "assistant" ||
|
|
156
|
-
!Array.isArray(payload.message.content)
|
|
204
|
+
!Array.isArray(payload.message.content) ||
|
|
205
|
+
payload.message.content.length !== 0)
|
|
157
206
|
throw new Error("Anthropic SSE initial message is invalid");
|
|
158
207
|
started = true;
|
|
159
208
|
responseId = stringOrUndefined(payload.message.id);
|
|
@@ -357,10 +406,21 @@ function mergeUsage(target, value) {
|
|
|
357
406
|
"output_tokens",
|
|
358
407
|
"cache_read_input_tokens",
|
|
359
408
|
"cache_creation_input_tokens",
|
|
409
|
+
"thinking_tokens",
|
|
360
410
|
]) {
|
|
361
411
|
if (value[field] !== undefined)
|
|
362
412
|
target[field] = value[field];
|
|
363
413
|
}
|
|
414
|
+
// Anthropic reports the billed thinking subset only on the final
|
|
415
|
+
// message_delta. Preserve the nested object so the shared usage normalizer
|
|
416
|
+
// sees the same shape for streaming and non-streaming responses.
|
|
417
|
+
if (isRecord(value.output_tokens_details))
|
|
418
|
+
target.output_tokens_details = {
|
|
419
|
+
...(isRecord(target.output_tokens_details)
|
|
420
|
+
? target.output_tokens_details
|
|
421
|
+
: {}),
|
|
422
|
+
...structuredClone(value.output_tokens_details),
|
|
423
|
+
};
|
|
364
424
|
}
|
|
365
425
|
function stringOrUndefined(value) {
|
|
366
426
|
return typeof value === "string" ? value : undefined;
|
|
@@ -1,118 +1,21 @@
|
|
|
1
|
-
import {
|
|
2
|
-
export
|
|
3
|
-
|
|
4
|
-
let reasoningContent = "";
|
|
5
|
-
let usage = null;
|
|
6
|
-
// Tool calls arrive as fragments keyed by index; id and name usually only
|
|
7
|
-
// appear on the first fragment, while arguments accumulate across many.
|
|
8
|
-
const calls = new Map();
|
|
9
|
-
for await (const event of providerStreamEvents(body)) {
|
|
10
|
-
const payload = providerStreamJson(event.data);
|
|
11
|
-
if (!isRecord(payload))
|
|
12
|
-
continue;
|
|
13
|
-
if (isRecord(payload.usage))
|
|
14
|
-
usage = payload.usage;
|
|
15
|
-
const choices = Array.isArray(payload.choices)
|
|
16
|
-
? payload.choices
|
|
17
|
-
: [];
|
|
18
|
-
const choice = choices[0];
|
|
19
|
-
if (!isRecord(choice) || !isRecord(choice.delta))
|
|
20
|
-
continue;
|
|
21
|
-
const { delta } = choice;
|
|
22
|
-
if (typeof delta.content === "string" && delta.content !== "") {
|
|
23
|
-
content += delta.content;
|
|
24
|
-
onDelta?.({ kind: "text", text: delta.content });
|
|
25
|
-
}
|
|
26
|
-
if (typeof delta.reasoning_content === "string" &&
|
|
27
|
-
delta.reasoning_content !== "") {
|
|
28
|
-
reasoningContent += delta.reasoning_content;
|
|
29
|
-
onDelta?.({ kind: "reasoning", text: delta.reasoning_content });
|
|
30
|
-
}
|
|
31
|
-
if (!Array.isArray(delta.tool_calls))
|
|
32
|
-
continue;
|
|
33
|
-
for (const [position, fragment] of delta.tool_calls.entries()) {
|
|
34
|
-
if (!isRecord(fragment))
|
|
35
|
-
continue;
|
|
36
|
-
const index = typeof fragment.index === "number" ? fragment.index : position;
|
|
37
|
-
const current = calls.get(index) ?? { id: "", name: "", arguments: "" };
|
|
38
|
-
if (typeof fragment.id === "string" && fragment.id !== "")
|
|
39
|
-
current.id = fragment.id;
|
|
40
|
-
const call = isRecord(fragment.function) ? fragment.function : {};
|
|
41
|
-
if (typeof call.name === "string" && call.name !== "")
|
|
42
|
-
current.name = call.name;
|
|
43
|
-
if (typeof call.arguments === "string" && call.arguments !== "") {
|
|
44
|
-
current.arguments += call.arguments;
|
|
45
|
-
onDelta?.({ kind: "tool", text: call.arguments });
|
|
46
|
-
}
|
|
47
|
-
calls.set(index, current);
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
return {
|
|
51
|
-
content,
|
|
52
|
-
reasoningContent,
|
|
53
|
-
toolCalls: [...calls.entries()]
|
|
54
|
-
.sort(([left], [right]) => left - right)
|
|
55
|
-
.map(([, call]) => call),
|
|
56
|
-
usage,
|
|
57
|
-
};
|
|
1
|
+
import { aggregateAnthropicModelStream, aggregateChatModelStream, } from "./ModelHostStream.js";
|
|
2
|
+
export function aggregateChatSettingStream(body, onDelta) {
|
|
3
|
+
return aggregateChatModelStream(body, onDelta);
|
|
58
4
|
}
|
|
59
5
|
export async function aggregateAnthropicSettingStream(body, onDelta) {
|
|
60
|
-
|
|
61
|
-
let reasoningContent = "";
|
|
62
|
-
let inputTokens;
|
|
63
|
-
let outputTokens;
|
|
64
|
-
// Anthropic streams one block at a time, so the open block's index decides
|
|
65
|
-
// which accumulator a delta belongs to.
|
|
66
|
-
const blocks = new Map();
|
|
67
|
-
for await (const event of providerStreamEvents(body)) {
|
|
68
|
-
const payload = providerStreamJson(event.data);
|
|
69
|
-
if (!isRecord(payload))
|
|
70
|
-
continue;
|
|
71
|
-
if (isRecord(payload.message) && isRecord(payload.message.usage))
|
|
72
|
-
inputTokens = payload.message.usage.input_tokens;
|
|
73
|
-
if (isRecord(payload.usage) && payload.usage.output_tokens !== undefined)
|
|
74
|
-
outputTokens = payload.usage.output_tokens;
|
|
75
|
-
const index = typeof payload.index === "number" ? payload.index : -1;
|
|
76
|
-
if (payload.type === "content_block_start") {
|
|
77
|
-
const block = isRecord(payload.content_block)
|
|
78
|
-
? payload.content_block
|
|
79
|
-
: {};
|
|
80
|
-
if (block.type === "tool_use")
|
|
81
|
-
blocks.set(index, {
|
|
82
|
-
id: typeof block.id === "string" ? block.id : "",
|
|
83
|
-
name: typeof block.name === "string" ? block.name : "",
|
|
84
|
-
arguments: "",
|
|
85
|
-
});
|
|
86
|
-
continue;
|
|
87
|
-
}
|
|
88
|
-
if (payload.type !== "content_block_delta" || !isRecord(payload.delta))
|
|
89
|
-
continue;
|
|
90
|
-
const { delta } = payload;
|
|
91
|
-
if (typeof delta.text === "string" && delta.text !== "") {
|
|
92
|
-
content += delta.text;
|
|
93
|
-
onDelta?.({ kind: "text", text: delta.text });
|
|
94
|
-
}
|
|
95
|
-
if (typeof delta.thinking === "string" && delta.thinking !== "") {
|
|
96
|
-
reasoningContent += delta.thinking;
|
|
97
|
-
onDelta?.({ kind: "reasoning", text: delta.thinking });
|
|
98
|
-
}
|
|
99
|
-
if (typeof delta.partial_json === "string" && delta.partial_json !== "") {
|
|
100
|
-
const current = blocks.get(index);
|
|
101
|
-
if (current !== undefined) {
|
|
102
|
-
current.arguments += delta.partial_json;
|
|
103
|
-
onDelta?.({ kind: "tool", text: delta.partial_json });
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
}
|
|
6
|
+
const result = await aggregateAnthropicModelStream(body, onDelta);
|
|
107
7
|
return {
|
|
108
|
-
content,
|
|
109
|
-
reasoningContent,
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
8
|
+
content: result.text,
|
|
9
|
+
reasoningContent: result.reasoningContent,
|
|
10
|
+
providerContent: result.content,
|
|
11
|
+
toolCalls: result.toolCalls,
|
|
12
|
+
usage: result.usage,
|
|
13
|
+
...(result.responseId === undefined
|
|
14
|
+
? {}
|
|
15
|
+
: { responseId: result.responseId }),
|
|
16
|
+
...(result.model === undefined ? {} : { model: result.model }),
|
|
17
|
+
...(result.stopReason === undefined
|
|
18
|
+
? {}
|
|
19
|
+
: { stopReason: result.stopReason }),
|
|
114
20
|
};
|
|
115
21
|
}
|
|
116
|
-
function isRecord(value) {
|
|
117
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
118
|
-
}
|