@quilltap/plugin-utils 2.2.19 → 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/CHANGELOG.md +8 -0
- package/dist/index.d.mts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +148 -24
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +147 -24
- package/dist/index.mjs.map +1 -1
- package/dist/providers/index.d.mts +97 -1
- package/dist/providers/index.d.ts +97 -1
- package/dist/providers/index.js +147 -23
- package/dist/providers/index.js.map +1 -1
- package/dist/providers/index.mjs +146 -23
- package/dist/providers/index.mjs.map +1 -1
- package/package.json +3 -3
|
@@ -178,6 +178,44 @@ declare class OpenAICompatibleProvider implements TextProvider {
|
|
|
178
178
|
timeout: number;
|
|
179
179
|
maxRetries: number;
|
|
180
180
|
} | undefined;
|
|
181
|
+
/**
|
|
182
|
+
* Profile-parameter keys this provider forwards from
|
|
183
|
+
* `LLMParams.profileParameters` onto the request body.
|
|
184
|
+
*
|
|
185
|
+
* **Empty by default**, so a subclass that declares nothing sends exactly the
|
|
186
|
+
* bytes it sent before this mechanism existed. Subclasses opt in by
|
|
187
|
+
* overriding with their own allow-list; `model`, `messages`, `stream`,
|
|
188
|
+
* `stream_options` and `tools` must never appear in one.
|
|
189
|
+
*/
|
|
190
|
+
protected readonly profileParamAllowlist: readonly string[];
|
|
191
|
+
/**
|
|
192
|
+
* Per-key hook for allow-listed profile parameters. The default forwards the
|
|
193
|
+
* stored value verbatim.
|
|
194
|
+
*
|
|
195
|
+
* Override to reshape a value (the editor stores flat strings where some APIs
|
|
196
|
+
* want objects), to gate a key on the model, or — by writing into `body` and
|
|
197
|
+
* returning `undefined` — to send it under a different key entirely.
|
|
198
|
+
*/
|
|
199
|
+
protected normalizeProfileParam(_key: string, value: unknown, _params: LLMParams, _body: Record<string, unknown>): unknown | undefined;
|
|
200
|
+
/**
|
|
201
|
+
* Apply {@link profileParamAllowlist} to a request body under construction.
|
|
202
|
+
* Called by both body builds so the streaming and non-streaming shapes cannot
|
|
203
|
+
* drift apart.
|
|
204
|
+
*/
|
|
205
|
+
protected applyProfileParams(body: Record<string, unknown>, params: LLMParams): void;
|
|
206
|
+
/**
|
|
207
|
+
* Normalize an OpenAI-shaped `tool_calls` array into Quilltap's `ToolCall`
|
|
208
|
+
* shape, tolerating servers that hand back already-parsed argument objects
|
|
209
|
+
* where the OpenAI wire format specifies a JSON string.
|
|
210
|
+
*/
|
|
211
|
+
protected normalizeToolCalls(toolCalls: readonly unknown[] | undefined | null): {
|
|
212
|
+
id: string;
|
|
213
|
+
type: 'function';
|
|
214
|
+
function: {
|
|
215
|
+
name: string;
|
|
216
|
+
arguments: string;
|
|
217
|
+
};
|
|
218
|
+
}[];
|
|
181
219
|
/**
|
|
182
220
|
* Sends a message and returns the complete response.
|
|
183
221
|
*
|
|
@@ -210,6 +248,64 @@ declare class OpenAICompatibleProvider implements TextProvider {
|
|
|
210
248
|
getAvailableModels(apiKey: string): Promise<string[]>;
|
|
211
249
|
}
|
|
212
250
|
|
|
251
|
+
/**
|
|
252
|
+
* Profile-parameter passthrough
|
|
253
|
+
*
|
|
254
|
+
* A connection profile's `parameters` blob is free-form JSON: the host stores
|
|
255
|
+
* whatever the profile editor (or a hand-edited profile) puts there and hands
|
|
256
|
+
* it back to the provider as `LLMParams.profileParameters` at call time. What
|
|
257
|
+
* reaches the wire is entirely the provider's business — and a provider that
|
|
258
|
+
* never reads the bag drops every setting in silence, which is exactly the
|
|
259
|
+
* defect Bug 71 records.
|
|
260
|
+
*
|
|
261
|
+
* This is the one mechanism every provider uses to forward those keys.
|
|
262
|
+
*
|
|
263
|
+
* **An exported function rather than a base-class method.** Only one plugin in
|
|
264
|
+
* the repository extends `OpenAICompatibleProvider` (DeepSeek); Z.AI and
|
|
265
|
+
* OpenRouter implement `TextProvider` directly, and Ollama speaks its own
|
|
266
|
+
* REST shape. A `protected` method would have reached one of them, so the
|
|
267
|
+
* mechanism is a free function usable by inheritance *and* composition, and
|
|
268
|
+
* the base class simply calls it.
|
|
269
|
+
*
|
|
270
|
+
* @packageDocumentation
|
|
271
|
+
*/
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Per-key hook applied to an allow-listed profile parameter on its way into
|
|
275
|
+
* the request body.
|
|
276
|
+
*
|
|
277
|
+
* Return the value to send, or `undefined` to omit the key entirely.
|
|
278
|
+
*
|
|
279
|
+
* The `body` is passed so a normalizer can place a value somewhere other than
|
|
280
|
+
* its own top-level key — the OpenAI-compatible plugin folds `reasoning_effort`
|
|
281
|
+
* into `chat_template_kwargs` this way, then returns `undefined` so no flat
|
|
282
|
+
* `reasoning_effort` key is sent. Normalizers that only reshape a value can
|
|
283
|
+
* ignore the argument.
|
|
284
|
+
*/
|
|
285
|
+
type ProfileParamNormalizer = (key: string, value: unknown, params: LLMParams, body: Record<string, unknown>) => unknown | undefined;
|
|
286
|
+
/**
|
|
287
|
+
* Copy allow-listed keys from `params.profileParameters` onto a request body.
|
|
288
|
+
*
|
|
289
|
+
* Rules, all of which predate this helper (they were hand-rolled identically in
|
|
290
|
+
* the DeepSeek and Z.AI providers before it existed):
|
|
291
|
+
*
|
|
292
|
+
* - **Allow-list, never spread.** `model`, `messages`, `stream`,
|
|
293
|
+
* `stream_options` and `tools` must be unreachable from a profile — a
|
|
294
|
+
* misconfigured bag cannot be allowed to retarget the request.
|
|
295
|
+
* - **`undefined` and `null` skip.** So does the empty string, which is what
|
|
296
|
+
* the schema-driven profile editor stores for "omit this and use the model
|
|
297
|
+
* default".
|
|
298
|
+
* - **The allow-list is ordered.** Keys are applied in the order given, so a
|
|
299
|
+
* normalizer that merges into an earlier key (`chat_template_kwargs`) sees
|
|
300
|
+
* whatever the profile set for it.
|
|
301
|
+
*
|
|
302
|
+
* @param body - Request body under construction; mutated in place
|
|
303
|
+
* @param params - The call's LLM parameters, carrying `profileParameters`
|
|
304
|
+
* @param allowlist - Keys this provider is willing to forward
|
|
305
|
+
* @param normalize - Optional per-key hook (see {@link ProfileParamNormalizer})
|
|
306
|
+
*/
|
|
307
|
+
declare function applyProfileParameters(body: Record<string, unknown>, params: LLMParams, allowlist: readonly string[], normalize?: ProfileParamNormalizer): void;
|
|
308
|
+
|
|
213
309
|
/**
|
|
214
310
|
* Per-request wall-clock budgets.
|
|
215
311
|
*
|
|
@@ -291,4 +387,4 @@ declare function buildSdkClientOptions(params: Pick<LLMParams, 'requestTimeoutMs
|
|
|
291
387
|
*/
|
|
292
388
|
declare function buildRequestAbortSignal(params: Pick<LLMParams, 'requestTimeoutMs'>, defaultMs?: number): AbortSignal;
|
|
293
389
|
|
|
294
|
-
export { DEFAULT_REQUEST_TIMEOUT_MS, OpenAICompatibleProvider, type OpenAICompatibleProviderConfig, buildRequestAbortSignal, buildSdkClientOptions, buildSdkRequestOptions, resolveRequestTimeoutMs };
|
|
390
|
+
export { DEFAULT_REQUEST_TIMEOUT_MS, OpenAICompatibleProvider, type OpenAICompatibleProviderConfig, type ProfileParamNormalizer, applyProfileParameters, buildRequestAbortSignal, buildSdkClientOptions, buildSdkRequestOptions, resolveRequestTimeoutMs };
|
package/dist/providers/index.js
CHANGED
|
@@ -32,6 +32,7 @@ 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,
|
|
@@ -131,6 +132,21 @@ function buildRequestAbortSignal(params, defaultMs = DEFAULT_REQUEST_TIMEOUT_MS)
|
|
|
131
132
|
return AbortSignal.timeout(resolveRequestTimeoutMs(params, defaultMs));
|
|
132
133
|
}
|
|
133
134
|
|
|
135
|
+
// src/providers/profile-parameters.ts
|
|
136
|
+
function applyProfileParameters(body, params, allowlist, normalize) {
|
|
137
|
+
const profile = params.profileParameters;
|
|
138
|
+
if (!profile || typeof profile !== "object") return;
|
|
139
|
+
const bag = profile;
|
|
140
|
+
for (const key of allowlist) {
|
|
141
|
+
const raw = bag[key];
|
|
142
|
+
if (raw === void 0 || raw === null) continue;
|
|
143
|
+
if (typeof raw === "string" && raw === "") continue;
|
|
144
|
+
const value = normalize ? normalize(key, raw, params, body) : raw;
|
|
145
|
+
if (value === void 0) continue;
|
|
146
|
+
body[key] = value;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
134
150
|
// src/providers/openai-compatible.ts
|
|
135
151
|
var DEFAULT_MAX_RETRIES = 2;
|
|
136
152
|
var OpenAICompatibleProvider = class {
|
|
@@ -146,6 +162,16 @@ var OpenAICompatibleProvider = class {
|
|
|
146
162
|
this.supportedMimeTypes = [];
|
|
147
163
|
/** Web search is not supported */
|
|
148
164
|
this.supportsWebSearch = false;
|
|
165
|
+
/**
|
|
166
|
+
* Profile-parameter keys this provider forwards from
|
|
167
|
+
* `LLMParams.profileParameters` onto the request body.
|
|
168
|
+
*
|
|
169
|
+
* **Empty by default**, so a subclass that declares nothing sends exactly the
|
|
170
|
+
* bytes it sent before this mechanism existed. Subclasses opt in by
|
|
171
|
+
* overriding with their own allow-list; `model`, `messages`, `stream`,
|
|
172
|
+
* `stream_options` and `tools` must never appear in one.
|
|
173
|
+
*/
|
|
174
|
+
this.profileParamAllowlist = [];
|
|
149
175
|
if (typeof config === "string") {
|
|
150
176
|
this.baseUrl = config;
|
|
151
177
|
this.providerName = "OpenAICompatible";
|
|
@@ -233,6 +259,53 @@ var OpenAICompatibleProvider = class {
|
|
|
233
259
|
buildRequestOptions(params) {
|
|
234
260
|
return buildSdkRequestOptions(params);
|
|
235
261
|
}
|
|
262
|
+
/**
|
|
263
|
+
* Per-key hook for allow-listed profile parameters. The default forwards the
|
|
264
|
+
* stored value verbatim.
|
|
265
|
+
*
|
|
266
|
+
* Override to reshape a value (the editor stores flat strings where some APIs
|
|
267
|
+
* want objects), to gate a key on the model, or — by writing into `body` and
|
|
268
|
+
* returning `undefined` — to send it under a different key entirely.
|
|
269
|
+
*/
|
|
270
|
+
normalizeProfileParam(_key, value, _params, _body) {
|
|
271
|
+
return value;
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* Apply {@link profileParamAllowlist} to a request body under construction.
|
|
275
|
+
* Called by both body builds so the streaming and non-streaming shapes cannot
|
|
276
|
+
* drift apart.
|
|
277
|
+
*/
|
|
278
|
+
applyProfileParams(body, params) {
|
|
279
|
+
applyProfileParameters(
|
|
280
|
+
body,
|
|
281
|
+
params,
|
|
282
|
+
this.profileParamAllowlist,
|
|
283
|
+
(key, value, callParams, target) => this.normalizeProfileParam(key, value, callParams, target)
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* Normalize an OpenAI-shaped `tool_calls` array into Quilltap's `ToolCall`
|
|
288
|
+
* shape, tolerating servers that hand back already-parsed argument objects
|
|
289
|
+
* where the OpenAI wire format specifies a JSON string.
|
|
290
|
+
*/
|
|
291
|
+
normalizeToolCalls(toolCalls) {
|
|
292
|
+
if (!toolCalls || toolCalls.length === 0) return [];
|
|
293
|
+
const out = [];
|
|
294
|
+
for (const raw of toolCalls) {
|
|
295
|
+
const tc = raw;
|
|
296
|
+
if (!tc?.function?.name) continue;
|
|
297
|
+
const args = tc.function.arguments;
|
|
298
|
+
out.push({
|
|
299
|
+
id: tc.id ?? "",
|
|
300
|
+
type: "function",
|
|
301
|
+
function: {
|
|
302
|
+
name: tc.function.name,
|
|
303
|
+
arguments: typeof args === "string" ? args : JSON.stringify(args ?? {})
|
|
304
|
+
}
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
return out;
|
|
308
|
+
}
|
|
236
309
|
/**
|
|
237
310
|
* Sends a message and returns the complete response.
|
|
238
311
|
*
|
|
@@ -271,17 +344,26 @@ var OpenAICompatibleProvider = class {
|
|
|
271
344
|
content: m.content
|
|
272
345
|
};
|
|
273
346
|
});
|
|
347
|
+
const body = {
|
|
348
|
+
model: params.model,
|
|
349
|
+
messages,
|
|
350
|
+
temperature: params.temperature ?? 0.7,
|
|
351
|
+
max_tokens: params.maxTokens ?? 4096,
|
|
352
|
+
top_p: params.topP ?? 1,
|
|
353
|
+
stop: params.stop,
|
|
354
|
+
...params.cacheKey ? { user: params.cacheKey } : {},
|
|
355
|
+
// Tools arrive only once the runtime gate (the profile's "Allow tool
|
|
356
|
+
// use") has passed, so no separate guard is needed here.
|
|
357
|
+
...params.tools && params.tools.length > 0 ? { tools: params.tools, tool_choice: params.toolChoice ?? "auto" } : {}
|
|
358
|
+
};
|
|
359
|
+
this.applyProfileParams(body, params);
|
|
274
360
|
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));
|
|
361
|
+
const response = await client.chat.completions.create(
|
|
362
|
+
body,
|
|
363
|
+
this.buildRequestOptions(params)
|
|
364
|
+
);
|
|
284
365
|
const choice = response.choices[0];
|
|
366
|
+
const toolCalls = this.normalizeToolCalls(choice.message.tool_calls);
|
|
285
367
|
return {
|
|
286
368
|
content: choice.message.content ?? "",
|
|
287
369
|
finishReason: choice.finish_reason,
|
|
@@ -291,7 +373,8 @@ var OpenAICompatibleProvider = class {
|
|
|
291
373
|
totalTokens: response.usage?.total_tokens ?? 0
|
|
292
374
|
},
|
|
293
375
|
raw: response,
|
|
294
|
-
attachmentResults
|
|
376
|
+
attachmentResults,
|
|
377
|
+
...toolCalls.length > 0 ? { toolCalls } : {}
|
|
295
378
|
};
|
|
296
379
|
} catch (error) {
|
|
297
380
|
this.logger.error(
|
|
@@ -340,23 +423,32 @@ var OpenAICompatibleProvider = class {
|
|
|
340
423
|
content: m.content
|
|
341
424
|
};
|
|
342
425
|
});
|
|
426
|
+
const body = {
|
|
427
|
+
model: params.model,
|
|
428
|
+
messages,
|
|
429
|
+
temperature: params.temperature ?? 0.7,
|
|
430
|
+
max_tokens: params.maxTokens ?? 4096,
|
|
431
|
+
top_p: params.topP ?? 1,
|
|
432
|
+
stop: params.stop,
|
|
433
|
+
stream: true,
|
|
434
|
+
stream_options: { include_usage: true },
|
|
435
|
+
...params.cacheKey ? { user: params.cacheKey } : {},
|
|
436
|
+
...params.tools && params.tools.length > 0 ? { tools: params.tools, tool_choice: params.toolChoice ?? "auto" } : {}
|
|
437
|
+
};
|
|
438
|
+
this.applyProfileParams(body, params);
|
|
343
439
|
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));
|
|
440
|
+
const stream = await client.chat.completions.create(
|
|
441
|
+
body,
|
|
442
|
+
this.buildRequestOptions(params)
|
|
443
|
+
);
|
|
355
444
|
let chunkCount = 0;
|
|
356
445
|
let accumulatedUsage = null;
|
|
446
|
+
const toolCallAccumulator = /* @__PURE__ */ new Map();
|
|
447
|
+
let finishReason = null;
|
|
357
448
|
for await (const chunk of stream) {
|
|
358
449
|
chunkCount++;
|
|
359
|
-
const
|
|
450
|
+
const choice = chunk.choices[0];
|
|
451
|
+
const content = choice?.delta?.content;
|
|
360
452
|
const hasUsage = chunk.usage;
|
|
361
453
|
if (hasUsage) {
|
|
362
454
|
accumulatedUsage = {
|
|
@@ -365,6 +457,17 @@ var OpenAICompatibleProvider = class {
|
|
|
365
457
|
total_tokens: chunk.usage?.total_tokens
|
|
366
458
|
};
|
|
367
459
|
}
|
|
460
|
+
if (choice?.delta?.tool_calls) {
|
|
461
|
+
for (const tcDelta of choice.delta.tool_calls) {
|
|
462
|
+
const idx = tcDelta.index ?? 0;
|
|
463
|
+
const existing = toolCallAccumulator.get(idx) ?? { id: "", name: "", arguments: "" };
|
|
464
|
+
if (tcDelta.id) existing.id = tcDelta.id;
|
|
465
|
+
if (tcDelta.function?.name) existing.name = tcDelta.function.name;
|
|
466
|
+
if (tcDelta.function?.arguments) existing.arguments += tcDelta.function.arguments;
|
|
467
|
+
toolCallAccumulator.set(idx, existing);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
if (choice?.finish_reason) finishReason = choice.finish_reason;
|
|
368
471
|
if (content) {
|
|
369
472
|
yield {
|
|
370
473
|
content,
|
|
@@ -372,6 +475,11 @@ var OpenAICompatibleProvider = class {
|
|
|
372
475
|
};
|
|
373
476
|
}
|
|
374
477
|
}
|
|
478
|
+
const toolCalls = Array.from(toolCallAccumulator.values()).filter((tc) => tc.name).map((tc) => ({
|
|
479
|
+
id: tc.id,
|
|
480
|
+
type: "function",
|
|
481
|
+
function: { name: tc.name, arguments: tc.arguments }
|
|
482
|
+
}));
|
|
375
483
|
yield {
|
|
376
484
|
content: "",
|
|
377
485
|
done: true,
|
|
@@ -380,7 +488,22 @@ var OpenAICompatibleProvider = class {
|
|
|
380
488
|
completionTokens: accumulatedUsage.completion_tokens ?? 0,
|
|
381
489
|
totalTokens: accumulatedUsage.total_tokens ?? 0
|
|
382
490
|
} : void 0,
|
|
383
|
-
attachmentResults
|
|
491
|
+
attachmentResults,
|
|
492
|
+
// Only synthesized when the stream actually carried tool calls: the
|
|
493
|
+
// host's tool detection reads `rawResponse`, and an empty one on every
|
|
494
|
+
// ordinary turn would be noise in the logs for no gain.
|
|
495
|
+
...toolCalls.length > 0 ? {
|
|
496
|
+
toolCalls,
|
|
497
|
+
rawResponse: {
|
|
498
|
+
choices: [
|
|
499
|
+
{
|
|
500
|
+
index: 0,
|
|
501
|
+
message: { role: "assistant", content: "", tool_calls: toolCalls },
|
|
502
|
+
finish_reason: finishReason
|
|
503
|
+
}
|
|
504
|
+
]
|
|
505
|
+
}
|
|
506
|
+
} : {}
|
|
384
507
|
};
|
|
385
508
|
} catch (error) {
|
|
386
509
|
this.logger.error(
|
|
@@ -446,6 +569,7 @@ var OpenAICompatibleProvider = class {
|
|
|
446
569
|
0 && (module.exports = {
|
|
447
570
|
DEFAULT_REQUEST_TIMEOUT_MS,
|
|
448
571
|
OpenAICompatibleProvider,
|
|
572
|
+
applyProfileParameters,
|
|
449
573
|
buildRequestAbortSignal,
|
|
450
574
|
buildSdkClientOptions,
|
|
451
575
|
buildSdkRequestOptions,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/providers/index.ts","../../src/providers/openai-compatible.ts","../../src/logging/plugin-logger.ts","../../src/logging/index.ts","../../src/version.ts","../../src/providers/request-budget.ts"],"sourcesContent":["/**\n * Provider Base Classes\n *\n * Reusable base classes for building LLM provider plugins.\n * External plugins can extend these classes to create custom providers\n * with minimal boilerplate.\n *\n * @packageDocumentation\n */\n\nexport {\n OpenAICompatibleProvider,\n type OpenAICompatibleProviderConfig,\n} from './openai-compatible';\n\nexport {\n DEFAULT_REQUEST_TIMEOUT_MS,\n resolveRequestTimeoutMs,\n buildSdkRequestOptions,\n buildSdkClientOptions,\n buildRequestAbortSignal,\n} from './request-budget';\n","/**\n * OpenAI-Compatible Provider Base Class\n *\n * A reusable base class for building LLM providers that use OpenAI-compatible APIs.\n * This includes services like:\n * - Local LLM servers (LM Studio, vLLM, Text Generation Web UI, Ollama with OpenAI compat)\n * - Cloud services with OpenAI-compatible APIs (Gab AI, Together AI, Fireworks, etc.)\n *\n * External plugins can extend this class to create custom providers with minimal code:\n *\n * @example\n * ```typescript\n * import { OpenAICompatibleProvider } from '@quilltap/plugin-utils';\n *\n * export class MyCustomProvider extends OpenAICompatibleProvider {\n * constructor() {\n * super({\n * baseUrl: 'https://api.my-service.com/v1',\n * providerName: 'MyService',\n * requireApiKey: true,\n * attachmentErrorMessage: 'MyService does not support file attachments',\n * });\n * }\n * }\n * ```\n *\n * @packageDocumentation\n */\n\nimport OpenAI from 'openai';\nimport type {\n TextProvider,\n LLMParams,\n LLMResponse,\n StreamChunk,\n PluginLogger,\n} from '@quilltap/plugin-types';\nimport { createPluginLogger } from '../logging';\nimport { getQuilltapUserAgent } from '../version';\nimport { DEFAULT_REQUEST_TIMEOUT_MS, buildSdkRequestOptions } from './request-budget';\n\n/** Matches the OpenAI SDK default; retries never apply to a caller-supplied budget. */\nconst DEFAULT_MAX_RETRIES = 2;\n\n/**\n * Configuration options for OpenAI-compatible providers.\n *\n * Use this interface when extending OpenAICompatibleProvider to customize\n * the provider's behavior for your specific service.\n */\nexport interface OpenAICompatibleProviderConfig {\n /**\n * Base URL for the API endpoint.\n * Should include the version path (e.g., 'https://api.example.com/v1')\n */\n baseUrl: string;\n\n /**\n * Provider name used for logging context.\n * This appears in log messages to identify which provider generated them.\n * @default 'OpenAICompatible'\n */\n providerName?: string;\n\n /**\n * Whether an API key is required for this provider.\n * If true, requests will fail with an error when no API key is provided.\n * If false, requests will use 'not-needed' as the API key (for local servers).\n * @default false\n */\n requireApiKey?: boolean;\n\n /**\n * Custom error message shown when file attachments are attempted.\n * @default 'OpenAI-compatible provider file attachment support varies by implementation (not yet implemented)'\n */\n attachmentErrorMessage?: string;\n\n /**\n * Default wall-clock budget per request, in milliseconds.\n *\n * The OpenAI SDK's own default is 600 000 ms, which means a service that\n * accepts a connection and then goes quiet holds the caller for ten minutes\n * before the first retry. That is far longer than any Quilltap caller wants\n * to wait, so the base class lowers it.\n *\n * Individual requests can tighten this further via `LLMParams.requestTimeoutMs`.\n *\n * @default 300000 (5 minutes)\n */\n requestTimeoutMs?: number;\n\n /**\n * How many times the SDK may retry a failed or timed-out request.\n *\n * Applies only to requests that did *not* specify their own\n * `LLMParams.requestTimeoutMs` — a caller-supplied budget is a hard ceiling\n * and never retries.\n *\n * @default 2 (the OpenAI SDK default)\n */\n maxRetries?: number;\n}\n\n/**\n * Base provider class for OpenAI-compatible APIs.\n *\n * This class implements the full LLMProvider interface using the OpenAI SDK,\n * allowing subclasses to create custom providers with just configuration.\n *\n * Features:\n * - Streaming and non-streaming chat completions\n * - API key validation\n * - Model listing\n * - Configurable API key requirements\n * - Dynamic logging with provider name context\n *\n * @remarks\n * File attachments and image generation are not supported by default,\n * as support varies across OpenAI-compatible implementations.\n */\nexport class OpenAICompatibleProvider implements TextProvider {\n /** File attachments are not supported by default */\n readonly supportsFileAttachments = false;\n /** No MIME types are supported for attachments */\n readonly supportedMimeTypes: string[] = [];\n /** Web search is not supported */\n readonly supportsWebSearch = false;\n\n /** Base URL for the API endpoint */\n protected readonly baseUrl: string;\n /** Provider name for logging */\n protected readonly providerName: string;\n /** Whether API key is required */\n protected readonly requireApiKey: boolean;\n /** Error message for attachment failures */\n protected readonly attachmentErrorMessage: string;\n /** Default per-request wall-clock budget in milliseconds */\n protected readonly requestTimeoutMs: number;\n /** Retry count for requests that carry no caller-supplied budget */\n protected readonly maxRetries: number;\n /** Logger instance */\n protected readonly logger: PluginLogger;\n\n /**\n * Creates a new OpenAI-compatible provider instance.\n *\n * @param config - Configuration object or base URL string (for backward compatibility)\n */\n constructor(config: string | OpenAICompatibleProviderConfig) {\n // Support both legacy string baseUrl and new config object\n if (typeof config === 'string') {\n this.baseUrl = config;\n this.providerName = 'OpenAICompatible';\n this.requireApiKey = false;\n this.attachmentErrorMessage =\n 'OpenAI-compatible provider file attachment support varies by implementation (not yet implemented)';\n this.requestTimeoutMs = DEFAULT_REQUEST_TIMEOUT_MS;\n this.maxRetries = DEFAULT_MAX_RETRIES;\n } else {\n this.baseUrl = config.baseUrl;\n this.providerName = config.providerName ?? 'OpenAICompatible';\n this.requireApiKey = config.requireApiKey ?? false;\n this.attachmentErrorMessage =\n config.attachmentErrorMessage ??\n 'OpenAI-compatible provider file attachment support varies by implementation (not yet implemented)';\n this.requestTimeoutMs = config.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;\n this.maxRetries = config.maxRetries ?? DEFAULT_MAX_RETRIES;\n }\n\n this.logger = createPluginLogger(`${this.providerName}Provider`);\n }\n\n /**\n * Collects attachment failures for messages with attachments.\n * Since attachments are not supported, all attachments are marked as failed.\n *\n * @param params - LLM parameters containing messages\n * @returns Object with empty sent array and failed attachments\n */\n protected collectAttachmentFailures(\n params: LLMParams\n ): { sent: string[]; failed: { id: string; error: string }[] } {\n const failed: { id: string; error: string }[] = [];\n for (const msg of params.messages) {\n if (msg.attachments) {\n for (const attachment of msg.attachments) {\n failed.push({\n id: attachment.id,\n error: this.attachmentErrorMessage,\n });\n }\n }\n }\n return { sent: [], failed };\n }\n\n /**\n * Validates that an API key is provided when required.\n * @throws Error if API key is required but not provided\n */\n protected validateApiKeyRequirement(apiKey: string): void {\n if (this.requireApiKey && !apiKey) {\n throw new Error(`${this.providerName} provider requires an API key`);\n }\n }\n\n /**\n * Gets the effective API key to use for requests.\n * Returns 'not-needed' for providers that don't require keys.\n */\n protected getEffectiveApiKey(apiKey: string): string {\n return this.requireApiKey ? apiKey : apiKey || 'not-needed';\n }\n\n /**\n * Creates an OpenAI client configured with the provider's base URL,\n * API key, and Quilltap User-Agent header.\n *\n * @param apiKey - API key for authentication\n * @returns Configured OpenAI client instance\n */\n protected createClient(apiKey: string): OpenAI {\n return new OpenAI({\n apiKey: this.getEffectiveApiKey(apiKey),\n baseURL: this.baseUrl,\n defaultHeaders: { 'User-Agent': getQuilltapUserAgent() },\n timeout: this.requestTimeoutMs,\n maxRetries: this.maxRetries,\n });\n }\n\n /**\n * Builds the per-request options for a single call.\n *\n * When the caller supplied `params.requestTimeoutMs` it is a hard ceiling:\n * the request uses that budget and does not retry, so the total time spent is\n * what the caller asked for rather than a multiple of it. Otherwise the\n * client-level defaults apply.\n *\n * Subclasses that build their own request bodies should pass the result as\n * the second argument to `client.chat.completions.create(...)`.\n *\n * @param params - LLM parameters for the request\n * @returns Request options for the OpenAI SDK, or undefined to use defaults\n */\n protected buildRequestOptions(\n params: LLMParams\n ): { timeout: number; maxRetries: number } | undefined {\n return buildSdkRequestOptions(params);\n }\n\n /**\n * Sends a message and returns the complete response.\n *\n * @param params - LLM parameters including messages, model, and settings\n * @param apiKey - API key for authentication\n * @returns Complete LLM response with content and usage statistics\n */\n async sendMessage(params: LLMParams, apiKey: string): Promise<LLMResponse> {\n this.validateApiKeyRequirement(apiKey);\n const attachmentResults = this.collectAttachmentFailures(params);\n\n const client = this.createClient(apiKey);\n\n // Map messages to OpenAI Chat Completions format, including tool messages\n const messages = params.messages\n .filter((m) => {\n // Skip tool messages without toolCallId (backward compat)\n if (m.role === 'tool' && !m.toolCallId) return false;\n return true;\n })\n .map((m) => {\n // Tool result messages\n if (m.role === 'tool' && m.toolCallId) {\n return {\n role: 'tool' as const,\n tool_call_id: m.toolCallId as string,\n content: m.content,\n };\n }\n // Assistant messages with tool calls\n if (m.role === 'assistant' && m.toolCalls && m.toolCalls.length > 0) {\n return {\n role: 'assistant' as const,\n content: m.content || null,\n tool_calls: m.toolCalls.map((tc: any) => ({\n id: tc.id,\n type: tc.type,\n function: tc.function,\n })),\n };\n }\n // Standard messages (strip attachments)\n return {\n role: m.role as 'system' | 'user' | 'assistant',\n content: m.content,\n };\n });\n\n try {\n const response = await client.chat.completions.create({\n model: params.model,\n messages,\n temperature: params.temperature ?? 0.7,\n max_tokens: params.maxTokens ?? 4096,\n top_p: params.topP ?? 1,\n stop: params.stop,\n ...(params.cacheKey ? { user: params.cacheKey } : {}),\n }, this.buildRequestOptions(params));\n\n const choice = response.choices[0];\n return {\n content: choice.message.content ?? '',\n finishReason: choice.finish_reason,\n usage: {\n promptTokens: response.usage?.prompt_tokens ?? 0,\n completionTokens: response.usage?.completion_tokens ?? 0,\n totalTokens: response.usage?.total_tokens ?? 0,\n },\n raw: response,\n attachmentResults,\n };\n } catch (error) {\n this.logger.error(\n `${this.providerName} API error in sendMessage`,\n { context: `${this.providerName}Provider.sendMessage`, baseUrl: this.baseUrl },\n error instanceof Error ? error : undefined\n );\n throw error;\n }\n }\n\n /**\n * Sends a message and streams the response.\n *\n * @param params - LLM parameters including messages, model, and settings\n * @param apiKey - API key for authentication\n * @yields Stream chunks with content and final usage statistics\n */\n async *streamMessage(params: LLMParams, apiKey: string): AsyncGenerator<StreamChunk> {\n this.validateApiKeyRequirement(apiKey);\n const attachmentResults = this.collectAttachmentFailures(params);\n\n const client = this.createClient(apiKey);\n\n // Map messages to OpenAI Chat Completions format, including tool messages\n const messages = params.messages\n .filter((m) => {\n // Skip tool messages without toolCallId (backward compat)\n if (m.role === 'tool' && !m.toolCallId) return false;\n return true;\n })\n .map((m) => {\n // Tool result messages\n if (m.role === 'tool' && m.toolCallId) {\n return {\n role: 'tool' as const,\n tool_call_id: m.toolCallId as string,\n content: m.content,\n };\n }\n // Assistant messages with tool calls\n if (m.role === 'assistant' && m.toolCalls && m.toolCalls.length > 0) {\n return {\n role: 'assistant' as const,\n content: m.content || null,\n tool_calls: m.toolCalls.map((tc: any) => ({\n id: tc.id,\n type: tc.type,\n function: tc.function,\n })),\n };\n }\n // Standard messages (strip attachments)\n return {\n role: m.role as 'system' | 'user' | 'assistant',\n content: m.content,\n };\n });\n\n try {\n const stream = await client.chat.completions.create({\n model: params.model,\n messages,\n temperature: params.temperature ?? 0.7,\n max_tokens: params.maxTokens ?? 4096,\n top_p: params.topP ?? 1,\n stop: params.stop,\n stream: true,\n stream_options: { include_usage: true },\n ...(params.cacheKey ? { user: params.cacheKey } : {}),\n }, this.buildRequestOptions(params));\n\n let chunkCount = 0;\n\n // Track usage - it may come in a separate final chunk\n let accumulatedUsage: { prompt_tokens?: number; completion_tokens?: number; total_tokens?: number } | null = null;\n\n for await (const chunk of stream) {\n chunkCount++;\n const content = chunk.choices[0]?.delta?.content;\n const hasUsage = chunk.usage;\n\n // Track usage when we get it (may come in a separate final chunk)\n if (hasUsage) {\n accumulatedUsage = {\n prompt_tokens: chunk.usage?.prompt_tokens,\n completion_tokens: chunk.usage?.completion_tokens,\n total_tokens: chunk.usage?.total_tokens,\n };\n }\n\n // Yield content chunks\n if (content) {\n yield {\n content,\n done: false,\n };\n }\n }\n\n // After stream ends, yield final chunk with accumulated usage\n yield {\n content: '',\n done: true,\n usage: accumulatedUsage ? {\n promptTokens: accumulatedUsage.prompt_tokens ?? 0,\n completionTokens: accumulatedUsage.completion_tokens ?? 0,\n totalTokens: accumulatedUsage.total_tokens ?? 0,\n } : undefined,\n attachmentResults,\n };\n } catch (error) {\n this.logger.error(\n `${this.providerName} API error in streamMessage`,\n { context: `${this.providerName}Provider.streamMessage`, baseUrl: this.baseUrl },\n error instanceof Error ? error : undefined\n );\n throw error;\n }\n }\n\n /**\n * Validates an API key by attempting to list models.\n *\n * @param apiKey - API key to validate\n * @returns true if the API key is valid, false otherwise\n */\n async validateApiKey(apiKey: string): Promise<boolean> {\n // For providers that require API key, return false if not provided\n if (this.requireApiKey && !apiKey) {\n return false;\n }\n\n try {\n const client = this.createClient(apiKey);\n await client.models.list();\n return true;\n } catch (error) {\n this.logger.error(\n `${this.providerName} API validation failed`,\n { context: `${this.providerName}Provider.validateApiKey`, baseUrl: this.baseUrl },\n error instanceof Error ? error : undefined\n );\n return false;\n }\n }\n\n /**\n * Fetches available models from the API.\n *\n * @param apiKey - API key for authentication\n * @returns Sorted array of model IDs, or empty array on failure\n */\n async getAvailableModels(apiKey: string): Promise<string[]> {\n // For providers that require API key, return empty if not provided\n if (this.requireApiKey && !apiKey) {\n this.logger.error(`${this.providerName} provider requires an API key to fetch models`, {\n context: `${this.providerName}Provider.getAvailableModels`,\n });\n return [];\n }\n\n try {\n const client = this.createClient(apiKey);\n const models = await client.models.list();\n const modelList = models.data.map((m) => m.id).sort();\n return modelList;\n } catch (error) {\n this.logger.error(\n `Failed to fetch ${this.providerName} models`,\n { context: `${this.providerName}Provider.getAvailableModels`, baseUrl: this.baseUrl },\n error instanceof Error ? error : undefined\n );\n return [];\n }\n }\n\n}\n","/**\n * Plugin Logger Bridge\n *\n * Provides a logger factory for plugins that automatically bridges\n * to Quilltap's core logging when running inside the host application,\n * or falls back to console logging when running standalone.\n *\n * @module @quilltap/plugin-utils/logging/plugin-logger\n */\n\nimport type { PluginLogger, LogContext, LogLevel } from '@quilltap/plugin-types';\n\n/**\n * Extended logger interface with child logger support\n */\nexport interface PluginLoggerWithChild extends PluginLogger {\n /**\n * Create a child logger with additional context\n * @param additionalContext Context to merge with parent context\n * @returns A new logger with combined context\n */\n child(additionalContext: LogContext): PluginLoggerWithChild;\n}\n\n/**\n * Type for the global Quilltap logger bridge\n * Stored on globalThis to work across different npm package copies\n */\ndeclare global {\n \n var __quilltap_logger_factory:\n | ((pluginName: string) => PluginLoggerWithChild)\n | undefined;\n}\n\n/**\n * Get the core logger factory from global namespace\n *\n * @returns The injected factory or null if not in Quilltap environment\n */\nfunction getCoreLoggerFactory(): ((pluginName: string) => PluginLoggerWithChild) | null {\n return globalThis.__quilltap_logger_factory ?? null;\n}\n\n/**\n * Inject the core logger factory from Quilltap host\n *\n * This is called by Quilltap core when loading plugins to bridge\n * plugin logging into the host's logging system. Uses globalThis\n * to ensure it works even when plugins have their own copy of\n * plugin-utils in their node_modules.\n *\n * **Internal API - not for plugin use**\n *\n * @param factory A function that creates a child logger for a plugin\n */\nexport function __injectCoreLoggerFactory(\n factory: (pluginName: string) => PluginLoggerWithChild\n): void {\n globalThis.__quilltap_logger_factory = factory;\n}\n\n/**\n * Clear the injected core logger factory\n *\n * Useful for testing or when unloading the plugin system.\n *\n * **Internal API - not for plugin use**\n */\nexport function __clearCoreLoggerFactory(): void {\n globalThis.__quilltap_logger_factory = undefined;\n}\n\n/**\n * Check if a core logger has been injected\n *\n * @returns True if running inside Quilltap with core logging available\n */\nexport function hasCoreLogger(): boolean {\n return getCoreLoggerFactory() !== null;\n}\n\n/**\n * Create a console logger with child support\n *\n * @param prefix Logger prefix\n * @param minLevel Minimum log level\n * @param baseContext Base context to include in all logs\n */\nfunction createConsoleLoggerWithChild(\n prefix: string,\n minLevel: LogLevel = 'debug',\n baseContext: LogContext = {}\n): PluginLoggerWithChild {\n const levels: LogLevel[] = ['debug', 'info', 'warn', 'error'];\n const shouldLog = (level: LogLevel): boolean =>\n levels.indexOf(level) >= levels.indexOf(minLevel);\n\n const formatContext = (context?: LogContext): string => {\n const merged = { ...baseContext, ...context };\n const entries = Object.entries(merged)\n .filter(([key]) => key !== 'context')\n .map(([key, value]) => `${key}=${JSON.stringify(value)}`)\n .join(' ');\n return entries ? ` ${entries}` : '';\n };\n\n const logger: PluginLoggerWithChild = {\n debug: (message: string, context?: LogContext): void => {\n if (shouldLog('debug')) {\n console.debug(`[${prefix}] ${message}${formatContext(context)}`);\n }\n },\n\n info: (message: string, context?: LogContext): void => {\n if (shouldLog('info')) {\n console.info(`[${prefix}] ${message}${formatContext(context)}`);\n }\n },\n\n warn: (message: string, context?: LogContext): void => {\n if (shouldLog('warn')) {\n console.warn(`[${prefix}] ${message}${formatContext(context)}`);\n }\n },\n\n error: (message: string, context?: LogContext, error?: Error): void => {\n if (shouldLog('error')) {\n console.error(\n `[${prefix}] ${message}${formatContext(context)}`,\n error ? `\\n${error.stack || error.message}` : ''\n );\n }\n },\n\n child: (additionalContext: LogContext): PluginLoggerWithChild => {\n return createConsoleLoggerWithChild(prefix, minLevel, {\n ...baseContext,\n ...additionalContext,\n });\n },\n };\n\n return logger;\n}\n\n/**\n * Create a plugin logger that bridges to Quilltap core logging\n *\n * When running inside Quilltap:\n * - Routes all logs to the core logger\n * - Tags logs with `{ plugin: pluginName, module: 'plugin' }`\n * - Logs appear in Quilltap's combined.log and console\n *\n * When running standalone:\n * - Falls back to console logging with `[pluginName]` prefix\n * - Respects the specified minimum log level\n *\n * @param pluginName - The plugin identifier (e.g., 'qtap-plugin-openai')\n * @param minLevel - Minimum log level when running standalone (default: 'debug')\n * @returns A logger instance\n *\n * @example\n * ```typescript\n * // In your plugin's provider.ts\n * import { createPluginLogger } from '@quilltap/plugin-utils';\n *\n * const logger = createPluginLogger('qtap-plugin-my-provider');\n *\n * export class MyProvider implements LLMProvider {\n * async sendMessage(params: LLMParams, apiKey: string): Promise<LLMResponse> {\n * logger.debug('Sending message', { model: params.model });\n *\n * try {\n * const response = await this.client.chat({...});\n * logger.info('Received response', { tokens: response.usage?.total_tokens });\n * return response;\n * } catch (error) {\n * logger.error('Failed to send message', { model: params.model }, error as Error);\n * throw error;\n * }\n * }\n * }\n * ```\n */\nexport function createPluginLogger(\n pluginName: string,\n minLevel: LogLevel = 'debug'\n): PluginLoggerWithChild {\n // Check for core logger factory from global namespace\n const coreFactory = getCoreLoggerFactory();\n if (coreFactory) {\n return coreFactory(pluginName);\n }\n\n // Standalone mode: use enhanced console logger\n return createConsoleLoggerWithChild(pluginName, minLevel);\n}\n\n/**\n * Get the minimum log level from environment\n *\n * Checks for LOG_LEVEL or QUILLTAP_LOG_LEVEL environment variables.\n * Useful for configuring standalone plugin logging.\n *\n * @returns The configured log level, or 'info' as default\n */\nexport function getLogLevelFromEnv(): LogLevel {\n if (typeof process !== 'undefined' && process.env) {\n const envLevel = process.env.LOG_LEVEL || process.env.QUILLTAP_LOG_LEVEL;\n if (envLevel && ['debug', 'info', 'warn', 'error'].includes(envLevel)) {\n return envLevel as LogLevel;\n }\n }\n return 'info';\n}\n","/**\n * Logging Utilities\n *\n * Exports the plugin logger bridge and related utilities.\n *\n * @module @quilltap/plugin-utils/logging\n */\n\nexport {\n createPluginLogger,\n hasCoreLogger,\n getLogLevelFromEnv,\n __injectCoreLoggerFactory,\n __clearCoreLoggerFactory,\n} from './plugin-logger';\n\nexport type { PluginLoggerWithChild } from './plugin-logger';\n\n// Re-export logger types from plugin-types\nexport type { PluginLogger, LogContext, LogLevel } from '@quilltap/plugin-types';\n\n// Re-export logger utilities from plugin-types for convenience\nexport { createConsoleLogger, createNoopLogger } from '@quilltap/plugin-types';\n","/**\n * Quilltap Version Management\n *\n * Provides access to the Quilltap application version for plugins.\n * Uses the same globalThis injection pattern as the logger factory,\n * allowing the host app to inject its version at startup.\n *\n * @module @quilltap/plugin-utils/version\n */\n\n/**\n * Global key used to store the injected Quilltap version.\n * Must match the key used by the host app's injection code.\n */\nconst GLOBAL_VERSION_KEY = '__quilltap_app_version';\n\n/**\n * Inject the Quilltap application version into the global namespace.\n *\n * This should be called early in plugin initialization (alongside the\n * logger factory injection), before any plugins are loaded. Plugins\n * can then retrieve the version via `getQuilltapVersion()`.\n *\n * @param version - The Quilltap application version string (e.g., '3.3.0')\n *\n * @example\n * ```typescript\n * import { __injectQuilltapVersion } from '@quilltap/plugin-utils';\n * import packageJson from './package.json';\n *\n * __injectQuilltapVersion(packageJson.version);\n * ```\n */\nexport function __injectQuilltapVersion(version: string): void {\n (globalThis as Record<string, unknown>)[GLOBAL_VERSION_KEY] = version;\n}\n\n/**\n * Clear the injected Quilltap version from the global namespace.\n *\n * Useful for testing or cleanup.\n */\nexport function __clearQuilltapVersion(): void {\n (globalThis as Record<string, unknown>)[GLOBAL_VERSION_KEY] = undefined;\n}\n\n/**\n * Get the Quilltap application version.\n *\n * Returns the version injected by the host app, or 'unknown' if\n * no version has been injected (e.g., running outside of Quilltap).\n *\n * @returns The Quilltap version string (e.g., '3.3.0') or 'unknown'\n */\nexport function getQuilltapVersion(): string {\n const version = (globalThis as Record<string, unknown>)[GLOBAL_VERSION_KEY];\n return typeof version === 'string' ? version : 'unknown';\n}\n\n/**\n * Get the Quilltap User-Agent string for API requests.\n *\n * Returns a string in the format `Quilltap/{version}` suitable for\n * use as a User-Agent header or app identifier in API calls.\n *\n * @returns User-Agent string (e.g., 'Quilltap/3.3.0')\n *\n * @example\n * ```typescript\n * import { getQuilltapUserAgent } from '@quilltap/plugin-utils';\n *\n * const client = new OpenAI({\n * apiKey,\n * defaultHeaders: { 'User-Agent': getQuilltapUserAgent() },\n * });\n * ```\n */\nexport function getQuilltapUserAgent(): string {\n return `Quilltap/${getQuilltapVersion()}`;\n}\n","/**\n * Per-request wall-clock budgets.\n *\n * Every provider SDK ships a generous default request timeout — the OpenAI and\n * Anthropic SDKs both use ten minutes — on the assumption that a slow answer is\n * better than no answer. That assumption is wrong for work Quilltap awaits\n * inline on a user-visible turn: a service that accepts the connection and then\n * goes silent holds the turn for the full ten minutes before the SDK's first\n * retry, with nothing logged, because request logs are only written when a call\n * finishes.\n *\n * `LLMParams.requestTimeoutMs` is the caller's answer to \"how long is this\n * worth waiting for\". These helpers are the single place that decides what that\n * number means, so an SDK-backed provider and a `fetch`-based one apply it the\n * same way.\n *\n * @packageDocumentation\n */\n\nimport type { LLMParams } from '@quilltap/plugin-types';\n\n/**\n * Fallback budget for a request whose caller expressed no preference.\n *\n * Half the SDK default: long enough for a slow non-streaming generation, short\n * enough that a silently-stalled endpoint fails in minutes rather than tens of\n * minutes.\n */\nexport const DEFAULT_REQUEST_TIMEOUT_MS = 300_000;\n\n/**\n * The budget for one request, in milliseconds.\n *\n * @param params - LLM parameters, whose `requestTimeoutMs` wins when set\n * @param defaultMs - Budget to use when the caller named none\n * @returns The effective budget in milliseconds\n */\nexport function resolveRequestTimeoutMs(\n params: Pick<LLMParams, 'requestTimeoutMs'>,\n defaultMs: number = DEFAULT_REQUEST_TIMEOUT_MS\n): number {\n const requested = params.requestTimeoutMs;\n return typeof requested === 'number' && requested > 0 ? requested : defaultMs;\n}\n\n/**\n * Per-request options for an OpenAI- or Anthropic-style SDK call.\n *\n * A caller-supplied budget is a *ceiling*, not a per-attempt allowance, so\n * retries are disabled for it — three attempts at the requested budget would\n * spend three times what the caller agreed to. Returns undefined when the\n * caller named no budget, leaving the client's own defaults in charge.\n *\n * @param params - LLM parameters for the request\n * @returns SDK request options, or undefined to use the client defaults\n */\nexport function buildSdkRequestOptions(\n params: Pick<LLMParams, 'requestTimeoutMs'>\n): { timeout: number; maxRetries: number } | undefined {\n const requested = params.requestTimeoutMs;\n if (typeof requested !== 'number' || requested <= 0) return undefined;\n return { timeout: requested, maxRetries: 0 };\n}\n\n/**\n * Client-construction options for a provider that builds a fresh SDK client\n * per call, where \"client default\" and \"this request\" are the same thing.\n *\n * Always defined, unlike {@link buildSdkRequestOptions} — a per-call client has\n * no pre-existing defaults worth preserving, so it takes the resolved budget\n * either way and only forgoes retries when the caller set a ceiling.\n *\n * @param params - LLM parameters for the request\n * @param defaultMs - Budget to use when the caller named none\n * @returns `timeout` and `maxRetries` to spread into the SDK constructor\n */\nexport function buildSdkClientOptions(\n params: Pick<LLMParams, 'requestTimeoutMs'>,\n defaultMs: number = DEFAULT_REQUEST_TIMEOUT_MS\n): { timeout: number; maxRetries: number } {\n const capped = typeof params.requestTimeoutMs === 'number' && params.requestTimeoutMs > 0;\n return {\n timeout: resolveRequestTimeoutMs(params, defaultMs),\n maxRetries: capped ? 0 : 2,\n };\n}\n\n/**\n * An abort signal that fires at the request's budget, for providers that call\n * `fetch` directly rather than through an SDK.\n *\n * Unlike the SDK options above this bounds the *whole* exchange, body included:\n * `fetch` resolves at the response headers, but the signal stays live while the\n * body streams. Pass a generous `defaultMs` on streaming paths, or compose with\n * the caller's own signal, so a long generation is not cut off mid-answer.\n *\n * @param params - LLM parameters for the request\n * @param defaultMs - Budget to use when the caller named none\n * @returns An AbortSignal that aborts once the budget elapses\n */\nexport function buildRequestAbortSignal(\n params: Pick<LLMParams, 'requestTimeoutMs'>,\n defaultMs: number = DEFAULT_REQUEST_TIMEOUT_MS\n): AbortSignal {\n return AbortSignal.timeout(resolveRequestTimeoutMs(params, defaultMs));\n}\n"],"mappings":"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","names":["OpenAI"]}
|
|
1
|
+
{"version":3,"sources":["../../src/providers/index.ts","../../src/providers/openai-compatible.ts","../../src/logging/plugin-logger.ts","../../src/logging/index.ts","../../src/version.ts","../../src/providers/request-budget.ts","../../src/providers/profile-parameters.ts"],"sourcesContent":["/**\n * Provider Base Classes\n *\n * Reusable base classes for building LLM provider plugins.\n * External plugins can extend these classes to create custom providers\n * with minimal boilerplate.\n *\n * @packageDocumentation\n */\n\nexport {\n OpenAICompatibleProvider,\n type OpenAICompatibleProviderConfig,\n} from './openai-compatible';\n\nexport {\n applyProfileParameters,\n type ProfileParamNormalizer,\n} from './profile-parameters';\n\nexport {\n DEFAULT_REQUEST_TIMEOUT_MS,\n resolveRequestTimeoutMs,\n buildSdkRequestOptions,\n buildSdkClientOptions,\n buildRequestAbortSignal,\n} from './request-budget';\n","/**\n * OpenAI-Compatible Provider Base Class\n *\n * A reusable base class for building LLM providers that use OpenAI-compatible APIs.\n * This includes services like:\n * - Local LLM servers (LM Studio, vLLM, Text Generation Web UI, Ollama with OpenAI compat)\n * - Cloud services with OpenAI-compatible APIs (Gab AI, Together AI, Fireworks, etc.)\n *\n * External plugins can extend this class to create custom providers with minimal code:\n *\n * @example\n * ```typescript\n * import { OpenAICompatibleProvider } from '@quilltap/plugin-utils';\n *\n * export class MyCustomProvider extends OpenAICompatibleProvider {\n * constructor() {\n * super({\n * baseUrl: 'https://api.my-service.com/v1',\n * providerName: 'MyService',\n * requireApiKey: true,\n * attachmentErrorMessage: 'MyService does not support file attachments',\n * });\n * }\n * }\n * ```\n *\n * @packageDocumentation\n */\n\nimport OpenAI from 'openai';\nimport type {\n TextProvider,\n LLMParams,\n LLMResponse,\n StreamChunk,\n PluginLogger,\n} from '@quilltap/plugin-types';\nimport { createPluginLogger } from '../logging';\nimport { getQuilltapUserAgent } from '../version';\nimport { DEFAULT_REQUEST_TIMEOUT_MS, buildSdkRequestOptions } from './request-budget';\nimport { applyProfileParameters } from './profile-parameters';\n\n/** Matches the OpenAI SDK default; retries never apply to a caller-supplied budget. */\nconst DEFAULT_MAX_RETRIES = 2;\n\n/**\n * Configuration options for OpenAI-compatible providers.\n *\n * Use this interface when extending OpenAICompatibleProvider to customize\n * the provider's behavior for your specific service.\n */\nexport interface OpenAICompatibleProviderConfig {\n /**\n * Base URL for the API endpoint.\n * Should include the version path (e.g., 'https://api.example.com/v1')\n */\n baseUrl: string;\n\n /**\n * Provider name used for logging context.\n * This appears in log messages to identify which provider generated them.\n * @default 'OpenAICompatible'\n */\n providerName?: string;\n\n /**\n * Whether an API key is required for this provider.\n * If true, requests will fail with an error when no API key is provided.\n * If false, requests will use 'not-needed' as the API key (for local servers).\n * @default false\n */\n requireApiKey?: boolean;\n\n /**\n * Custom error message shown when file attachments are attempted.\n * @default 'OpenAI-compatible provider file attachment support varies by implementation (not yet implemented)'\n */\n attachmentErrorMessage?: string;\n\n /**\n * Default wall-clock budget per request, in milliseconds.\n *\n * The OpenAI SDK's own default is 600 000 ms, which means a service that\n * accepts a connection and then goes quiet holds the caller for ten minutes\n * before the first retry. That is far longer than any Quilltap caller wants\n * to wait, so the base class lowers it.\n *\n * Individual requests can tighten this further via `LLMParams.requestTimeoutMs`.\n *\n * @default 300000 (5 minutes)\n */\n requestTimeoutMs?: number;\n\n /**\n * How many times the SDK may retry a failed or timed-out request.\n *\n * Applies only to requests that did *not* specify their own\n * `LLMParams.requestTimeoutMs` — a caller-supplied budget is a hard ceiling\n * and never retries.\n *\n * @default 2 (the OpenAI SDK default)\n */\n maxRetries?: number;\n}\n\n/**\n * Base provider class for OpenAI-compatible APIs.\n *\n * This class implements the full LLMProvider interface using the OpenAI SDK,\n * allowing subclasses to create custom providers with just configuration.\n *\n * Features:\n * - Streaming and non-streaming chat completions\n * - API key validation\n * - Model listing\n * - Configurable API key requirements\n * - Dynamic logging with provider name context\n *\n * @remarks\n * File attachments and image generation are not supported by default,\n * as support varies across OpenAI-compatible implementations.\n */\nexport class OpenAICompatibleProvider implements TextProvider {\n /** File attachments are not supported by default */\n readonly supportsFileAttachments = false;\n /** No MIME types are supported for attachments */\n readonly supportedMimeTypes: string[] = [];\n /** Web search is not supported */\n readonly supportsWebSearch = false;\n\n /** Base URL for the API endpoint */\n protected readonly baseUrl: string;\n /** Provider name for logging */\n protected readonly providerName: string;\n /** Whether API key is required */\n protected readonly requireApiKey: boolean;\n /** Error message for attachment failures */\n protected readonly attachmentErrorMessage: string;\n /** Default per-request wall-clock budget in milliseconds */\n protected readonly requestTimeoutMs: number;\n /** Retry count for requests that carry no caller-supplied budget */\n protected readonly maxRetries: number;\n /** Logger instance */\n protected readonly logger: PluginLogger;\n\n /**\n * Creates a new OpenAI-compatible provider instance.\n *\n * @param config - Configuration object or base URL string (for backward compatibility)\n */\n constructor(config: string | OpenAICompatibleProviderConfig) {\n // Support both legacy string baseUrl and new config object\n if (typeof config === 'string') {\n this.baseUrl = config;\n this.providerName = 'OpenAICompatible';\n this.requireApiKey = false;\n this.attachmentErrorMessage =\n 'OpenAI-compatible provider file attachment support varies by implementation (not yet implemented)';\n this.requestTimeoutMs = DEFAULT_REQUEST_TIMEOUT_MS;\n this.maxRetries = DEFAULT_MAX_RETRIES;\n } else {\n this.baseUrl = config.baseUrl;\n this.providerName = config.providerName ?? 'OpenAICompatible';\n this.requireApiKey = config.requireApiKey ?? false;\n this.attachmentErrorMessage =\n config.attachmentErrorMessage ??\n 'OpenAI-compatible provider file attachment support varies by implementation (not yet implemented)';\n this.requestTimeoutMs = config.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;\n this.maxRetries = config.maxRetries ?? DEFAULT_MAX_RETRIES;\n }\n\n this.logger = createPluginLogger(`${this.providerName}Provider`);\n }\n\n /**\n * Collects attachment failures for messages with attachments.\n * Since attachments are not supported, all attachments are marked as failed.\n *\n * @param params - LLM parameters containing messages\n * @returns Object with empty sent array and failed attachments\n */\n protected collectAttachmentFailures(\n params: LLMParams\n ): { sent: string[]; failed: { id: string; error: string }[] } {\n const failed: { id: string; error: string }[] = [];\n for (const msg of params.messages) {\n if (msg.attachments) {\n for (const attachment of msg.attachments) {\n failed.push({\n id: attachment.id,\n error: this.attachmentErrorMessage,\n });\n }\n }\n }\n return { sent: [], failed };\n }\n\n /**\n * Validates that an API key is provided when required.\n * @throws Error if API key is required but not provided\n */\n protected validateApiKeyRequirement(apiKey: string): void {\n if (this.requireApiKey && !apiKey) {\n throw new Error(`${this.providerName} provider requires an API key`);\n }\n }\n\n /**\n * Gets the effective API key to use for requests.\n * Returns 'not-needed' for providers that don't require keys.\n */\n protected getEffectiveApiKey(apiKey: string): string {\n return this.requireApiKey ? apiKey : apiKey || 'not-needed';\n }\n\n /**\n * Creates an OpenAI client configured with the provider's base URL,\n * API key, and Quilltap User-Agent header.\n *\n * @param apiKey - API key for authentication\n * @returns Configured OpenAI client instance\n */\n protected createClient(apiKey: string): OpenAI {\n return new OpenAI({\n apiKey: this.getEffectiveApiKey(apiKey),\n baseURL: this.baseUrl,\n defaultHeaders: { 'User-Agent': getQuilltapUserAgent() },\n timeout: this.requestTimeoutMs,\n maxRetries: this.maxRetries,\n });\n }\n\n /**\n * Builds the per-request options for a single call.\n *\n * When the caller supplied `params.requestTimeoutMs` it is a hard ceiling:\n * the request uses that budget and does not retry, so the total time spent is\n * what the caller asked for rather than a multiple of it. Otherwise the\n * client-level defaults apply.\n *\n * Subclasses that build their own request bodies should pass the result as\n * the second argument to `client.chat.completions.create(...)`.\n *\n * @param params - LLM parameters for the request\n * @returns Request options for the OpenAI SDK, or undefined to use defaults\n */\n protected buildRequestOptions(\n params: LLMParams\n ): { timeout: number; maxRetries: number } | undefined {\n return buildSdkRequestOptions(params);\n }\n\n /**\n * Profile-parameter keys this provider forwards from\n * `LLMParams.profileParameters` onto the request body.\n *\n * **Empty by default**, so a subclass that declares nothing sends exactly the\n * bytes it sent before this mechanism existed. Subclasses opt in by\n * overriding with their own allow-list; `model`, `messages`, `stream`,\n * `stream_options` and `tools` must never appear in one.\n */\n protected readonly profileParamAllowlist: readonly string[] = [];\n\n /**\n * Per-key hook for allow-listed profile parameters. The default forwards the\n * stored value verbatim.\n *\n * Override to reshape a value (the editor stores flat strings where some APIs\n * want objects), to gate a key on the model, or — by writing into `body` and\n * returning `undefined` — to send it under a different key entirely.\n */\n protected normalizeProfileParam(\n _key: string,\n value: unknown,\n _params: LLMParams,\n _body: Record<string, unknown>\n ): unknown | undefined {\n return value;\n }\n\n /**\n * Apply {@link profileParamAllowlist} to a request body under construction.\n * Called by both body builds so the streaming and non-streaming shapes cannot\n * drift apart.\n */\n protected applyProfileParams(body: Record<string, unknown>, params: LLMParams): void {\n applyProfileParameters(\n body,\n params,\n this.profileParamAllowlist,\n (key, value, callParams, target) =>\n this.normalizeProfileParam(key, value, callParams, target)\n );\n }\n\n /**\n * Normalize an OpenAI-shaped `tool_calls` array into Quilltap's `ToolCall`\n * shape, tolerating servers that hand back already-parsed argument objects\n * where the OpenAI wire format specifies a JSON string.\n */\n protected normalizeToolCalls(\n toolCalls: readonly unknown[] | undefined | null\n ): { id: string; type: 'function'; function: { name: string; arguments: string } }[] {\n if (!toolCalls || toolCalls.length === 0) return [];\n const out: { id: string; type: 'function'; function: { name: string; arguments: string } }[] = [];\n for (const raw of toolCalls) {\n const tc = raw as {\n id?: string;\n function?: { name?: string; arguments?: unknown };\n };\n if (!tc?.function?.name) continue;\n const args = tc.function.arguments;\n out.push({\n id: tc.id ?? '',\n type: 'function',\n function: {\n name: tc.function.name,\n arguments: typeof args === 'string' ? args : JSON.stringify(args ?? {}),\n },\n });\n }\n return out;\n }\n\n /**\n * Sends a message and returns the complete response.\n *\n * @param params - LLM parameters including messages, model, and settings\n * @param apiKey - API key for authentication\n * @returns Complete LLM response with content and usage statistics\n */\n async sendMessage(params: LLMParams, apiKey: string): Promise<LLMResponse> {\n this.validateApiKeyRequirement(apiKey);\n const attachmentResults = this.collectAttachmentFailures(params);\n\n const client = this.createClient(apiKey);\n\n // Map messages to OpenAI Chat Completions format, including tool messages\n const messages = params.messages\n .filter((m) => {\n // Skip tool messages without toolCallId (backward compat)\n if (m.role === 'tool' && !m.toolCallId) return false;\n return true;\n })\n .map((m) => {\n // Tool result messages\n if (m.role === 'tool' && m.toolCallId) {\n return {\n role: 'tool' as const,\n tool_call_id: m.toolCallId as string,\n content: m.content,\n };\n }\n // Assistant messages with tool calls\n if (m.role === 'assistant' && m.toolCalls && m.toolCalls.length > 0) {\n return {\n role: 'assistant' as const,\n content: m.content || null,\n tool_calls: m.toolCalls.map((tc: any) => ({\n id: tc.id,\n type: tc.type,\n function: tc.function,\n })),\n };\n }\n // Standard messages (strip attachments)\n return {\n role: m.role as 'system' | 'user' | 'assistant',\n content: m.content,\n };\n });\n\n // Built as a Record so the allow-list can write non-standard keys the\n // OpenAI SDK's own types don't know about (`chat_template_kwargs`, `top_k`,\n // …). Widening happens once, here, rather than as `as any` at each write.\n const body: Record<string, unknown> = {\n model: params.model,\n messages,\n temperature: params.temperature ?? 0.7,\n max_tokens: params.maxTokens ?? 4096,\n top_p: params.topP ?? 1,\n stop: params.stop,\n ...(params.cacheKey ? { user: params.cacheKey } : {}),\n // Tools arrive only once the runtime gate (the profile's \"Allow tool\n // use\") has passed, so no separate guard is needed here.\n ...(params.tools && params.tools.length > 0\n ? { tools: params.tools, tool_choice: params.toolChoice ?? 'auto' }\n : {}),\n };\n this.applyProfileParams(body, params);\n\n try {\n const response = (await client.chat.completions.create(\n body as unknown as OpenAI.Chat.Completions.ChatCompletionCreateParamsNonStreaming,\n this.buildRequestOptions(params)\n )) as OpenAI.Chat.Completions.ChatCompletion;\n\n const choice = response.choices[0];\n const toolCalls = this.normalizeToolCalls(choice.message.tool_calls);\n return {\n content: choice.message.content ?? '',\n finishReason: choice.finish_reason,\n usage: {\n promptTokens: response.usage?.prompt_tokens ?? 0,\n completionTokens: response.usage?.completion_tokens ?? 0,\n totalTokens: response.usage?.total_tokens ?? 0,\n },\n raw: response,\n attachmentResults,\n ...(toolCalls.length > 0 ? { toolCalls } : {}),\n };\n } catch (error) {\n this.logger.error(\n `${this.providerName} API error in sendMessage`,\n { context: `${this.providerName}Provider.sendMessage`, baseUrl: this.baseUrl },\n error instanceof Error ? error : undefined\n );\n throw error;\n }\n }\n\n /**\n * Sends a message and streams the response.\n *\n * @param params - LLM parameters including messages, model, and settings\n * @param apiKey - API key for authentication\n * @yields Stream chunks with content and final usage statistics\n */\n async *streamMessage(params: LLMParams, apiKey: string): AsyncGenerator<StreamChunk> {\n this.validateApiKeyRequirement(apiKey);\n const attachmentResults = this.collectAttachmentFailures(params);\n\n const client = this.createClient(apiKey);\n\n // Map messages to OpenAI Chat Completions format, including tool messages\n const messages = params.messages\n .filter((m) => {\n // Skip tool messages without toolCallId (backward compat)\n if (m.role === 'tool' && !m.toolCallId) return false;\n return true;\n })\n .map((m) => {\n // Tool result messages\n if (m.role === 'tool' && m.toolCallId) {\n return {\n role: 'tool' as const,\n tool_call_id: m.toolCallId as string,\n content: m.content,\n };\n }\n // Assistant messages with tool calls\n if (m.role === 'assistant' && m.toolCalls && m.toolCalls.length > 0) {\n return {\n role: 'assistant' as const,\n content: m.content || null,\n tool_calls: m.toolCalls.map((tc: any) => ({\n id: tc.id,\n type: tc.type,\n function: tc.function,\n })),\n };\n }\n // Standard messages (strip attachments)\n return {\n role: m.role as 'system' | 'user' | 'assistant',\n content: m.content,\n };\n });\n\n // See the note in sendMessage: a Record so non-standard allow-listed keys\n // are writable without scattering casts.\n const body: Record<string, unknown> = {\n model: params.model,\n messages,\n temperature: params.temperature ?? 0.7,\n max_tokens: params.maxTokens ?? 4096,\n top_p: params.topP ?? 1,\n stop: params.stop,\n stream: true,\n stream_options: { include_usage: true },\n ...(params.cacheKey ? { user: params.cacheKey } : {}),\n ...(params.tools && params.tools.length > 0\n ? { tools: params.tools, tool_choice: params.toolChoice ?? 'auto' }\n : {}),\n };\n this.applyProfileParams(body, params);\n\n try {\n const stream = (await client.chat.completions.create(\n body as unknown as OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming,\n this.buildRequestOptions(params)\n )) as AsyncIterable<OpenAI.Chat.Completions.ChatCompletionChunk>;\n\n let chunkCount = 0;\n\n // Track usage - it may come in a separate final chunk\n let accumulatedUsage: { prompt_tokens?: number; completion_tokens?: number; total_tokens?: number } | null = null;\n // Tool-call fragments arrive index-keyed, with `arguments` split across\n // deltas as raw string pieces; accumulate per index and assemble at the\n // end rather than trying to parse mid-stream.\n const toolCallAccumulator = new Map<number, { id: string; name: string; arguments: string }>();\n let finishReason: string | null = null;\n\n for await (const chunk of stream) {\n chunkCount++;\n const choice = chunk.choices[0];\n const content = choice?.delta?.content;\n const hasUsage = chunk.usage;\n\n // Track usage when we get it (may come in a separate final chunk)\n if (hasUsage) {\n accumulatedUsage = {\n prompt_tokens: chunk.usage?.prompt_tokens,\n completion_tokens: chunk.usage?.completion_tokens,\n total_tokens: chunk.usage?.total_tokens,\n };\n }\n\n if (choice?.delta?.tool_calls) {\n for (const tcDelta of choice.delta.tool_calls) {\n const idx = tcDelta.index ?? 0;\n const existing = toolCallAccumulator.get(idx) ?? { id: '', name: '', arguments: '' };\n if (tcDelta.id) existing.id = tcDelta.id;\n if (tcDelta.function?.name) existing.name = tcDelta.function.name;\n if (tcDelta.function?.arguments) existing.arguments += tcDelta.function.arguments;\n toolCallAccumulator.set(idx, existing);\n }\n }\n\n if (choice?.finish_reason) finishReason = choice.finish_reason;\n\n // Yield content chunks\n if (content) {\n yield {\n content,\n done: false,\n };\n }\n }\n\n const toolCalls = Array.from(toolCallAccumulator.values())\n .filter((tc) => tc.name)\n .map((tc) => ({\n id: tc.id,\n type: 'function' as const,\n function: { name: tc.name, arguments: tc.arguments },\n }));\n\n // After stream ends, yield final chunk with accumulated usage\n yield {\n content: '',\n done: true,\n usage: accumulatedUsage ? {\n promptTokens: accumulatedUsage.prompt_tokens ?? 0,\n completionTokens: accumulatedUsage.completion_tokens ?? 0,\n totalTokens: accumulatedUsage.total_tokens ?? 0,\n } : undefined,\n attachmentResults,\n // Only synthesized when the stream actually carried tool calls: the\n // host's tool detection reads `rawResponse`, and an empty one on every\n // ordinary turn would be noise in the logs for no gain.\n ...(toolCalls.length > 0\n ? {\n toolCalls,\n rawResponse: {\n choices: [\n {\n index: 0,\n message: { role: 'assistant', content: '', tool_calls: toolCalls },\n finish_reason: finishReason,\n },\n ],\n },\n }\n : {}),\n };\n } catch (error) {\n this.logger.error(\n `${this.providerName} API error in streamMessage`,\n { context: `${this.providerName}Provider.streamMessage`, baseUrl: this.baseUrl },\n error instanceof Error ? error : undefined\n );\n throw error;\n }\n }\n\n /**\n * Validates an API key by attempting to list models.\n *\n * @param apiKey - API key to validate\n * @returns true if the API key is valid, false otherwise\n */\n async validateApiKey(apiKey: string): Promise<boolean> {\n // For providers that require API key, return false if not provided\n if (this.requireApiKey && !apiKey) {\n return false;\n }\n\n try {\n const client = this.createClient(apiKey);\n await client.models.list();\n return true;\n } catch (error) {\n this.logger.error(\n `${this.providerName} API validation failed`,\n { context: `${this.providerName}Provider.validateApiKey`, baseUrl: this.baseUrl },\n error instanceof Error ? error : undefined\n );\n return false;\n }\n }\n\n /**\n * Fetches available models from the API.\n *\n * @param apiKey - API key for authentication\n * @returns Sorted array of model IDs, or empty array on failure\n */\n async getAvailableModels(apiKey: string): Promise<string[]> {\n // For providers that require API key, return empty if not provided\n if (this.requireApiKey && !apiKey) {\n this.logger.error(`${this.providerName} provider requires an API key to fetch models`, {\n context: `${this.providerName}Provider.getAvailableModels`,\n });\n return [];\n }\n\n try {\n const client = this.createClient(apiKey);\n const models = await client.models.list();\n const modelList = models.data.map((m) => m.id).sort();\n return modelList;\n } catch (error) {\n this.logger.error(\n `Failed to fetch ${this.providerName} models`,\n { context: `${this.providerName}Provider.getAvailableModels`, baseUrl: this.baseUrl },\n error instanceof Error ? error : undefined\n );\n return [];\n }\n }\n\n}\n","/**\n * Plugin Logger Bridge\n *\n * Provides a logger factory for plugins that automatically bridges\n * to Quilltap's core logging when running inside the host application,\n * or falls back to console logging when running standalone.\n *\n * @module @quilltap/plugin-utils/logging/plugin-logger\n */\n\nimport type { PluginLogger, LogContext, LogLevel } from '@quilltap/plugin-types';\n\n/**\n * Extended logger interface with child logger support\n */\nexport interface PluginLoggerWithChild extends PluginLogger {\n /**\n * Create a child logger with additional context\n * @param additionalContext Context to merge with parent context\n * @returns A new logger with combined context\n */\n child(additionalContext: LogContext): PluginLoggerWithChild;\n}\n\n/**\n * Type for the global Quilltap logger bridge\n * Stored on globalThis to work across different npm package copies\n */\ndeclare global {\n \n var __quilltap_logger_factory:\n | ((pluginName: string) => PluginLoggerWithChild)\n | undefined;\n}\n\n/**\n * Get the core logger factory from global namespace\n *\n * @returns The injected factory or null if not in Quilltap environment\n */\nfunction getCoreLoggerFactory(): ((pluginName: string) => PluginLoggerWithChild) | null {\n return globalThis.__quilltap_logger_factory ?? null;\n}\n\n/**\n * Inject the core logger factory from Quilltap host\n *\n * This is called by Quilltap core when loading plugins to bridge\n * plugin logging into the host's logging system. Uses globalThis\n * to ensure it works even when plugins have their own copy of\n * plugin-utils in their node_modules.\n *\n * **Internal API - not for plugin use**\n *\n * @param factory A function that creates a child logger for a plugin\n */\nexport function __injectCoreLoggerFactory(\n factory: (pluginName: string) => PluginLoggerWithChild\n): void {\n globalThis.__quilltap_logger_factory = factory;\n}\n\n/**\n * Clear the injected core logger factory\n *\n * Useful for testing or when unloading the plugin system.\n *\n * **Internal API - not for plugin use**\n */\nexport function __clearCoreLoggerFactory(): void {\n globalThis.__quilltap_logger_factory = undefined;\n}\n\n/**\n * Check if a core logger has been injected\n *\n * @returns True if running inside Quilltap with core logging available\n */\nexport function hasCoreLogger(): boolean {\n return getCoreLoggerFactory() !== null;\n}\n\n/**\n * Create a console logger with child support\n *\n * @param prefix Logger prefix\n * @param minLevel Minimum log level\n * @param baseContext Base context to include in all logs\n */\nfunction createConsoleLoggerWithChild(\n prefix: string,\n minLevel: LogLevel = 'debug',\n baseContext: LogContext = {}\n): PluginLoggerWithChild {\n const levels: LogLevel[] = ['debug', 'info', 'warn', 'error'];\n const shouldLog = (level: LogLevel): boolean =>\n levels.indexOf(level) >= levels.indexOf(minLevel);\n\n const formatContext = (context?: LogContext): string => {\n const merged = { ...baseContext, ...context };\n const entries = Object.entries(merged)\n .filter(([key]) => key !== 'context')\n .map(([key, value]) => `${key}=${JSON.stringify(value)}`)\n .join(' ');\n return entries ? ` ${entries}` : '';\n };\n\n const logger: PluginLoggerWithChild = {\n debug: (message: string, context?: LogContext): void => {\n if (shouldLog('debug')) {\n console.debug(`[${prefix}] ${message}${formatContext(context)}`);\n }\n },\n\n info: (message: string, context?: LogContext): void => {\n if (shouldLog('info')) {\n console.info(`[${prefix}] ${message}${formatContext(context)}`);\n }\n },\n\n warn: (message: string, context?: LogContext): void => {\n if (shouldLog('warn')) {\n console.warn(`[${prefix}] ${message}${formatContext(context)}`);\n }\n },\n\n error: (message: string, context?: LogContext, error?: Error): void => {\n if (shouldLog('error')) {\n console.error(\n `[${prefix}] ${message}${formatContext(context)}`,\n error ? `\\n${error.stack || error.message}` : ''\n );\n }\n },\n\n child: (additionalContext: LogContext): PluginLoggerWithChild => {\n return createConsoleLoggerWithChild(prefix, minLevel, {\n ...baseContext,\n ...additionalContext,\n });\n },\n };\n\n return logger;\n}\n\n/**\n * Create a plugin logger that bridges to Quilltap core logging\n *\n * When running inside Quilltap:\n * - Routes all logs to the core logger\n * - Tags logs with `{ plugin: pluginName, module: 'plugin' }`\n * - Logs appear in Quilltap's combined.log and console\n *\n * When running standalone:\n * - Falls back to console logging with `[pluginName]` prefix\n * - Respects the specified minimum log level\n *\n * @param pluginName - The plugin identifier (e.g., 'qtap-plugin-openai')\n * @param minLevel - Minimum log level when running standalone (default: 'debug')\n * @returns A logger instance\n *\n * @example\n * ```typescript\n * // In your plugin's provider.ts\n * import { createPluginLogger } from '@quilltap/plugin-utils';\n *\n * const logger = createPluginLogger('qtap-plugin-my-provider');\n *\n * export class MyProvider implements LLMProvider {\n * async sendMessage(params: LLMParams, apiKey: string): Promise<LLMResponse> {\n * logger.debug('Sending message', { model: params.model });\n *\n * try {\n * const response = await this.client.chat({...});\n * logger.info('Received response', { tokens: response.usage?.total_tokens });\n * return response;\n * } catch (error) {\n * logger.error('Failed to send message', { model: params.model }, error as Error);\n * throw error;\n * }\n * }\n * }\n * ```\n */\nexport function createPluginLogger(\n pluginName: string,\n minLevel: LogLevel = 'debug'\n): PluginLoggerWithChild {\n // Check for core logger factory from global namespace\n const coreFactory = getCoreLoggerFactory();\n if (coreFactory) {\n return coreFactory(pluginName);\n }\n\n // Standalone mode: use enhanced console logger\n return createConsoleLoggerWithChild(pluginName, minLevel);\n}\n\n/**\n * Get the minimum log level from environment\n *\n * Checks for LOG_LEVEL or QUILLTAP_LOG_LEVEL environment variables.\n * Useful for configuring standalone plugin logging.\n *\n * @returns The configured log level, or 'info' as default\n */\nexport function getLogLevelFromEnv(): LogLevel {\n if (typeof process !== 'undefined' && process.env) {\n const envLevel = process.env.LOG_LEVEL || process.env.QUILLTAP_LOG_LEVEL;\n if (envLevel && ['debug', 'info', 'warn', 'error'].includes(envLevel)) {\n return envLevel as LogLevel;\n }\n }\n return 'info';\n}\n","/**\n * Logging Utilities\n *\n * Exports the plugin logger bridge and related utilities.\n *\n * @module @quilltap/plugin-utils/logging\n */\n\nexport {\n createPluginLogger,\n hasCoreLogger,\n getLogLevelFromEnv,\n __injectCoreLoggerFactory,\n __clearCoreLoggerFactory,\n} from './plugin-logger';\n\nexport type { PluginLoggerWithChild } from './plugin-logger';\n\n// Re-export logger types from plugin-types\nexport type { PluginLogger, LogContext, LogLevel } from '@quilltap/plugin-types';\n\n// Re-export logger utilities from plugin-types for convenience\nexport { createConsoleLogger, createNoopLogger } from '@quilltap/plugin-types';\n","/**\n * Quilltap Version Management\n *\n * Provides access to the Quilltap application version for plugins.\n * Uses the same globalThis injection pattern as the logger factory,\n * allowing the host app to inject its version at startup.\n *\n * @module @quilltap/plugin-utils/version\n */\n\n/**\n * Global key used to store the injected Quilltap version.\n * Must match the key used by the host app's injection code.\n */\nconst GLOBAL_VERSION_KEY = '__quilltap_app_version';\n\n/**\n * Inject the Quilltap application version into the global namespace.\n *\n * This should be called early in plugin initialization (alongside the\n * logger factory injection), before any plugins are loaded. Plugins\n * can then retrieve the version via `getQuilltapVersion()`.\n *\n * @param version - The Quilltap application version string (e.g., '3.3.0')\n *\n * @example\n * ```typescript\n * import { __injectQuilltapVersion } from '@quilltap/plugin-utils';\n * import packageJson from './package.json';\n *\n * __injectQuilltapVersion(packageJson.version);\n * ```\n */\nexport function __injectQuilltapVersion(version: string): void {\n (globalThis as Record<string, unknown>)[GLOBAL_VERSION_KEY] = version;\n}\n\n/**\n * Clear the injected Quilltap version from the global namespace.\n *\n * Useful for testing or cleanup.\n */\nexport function __clearQuilltapVersion(): void {\n (globalThis as Record<string, unknown>)[GLOBAL_VERSION_KEY] = undefined;\n}\n\n/**\n * Get the Quilltap application version.\n *\n * Returns the version injected by the host app, or 'unknown' if\n * no version has been injected (e.g., running outside of Quilltap).\n *\n * @returns The Quilltap version string (e.g., '3.3.0') or 'unknown'\n */\nexport function getQuilltapVersion(): string {\n const version = (globalThis as Record<string, unknown>)[GLOBAL_VERSION_KEY];\n return typeof version === 'string' ? version : 'unknown';\n}\n\n/**\n * Get the Quilltap User-Agent string for API requests.\n *\n * Returns a string in the format `Quilltap/{version}` suitable for\n * use as a User-Agent header or app identifier in API calls.\n *\n * @returns User-Agent string (e.g., 'Quilltap/3.3.0')\n *\n * @example\n * ```typescript\n * import { getQuilltapUserAgent } from '@quilltap/plugin-utils';\n *\n * const client = new OpenAI({\n * apiKey,\n * defaultHeaders: { 'User-Agent': getQuilltapUserAgent() },\n * });\n * ```\n */\nexport function getQuilltapUserAgent(): string {\n return `Quilltap/${getQuilltapVersion()}`;\n}\n","/**\n * Per-request wall-clock budgets.\n *\n * Every provider SDK ships a generous default request timeout — the OpenAI and\n * Anthropic SDKs both use ten minutes — on the assumption that a slow answer is\n * better than no answer. That assumption is wrong for work Quilltap awaits\n * inline on a user-visible turn: a service that accepts the connection and then\n * goes silent holds the turn for the full ten minutes before the SDK's first\n * retry, with nothing logged, because request logs are only written when a call\n * finishes.\n *\n * `LLMParams.requestTimeoutMs` is the caller's answer to \"how long is this\n * worth waiting for\". These helpers are the single place that decides what that\n * number means, so an SDK-backed provider and a `fetch`-based one apply it the\n * same way.\n *\n * @packageDocumentation\n */\n\nimport type { LLMParams } from '@quilltap/plugin-types';\n\n/**\n * Fallback budget for a request whose caller expressed no preference.\n *\n * Half the SDK default: long enough for a slow non-streaming generation, short\n * enough that a silently-stalled endpoint fails in minutes rather than tens of\n * minutes.\n */\nexport const DEFAULT_REQUEST_TIMEOUT_MS = 300_000;\n\n/**\n * The budget for one request, in milliseconds.\n *\n * @param params - LLM parameters, whose `requestTimeoutMs` wins when set\n * @param defaultMs - Budget to use when the caller named none\n * @returns The effective budget in milliseconds\n */\nexport function resolveRequestTimeoutMs(\n params: Pick<LLMParams, 'requestTimeoutMs'>,\n defaultMs: number = DEFAULT_REQUEST_TIMEOUT_MS\n): number {\n const requested = params.requestTimeoutMs;\n return typeof requested === 'number' && requested > 0 ? requested : defaultMs;\n}\n\n/**\n * Per-request options for an OpenAI- or Anthropic-style SDK call.\n *\n * A caller-supplied budget is a *ceiling*, not a per-attempt allowance, so\n * retries are disabled for it — three attempts at the requested budget would\n * spend three times what the caller agreed to. Returns undefined when the\n * caller named no budget, leaving the client's own defaults in charge.\n *\n * @param params - LLM parameters for the request\n * @returns SDK request options, or undefined to use the client defaults\n */\nexport function buildSdkRequestOptions(\n params: Pick<LLMParams, 'requestTimeoutMs'>\n): { timeout: number; maxRetries: number } | undefined {\n const requested = params.requestTimeoutMs;\n if (typeof requested !== 'number' || requested <= 0) return undefined;\n return { timeout: requested, maxRetries: 0 };\n}\n\n/**\n * Client-construction options for a provider that builds a fresh SDK client\n * per call, where \"client default\" and \"this request\" are the same thing.\n *\n * Always defined, unlike {@link buildSdkRequestOptions} — a per-call client has\n * no pre-existing defaults worth preserving, so it takes the resolved budget\n * either way and only forgoes retries when the caller set a ceiling.\n *\n * @param params - LLM parameters for the request\n * @param defaultMs - Budget to use when the caller named none\n * @returns `timeout` and `maxRetries` to spread into the SDK constructor\n */\nexport function buildSdkClientOptions(\n params: Pick<LLMParams, 'requestTimeoutMs'>,\n defaultMs: number = DEFAULT_REQUEST_TIMEOUT_MS\n): { timeout: number; maxRetries: number } {\n const capped = typeof params.requestTimeoutMs === 'number' && params.requestTimeoutMs > 0;\n return {\n timeout: resolveRequestTimeoutMs(params, defaultMs),\n maxRetries: capped ? 0 : 2,\n };\n}\n\n/**\n * An abort signal that fires at the request's budget, for providers that call\n * `fetch` directly rather than through an SDK.\n *\n * Unlike the SDK options above this bounds the *whole* exchange, body included:\n * `fetch` resolves at the response headers, but the signal stays live while the\n * body streams. Pass a generous `defaultMs` on streaming paths, or compose with\n * the caller's own signal, so a long generation is not cut off mid-answer.\n *\n * @param params - LLM parameters for the request\n * @param defaultMs - Budget to use when the caller named none\n * @returns An AbortSignal that aborts once the budget elapses\n */\nexport function buildRequestAbortSignal(\n params: Pick<LLMParams, 'requestTimeoutMs'>,\n defaultMs: number = DEFAULT_REQUEST_TIMEOUT_MS\n): AbortSignal {\n return AbortSignal.timeout(resolveRequestTimeoutMs(params, defaultMs));\n}\n","/**\n * Profile-parameter passthrough\n *\n * A connection profile's `parameters` blob is free-form JSON: the host stores\n * whatever the profile editor (or a hand-edited profile) puts there and hands\n * it back to the provider as `LLMParams.profileParameters` at call time. What\n * reaches the wire is entirely the provider's business — and a provider that\n * never reads the bag drops every setting in silence, which is exactly the\n * defect Bug 71 records.\n *\n * This is the one mechanism every provider uses to forward those keys.\n *\n * **An exported function rather than a base-class method.** Only one plugin in\n * the repository extends `OpenAICompatibleProvider` (DeepSeek); Z.AI and\n * OpenRouter implement `TextProvider` directly, and Ollama speaks its own\n * REST shape. A `protected` method would have reached one of them, so the\n * mechanism is a free function usable by inheritance *and* composition, and\n * the base class simply calls it.\n *\n * @packageDocumentation\n */\n\nimport type { LLMParams } from '@quilltap/plugin-types';\n\n/**\n * Per-key hook applied to an allow-listed profile parameter on its way into\n * the request body.\n *\n * Return the value to send, or `undefined` to omit the key entirely.\n *\n * The `body` is passed so a normalizer can place a value somewhere other than\n * its own top-level key — the OpenAI-compatible plugin folds `reasoning_effort`\n * into `chat_template_kwargs` this way, then returns `undefined` so no flat\n * `reasoning_effort` key is sent. Normalizers that only reshape a value can\n * ignore the argument.\n */\nexport type ProfileParamNormalizer = (\n key: string,\n value: unknown,\n params: LLMParams,\n body: Record<string, unknown>\n) => unknown | undefined;\n\n/**\n * Copy allow-listed keys from `params.profileParameters` onto a request body.\n *\n * Rules, all of which predate this helper (they were hand-rolled identically in\n * the DeepSeek and Z.AI providers before it existed):\n *\n * - **Allow-list, never spread.** `model`, `messages`, `stream`,\n * `stream_options` and `tools` must be unreachable from a profile — a\n * misconfigured bag cannot be allowed to retarget the request.\n * - **`undefined` and `null` skip.** So does the empty string, which is what\n * the schema-driven profile editor stores for \"omit this and use the model\n * default\".\n * - **The allow-list is ordered.** Keys are applied in the order given, so a\n * normalizer that merges into an earlier key (`chat_template_kwargs`) sees\n * whatever the profile set for it.\n *\n * @param body - Request body under construction; mutated in place\n * @param params - The call's LLM parameters, carrying `profileParameters`\n * @param allowlist - Keys this provider is willing to forward\n * @param normalize - Optional per-key hook (see {@link ProfileParamNormalizer})\n */\nexport function applyProfileParameters(\n body: Record<string, unknown>,\n params: LLMParams,\n allowlist: readonly string[],\n normalize?: ProfileParamNormalizer\n): void {\n const profile = params.profileParameters;\n if (!profile || typeof profile !== 'object') return;\n const bag = profile as Record<string, unknown>;\n\n for (const key of allowlist) {\n const raw = bag[key];\n if (raw === undefined || raw === null) continue;\n // Empty string from the schema-driven profile editor means \"omit the\n // parameter and use the model default.\"\n if (typeof raw === 'string' && raw === '') continue;\n const value = normalize ? normalize(key, raw, params, body) : raw;\n if (value === undefined) continue;\n body[key] = value;\n }\n}\n"],"mappings":"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","names":["OpenAI"]}
|