@theokit/sdk 2.15.0 → 2.15.2

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/index.cjs CHANGED
@@ -13650,11 +13650,16 @@ function sanitizeToolInput(input, options) {
13650
13650
  // src/internal/llm/hermes-tool-extract.ts
13651
13651
  var HERMES_BLOCK = /<function=\s*([^>\s]+)\s*>([\s\S]*?)<\/tool_call>/g;
13652
13652
  var HERMES_PARAM = /<parameter=\s*([^>\s]+)\s*>([\s\S]*?)<\/parameter>/g;
13653
- function extractHermesToolCalls(content, makeId) {
13653
+ function extractHermesToolCalls(content, makeId, allowedToolNames) {
13654
+ const isPromoted = (name) => name.length > 0 && (allowedToolNames === void 0 || allowedToolNames.has(name));
13654
13655
  const toolCalls = [];
13656
+ const droppedNames = [];
13655
13657
  for (const block of content.matchAll(HERMES_BLOCK)) {
13656
13658
  const name = (block[1] ?? "").trim();
13657
- if (name.length === 0) continue;
13659
+ if (!isPromoted(name)) {
13660
+ if (name.length > 0 && allowedToolNames !== void 0) droppedNames.push(name);
13661
+ continue;
13662
+ }
13658
13663
  toolCalls.push({
13659
13664
  type: "tool_use",
13660
13665
  id: makeId(),
@@ -13662,8 +13667,11 @@ function extractHermesToolCalls(content, makeId) {
13662
13667
  input: parseHermesParams(block[2] ?? "")
13663
13668
  });
13664
13669
  }
13665
- const residualText = toolCalls.length === 0 ? content : content.replace(HERMES_BLOCK, "").trim();
13666
- return { toolCalls, residualText };
13670
+ const residualText = toolCalls.length === 0 ? content : content.replace(
13671
+ HERMES_BLOCK,
13672
+ (full, rawName) => isPromoted((rawName ?? "").trim()) ? "" : full
13673
+ ).trim();
13674
+ return { toolCalls, residualText, droppedNames };
13667
13675
  }
13668
13676
  function parseHermesParams(inner) {
13669
13677
  const input = {};
@@ -13675,6 +13683,77 @@ function parseHermesParams(inner) {
13675
13683
  }
13676
13684
  return sanitizeToolInput(input, { trim: true }).value;
13677
13685
  }
13686
+ var STREAM_MARKER = "<function=";
13687
+ var DEFAULT_STREAM_BUFFER_CAP = 8192;
13688
+ var isStreamWs = (c) => c === " " || c === " " || c === "\n" || c === "\r";
13689
+ function streamToolCallBufferState(held, allowedToolNames, cap2 = DEFAULT_STREAM_BUFFER_CAP) {
13690
+ if (allowedToolNames.size === 0) return "impossible";
13691
+ const t = held.trimStart();
13692
+ if (t.length < STREAM_MARKER.length) {
13693
+ return STREAM_MARKER.startsWith(t) ? "possible" : "impossible";
13694
+ }
13695
+ if (!t.startsWith(STREAM_MARKER)) return "impossible";
13696
+ const parsed = parseStreamMarkerName(t);
13697
+ if (parsed === "building") return "possible";
13698
+ if (parsed === "invalid") return "impossible";
13699
+ const nameOk = parsed.complete ? allowedToolNames.has(parsed.name) : someToolNameStartsWith(allowedToolNames, parsed.name);
13700
+ if (!nameOk) return "impossible";
13701
+ return held.length > cap2 ? "impossible" : "possible";
13702
+ }
13703
+ function parseStreamMarkerName(t) {
13704
+ let cursor = STREAM_MARKER.length;
13705
+ while (cursor < t.length && isStreamWs(t[cursor])) cursor += 1;
13706
+ const nameStart = cursor;
13707
+ while (cursor < t.length && t[cursor] !== ">" && !isStreamWs(t[cursor])) cursor += 1;
13708
+ const name = t.slice(nameStart, cursor);
13709
+ if (name.length === 0) return cursor >= t.length ? "building" : "invalid";
13710
+ return { name, complete: cursor < t.length && t[cursor] === ">" };
13711
+ }
13712
+ function someToolNameStartsWith(allowedToolNames, prefix) {
13713
+ for (const name of allowedToolNames) {
13714
+ if (name.startsWith(prefix)) return true;
13715
+ }
13716
+ return false;
13717
+ }
13718
+ function firstPossibleMarkerStart(held, allowedToolNames) {
13719
+ for (let i = held.indexOf("<"); i !== -1; i = held.indexOf("<", i + 1)) {
13720
+ if (streamToolCallBufferState(held.slice(i), allowedToolNames) === "possible") return i;
13721
+ }
13722
+ return -1;
13723
+ }
13724
+ var StreamSuppressionBuffer = class {
13725
+ constructor(allowedToolNames) {
13726
+ this.allowedToolNames = allowedToolNames;
13727
+ }
13728
+ allowedToolNames;
13729
+ #held = "";
13730
+ /** Feed a content delta; returns the text to emit as a `text_delta` now, or `undefined` to hold. */
13731
+ push(content) {
13732
+ this.#held += content;
13733
+ if (streamToolCallBufferState(this.#held, this.allowedToolNames) === "possible")
13734
+ return void 0;
13735
+ const holdStart = firstPossibleMarkerStart(this.#held, this.allowedToolNames);
13736
+ if (holdStart > 0) {
13737
+ const flush2 = this.#held.slice(0, holdStart);
13738
+ this.#held = this.#held.slice(holdStart);
13739
+ return flush2;
13740
+ }
13741
+ const flush = this.#held;
13742
+ this.#held = "";
13743
+ return flush;
13744
+ }
13745
+ /** Drain the held buffer at stream end. `hasNativeCalls` mirrors `finish()`'s size-guard: when
13746
+ * native `tool_calls` exist, `finish()` won't strip the leaked block, so stream the held text WHOLE
13747
+ * (keeping `accumulatedText == finish.text`); otherwise strip the recoverable blocks. Idempotent. */
13748
+ drain(hasNativeCalls) {
13749
+ if (this.#held.length === 0) return void 0;
13750
+ const held = this.#held;
13751
+ this.#held = "";
13752
+ if (hasNativeCalls) return held;
13753
+ const residual = extractHermesToolCalls(held, () => "held", this.allowedToolNames).residualText;
13754
+ return residual.length > 0 ? residual : void 0;
13755
+ }
13756
+ };
13678
13757
 
13679
13758
  // src/internal/llm/openai.ts
13680
13759
  var OpenAIClient = class {
@@ -13747,7 +13826,10 @@ var OpenAIClient = class {
13747
13826
  }
13748
13827
  const accumulator = new OpenAIStreamAccumulator(
13749
13828
  this.options.extractToolCallsFromContent ?? false,
13750
- providerId
13829
+ providerId,
13830
+ // R5: request-scoped allowlist — leaked recovery only promotes a block whose name is a tool the
13831
+ // model was actually given. Empty set (no tools) recovers nothing.
13832
+ new Set(request.tools?.map((tool) => tool.name) ?? [])
13751
13833
  );
13752
13834
  for await (const record of parseSseStream(response.body, signal)) {
13753
13835
  if (record.data === "[DONE]") break;
@@ -13770,6 +13852,8 @@ var OpenAIClient = class {
13770
13852
  const events = accumulator.consume(chunk);
13771
13853
  for (const event of events) yield event;
13772
13854
  }
13855
+ const drainEvent = accumulator.finalizeHeldText();
13856
+ if (drainEvent !== void 0) yield drainEvent;
13773
13857
  return accumulator.finish();
13774
13858
  }
13775
13859
  };
@@ -13777,13 +13861,19 @@ var OpenAIStreamAccumulator = class {
13777
13861
  /**
13778
13862
  * @param extractFromContent opt-in leaked-dialect safe-parse (theokit#58). Default false.
13779
13863
  * @param providerName provider id, used only to label the recovery log line.
13864
+ * @param allowedToolNames R5 request-scoped allowlist — built from `request.tools` at `stream()`;
13865
+ * leaked recovery in `finish()` only promotes a block whose name is in this set. `undefined`
13866
+ * (direct construction) recovers all (back-compat); an empty set recovers nothing.
13780
13867
  */
13781
- constructor(extractFromContent = false, providerName = "openai") {
13868
+ constructor(extractFromContent = false, providerName = "openai", allowedToolNames) {
13782
13869
  this.extractFromContent = extractFromContent;
13783
13870
  this.providerName = providerName;
13871
+ this.allowedToolNames = allowedToolNames;
13872
+ this.suppress = extractFromContent && allowedToolNames !== void 0 && allowedToolNames.size > 0 ? new StreamSuppressionBuffer(allowedToolNames) : void 0;
13784
13873
  }
13785
13874
  extractFromContent;
13786
13875
  providerName;
13876
+ allowedToolNames;
13787
13877
  text = "";
13788
13878
  stopReason = "end_turn";
13789
13879
  inputTokens;
@@ -13792,18 +13882,30 @@ var OpenAIStreamAccumulator = class {
13792
13882
  cacheWriteTokens;
13793
13883
  reasoningTokens;
13794
13884
  toolCalls = /* @__PURE__ */ new Map();
13885
+ /** R7: present only when recovery is enabled AND the request declares tools — holds suspected
13886
+ * leaked-dialect content back from the `text_delta` stream. `undefined` ⇒ stream immediately. */
13887
+ suppress;
13795
13888
  consume(chunk) {
13796
13889
  const events = [];
13797
13890
  this.applyUsage(chunk.usage);
13798
13891
  for (const choice of chunk.choices ?? []) {
13799
- const reasoningEvent = this.applyReasoningDelta(
13800
- choice.delta?.reasoning ?? choice.delta?.reasoning_content
13801
- );
13802
- if (reasoningEvent !== void 0) events.push(reasoningEvent);
13803
- const textEvent = this.applyContentDelta(choice.delta?.content);
13804
- if (textEvent !== void 0) events.push(textEvent);
13805
- this.mergeToolCallDeltas(choice.delta?.tool_calls);
13806
- this.applyFinishReason(choice.finish_reason);
13892
+ events.push(...this.applyChoice(choice));
13893
+ }
13894
+ return events;
13895
+ }
13896
+ applyChoice(choice) {
13897
+ const events = [];
13898
+ const reasoningEvent = this.applyReasoningDelta(
13899
+ choice.delta?.reasoning ?? choice.delta?.reasoning_content
13900
+ );
13901
+ if (reasoningEvent !== void 0) events.push(reasoningEvent);
13902
+ const textEvent = this.applyContentDelta(choice.delta?.content);
13903
+ if (textEvent !== void 0) events.push(textEvent);
13904
+ this.mergeToolCallDeltas(choice.delta?.tool_calls);
13905
+ this.applyFinishReason(choice.finish_reason);
13906
+ if (choice.finish_reason !== void 0 && choice.finish_reason !== null) {
13907
+ const flushEvent = this.finalizeHeldText();
13908
+ if (flushEvent !== void 0) events.push(flushEvent);
13807
13909
  }
13808
13910
  return events;
13809
13911
  }
@@ -13828,7 +13930,17 @@ var OpenAIStreamAccumulator = class {
13828
13930
  applyContentDelta(content) {
13829
13931
  if (typeof content !== "string" || content.length === 0) return void 0;
13830
13932
  this.text += content;
13831
- return { type: "text_delta", text: content };
13933
+ if (this.suppress === void 0) return { type: "text_delta", text: content };
13934
+ const emit2 = this.suppress.push(content);
13935
+ return emit2 !== void 0 ? { type: "text_delta", text: emit2 } : void 0;
13936
+ }
13937
+ /** R7 held-buffer finalizer, called at the `finish_reason` chunk (in `applyChoice`) AND after the
13938
+ * SSE loop in `stream()` — so a stream that omits a `finish_reason` terminal never silently drops
13939
+ * held text. `toolCalls.size > 0` (native calls present) makes `finish()` skip recovery, so the
13940
+ * buffer streams the held text whole. Idempotent once drained. */
13941
+ finalizeHeldText() {
13942
+ const emit2 = this.suppress?.drain(this.toolCalls.size > 0);
13943
+ return emit2 !== void 0 ? { type: "text_delta", text: emit2 } : void 0;
13832
13944
  }
13833
13945
  mergeToolCallDeltas(deltas) {
13834
13946
  for (const call of deltas ?? []) {
@@ -13854,7 +13966,8 @@ var OpenAIStreamAccumulator = class {
13854
13966
  if (this.extractFromContent && toolCalls.length === 0) {
13855
13967
  const recovered = extractHermesToolCalls(
13856
13968
  this.text,
13857
- () => `hermes-${globalThis.crypto.randomUUID()}`
13969
+ () => `hermes-${globalThis.crypto.randomUUID()}`,
13970
+ this.allowedToolNames
13858
13971
  );
13859
13972
  if (recovered.toolCalls.length > 0) {
13860
13973
  toolCalls.push(...recovered.toolCalls);
@@ -13862,6 +13975,12 @@ var OpenAIStreamAccumulator = class {
13862
13975
  stopReason = "tool_use";
13863
13976
  process.stderr.write(
13864
13977
  `[theokit-sdk] recovered ${recovered.toolCalls.length} leaked tool call(s) from assistant content (provider="${this.providerName}", names=${recovered.toolCalls.map((c) => c.name).join(",")})
13978
+ `
13979
+ );
13980
+ }
13981
+ if (recovered.droppedNames.length > 0) {
13982
+ process.stderr.write(
13983
+ `[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(",")})
13865
13984
  `
13866
13985
  );
13867
13986
  }