extrait 0.7.3 → 0.7.4

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.
@@ -7,4 +7,15 @@ export declare function aggregateUsage<T extends {
7
7
  usage?: LLMUsage;
8
8
  }>(attempts: T[]): LLMUsage | undefined;
9
9
  export declare function mergeUsage(base: LLMUsage | undefined, next: LLMUsage | undefined): LLMUsage | undefined;
10
+ /**
11
+ * Drops a trailing run of `value` that could be the start of a `<think>` /
12
+ * `</think>` tag whose remaining characters have not streamed in yet (e.g. a
13
+ * chunk ending in `<th`). Streaming consumers diff successive snapshots to emit
14
+ * deltas; without this, a partial tag is emitted as a delta and then can never
15
+ * be retracted once it resolves into a real tag that sanitization removes.
16
+ *
17
+ * Only safe for incremental streaming snapshots — never apply it to a final
18
+ * result, where a legitimate trailing `<` must survive.
19
+ */
20
+ export declare function withoutTrailingThinkTagPrefix(value: string): string;
10
21
  export declare function toStreamDataFingerprint(value: unknown): string;
package/dist/index.cjs CHANGED
@@ -40,6 +40,7 @@ var __export = (target, all) => {
40
40
  var exports_src = {};
41
41
  __export(exports_src, {
42
42
  wrapMCPClient: () => wrapMCPClient,
43
+ withoutTrailingThinkTagPrefix: () => withoutTrailingThinkTagPrefix,
43
44
  withFormat: () => withFormat,
44
45
  structured: () => structured,
45
46
  sanitizeThink: () => sanitizeThink,
@@ -49,6 +50,7 @@ __export(exports_src, {
49
50
  registerBuiltinProviders: () => registerBuiltinProviders,
50
51
  prompt: () => prompt,
51
52
  parseLLMOutput: () => parseLLMOutput,
53
+ normalizeModelOutput: () => normalizeModelOutput,
52
54
  inspectSchemaMetadata: () => inspectSchemaMetadata,
53
55
  inferSchemaExample: () => inferSchemaExample,
54
56
  images: () => images,
@@ -1168,6 +1170,110 @@ function countHiddenChars(value) {
1168
1170
  function maskKeepingLineBreaks(value) {
1169
1171
  return value.replace(RE_NON_LINE_BREAK, " ");
1170
1172
  }
1173
+ // src/generate-output.ts
1174
+ var RE_THINK_TAGS = /<\/?think\s*>/gi;
1175
+ function normalizeModelOutput(text, dedicatedReasoning, reasoningBlocks) {
1176
+ const sanitized = sanitizeThink(text);
1177
+ const visibleText = stripThinkBlocks(text, sanitized.thinkBlocks);
1178
+ const reasoning = joinReasoningSegments([
1179
+ sanitizeReasoningText(dedicatedReasoning),
1180
+ ...sanitized.thinkBlocks.map((block) => block.content)
1181
+ ]);
1182
+ return {
1183
+ text: visibleText,
1184
+ reasoning,
1185
+ reasoningBlocks: normalizeReasoningBlocks(reasoningBlocks),
1186
+ thinkBlocks: sanitized.thinkBlocks,
1187
+ parseSource: composeParseSource(visibleText, reasoning)
1188
+ };
1189
+ }
1190
+ function normalizeReasoningBlocks(blocks) {
1191
+ if (!Array.isArray(blocks)) {
1192
+ return;
1193
+ }
1194
+ const normalized = blocks.map((block) => ({
1195
+ turnIndex: block.turnIndex,
1196
+ text: block.text.replace(RE_THINK_TAGS, "").trim()
1197
+ })).filter((block) => Number.isFinite(block.turnIndex) && block.text.length > 0);
1198
+ return normalized.length > 0 ? normalized : undefined;
1199
+ }
1200
+ function appendReasoningBlock(blocks, transition) {
1201
+ const text = transition.reasoningText?.replace(RE_THINK_TAGS, "").trim();
1202
+ if (!text) {
1203
+ return blocks;
1204
+ }
1205
+ const next = [...blocks ?? [], { turnIndex: transition.turnIndex, text }];
1206
+ return normalizeReasoningBlocks(next);
1207
+ }
1208
+ function composeParseSource(text, reasoning) {
1209
+ if (typeof reasoning !== "string" || reasoning.length === 0) {
1210
+ return text;
1211
+ }
1212
+ const sanitized = reasoning.replace(RE_THINK_TAGS, "");
1213
+ if (sanitized.length === 0) {
1214
+ return text;
1215
+ }
1216
+ return `<think>${sanitized}</think>${text}`;
1217
+ }
1218
+ function aggregateUsage(attempts) {
1219
+ let usage;
1220
+ for (const attempt of attempts) {
1221
+ usage = mergeUsage(usage, attempt.usage);
1222
+ }
1223
+ return usage;
1224
+ }
1225
+ function mergeUsage(base, next) {
1226
+ if (!base && !next) {
1227
+ return;
1228
+ }
1229
+ return {
1230
+ inputTokens: (base?.inputTokens ?? 0) + (next?.inputTokens ?? 0),
1231
+ outputTokens: (base?.outputTokens ?? 0) + (next?.outputTokens ?? 0),
1232
+ totalTokens: (base?.totalTokens ?? 0) + (next?.totalTokens ?? 0),
1233
+ cost: (base?.cost ?? 0) + (next?.cost ?? 0)
1234
+ };
1235
+ }
1236
+ function joinReasoningSegments(parts) {
1237
+ return parts.map((value) => value?.trim()).filter((value) => Boolean(value)).join(`
1238
+
1239
+ `);
1240
+ }
1241
+ function sanitizeReasoningText(value) {
1242
+ const sanitized = value?.replace(RE_THINK_TAGS, "").trim();
1243
+ return sanitized ? sanitized : undefined;
1244
+ }
1245
+ var THINK_TAG_VARIANTS = ["<think>", "</think>"];
1246
+ var MAX_THINK_TAG_PREFIX = Math.max(...THINK_TAG_VARIANTS.map((tag) => tag.length)) - 1;
1247
+ function withoutTrailingThinkTagPrefix(value) {
1248
+ const max = Math.min(value.length, MAX_THINK_TAG_PREFIX);
1249
+ for (let length = max;length > 0; length -= 1) {
1250
+ const suffix = value.slice(value.length - length);
1251
+ if (THINK_TAG_VARIANTS.some((tag) => tag.length > suffix.length && tag.startsWith(suffix))) {
1252
+ return value.slice(0, value.length - length);
1253
+ }
1254
+ }
1255
+ return value;
1256
+ }
1257
+ function stripThinkBlocks(text, thinkBlocks) {
1258
+ if (thinkBlocks.length === 0) {
1259
+ return text;
1260
+ }
1261
+ let output = "";
1262
+ let cursor = 0;
1263
+ for (const block of thinkBlocks) {
1264
+ output += text.slice(cursor, block.start);
1265
+ cursor = block.end;
1266
+ }
1267
+ output += text.slice(cursor);
1268
+ return output;
1269
+ }
1270
+ function toStreamDataFingerprint(value) {
1271
+ try {
1272
+ return JSON.stringify(value);
1273
+ } catch {
1274
+ return "__unserializable__";
1275
+ }
1276
+ }
1171
1277
  // src/providers/stream-utils.ts
1172
1278
  var RE_LINE_ENDING = /\r?\n/;
1173
1279
  async function consumeSSE(response, onEvent) {
@@ -1597,7 +1703,7 @@ function pickString(value) {
1597
1703
  function toFiniteNumber(value) {
1598
1704
  return typeof value === "number" && Number.isFinite(value) ? value : undefined;
1599
1705
  }
1600
- function mergeUsage(base, next) {
1706
+ function mergeUsage2(base, next) {
1601
1707
  if (!base && !next) {
1602
1708
  return;
1603
1709
  }
@@ -1722,6 +1828,8 @@ async function streamWithChatCompletionsPassThrough(options, fetcher, path, requ
1722
1828
  let reasoning = "";
1723
1829
  let usage;
1724
1830
  let finishReason;
1831
+ const streamedToolCalls = new Map;
1832
+ const nativeToolCalls = new NativeToolCallStreamState(requestDeclaresTools(options, request));
1725
1833
  await consumeSSE(response, (data) => {
1726
1834
  if (data === "[DONE]") {
1727
1835
  return;
@@ -1730,10 +1838,14 @@ async function streamWithChatCompletionsPassThrough(options, fetcher, path, requ
1730
1838
  if (!isRecord2(json)) {
1731
1839
  return;
1732
1840
  }
1733
- const delta = pickAssistantDelta(json);
1841
+ const rawDelta = pickAssistantDelta(json);
1734
1842
  const reasoningDelta = pickAssistantReasoningDelta(json);
1735
1843
  const chunkUsage = pickUsage(json);
1736
1844
  const chunkFinishReason = pickFinishReason(json);
1845
+ collectOpenAIStreamToolCalls(json, streamedToolCalls);
1846
+ const nativeDelta = nativeToolCalls.push(rawDelta);
1847
+ const delta = nativeDelta.textDelta;
1848
+ const chunkToolCalls = mergeToolCalls(buildOpenAIStreamToolCalls(streamedToolCalls), nativeDelta.toolCalls);
1737
1849
  usage = preferLatestUsage(usage, chunkUsage);
1738
1850
  if (chunkFinishReason) {
1739
1851
  finishReason = chunkFinishReason;
@@ -1745,13 +1857,21 @@ async function streamWithChatCompletionsPassThrough(options, fetcher, path, requ
1745
1857
  if (reasoningDelta) {
1746
1858
  reasoning += reasoningDelta;
1747
1859
  }
1748
- emitOpenAIStreamChunk(callbacks, undefined, json, delta, reasoningDelta, chunkUsage, chunkFinishReason);
1860
+ emitOpenAIStreamChunk(callbacks, undefined, json, delta, reasoningDelta, chunkUsage, chunkFinishReason, chunkToolCalls.length > 0 ? chunkToolCalls : undefined);
1749
1861
  });
1862
+ const tail = nativeToolCalls.flush();
1863
+ if (tail.textDelta) {
1864
+ text += tail.textDelta;
1865
+ callbacks.onToken?.(tail.textDelta);
1866
+ emitOpenAIStreamChunk(callbacks, undefined, {}, tail.textDelta, "", undefined, undefined);
1867
+ }
1868
+ const toolCalls = mergeToolCalls(buildOpenAIStreamToolCalls(streamedToolCalls), nativeToolCalls.calls);
1750
1869
  const out = {
1751
1870
  text,
1752
1871
  reasoning: reasoning.length > 0 ? reasoning : undefined,
1753
1872
  usage,
1754
- finishReason
1873
+ finishReason: finishReason ?? (toolCalls.length > 0 ? "tool_calls" : undefined),
1874
+ toolCalls: toolCalls.length > 0 ? toolCalls : undefined
1755
1875
  };
1756
1876
  callbacks.onComplete?.(out);
1757
1877
  return out;
@@ -1861,15 +1981,16 @@ function buildResponsesMCPResult(state, text, raw) {
1861
1981
  toolExecutions: state.toolExecutions.length > 0 ? state.toolExecutions : undefined
1862
1982
  };
1863
1983
  }
1864
- function emitOpenAIStreamChunk(callbacks, round, raw, delta, reasoningDelta, usage, finishReason) {
1865
- if (delta || reasoningDelta || usage || finishReason) {
1984
+ function emitOpenAIStreamChunk(callbacks, round, raw, delta, reasoningDelta, usage, finishReason, toolCalls) {
1985
+ if (delta || reasoningDelta || usage || finishReason || toolCalls) {
1866
1986
  callbacks.onChunk?.({
1867
1987
  textDelta: delta,
1868
1988
  reasoningDelta: reasoningDelta || undefined,
1869
1989
  ...round !== undefined ? { turnIndex: round } : {},
1870
1990
  raw,
1871
1991
  usage,
1872
- finishReason
1992
+ finishReason,
1993
+ toolCalls
1873
1994
  });
1874
1995
  }
1875
1996
  }
@@ -1936,7 +2057,7 @@ async function completeWithChatCompletionsWithMCP(options, fetcher, path, reques
1936
2057
  parallel_tool_calls: request.parallelToolCalls
1937
2058
  }));
1938
2059
  lastPayload = payload;
1939
- aggregatedUsage = mergeUsage(aggregatedUsage, pickUsage(payload));
2060
+ aggregatedUsage = mergeUsage2(aggregatedUsage, pickUsage(payload));
1940
2061
  finishReason = pickFinishReason(payload);
1941
2062
  const assistantMessage = pickAssistantMessage(payload);
1942
2063
  const calledTools = pickChatToolCalls(payload);
@@ -2016,7 +2137,7 @@ async function completeWithResponsesAPIWithMCP(options, fetcher, path, request)
2016
2137
  parallel_tool_calls: request.parallelToolCalls
2017
2138
  }));
2018
2139
  state.lastPayload = payload;
2019
- state.aggregatedUsage = mergeUsage(state.aggregatedUsage, pickUsage(payload));
2140
+ state.aggregatedUsage = mergeUsage2(state.aggregatedUsage, pickUsage(payload));
2020
2141
  state.finishReason = pickResponsesFinishReason(payload) ?? state.finishReason;
2021
2142
  pushReasoningBlock(state.reasoningBlocks, round, pickResponsesReasoning(payload));
2022
2143
  const providerToolCalls = pickResponsesToolCalls(payload);
@@ -2076,6 +2197,7 @@ async function streamWithChatCompletionsWithMCP(options, fetcher, path, request,
2076
2197
  let roundUsage;
2077
2198
  let roundFinishReason;
2078
2199
  const streamedToolCalls = new Map;
2200
+ const nativeToolCalls = new NativeToolCallStreamState;
2079
2201
  let reasoningFieldName;
2080
2202
  await consumeSSE(response, (data) => {
2081
2203
  if (data === "[DONE]") {
@@ -2086,10 +2208,12 @@ async function streamWithChatCompletionsWithMCP(options, fetcher, path, request,
2086
2208
  return;
2087
2209
  }
2088
2210
  lastPayload = json;
2089
- const delta = pickAssistantDelta(json);
2211
+ const rawDelta = pickAssistantDelta(json);
2090
2212
  const reasoningDelta = pickAssistantReasoningDelta(json);
2091
2213
  const chunkUsage = pickUsage(json);
2092
2214
  const chunkFinishReason = pickFinishReason(json);
2215
+ const nativeDelta = nativeToolCalls.push(rawDelta);
2216
+ const delta = nativeDelta.textDelta;
2093
2217
  collectOpenAIStreamToolCalls(json, streamedToolCalls);
2094
2218
  roundUsage = preferLatestUsage(roundUsage, chunkUsage);
2095
2219
  if (chunkFinishReason) {
@@ -2103,13 +2227,20 @@ async function streamWithChatCompletionsWithMCP(options, fetcher, path, request,
2103
2227
  roundReasoning += reasoningDelta;
2104
2228
  reasoningFieldName ??= pickAssistantReasoningDeltaFieldName(json);
2105
2229
  }
2106
- emitOpenAIStreamChunk(callbacks, round, json, delta, reasoningDelta, chunkUsage, chunkFinishReason);
2230
+ const chunkToolCalls = nativeDelta.toolCalls.length > 0 ? mergeToolCalls(buildOpenAIStreamToolCalls(streamedToolCalls), nativeDelta.toolCalls) : undefined;
2231
+ emitOpenAIStreamChunk(callbacks, round, json, delta, reasoningDelta, chunkUsage, chunkFinishReason, chunkToolCalls);
2107
2232
  });
2108
- aggregatedUsage = mergeUsage(aggregatedUsage, roundUsage);
2233
+ const tail = nativeToolCalls.flush();
2234
+ if (tail.textDelta) {
2235
+ roundText += tail.textDelta;
2236
+ callbacks.onToken?.(tail.textDelta);
2237
+ emitOpenAIStreamChunk(callbacks, round, {}, tail.textDelta, "", undefined, undefined);
2238
+ }
2239
+ aggregatedUsage = mergeUsage2(aggregatedUsage, roundUsage);
2109
2240
  if (roundFinishReason) {
2110
2241
  finishReason = roundFinishReason;
2111
2242
  }
2112
- const calledTools = buildOpenAIStreamToolCalls(streamedToolCalls);
2243
+ const calledTools = mergeToolCalls(buildOpenAIStreamToolCalls(streamedToolCalls), nativeToolCalls.calls);
2113
2244
  pushReasoningBlock(reasoningBlocks, round, roundReasoning);
2114
2245
  request.onTurnTransition?.({
2115
2246
  turnIndex: round,
@@ -2196,6 +2327,7 @@ async function streamWithResponsesAPIPassThrough(options, fetcher, path, request
2196
2327
  let usage;
2197
2328
  let finishReason;
2198
2329
  let lastPayload;
2330
+ const streamedToolCalls = new Map;
2199
2331
  await consumeSSE(response, (data) => {
2200
2332
  if (data === "[DONE]") {
2201
2333
  return;
@@ -2211,6 +2343,8 @@ async function streamWithResponsesAPIPassThrough(options, fetcher, path, request
2211
2343
  const delta = pickResponsesStreamTextDelta(json);
2212
2344
  const chunkUsage = pickResponsesStreamUsage(json);
2213
2345
  const chunkFinishReason = pickResponsesStreamFinishReason(json);
2346
+ collectResponsesStreamToolCalls(json, streamedToolCalls);
2347
+ const chunkToolCalls = buildResponsesStreamToolCalls(streamedToolCalls);
2214
2348
  usage = preferLatestUsage(usage, chunkUsage);
2215
2349
  if (chunkFinishReason) {
2216
2350
  finishReason = chunkFinishReason;
@@ -2219,14 +2353,16 @@ async function streamWithResponsesAPIPassThrough(options, fetcher, path, request
2219
2353
  text += delta;
2220
2354
  callbacks.onToken?.(delta);
2221
2355
  }
2222
- emitOpenAIStreamChunk(callbacks, undefined, json, delta, "", chunkUsage, chunkFinishReason);
2356
+ emitOpenAIStreamChunk(callbacks, undefined, json, delta, "", chunkUsage, chunkFinishReason, chunkToolCalls.length > 0 ? chunkToolCalls : undefined);
2223
2357
  });
2224
2358
  const finalPayload = lastPayload ?? {};
2359
+ const toolCalls = buildResponsesStreamToolCalls(streamedToolCalls);
2225
2360
  const out = {
2226
2361
  text: text.length > 0 ? text : pickResponsesText(finalPayload) || pickAssistantText(finalPayload),
2227
2362
  raw: finalPayload,
2228
2363
  usage: preferLatestUsage(usage, pickUsage(finalPayload)),
2229
- finishReason: finishReason ?? pickResponsesFinishReason(finalPayload) ?? pickFinishReason(finalPayload)
2364
+ finishReason: finishReason ?? pickResponsesFinishReason(finalPayload) ?? pickFinishReason(finalPayload),
2365
+ toolCalls: toolCalls.length > 0 ? toolCalls : undefined
2230
2366
  };
2231
2367
  callbacks.onComplete?.(out);
2232
2368
  return out;
@@ -2288,7 +2424,7 @@ async function streamWithResponsesAPIWithMCP(options, fetcher, path, request, ca
2288
2424
  emitOpenAIStreamChunk(callbacks, round, json, delta, reasoningDelta, chunkUsage, chunkFinishReason);
2289
2425
  });
2290
2426
  const resolvedRoundUsage = preferLatestUsage(roundUsage, roundPayload ? pickUsage(roundPayload) : undefined);
2291
- state.aggregatedUsage = mergeUsage(state.aggregatedUsage, resolvedRoundUsage);
2427
+ state.aggregatedUsage = mergeUsage2(state.aggregatedUsage, resolvedRoundUsage);
2292
2428
  if (roundFinishReason) {
2293
2429
  state.finishReason = roundFinishReason;
2294
2430
  } else if (roundPayload) {
@@ -2542,43 +2678,204 @@ function pickAssistantReasoningDeltaFieldName(payload) {
2542
2678
  }
2543
2679
  return;
2544
2680
  }
2545
- function collectOpenAIStreamToolCalls(payload, state) {
2546
- const choices = payload.choices;
2547
- if (!Array.isArray(choices) || choices.length === 0 || !isRecord2(choices[0])) {
2681
+ var NATIVE_TOOL_CALL_OPEN = "<tool_call";
2682
+ var NATIVE_TOOL_CALL_CLOSE = "</tool_call>";
2683
+
2684
+ class NativeToolCallStreamState {
2685
+ enabled;
2686
+ calls = [];
2687
+ pending = "";
2688
+ constructor(enabled = true) {
2689
+ this.enabled = enabled;
2690
+ }
2691
+ push(delta) {
2692
+ if (!delta || !this.enabled) {
2693
+ return { textDelta: delta, toolCalls: [] };
2694
+ }
2695
+ this.pending += delta;
2696
+ return this.drain(false);
2697
+ }
2698
+ flush() {
2699
+ return this.enabled ? this.drain(true) : { textDelta: "", toolCalls: [] };
2700
+ }
2701
+ drain(flush) {
2702
+ let textDelta = "";
2703
+ const toolCalls = [];
2704
+ while (this.pending.length > 0) {
2705
+ const openIndex = this.pending.indexOf(NATIVE_TOOL_CALL_OPEN);
2706
+ if (openIndex < 0) {
2707
+ const keep = flush ? 0 : nativeToolCallPrefixSuffixLength(this.pending);
2708
+ const emitLength = this.pending.length - keep;
2709
+ if (emitLength > 0) {
2710
+ textDelta += this.pending.slice(0, emitLength);
2711
+ this.pending = this.pending.slice(emitLength);
2712
+ }
2713
+ break;
2714
+ }
2715
+ if (openIndex > 0) {
2716
+ textDelta += this.pending.slice(0, openIndex);
2717
+ this.pending = this.pending.slice(openIndex);
2718
+ continue;
2719
+ }
2720
+ const closeIndex = this.pending.indexOf(NATIVE_TOOL_CALL_CLOSE);
2721
+ if (closeIndex < 0) {
2722
+ if (flush) {
2723
+ this.pending = "";
2724
+ }
2725
+ break;
2726
+ }
2727
+ const blockEnd = closeIndex + NATIVE_TOOL_CALL_CLOSE.length;
2728
+ const call = parseNativeToolCallBlock(this.pending.slice(0, blockEnd), this.calls.length);
2729
+ if (call) {
2730
+ this.calls.push(call);
2731
+ toolCalls.push(call);
2732
+ }
2733
+ this.pending = this.pending.slice(blockEnd);
2734
+ }
2735
+ return { textDelta, toolCalls };
2736
+ }
2737
+ }
2738
+ function requestDeclaresTools(options, request) {
2739
+ const hasTools = (body) => Array.isArray(body?.tools) && body.tools.length > 0;
2740
+ return hasTools(request.body) || hasTools(options.defaultBody);
2741
+ }
2742
+ var NATIVE_FUNCTION_PATTERN = /<function=([^>\s]+)\s*>([\s\S]*?)<\/function>/;
2743
+ var NATIVE_PARAMETER_PATTERN = /<parameter=([^>\s]+)\s*>([\s\S]*?)<\/parameter>/g;
2744
+ function parseNativeToolCallBlock(block, index) {
2745
+ const inner = extractNativeToolCallInner(block);
2746
+ if (inner === undefined) {
2548
2747
  return;
2549
2748
  }
2550
- const delta = choices[0].delta;
2551
- if (!isRecord2(delta) || !Array.isArray(delta.tool_calls)) {
2749
+ return parseNativeJsonToolCall(inner, index) ?? parseNativeXmlToolCall(inner, index);
2750
+ }
2751
+ function extractNativeToolCallInner(block) {
2752
+ const openEnd = block.indexOf(">");
2753
+ const closeStart = block.lastIndexOf(NATIVE_TOOL_CALL_CLOSE);
2754
+ if (openEnd < 0 || closeStart < 0 || closeStart <= openEnd) {
2552
2755
  return;
2553
2756
  }
2554
- for (const rawToolCall of delta.tool_calls) {
2555
- if (!isRecord2(rawToolCall)) {
2556
- continue;
2757
+ return block.slice(openEnd + 1, closeStart).trim();
2758
+ }
2759
+ function parseNativeXmlToolCall(inner, index) {
2760
+ const functionMatch = NATIVE_FUNCTION_PATTERN.exec(inner);
2761
+ const functionName = functionMatch?.[1];
2762
+ const functionBody = functionMatch?.[2];
2763
+ if (!functionName || functionBody === undefined) {
2764
+ return;
2765
+ }
2766
+ const args = {};
2767
+ for (const [, key, rawValue] of functionBody.matchAll(NATIVE_PARAMETER_PATTERN)) {
2768
+ if (key && rawValue !== undefined) {
2769
+ args[key] = coerceNativeParameterValue(rawValue.trim());
2557
2770
  }
2558
- const index = toFiniteNumber(rawToolCall.index);
2559
- const toolIndex = index !== undefined ? Math.floor(index) : 0;
2560
- const existing = state.get(toolIndex) ?? {
2561
- index: toolIndex,
2562
- argumentsText: ""
2563
- };
2564
- const id = pickString(rawToolCall.id);
2565
- if (id) {
2566
- existing.id = id;
2771
+ }
2772
+ return {
2773
+ id: `call_native_${index}`,
2774
+ type: "function",
2775
+ name: functionName,
2776
+ arguments: JSON.stringify(args)
2777
+ };
2778
+ }
2779
+ function coerceNativeParameterValue(value) {
2780
+ if (value.length === 0) {
2781
+ return "";
2782
+ }
2783
+ const parsed = safeJSONParse(value);
2784
+ if (parsed === null) {
2785
+ return value === "null" ? null : value;
2786
+ }
2787
+ return parsed;
2788
+ }
2789
+ function nativeToolCallPrefixSuffixLength(value) {
2790
+ const max = Math.min(value.length, NATIVE_TOOL_CALL_OPEN.length - 1);
2791
+ for (let length = max;length > 0; length -= 1) {
2792
+ if (NATIVE_TOOL_CALL_OPEN.startsWith(value.slice(-length))) {
2793
+ return length;
2794
+ }
2795
+ }
2796
+ return 0;
2797
+ }
2798
+ function parseNativeJsonToolCall(inner, index) {
2799
+ const parsed = safeJSONParse(inner);
2800
+ if (!isRecord2(parsed)) {
2801
+ return;
2802
+ }
2803
+ const name = pickString(parsed.name) ?? pickString(parsed.function);
2804
+ if (!name) {
2805
+ return;
2806
+ }
2807
+ const rawArguments = parsed.arguments ?? parsed.parameters ?? {};
2808
+ const args = typeof rawArguments === "string" ? rawArguments : JSON.stringify(rawArguments);
2809
+ return {
2810
+ id: pickString(parsed.id) ?? `call_native_${index}`,
2811
+ type: "function",
2812
+ name,
2813
+ arguments: args
2814
+ };
2815
+ }
2816
+ function mergeToolCalls(...groups) {
2817
+ const merged = [];
2818
+ const seen = new Set;
2819
+ for (const group of groups) {
2820
+ for (const call of group) {
2821
+ const key = call.id || `${call.name ?? ""}:${String(call.arguments ?? "")}`;
2822
+ if (seen.has(key)) {
2823
+ continue;
2824
+ }
2825
+ seen.add(key);
2826
+ merged.push(call);
2567
2827
  }
2568
- const type = pickString(rawToolCall.type);
2569
- if (type) {
2570
- existing.type = type;
2828
+ }
2829
+ return merged;
2830
+ }
2831
+ function collectOpenAIStreamToolCalls(payload, state) {
2832
+ const choices = payload.choices;
2833
+ if (!Array.isArray(choices) || choices.length === 0) {
2834
+ return;
2835
+ }
2836
+ for (const choice of choices) {
2837
+ if (!isRecord2(choice)) {
2838
+ continue;
2571
2839
  }
2572
- const functionCall = isRecord2(rawToolCall.function) ? rawToolCall.function : undefined;
2573
- const name = pickString(functionCall?.name);
2574
- if (name) {
2575
- existing.name = `${existing.name ?? ""}${name}`;
2840
+ const delta = isRecord2(choice.delta) ? choice.delta : undefined;
2841
+ const message = isRecord2(choice.message) ? choice.message : undefined;
2842
+ const toolCalls = Array.isArray(delta?.tool_calls) ? delta.tool_calls : Array.isArray(message?.tool_calls) ? message.tool_calls : Array.isArray(choice.tool_calls) ? choice.tool_calls : undefined;
2843
+ if (!toolCalls) {
2844
+ continue;
2576
2845
  }
2577
- const argumentsDelta = pickString(functionCall?.arguments);
2578
- if (argumentsDelta) {
2579
- existing.argumentsText += argumentsDelta;
2846
+ for (const rawToolCall of toolCalls) {
2847
+ if (!isRecord2(rawToolCall)) {
2848
+ continue;
2849
+ }
2850
+ const index = toFiniteNumber(rawToolCall.index);
2851
+ const toolIndex = index !== undefined ? Math.floor(index) : state.size;
2852
+ const existing = state.get(toolIndex) ?? {
2853
+ index: toolIndex,
2854
+ argumentsText: ""
2855
+ };
2856
+ const id = pickString(rawToolCall.id);
2857
+ if (id) {
2858
+ existing.id = id;
2859
+ }
2860
+ const type = pickString(rawToolCall.type);
2861
+ if (type) {
2862
+ existing.type = type;
2863
+ }
2864
+ const functionCall = isRecord2(rawToolCall.function) ? rawToolCall.function : undefined;
2865
+ const name = pickString(functionCall?.name);
2866
+ if (name) {
2867
+ existing.name = `${existing.name ?? ""}${name}`;
2868
+ }
2869
+ const argumentsDelta = pickString(functionCall?.arguments);
2870
+ if (argumentsDelta) {
2871
+ if (message?.tool_calls === toolCalls || choice.tool_calls === toolCalls) {
2872
+ existing.argumentsText = argumentsDelta;
2873
+ } else {
2874
+ existing.argumentsText += argumentsDelta;
2875
+ }
2876
+ }
2877
+ state.set(toolIndex, existing);
2580
2878
  }
2581
- state.set(toolIndex, existing);
2582
2879
  }
2583
2880
  }
2584
2881
  function buildOpenAIStreamToolCalls(state) {
@@ -3026,7 +3323,7 @@ async function completeWithMCPToolLoop(options, fetcher, path, request) {
3026
3323
  }
3027
3324
  const payload = await response.json();
3028
3325
  lastPayload = payload;
3029
- aggregatedUsage = mergeUsage(aggregatedUsage, pickUsage2(payload));
3326
+ aggregatedUsage = mergeUsage2(aggregatedUsage, pickUsage2(payload));
3030
3327
  finishReason = pickFinishReason2(payload);
3031
3328
  const content = Array.isArray(payload.content) ? payload.content : [];
3032
3329
  const calledTools = pickAnthropicToolCalls(payload).filter((call) => call.type === "function");
@@ -3147,7 +3444,7 @@ async function streamWithMCPToolLoop(options, fetcher, path, request, callbacks)
3147
3444
  callbacks.onChunk?.(chunk);
3148
3445
  }
3149
3446
  });
3150
- aggregatedUsage = mergeUsage(aggregatedUsage, roundUsage);
3447
+ aggregatedUsage = mergeUsage2(aggregatedUsage, roundUsage);
3151
3448
  if (roundFinishReason) {
3152
3449
  finishReason = roundFinishReason;
3153
3450
  }
@@ -3806,95 +4103,6 @@ function withToolTimeout(client, toolTimeoutMs) {
3806
4103
  function applyToolTimeout(clients, toolTimeoutMs) {
3807
4104
  return clients.map((client) => withToolTimeout(client, toolTimeoutMs));
3808
4105
  }
3809
- // src/generate-output.ts
3810
- var RE_THINK_TAGS = /<\/?think\s*>/gi;
3811
- function normalizeModelOutput(text, dedicatedReasoning, reasoningBlocks) {
3812
- const sanitized = sanitizeThink(text);
3813
- const visibleText = stripThinkBlocks(text, sanitized.thinkBlocks);
3814
- const reasoning = joinReasoningSegments([
3815
- dedicatedReasoning,
3816
- ...sanitized.thinkBlocks.map((block) => block.content)
3817
- ]);
3818
- return {
3819
- text: visibleText,
3820
- reasoning,
3821
- reasoningBlocks: normalizeReasoningBlocks(reasoningBlocks),
3822
- thinkBlocks: sanitized.thinkBlocks,
3823
- parseSource: composeParseSource(visibleText, reasoning)
3824
- };
3825
- }
3826
- function normalizeReasoningBlocks(blocks) {
3827
- if (!Array.isArray(blocks)) {
3828
- return;
3829
- }
3830
- const normalized = blocks.map((block) => ({
3831
- turnIndex: block.turnIndex,
3832
- text: block.text.replace(RE_THINK_TAGS, "").trim()
3833
- })).filter((block) => Number.isFinite(block.turnIndex) && block.text.length > 0);
3834
- return normalized.length > 0 ? normalized : undefined;
3835
- }
3836
- function appendReasoningBlock(blocks, transition) {
3837
- const text = transition.reasoningText?.replace(RE_THINK_TAGS, "").trim();
3838
- if (!text) {
3839
- return blocks;
3840
- }
3841
- const next = [...blocks ?? [], { turnIndex: transition.turnIndex, text }];
3842
- return normalizeReasoningBlocks(next);
3843
- }
3844
- function composeParseSource(text, reasoning) {
3845
- if (typeof reasoning !== "string" || reasoning.length === 0) {
3846
- return text;
3847
- }
3848
- const sanitized = reasoning.replace(RE_THINK_TAGS, "");
3849
- if (sanitized.length === 0) {
3850
- return text;
3851
- }
3852
- return `<think>${sanitized}</think>${text}`;
3853
- }
3854
- function aggregateUsage(attempts) {
3855
- let usage;
3856
- for (const attempt of attempts) {
3857
- usage = mergeUsage2(usage, attempt.usage);
3858
- }
3859
- return usage;
3860
- }
3861
- function mergeUsage2(base, next) {
3862
- if (!base && !next) {
3863
- return;
3864
- }
3865
- return {
3866
- inputTokens: (base?.inputTokens ?? 0) + (next?.inputTokens ?? 0),
3867
- outputTokens: (base?.outputTokens ?? 0) + (next?.outputTokens ?? 0),
3868
- totalTokens: (base?.totalTokens ?? 0) + (next?.totalTokens ?? 0),
3869
- cost: (base?.cost ?? 0) + (next?.cost ?? 0)
3870
- };
3871
- }
3872
- function joinReasoningSegments(parts) {
3873
- return parts.map((value) => value?.trim()).filter((value) => Boolean(value)).join(`
3874
-
3875
- `);
3876
- }
3877
- function stripThinkBlocks(text, thinkBlocks) {
3878
- if (thinkBlocks.length === 0) {
3879
- return text;
3880
- }
3881
- let output = "";
3882
- let cursor = 0;
3883
- for (const block of thinkBlocks) {
3884
- output += text.slice(cursor, block.start);
3885
- cursor = block.end;
3886
- }
3887
- output += text.slice(cursor);
3888
- return output;
3889
- }
3890
- function toStreamDataFingerprint(value) {
3891
- try {
3892
- return JSON.stringify(value);
3893
- } catch {
3894
- return "__unserializable__";
3895
- }
3896
- }
3897
-
3898
4106
  // src/utils/debug-colors.ts
3899
4107
  var ANSI = {
3900
4108
  reset: "\x1B[0m",
@@ -4056,13 +4264,15 @@ async function callModel(adapter, options) {
4056
4264
  if (!done && fingerprint === lastSnapshotFingerprint) {
4057
4265
  return;
4058
4266
  }
4267
+ const stableText = done ? normalized2.text : withoutTrailingThinkTagPrefix(normalized2.text);
4268
+ const stableReasoning = done ? normalized2.reasoning : withoutTrailingThinkTagPrefix(normalized2.reasoning);
4059
4269
  const delta = {
4060
- text: normalized2.text.startsWith(previousSnapshotText) ? normalized2.text.slice(previousSnapshotText.length) : "",
4061
- reasoning: normalized2.reasoning.startsWith(previousSnapshotReasoning) ? normalized2.reasoning.slice(previousSnapshotReasoning.length) : ""
4270
+ text: stableText.startsWith(previousSnapshotText) ? stableText.slice(previousSnapshotText.length) : "",
4271
+ reasoning: stableReasoning.startsWith(previousSnapshotReasoning) ? stableReasoning.slice(previousSnapshotReasoning.length) : ""
4062
4272
  };
4063
4273
  lastSnapshotFingerprint = fingerprint;
4064
- previousSnapshotText = normalized2.text;
4065
- previousSnapshotReasoning = normalized2.reasoning;
4274
+ previousSnapshotText = stableText;
4275
+ previousSnapshotReasoning = stableReasoning;
4066
4276
  options.stream.onData?.({
4067
4277
  delta,
4068
4278
  snapshot,