@quilltap/plugin-utils 2.2.19 → 2.3.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
@@ -2,6 +2,14 @@
2
2
 
3
3
  All notable changes to @quilltap/plugin-utils will be documented in this file.
4
4
 
5
+ ## [2.3.0] - 2026-08-15
6
+
7
+ ### Added
8
+
9
+ - `applyProfileParameters(body, params, allowlist, normalize?)` — the one mechanism providers use to forward a connection profile's free-form `parameters` blob onto a request body. Allow-listed (never spread, so `model` / `messages` / `stream` / `tools` stay unreachable from a profile); `undefined`, `null` and the empty string omit the key. Exported as a free function rather than a base-class method because only one plugin extends `OpenAICompatibleProvider` — Z.AI, OpenRouter and Ollama implement their providers directly and reach it by composition.
10
+ - `OpenAICompatibleProvider` gained `profileParamAllowlist` (empty by default, so every existing subclass is byte-identical on the wire until it opts in) and an overridable `normalizeProfileParam`, applied in both the streaming and non-streaming body builds.
11
+ - `OpenAICompatibleProvider` now sends `tools` / `tool_choice` when the caller supplies tools, and parses `tool_calls` back — from `choice.message.tool_calls` on the non-streaming path, and by accumulating index-keyed `delta.tool_calls` fragments while streaming. Previously it could never call a tool.
12
+
5
13
  ## [2.2.19] - 2026-08-03
6
14
 
7
15
  ### Fixed
package/dist/index.d.mts CHANGED
@@ -1,7 +1,7 @@
1
1
  export { AnthropicToolDefinition, CharacterPluginDataEntry, CharacterPluginDataMap, GoogleToolDefinition, LogContext, LogLevel, OpenAIToolDefinition, PluginLogger, ToolCall, ToolCallRequest, ToolFormatOptions, ToolResult, UniversalTool, createConsoleLogger, createNoopLogger } from '@quilltap/plugin-types';
2
2
  export { T as ToolCallFormat, a as ToolConvertTarget, b as applyDescriptionLimit, c as convertFromAnthropicFormat, d as convertFromGoogleFormat, e as convertOpenAIToAnthropicFormat, f as convertOpenAIToGoogleFormat, g as convertToAnthropicFormat, h as convertToGoogleFormat, i as convertToolTo, j as convertToolsTo, k as detectToolCallFormat, l as hasToolCalls, p as parseAnthropicToolCalls, m as parseGoogleToolCalls, n as parseOpenAIToolCalls, o as parseToolCalls } from './converters-BPYo8xdH.mjs';
3
3
  export { PluginLoggerWithChild, __clearCoreLoggerFactory, __injectCoreLoggerFactory, createPluginLogger, getLogLevelFromEnv, hasCoreLogger } from './logging/index.mjs';
4
- export { DEFAULT_REQUEST_TIMEOUT_MS, OpenAICompatibleProvider, OpenAICompatibleProviderConfig, buildRequestAbortSignal, buildSdkClientOptions, buildSdkRequestOptions, resolveRequestTimeoutMs } from './providers/index.mjs';
4
+ export { DEFAULT_REQUEST_TIMEOUT_MS, OpenAICompatibleProvider, OpenAICompatibleProviderConfig, ProfileParamNormalizer, applyProfileParameters, buildRequestAbortSignal, buildSdkClientOptions, buildSdkRequestOptions, resolveRequestTimeoutMs } from './providers/index.mjs';
5
5
  export { CreateSystemPromptPluginOptions, createSystemPromptPlugin, validateSystemPromptPlugin } from './system-prompts/index.mjs';
6
6
  export { isVMEnvironment, resolveHostGateway, rewriteLocalhostUrl } from './host-rewrite.mjs';
7
7
  import 'openai';
@@ -124,6 +124,6 @@ declare function getQuilltapUserAgent(): string;
124
124
  * Version of the plugin-utils package.
125
125
  * Can be used at runtime to check compatibility.
126
126
  */
127
- declare const PLUGIN_UTILS_VERSION = "2.2.19";
127
+ declare const PLUGIN_UTILS_VERSION = "2.3.0";
128
128
 
129
129
  export { BUILTIN_TOOL_NAMES, PLUGIN_UTILS_VERSION, __clearQuilltapVersion, __injectQuilltapVersion, getBuiltinToolNames, getQuilltapUserAgent, getQuilltapVersion };
