@quilltap/plugin-utils 2.3.0 → 2.4.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,13 @@
2
2
 
3
3
  All notable changes to @quilltap/plugin-utils will be documented in this file.
4
4
 
5
+ ## [2.4.0] - 2026-08-19
6
+
7
+ ### Added
8
+
9
+ - `collapseLeadingSystemMessages(messages)` — folds a run of consecutive leading `system` messages into one, joining the contents with a blank line. Arrays with fewer than two leading system messages come back untouched (same reference).
10
+ - `OpenAICompatibleProvider` gained `acceptsRepeatedSystemMessages` (default `true`, so every existing subclass is byte-identical on the wire) and applies it through `applySystemMessagePolicy` in both the streaming and non-streaming body builds. A subclass pointing at a local runtime sets it `false`: the model's own chat template is what runs there, and the Qwen family — plus several Llama- and Gemma-derived templates — `raise_exception` on any system message after index 0, rejecting the whole request. Quilltap emits up to three leading system blocks so its cache breakpoints survive, so every non-opening turn against such a model failed outright. (Quilltap bug 82.)
11
+
5
12
  ## [2.3.0] - 2026-08-15
6
13
 
7
14
  ### Added
package/README.md CHANGED
@@ -150,6 +150,37 @@ This gives you a complete `LLMProvider` implementation with:
150
150
  npm install openai
