@quilltap/plugin-utils 2.2.20 → 2.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/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 +1 -1
package/dist/providers/index.mjs
CHANGED
|
@@ -90,6 +90,21 @@ function buildRequestAbortSignal(params, defaultMs = DEFAULT_REQUEST_TIMEOUT_MS)
|
|
|
90
90
|
return AbortSignal.timeout(resolveRequestTimeoutMs(params, defaultMs));
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
+
// src/providers/profile-parameters.ts
|
|
94
|
+
function applyProfileParameters(body, params, allowlist, normalize) {
|
|
95
|
+
const profile = params.profileParameters;
|
|
96
|
+
if (!profile || typeof profile !== "object") return;
|
|
97
|
+
const bag = profile;
|
|
98
|
+
for (const key of allowlist) {
|
|
99
|
+
const raw = bag[key];
|
|
100
|
+
if (raw === void 0 || raw === null) continue;
|
|
101
|
+
if (typeof raw === "string" && raw === "") continue;
|
|
102
|
+
const value = normalize ? normalize(key, raw, params, body) : raw;
|
|
103
|
+
if (value === void 0) continue;
|
|
104
|
+
body[key] = value;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
93
108
|
// src/providers/openai-compatible.ts
|
|
94
109
|
var DEFAULT_MAX_RETRIES = 2;
|
|
95
110
|
var OpenAICompatibleProvider = class {
|
|
@@ -105,6 +120,16 @@ var OpenAICompatibleProvider = class {
|
|
|
105
120
|
this.supportedMimeTypes = [];
|
|
106
121
|
/** Web search is not supported */
|
|
107
122
|
this.supportsWebSearch = false;
|
|
123
|
+
/**
|
|
124
|
+
* Profile-parameter keys this provider forwards from
|
|
125
|
+
* `LLMParams.profileParameters` onto the request body.
|
|
126
|
+
*
|
|
127
|
+
* **Empty by default**, so a subclass that declares nothing sends exactly the
|
|
128
|
+
* bytes it sent before this mechanism existed. Subclasses opt in by
|
|
129
|
+
* overriding with their own allow-list; `model`, `messages`, `stream`,
|
|
130
|
+
* `stream_options` and `tools` must never appear in one.
|
|
131
|
+
*/
|
|
132
|
+
this.profileParamAllowlist = [];
|
|
108
133
|
if (typeof config === "string") {
|
|
109
134
|
this.baseUrl = config;
|
|
110
135
|
this.providerName = "OpenAICompatible";
|
|
@@ -192,6 +217,53 @@ var OpenAICompatibleProvider = class {
|
|
|
192
217
|
buildRequestOptions(params) {
|
|
193
218
|
return buildSdkRequestOptions(params);
|
|
194
219
|
}
|
|
220
|
+
/**
|
|
221
|
+
* Per-key hook for allow-listed profile parameters. The default forwards the
|
|
222
|
+
* stored value verbatim.
|
|
223
|
+
*
|
|
224
|
+
* Override to reshape a value (the editor stores flat strings where some APIs
|
|
225
|
+
* want objects), to gate a key on the model, or — by writing into `body` and
|
|
226
|
+
* returning `undefined` — to send it under a different key entirely.
|
|
227
|
+
*/
|
|
228
|
+
normalizeProfileParam(_key, value, _params, _body) {
|
|
229
|
+
return value;
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* Apply {@link profileParamAllowlist} to a request body under construction.
|
|
233
|
+
* Called by both body builds so the streaming and non-streaming shapes cannot
|
|
234
|
+
* drift apart.
|
|
235
|
+
*/
|
|
236
|
+
applyProfileParams(body, params) {
|
|
237
|
+
applyProfileParameters(
|
|
238
|
+
body,
|
|
239
|
+
params,
|
|
240
|
+
this.profileParamAllowlist,
|
|
241
|
+
(key, value, callParams, target) => this.normalizeProfileParam(key, value, callParams, target)
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* Normalize an OpenAI-shaped `tool_calls` array into Quilltap's `ToolCall`
|
|
246
|
+
* shape, tolerating servers that hand back already-parsed argument objects
|
|
247
|
+
* where the OpenAI wire format specifies a JSON string.
|
|
248
|
+
*/
|
|
249
|
+
normalizeToolCalls(toolCalls) {
|
|
250
|
+
if (!toolCalls || toolCalls.length === 0) return [];
|
|
251
|
+
const out = [];
|
|
252
|
+
for (const raw of toolCalls) {
|
|
253
|
+
const tc = raw;
|
|
254
|
+
if (!tc?.function?.name) continue;
|
|
255
|
+
const args = tc.function.arguments;
|
|
256
|
+
out.push({
|
|
257
|
+
id: tc.id ?? "",
|
|
258
|
+
type: "function",
|
|
259
|
+
function: {
|
|
260
|
+
name: tc.function.name,
|
|
261
|
+
arguments: typeof args === "string" ? args : JSON.stringify(args ?? {})
|
|
262
|
+
}
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
return out;
|
|
266
|
+
}
|
|
195
267
|
/**
|
|
196
268
|
* Sends a message and returns the complete response.
|
|
197
269
|
*
|
|
@@ -230,17 +302,26 @@ var OpenAICompatibleProvider = class {
|
|
|
230
302
|
content: m.content
|
|
231
303
|
};
|
|
232
304
|
});
|
|
305
|
+
const body = {
|
|
306
|
+
model: params.model,
|
|
307
|
+
messages,
|
|
308
|
+
temperature: params.temperature ?? 0.7,
|
|
309
|
+
max_tokens: params.maxTokens ?? 4096,
|
|
310
|
+
top_p: params.topP ?? 1,
|
|
311
|
+
stop: params.stop,
|
|
312
|
+
...params.cacheKey ? { user: params.cacheKey } : {},
|
|
313
|
+
// Tools arrive only once the runtime gate (the profile's "Allow tool
|
|
314
|
+
// use") has passed, so no separate guard is needed here.
|
|
315
|
+
...params.tools && params.tools.length > 0 ? { tools: params.tools, tool_choice: params.toolChoice ?? "auto" } : {}
|
|
316
|
+
};
|
|
317
|
+
this.applyProfileParams(body, params);
|
|
233
318
|
try {
|
|
234
|
-
const response = await client.chat.completions.create(
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
max_tokens: params.maxTokens ?? 4096,
|
|
239
|
-
top_p: params.topP ?? 1,
|
|
240
|
-
stop: params.stop,
|
|
241
|
-
...params.cacheKey ? { user: params.cacheKey } : {}
|
|
242
|
-
}, this.buildRequestOptions(params));
|
|
319
|
+
const response = await client.chat.completions.create(
|
|
320
|
+
body,
|
|
321
|
+
this.buildRequestOptions(params)
|
|
322
|
+
);
|
|
243
323
|
const choice = response.choices[0];
|
|
324
|
+
const toolCalls = this.normalizeToolCalls(choice.message.tool_calls);
|
|
244
325
|
return {
|
|
245
326
|
content: choice.message.content ?? "",
|
|
246
327
|
finishReason: choice.finish_reason,
|
|
@@ -250,7 +331,8 @@ var OpenAICompatibleProvider = class {
|
|
|
250
331
|
totalTokens: response.usage?.total_tokens ?? 0
|
|
251
332
|
},
|
|
252
333
|
raw: response,
|
|
253
|
-
attachmentResults
|
|
334
|
+
attachmentResults,
|
|
335
|
+
...toolCalls.length > 0 ? { toolCalls } : {}
|
|
254
336
|
};
|
|
255
337
|
} catch (error) {
|
|
256
338
|
this.logger.error(
|
|
@@ -299,23 +381,32 @@ var OpenAICompatibleProvider = class {
|
|
|
299
381
|
content: m.content
|
|
300
382
|
};
|
|
301
383
|
});
|
|
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
|
+
stream: true,
|
|
392
|
+
stream_options: { include_usage: true },
|
|
393
|
+
...params.cacheKey ? { user: params.cacheKey } : {},
|
|
394
|
+
...params.tools && params.tools.length > 0 ? { tools: params.tools, tool_choice: params.toolChoice ?? "auto" } : {}
|
|
395
|
+
};
|
|
396
|
+
this.applyProfileParams(body, params);
|
|
302
397
|
try {
|
|
303
|
-
const stream = await client.chat.completions.create(
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
max_tokens: params.maxTokens ?? 4096,
|
|
308
|
-
top_p: params.topP ?? 1,
|
|
309
|
-
stop: params.stop,
|
|
310
|
-
stream: true,
|
|
311
|
-
stream_options: { include_usage: true },
|
|
312
|
-
...params.cacheKey ? { user: params.cacheKey } : {}
|
|
313
|
-
}, this.buildRequestOptions(params));
|
|
398
|
+
const stream = await client.chat.completions.create(
|
|
399
|
+
body,
|
|
400
|
+
this.buildRequestOptions(params)
|
|
401
|
+
);
|
|
314
402
|
let chunkCount = 0;
|
|
315
403
|
let accumulatedUsage = null;
|
|
404
|
+
const toolCallAccumulator = /* @__PURE__ */ new Map();
|
|
405
|
+
let finishReason = null;
|
|
316
406
|
for await (const chunk of stream) {
|
|
317
407
|
chunkCount++;
|
|
318
|
-
const
|
|
408
|
+
const choice = chunk.choices[0];
|
|
409
|
+
const content = choice?.delta?.content;
|
|
319
410
|
const hasUsage = chunk.usage;
|
|
320
411
|
if (hasUsage) {
|
|
321
412
|
accumulatedUsage = {
|
|
@@ -324,6 +415,17 @@ var OpenAICompatibleProvider = class {
|
|
|
324
415
|
total_tokens: chunk.usage?.total_tokens
|
|
325
416
|
};
|
|
326
417
|
}
|
|
418
|
+
if (choice?.delta?.tool_calls) {
|
|
419
|
+
for (const tcDelta of choice.delta.tool_calls) {
|
|
420
|
+
const idx = tcDelta.index ?? 0;
|
|
421
|
+
const existing = toolCallAccumulator.get(idx) ?? { id: "", name: "", arguments: "" };
|
|
422
|
+
if (tcDelta.id) existing.id = tcDelta.id;
|
|
423
|
+
if (tcDelta.function?.name) existing.name = tcDelta.function.name;
|
|
424
|
+
if (tcDelta.function?.arguments) existing.arguments += tcDelta.function.arguments;
|
|
425
|
+
toolCallAccumulator.set(idx, existing);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
if (choice?.finish_reason) finishReason = choice.finish_reason;
|
|
327
429
|
if (content) {
|
|
328
430
|
yield {
|
|
329
431
|
content,
|
|
@@ -331,6 +433,11 @@ var OpenAICompatibleProvider = class {
|
|
|
331
433
|
};
|
|
332
434
|
}
|
|
333
435
|
}
|
|
436
|
+
const toolCalls = Array.from(toolCallAccumulator.values()).filter((tc) => tc.name).map((tc) => ({
|
|
437
|
+
id: tc.id,
|
|
438
|
+
type: "function",
|
|
439
|
+
function: { name: tc.name, arguments: tc.arguments }
|
|
440
|
+
}));
|
|
334
441
|
yield {
|
|
335
442
|
content: "",
|
|
336
443
|
done: true,
|
|
@@ -339,7 +446,22 @@ var OpenAICompatibleProvider = class {
|
|
|
339
446
|
completionTokens: accumulatedUsage.completion_tokens ?? 0,
|
|
340
447
|
totalTokens: accumulatedUsage.total_tokens ?? 0
|
|
341
448
|
} : void 0,
|
|
342
|
-
attachmentResults
|
|
449
|
+
attachmentResults,
|
|
450
|
+
// Only synthesized when the stream actually carried tool calls: the
|
|
451
|
+
// host's tool detection reads `rawResponse`, and an empty one on every
|
|
452
|
+
// ordinary turn would be noise in the logs for no gain.
|
|
453
|
+
...toolCalls.length > 0 ? {
|
|
454
|
+
toolCalls,
|
|
455
|
+
rawResponse: {
|
|
456
|
+
choices: [
|
|
457
|
+
{
|
|
458
|
+
index: 0,
|
|
459
|
+
message: { role: "assistant", content: "", tool_calls: toolCalls },
|
|
460
|
+
finish_reason: finishReason
|
|
461
|
+
}
|
|
462
|
+
]
|
|
463
|
+
}
|
|
464
|
+
} : {}
|
|
343
465
|
};
|
|
344
466
|
} catch (error) {
|
|
345
467
|
this.logger.error(
|
|
@@ -404,6 +526,7 @@ var OpenAICompatibleProvider = class {
|
|
|
404
526
|
export {
|
|
405
527
|
DEFAULT_REQUEST_TIMEOUT_MS,
|
|
406
528
|
OpenAICompatibleProvider,
|
|
529
|
+
applyProfileParameters,
|
|
407
530
|
buildRequestAbortSignal,
|
|
408
531
|
buildSdkClientOptions,
|
|
409
532
|
buildSdkRequestOptions,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/providers/openai-compatible.ts","../../src/logging/plugin-logger.ts","../../src/logging/index.ts","../../src/version.ts","../../src/providers/request-budget.ts"],"sourcesContent":["/**\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":[]}
|
|
1
|
+
{"version":3,"sources":["../../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 * 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":[]}
|