package/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  export { AnthropicToolDefinition, CharacterPluginDataEntry, CharacterPluginDataMap, GoogleToolDefinition, LogContext, LogLevel, OpenAIToolDefinition, PluginLogger, ToolCall, ToolCallRequest, ToolFormatOptions, ToolResult, UniversalTool, createConsoleLogger, createNoopLogger } from '@quilltap/plugin-types';
2
2
  export { T as ToolCallFormat, a as ToolConvertTarget, b as applyDescriptionLimit, c as convertFromAnthropicFormat, d as convertFromGoogleFormat, e as convertOpenAIToAnthropicFormat, f as convertOpenAIToGoogleFormat, g as convertToAnthropicFormat, h as convertToGoogleFormat, i as convertToolTo, j as convertToolsTo, k as detectToolCallFormat, l as hasToolCalls, p as parseAnthropicToolCalls, m as parseGoogleToolCalls, n as parseOpenAIToolCalls, o as parseToolCalls } from './converters-BPYo8xdH.js';
3
3
  export { PluginLoggerWithChild, __clearCoreLoggerFactory, __injectCoreLoggerFactory, createPluginLogger, getLogLevelFromEnv, hasCoreLogger } from './logging/index.js';
4
- export { DEFAULT_REQUEST_TIMEOUT_MS, OpenAICompatibleProvider, OpenAICompatibleProviderConfig, buildRequestAbortSignal, buildSdkClientOptions, buildSdkRequestOptions, resolveRequestTimeoutMs } from './providers/index.js';
4
+ export { DEFAULT_REQUEST_TIMEOUT_MS, OpenAICompatibleProvider, OpenAICompatibleProviderConfig, ProfileParamNormalizer, applyProfileParameters, buildRequestAbortSignal, buildSdkClientOptions, buildSdkRequestOptions, resolveRequestTimeoutMs } from './providers/index.js';
5
5
  export { CreateSystemPromptPluginOptions, createSystemPromptPlugin, validateSystemPromptPlugin } from './system-prompts/index.js';
6
6
  export { isVMEnvironment, resolveHostGateway, rewriteLocalhostUrl } from './host-rewrite.js';
7
7
  import 'openai';
@@ -124,6 +124,6 @@ declare function getQuilltapUserAgent(): string;
124
124
  * Version of the plugin-utils package.
125
125
  * Can be used at runtime to check compatibility.
126
126
  */
127
- declare const PLUGIN_UTILS_VERSION = "2.2.19";
127
+ declare const PLUGIN_UTILS_VERSION = "2.3.0";
128
128
 
129
129
  export { BUILTIN_TOOL_NAMES, PLUGIN_UTILS_VERSION, __clearQuilltapVersion, __injectQuilltapVersion, getBuiltinToolNames, getQuilltapUserAgent, getQuilltapVersion };
