narraeon 0.1.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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";
@@ -31,6 +40,11 @@ export class ModelHostOutcomeUnknownError extends Error {
31
40
  }
32
41
  }
33
42
  /** Deterministic adapter for exercising the true external ModelHost port. */
43
+ /**
44
+ * Deterministic test double. Its synthetic provider state is fixture data, not
45
+ * a production continuation codec; real adapters must retain returned native
46
+ * state and never call scriptedProviderState.
47
+ */
34
48
  export class ScriptedModelHost {
35
49
  requests = [];
36
50
  #binding;
@@ -57,8 +71,8 @@ export class ScriptedModelHost {
57
71
  for (const delta of deltas ?? [])
58
72
  observer?.onDelta?.(structuredClone(delta));
59
73
  const providerState = response.providerState ??
60
- (continuation === "available"
61
- ? scriptedProviderState(this.#binding.provider, this.#responseIndex)
74
+ (continuation !== "lost"
75
+ ? scriptedProviderState(this.#binding.provider, this.#responseIndex, response)
62
76
  : undefined);
63
77
  this.#responseIndex += 1;
64
78
  return Promise.resolve({
@@ -69,21 +83,72 @@ export class ScriptedModelHost {
69
83
  });
70
84
  }
71
85
  }
72
- function scriptedProviderState(provider, responseIndex) {
73
- if (provider === "openai_responses")
86
+ function scriptedProviderState(provider, responseIndex, response) {
87
+ if (provider === "openai_responses") {
88
+ const output = [];
89
+ if (response.reasoningContent !== undefined)
90
+ output.push({
91
+ type: "reasoning",
92
+ summary: [{ type: "summary_text", text: response.reasoningContent }],
93
+ });
94
+ if (response.text !== undefined)
95
+ output.push({
96
+ type: "message",
97
+ role: "assistant",
98
+ content: [{ type: "output_text", text: response.text }],
99
+ });
100
+ for (const call of response.toolCalls ?? [])
101
+ output.push({
102
+ type: "function_call",
103
+ call_id: call.id,
104
+ name: call.name,
105
+ arguments: JSON.stringify(call.arguments),
106
+ });
74
107
  return {
75
108
  protocol: "openai_responses",
76
- output: [],
109
+ output,
77
110
  responseId: `scripted-${responseIndex}`,
78
111
  };
79
- if (provider === "anthropic_messages")
112
+ }
113
+ if (provider === "anthropic_messages") {
114
+ const content = [];
115
+ if (response.reasoningContent !== undefined)
116
+ content.push({ type: "thinking", thinking: response.reasoningContent });
117
+ if (response.text !== undefined)
118
+ content.push({ type: "text", text: response.text });
119
+ for (const call of response.toolCalls ?? [])
120
+ content.push({
121
+ type: "tool_use",
122
+ id: call.id,
123
+ name: call.name,
124
+ input: structuredClone(call.arguments),
125
+ });
80
126
  return {
81
127
  protocol: "anthropic_messages",
82
- content: [],
128
+ content,
83
129
  responseId: `scripted-${responseIndex}`,
84
130
  };
131
+ }
85
132
  return {
86
133
  protocol: "chat_completions",
87
- assistantMessage: { role: "assistant", content: null },
134
+ assistantMessage: {
135
+ role: "assistant",
136
+ content: response.text ?? null,
137
+ ...(response.reasoningContent === undefined
138
+ ? {}
139
+ : { reasoning_content: response.reasoningContent }),
140
+ ...((response.toolCalls ?? []).length === 0
141
+ ? {}
142
+ : {
143
+ tool_calls: response.toolCalls.map((call) => ({
144
+ id: call.id,
145
+ type: "function",
146
+ function: {
147
+ name: call.name,
148
+ arguments: JSON.stringify(call.arguments),
149
+ },
150
+ })),
151
+ }),
152
+ },
88
153
  };
89
154
  }
@@ -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 { providerStreamEvents, providerStreamJson } from "./ProviderStream.js";
2
- export async function aggregateChatSettingStream(body, onDelta) {
3
- let content = "";
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
- let content = "";
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
- toolCalls: [...blocks.entries()]
111
- .sort(([left], [right]) => left - right)
112
- .map(([, call]) => call),
113
- usage: { input_tokens: inputTokens, output_tokens: outputTokens },
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
- }