extrait 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.
package/README.md CHANGED
@@ -215,6 +215,10 @@ const result = await llm.structured(
215
215
  onToolExecution: (execution) => {
216
216
  console.log(execution.name, execution.durationMs);
217
217
  },
218
+ // Optional: transform tool output before it is sent back to the LLM
219
+ transformToolOutput: (output, execution) => {
220
+ return { ...output, source: execution.name };
221
+ },
218
222
  },
219
223
  }
220
224
  );
package/dist/index.cjs CHANGED
@@ -298,6 +298,9 @@ function isOnlyWhitespace(value) {
298
298
  }
299
299
 
300
300
  // src/extract.ts
301
+ var RE_EMPTY_OBJECT = /^\{\s*\}$/;
302
+ var RE_EMPTY_ARRAY = /^\[\s*\]$/;
303
+ var RE_BOUNDARY_CHAR = /[\s,.;:!?`"'()\[\]{}<>]/;
301
304
  var DEFAULT_EXTRACTION_HEURISTICS = {
302
305
  firstPassMin: 12,
303
306
  firstPassCap: 30,
@@ -522,7 +525,7 @@ function jsonShapeScore(content, acceptArrays) {
522
525
  const commaCount = countChar(trimmed, ",");
523
526
  const quoteCount = countChar(trimmed, '"');
524
527
  if (root === "{") {
525
- if (/^\{\s*\}$/.test(trimmed)) {
528
+ if (RE_EMPTY_OBJECT.test(trimmed)) {
526
529
  score += 12;
527
530
  } else if (colonCount > 0) {
528
531
  score += 22;
@@ -533,7 +536,7 @@ function jsonShapeScore(content, acceptArrays) {
533
536
  score += quoteCount % 2 === 0 ? 8 : -8;
534
537
  }
535
538
  } else {
536
- score += /^\[\s*\]$/.test(trimmed) ? 8 : 4;
539
+ score += RE_EMPTY_ARRAY.test(trimmed) ? 8 : 4;
537
540
  if (colonCount > 0) {
538
541
  score += 4;
539
542
  }
@@ -566,7 +569,7 @@ function isBoundary(char) {
566
569
  if (!char) {
567
570
  return true;
568
571
  }
569
- return /[\s,.;:!?`"'()[\]{}<>]/.test(char);
572
+ return RE_BOUNDARY_CHAR.test(char);
570
573
  }
