@theokit/sdk 2.11.3 → 2.13.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.
@@ -760,6 +760,17 @@ interface ProviderRoute {
760
760
  capability: ProviderCapability;
761
761
  provider: string;
762
762
  model?: string;
763
+ /**
764
+ * Opt-in leaked-dialect safe-parse for this route's provider (theokit#58
765
+ * follow-up). When `true`, a `chat_completions` finish that carries ZERO
766
+ * native `tool_calls` has its assistant text scanned for the Hermes
767
+ * `<function=…></tool_call>` dialect, and any recovered calls are surfaced as
768
+ * real `tool_calls` so the loop executes them — for models (qwen3-coder via
769
+ * OpenRouter) that intermittently leak tool calls as text. Default `false`;
770
+ * fail-open (a partial/unclosed block never fabricates a call). Scoped to the
771
+ * resolved chat chain, so a non-leaking route is unaffected.
772
+ */
773
+ extractToolCallsFromContent?: boolean;
763
774
  }
764
775
  /**
765
776
  * Provider routing configuration accepted by `Agent.create()` via
@@ -873,6 +884,13 @@ interface ProviderProfile {
873
884
  fallbackModels: ReadonlyArray<string>;
874
885
  extraHeaders?: Record<string, string>;
875
886
  bodyOverrides?: Record<string, unknown>;
887
+ /**
888
+ * Opt-in leaked-dialect safe-parse (theokit#58 follow-up). When `true`, a chat_completions finish
889
+ * with ZERO native `tool_calls` has its assistant content scanned for the Hermes
890
+ * `<function=…></tool_call>` dialect and any recovered calls are surfaced as real `tool_calls`.
891
+ * Default off — only enable for routes/models known to leak (e.g. a qwen3-coder profile variant).
892
+ */
893
+ extractToolCallsFromContent?: boolean;
876
894
  }
877
895
 
878
896
  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";
@@ -760,6 +760,17 @@ interface ProviderRoute {
760
760
  capability: ProviderCapability;
761
761
  provider: string;
762
762
  model?: string;
763
+ /**
764
+ * Opt-in leaked-dialect safe-parse for this route's provider (theokit#58
765
+ * follow-up). When `true`, a `chat_completions` finish that carries ZERO
766
+ * native `tool_calls` has its assistant text scanned for the Hermes
767
+ * `<function=…></tool_call>` dialect, and any recovered calls are surfaced as
768
+ * real `tool_calls` so the loop executes them — for models (qwen3-coder via
769
+ * OpenRouter) that intermittently leak tool calls as text. Default `false`;
770
+ * fail-open (a partial/unclosed block never fabricates a call). Scoped to the
771
+ * resolved chat chain, so a non-leaking route is unaffected.
772
+ */
773
+ extractToolCallsFromContent?: boolean;
763
774
  }
764
775
  /**
765
776
  * Provider routing configuration accepted by `Agent.create()` via
@@ -873,6 +884,13 @@ interface ProviderProfile {
873
884
  fallbackModels: ReadonlyArray<string>;
874
885
  extraHeaders?: Record<string, string>;
875
886
  bodyOverrides?: Record<string, unknown>;
887
+ /**
888
+ * Opt-in leaked-dialect safe-parse (theokit#58 follow-up). When `true`, a chat_completions finish
889
+ * with ZERO native `tool_calls` has its assistant content scanned for the Hermes
890
+ * `<function=…></tool_call>` dialect and any recovered calls are surfaced as real `tool_calls`.
891
+ * Default off — only enable for routes/models known to leak (e.g. a qwen3-coder profile variant).
892
+ */
893
+ extractToolCallsFromContent?: boolean;
876
894
  }
877
895
 
