@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/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # Changelog
2
2
 
3
+ ## 2.15.2
4
+
5
+ ### Patch Changes
6
+
7
+ - 6336f81: Suppress the leaked-dialect tool-call from the visible stream (R7). When `extractToolCallsFromContent` is enabled and a model leaks a `<function=NAME>` tool call as assistant text, the OpenAI-compat streaming now HOLDS that text back at the stream boundary (a small suspicion-buffer FSM that reuses the request-scoped allowlist from R5) instead of emitting it as `text_delta` events — so the raw dialect no longer flashes by in the live stream or lands in the final assistant text. `finish()` still recovers the call (unchanged). Fail-open: a never-closing marker or un-suppressable input is flushed as visible text (never held forever). Flag-off streaming is byte-for-byte unchanged. Grounded in openclaw's stream-normalizer FSM.
8
+
3
9
  ## 2.15.1
4
10
 
5
11
  ### Patch Changes
@@ -10365,12 +10365,83 @@ function parseHermesParams(inner) {
10365
10365
  }
10366
10366
  return sanitizeToolInput(input, { trim: true }).value;
10367
10367
  }
10368
- var HERMES_BLOCK, HERMES_PARAM;
10368
+ function streamToolCallBufferState(held, allowedToolNames, cap = DEFAULT_STREAM_BUFFER_CAP) {
10369
+ if (allowedToolNames.size === 0) return "impossible";
10370
+ const t = held.trimStart();
10371
+ if (t.length < STREAM_MARKER.length) {
10372
+ return STREAM_MARKER.startsWith(t) ? "possible" : "impossible";
10373
+ }
10374
+ if (!t.startsWith(STREAM_MARKER)) return "impossible";
10375
+ const parsed = parseStreamMarkerName(t);
10376
+ if (parsed === "building") return "possible";
10377
+ if (parsed === "invalid") return "impossible";
10378
+ const nameOk = parsed.complete ? allowedToolNames.has(parsed.name) : someToolNameStartsWith(allowedToolNames, parsed.name);
10379
+ if (!nameOk) return "impossible";
10380
+ return held.length > cap ? "impossible" : "possible";
10381
+ }
10382
+ function parseStreamMarkerName(t) {
10383
+ let cursor = STREAM_MARKER.length;
10384
+ while (cursor < t.length && isStreamWs(t[cursor])) cursor += 1;
10385
+ const nameStart = cursor;
10386
+ while (cursor < t.length && t[cursor] !== ">" && !isStreamWs(t[cursor])) cursor += 1;
10387
+ const name = t.slice(nameStart, cursor);
10388
+ if (name.length === 0) return cursor >= t.length ? "building" : "invalid";
10389
+ return { name, complete: cursor < t.length && t[cursor] === ">" };
10390
+ }
10391
+ function someToolNameStartsWith(allowedToolNames, prefix) {
10392
+ for (const name of allowedToolNames) {
10393
+ if (name.startsWith(prefix)) return true;
10394
+ }
10395
+ return false;
10396
+ }
10397
+ function firstPossibleMarkerStart(held, allowedToolNames) {
10398
+ for (let i = held.indexOf("<"); i !== -1; i = held.indexOf("<", i + 1)) {
10399
+ if (streamToolCallBufferState(held.slice(i), allowedToolNames) === "possible") return i;
10400
+ }
10401
+ return -1;
10402
+ }
10403
+ var HERMES_BLOCK, HERMES_PARAM, STREAM_MARKER, DEFAULT_STREAM_BUFFER_CAP, isStreamWs, StreamSuppressionBuffer;
10369
10404
  var init_hermes_tool_extract = __esm({
10370
10405
  "src/internal/llm/hermes-tool-extract.ts"() {
10371
10406
  init_sanitize_tool_input();
10372
10407
  HERMES_BLOCK = /<function=\s*([^>\s]+)\s*>([\s\S]*?)<\/tool_call>/g;
10373
10408
  HERMES_PARAM = /<parameter=\s*([^>\s]+)\s*>([\s\S]*?)<\/parameter>/g;
10409
+ STREAM_MARKER = "<function=";
10410
+ DEFAULT_STREAM_BUFFER_CAP = 8192;
10411
+ isStreamWs = (c) => c === " " || c === " " || c === "\n" || c === "\r";
10412
+ StreamSuppressionBuffer = class {
10413
+ constructor(allowedToolNames) {
10414
+ this.allowedToolNames = allowedToolNames;
10415
+ }
10416
+ allowedToolNames;
10417
+ #held = "";
10418
+ /** Feed a content delta; returns the text to emit as a `text_delta` now, or `undefined` to hold. */
10419
+ push(content) {
10420
+ this.#held += content;
10421
+ if (streamToolCallBufferState(this.#held, this.allowedToolNames) === "possible")
10422
+ return void 0;
10423
+ const holdStart = firstPossibleMarkerStart(this.#held, this.allowedToolNames);
10424
+ if (holdStart > 0) {
10425
+ const flush2 = this.#held.slice(0, holdStart);
10426
+ this.#held = this.#held.slice(holdStart);
10427
+ return flush2;
10428
+ }
10429
+ const flush = this.#held;
10430
+ this.#held = "";
10431
+ return flush;
10432
+ }
10433
+ /** Drain the held buffer at stream end. `hasNativeCalls` mirrors `finish()`'s size-guard: when
10434
+ * native `tool_calls` exist, `finish()` won't strip the leaked block, so stream the held text WHOLE
10435
+ * (keeping `accumulatedText == finish.text`); otherwise strip the recoverable blocks. Idempotent. */
10436
+ drain(hasNativeCalls) {
10437
+ if (this.#held.length === 0) return void 0;
10438
+ const held = this.#held;
10439
+ this.#held = "";
10440
+ if (hasNativeCalls) return held;
10441
+ const residual = extractHermesToolCalls(held, () => "held", this.allowedToolNames).residualText;
10442
+ return residual.length > 0 ? residual : void 0;
10443
+ }
10444
+ };
10374
10445
  }
10375
10446
  });
10376
10447
 
@@ -10584,6 +10655,8 @@ var init_openai2 = __esm({
10584
10655
  const events = accumulator.consume(chunk);
10585
10656
  for (const event of events) yield event;
10586
10657
  }
10658
+ const drainEvent = accumulator.finalizeHeldText();
10659
+ if (drainEvent !== void 0) yield drainEvent;
10587
10660
  return accumulator.finish();
10588
10661
  }
10589
10662
  };
@@ -10599,6 +10672,7 @@ var init_openai2 = __esm({
10599
10672
  this.extractFromContent = extractFromContent;
10600
10673
  this.providerName = providerName;
10601
10674
  this.allowedToolNames = allowedToolNames;
10675
+ this.suppress = extractFromContent && allowedToolNames !== void 0 && allowedToolNames.size > 0 ? new StreamSuppressionBuffer(allowedToolNames) : void 0;
10602
10676
  }
10603
10677
  extractFromContent;
10604
10678
  providerName;
@@ -10611,18 +10685,30 @@ var init_openai2 = __esm({
10611
10685
  cacheWriteTokens;
10612
10686
  reasoningTokens;
10613
10687
  toolCalls = /* @__PURE__ */ new Map();
10688
+ /** R7: present only when recovery is enabled AND the request declares tools — holds suspected
10689
+ * leaked-dialect content back from the `text_delta` stream. `undefined` ⇒ stream immediately. */
10690
+ suppress;
10614
10691
  consume(chunk) {
10615
10692
  const events = [];
10616
10693
  this.applyUsage(chunk.usage);
10617
10694
  for (const choice of chunk.choices ?? []) {
10618
- const reasoningEvent = this.applyReasoningDelta(
10619
- choice.delta?.reasoning ?? choice.delta?.reasoning_content
10620
- );
10621
- if (reasoningEvent !== void 0) events.push(reasoningEvent);
10622
- const textEvent = this.applyContentDelta(choice.delta?.content);
10623
- if (textEvent !== void 0) events.push(textEvent);
10624
- this.mergeToolCallDeltas(choice.delta?.tool_calls);
10625
- this.applyFinishReason(choice.finish_reason);
10695
+ events.push(...this.applyChoice(choice));
10696
+ }
10697
+ return events;
10698
+ }
10699
+ applyChoice(choice) {
10700
+ const events = [];
10701
+ const reasoningEvent = this.applyReasoningDelta(
10702
+ choice.delta?.reasoning ?? choice.delta?.reasoning_content
10703
+ );
10704
+ if (reasoningEvent !== void 0) events.push(reasoningEvent);
10705
+ const textEvent = this.applyContentDelta(choice.delta?.content);
10706
+ if (textEvent !== void 0) events.push(textEvent);
10707
+ this.mergeToolCallDeltas(choice.delta?.tool_calls);
10708
+ this.applyFinishReason(choice.finish_reason);
10709
+ if (choice.finish_reason !== void 0 && choice.finish_reason !== null) {
10710
+ const flushEvent = this.finalizeHeldText();
10711
+ if (flushEvent !== void 0) events.push(flushEvent);
10626
10712
  }
10627
10713
  return events;
10628
10714
  }
@@ -10647,7 +10733,17 @@ var init_openai2 = __esm({
10647
10733
  applyContentDelta(content) {
10648
10734
  if (typeof content !== "string" || content.length === 0) return void 0;
10649
10735
  this.text += content;
10650
- return { type: "text_delta", text: content };
10736
+ if (this.suppress === void 0) return { type: "text_delta", text: content };
10737
+ const emit = this.suppress.push(content);
10738
+ return emit !== void 0 ? { type: "text_delta", text: emit } : void 0;
10739
+ }
10740
+ /** R7 held-buffer finalizer, called at the `finish_reason` chunk (in `applyChoice`) AND after the
10741
+ * SSE loop in `stream()` — so a stream that omits a `finish_reason` terminal never silently drops
10742
+ * held text. `toolCalls.size > 0` (native calls present) makes `finish()` skip recovery, so the
10743
+ * buffer streams the held text whole. Idempotent once drained. */
10744
+ finalizeHeldText() {
10745
+ const emit = this.suppress?.drain(this.toolCalls.size > 0);
10746
+ return emit !== void 0 ? { type: "text_delta", text: emit } : void 0;
10651
10747
  }
10652
10748
  mergeToolCallDeltas(deltas) {
10653
10749
  for (const call of deltas ?? []) {