@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.
@@ -873,6 +873,13 @@ interface ProviderProfile {
873
873
  fallbackModels: ReadonlyArray<string>;
874
874
  extraHeaders?: Record<string, string>;
875
875
  bodyOverrides?: Record<string, unknown>;
876
+ /**
877
+ * Opt-in leaked-dialect safe-parse (theokit#58 follow-up). When `true`, a chat_completions finish
878
+ * with ZERO native `tool_calls` has its assistant content scanned for the Hermes
879
+ * `<function=…></tool_call>` dialect and any recovered calls are surfaced as real `tool_calls`.
880
+ * Default off — only enable for routes/models known to leak (e.g. a qwen3-coder profile variant).
881
+ */
882
+ extractToolCallsFromContent?: boolean;
876
883
  }
877
884
 
878
885
  type HookName = "pre_tool_call" | "post_tool_call" | "pre_llm_call" | "post_llm_call" | "on_session_start" | "on_session_end" | "transform_tool_result" | "transform_llm_output" | "pre_user_send" | "post_assistant_reply";
@@ -873,6 +873,13 @@ interface ProviderProfile {
873
873
  fallbackModels: ReadonlyArray<string>;
874
874
  extraHeaders?: Record<string, string>;
875
875
  bodyOverrides?: Record<string, unknown>;
876
+ /**
877
+ * Opt-in leaked-dialect safe-parse (theokit#58 follow-up). When `true`, a chat_completions finish
878
+ * with ZERO native `tool_calls` has its assistant content scanned for the Hermes
879
+ * `<function=…></tool_call>` dialect and any recovered calls are surfaced as real `tool_calls`.
880
+ * Default off — only enable for routes/models known to leak (e.g. a qwen3-coder profile variant).
881
+ */
882
+ extractToolCallsFromContent?: boolean;
876
883
  }
877
884
 
878
885
  type HookName = "pre_tool_call" | "post_tool_call" | "pre_llm_call" | "post_llm_call" | "on_session_start" | "on_session_end" | "transform_tool_result" | "transform_llm_output" | "pre_user_send" | "post_assistant_reply";
package/dist/cron.cjs CHANGED
@@ -10956,6 +10956,35 @@ function toOllamaTools(tools) {
10956
10956
  }));
10957
10957
  }
10958
10958
 
10959
+ // src/internal/llm/hermes-tool-extract.ts
10960
+ var HERMES_BLOCK = /<function=\s*([^>\s]+)\s*>([\s\S]*?)<\/tool_call>/g;
10961
+ var HERMES_PARAM = /<parameter=\s*([^>\s]+)\s*>([\s\S]*?)<\/parameter>/g;
10962
+ function extractHermesToolCalls(content, makeId) {
10963
+ const toolCalls = [];
10964
+ for (const block of content.matchAll(HERMES_BLOCK)) {
10965
+ const name = (block[1] ?? "").trim();
10966
+ if (name.length === 0) continue;
10967
+ toolCalls.push({
10968
+ type: "tool_use",
10969
+ id: makeId(),
10970
+ name,
10971
+ input: parseHermesParams(block[2] ?? "")
10972
+ });
10973
+ }
10974
+ const residualText = toolCalls.length === 0 ? content : content.replace(HERMES_BLOCK, "").trim();
10975
+ return { toolCalls, residualText };
10976
+ }
10977
+ function parseHermesParams(inner) {
10978
+ const input = {};
10979
+ for (const param of inner.matchAll(HERMES_PARAM)) {
10980
+ const key = param[1];
10981
+ const value = param[2];
10982
+ if (key === void 0 || value === void 0) continue;
10983
+ input[key.trim()] = value;
10984
+ }
10985
+ return input;
10986
+ }
10987
+
10959
10988
  // src/internal/llm/openai.ts
10960
10989
  var OpenAIClient = class {
10961
10990
  constructor(options) {
@@ -11017,7 +11046,10 @@ var OpenAIClient = class {
11017
11046
  endpoint: "/v1/chat/completions"
11018
11047
  });
11019
11048
  }
11020
- const accumulator = new OpenAIStreamAccumulator();
11049
+ const accumulator = new OpenAIStreamAccumulator(
11050
+ this.options.extractToolCallsFromContent ?? false,
11051
+ providerId
11052
+ );
11021
11053
  for await (const record of parseSseStream(response.body, signal)) {
11022
11054
  if (record.data === "[DONE]") break;
11023
11055
  let chunk;
@@ -11043,6 +11075,16 @@ var OpenAIClient = class {
11043
11075
  }
11044
11076
  };
11045
11077
  var OpenAIStreamAccumulator = class {
11078
+ /**
11079
+ * @param extractFromContent opt-in leaked-dialect safe-parse (theokit#58). Default false.
11080
+ * @param providerName provider id, used only to label the recovery log line.
11081
+ */
11082
+ constructor(extractFromContent = false, providerName = "openai") {
11083
+ this.extractFromContent = extractFromContent;
11084
+ this.providerName = providerName;
11085
+ }
11086
+ extractFromContent;
11087
+ providerName;
11046
11088
  text = "";
11047
11089
  stopReason = "end_turn";
11048
11090
  inputTokens;
@@ -11108,9 +11150,26 @@ var OpenAIStreamAccumulator = class {
11108
11150
  const input = parseToolArguments(call.args);
11109
11151
  toolCalls.push({ type: "tool_use", id: call.id, name: call.name, input });
11110
11152
  }
11153
+ let text = this.text;
11154
+ let stopReason = this.stopReason;
11155
+ if (this.extractFromContent && toolCalls.length === 0) {
11156
+ const recovered = extractHermesToolCalls(
11157
+ this.text,
11158
+ () => `hermes-${globalThis.crypto.randomUUID()}`
11159
+ );
11160
+ if (recovered.toolCalls.length > 0) {
11161
+ toolCalls.push(...recovered.toolCalls);
11162
+ text = recovered.residualText;
11163
+ stopReason = "tool_use";
11164
+ process.stderr.write(
11165
+ `[theokit-sdk] recovered ${recovered.toolCalls.length} leaked tool call(s) from assistant content (provider="${this.providerName}", names=${recovered.toolCalls.map((c) => c.name).join(",")})
11166
+ `
11167
+ );
11168
+ }
11169
+ }
11111
11170
  return makeLlmFinish({
11112
- stopReason: this.stopReason,
11113
- text: this.text,
11171
+ stopReason,
11172
+ text,
11114
11173
  toolCalls,
11115
11174
  inputTokens: this.inputTokens,
11116
11175
  outputTokens: this.outputTokens,
@@ -11690,6 +11749,9 @@ function selectTransport(profile, apiKey) {
11690
11749
  const opts = { apiKey };
11691
11750
  opts.baseUrl = profile.baseUrl;
11692
11751
  opts.providerName = profile.name;
11752
+ if (profile.extractToolCallsFromContent === true) {
11753
+ opts.extractToolCallsFromContent = true;
11754
+ }
11693
11755
  if (profile.name === "openai" && process.env.OPENAI_ORGANIZATION !== void 0) {
11694
11756
  opts.organization = process.env.OPENAI_ORGANIZATION;
11695
11757
  }