@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
|
@@ -122,6 +122,27 @@ declare class OpenAICompatibleProvider implements TextProvider {
|
|
|
122
122
|
protected readonly maxRetries: number;
|
|
123
123
|
/** Logger instance */
|
|
124
124
|
protected readonly logger: PluginLogger;
|
|
125
|
+
/**
|
|
126
|
+
* Whether the endpoint tolerates more than one leading `system` message.
|
|
127
|
+
*
|
|
128
|
+
* True for every hosted service — and so the default, which keeps the
|
|
129
|
+
* outbound bytes of OpenRouter, Z.AI and every other subclass exactly as they
|
|
130
|
+
* were. A subclass that points at a local runtime applying the *model's* chat
|
|
131
|
+
* template sets this false: several template families reject a system message
|
|
132
|
+
* anywhere but index 0, and Quilltap's context builder deliberately emits up
|
|
133
|
+
* to three of them so its cache breakpoints survive. See Bug 82 and
|
|
134
|
+
* {@link collapseLeadingSystemMessages}.
|
|
135
|
+
*/
|
|
136
|
+
protected readonly acceptsRepeatedSystemMessages: boolean;
|
|
137
|
+
/**
|
|
138
|
+
* Apply {@link acceptsRepeatedSystemMessages} to a mapped wire-message array.
|
|
139
|
+
* Called by both body builds so the streaming and non-streaming shapes cannot
|
|
140
|
+
* drift apart.
|
|
141
|
+
*/
|
|
142
|
+
protected applySystemMessagePolicy<T extends {
|
|
143
|
+
role: string;
|
|
144
|
+
content?: string | null;
|
|
145
|
+
}>(messages: T[]): T[];
|
|
125
146
|
/**
|
|
126
147
|
* Creates a new OpenAI-compatible provider instance.
|
|
127
148
|
*
|
|
@@ -178,6 +199,44 @@ declare class OpenAICompatibleProvider implements TextProvider {
|
|
|
178
199
|
timeout: number;
|
|
179
200
|
maxRetries: number;
|
|
180
201
|
} | undefined;
|
|
202
|
+
/**
|
|
203
|
+
* Profile-parameter keys this provider forwards from
|
|
204
|
+
* `LLMParams.profileParameters` onto the request body.
|
|
205
|
+
*
|
|
206
|
+
* **Empty by default**, so a subclass that declares nothing sends exactly the
|
|
207
|
+
* bytes it sent before this mechanism existed. Subclasses opt in by
|
|
208
|
+
* overriding with their own allow-list; `model`, `messages`, `stream`,
|
|
209
|
+
* `stream_options` and `tools` must never appear in one.
|
|
210
|
+
*/
|
|
211
|
+
protected readonly profileParamAllowlist: readonly string[];
|
|
212
|
+
/**
|
|
213
|
+
* Per-key hook for allow-listed profile parameters. The default forwards the
|
|
214
|
+
* stored value verbatim.
|
|
215
|
+
*
|
|
216
|
+
* Override to reshape a value (the editor stores flat strings where some APIs
|
|
217
|
+
* want objects), to gate a key on the model, or — by writing into `body` and
|
|
218
|
+
* returning `undefined` — to send it under a different key entirely.
|
|
219
|
+
*/
|
|
220
|
+
protected normalizeProfileParam(_key: string, value: unknown, _params: LLMParams, _body: Record<string, unknown>): unknown | undefined;
|
|
221
|
+
/**
|
|
222
|
+
* Apply {@link profileParamAllowlist} to a request body under construction.
|
|
223
|
+
* Called by both body builds so the streaming and non-streaming shapes cannot
|
|
224
|
+
* drift apart.
|
|
225
|
+
*/
|
|
226
|
+
protected applyProfileParams(body: Record<string, unknown>, params: LLMParams): void;
|
|
227
|
+
/**
|
|
228
|
+
* Normalize an OpenAI-shaped `tool_calls` array into Quilltap's `ToolCall`
|
|
229
|
+
* shape, tolerating servers that hand back already-parsed argument objects
|
|
230
|
+
* where the OpenAI wire format specifies a JSON string.
|
|
231
|
+
*/
|
|
232
|
+
protected normalizeToolCalls(toolCalls: readonly unknown[] | undefined | null): {
|
|
233
|
+
id: string;
|
|
234
|
+
type: 'function';
|
|
235
|
+
function: {
|
|
236
|
+
name: string;
|
|
237
|
+
arguments: string;
|
|
238
|
+
};
|
|
239
|
+
}[];
|
|
181
240
|
/**
|
|
182
241
|
* Sends a message and returns the complete response.
|
|
183
242
|
*
|
|
@@ -210,6 +269,106 @@ declare class OpenAICompatibleProvider implements TextProvider {
|
|
|
210
269
|
getAvailableModels(apiKey: string): Promise<string[]>;
|
|
211
270
|
}
|
|
212
271
|
|
|
272
|
+
/**
|
|
273
|
+
* Profile-parameter passthrough
|
|
274
|
+
*
|
|
275
|
+
* A connection profile's `parameters` blob is free-form JSON: the host stores
|
|
276
|
+
* whatever the profile editor (or a hand-edited profile) puts there and hands
|
|
277
|
+
* it back to the provider as `LLMParams.profileParameters` at call time. What
|
|
278
|
+
* reaches the wire is entirely the provider's business — and a provider that
|
|
279
|
+
* never reads the bag drops every setting in silence, which is exactly the
|
|
280
|
+
* defect Bug 71 records.
|
|
281
|
+
*
|
|
282
|
+
* This is the one mechanism every provider uses to forward those keys.
|
|
283
|
+
*
|
|
284
|
+
* **An exported function rather than a base-class method.** Only one plugin in
|
|
285
|
+
* the repository extends `OpenAICompatibleProvider` (DeepSeek); Z.AI and
|
|
286
|
+
* OpenRouter implement `TextProvider` directly, and Ollama speaks its own
|
|
287
|
+
* REST shape. A `protected` method would have reached one of them, so the
|
|
288
|
+
* mechanism is a free function usable by inheritance *and* composition, and
|
|
289
|
+
* the base class simply calls it.
|
|
290
|
+
*
|
|
291
|
+
* @packageDocumentation
|
|
292
|
+
*/
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Per-key hook applied to an allow-listed profile parameter on its way into
|
|
296
|
+
* the request body.
|
|
297
|
+
*
|
|
298
|
+
* Return the value to send, or `undefined` to omit the key entirely.
|
|
299
|
+
*
|
|
300
|
+
* The `body` is passed so a normalizer can place a value somewhere other than
|
|
301
|
+
* its own top-level key — the OpenAI-compatible plugin folds `reasoning_effort`
|
|
302
|
+
* into `chat_template_kwargs` this way, then returns `undefined` so no flat
|
|
303
|
+
* `reasoning_effort` key is sent. Normalizers that only reshape a value can
|
|
304
|
+
* ignore the argument.
|
|
305
|
+
*/
|
|
306
|
+
type ProfileParamNormalizer = (key: string, value: unknown, params: LLMParams, body: Record<string, unknown>) => unknown | undefined;
|
|
307
|
+
/**
|
|
308
|
+
* Copy allow-listed keys from `params.profileParameters` onto a request body.
|
|
309
|
+
*
|
|
310
|
+
* Rules, all of which predate this helper (they were hand-rolled identically in
|
|
311
|
+
* the DeepSeek and Z.AI providers before it existed):
|
|
312
|
+
*
|
|
313
|
+
* - **Allow-list, never spread.** `model`, `messages`, `stream`,
|
|
314
|
+
* `stream_options` and `tools` must be unreachable from a profile — a
|
|
315
|
+
* misconfigured bag cannot be allowed to retarget the request.
|
|
316
|
+
* - **`undefined` and `null` skip.** So does the empty string, which is what
|
|
317
|
+
* the schema-driven profile editor stores for "omit this and use the model
|
|
318
|
+
* default".
|
|
319
|
+
* - **The allow-list is ordered.** Keys are applied in the order given, so a
|
|
320
|
+
* normalizer that merges into an earlier key (`chat_template_kwargs`) sees
|
|
321
|
+
* whatever the profile set for it.
|
|
322
|
+
*
|
|
323
|
+
* @param body - Request body under construction; mutated in place
|
|
324
|
+
* @param params - The call's LLM parameters, carrying `profileParameters`
|
|
325
|
+
* @param allowlist - Keys this provider is willing to forward
|
|
326
|
+
* @param normalize - Optional per-key hook (see {@link ProfileParamNormalizer})
|
|
327
|
+
*/
|
|
328
|
+
declare function applyProfileParameters(body: Record<string, unknown>, params: LLMParams, allowlist: readonly string[], normalize?: ProfileParamNormalizer): void;
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Leading-system-message collapse.
|
|
332
|
+
*
|
|
333
|
+
* Quilltap's context builder deliberately emits the head of a turn as up to
|
|
334
|
+
* three consecutive `system` messages — the cacheable persona prefix, the
|
|
335
|
+
* static identity reinforcement, and the compressed-history summary — so that a
|
|
336
|
+
* cache breakpoint on the first is not invalidated by churn in the others.
|
|
337
|
+
* Hosted providers accept that happily.
|
|
338
|
+
*
|
|
339
|
+
* A local runtime does not answer for itself: it applies the *model's own* chat
|
|
340
|
+
* template, and several families (Qwen most notably, plus Llama- and
|
|
341
|
+
* Gemma-derived templates) `raise_exception` on any system message after index
|
|
342
|
+
* 0. The whole request is then rejected before a token is generated, so the
|
|
343
|
+
* opening greeting — one system message — works and every turn after it dies.
|
|
344
|
+
*
|
|
345
|
+
* The repair belongs here, at request-build time, rather than in the context
|
|
346
|
+
* assembly: the blocks stay separate for every provider that wants them
|
|
347
|
+
* separate, and the providers that cannot take them fold the run on the way
|
|
348
|
+
* out. See Bug 82.
|
|
349
|
+
*
|
|
350
|
+
* @packageDocumentation
|
|
351
|
+
*/
|
|
352
|
+
/** The shape this helper needs; wire messages carry more, and keep it. */
|
|
353
|
+
interface RoleAndContent {
|
|
354
|
+
role: string;
|
|
355
|
+
content?: string | null;
|
|
356
|
+
}
|
|
357
|
+
/**
|
|
358
|
+
* Fold a run of consecutive leading `system` messages into a single one,
|
|
359
|
+
* joining their contents with a blank line.
|
|
360
|
+
*
|
|
361
|
+
* Only the *leading* run is touched — a system message that appears later is
|
|
362
|
+
* left where it is, because moving or merging it would change what the model is
|
|
363
|
+
* told and when. Arrays with fewer than two leading system messages are
|
|
364
|
+
* returned unchanged (the same array reference), so a provider that opts in
|
|
365
|
+
* still sends byte-identical requests everywhere the problem does not arise.
|
|
366
|
+
*
|
|
367
|
+
* @param messages - Wire-format messages, already mapped for the endpoint
|
|
368
|
+
* @returns The messages with their leading system run collapsed
|
|
369
|
+
*/
|
|
370
|
+
declare function collapseLeadingSystemMessages<T extends RoleAndContent>(messages: T[]): T[];
|
|
371
|
+
|
|
213
372
|
/**
|
|
214
373
|
* Per-request wall-clock budgets.
|
|
215
374
|
*
|
|
@@ -291,4 +450,4 @@ declare function buildSdkClientOptions(params: Pick<LLMParams, 'requestTimeoutMs
|
|
|
291
450
|
*/
|
|
292
451
|
declare function buildRequestAbortSignal(params: Pick<LLMParams, 'requestTimeoutMs'>, defaultMs?: number): AbortSignal;
|
|
293
452
|
|
|
294
|
-
export { DEFAULT_REQUEST_TIMEOUT_MS, OpenAICompatibleProvider, type OpenAICompatibleProviderConfig, buildRequestAbortSignal, buildSdkClientOptions, buildSdkRequestOptions, resolveRequestTimeoutMs };
|
|
453
|
+
export { DEFAULT_REQUEST_TIMEOUT_MS, OpenAICompatibleProvider, type OpenAICompatibleProviderConfig, type ProfileParamNormalizer, applyProfileParameters, buildRequestAbortSignal, buildSdkClientOptions, buildSdkRequestOptions, collapseLeadingSystemMessages, resolveRequestTimeoutMs };
|
|
@@ -122,6 +122,27 @@ declare class OpenAICompatibleProvider implements TextProvider {
|
|
|
122
122
|
protected readonly maxRetries: number;
|
|
123
123
|
/** Logger instance */
|
|
124
124
|
protected readonly logger: PluginLogger;
|
|
125
|
+
/**
|
|
126
|
+
* Whether the endpoint tolerates more than one leading `system` message.
|
|
127
|
+
*
|
|
128
|
+
* True for every hosted service — and so the default, which keeps the
|
|
129
|
+
* outbound bytes of OpenRouter, Z.AI and every other subclass exactly as they
|
|
130
|
+
* were. A subclass that points at a local runtime applying the *model's* chat
|
|
131
|
+
* template sets this false: several template families reject a system message
|
|
132
|
+
* anywhere but index 0, and Quilltap's context builder deliberately emits up
|
|
133
|
+
* to three of them so its cache breakpoints survive. See Bug 82 and
|
|
134
|
+
* {@link collapseLeadingSystemMessages}.
|
|
135
|
+
*/
|
|
136
|
+
protected readonly acceptsRepeatedSystemMessages: boolean;
|
|
137
|
+
/**
|
|
138
|
+
* Apply {@link acceptsRepeatedSystemMessages} to a mapped wire-message array.
|
|
139
|
+
* Called by both body builds so the streaming and non-streaming shapes cannot
|
|
140
|
+
* drift apart.
|
|
141
|
+
*/
|
|
142
|
+
protected applySystemMessagePolicy<T extends {
|
|
143
|
+
role: string;
|
|
144
|
+
content?: string | null;
|
|
145
|
+
}>(messages: T[]): T[];
|
|
125
146
|
/**
|
|
126
147
|
* Creates a new OpenAI-compatible provider instance.
|
|
127
148
|
*
|
|
@@ -178,6 +199,44 @@ declare class OpenAICompatibleProvider implements TextProvider {
|
|
|
178
199
|
timeout: number;
|
|
179
200
|
maxRetries: number;
|
|
180
201
|
} | undefined;
|
|
202
|
+
/**
|
|
203
|
+
* Profile-parameter keys this provider forwards from
|
|
204
|
+
* `LLMParams.profileParameters` onto the request body.
|
|
205
|
+
*
|
|
206
|
+
* **Empty by default**, so a subclass that declares nothing sends exactly the
|
|
207
|
+
* bytes it sent before this mechanism existed. Subclasses opt in by
|
|
208
|
+
* overriding with their own allow-list; `model`, `messages`, `stream`,
|
|
209
|
+
* `stream_options` and `tools` must never appear in one.
|
|
210
|
+
*/
|
|
211
|
+
protected readonly profileParamAllowlist: readonly string[];
|
|
212
|
+
/**
|
|
213
|
+
* Per-key hook for allow-listed profile parameters. The default forwards the
|
|
214
|
+
* stored value verbatim.
|
|
215
|
+
*
|
|
216
|
+
* Override to reshape a value (the editor stores flat strings where some APIs
|
|
217
|
+
* want objects), to gate a key on the model, or — by writing into `body` and
|
|
218
|
+
* returning `undefined` — to send it under a different key entirely.
|
|
219
|
+
*/
|
|
220
|
+
protected normalizeProfileParam(_key: string, value: unknown, _params: LLMParams, _body: Record<string, unknown>): unknown | undefined;
|
|
221
|
+
/**
|
|
222
|
+
* Apply {@link profileParamAllowlist} to a request body under construction.
|
|
223
|
+
* Called by both body builds so the streaming and non-streaming shapes cannot
|
|
224
|
+
* drift apart.
|
|
225
|
+
*/
|
|
226
|
+
protected applyProfileParams(body: Record<string, unknown>, params: LLMParams): void;
|
|
227
|
+
/**
|
|
228
|
+
* Normalize an OpenAI-shaped `tool_calls` array into Quilltap's `ToolCall`
|
|
229
|
+
* shape, tolerating servers that hand back already-parsed argument objects
|
|
230
|
+
* where the OpenAI wire format specifies a JSON string.
|
|
231
|
+
*/
|
|
232
|
+
protected normalizeToolCalls(toolCalls: readonly unknown[] | undefined | null): {
|
|
233
|
+
id: string;
|
|
234
|
+
type: 'function';
|
|
235
|
+
function: {
|
|
236
|
+
name: string;
|
|
237
|
+
arguments: string;
|
|
238
|
+
};
|
|
239
|
+
}[];
|
|
181
240
|
/**
|
|
182
241
|
* Sends a message and returns the complete response.
|
|
183
242
|
*
|
|
@@ -210,6 +269,106 @@ declare class OpenAICompatibleProvider implements TextProvider {
|
|
|
210
269
|
getAvailableModels(apiKey: string): Promise<string[]>;
|
|
211
270
|
}
|
|
212
271
|
|
|
272
|
+
/**
|
|
273
|
+
* Profile-parameter passthrough
|
|
274
|
+
*
|
|
275
|
+
* A connection profile's `parameters` blob is free-form JSON: the host stores
|
|
276
|
+
* whatever the profile editor (or a hand-edited profile) puts there and hands
|
|
277
|
+
* it back to the provider as `LLMParams.profileParameters` at call time. What
|
|
278
|
+
* reaches the wire is entirely the provider's business — and a provider that
|
|
279
|
+
* never reads the bag drops every setting in silence, which is exactly the
|
|
280
|
+
* defect Bug 71 records.
|
|
281
|
+
*
|
|
282
|
+
* This is the one mechanism every provider uses to forward those keys.
|
|
283
|
+
*
|
|
284
|
+
* **An exported function rather than a base-class method.** Only one plugin in
|
|
285
|
+
* the repository extends `OpenAICompatibleProvider` (DeepSeek); Z.AI and
|
|
286
|
+
* OpenRouter implement `TextProvider` directly, and Ollama speaks its own
|
|
287
|
+
* REST shape. A `protected` method would have reached one of them, so the
|
|
288
|
+
* mechanism is a free function usable by inheritance *and* composition, and
|
|
289
|
+
* the base class simply calls it.
|
|
290
|
+
*
|
|
291
|
+
* @packageDocumentation
|
|
292
|
+
*/
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Per-key hook applied to an allow-listed profile parameter on its way into
|
|
296
|
+
* the request body.
|
|
297
|
+
*
|
|
298
|
+
* Return the value to send, or `undefined` to omit the key entirely.
|
|
299
|
+
*
|
|
300
|
+
* The `body` is passed so a normalizer can place a value somewhere other than
|
|
301
|
+
* its own top-level key — the OpenAI-compatible plugin folds `reasoning_effort`
|
|
302
|
+
* into `chat_template_kwargs` this way, then returns `undefined` so no flat
|
|
303
|
+
* `reasoning_effort` key is sent. Normalizers that only reshape a value can
|
|
304
|
+
* ignore the argument.
|
|
305
|
+
*/
|
|
306
|
+
type ProfileParamNormalizer = (key: string, value: unknown, params: LLMParams, body: Record<string, unknown>) => unknown | undefined;
|
|
307
|
+
/**
|
|
308
|
+
* Copy allow-listed keys from `params.profileParameters` onto a request body.
|
|
309
|
+
*
|
|
310
|
+
* Rules, all of which predate this helper (they were hand-rolled identically in
|
|
311
|
+
* the DeepSeek and Z.AI providers before it existed):
|
|
312
|
+
*
|
|
313
|
+
* - **Allow-list, never spread.** `model`, `messages`, `stream`,
|
|
314
|
+
* `stream_options` and `tools` must be unreachable from a profile — a
|
|
315
|
+
* misconfigured bag cannot be allowed to retarget the request.
|
|
316
|
+
* - **`undefined` and `null` skip.** So does the empty string, which is what
|
|
317
|
+
* the schema-driven profile editor stores for "omit this and use the model
|
|
318
|
+
* default".
|
|
319
|
+
* - **The allow-list is ordered.** Keys are applied in the order given, so a
|
|
320
|
+
* normalizer that merges into an earlier key (`chat_template_kwargs`) sees
|
|
321
|
+
* whatever the profile set for it.
|
|
322
|
+
*
|
|
323
|
+
* @param body - Request body under construction; mutated in place
|
|
324
|
+
* @param params - The call's LLM parameters, carrying `profileParameters`
|
|
325
|
+
* @param allowlist - Keys this provider is willing to forward
|
|
326
|
+
* @param normalize - Optional per-key hook (see {@link ProfileParamNormalizer})
|
|
327
|
+
*/
|
|
328
|
+
declare function applyProfileParameters(body: Record<string, unknown>, params: LLMParams, allowlist: readonly string[], normalize?: ProfileParamNormalizer): void;
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Leading-system-message collapse.
|
|
332
|
+
*
|
|
333
|
+
* Quilltap's context builder deliberately emits the head of a turn as up to
|
|
334
|
+
* three consecutive `system` messages — the cacheable persona prefix, the
|
|
335
|
+
* static identity reinforcement, and the compressed-history summary — so that a
|
|
336
|
+
* cache breakpoint on the first is not invalidated by churn in the others.
|
|
337
|
+
* Hosted providers accept that happily.
|
|
338
|
+
*
|
|
339
|
+
* A local runtime does not answer for itself: it applies the *model's own* chat
|
|
340
|
+
* template, and several families (Qwen most notably, plus Llama- and
|
|
341
|
+
* Gemma-derived templates) `raise_exception` on any system message after index
|
|
342
|
+
* 0. The whole request is then rejected before a token is generated, so the
|
|
343
|
+
* opening greeting — one system message — works and every turn after it dies.
|
|
344
|
+
*
|
|
345
|
+
* The repair belongs here, at request-build time, rather than in the context
|
|
346
|
+
* assembly: the blocks stay separate for every provider that wants them
|
|
347
|
+
* separate, and the providers that cannot take them fold the run on the way
|
|
348
|
+
* out. See Bug 82.
|
|
349
|
+
*
|
|
350
|
+
* @packageDocumentation
|
|
351
|
+
*/
|
|
352
|
+
/** The shape this helper needs; wire messages carry more, and keep it. */
|
|
353
|
+
interface RoleAndContent {
|
|
354
|
+
role: string;
|
|
355
|
+
content?: string | null;
|
|
356
|
+
}
|
|
357
|
+
/**
|
|
358
|
+
* Fold a run of consecutive leading `system` messages into a single one,
|
|
359
|
+
* joining their contents with a blank line.
|
|
360
|
+
*
|
|
361
|
+
* Only the *leading* run is touched — a system message that appears later is
|
|
362
|
+
* left where it is, because moving or merging it would change what the model is
|
|
363
|
+
* told and when. Arrays with fewer than two leading system messages are
|
|
364
|
+
* returned unchanged (the same array reference), so a provider that opts in
|
|
365
|
+
* still sends byte-identical requests everywhere the problem does not arise.
|
|
366
|
+
*
|
|
367
|
+
* @param messages - Wire-format messages, already mapped for the endpoint
|
|
368
|
+
* @returns The messages with their leading system run collapsed
|
|
369
|
+
*/
|
|
370
|
+
declare function collapseLeadingSystemMessages<T extends RoleAndContent>(messages: T[]): T[];
|
|
371
|
+
|
|
213
372
|
/**
|
|
214
373
|
* Per-request wall-clock budgets.
|
|
215
374
|
*
|
|
@@ -291,4 +450,4 @@ declare function buildSdkClientOptions(params: Pick<LLMParams, 'requestTimeoutMs
|
|
|
291
450
|
*/
|
|
292
451
|
declare function buildRequestAbortSignal(params: Pick<LLMParams, 'requestTimeoutMs'>, defaultMs?: number): AbortSignal;
|
|
293
452
|
|
|
294
|
-
export { DEFAULT_REQUEST_TIMEOUT_MS, OpenAICompatibleProvider, type OpenAICompatibleProviderConfig, buildRequestAbortSignal, buildSdkClientOptions, buildSdkRequestOptions, resolveRequestTimeoutMs };
|
|
453
|
+
export { DEFAULT_REQUEST_TIMEOUT_MS, OpenAICompatibleProvider, type OpenAICompatibleProviderConfig, type ProfileParamNormalizer, applyProfileParameters, buildRequestAbortSignal, buildSdkClientOptions, buildSdkRequestOptions, collapseLeadingSystemMessages, resolveRequestTimeoutMs };
|
package/dist/providers/index.js
CHANGED
|
@@ -32,9 +32,11 @@ var providers_exports = {};
|
|
|
32
32
|
__export(providers_exports, {
|
|
33
33
|
DEFAULT_REQUEST_TIMEOUT_MS: () => DEFAULT_REQUEST_TIMEOUT_MS,
|
|
34
34
|
OpenAICompatibleProvider: () => OpenAICompatibleProvider,
|
|
35
|
+
applyProfileParameters: () => applyProfileParameters,
|
|
35
36
|
buildRequestAbortSignal: () => buildRequestAbortSignal,
|
|
36
37
|
buildSdkClientOptions: () => buildSdkClientOptions,
|
|
37
38
|
buildSdkRequestOptions: () => buildSdkRequestOptions,
|
|
39
|
+
collapseLeadingSystemMessages: () => collapseLeadingSystemMessages,
|
|
38
40
|
resolveRequestTimeoutMs: () => resolveRequestTimeoutMs
|
|
39
41
|
});
|
|
40
42
|
module.exports = __toCommonJS(providers_exports);
|
|
@@ -131,6 +133,36 @@ function buildRequestAbortSignal(params, defaultMs = DEFAULT_REQUEST_TIMEOUT_MS)
|
|
|
131
133
|
return AbortSignal.timeout(resolveRequestTimeoutMs(params, defaultMs));
|
|
132
134
|
}
|
|
133
135
|
|
|
136
|
+
// src/providers/profile-parameters.ts
|
|
137
|
+
function applyProfileParameters(body, params, allowlist, normalize) {
|
|
138
|
+
const profile = params.profileParameters;
|
|
139
|
+
if (!profile || typeof profile !== "object") return;
|
|
140
|
+
const bag = profile;
|
|
141
|
+
for (const key of allowlist) {
|
|
142
|
+
const raw = bag[key];
|
|
143
|
+
if (raw === void 0 || raw === null) continue;
|
|
144
|
+
if (typeof raw === "string" && raw === "") continue;
|
|
145
|
+
const value = normalize ? normalize(key, raw, params, body) : raw;
|
|
146
|
+
if (value === void 0) continue;
|
|
147
|
+
body[key] = value;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// src/providers/system-messages.ts
|
|
152
|
+
function collapseLeadingSystemMessages(messages) {
|
|
153
|
+
let runLength = 0;
|
|
154
|
+
while (runLength < messages.length && messages[runLength].role === "system") {
|
|
155
|
+
runLength++;
|
|
156
|
+
}
|
|
157
|
+
if (runLength < 2) return messages;
|
|
158
|
+
const run = messages.slice(0, runLength);
|
|
159
|
+
const merged = {
|
|
160
|
+
...run[0],
|
|
161
|
+
content: run.map((m) => m.content ?? "").filter((c) => c.length > 0).join("\n\n")
|
|
162
|
+
};
|
|
163
|
+
return [merged, ...messages.slice(runLength)];
|
|
164
|
+
}
|
|
165
|
+
|
|
134
166
|
// src/providers/openai-compatible.ts
|
|
135
167
|
var DEFAULT_MAX_RETRIES = 2;
|
|
136
168
|
var OpenAICompatibleProvider = class {
|
|
@@ -146,6 +178,28 @@ var OpenAICompatibleProvider = class {
|
|
|
146
178
|
this.supportedMimeTypes = [];
|
|
147
179
|
/** Web search is not supported */
|
|
148
180
|
this.supportsWebSearch = false;
|
|
181
|
+
/**
|
|
182
|
+
* Whether the endpoint tolerates more than one leading `system` message.
|
|
183
|
+
*
|
|
184
|
+
* True for every hosted service — and so the default, which keeps the
|
|
185
|
+
* outbound bytes of OpenRouter, Z.AI and every other subclass exactly as they
|
|
186
|
+
* were. A subclass that points at a local runtime applying the *model's* chat
|
|
187
|
+
* template sets this false: several template families reject a system message
|
|
188
|
+
* anywhere but index 0, and Quilltap's context builder deliberately emits up
|
|
189
|
+
* to three of them so its cache breakpoints survive. See Bug 82 and
|
|
190
|
+
* {@link collapseLeadingSystemMessages}.
|
|
191
|
+
*/
|
|
192
|
+
this.acceptsRepeatedSystemMessages = true;
|
|
193
|
+
/**
|
|
194
|
+
* Profile-parameter keys this provider forwards from
|
|
195
|
+
* `LLMParams.profileParameters` onto the request body.
|
|
196
|
+
*
|
|
197
|
+
* **Empty by default**, so a subclass that declares nothing sends exactly the
|
|
198
|
+
* bytes it sent before this mechanism existed. Subclasses opt in by
|
|
199
|
+
* overriding with their own allow-list; `model`, `messages`, `stream`,
|
|
200
|
+
* `stream_options` and `tools` must never appear in one.
|
|
201
|
+
*/
|
|
202
|
+
this.profileParamAllowlist = [];
|
|
149
203
|
if (typeof config === "string") {
|
|
150
204
|
this.baseUrl = config;
|
|
151
205
|
this.providerName = "OpenAICompatible";
|
|
@@ -163,6 +217,14 @@ var OpenAICompatibleProvider = class {
|
|
|
163
217
|
}
|
|
164
218
|
this.logger = createPluginLogger(`${this.providerName}Provider`);
|
|
165
219
|
}
|
|
220
|
+
/**
|
|
221
|
+
* Apply {@link acceptsRepeatedSystemMessages} to a mapped wire-message array.
|
|
222
|
+
* Called by both body builds so the streaming and non-streaming shapes cannot
|
|
223
|
+
* drift apart.
|
|
224
|
+
*/
|
|
225
|
+
applySystemMessagePolicy(messages) {
|
|
226
|
+
return this.acceptsRepeatedSystemMessages ? messages : collapseLeadingSystemMessages(messages);
|
|
227
|
+
}
|
|
166
228
|
/**
|
|
167
229
|
* Collects attachment failures for messages with attachments.
|
|
168
230
|
* Since attachments are not supported, all attachments are marked as failed.
|
|
@@ -233,6 +295,53 @@ var OpenAICompatibleProvider = class {
|
|
|
233
295
|
buildRequestOptions(params) {
|
|
234
296
|
return buildSdkRequestOptions(params);
|
|
235
297
|
}
|
|
298
|
+
/**
|
|
299
|
+
* Per-key hook for allow-listed profile parameters. The default forwards the
|
|
300
|
+
* stored value verbatim.
|
|
301
|
+
*
|
|
302
|
+
* Override to reshape a value (the editor stores flat strings where some APIs
|
|
303
|
+
* want objects), to gate a key on the model, or — by writing into `body` and
|
|
304
|
+
* returning `undefined` — to send it under a different key entirely.
|
|
305
|
+
*/
|
|
306
|
+
normalizeProfileParam(_key, value, _params, _body) {
|
|
307
|
+
return value;
|
|
308
|
+
}
|
|
309
|
+
/**
|
|
310
|
+
* Apply {@link profileParamAllowlist} to a request body under construction.
|
|
311
|
+
* Called by both body builds so the streaming and non-streaming shapes cannot
|
|
312
|
+
* drift apart.
|
|
313
|
+
*/
|
|
314
|
+
applyProfileParams(body, params) {
|
|
315
|
+
applyProfileParameters(
|
|
316
|
+
body,
|
|
317
|
+
params,
|
|
318
|
+
this.profileParamAllowlist,
|
|
319
|
+
(key, value, callParams, target) => this.normalizeProfileParam(key, value, callParams, target)
|
|
320
|
+
);
|
|
321
|
+
}
|
|
322
|
+
/**
|
|
323
|
+
* Normalize an OpenAI-shaped `tool_calls` array into Quilltap's `ToolCall`
|
|
324
|
+
* shape, tolerating servers that hand back already-parsed argument objects
|
|
325
|
+
* where the OpenAI wire format specifies a JSON string.
|
|
326
|
+
*/
|
|
327
|
+
normalizeToolCalls(toolCalls) {
|
|
328
|
+
if (!toolCalls || toolCalls.length === 0) return [];
|
|
329
|
+
const out = [];
|
|
330
|
+
for (const raw of toolCalls) {
|
|
331
|
+
const tc = raw;
|
|
332
|
+
if (!tc?.function?.name) continue;
|
|
333
|
+
const args = tc.function.arguments;
|
|
334
|
+
out.push({
|
|
335
|
+
id: tc.id ?? "",
|
|
336
|
+
type: "function",
|
|
337
|
+
function: {
|
|
338
|
+
name: tc.function.name,
|
|
339
|
+
arguments: typeof args === "string" ? args : JSON.stringify(args ?? {})
|
|
340
|
+
}
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
return out;
|
|
344
|
+
}
|
|
236
345
|
/**
|
|
237
346
|
* Sends a message and returns the complete response.
|
|
238
347
|
*
|
|
@@ -244,7 +353,7 @@ var OpenAICompatibleProvider = class {
|
|
|
244
353
|
this.validateApiKeyRequirement(apiKey);
|
|
245
354
|
const attachmentResults = this.collectAttachmentFailures(params);
|
|
246
355
|
const client = this.createClient(apiKey);
|
|
247
|
-
const
|
|
356
|
+
const mappedMessages = params.messages.filter((m) => {
|
|
248
357
|
if (m.role === "tool" && !m.toolCallId) return false;
|
|
249
358
|
return true;
|
|
250
359
|
}).map((m) => {
|
|
@@ -271,17 +380,27 @@ var OpenAICompatibleProvider = class {
|
|
|
271
380
|
content: m.content
|
|
272
381
|
};
|
|
273
382
|
});
|
|
383
|
+
const messages = this.applySystemMessagePolicy(mappedMessages);
|
|
384
|
+
const body = {
|
|
385
|
+
model: params.model,
|
|
386
|
+
messages,
|
|
387
|
+
temperature: params.temperature ?? 0.7,
|
|
388
|
+
max_tokens: params.maxTokens ?? 4096,
|
|
389
|
+
top_p: params.topP ?? 1,
|
|
390
|
+
stop: params.stop,
|
|
391
|
+
...params.cacheKey ? { user: params.cacheKey } : {},
|
|
392
|
+
// Tools arrive only once the runtime gate (the profile's "Allow tool
|
|
393
|
+
// use") has passed, so no separate guard is needed here.
|
|
394
|
+
...params.tools && params.tools.length > 0 ? { tools: params.tools, tool_choice: params.toolChoice ?? "auto" } : {}
|
|
395
|
+
};
|
|
396
|
+
this.applyProfileParams(body, params);
|
|
274
397
|
try {
|
|
275
|
-
const response = await client.chat.completions.create(
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
max_tokens: params.maxTokens ?? 4096,
|
|
280
|
-
top_p: params.topP ?? 1,
|
|
281
|
-
stop: params.stop,
|
|
282
|
-
...params.cacheKey ? { user: params.cacheKey } : {}
|
|
283
|
-
}, this.buildRequestOptions(params));
|
|
398
|
+
const response = await client.chat.completions.create(
|
|
399
|
+
body,
|
|
400
|
+
this.buildRequestOptions(params)
|
|
401
|
+
);
|
|
284
402
|
const choice = response.choices[0];
|
|
403
|
+
const toolCalls = this.normalizeToolCalls(choice.message.tool_calls);
|
|
285
404
|
return {
|
|
286
405
|
content: choice.message.content ?? "",
|
|
287
406
|
finishReason: choice.finish_reason,
|
|
@@ -291,7 +410,8 @@ var OpenAICompatibleProvider = class {
|
|
|
291
410
|
totalTokens: response.usage?.total_tokens ?? 0
|
|
292
411
|
},
|
|
293
412
|
raw: response,
|
|
294
|
-
attachmentResults
|
|
413
|
+
attachmentResults,
|
|
414
|
+
...toolCalls.length > 0 ? { toolCalls } : {}
|
|
295
415
|
};
|
|
296
416
|
} catch (error) {
|
|
297
417
|
this.logger.error(
|
|
@@ -313,7 +433,7 @@ var OpenAICompatibleProvider = class {
|
|
|
313
433
|
this.validateApiKeyRequirement(apiKey);
|
|
314
434
|
const attachmentResults = this.collectAttachmentFailures(params);
|
|
315
435
|
const client = this.createClient(apiKey);
|
|
316
|
-
const
|
|
436
|
+
const mappedMessages = params.messages.filter((m) => {
|
|
317
437
|
if (m.role === "tool" && !m.toolCallId) return false;
|
|
318
438
|
return true;
|
|
319
439
|
}).map((m) => {
|
|
@@ -340,23 +460,33 @@ var OpenAICompatibleProvider = class {
|
|
|
340
460
|
content: m.content
|
|
341
461
|
};
|
|
342
462
|
});
|
|
463
|
+
const messages = this.applySystemMessagePolicy(mappedMessages);
|
|
464
|
+
const body = {
|
|
465
|
+
model: params.model,
|
|
466
|
+
messages,
|
|
467
|
+
temperature: params.temperature ?? 0.7,
|
|
468
|
+
max_tokens: params.maxTokens ?? 4096,
|
|
469
|
+
top_p: params.topP ?? 1,
|
|
470
|
+
stop: params.stop,
|
|
471
|
+
stream: true,
|
|
472
|
+
stream_options: { include_usage: true },
|
|
473
|
+
...params.cacheKey ? { user: params.cacheKey } : {},
|
|
474
|
+
...params.tools && params.tools.length > 0 ? { tools: params.tools, tool_choice: params.toolChoice ?? "auto" } : {}
|
|
475
|
+
};
|
|
476
|
+
this.applyProfileParams(body, params);
|
|
343
477
|
try {
|
|
344
|
-
const stream = await client.chat.completions.create(
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
max_tokens: params.maxTokens ?? 4096,
|
|
349
|
-
top_p: params.topP ?? 1,
|
|
350
|
-
stop: params.stop,
|
|
351
|
-
stream: true,
|
|
352
|
-
stream_options: { include_usage: true },
|
|
353
|
-
...params.cacheKey ? { user: params.cacheKey } : {}
|
|
354
|
-
}, this.buildRequestOptions(params));
|
|
478
|
+
const stream = await client.chat.completions.create(
|
|
479
|
+
body,
|
|
480
|
+
this.buildRequestOptions(params)
|
|
481
|
+
);
|
|
355
482
|
let chunkCount = 0;
|
|
356
483
|
let accumulatedUsage = null;
|
|
484
|
+
const toolCallAccumulator = /* @__PURE__ */ new Map();
|
|
485
|
+
let finishReason = null;
|
|
357
486
|
for await (const chunk of stream) {
|
|
358
487
|
chunkCount++;
|
|
359
|
-
const
|
|
488
|
+
const choice = chunk.choices[0];
|
|
489
|
+
const content = choice?.delta?.content;
|
|
360
490
|
const hasUsage = chunk.usage;
|
|
361
491
|
if (hasUsage) {
|
|
362
492
|
accumulatedUsage = {
|
|
@@ -365,6 +495,17 @@ var OpenAICompatibleProvider = class {
|
|
|
365
495
|
total_tokens: chunk.usage?.total_tokens
|
|
366
496
|
};
|
|
367
497
|
}
|
|
498
|
+
if (choice?.delta?.tool_calls) {
|
|
499
|
+
for (const tcDelta of choice.delta.tool_calls) {
|
|
500
|
+
const idx = tcDelta.index ?? 0;
|
|
501
|
+
const existing = toolCallAccumulator.get(idx) ?? { id: "", name: "", arguments: "" };
|
|
502
|
+
if (tcDelta.id) existing.id = tcDelta.id;
|
|
503
|
+
if (tcDelta.function?.name) existing.name = tcDelta.function.name;
|
|
504
|
+
if (tcDelta.function?.arguments) existing.arguments += tcDelta.function.arguments;
|
|
505
|
+
toolCallAccumulator.set(idx, existing);
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
if (choice?.finish_reason) finishReason = choice.finish_reason;
|
|
368
509
|
if (content) {
|
|
369
510
|
yield {
|
|
370
511
|
content,
|
|
@@ -372,6 +513,11 @@ var OpenAICompatibleProvider = class {
|
|
|
372
513
|
};
|
|
373
514
|
}
|
|
374
515
|
}
|
|
516
|
+
const toolCalls = Array.from(toolCallAccumulator.values()).filter((tc) => tc.name).map((tc) => ({
|
|
517
|
+
id: tc.id,
|
|
518
|
+
type: "function",
|
|
519
|
+
function: { name: tc.name, arguments: tc.arguments }
|
|
520
|
+
}));
|
|
375
521
|
yield {
|
|
376
522
|
content: "",
|
|
377
523
|
done: true,
|
|
@@ -380,7 +526,22 @@ var OpenAICompatibleProvider = class {
|
|
|
380
526
|
completionTokens: accumulatedUsage.completion_tokens ?? 0,
|
|
381
527
|
totalTokens: accumulatedUsage.total_tokens ?? 0
|
|
382
528
|
} : void 0,
|
|
383
|
-
attachmentResults
|
|
529
|
+
attachmentResults,
|
|
530
|
+
// Only synthesized when the stream actually carried tool calls: the
|
|
531
|
+
// host's tool detection reads `rawResponse`, and an empty one on every
|
|
532
|
+
// ordinary turn would be noise in the logs for no gain.
|
|
533
|
+
...toolCalls.length > 0 ? {
|
|
534
|
+
toolCalls,
|
|
535
|
+
rawResponse: {
|
|
536
|
+
choices: [
|
|
537
|
+
{
|
|
538
|
+
index: 0,
|
|
539
|
+
message: { role: "assistant", content: "", tool_calls: toolCalls },
|
|
540
|
+
finish_reason: finishReason
|
|
541
|
+
}
|
|
542
|
+
]
|
|
543
|
+
}
|
|
544
|
+
} : {}
|
|
384
545
|
};
|
|
385
546
|
} catch (error) {
|
|
386
547
|
this.logger.error(
|
|
@@ -446,9 +607,11 @@ var OpenAICompatibleProvider = class {
|
|
|
446
607
|
0 && (module.exports = {
|
|
447
608
|
DEFAULT_REQUEST_TIMEOUT_MS,
|
|
448
609
|
OpenAICompatibleProvider,
|
|
610
|
+
applyProfileParameters,
|
|
449
611
|
buildRequestAbortSignal,
|
|
450
612
|
buildSdkClientOptions,
|
|
451
613
|
buildSdkRequestOptions,
|
|
614
|
+
collapseLeadingSystemMessages,
|
|
452
615
|
resolveRequestTimeoutMs
|
|
453
616
|
});
|
|
454
617
|
//# sourceMappingURL=index.js.map
|