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