@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/CHANGELOG.md +12 -0
- package/dist/a2a/index.cjs +136 -17
- package/dist/a2a/index.cjs.map +1 -1
- package/dist/a2a/index.js +136 -17
- package/dist/a2a/index.js.map +1 -1
- package/dist/cron.cjs +135 -16
- package/dist/cron.cjs.map +1 -1
- package/dist/cron.js +135 -16
- package/dist/cron.js.map +1 -1
- package/dist/eval.cjs +135 -16
- package/dist/eval.cjs.map +1 -1
- package/dist/eval.js +135 -16
- package/dist/eval.js.map +1 -1
- package/dist/index.cjs +135 -16
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +135 -16
- package/dist/index.js.map +1 -1
- package/dist/internal/llm/hermes-tool-extract.d.ts +5 -1
- package/dist/internal/llm/openai.d.ts +14 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
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
|
+
|
|
9
|
+
## 2.15.1
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- 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.
|
|
14
|
+
|
|
3
15
|
## 2.15.0
|
|
4
16
|
|
|
5
17
|
### Minor Changes
|
package/dist/a2a/index.cjs
CHANGED
|
@@ -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
|
|
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(
|
|
10348
|
-
|
|
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 = {};
|
|
@@ -10357,12 +10365,83 @@ function parseHermesParams(inner) {
|
|
|
10357
10365
|
}
|
|
10358
10366
|
return sanitizeToolInput(input, { trim: true }).value;
|
|
10359
10367
|
}
|
|
10360
|
-
|
|
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;
|
|
10361
10404
|
var init_hermes_tool_extract = __esm({
|
|
10362
10405
|
"src/internal/llm/hermes-tool-extract.ts"() {
|
|
10363
10406
|
init_sanitize_tool_input();
|
|
10364
10407
|
HERMES_BLOCK = /<function=\s*([^>\s]+)\s*>([\s\S]*?)<\/tool_call>/g;
|
|
10365
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
|
+
};
|
|
10366
10445
|
}
|
|
10367
10446
|
});
|
|
10368
10447
|
|
|
@@ -10550,7 +10629,10 @@ var init_openai2 = __esm({
|
|
|
10550
10629
|
}
|
|
10551
10630
|
const accumulator = new OpenAIStreamAccumulator(
|
|
10552
10631
|
this.options.extractToolCallsFromContent ?? false,
|
|
10553
|
-
providerId
|
|
10632
|
+
providerId,
|
|
10633
|
+
// R5: request-scoped allowlist — leaked recovery only promotes a block whose name is a tool the
|
|
10634
|
+
// model was actually given. Empty set (no tools) recovers nothing.
|
|
10635
|
+
new Set(request.tools?.map((tool) => tool.name) ?? [])
|
|
10554
10636
|
);
|
|
10555
10637
|
for await (const record of parseSseStream(response.body, signal)) {
|
|
10556
10638
|
if (record.data === "[DONE]") break;
|
|
@@ -10573,6 +10655,8 @@ var init_openai2 = __esm({
|
|
|
10573
10655
|
const events = accumulator.consume(chunk);
|
|
10574
10656
|
for (const event of events) yield event;
|
|
10575
10657
|
}
|
|
10658
|
+
const drainEvent = accumulator.finalizeHeldText();
|
|
10659
|
+
if (drainEvent !== void 0) yield drainEvent;
|
|
10576
10660
|
return accumulator.finish();
|
|
10577
10661
|
}
|
|
10578
10662
|
};
|
|
@@ -10580,13 +10664,19 @@ var init_openai2 = __esm({
|
|
|
10580
10664
|
/**
|
|
10581
10665
|
* @param extractFromContent opt-in leaked-dialect safe-parse (theokit#58). Default false.
|
|
10582
10666
|
* @param providerName provider id, used only to label the recovery log line.
|
|
10667
|
+
* @param allowedToolNames R5 request-scoped allowlist — built from `request.tools` at `stream()`;
|
|
10668
|
+
* leaked recovery in `finish()` only promotes a block whose name is in this set. `undefined`
|
|
10669
|
+
* (direct construction) recovers all (back-compat); an empty set recovers nothing.
|
|
10583
10670
|
*/
|
|
10584
|
-
constructor(extractFromContent = false, providerName = "openai") {
|
|
10671
|
+
constructor(extractFromContent = false, providerName = "openai", allowedToolNames) {
|
|
10585
10672
|
this.extractFromContent = extractFromContent;
|
|
10586
10673
|
this.providerName = providerName;
|
|
10674
|
+
this.allowedToolNames = allowedToolNames;
|
|
10675
|
+
this.suppress = extractFromContent && allowedToolNames !== void 0 && allowedToolNames.size > 0 ? new StreamSuppressionBuffer(allowedToolNames) : void 0;
|
|
10587
10676
|
}
|
|
10588
10677
|
extractFromContent;
|
|
10589
10678
|
providerName;
|
|
10679
|
+
allowedToolNames;
|
|
10590
10680
|
text = "";
|
|
10591
10681
|
stopReason = "end_turn";
|
|
10592
10682
|
inputTokens;
|
|
@@ -10595,18 +10685,30 @@ var init_openai2 = __esm({
|
|
|
10595
10685
|
cacheWriteTokens;
|
|
10596
10686
|
reasoningTokens;
|
|
10597
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;
|
|
10598
10691
|
consume(chunk) {
|
|
10599
10692
|
const events = [];
|
|
10600
10693
|
this.applyUsage(chunk.usage);
|
|
10601
10694
|
for (const choice of chunk.choices ?? []) {
|
|
10602
|
-
|
|
10603
|
-
|
|
10604
|
-
|
|
10605
|
-
|
|
10606
|
-
|
|
10607
|
-
|
|
10608
|
-
|
|
10609
|
-
|
|
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);
|
|
10610
10712
|
}
|
|
10611
10713
|
return events;
|
|
10612
10714
|
}
|
|
@@ -10631,7 +10733,17 @@ var init_openai2 = __esm({
|
|
|
10631
10733
|
applyContentDelta(content) {
|
|
10632
10734
|
if (typeof content !== "string" || content.length === 0) return void 0;
|
|
10633
10735
|
this.text += content;
|
|
10634
|
-
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;
|
|
10635
10747
|
}
|
|
10636
10748
|
mergeToolCallDeltas(deltas) {
|
|
10637
10749
|
for (const call of deltas ?? []) {
|
|
@@ -10657,7 +10769,8 @@ var init_openai2 = __esm({
|
|
|
10657
10769
|
if (this.extractFromContent && toolCalls.length === 0) {
|
|
10658
10770
|
const recovered = extractHermesToolCalls(
|
|
10659
10771
|
this.text,
|
|
10660
|
-
() => `hermes-${globalThis.crypto.randomUUID()}
|
|
10772
|
+
() => `hermes-${globalThis.crypto.randomUUID()}`,
|
|
10773
|
+
this.allowedToolNames
|
|
10661
10774
|
);
|
|
10662
10775
|
if (recovered.toolCalls.length > 0) {
|
|
10663
10776
|
toolCalls.push(...recovered.toolCalls);
|
|
@@ -10665,6 +10778,12 @@ var init_openai2 = __esm({
|
|
|
10665
10778
|
stopReason = "tool_use";
|
|
10666
10779
|
process.stderr.write(
|
|
10667
10780
|
`[theokit-sdk] recovered ${recovered.toolCalls.length} leaked tool call(s) from assistant content (provider="${this.providerName}", names=${recovered.toolCalls.map((c) => c.name).join(",")})
|
|
10781
|
+
`
|
|
10782
|
+
);
|
|
10783
|
+
}
|
|
10784
|
+
if (recovered.droppedNames.length > 0) {
|
|
10785
|
+
process.stderr.write(
|
|
10786
|
+
`[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
10787
|
`
|
|
10669
10788
|
);
|
|
10670
10789
|
}
|