@theokit/sdk 2.15.0 → 2.15.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # Changelog
2
2
 
3
+ ## 2.15.1
4
+
5
+ ### Patch Changes
6
+
7
+ - bec2077: Make the leaked-dialect recovery **request-scoped (R5)**. The opt-in `extractToolCallsFromContent` recovery previously promoted ANY `<function=NAME>` block leaked into assistant text on an enabled route, so a code assistant printing a literal `<function=example>` in a fenced code block could be wrongly turned into a tool call. Recovery now gates on an exact, case-sensitive allowlist derived automatically from the current request's declared tools (`request.tools`): the per-route flag stays the coarse enable, and the allowlist is the precise false-positive guard. A request with no tools recovers nothing; a gated-out block keeps its text visible (it is not silently deleted). No public API change — the allowlist is derived from the tools you already pass. Mirrors openclaw's `@openclaw/tool-call-repair` allowlist.
8
+
3
9
  ## 2.15.0
4
10
 
5
11
  ### Minor Changes
@@ -10332,11 +10332,16 @@ var init_sanitize_tool_input = __esm({
10332
10332
  });
10333
10333
 
10334
10334
  // src/internal/llm/hermes-tool-extract.ts
10335
- function extractHermesToolCalls(content, makeId) {
10335
+ function extractHermesToolCalls(content, makeId, allowedToolNames) {
10336
+ const isPromoted = (name) => name.length > 0 && (allowedToolNames === void 0 || allowedToolNames.has(name));
10336
10337
  const toolCalls = [];
10338
+ const droppedNames = [];
10337
10339
  for (const block of content.matchAll(HERMES_BLOCK)) {
10338
10340
  const name = (block[1] ?? "").trim();
10339
- if (name.length === 0) continue;
10341
+ if (!isPromoted(name)) {
10342
+ if (name.length > 0 && allowedToolNames !== void 0) droppedNames.push(name);
10343
+ continue;
10344
+ }
10340
10345
  toolCalls.push({
10341
10346
  type: "tool_use",
10342
10347
  id: makeId(),
@@ -10344,8 +10349,11 @@ function extractHermesToolCalls(content, makeId) {
10344
10349
  input: parseHermesParams(block[2] ?? "")
10345
10350
  });
10346
10351
  }
10347
- const residualText = toolCalls.length === 0 ? content : content.replace(HERMES_BLOCK, "").trim();
10348
- return { toolCalls, residualText };
10352
+ const residualText = toolCalls.length === 0 ? content : content.replace(
10353
+ HERMES_BLOCK,
10354
+ (full, rawName) => isPromoted((rawName ?? "").trim()) ? "" : full
10355
+ ).trim();
10356
+ return { toolCalls, residualText, droppedNames };
10349
10357
  }
10350
10358
  function parseHermesParams(inner) {
10351
10359
  const input = {};
@@ -10550,7 +10558,10 @@ var init_openai2 = __esm({
10550
10558
  }
10551
10559
  const accumulator = new OpenAIStreamAccumulator(
10552
10560
  this.options.extractToolCallsFromContent ?? false,
10553
- providerId
10561
+ providerId,
10562
+ // R5: request-scoped allowlist — leaked recovery only promotes a block whose name is a tool the
10563
+ // model was actually given. Empty set (no tools) recovers nothing.
10564
+ new Set(request.tools?.map((tool) => tool.name) ?? [])
10554
10565
  );
10555
10566
  for await (const record of parseSseStream(response.body, signal)) {
10556
10567
  if (record.data === "[DONE]") break;
@@ -10580,13 +10591,18 @@ var init_openai2 = __esm({
10580
10591
  /**
10581
10592
  * @param extractFromContent opt-in leaked-dialect safe-parse (theokit#58). Default false.
10582
10593
  * @param providerName provider id, used only to label the recovery log line.
10594
+ * @param allowedToolNames R5 request-scoped allowlist — built from `request.tools` at `stream()`;
10595
+ * leaked recovery in `finish()` only promotes a block whose name is in this set. `undefined`
10596
+ * (direct construction) recovers all (back-compat); an empty set recovers nothing.
10583
10597
  */
10584
- constructor(extractFromContent = false, providerName = "openai") {
10598
+ constructor(extractFromContent = false, providerName = "openai", allowedToolNames) {
10585
10599
  this.extractFromContent = extractFromContent;
10586
10600
  this.providerName = providerName;
10601
+ this.allowedToolNames = allowedToolNames;
10587
10602
  }
10588
10603
  extractFromContent;
10589
10604
  providerName;
10605
+ allowedToolNames;
10590
10606
  text = "";
10591
10607
  stopReason = "end_turn";
10592
10608
  inputTokens;
@@ -10657,7 +10673,8 @@ var init_openai2 = __esm({
10657
10673
  if (this.extractFromContent && toolCalls.length === 0) {
10658
10674
  const recovered = extractHermesToolCalls(
10659
10675
  this.text,
10660
- () => `hermes-${globalThis.crypto.randomUUID()}`
10676
+ () => `hermes-${globalThis.crypto.randomUUID()}`,
10677
+ this.allowedToolNames
10661
10678
  );
10662
10679
  if (recovered.toolCalls.length > 0) {
10663
10680
  toolCalls.push(...recovered.toolCalls);
@@ -10665,6 +10682,12 @@ var init_openai2 = __esm({
10665
10682
  stopReason = "tool_use";
10666
10683
  process.stderr.write(
10667
10684
  `[theokit-sdk] recovered ${recovered.toolCalls.length} leaked tool call(s) from assistant content (provider="${this.providerName}", names=${recovered.toolCalls.map((c) => c.name).join(",")})
10685
+ `
10686
+ );
10687
+ }
10688
+ if (recovered.droppedNames.length > 0) {
10689
+ process.stderr.write(
10690
+ `[theokit-sdk] dropped ${recovered.droppedNames.length} leaked block(s) whose name is not a tool in the request (provider="${this.providerName}", names=${recovered.droppedNames.join(",")})
10668
10691
  `
10669
10692
  );
10670
10693
  }