package/dist/index.js CHANGED
@@ -39,6 +39,7 @@ __export(src_exports, {
39
39
  __injectCoreLoggerFactory: () => __injectCoreLoggerFactory,
40
40
  __injectQuilltapVersion: () => __injectQuilltapVersion,
41
41
  applyDescriptionLimit: () => applyDescriptionLimit,
42
+ applyProfileParameters: () => applyProfileParameters,
42
43
  buildRequestAbortSignal: () => buildRequestAbortSignal,
43
44
  buildSdkClientOptions: () => buildSdkClientOptions,
44
45
  buildSdkRequestOptions: () => buildSdkRequestOptions,
@@ -427,6 +428,21 @@ function buildRequestAbortSignal(params, defaultMs = DEFAULT_REQUEST_TIMEOUT_MS)
427
428
  return AbortSignal.timeout(resolveRequestTimeoutMs(params, defaultMs));
428
429
  }
429
430
 
431
+ // src/providers/profile-parameters.ts
432
+ function applyProfileParameters(body, params, allowlist, normalize) {
433
+ const profile = params.profileParameters;
434
+ if (!profile || typeof profile !== "object") return;
435
+ const bag = profile;
436
+ for (const key of allowlist) {
437
+ const raw = bag[key];
438
+ if (raw === void 0 || raw === null) continue;
439
+ if (typeof raw === "string" && raw === "") continue;
440
+ const value = normalize ? normalize(key, raw, params, body) : raw;
441
+ if (value === void 0) continue;
442
+ body[key] = value;
443
+ }
444
+ }
445
+
430
446
  // src/providers/openai-compatible.ts
431
447
  var DEFAULT_MAX_RETRIES = 2;
432
448
  var OpenAICompatibleProvider = class {
@@ -442,6 +458,16 @@ var OpenAICompatibleProvider = class {
442
458
  this.supportedMimeTypes = [];
443
459
  /** Web search is not supported */
444
460
  this.supportsWebSearch = false;
461
+ /**
462
+ * Profile-parameter keys this provider forwards from
463
+ * `LLMParams.profileParameters` onto the request body.
464
+ *
465
+ * **Empty by default**, so a subclass that declares nothing sends exactly the
466
+ * bytes it sent before this mechanism existed. Subclasses opt in by
467
+ * overriding with their own allow-list; `model`, `messages`, `stream`,
468
+ * `stream_options` and `tools` must never appear in one.
469
+ */
470
+ this.profileParamAllowlist = [];
445
471
  if (typeof config === "string") {
446
472
  this.baseUrl = config;
447
473
  this.providerName = "OpenAICompatible";
@@ -529,6 +555,53 @@ var OpenAICompatibleProvider = class {
529
555
  buildRequestOptions(params) {
530
556
  return buildSdkRequestOptions(params);
531
557
  }
558
+ /**
559
+ * Per-key hook for allow-listed profile parameters. The default forwards the
560
+ * stored value verbatim.
561
+ *
562
+ * Override to reshape a value (the editor stores flat strings where some APIs
563
+ * want objects), to gate a key on the model, or — by writing into `body` and
564
+ * returning `undefined` — to send it under a different key entirely.
565
+ */
566
+ normalizeProfileParam(_key, value, _params, _body) {
567
+ return value;
568
+ }
569
+ /**
570
+ * Apply {@link profileParamAllowlist} to a request body under construction.
571
+ * Called by both body builds so the streaming and non-streaming shapes cannot
572
+ * drift apart.
573
+ */
574
+ applyProfileParams(body, params) {
575
+ applyProfileParameters(
576
+ body,
577
+ params,
578
+ this.profileParamAllowlist,
579
+ (key, value, callParams, target) => this.normalizeProfileParam(key, value, callParams, target)
580
+ );
581
+ }
582
+ /**
583
+ * Normalize an OpenAI-shaped `tool_calls` array into Quilltap's `ToolCall`
584
+ * shape, tolerating servers that hand back already-parsed argument objects
585
+ * where the OpenAI wire format specifies a JSON string.
586
+ */
587
+ normalizeToolCalls(toolCalls) {
588
+ if (!toolCalls || toolCalls.length === 0) return [];
589
+ const out = [];
590
+ for (const raw of toolCalls) {
591
+ const tc = raw;
592
+ if (!tc?.function?.name) continue;
593
+ const args = tc.function.arguments;
594
+ out.push({
595
+ id: tc.id ?? "",
596
+ type: "function",
597
+ function: {
598
+ name: tc.function.name,
599
+ arguments: typeof args === "string" ? args : JSON.stringify(args ?? {})
600
+ }
601
+ });
602
+ }
603
+ return out;
604
+ }
532
605
  /**
533
606
  * Sends a message and returns the complete response.
534
607
  *
@@ -567,17 +640,26 @@ var OpenAICompatibleProvider = class {
567
640
  content: m.content
568
641
  };
569
642
  });
643
+ const body = {
644
+ model: params.model,
645
+ messages,
646
+ temperature: params.temperature ?? 0.7,
647
+ max_tokens: params.maxTokens ?? 4096,
648
+ top_p: params.topP ?? 1,
649
+ stop: params.stop,
650
+ ...params.cacheKey ? { user: params.cacheKey } : {},
651
+ // Tools arrive only once the runtime gate (the profile's "Allow tool
652
+ // use") has passed, so no separate guard is needed here.
653
+ ...params.tools && params.tools.length > 0 ? { tools: params.tools, tool_choice: params.toolChoice ?? "auto" } : {}
654
+ };
655
+ this.applyProfileParams(body, params);
570
656
  try {
571
- const response = await client.chat.completions.create({
572
- model: params.model,
573
- messages,
574
- temperature: params.temperature ?? 0.7,
575
- max_tokens: params.maxTokens ?? 4096,
576
- top_p: params.topP ?? 1,
577
- stop: params.stop,
578
- ...params.cacheKey ? { user: params.cacheKey } : {}
579
- }, this.buildRequestOptions(params));
657
+ const response = await client.chat.completions.create(
658
+ body,
659
+ this.buildRequestOptions(params)
660
+ );
580
661
  const choice = response.choices[0];
662
+ const toolCalls = this.normalizeToolCalls(choice.message.tool_calls);
581
663
  return {
582
664
  content: choice.message.content ?? "",
583
665
  finishReason: choice.finish_reason,
@@ -587,7 +669,8 @@ var OpenAICompatibleProvider = class {
587
669
  totalTokens: response.usage?.total_tokens ?? 0
588
670
  },
589
671
  raw: response,
590
- attachmentResults
672
+ attachmentResults,
673
+ ...toolCalls.length > 0 ? { toolCalls } : {}
591
674
  };
592
675
  } catch (error) {
593
676
  this.logger.error(
@@ -636,23 +719,32 @@ var OpenAICompatibleProvider = class {
636
719
  content: m.content
637
720
  };
638
721
  });
722
+ const body = {
723
+ model: params.model,
724
+ messages,
725
+ temperature: params.temperature ?? 0.7,
726
+ max_tokens: params.maxTokens ?? 4096,
727
+ top_p: params.topP ?? 1,
728
+ stop: params.stop,
729
+ stream: true,
730
+ stream_options: { include_usage: true },
731
+ ...params.cacheKey ? { user: params.cacheKey } : {},
732
+ ...params.tools && params.tools.length > 0 ? { tools: params.tools, tool_choice: params.toolChoice ?? "auto" } : {}
733
+ };
734
+ this.applyProfileParams(body, params);
639
735
  try {
640
- const stream = await client.chat.completions.create({
641
- model: params.model,
642
- messages,
643
- temperature: params.temperature ?? 0.7,
644
- max_tokens: params.maxTokens ?? 4096,
645
- top_p: params.topP ?? 1,
646
- stop: params.stop,
647
- stream: true,
648
- stream_options: { include_usage: true },
649
- ...params.cacheKey ? { user: params.cacheKey } : {}
650
- }, this.buildRequestOptions(params));
736
+ const stream = await client.chat.completions.create(
737
+ body,
738
+ this.buildRequestOptions(params)
739
+ );
651
740
  let chunkCount = 0;
652
741
  let accumulatedUsage = null;
742
+ const toolCallAccumulator = /* @__PURE__ */ new Map();
743
+ let finishReason = null;
653
744
  for await (const chunk of stream) {
654
745
  chunkCount++;
655
- const content = chunk.choices[0]?.delta?.content;
746
+ const choice = chunk.choices[0];
747
+ const content = choice?.delta?.content;
656
748
  const hasUsage = chunk.usage;
657
749
  if (hasUsage) {
658
750
  accumulatedUsage = {
@@ -661,6 +753,17 @@ var OpenAICompatibleProvider = class {
661
753
  total_tokens: chunk.usage?.total_tokens
662
754
  };
663
755
  }
756
+ if (choice?.delta?.tool_calls) {
757
+ for (const tcDelta of choice.delta.tool_calls) {
758
+ const idx = tcDelta.index ?? 0;
759
+ const existing = toolCallAccumulator.get(idx) ?? { id: "", name: "", arguments: "" };
760
+ if (tcDelta.id) existing.id = tcDelta.id;
761
+ if (tcDelta.function?.name) existing.name = tcDelta.function.name;
762
+ if (tcDelta.function?.arguments) existing.arguments += tcDelta.function.arguments;
763
+ toolCallAccumulator.set(idx, existing);
764
+ }
765
+ }
766
+ if (choice?.finish_reason) finishReason = choice.finish_reason;
664
767
  if (content) {
665
768
  yield {
666
769
  content,
@@ -668,6 +771,11 @@ var OpenAICompatibleProvider = class {
668
771
  };
669
772
  }
670
773
  }
774
+ const toolCalls = Array.from(toolCallAccumulator.values()).filter((tc) => tc.name).map((tc) => ({
775
+ id: tc.id,
776
+ type: "function",
777
+ function: { name: tc.name, arguments: tc.arguments }
778
+ }));
671
779
  yield {
672
780
  content: "",
673
781
  done: true,
@@ -676,7 +784,22 @@ var OpenAICompatibleProvider = class {
676
784
  completionTokens: accumulatedUsage.completion_tokens ?? 0,
677
785
  totalTokens: accumulatedUsage.total_tokens ?? 0
678
786
  } : void 0,
679
- attachmentResults
787
+ attachmentResults,
788
+ // Only synthesized when the stream actually carried tool calls: the
789
+ // host's tool detection reads `rawResponse`, and an empty one on every
790
+ // ordinary turn would be noise in the logs for no gain.
791
+ ...toolCalls.length > 0 ? {
792
+ toolCalls,
793
+ rawResponse: {
794
+ choices: [
795
+ {
796
+ index: 0,
797
+ message: { role: "assistant", content: "", tool_calls: toolCalls },
798
+ finish_reason: finishReason
799
+ }
800
+ ]
801
+ }
802
+ } : {}
680
803
  };
681
804
  } catch (error) {
682
805
  this.logger.error(
@@ -977,7 +1100,7 @@ function rewriteLocalhostUrl(url) {
977
1100
  }
978
1101
 
979
1102
  // src/version.generated.ts
980
- var PLUGIN_UTILS_VERSION = "2.2.19";
1103
+ var PLUGIN_UTILS_VERSION = "2.3.0";
981
1104
  // Annotate the CommonJS export names for ESM import in node:
982
1105
  0 && (module.exports = {
983
1106
  BUILTIN_TOOL_NAMES,
@@ -989,6 +1112,7 @@ var PLUGIN_UTILS_VERSION = "2.2.19";
989
1112
  __injectCoreLoggerFactory,
990
1113
  __injectQuilltapVersion,
991
1114
  applyDescriptionLimit,
1115
+ applyProfileParameters,
992
1116
  buildRequestAbortSignal,
993
1117
  buildSdkClientOptions,
994
1118
  buildSdkRequestOptions,