878
896
  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) {
@@ -10967,6 +10996,14 @@ var OpenAIClient = class {
10967
10996
  name = "openai";
10968
10997
  baseUrl;
10969
10998
  fetchImpl;
10999
+ /**
11000
+ * Whether this client recovers leaked Hermes tool-call dialect from assistant
11001
+ * text (theokit#58 follow-up). Observability for the route→client wiring of
11002
+ * `extractToolCallsFromContent`. @internal
11003
+ */
11004
+ get recoversLeakedToolCalls() {
11005
+ return this.options.extractToolCallsFromContent ?? false;
11006
+ }
10970
11007
  // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: HTTP+SSE handshake + accumulator is intentionally one block
10971
11008
  async *stream(request, signal) {
10972
11009
  const headers = {
@@ -11017,7 +11054,10 @@ var OpenAIClient = class {
11017
11054
  endpoint: "/v1/chat/completions"
11018
11055
  });
11019
11056
  }
11020
- const accumulator = new OpenAIStreamAccumulator();
11057
+ const accumulator = new OpenAIStreamAccumulator(
11058
+ this.options.extractToolCallsFromContent ?? false,
11059
+ providerId
11060
+ );
11021
11061
  for await (const record of parseSseStream(response.body, signal)) {
11022
11062
  if (record.data === "[DONE]") break;
11023
11063
  let chunk;
@@ -11043,6 +11083,16 @@ var OpenAIClient = class {
11043
11083
  }
11044
11084
  };
11045
11085
  var OpenAIStreamAccumulator = class {
11086
+ /**
11087
+ * @param extractFromContent opt-in leaked-dialect safe-parse (theokit#58). Default false.
11088
+ * @param providerName provider id, used only to label the recovery log line.
11089
+ */
11090
+ constructor(extractFromContent = false, providerName = "openai") {
11091
+ this.extractFromContent = extractFromContent;
11092
+ this.providerName = providerName;
11093
+ }
11094
+ extractFromContent;
11095
+ providerName;
11046
11096
  text = "";
11047
11097
  stopReason = "end_turn";
11048
11098
  inputTokens;
@@ -11108,9 +11158,26 @@ var OpenAIStreamAccumulator = class {
11108
11158
  const input = parseToolArguments(call.args);
11109
11159
  toolCalls.push({ type: "tool_use", id: call.id, name: call.name, input });
11110
11160
  }
11161
+ let text = this.text;
11162
+ let stopReason = this.stopReason;
11163
+ if (this.extractFromContent && toolCalls.length === 0) {
11164
+ const recovered = extractHermesToolCalls(
11165
+ this.text,
11166
+ () => `hermes-${globalThis.crypto.randomUUID()}`
11167
+ );
11168
+ if (recovered.toolCalls.length > 0) {
11169
+ toolCalls.push(...recovered.toolCalls);
11170
+ text = recovered.residualText;
11171
+ stopReason = "tool_use";
11172
+ process.stderr.write(
11173
+ `[theokit-sdk] recovered ${recovered.toolCalls.length} leaked tool call(s) from assistant content (provider="${this.providerName}", names=${recovered.toolCalls.map((c) => c.name).join(",")})
11174
+ `
11175
+ );
11176
+ }
11177
+ }
11111
11178
  return makeLlmFinish({
11112
- stopReason: this.stopReason,
11113
- text: this.text,
11179
+ stopReason,
11180
+ text,
11114
11181
  toolCalls,
11115
11182
  inputTokens: this.inputTokens,
11116
11183
  outputTokens: this.outputTokens,
@@ -11587,8 +11654,9 @@ function buildChain(options) {
11587
11654
  return clients;
11588
11655
  }
11589
11656
  function buildClient(name, routerOptions) {
11590
- const profile = getProviderProfile(name);
11591
- if (profile === void 0) return void 0;
11657
+ const baseProfile = getProviderProfile(name);
11658
+ if (baseProfile === void 0) return void 0;
11659
+ const profile = routerOptions.extractToolCallsFromContent === true && baseProfile.extractToolCallsFromContent !== true ? { ...baseProfile, extractToolCallsFromContent: true } : baseProfile;
11592
11660
  const ambient = currentCredentialPool(name);
11593
11661
  if (ambient !== void 0) {
11594
11662
  return new PoolAwareLlmClient(ambient, (apiKey) => selectTransport(profile, apiKey));
@@ -11690,6 +11758,9 @@ function selectTransport(profile, apiKey) {
11690
11758
  const opts = { apiKey };
11691
11759
  opts.baseUrl = profile.baseUrl;
11692
11760
  opts.providerName = profile.name;
11761
+ if (profile.extractToolCallsFromContent === true) {
11762
+ opts.extractToolCallsFromContent = true;
11763
+ }
11693
11764
  if (profile.name === "openai" && process.env.OPENAI_ORGANIZATION !== void 0) {
11694
11765
  opts.organization = process.env.OPENAI_ORGANIZATION;
11695
11766
  }
@@ -11986,11 +12057,13 @@ function buildLoopInputs(options, runId, userText) {
11986
12057
  const fallback = options.agentOptions.providers?.fallback;
11987
12058
  const apiKeys = options.agentOptions.providers?.apiKeys;
11988
12059
  const credentialPoolStrategy = options.agentOptions.providers?.credentialPoolStrategy;
12060
+ const extractToolCallsFromContent = options.agentOptions.providers?.routes?.[0]?.extractToolCallsFromContent;
11989
12061
  const chain = resolveProviderChain({
11990
12062
  primary,
11991
12063
  ...fallback !== void 0 ? { fallback } : {},
11992
12064
  ...apiKeys !== void 0 ? { apiKeys } : {},
11993
- ...credentialPoolStrategy !== void 0 ? { credentialPoolStrategy } : {}
12065
+ ...credentialPoolStrategy !== void 0 ? { credentialPoolStrategy } : {},
12066
+ ...extractToolCallsFromContent === true ? { extractToolCallsFromContent: true } : {}
11994
12067
  });
11995
12068
  const llm = chain.length === 1 ? chain[0] : new FallbackLlmClient(chain);
11996
12069
  return {