@quilltap/plugin-utils 2.2.20 → 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/dist/index.mjs CHANGED
@@ -352,6 +352,21 @@ function buildRequestAbortSignal(params, defaultMs = DEFAULT_REQUEST_TIMEOUT_MS)
352
352
  return AbortSignal.timeout(resolveRequestTimeoutMs(params, defaultMs));
353
353
  }
354
354
 
355
+ // src/providers/profile-parameters.ts
356
+ function applyProfileParameters(body, params, allowlist, normalize) {
357
+ const profile = params.profileParameters;
358
+ if (!profile || typeof profile !== "object") return;
359
+ const bag = profile;
360
+ for (const key of allowlist) {
361
+ const raw = bag[key];
362
+ if (raw === void 0 || raw === null) continue;
363
+ if (typeof raw === "string" && raw === "") continue;
364
+ const value = normalize ? normalize(key, raw, params, body) : raw;
365
+ if (value === void 0) continue;
366
+ body[key] = value;
367
+ }
368
+ }
369
+
355
370
  // src/providers/openai-compatible.ts
356
371
  var DEFAULT_MAX_RETRIES = 2;
357
372
  var OpenAICompatibleProvider = class {
@@ -367,6 +382,16 @@ var OpenAICompatibleProvider = class {
367
382
  this.supportedMimeTypes = [];
368
383
  /** Web search is not supported */
369
384
  this.supportsWebSearch = false;
385
+ /**
386
+ * Profile-parameter keys this provider forwards from
387
+ * `LLMParams.profileParameters` onto the request body.
388
+ *
389
+ * **Empty by default**, so a subclass that declares nothing sends exactly the
390
+ * bytes it sent before this mechanism existed. Subclasses opt in by
391
+ * overriding with their own allow-list; `model`, `messages`, `stream`,
392
+ * `stream_options` and `tools` must never appear in one.
393
+ */
394
+ this.profileParamAllowlist = [];
370
395
  if (typeof config === "string") {
371
396
  this.baseUrl = config;
372
397
  this.providerName = "OpenAICompatible";
@@ -454,6 +479,53 @@ var OpenAICompatibleProvider = class {
454
479
  buildRequestOptions(params) {
455
480
  return buildSdkRequestOptions(params);
456
481
  }
482
+ /**
483
+ * Per-key hook for allow-listed profile parameters. The default forwards the
484
+ * stored value verbatim.
485
+ *
486
+ * Override to reshape a value (the editor stores flat strings where some APIs
487
+ * want objects), to gate a key on the model, or — by writing into `body` and
488
+ * returning `undefined` — to send it under a different key entirely.
489
+ */
490
+ normalizeProfileParam(_key, value, _params, _body) {
491
+ return value;
492
+ }
493
+ /**
494
+ * Apply {@link profileParamAllowlist} to a request body under construction.
495
+ * Called by both body builds so the streaming and non-streaming shapes cannot
496
+ * drift apart.
497
+ */
498
+ applyProfileParams(body, params) {
499
+ applyProfileParameters(
500
+ body,
501
+ params,
502
+ this.profileParamAllowlist,
503
+ (key, value, callParams, target) => this.normalizeProfileParam(key, value, callParams, target)
504
+ );
505
+ }
506
+ /**
507
+ * Normalize an OpenAI-shaped `tool_calls` array into Quilltap's `ToolCall`
508
+ * shape, tolerating servers that hand back already-parsed argument objects
509
+ * where the OpenAI wire format specifies a JSON string.
510
+ */
511
+ normalizeToolCalls(toolCalls) {
512
+ if (!toolCalls || toolCalls.length === 0) return [];
513
+ const out = [];
514
+ for (const raw of toolCalls) {
515
+ const tc = raw;
516
+ if (!tc?.function?.name) continue;
517
+ const args = tc.function.arguments;
518
+ out.push({
519
+ id: tc.id ?? "",
520
+ type: "function",
521
+ function: {
522
+ name: tc.function.name,
523
+ arguments: typeof args === "string" ? args : JSON.stringify(args ?? {})
524
+ }
525
+ });
526
+ }
527
+ return out;
528
+ }
457
529
  /**
458
530
  * Sends a message and returns the complete response.
459
531
  *
@@ -492,17 +564,26 @@ var OpenAICompatibleProvider = class {
492
564
  content: m.content
493
565
  };
494
566
  });
567
+ const body = {
568
+ model: params.model,
569
+ messages,
570
+ temperature: params.temperature ?? 0.7,
571
+ max_tokens: params.maxTokens ?? 4096,
572
+ top_p: params.topP ?? 1,
573
+ stop: params.stop,
574
+ ...params.cacheKey ? { user: params.cacheKey } : {},
575
+ // Tools arrive only once the runtime gate (the profile's "Allow tool
576
+ // use") has passed, so no separate guard is needed here.
577
+ ...params.tools && params.tools.length > 0 ? { tools: params.tools, tool_choice: params.toolChoice ?? "auto" } : {}
578
+ };
579
+ this.applyProfileParams(body, params);
495
580
  try {
496
- const response = await client.chat.completions.create({
497
- model: params.model,
498
- messages,
499
- temperature: params.temperature ?? 0.7,
500
- max_tokens: params.maxTokens ?? 4096,
501
- top_p: params.topP ?? 1,
502
- stop: params.stop,
503
- ...params.cacheKey ? { user: params.cacheKey } : {}
504
- }, this.buildRequestOptions(params));
581
+ const response = await client.chat.completions.create(
582
+ body,
583
+ this.buildRequestOptions(params)
584
+ );
505
585
  const choice = response.choices[0];
586
+ const toolCalls = this.normalizeToolCalls(choice.message.tool_calls);
506
587
  return {
507
588
  content: choice.message.content ?? "",
508
589
  finishReason: choice.finish_reason,
@@ -512,7 +593,8 @@ var OpenAICompatibleProvider = class {
512
593
  totalTokens: response.usage?.total_tokens ?? 0
513
594
  },
514
595
  raw: response,
515
- attachmentResults
596
+ attachmentResults,
597
+ ...toolCalls.length > 0 ? { toolCalls } : {}
516
598
  };
517
599
  } catch (error) {
518
600
  this.logger.error(
@@ -561,23 +643,32 @@ var OpenAICompatibleProvider = class {
561
643
  content: m.content
562
644
  };
563
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);
564
659
  try {
565
- const stream = await client.chat.completions.create({
566
- model: params.model,
567
- messages,
568
- temperature: params.temperature ?? 0.7,
569
- max_tokens: params.maxTokens ?? 4096,
570
- top_p: params.topP ?? 1,
571
- stop: params.stop,
572
- stream: true,
573
- stream_options: { include_usage: true },
574
- ...params.cacheKey ? { user: params.cacheKey } : {}
575
- }, this.buildRequestOptions(params));
660
+ const stream = await client.chat.completions.create(
661
+ body,
662
+ this.buildRequestOptions(params)
663
+ );
576
664
  let chunkCount = 0;
577
665
  let accumulatedUsage = null;
666
+ const toolCallAccumulator = /* @__PURE__ */ new Map();
667
+ let finishReason = null;
578
668
  for await (const chunk of stream) {
579
669
  chunkCount++;
580
- const content = chunk.choices[0]?.delta?.content;
670
+ const choice = chunk.choices[0];
671
+ const content = choice?.delta?.content;
581
672
  const hasUsage = chunk.usage;
582
673
  if (hasUsage) {
583
674
  accumulatedUsage = {
@@ -586,6 +677,17 @@ var OpenAICompatibleProvider = class {
586
677
  total_tokens: chunk.usage?.total_tokens
587
678
  };
588
679
  }
680
+ if (choice?.delta?.tool_calls) {
681
+ for (const tcDelta of choice.delta.tool_calls) {
682
+ const idx = tcDelta.index ?? 0;
683
+ const existing = toolCallAccumulator.get(idx) ?? { id: "", name: "", arguments: "" };
684
+ if (tcDelta.id) existing.id = tcDelta.id;
685
+ if (tcDelta.function?.name) existing.name = tcDelta.function.name;
686
+ if (tcDelta.function?.arguments) existing.arguments += tcDelta.function.arguments;
687
+ toolCallAccumulator.set(idx, existing);
688
+ }
689
+ }
690
+ if (choice?.finish_reason) finishReason = choice.finish_reason;
589
691
  if (content) {
590
692
  yield {
591
693
  content,
@@ -593,6 +695,11 @@ var OpenAICompatibleProvider = class {
593
695
  };
594
696
  }
595
697
  }
698
+ const toolCalls = Array.from(toolCallAccumulator.values()).filter((tc) => tc.name).map((tc) => ({
699
+ id: tc.id,
700
+ type: "function",
701
+ function: { name: tc.name, arguments: tc.arguments }
702
+ }));
596
703
  yield {
597
704
  content: "",
598
705
  done: true,
@@ -601,7 +708,22 @@ var OpenAICompatibleProvider = class {
601
708
  completionTokens: accumulatedUsage.completion_tokens ?? 0,
602
709
  totalTokens: accumulatedUsage.total_tokens ?? 0
603
710
  } : void 0,
604
- attachmentResults
711
+ attachmentResults,
712
+ // Only synthesized when the stream actually carried tool calls: the
713
+ // host's tool detection reads `rawResponse`, and an empty one on every
714
+ // ordinary turn would be noise in the logs for no gain.
715
+ ...toolCalls.length > 0 ? {
716
+ toolCalls,
717
+ rawResponse: {
718
+ choices: [
719
+ {
720
+ index: 0,
721
+ message: { role: "assistant", content: "", tool_calls: toolCalls },
722
+ finish_reason: finishReason
723
+ }
724
+ ]
725
+ }
726
+ } : {}
605
727
  };
606
728
  } catch (error) {
607
729
  this.logger.error(
@@ -902,7 +1024,7 @@ function rewriteLocalhostUrl(url) {
902
1024
  }
903
1025
 
904
1026
  // src/version.generated.ts
905
- var PLUGIN_UTILS_VERSION = "2.2.20";
1027
+ var PLUGIN_UTILS_VERSION = "2.3.0";
906
1028
  export {
907
1029
  BUILTIN_TOOL_NAMES,
908
1030
  DEFAULT_REQUEST_TIMEOUT_MS,
@@ -913,6 +1035,7 @@ export {
913
1035
  __injectCoreLoggerFactory,
914
1036
  __injectQuilltapVersion,
915
1037
  applyDescriptionLimit,
1038
+ applyProfileParameters,
916
1039
  buildRequestAbortSignal,
917
1040
  buildSdkClientOptions,
918
1041
  buildSdkRequestOptions,