571
574
  function lengthScore(length) {
572
575
  return Math.min(120, Math.floor(Math.sqrt(length) * 6));
@@ -755,6 +758,8 @@ function clamp(value, min, max) {
755
758
  return Math.max(min, Math.min(max, Math.floor(value)));
756
759
  }
757
760
  // src/schema.ts
761
+ var RE_SIMPLE_IDENTIFIER = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
762
+ var RE_WHITESPACE = /\s+/g;
758
763
  function formatZodSchemaLikeTypeScript(schema) {
759
764
  return formatType(schema, 0, new WeakSet);
760
765
  }
@@ -926,7 +931,7 @@ ${lines.join(`
926
931
  ${indent}}`;
927
932
  }
928
933
  function formatKey(key) {
929
- return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(key) ? key : JSON.stringify(key);
934
+ return RE_SIMPLE_IDENTIFIER.test(key) ? key : JSON.stringify(key);
930
935
  }
931
936
  function requiresParentheses(typeText) {
932
937
  return typeText.includes(" | ") || typeText.includes(" & ");
@@ -972,7 +977,7 @@ function readSchemaDescription(schema) {
972
977
  return;
973
978
  }
974
979
  function sanitizeDescription(value) {
975
- return value.replace(/\s+/g, " ").trim();
980
+ return value.replace(RE_WHITESPACE, " ").trim();
976
981
  }
977
982
 
978
983
  // src/format.ts
@@ -997,6 +1002,8 @@ function resolveSchemaInstruction(instruction) {
997
1002
  }
998
1003
  // src/think.ts
999
1004
  var THINK_TAG_NAME = "think";
1005
+ var RE_IDENTIFIER_CHAR = /[a-zA-Z0-9:_-]/;
1006
+ var RE_NON_LINE_BREAK = /[^\r\n]/g;
1000
1007
  function sanitizeThink(input) {
1001
1008
  const thinkBlocks = [];
1002
1009
  const diagnostics = {
@@ -1095,6 +1102,9 @@ function parseThinkTagAt(input, index) {
1095
1102
  if (input[cursor] === "/") {
1096
1103
  closing = true;
1097
1104
  cursor += 1;
1105
+ while (cursor < input.length && isWhitespace(input[cursor])) {
1106
+ cursor += 1;
1107
+ }
1098
1108
  }
1099
1109
  if (!matchesIgnoreCase(input, cursor, THINK_TAG_NAME)) {
1100
1110
  return null;
@@ -1151,7 +1161,7 @@ function matchesIgnoreCase(input, index, expected) {
1151
1161
  return input.slice(index, index + expected.length).toLowerCase() === expected;
1152
1162
  }
1153
1163
  function isIdentifierChar(char) {
1154
- return /[a-zA-Z0-9:_-]/.test(char);
1164
+ return RE_IDENTIFIER_CHAR.test(char);
1155
1165
  }
1156
1166
  function countHiddenChars(value) {
1157
1167
  let count = 0;
@@ -1165,9 +1175,10 @@ function countHiddenChars(value) {
1165
1175
  return count;
1166
1176
  }
1167
1177
  function maskKeepingLineBreaks(value) {
1168
- return value.replace(/[^\r\n]/g, " ");
1178
+ return value.replace(RE_NON_LINE_BREAK, " ");
1169
1179
  }
1170
1180
  // src/providers/stream-utils.ts
1181
+ var RE_LINE_ENDING = /\r?\n/;
1171
1182
  async function consumeSSE(response, onEvent) {
1172
1183
  if (!response.body) {
1173
1184
  return;
@@ -1190,7 +1201,7 @@ async function consumeSSE(response, onEvent) {
1190
1201
  buffer = buffer.slice(boundary + (buffer.startsWith(`\r
1191
1202
  \r
1192
1203
  `, boundary) ? 4 : 2));
1193
- const dataLines = rawEvent.split(/\r?\n/).filter((line) => line.startsWith("data:")).map((line) => line.slice(5).trim());
1204
+ const dataLines = rawEvent.split(RE_LINE_ENDING).filter((line) => line.startsWith("data:")).map((line) => line.slice(5).trim());
1194
1205
  if (dataLines.length === 0) {
1195
1206
  continue;
1196
1207
  }
@@ -1200,7 +1211,7 @@ async function consumeSSE(response, onEvent) {
1200
1211
  }
1201
1212
  const remainder = buffer.trim();
1202
1213
  if (remainder.length > 0) {
1203
- const dataLines = remainder.split(/\r?\n/).filter((line) => line.startsWith("data:")).map((line) => line.slice(5).trim());
1214
+ const dataLines = remainder.split(RE_LINE_ENDING).filter((line) => line.startsWith("data:")).map((line) => line.slice(5).trim());
1204
1215
  if (dataLines.length > 0) {
1205
1216
  onEvent(dataLines.join(`
1206
1217
  `));
@@ -1301,20 +1312,25 @@ async function executeMCPToolCalls(calls, toolset, context) {
1301
1312
  name: tool.remoteName,
1302
1313
  arguments: args
1303
1314
  });
1304
- metadata.output = output;
1305
- const execution = {
1315
+ const executionContext = {
1306
1316
  callId,
1307
- type: metadata.type,
1317
+ type: call.type ?? "function",
1308
1318
  name: toolName,
1309
1319
  clientId: tool.clientId,
1310
1320
  remoteName: tool.remoteName,
1311
1321
  arguments: parsedArguments,
1312
- output,
1313
1322
  round: context.round,
1314
1323
  provider: context.provider,
1315
1324
  model: context.model,
1316
1325
  handledLocally: true,
1317
1326
  startedAt,
1327
+ error: undefined
1328
+ };
1329
+ const transformedOutput = context.request.transformToolOutput ? await context.request.transformToolOutput(output, executionContext) : output;
1330
+ metadata.output = transformedOutput;
1331
+ const execution = {
1332
+ ...executionContext,
1333
+ output: transformedOutput,
1318
1334
  durationMs: Date.now() - startedAtMs
1319
1335
  };
1320
1336
  emitToolExecution(context.request, execution);
@@ -1485,13 +1501,18 @@ function describeTool(clientId, tool, hasCollision) {
1485
1501
  }
1486
1502
  return;
1487
1503
  }
1504
+ var RE_NON_ALPHANUMERIC = /[^A-Za-z0-9_]/g;
1505
+ var RE_MULTIPLE_UNDERSCORES = /_+/g;
1506
+ var RE_LEADING_UNDERSCORES = /^_+/;
1507
+ var RE_TRAILING_UNDERSCORES = /_+$/;
1508
+ var RE_STARTS_WITH_DIGIT = /^[0-9]/;
1488
1509
  function sanitizeToolName(input) {
1489
- const sanitized = input.replace(/[^A-Za-z0-9_]/g, "_").replace(/_+/g, "_");
1490
- const trimmed = sanitized.replace(/^_+/, "").replace(/_+$/, "");
1510
+ const sanitized = input.replace(RE_NON_ALPHANUMERIC, "_").replace(RE_MULTIPLE_UNDERSCORES, "_");
1511
+ const trimmed = sanitized.replace(RE_LEADING_UNDERSCORES, "").replace(RE_TRAILING_UNDERSCORES, "");
1491
1512
  if (!trimmed) {
1492
1513
  return "tool";
1493
1514
  }
1494
- if (/^[0-9]/.test(trimmed)) {
1515
+ if (RE_STARTS_WITH_DIGIT.test(trimmed)) {
1495
1516
  return `tool_${trimmed}`;
1496
1517
  }
1497
1518
  return trimmed;
@@ -1682,8 +1703,6 @@ async function completeWithChatCompletionsPassThrough(options, fetcher, path, re
1682
1703
  };
1683
1704
  }
1684
1705
  async function completeWithChatCompletionsWithMCP(options, fetcher, path, request) {
1685
- const mcpToolset = await resolveMCPToolset(request.mcpClients);
1686
- const transportTools = toProviderFunctionTools(mcpToolset);
1687
1706
  const maxToolRounds = normalizeMaxToolRounds(request.maxToolRounds ?? options.defaultMaxToolRounds);
1688
1707
  let messages = buildMessages(request);
1689
1708
  let aggregatedUsage;
@@ -1692,6 +1711,8 @@ async function completeWithChatCompletionsWithMCP(options, fetcher, path, reques
1692
1711
  const toolCalls = [];
1693
1712
  const toolExecutions = [];
1694
1713
  for (let round = 1;round <= maxToolRounds + 1; round += 1) {
1714
+ const mcpToolset = await resolveMCPToolset(request.mcpClients);
1715
+ const transportTools = toProviderFunctionTools(mcpToolset);
1695
1716
  const response = await fetcher(buildURL(options.baseURL, path), {
1696
1717
  method: "POST",
1697
1718
  headers: buildHeaders(options),
@@ -1787,8 +1808,6 @@ async function completeWithResponsesAPIPassThrough(options, fetcher, path, reque
1787
1808
  };
1788
1809
  }
1789
1810
  async function completeWithResponsesAPIWithMCP(options, fetcher, path, request) {
1790
- const mcpToolset = await resolveMCPToolset(request.mcpClients);
1791
- const transportTools = toResponsesTools(toProviderFunctionTools(mcpToolset));
1792
1811
  const maxToolRounds = normalizeMaxToolRounds(request.maxToolRounds ?? options.defaultMaxToolRounds);
1793
1812
  let input = buildResponsesInput(request);
1794
1813
  let previousResponseId = pickString(isRecord2(request.body) ? request.body.previous_response_id : undefined);
@@ -1798,6 +1817,8 @@ async function completeWithResponsesAPIWithMCP(options, fetcher, path, request)
1798
1817
  const executedToolCalls = [];
1799
1818
  const toolExecutions = [];
1800
1819
  for (let round = 1;round <= maxToolRounds + 1; round += 1) {
1820
+ const mcpToolset = await resolveMCPToolset(request.mcpClients);
1821
+ const transportTools = toResponsesTools(toProviderFunctionTools(mcpToolset));
1801
1822
  const response = await fetcher(buildURL(options.baseURL, path), {
1802
1823
  method: "POST",
1803
1824
  headers: buildHeaders(options),
@@ -2237,8 +2258,6 @@ async function completePassThrough(options, fetcher, path, request) {
2237
2258
  };
2238
2259
  }
2239
2260
  async function completeWithMCPToolLoop(options, fetcher, path, request) {
2240
- const mcpToolset = await resolveMCPToolset(request.mcpClients);
2241
- const tools = toAnthropicTools(toProviderFunctionTools(mcpToolset));
2242
2261
  const maxToolRounds = normalizeMaxToolRounds(request.maxToolRounds ?? options.defaultMaxToolRounds);
2243
2262
  let messages = [{ role: "user", content: request.prompt }];
2244
2263
  let aggregatedUsage;
@@ -2247,6 +2266,8 @@ async function completeWithMCPToolLoop(options, fetcher, path, request) {
2247
2266
  const toolCalls = [];
2248
2267
  const toolExecutions = [];
2249
2268
  for (let round = 1;round <= maxToolRounds + 1; round += 1) {
2269
+ const mcpToolset = await resolveMCPToolset(request.mcpClients);
2270
+ const tools = toAnthropicTools(toProviderFunctionTools(mcpToolset));
2250
2271
  const response = await fetcher(buildURL(options.baseURL, path), {
2251
2272
  method: "POST",
2252
2273
  headers: buildHeaders2(options),
@@ -3056,6 +3077,9 @@ var DEFAULT_SELF_HEAL_PROTOCOL = "extrait.self-heal.v2";
3056
3077
  var DEFAULT_SELF_HEAL_MAX_CONTEXT_CHARS = 12000;
3057
3078
  var DEFAULT_SELF_HEAL_STOP_ON_NO_PROGRESS = true;
3058
3079
  var DEFAULT_SELF_HEAL_MAX_ERRORS = 8;
3080
+ var RE_SIMPLE_IDENTIFIER2 = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
3081
+ var RE_ESCAPE_QUOTE = /"/g;
3082
+ var RE_WHITESPACE2 = /\s+/g;
3059
3083
  var DEFAULT_SELF_HEAL_MAX_DIAGNOSTICS = 8;
3060
3084
  var structuredOutdent = createOutdent({
3061
3085
  trimLeadingNewline: true,
@@ -3400,11 +3424,11 @@ function formatIssuePath(path) {
3400
3424
  out += `[${segment}]`;
3401
3425
  continue;
3402
3426
  }
3403
- if (/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(segment)) {
3427
+ if (RE_SIMPLE_IDENTIFIER2.test(segment)) {
3404
3428
  out += `.${segment}`;
3405
3429
  continue;
3406
3430
  }
3407
- out += `["${segment.replace(/"/g, "\\\"")}"]`;
3431
+ out += `["${segment.replace(RE_ESCAPE_QUOTE, "\\\"")}"]`;
3408
3432
  }
3409
3433
  return out;
3410
3434
  }
@@ -3447,7 +3471,7 @@ function buildSelfHealFailureFingerprint(attempt) {
3447
3471
  return [issues, errors, source].join("::");
3448
3472
  }
3449
3473
  function normalizeWhitespace(value) {
3450
- return value.replace(/\s+/g, " ").trim();
3474
+ return value.replace(RE_WHITESPACE2, " ").trim();
3451
3475
  }
3452
3476
  function normalizeStreamConfig(option) {
3453
3477
  if (typeof option === "boolean") {
package/dist/index.d.ts CHANGED
@@ -12,4 +12,4 @@ export { createOpenAICompatibleAdapter, type OpenAICompatibleAdapterOptions, } f
12
12
  export { createAnthropicCompatibleAdapter, DEFAULT_ANTHROPIC_MAX_TOKENS, DEFAULT_ANTHROPIC_VERSION, type AnthropicCompatibleAdapterOptions, } from "./providers/anthropic-compatible";
13
13
  export { DEFAULT_MAX_TOOL_ROUNDS } from "./providers/mcp-runtime";
14
14
  export { createDefaultProviderRegistry, createModelAdapter, createProviderRegistry, registerBuiltinProviders, type BuiltinProviderKind, type ModelAdapterConfig, type ProviderFactory, type ProviderRegistry, type ProviderTransportConfig, } from "./providers/registry";
15
- export type { CandidateDiagnostics, ExtractJsonCandidatesOptions, ExtractionCandidate, ExtractionHeuristicsOptions, ExtractionParseHint, HTTPHeaders, LLMAdapter, LLMRequest, LLMResponse, LLMStreamCallbacks, LLMStreamChunk, LLMToolCall, LLMToolDebugOptions, LLMToolExecution, LLMToolChoice, MCPCallToolParams, MCPListToolsResult, MCPToolClient, MCPToolDescriptor, MCPToolSchema, LLMUsage, MarkdownCodeBlock, MarkdownCodeOptions, ParseLLMOutputOptions, ParseLLMOutputResult, ParseTraceEvent, PipelineError, StructuredAttempt, StructuredCallOptions, StructuredDebugOptions, StructuredError, StructuredMode, StructuredOptions, StructuredPromptBuilder, StructuredPromptContext, StructuredPromptPayload, StructuredPromptResolver, StructuredPromptValue, StructuredResult, StructuredStreamData, StructuredStreamEvent, StructuredStreamInput, StructuredStreamOptions, StructuredSelfHealInput, ThinkDiagnostics, ThinkBlock, StructuredTraceEvent, } from "./types";
15
+ export type { CandidateDiagnostics, ExtractJsonCandidatesOptions, ExtractionCandidate, ExtractionHeuristicsOptions, ExtractionParseHint, HTTPHeaders, LLMAdapter, LLMRequest, LLMResponse, LLMStreamCallbacks, LLMStreamChunk, LLMToolCall, LLMToolDebugOptions, LLMToolExecution, LLMToolOutputTransformer, LLMToolChoice, MCPCallToolParams, MCPListToolsResult, MCPToolClient, MCPToolDescriptor, MCPToolSchema, LLMUsage, MarkdownCodeBlock, MarkdownCodeOptions, ParseLLMOutputOptions, ParseLLMOutputResult, ParseTraceEvent, PipelineError, StructuredAttempt, StructuredCallOptions, StructuredDebugOptions, StructuredError, StructuredMode, StructuredOptions, StructuredPromptBuilder, StructuredPromptContext, StructuredPromptPayload, StructuredPromptResolver, StructuredPromptValue, StructuredResult, StructuredStreamData, StructuredStreamEvent, StructuredStreamInput, StructuredStreamOptions, StructuredSelfHealInput, ThinkDiagnostics, ThinkBlock, StructuredTraceEvent, } from "./types";
package/dist/index.js CHANGED
@@ -219,6 +219,9 @@ function isOnlyWhitespace(value) {
219
219
  }
220
220
 
221
221
  // src/extract.ts
222
+ var RE_EMPTY_OBJECT = /^\{\s*\}$/;
223
+ var RE_EMPTY_ARRAY = /^\[\s*\]$/;
224
+ var RE_BOUNDARY_CHAR = /[\s,.;:!?`"'()\[\]{}<>]/;
222
225
  var DEFAULT_EXTRACTION_HEURISTICS = {
223
226
  firstPassMin: 12,
224
227
  firstPassCap: 30,
@@ -443,7 +446,7 @@ function jsonShapeScore(content, acceptArrays) {
443
446
  const commaCount = countChar(trimmed, ",");
444
447
  const quoteCount = countChar(trimmed, '"');
445
448
  if (root === "{") {
446
- if (/^\{\s*\}$/.test(trimmed)) {
449
+ if (RE_EMPTY_OBJECT.test(trimmed)) {
447
450
  score += 12;
448
451
  } else if (colonCount > 0) {
449
452
  score += 22;
@@ -454,7 +457,7 @@ function jsonShapeScore(content, acceptArrays) {
454
457
  score += quoteCount % 2 === 0 ? 8 : -8;
455
458
  }
456
459
  } else {
457
- score += /^\[\s*\]$/.test(trimmed) ? 8 : 4;
460
+ score += RE_EMPTY_ARRAY.test(trimmed) ? 8 : 4;
458
461
  if (colonCount > 0) {
459
462
  score += 4;
460
463
  }
@@ -487,7 +490,7 @@ function isBoundary(char) {
487
490
  if (!char) {
488
491
  return true;
489
492
  }
490
- return /[\s,.;:!?`"'()[\]{}<>]/.test(char);
493
+ return RE_BOUNDARY_CHAR.test(char);
491
494
  }
492
495
  function lengthScore(length) {
493
496
  return Math.min(120, Math.floor(Math.sqrt(length) * 6));
@@ -676,6 +679,8 @@ function clamp(value, min, max) {
676
679
  return Math.max(min, Math.min(max, Math.floor(value)));
677
680
  }
678
681
  // src/schema.ts
682
+ var RE_SIMPLE_IDENTIFIER = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
683
+ var RE_WHITESPACE = /\s+/g;
679
684
  function formatZodSchemaLikeTypeScript(schema) {
680
685
  return formatType(schema, 0, new WeakSet);
681
686
  }
@@ -847,7 +852,7 @@ ${lines.join(`
847
852
  ${indent}}`;
848
853
  }
849
854
  function formatKey(key) {
850
- return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(key) ? key : JSON.stringify(key);
855
+ return RE_SIMPLE_IDENTIFIER.test(key) ? key : JSON.stringify(key);
851
856
  }
852
857
  function requiresParentheses(typeText) {
853
858
  return typeText.includes(" | ") || typeText.includes(" & ");
@@ -893,7 +898,7 @@ function readSchemaDescription(schema) {
893
898
  return;
894
899
  }
895
900
  function sanitizeDescription(value) {
896
- return value.replace(/\s+/g, " ").trim();
901
+ return value.replace(RE_WHITESPACE, " ").trim();
897
902
  }
898
903
 
899
904
  // src/format.ts
@@ -918,6 +923,8 @@ function resolveSchemaInstruction(instruction) {
918
923
  }
919
924
  // src/think.ts
920
925
  var THINK_TAG_NAME = "think";
926
+ var RE_IDENTIFIER_CHAR = /[a-zA-Z0-9:_-]/;
927
+ var RE_NON_LINE_BREAK = /[^\r\n]/g;
921
928
  function sanitizeThink(input) {
922
929
  const thinkBlocks = [];
923
930
  const diagnostics = {
@@ -1016,6 +1023,9 @@ function parseThinkTagAt(input, index) {
1016
1023
  if (input[cursor] === "/") {
1017
1024
  closing = true;
1018
1025
  cursor += 1;
1026
+ while (cursor < input.length && isWhitespace(input[cursor])) {
1027
+ cursor += 1;
1028
+ }
1019
1029
  }
1020
1030
  if (!matchesIgnoreCase(input, cursor, THINK_TAG_NAME)) {
1021
1031
  return null;
@@ -1072,7 +1082,7 @@ function matchesIgnoreCase(input, index, expected) {
1072
1082
  return input.slice(index, index + expected.length).toLowerCase() === expected;
1073
1083
  }
1074
1084
  function isIdentifierChar(char) {
1075
- return /[a-zA-Z0-9:_-]/.test(char);
1085
+ return RE_IDENTIFIER_CHAR.test(char);
1076
1086
  }
1077
1087
  function countHiddenChars(value) {
1078
1088
  let count = 0;
@@ -1086,9 +1096,10 @@ function countHiddenChars(value) {
1086
1096
  return count;
1087
1097
  }
1088
1098
  function maskKeepingLineBreaks(value) {
1089
- return value.replace(/[^\r\n]/g, " ");
1099
+ return value.replace(RE_NON_LINE_BREAK, " ");
1090
1100
  }
1091
1101
  // src/providers/stream-utils.ts
1102
+ var RE_LINE_ENDING = /\r?\n/;
1092
1103
  async function consumeSSE(response, onEvent) {
1093
1104
  if (!response.body) {
1094
1105
  return;
@@ -1111,7 +1122,7 @@ async function consumeSSE(response, onEvent) {
1111
1122
  buffer = buffer.slice(boundary + (buffer.startsWith(`\r
1112
1123
  \r
1113
1124
  `, boundary) ? 4 : 2));
1114
- const dataLines = rawEvent.split(/\r?\n/).filter((line) => line.startsWith("data:")).map((line) => line.slice(5).trim());
1125
+ const dataLines = rawEvent.split(RE_LINE_ENDING).filter((line) => line.startsWith("data:")).map((line) => line.slice(5).trim());
1115
1126
  if (dataLines.length === 0) {
1116
1127
  continue;
1117
1128
  }
@@ -1121,7 +1132,7 @@ async function consumeSSE(response, onEvent) {
1121
1132
  }
1122
1133
  const remainder = buffer.trim();
1123
1134
  if (remainder.length > 0) {
1124
- const dataLines = remainder.split(/\r?\n/).filter((line) => line.startsWith("data:")).map((line) => line.slice(5).trim());
1135
+ const dataLines = remainder.split(RE_LINE_ENDING).filter((line) => line.startsWith("data:")).map((line) => line.slice(5).trim());
1125
1136
  if (dataLines.length > 0) {
1126
1137
  onEvent(dataLines.join(`
1127
1138
  `));
@@ -1222,20 +1233,25 @@ async function executeMCPToolCalls(calls, toolset, context) {
1222
1233
  name: tool.remoteName,
1223
1234
  arguments: args
1224
1235
  });
1225
- metadata.output = output;
1226
- const execution = {
1236
+ const executionContext = {
1227
1237
  callId,
1228
- type: metadata.type,
1238
+ type: call.type ?? "function",
1229
1239
  name: toolName,
1230
1240
  clientId: tool.clientId,
1231
1241
  remoteName: tool.remoteName,
1232
1242
  arguments: parsedArguments,
1233
- output,
1234
1243
  round: context.round,
1235
1244
  provider: context.provider,
1236
1245
  model: context.model,
1237
1246
  handledLocally: true,
1238
1247
  startedAt,
1248
+ error: undefined
1249
+ };
1250
+ const transformedOutput = context.request.transformToolOutput ? await context.request.transformToolOutput(output, executionContext) : output;
1251
+ metadata.output = transformedOutput;
1252
+ const execution = {
1253
+ ...executionContext,
1254
+ output: transformedOutput,
1239
1255
  durationMs: Date.now() - startedAtMs
1240
1256
  };
1241
1257
  emitToolExecution(context.request, execution);
@@ -1406,13 +1422,18 @@ function describeTool(clientId, tool, hasCollision) {
1406
1422
  }
1407
1423
  return;
1408
1424
  }
1425
+ var RE_NON_ALPHANUMERIC = /[^A-Za-z0-9_]/g;
1426
+ var RE_MULTIPLE_UNDERSCORES = /_+/g;
1427
+ var RE_LEADING_UNDERSCORES = /^_+/;
1428
+ var RE_TRAILING_UNDERSCORES = /_+$/;
1429
+ var RE_STARTS_WITH_DIGIT = /^[0-9]/;
1409
1430
  function sanitizeToolName(input) {
1410
- const sanitized = input.replace(/[^A-Za-z0-9_]/g, "_").replace(/_+/g, "_");
1411
- const trimmed = sanitized.replace(/^_+/, "").replace(/_+$/, "");
1431
+ const sanitized = input.replace(RE_NON_ALPHANUMERIC, "_").replace(RE_MULTIPLE_UNDERSCORES, "_");
1432
+ const trimmed = sanitized.replace(RE_LEADING_UNDERSCORES, "").replace(RE_TRAILING_UNDERSCORES, "");
1412
1433
  if (!trimmed) {
1413
1434
  return "tool";
1414
1435
  }
1415
- if (/^[0-9]/.test(trimmed)) {
1436
+ if (RE_STARTS_WITH_DIGIT.test(trimmed)) {
1416
1437
  return `tool_${trimmed}`;
1417
1438
  }
1418
1439
  return trimmed;
@@ -1603,8 +1624,6 @@ async function completeWithChatCompletionsPassThrough(options, fetcher, path, re
1603
1624
  };
1604
1625
  }
1605
1626
  async function completeWithChatCompletionsWithMCP(options, fetcher, path, request) {
1606
- const mcpToolset = await resolveMCPToolset(request.mcpClients);
1607
- const transportTools = toProviderFunctionTools(mcpToolset);
1608
1627
  const maxToolRounds = normalizeMaxToolRounds(request.maxToolRounds ?? options.defaultMaxToolRounds);
1609
1628
  let messages = buildMessages(request);
1610
1629
  let aggregatedUsage;
@@ -1613,6 +1632,8 @@ async function completeWithChatCompletionsWithMCP(options, fetcher, path, reques
1613
1632
  const toolCalls = [];
1614
1633
  const toolExecutions = [];
1615
1634
  for (let round = 1;round <= maxToolRounds + 1; round += 1) {
1635
+ const mcpToolset = await resolveMCPToolset(request.mcpClients);
1636
+ const transportTools = toProviderFunctionTools(mcpToolset);
1616
1637
  const response = await fetcher(buildURL(options.baseURL, path), {
1617
1638
  method: "POST",
1618
1639
  headers: buildHeaders(options),
@@ -1708,8 +1729,6 @@ async function completeWithResponsesAPIPassThrough(options, fetcher, path, reque
1708
1729
  };
1709
1730
  }
1710
1731
  async function completeWithResponsesAPIWithMCP(options, fetcher, path, request) {
1711
- const mcpToolset = await resolveMCPToolset(request.mcpClients);
1712
- const transportTools = toResponsesTools(toProviderFunctionTools(mcpToolset));
1713
1732
  const maxToolRounds = normalizeMaxToolRounds(request.maxToolRounds ?? options.defaultMaxToolRounds);
1714
1733
  let input = buildResponsesInput(request);
1715
1734
  let previousResponseId = pickString(isRecord2(request.body) ? request.body.previous_response_id : undefined);
@@ -1719,6 +1738,8 @@ async function completeWithResponsesAPIWithMCP(options, fetcher, path, request)
1719
1738
  const executedToolCalls = [];
1720
1739
  const toolExecutions = [];
1721
1740
  for (let round = 1;round <= maxToolRounds + 1; round += 1) {
1741
+ const mcpToolset = await resolveMCPToolset(request.mcpClients);
1742
+ const transportTools = toResponsesTools(toProviderFunctionTools(mcpToolset));
1722
1743
  const response = await fetcher(buildURL(options.baseURL, path), {
1723
1744
  method: "POST",
1724
1745
  headers: buildHeaders(options),
@@ -2158,8 +2179,6 @@ async function completePassThrough(options, fetcher, path, request) {
2158
2179
  };
2159
2180
  }
2160
2181
  async function completeWithMCPToolLoop(options, fetcher, path, request) {
2161
- const mcpToolset = await resolveMCPToolset(request.mcpClients);
2162
- const tools = toAnthropicTools(toProviderFunctionTools(mcpToolset));
2163
2182
  const maxToolRounds = normalizeMaxToolRounds(request.maxToolRounds ?? options.defaultMaxToolRounds);
2164
2183
  let messages = [{ role: "user", content: request.prompt }];
2165
2184
  let aggregatedUsage;
@@ -2168,6 +2187,8 @@ async function completeWithMCPToolLoop(options, fetcher, path, request) {
2168
2187
  const toolCalls = [];
2169
2188
  const toolExecutions = [];
2170
2189
  for (let round = 1;round <= maxToolRounds + 1; round += 1) {
2190
+ const mcpToolset = await resolveMCPToolset(request.mcpClients);
2191
+ const tools = toAnthropicTools(toProviderFunctionTools(mcpToolset));
2171
2192
  const response = await fetcher(buildURL(options.baseURL, path), {
2172
2193
  method: "POST",
2173
2194
  headers: buildHeaders2(options),
@@ -2977,6 +2998,9 @@ var DEFAULT_SELF_HEAL_PROTOCOL = "extrait.self-heal.v2";
2977
2998
  var DEFAULT_SELF_HEAL_MAX_CONTEXT_CHARS = 12000;
2978
2999
  var DEFAULT_SELF_HEAL_STOP_ON_NO_PROGRESS = true;
2979
3000
  var DEFAULT_SELF_HEAL_MAX_ERRORS = 8;
3001
+ var RE_SIMPLE_IDENTIFIER2 = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
3002
+ var RE_ESCAPE_QUOTE = /"/g;
3003
+ var RE_WHITESPACE2 = /\s+/g;
2980
3004
  var DEFAULT_SELF_HEAL_MAX_DIAGNOSTICS = 8;
2981
3005
  var structuredOutdent = createOutdent({
2982
3006
  trimLeadingNewline: true,
@@ -3321,11 +3345,11 @@ function formatIssuePath(path) {
3321
3345
  out += `[${segment}]`;
3322
3346
  continue;
3323
3347
  }
3324
- if (/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(segment)) {
3348
+ if (RE_SIMPLE_IDENTIFIER2.test(segment)) {
3325
3349
  out += `.${segment}`;
3326
3350
  continue;
3327
3351
  }
3328
- out += `["${segment.replace(/"/g, "\\\"")}"]`;
3352
+ out += `["${segment.replace(RE_ESCAPE_QUOTE, "\\\"")}"]`;
3329
3353
  }
3330
3354
  return out;
3331
3355
  }
@@ -3368,7 +3392,7 @@ function buildSelfHealFailureFingerprint(attempt) {
3368
3392
  return [issues, errors, source].join("::");
3369
3393
  }
3370
3394
  function normalizeWhitespace(value) {
3371
- return value.replace(/\s+/g, " ").trim();
3395
+ return value.replace(RE_WHITESPACE2, " ").trim();
3372
3396
  }
3373
3397
  function normalizeStreamConfig(option) {
3374
3398
  if (typeof option === "boolean") {
@@ -36,3 +36,4 @@ export declare function normalizeMaxToolRounds(value: number | undefined): numbe
36
36
  export declare function parseToolArguments(value: unknown): unknown;
37
37
  export declare function stringifyToolOutput(value: unknown): string;
38
38
  export declare function formatToolExecutionDebugLine(execution: LLMToolExecution): string;
39
+ export declare function sanitizeToolName(input: string): string;
package/dist/types.d.ts CHANGED
@@ -129,6 +129,7 @@ export interface LLMRequest {
129
129
  parallelToolCalls?: boolean;
130
130
  maxToolRounds?: number;
131
131
  onToolExecution?: (execution: LLMToolExecution) => void;
132
+ transformToolOutput?: LLMToolOutputTransformer;
132
133
  toolDebug?: boolean | LLMToolDebugOptions;
133
134
  body?: Record<string, unknown>;
134
135
  }
@@ -189,6 +190,7 @@ export interface LLMToolExecution {
189
190
  startedAt: string;
190
191
  durationMs?: number;
191
192
  }
193
+ export type LLMToolOutputTransformer = (output: unknown, execution: Omit<LLMToolExecution, "output" | "durationMs">) => unknown | Promise<unknown>;
192
194
  export interface LLMToolDebugOptions {
193
195
  enabled?: boolean;
194
196
  logger?: (line: string) => void;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "extrait",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -34,16 +34,16 @@
34
34
  "build:types": "bunx tsc -p tsconfig.build.json",
35
35
  "lint": "bunx tsc -p tsconfig.lint.json",
36
36
  "prepublishOnly": "bun run lint && bun run build && bun run build:types",
37
- "test": "bun test",
37
+ "test": "bun test tests/ --reporter=dots --only-failures",
38
38
  "typecheck": "bunx tsc --noEmit"
39
39
  },
40
40
  "dependencies": {
41
41
  "@modelcontextprotocol/sdk": "^1.26.0",
42
- "jsonrepair": "^3.13.1",
43
- "zod": "^3.24.2"
42
+ "jsonrepair": "^3.13.2",
43
+ "zod": "^3.25.76"
44
44
  },
45
45
  "devDependencies": {
46
46
  "@types/bun": "latest",
47
- "typescript": "^5"
47
+ "typescript": "^5.9.3"
48
48
  }
49
49
  }