@theokit/sdk 2.11.3 → 2.12.0

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.12.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 95e9cba: Add opt-in leaked-dialect safe-parse for OpenAI-compatible providers (`ProviderProfile.extractToolCallsFromContent`, default off). Some models — notably qwen3-coder via OpenRouter — intermittently emit their Hermes tool-call dialect (`<function=NAME><parameter=KEY>VALUE</parameter></function></tool_call>`) as assistant TEXT instead of native `tool_calls`. When that happens the provider sends ZERO native `tool_calls`, so the agent loop sees a plain `end_turn` and the intended call is silently lost (theokit#58 follow-up). With the flag enabled, a `chat_completions` finish that has no native `tool_calls` has its assistant content scanned for the leaked dialect; any recovered calls are surfaced as real `tool_calls` and the stop reason flips to `tool_use` so the loop dispatches them. Fail-open like `stripThinkBlocks` — a partial/unclosed block is left as text and never fabricates a call. Default off, dedup-guarded (native `tool_calls` always win, no double-count), and scoped per-provider so a code assistant printing a literal `<function=` in a fenced block on a non-leaking route is unaffected.
8
+
3
9
  ## 2.11.3
4
10
 
5
11
  ### Patch Changes
@@ -10097,6 +10097,40 @@ var init_ollama_native = __esm({
10097
10097
  }
10098
10098
  });
10099
10099
 
10100
+ // src/internal/llm/hermes-tool-extract.ts
10101
+ function extractHermesToolCalls(content, makeId) {
10102
+ const toolCalls = [];
10103
+ for (const block of content.matchAll(HERMES_BLOCK)) {
10104
+ const name = (block[1] ?? "").trim();
10105
+ if (name.length === 0) continue;
10106
+ toolCalls.push({
10107
+ type: "tool_use",
10108
+ id: makeId(),
10109
+ name,
10110
+ input: parseHermesParams(block[2] ?? "")
10111
+ });
10112
+ }
10113
+ const residualText = toolCalls.length === 0 ? content : content.replace(HERMES_BLOCK, "").trim();
10114
+ return { toolCalls, residualText };
10115
+ }
10116
+ function parseHermesParams(inner) {
10117
+ const input = {};
10118
+ for (const param of inner.matchAll(HERMES_PARAM)) {
10119
+ const key = param[1];
10120
+ const value = param[2];
10121
+ if (key === void 0 || value === void 0) continue;
10122
+ input[key.trim()] = value;
10123
+ }
10124
+ return input;
10125
+ }
10126
+ var HERMES_BLOCK, HERMES_PARAM;
10127
+ var init_hermes_tool_extract = __esm({
10128
+ "src/internal/llm/hermes-tool-extract.ts"() {
10129
+ HERMES_BLOCK = /<function=\s*([^>\s]+)\s*>([\s\S]*?)<\/tool_call>/g;
10130
+ HERMES_PARAM = /<parameter=\s*([^>\s]+)\s*>([\s\S]*?)<\/parameter>/g;
10131
+ }
10132
+ });
10133
+
10100
10134
  // src/internal/llm/openai.ts
10101
10135
  function mapOpenAIFinish(reason) {
10102
10136
  switch (reason) {
@@ -10209,6 +10243,7 @@ var init_openai2 = __esm({
10209
10243
  init_ollama2();
10210
10244
  init_openai_compatible();
10211
10245
  init_finish();
10246
+ init_hermes_tool_extract();
10212
10247
  init_sse();
10213
10248
  OpenAIClient = class {
10214
10249
  constructor(options) {
@@ -10270,7 +10305,10 @@ var init_openai2 = __esm({
10270
10305
  endpoint: "/v1/chat/completions"
10271
10306
  });
10272
10307
  }
10273
- const accumulator = new OpenAIStreamAccumulator();
10308
+ const accumulator = new OpenAIStreamAccumulator(
10309
+ this.options.extractToolCallsFromContent ?? false,
10310
+ providerId
10311
+ );
10274
10312
  for await (const record of parseSseStream(response.body, signal)) {
10275
10313
  if (record.data === "[DONE]") break;
10276
10314
  let chunk;
@@ -10296,6 +10334,16 @@ var init_openai2 = __esm({
10296
10334
  }
10297
10335
  };
10298
10336
  OpenAIStreamAccumulator = class {
10337
+ /**
10338
+ * @param extractFromContent opt-in leaked-dialect safe-parse (theokit#58). Default false.
10339
+ * @param providerName provider id, used only to label the recovery log line.
10340
+ */
10341
+ constructor(extractFromContent = false, providerName = "openai") {
10342
+ this.extractFromContent = extractFromContent;
10343
+ this.providerName = providerName;
10344
+ }
10345
+ extractFromContent;
10346
+ providerName;
10299
10347
  text = "";
10300
10348
  stopReason = "end_turn";
10301
10349
  inputTokens;
@@ -10361,9 +10409,26 @@ var init_openai2 = __esm({
10361
10409
  const input = parseToolArguments(call.args);
10362
10410
  toolCalls.push({ type: "tool_use", id: call.id, name: call.name, input });
10363
10411
  }
10412
+ let text = this.text;
10413
+ let stopReason = this.stopReason;
10414
+ if (this.extractFromContent && toolCalls.length === 0) {
10415
+ const recovered = extractHermesToolCalls(
10416
+ this.text,
10417
+ () => `hermes-${globalThis.crypto.randomUUID()}`
10418
+ );
10419
+ if (recovered.toolCalls.length > 0) {
10420
+ toolCalls.push(...recovered.toolCalls);
10421
+ text = recovered.residualText;
10422
+ stopReason = "tool_use";
10423
+ process.stderr.write(
10424
+ `[theokit-sdk] recovered ${recovered.toolCalls.length} leaked tool call(s) from assistant content (provider="${this.providerName}", names=${recovered.toolCalls.map((c) => c.name).join(",")})
10425
+ `
10426
+ );
10427
+ }
10428
+ }
10364
10429
  return makeLlmFinish({
10365
- stopReason: this.stopReason,
10366
- text: this.text,
10430
+ stopReason,
10431
+ text,
10367
10432
  toolCalls,
10368
10433
  inputTokens: this.inputTokens,
10369
10434
  outputTokens: this.outputTokens,
@@ -10876,6 +10941,9 @@ function selectTransport(profile, apiKey) {
10876
10941
  const opts = { apiKey };
10877
10942
  opts.baseUrl = profile.baseUrl;
10878
10943
  opts.providerName = profile.name;
10944
+ if (profile.extractToolCallsFromContent === true) {
10945
+ opts.extractToolCallsFromContent = true;
10946
+ }
10879
10947
  if (profile.name === "openai" && process.env.OPENAI_ORGANIZATION !== void 0) {
10880
10948
  opts.organization = process.env.OPENAI_ORGANIZATION;
10881
10949
  }