151
151
  ```
152
152
 
153
+ ### Profile Parameters
154
+
155
+ A connection profile's `parameters` blob is free-form JSON, handed to the
156
+ provider as `LLMParams.profileParameters`. A provider that never reads it drops
157
+ every setting the user saved, in silence — so forwarding is opt-in but explicit,
158
+ and always allow-listed:
159
+
160
+ ```typescript
161
+ import { applyProfileParameters } from '@quilltap/plugin-utils';
162
+
163
+ const MY_ALLOWLIST = ['top_k', 'min_p', 'reasoning_effort'] as const;
164
+
165
+ applyProfileParameters(body, params, MY_ALLOWLIST, (key, value, callParams, target) => {
166
+ // Return the value to send, or `undefined` to omit the key.
167
+ // Write into `target` to send it under a different key entirely.
168
+ return value;
169
+ });
170
+ ```
171
+
172
+ `undefined`, `null` and the empty string omit the key — the empty string is what
173
+ the schema-driven profile editor stores for "use the model default". Never
174
+ spread the blob: `model`, `messages`, `stream`, `stream_options` and `tools`
175
+ must stay unreachable from a profile.
176
+
177
+ Subclasses of `OpenAICompatibleProvider` get this by declaring
178
+ `profileParamAllowlist` (empty by default, so a subclass that declares nothing
179
+ is byte-identical on the wire) and optionally overriding `normalizeProfileParam`;
180
+ providers that implement `TextProvider` directly call the function themselves.
181
+ The keys must match whatever the plugin's `getProviderOptionsSchema()` renders,
182
+ or a field the editor draws goes nowhere.
183
+
153
184
  ### System Prompt Plugin Utilities
154
185
 
155
186
  Create system prompt plugins that provide character prompt templates from `.md` files:
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, ProfileParamNormalizer, applyProfileParameters, buildRequestAbortSignal, buildSdkClientOptions, buildSdkRequestOptions, resolveRequestTimeoutMs } from './providers/index.mjs';
4
+ export { DEFAULT_REQUEST_TIMEOUT_MS, OpenAICompatibleProvider, OpenAICompatibleProviderConfig, ProfileParamNormalizer, applyProfileParameters, buildRequestAbortSignal, buildSdkClientOptions, buildSdkRequestOptions, collapseLeadingSystemMessages, 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.3.0";
127
+ declare const PLUGIN_UTILS_VERSION = "2.4.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, ProfileParamNormalizer, applyProfileParameters, buildRequestAbortSignal, buildSdkClientOptions, buildSdkRequestOptions, resolveRequestTimeoutMs } from './providers/index.js';
4
+ export { DEFAULT_REQUEST_TIMEOUT_MS, OpenAICompatibleProvider, OpenAICompatibleProviderConfig, ProfileParamNormalizer, applyProfileParameters, buildRequestAbortSignal, buildSdkClientOptions, buildSdkRequestOptions, collapseLeadingSystemMessages, 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.3.0";
127
+ declare const PLUGIN_UTILS_VERSION = "2.4.0";
128
128
 
129
129
  export { BUILTIN_TOOL_NAMES, PLUGIN_UTILS_VERSION, __clearQuilltapVersion, __injectQuilltapVersion, getBuiltinToolNames, getQuilltapUserAgent, getQuilltapVersion };
package/dist/index.js CHANGED
@@ -43,6 +43,7 @@ __export(src_exports, {
43
43
  buildRequestAbortSignal: () => buildRequestAbortSignal,
44
44
  buildSdkClientOptions: () => buildSdkClientOptions,
45
45
  buildSdkRequestOptions: () => buildSdkRequestOptions,
46
+ collapseLeadingSystemMessages: () => collapseLeadingSystemMessages,
46
47
  convertFromAnthropicFormat: () => convertFromAnthropicFormat,
47
48
  convertFromGoogleFormat: () => convertFromGoogleFormat,
48
49
  convertOpenAIToAnthropicFormat: () => convertOpenAIToAnthropicFormat,
@@ -443,6 +444,21 @@ function applyProfileParameters(body, params, allowlist, normalize) {
443
444
  }
444
445
  }
445
446
 
447
+ // src/providers/system-messages.ts
448
+ function collapseLeadingSystemMessages(messages) {
449
+ let runLength = 0;
450
+ while (runLength < messages.length && messages[runLength].role === "system") {
451
+ runLength++;
452
+ }
453
+ if (runLength < 2) return messages;
454
+ const run = messages.slice(0, runLength);
455
+ const merged = {
456
+ ...run[0],
457
+ content: run.map((m) => m.content ?? "").filter((c) => c.length > 0).join("\n\n")
458
+ };
459
+ return [merged, ...messages.slice(runLength)];
460
+ }
461
+
446
462
  // src/providers/openai-compatible.ts
447
463
  var DEFAULT_MAX_RETRIES = 2;
448
464
  var OpenAICompatibleProvider = class {
@@ -458,6 +474,18 @@ var OpenAICompatibleProvider = class {
458
474
  this.supportedMimeTypes = [];
459
475
  /** Web search is not supported */
460
476
  this.supportsWebSearch = false;
477
+ /**
478
+ * Whether the endpoint tolerates more than one leading `system` message.
479
+ *
480
+ * True for every hosted service — and so the default, which keeps the
481
+ * outbound bytes of OpenRouter, Z.AI and every other subclass exactly as they
482
+ * were. A subclass that points at a local runtime applying the *model's* chat
483
+ * template sets this false: several template families reject a system message
484
+ * anywhere but index 0, and Quilltap's context builder deliberately emits up
485
+ * to three of them so its cache breakpoints survive. See Bug 82 and
486
+ * {@link collapseLeadingSystemMessages}.
487
+ */
488
+ this.acceptsRepeatedSystemMessages = true;
461
489
  /**
462
490
  * Profile-parameter keys this provider forwards from
463
491
  * `LLMParams.profileParameters` onto the request body.
@@ -485,6 +513,14 @@ var OpenAICompatibleProvider = class {
485
513
  }
486
514
  this.logger = createPluginLogger(`${this.providerName}Provider`);
487
515
  }
516
+ /**
517
+ * Apply {@link acceptsRepeatedSystemMessages} to a mapped wire-message array.
518
+ * Called by both body builds so the streaming and non-streaming shapes cannot
519
+ * drift apart.
520
+ */
521
+ applySystemMessagePolicy(messages) {
522
+ return this.acceptsRepeatedSystemMessages ? messages : collapseLeadingSystemMessages(messages);
523
+ }
488
524
  /**
489
525
  * Collects attachment failures for messages with attachments.
490
526
  * Since attachments are not supported, all attachments are marked as failed.
@@ -613,7 +649,7 @@ var OpenAICompatibleProvider = class {
613
649
  this.validateApiKeyRequirement(apiKey);
614
650
  const attachmentResults = this.collectAttachmentFailures(params);
615
651
  const client = this.createClient(apiKey);
616
- const messages = params.messages.filter((m) => {
652
+ const mappedMessages = params.messages.filter((m) => {
617
653
  if (m.role === "tool" && !m.toolCallId) return false;
618
654
  return true;
619
655
  }).map((m) => {
@@ -640,6 +676,7 @@ var OpenAICompatibleProvider = class {
640
676
  content: m.content
641
677
  };
642
678
  });
679
+ const messages = this.applySystemMessagePolicy(mappedMessages);
643
680
  const body = {
644
681
  model: params.model,
645
682
  messages,
@@ -692,7 +729,7 @@ var OpenAICompatibleProvider = class {
692
729
  this.validateApiKeyRequirement(apiKey);
693
730
  const attachmentResults = this.collectAttachmentFailures(params);
694
731
  const client = this.createClient(apiKey);
695
- const messages = params.messages.filter((m) => {
732
+ const mappedMessages = params.messages.filter((m) => {
696
733
  if (m.role === "tool" && !m.toolCallId) return false;
697
734
  return true;
698
735
  }).map((m) => {
@@ -719,6 +756,7 @@ var OpenAICompatibleProvider = class {
719
756
  content: m.content
720
757
  };
721
758
  });
759
+ const messages = this.applySystemMessagePolicy(mappedMessages);
722
760
  const body = {
723
761
  model: params.model,
724
762
  messages,
@@ -1100,7 +1138,7 @@ function rewriteLocalhostUrl(url) {
1100
1138
  }
1101
1139
 
1102
1140
  // src/version.generated.ts
1103
- var PLUGIN_UTILS_VERSION = "2.3.0";
1141
+ var PLUGIN_UTILS_VERSION = "2.4.0";
1104
1142
  // Annotate the CommonJS export names for ESM import in node:
1105
1143
  0 && (module.exports = {
1106
1144
  BUILTIN_TOOL_NAMES,
@@ -1116,6 +1154,7 @@ var PLUGIN_UTILS_VERSION = "2.3.0";
1116
1154
  buildRequestAbortSignal,
1117
1155
  buildSdkClientOptions,
1118
1156
  buildSdkRequestOptions,
1157
+ collapseLeadingSystemMessages,
1119
1158
  convertFromAnthropicFormat,
1120
1159
  convertFromGoogleFormat,
1121
1160
  convertOpenAIToAnthropicFormat,