@quilltap/plugin-utils 2.3.0 → 2.5.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/dist/index.mjs CHANGED
@@ -367,6 +367,21 @@ function applyProfileParameters(body, params, allowlist, normalize) {
367
367
  }
368
368
  }
369
369
 
370
+ // src/providers/system-messages.ts
371
+ function collapseLeadingSystemMessages(messages) {
372
+ let runLength = 0;
373
+ while (runLength < messages.length && messages[runLength].role === "system") {
374
+ runLength++;
375
+ }
376
+ if (runLength < 2) return messages;
377
+ const run = messages.slice(0, runLength);
378
+ const merged = {
379
+ ...run[0],
380
+ content: run.map((m) => m.content ?? "").filter((c) => c.length > 0).join("\n\n")
381
+ };
382
+ return [merged, ...messages.slice(runLength)];
383
+ }
384
+
370
385
  // src/providers/openai-compatible.ts
371
386
  var DEFAULT_MAX_RETRIES = 2;
372
387
  var OpenAICompatibleProvider = class {
@@ -382,6 +397,18 @@ var OpenAICompatibleProvider = class {
382
397
  this.supportedMimeTypes = [];
383
398
  /** Web search is not supported */
384
399
  this.supportsWebSearch = false;
400
+ /**
401
+ * Whether the endpoint tolerates more than one leading `system` message.
402
+ *
403
+ * True for every hosted service — and so the default, which keeps the
404
+ * outbound bytes of OpenRouter, Z.AI and every other subclass exactly as they
405
+ * were. A subclass that points at a local runtime applying the *model's* chat
406
+ * template sets this false: several template families reject a system message
407
+ * anywhere but index 0, and Quilltap's context builder deliberately emits up
408
+ * to three of them so its cache breakpoints survive. See Bug 82 and
409
+ * {@link collapseLeadingSystemMessages}.
410
+ */
411
+ this.acceptsRepeatedSystemMessages = true;
385
412
  /**
386
413
  * Profile-parameter keys this provider forwards from
387
414
  * `LLMParams.profileParameters` onto the request body.
@@ -409,6 +436,14 @@ var OpenAICompatibleProvider = class {
409
436
  }
410
437
  this.logger = createPluginLogger(`${this.providerName}Provider`);
411
438
  }
439
+ /**
440
+ * Apply {@link acceptsRepeatedSystemMessages} to a mapped wire-message array.
441
+ * Called by both body builds so the streaming and non-streaming shapes cannot
442
+ * drift apart.
443
+ */
444
+ applySystemMessagePolicy(messages) {
445
+ return this.acceptsRepeatedSystemMessages ? messages : collapseLeadingSystemMessages(messages);
446
+ }
412
447
  /**
413
448
  * Collects attachment failures for messages with attachments.
414
449
  * Since attachments are not supported, all attachments are marked as failed.
@@ -527,17 +562,22 @@ var OpenAICompatibleProvider = class {
527
562
  return out;
528
563
  }
529
564
  /**
530
- * Sends a message and returns the complete response.
565
+ * Build the Chat Completions request body — the ONE build both
566
+ * {@link sendMessage} and {@link streamMessage} call, so the streaming and
567
+ * non-streaming shapes cannot drift apart. Maps Quilltap messages to the
568
+ * OpenAI wire format (tool results and assistant tool calls included),
569
+ * applies {@link applySystemMessagePolicy}, assembles the body literal, and
570
+ * finishes with {@link applyProfileParams}.
531
571
  *
532
- * @param params - LLM parameters including messages, model, and settings
533
- * @param apiKey - API key for authentication
534
- * @returns Complete LLM response with content and usage statistics
572
+ * Subclasses that keep the base class's send/stream loops can override this
573
+ * to reshape the body in one place.
574
+ *
575
+ * @param params - LLM parameters for the request
576
+ * @param stream - Whether the body is for a streaming request; adds
577
+ * `stream: true` and `stream_options: { include_usage: true }`
535
578
  */
536
- async sendMessage(params, apiKey) {
537
- this.validateApiKeyRequirement(apiKey);
538
- const attachmentResults = this.collectAttachmentFailures(params);
539
- const client = this.createClient(apiKey);
540
- const messages = params.messages.filter((m) => {
579
+ buildRequestBody(params, stream) {
580
+ const mappedMessages = params.messages.filter((m) => {
541
581
  if (m.role === "tool" && !m.toolCallId) return false;
542
582
  return true;
543
583
  }).map((m) => {
@@ -564,6 +604,7 @@ var OpenAICompatibleProvider = class {
564
604
  content: m.content
565
605
  };
566
606
  });
607
+ const messages = this.applySystemMessagePolicy(mappedMessages);
567
608
  const body = {
568
609
  model: params.model,
569
610
  messages,
@@ -571,12 +612,30 @@ var OpenAICompatibleProvider = class {
571
612
  max_tokens: params.maxTokens ?? 4096,
572
613
  top_p: params.topP ?? 1,
573
614
  stop: params.stop,
615
+ // Spread here — not appended after the literal — so the streaming keys
616
+ // keep their historical position between `stop` and `user` and the
617
+ // serialized body stays byte-identical.
618
+ ...stream ? { stream: true, stream_options: { include_usage: true } } : {},
574
619
  ...params.cacheKey ? { user: params.cacheKey } : {},
575
620
  // Tools arrive only once the runtime gate (the profile's "Allow tool
576
621
  // use") has passed, so no separate guard is needed here.
577
622
  ...params.tools && params.tools.length > 0 ? { tools: params.tools, tool_choice: params.toolChoice ?? "auto" } : {}
578
623
  };
579
624
  this.applyProfileParams(body, params);
625
+ return body;
626
+ }
627
+ /**
628
+ * Sends a message and returns the complete response.
629
+ *
630
+ * @param params - LLM parameters including messages, model, and settings
631
+ * @param apiKey - API key for authentication
632
+ * @returns Complete LLM response with content and usage statistics
633
+ */
634
+ async sendMessage(params, apiKey) {
635
+ this.validateApiKeyRequirement(apiKey);
636
+ const attachmentResults = this.collectAttachmentFailures(params);
637
+ const client = this.createClient(apiKey);
638
+ const body = this.buildRequestBody(params, false);
580
639
  try {
581
640
  const response = await client.chat.completions.create(
582
641
  body,
@@ -616,46 +675,7 @@ var OpenAICompatibleProvider = class {
616
675
  this.validateApiKeyRequirement(apiKey);
617
676
  const attachmentResults = this.collectAttachmentFailures(params);
618
677
  const client = this.createClient(apiKey);
619
- const messages = params.messages.filter((m) => {
620
- if (m.role === "tool" && !m.toolCallId) return false;
621
- return true;
622
- }).map((m) => {
623
- if (m.role === "tool" && m.toolCallId) {
624
- return {
625
- role: "tool",
626
- tool_call_id: m.toolCallId,
627
- content: m.content
628
- };
629
- }
630
- if (m.role === "assistant" && m.toolCalls && m.toolCalls.length > 0) {
631
- return {
632
- role: "assistant",
633
- content: m.content || null,
634
- tool_calls: m.toolCalls.map((tc) => ({
635
- id: tc.id,
636
- type: tc.type,
637
- function: tc.function
638
- }))
639
- };
640
- }
641
- return {
642
- role: m.role,
643
- content: m.content
644
- };
645
- });
646
- const body = {
647
- model: params.model,
648
- messages,
649
- temperature: params.temperature ?? 0.7,
650
- max_tokens: params.maxTokens ?? 4096,
651
- top_p: params.topP ?? 1,
652
- stop: params.stop,
653
- stream: true,
654
- stream_options: { include_usage: true },
655
- ...params.cacheKey ? { user: params.cacheKey } : {},
656
- ...params.tools && params.tools.length > 0 ? { tools: params.tools, tool_choice: params.toolChoice ?? "auto" } : {}
657
- };
658
- this.applyProfileParams(body, params);
678
+ const body = this.buildRequestBody(params, true);
659
679
  try {
660
680
  const stream = await client.chat.completions.create(
661
681
  body,
@@ -1024,7 +1044,7 @@ function rewriteLocalhostUrl(url) {
1024
1044
  }
1025
1045
 
1026
1046
  // src/version.generated.ts
1027
- var PLUGIN_UTILS_VERSION = "2.3.0";
1047
+ var PLUGIN_UTILS_VERSION = "2.5.0";
1028
1048
  export {
1029
1049
  BUILTIN_TOOL_NAMES,
1030
1050
  DEFAULT_REQUEST_TIMEOUT_MS,
@@ -1039,6 +1059,7 @@ export {
1039
1059
  buildRequestAbortSignal,
1040
1060
  buildSdkClientOptions,
1041
1061
  buildSdkRequestOptions,
1062
+ collapseLeadingSystemMessages,
1042
1063
  convertFromAnthropicFormat,
1043
1064
  convertFromGoogleFormat,
1044
1065
  convertOpenAIToAnthropicFormat,