@theokit/sdk 2.15.1 → 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
@@ -13680,6 +13680,77 @@ function parseHermesParams(inner) {
13680
13680
  }
13681
13681
  return sanitizeToolInput(input, { trim: true }).value;
13682
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
+ };
13683
13754
 
13684
13755
  // src/internal/llm/openai.ts
13685
13756
  var OpenAIClient = class {
@@ -13778,6 +13849,8 @@ var OpenAIClient = class {
13778
13849
  const events = accumulator.consume(chunk);
13779
13850
  for (const event of events) yield event;
13780
13851
  }
13852
+ const drainEvent = accumulator.finalizeHeldText();
13853
+ if (drainEvent !== void 0) yield drainEvent;
13781
13854
  return accumulator.finish();
13782
13855
  }
13783
13856
  };
@@ -13793,6 +13866,7 @@ var OpenAIStreamAccumulator = class {
13793
13866
  this.extractFromContent = extractFromContent;
13794
13867
  this.providerName = providerName;
13795
13868
  this.allowedToolNames = allowedToolNames;
13869
+ this.suppress = extractFromContent && allowedToolNames !== void 0 && allowedToolNames.size > 0 ? new StreamSuppressionBuffer(allowedToolNames) : void 0;
13796
13870
  }
13797
13871
  extractFromContent;
13798
13872
  providerName;
@@ -13805,18 +13879,30 @@ var OpenAIStreamAccumulator = class {
13805
13879
  cacheWriteTokens;
13806
13880
  reasoningTokens;
13807
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;
13808
13885
  consume(chunk) {
13809
13886
  const events = [];
13810
13887
  this.applyUsage(chunk.usage);
13811
13888
  for (const choice of chunk.choices ?? []) {
13812
- const reasoningEvent = this.applyReasoningDelta(
13813
- choice.delta?.reasoning ?? choice.delta?.reasoning_content
13814
- );
13815
- if (reasoningEvent !== void 0) events.push(reasoningEvent);
13816
- const textEvent = this.applyContentDelta(choice.delta?.content);
13817
- if (textEvent !== void 0) events.push(textEvent);
13818
- this.mergeToolCallDeltas(choice.delta?.tool_calls);
13819
- 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);
13820
13906
  }
13821
13907
  return events;
13822
13908
  }
@@ -13841,7 +13927,17 @@ var OpenAIStreamAccumulator = class {
13841
13927
  applyContentDelta(content) {
13842
13928
  if (typeof content !== "string" || content.length === 0) return void 0;
13843
13929
  this.text += content;
13844
- 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;
13845
13941
  }
13846
13942
  mergeToolCallDeltas(deltas) {
13847
13943
  for (const call of deltas ?? []) {