@quilltap/plugin-utils 2.2.20 → 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 +15 -0
- package/README.md +31 -0
- package/dist/index.d.mts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +189 -26
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +187 -26
- package/dist/index.mjs.map +1 -1
- package/dist/providers/index.d.mts +160 -1
- package/dist/providers/index.d.ts +160 -1
- package/dist/providers/index.js +188 -25
- package/dist/providers/index.js.map +1 -1
- package/dist/providers/index.mjs +186 -25
- package/dist/providers/index.mjs.map +1 -1
- package/package.json +2 -2
package/dist/index.mjs
CHANGED
|
@@ -352,6 +352,36 @@ 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
|
+
|
|
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
|
+
|
|
355
385
|
// src/providers/openai-compatible.ts
|
|
356
386
|
var DEFAULT_MAX_RETRIES = 2;
|
|
357
387
|
var OpenAICompatibleProvider = class {
|
|
@@ -367,6 +397,28 @@ var OpenAICompatibleProvider = class {
|
|
|
367
397
|
this.supportedMimeTypes = [];
|
|
368
398
|
/** Web search is not supported */
|
|
369
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;
|
|
412
|
+
/**
|
|
413
|
+
* Profile-parameter keys this provider forwards from
|
|
414
|
+
* `LLMParams.profileParameters` onto the request body.
|
|
415
|
+
*
|
|
416
|
+
* **Empty by default**, so a subclass that declares nothing sends exactly the
|
|
417
|
+
* bytes it sent before this mechanism existed. Subclasses opt in by
|
|
418
|
+
* overriding with their own allow-list; `model`, `messages`, `stream`,
|
|
419
|
+
* `stream_options` and `tools` must never appear in one.
|
|
420
|
+
*/
|
|
421
|
+
this.profileParamAllowlist = [];
|
|
370
422
|
if (typeof config === "string") {
|
|
371
423
|
this.baseUrl = config;
|
|
372
424
|
this.providerName = "OpenAICompatible";
|
|
@@ -384,6 +436,14 @@ var OpenAICompatibleProvider = class {
|
|
|
384
436
|
}
|
|
385
437
|
this.logger = createPluginLogger(`${this.providerName}Provider`);
|
|
386
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
|
+
}
|
|
387
447
|
/**
|
|
388
448
|
* Collects attachment failures for messages with attachments.
|
|
389
449
|
* Since attachments are not supported, all attachments are marked as failed.
|
|
@@ -454,6 +514,53 @@ var OpenAICompatibleProvider = class {
|
|
|
454
514
|
buildRequestOptions(params) {
|
|
455
515
|
return buildSdkRequestOptions(params);
|
|
456
516
|
}
|
|
517
|
+
/**
|
|
518
|
+
* Per-key hook for allow-listed profile parameters. The default forwards the
|
|
519
|
+
* stored value verbatim.
|
|
520
|
+
*
|
|
521
|
+
* Override to reshape a value (the editor stores flat strings where some APIs
|
|
522
|
+
* want objects), to gate a key on the model, or — by writing into `body` and
|
|
523
|
+
* returning `undefined` — to send it under a different key entirely.
|
|
524
|
+
*/
|
|
525
|
+
normalizeProfileParam(_key, value, _params, _body) {
|
|
526
|
+
return value;
|
|
527
|
+
}
|
|
528
|
+
/**
|
|
529
|
+
* Apply {@link profileParamAllowlist} to a request body under construction.
|
|
530
|
+
* Called by both body builds so the streaming and non-streaming shapes cannot
|
|
531
|
+
* drift apart.
|
|
532
|
+
*/
|
|
533
|
+
applyProfileParams(body, params) {
|
|
534
|
+
applyProfileParameters(
|
|
535
|
+
body,
|
|
536
|
+
params,
|
|
537
|
+
this.profileParamAllowlist,
|
|
538
|
+
(key, value, callParams, target) => this.normalizeProfileParam(key, value, callParams, target)
|
|
539
|
+
);
|
|
540
|
+
}
|
|
541
|
+
/**
|
|
542
|
+
* Normalize an OpenAI-shaped `tool_calls` array into Quilltap's `ToolCall`
|
|
543
|
+
* shape, tolerating servers that hand back already-parsed argument objects
|
|
544
|
+
* where the OpenAI wire format specifies a JSON string.
|
|
545
|
+
*/
|
|
546
|
+
normalizeToolCalls(toolCalls) {
|
|
547
|
+
if (!toolCalls || toolCalls.length === 0) return [];
|
|
548
|
+
const out = [];
|
|
549
|
+
for (const raw of toolCalls) {
|
|
550
|
+
const tc = raw;
|
|
551
|
+
if (!tc?.function?.name) continue;
|
|
552
|
+
const args = tc.function.arguments;
|
|
553
|
+
out.push({
|
|
554
|
+
id: tc.id ?? "",
|
|
555
|
+
type: "function",
|
|
556
|
+
function: {
|
|
557
|
+
name: tc.function.name,
|
|
558
|
+
arguments: typeof args === "string" ? args : JSON.stringify(args ?? {})
|
|
559
|
+
}
|
|
560
|
+
});
|
|
561
|
+
}
|
|
562
|
+
return out;
|
|
563
|
+
}
|
|
457
564
|
/**
|
|
458
565
|
* Sends a message and returns the complete response.
|
|
459
566
|
*
|
|
@@ -465,7 +572,7 @@ var OpenAICompatibleProvider = class {
|
|
|
465
572
|
this.validateApiKeyRequirement(apiKey);
|
|
466
573
|
const attachmentResults = this.collectAttachmentFailures(params);
|
|
467
574
|
const client = this.createClient(apiKey);
|
|
468
|
-
const
|
|
575
|
+
const mappedMessages = params.messages.filter((m) => {
|
|
469
576
|
if (m.role === "tool" && !m.toolCallId) return false;
|
|
470
577
|
return true;
|
|
471
578
|
}).map((m) => {
|
|
@@ -492,17 +599,27 @@ var OpenAICompatibleProvider = class {
|
|
|
492
599
|
content: m.content
|
|
493
600
|
};
|
|
494
601
|
});
|
|
602
|
+
const messages = this.applySystemMessagePolicy(mappedMessages);
|
|
603
|
+
const body = {
|
|
604
|
+
model: params.model,
|
|
605
|
+
messages,
|
|
606
|
+
temperature: params.temperature ?? 0.7,
|
|
607
|
+
max_tokens: params.maxTokens ?? 4096,
|
|
608
|
+
top_p: params.topP ?? 1,
|
|
609
|
+
stop: params.stop,
|
|
610
|
+
...params.cacheKey ? { user: params.cacheKey } : {},
|
|
611
|
+
// Tools arrive only once the runtime gate (the profile's "Allow tool
|
|
612
|
+
// use") has passed, so no separate guard is needed here.
|
|
613
|
+
...params.tools && params.tools.length > 0 ? { tools: params.tools, tool_choice: params.toolChoice ?? "auto" } : {}
|
|
614
|
+
};
|
|
615
|
+
this.applyProfileParams(body, params);
|
|
495
616
|
try {
|
|
496
|
-
const response = await client.chat.completions.create(
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
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));
|
|
617
|
+
const response = await client.chat.completions.create(
|
|
618
|
+
body,
|
|
619
|
+
this.buildRequestOptions(params)
|
|
620
|
+
);
|
|
505
621
|
const choice = response.choices[0];
|
|
622
|
+
const toolCalls = this.normalizeToolCalls(choice.message.tool_calls);
|
|
506
623
|
return {
|
|
507
624
|
content: choice.message.content ?? "",
|
|
508
625
|
finishReason: choice.finish_reason,
|
|
@@ -512,7 +629,8 @@ var OpenAICompatibleProvider = class {
|
|
|
512
629
|
totalTokens: response.usage?.total_tokens ?? 0
|
|
513
630
|
},
|
|
514
631
|
raw: response,
|
|
515
|
-
attachmentResults
|
|
632
|
+
attachmentResults,
|
|
633
|
+
...toolCalls.length > 0 ? { toolCalls } : {}
|
|
516
634
|
};
|
|
517
635
|
} catch (error) {
|
|
518
636
|
this.logger.error(
|
|
@@ -534,7 +652,7 @@ var OpenAICompatibleProvider = class {
|
|
|
534
652
|
this.validateApiKeyRequirement(apiKey);
|
|
535
653
|
const attachmentResults = this.collectAttachmentFailures(params);
|
|
536
654
|
const client = this.createClient(apiKey);
|
|
537
|
-
const
|
|
655
|
+
const mappedMessages = params.messages.filter((m) => {
|
|
538
656
|
if (m.role === "tool" && !m.toolCallId) return false;
|
|
539
657
|
return true;
|
|
540
658
|
}).map((m) => {
|
|
@@ -561,23 +679,33 @@ var OpenAICompatibleProvider = class {
|
|
|
561
679
|
content: m.content
|
|
562
680
|
};
|
|
563
681
|
});
|
|
682
|
+
const messages = this.applySystemMessagePolicy(mappedMessages);
|
|
683
|
+
const body = {
|
|
684
|
+
model: params.model,
|
|
685
|
+
messages,
|
|
686
|
+
temperature: params.temperature ?? 0.7,
|
|
687
|
+
max_tokens: params.maxTokens ?? 4096,
|
|
688
|
+
top_p: params.topP ?? 1,
|
|
689
|
+
stop: params.stop,
|
|
690
|
+
stream: true,
|
|
691
|
+
stream_options: { include_usage: true },
|
|
692
|
+
...params.cacheKey ? { user: params.cacheKey } : {},
|
|
693
|
+
...params.tools && params.tools.length > 0 ? { tools: params.tools, tool_choice: params.toolChoice ?? "auto" } : {}
|
|
694
|
+
};
|
|
695
|
+
this.applyProfileParams(body, params);
|
|
564
696
|
try {
|
|
565
|
-
const stream = await client.chat.completions.create(
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
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));
|
|
697
|
+
const stream = await client.chat.completions.create(
|
|
698
|
+
body,
|
|
699
|
+
this.buildRequestOptions(params)
|
|
700
|
+
);
|
|
576
701
|
let chunkCount = 0;
|
|
577
702
|
let accumulatedUsage = null;
|
|
703
|
+
const toolCallAccumulator = /* @__PURE__ */ new Map();
|
|
704
|
+
let finishReason = null;
|
|
578
705
|
for await (const chunk of stream) {
|
|
579
706
|
chunkCount++;
|
|
580
|
-
const
|
|
707
|
+
const choice = chunk.choices[0];
|
|
708
|
+
const content = choice?.delta?.content;
|
|
581
709
|
const hasUsage = chunk.usage;
|
|
582
710
|
if (hasUsage) {
|
|
583
711
|
accumulatedUsage = {
|
|
@@ -586,6 +714,17 @@ var OpenAICompatibleProvider = class {
|
|
|
586
714
|
total_tokens: chunk.usage?.total_tokens
|
|
587
715
|
};
|
|
588
716
|
}
|
|
717
|
+
if (choice?.delta?.tool_calls) {
|
|
718
|
+
for (const tcDelta of choice.delta.tool_calls) {
|
|
719
|
+
const idx = tcDelta.index ?? 0;
|
|
720
|
+
const existing = toolCallAccumulator.get(idx) ?? { id: "", name: "", arguments: "" };
|
|
721
|
+
if (tcDelta.id) existing.id = tcDelta.id;
|
|
722
|
+
if (tcDelta.function?.name) existing.name = tcDelta.function.name;
|
|
723
|
+
if (tcDelta.function?.arguments) existing.arguments += tcDelta.function.arguments;
|
|
724
|
+
toolCallAccumulator.set(idx, existing);
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
if (choice?.finish_reason) finishReason = choice.finish_reason;
|
|
589
728
|
if (content) {
|
|
590
729
|
yield {
|
|
591
730
|
content,
|
|
@@ -593,6 +732,11 @@ var OpenAICompatibleProvider = class {
|
|
|
593
732
|
};
|
|
594
733
|
}
|
|
595
734
|
}
|
|
735
|
+
const toolCalls = Array.from(toolCallAccumulator.values()).filter((tc) => tc.name).map((tc) => ({
|
|
736
|
+
id: tc.id,
|
|
737
|
+
type: "function",
|
|
738
|
+
function: { name: tc.name, arguments: tc.arguments }
|
|
739
|
+
}));
|
|
596
740
|
yield {
|
|
597
741
|
content: "",
|
|
598
742
|
done: true,
|
|
@@ -601,7 +745,22 @@ var OpenAICompatibleProvider = class {
|
|
|
601
745
|
completionTokens: accumulatedUsage.completion_tokens ?? 0,
|
|
602
746
|
totalTokens: accumulatedUsage.total_tokens ?? 0
|
|
603
747
|
} : void 0,
|
|
604
|
-
attachmentResults
|
|
748
|
+
attachmentResults,
|
|
749
|
+
// Only synthesized when the stream actually carried tool calls: the
|
|
750
|
+
// host's tool detection reads `rawResponse`, and an empty one on every
|
|
751
|
+
// ordinary turn would be noise in the logs for no gain.
|
|
752
|
+
...toolCalls.length > 0 ? {
|
|
753
|
+
toolCalls,
|
|
754
|
+
rawResponse: {
|
|
755
|
+
choices: [
|
|
756
|
+
{
|
|
757
|
+
index: 0,
|
|
758
|
+
message: { role: "assistant", content: "", tool_calls: toolCalls },
|
|
759
|
+
finish_reason: finishReason
|
|
760
|
+
}
|
|
761
|
+
]
|
|
762
|
+
}
|
|
763
|
+
} : {}
|
|
605
764
|
};
|
|
606
765
|
} catch (error) {
|
|
607
766
|
this.logger.error(
|
|
@@ -902,7 +1061,7 @@ function rewriteLocalhostUrl(url) {
|
|
|
902
1061
|
}
|
|
903
1062
|
|
|
904
1063
|
// src/version.generated.ts
|
|
905
|
-
var PLUGIN_UTILS_VERSION = "2.
|
|
1064
|
+
var PLUGIN_UTILS_VERSION = "2.4.0";
|
|
906
1065
|
export {
|
|
907
1066
|
BUILTIN_TOOL_NAMES,
|
|
908
1067
|
DEFAULT_REQUEST_TIMEOUT_MS,
|
|
@@ -913,9 +1072,11 @@ export {
|
|
|
913
1072
|
__injectCoreLoggerFactory,
|
|
914
1073
|
__injectQuilltapVersion,
|
|
915
1074
|
applyDescriptionLimit,
|
|
1075
|
+
applyProfileParameters,
|
|
916
1076
|
buildRequestAbortSignal,
|
|
917
1077
|
buildSdkClientOptions,
|
|
918
1078
|
buildSdkRequestOptions,
|
|
1079
|
+
collapseLeadingSystemMessages,
|
|
919
1080
|
convertFromAnthropicFormat,
|
|
920
1081
|
convertFromGoogleFormat,
|
|
921
1082
|
convertOpenAIToAnthropicFormat,
|