@quilltap/plugin-utils 2.2.16 → 2.2.18

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.
@@ -60,6 +60,29 @@ interface OpenAICompatibleProviderConfig {
60
60
  * @default 'OpenAI-compatible provider file attachment support varies by implementation (not yet implemented)'
61
61
  */
62
62
  attachmentErrorMessage?: string;
63
+ /**
64
+ * Default wall-clock budget per request, in milliseconds.
65
+ *
66
+ * The OpenAI SDK's own default is 600 000 ms, which means a service that
67
+ * accepts a connection and then goes quiet holds the caller for ten minutes
68
+ * before the first retry. That is far longer than any Quilltap caller wants
69
+ * to wait, so the base class lowers it.
70
+ *
71
+ * Individual requests can tighten this further via `LLMParams.requestTimeoutMs`.
72
+ *
73
+ * @default 300000 (5 minutes)
74
+ */
75
+ requestTimeoutMs?: number;
76
+ /**
77
+ * How many times the SDK may retry a failed or timed-out request.
78
+ *
79
+ * Applies only to requests that did *not* specify their own
80
+ * `LLMParams.requestTimeoutMs` — a caller-supplied budget is a hard ceiling
81
+ * and never retries.
82
+ *
83
+ * @default 2 (the OpenAI SDK default)
84
+ */
85
+ maxRetries?: number;
63
86
  }
64
87
  /**
65
88
  * Base provider class for OpenAI-compatible APIs.
@@ -93,6 +116,10 @@ declare class OpenAICompatibleProvider implements TextProvider {
93
116
  protected readonly requireApiKey: boolean;
94
117
  /** Error message for attachment failures */
95
118
  protected readonly attachmentErrorMessage: string;
119
+ /** Default per-request wall-clock budget in milliseconds */
120
+ protected readonly requestTimeoutMs: number;
121
+ /** Retry count for requests that carry no caller-supplied budget */
122
+ protected readonly maxRetries: number;
96
123
  /** Logger instance */
97
124
  protected readonly logger: PluginLogger;
98
125
  /**
@@ -133,6 +160,24 @@ declare class OpenAICompatibleProvider implements TextProvider {
133
160
  * @returns Configured OpenAI client instance
134
161
  */
135
162
  protected createClient(apiKey: string): OpenAI;
163
+ /**
164
+ * Builds the per-request options for a single call.
165
+ *
166
+ * When the caller supplied `params.requestTimeoutMs` it is a hard ceiling:
167
+ * the request uses that budget and does not retry, so the total time spent is
168
+ * what the caller asked for rather than a multiple of it. Otherwise the
169
+ * client-level defaults apply.
170
+ *
171
+ * Subclasses that build their own request bodies should pass the result as
172
+ * the second argument to `client.chat.completions.create(...)`.
173
+ *
174
+ * @param params - LLM parameters for the request
175
+ * @returns Request options for the OpenAI SDK, or undefined to use defaults
176
+ */
177
+ protected buildRequestOptions(params: LLMParams): {
178
+ timeout: number;
179
+ maxRetries: number;
180
+ } | undefined;
136
181
  /**
137
182
  * Sends a message and returns the complete response.
138
183
  *
@@ -165,4 +210,85 @@ declare class OpenAICompatibleProvider implements TextProvider {
165
210
  getAvailableModels(apiKey: string): Promise<string[]>;
166
211
  }
167
212
 
168
- export { OpenAICompatibleProvider, type OpenAICompatibleProviderConfig };
213
+ /**
214
+ * Per-request wall-clock budgets.
215
+ *
216
+ * Every provider SDK ships a generous default request timeout — the OpenAI and
217
+ * Anthropic SDKs both use ten minutes — on the assumption that a slow answer is
218
+ * better than no answer. That assumption is wrong for work Quilltap awaits
219
+ * inline on a user-visible turn: a service that accepts the connection and then
220
+ * goes silent holds the turn for the full ten minutes before the SDK's first
221
+ * retry, with nothing logged, because request logs are only written when a call
222
+ * finishes.
223
+ *
224
+ * `LLMParams.requestTimeoutMs` is the caller's answer to "how long is this
225
+ * worth waiting for". These helpers are the single place that decides what that
226
+ * number means, so an SDK-backed provider and a `fetch`-based one apply it the
227
+ * same way.
228
+ *
229
+ * @packageDocumentation
230
+ */
231
+
232
+ /**
233
+ * Fallback budget for a request whose caller expressed no preference.
234
+ *
235
+ * Half the SDK default: long enough for a slow non-streaming generation, short
236
+ * enough that a silently-stalled endpoint fails in minutes rather than tens of
237
+ * minutes.
238
+ */
239
+ declare const DEFAULT_REQUEST_TIMEOUT_MS = 300000;
240
+ /**
241
+ * The budget for one request, in milliseconds.
242
+ *
243
+ * @param params - LLM parameters, whose `requestTimeoutMs` wins when set
244
+ * @param defaultMs - Budget to use when the caller named none
245
+ * @returns The effective budget in milliseconds
246
+ */
247
+ declare function resolveRequestTimeoutMs(params: Pick<LLMParams, 'requestTimeoutMs'>, defaultMs?: number): number;
248
+ /**
249
+ * Per-request options for an OpenAI- or Anthropic-style SDK call.
250
+ *
251
+ * A caller-supplied budget is a *ceiling*, not a per-attempt allowance, so
252
+ * retries are disabled for it — three attempts at the requested budget would
253
+ * spend three times what the caller agreed to. Returns undefined when the
254
+ * caller named no budget, leaving the client's own defaults in charge.
255
+ *
256
+ * @param params - LLM parameters for the request
257
+ * @returns SDK request options, or undefined to use the client defaults
258
+ */
259
+ declare function buildSdkRequestOptions(params: Pick<LLMParams, 'requestTimeoutMs'>): {
260
+ timeout: number;
261
+ maxRetries: number;
262
+ } | undefined;
263
+ /**
264
+ * Client-construction options for a provider that builds a fresh SDK client
265
+ * per call, where "client default" and "this request" are the same thing.
266
+ *
267
+ * Always defined, unlike {@link buildSdkRequestOptions} — a per-call client has
268
+ * no pre-existing defaults worth preserving, so it takes the resolved budget
269
+ * either way and only forgoes retries when the caller set a ceiling.
270
+ *
271
+ * @param params - LLM parameters for the request
272
+ * @param defaultMs - Budget to use when the caller named none
273
+ * @returns `timeout` and `maxRetries` to spread into the SDK constructor
274
+ */
275
+ declare function buildSdkClientOptions(params: Pick<LLMParams, 'requestTimeoutMs'>, defaultMs?: number): {
276
+ timeout: number;
277
+ maxRetries: number;
278
+ };
279
+ /**
280
+ * An abort signal that fires at the request's budget, for providers that call
281
+ * `fetch` directly rather than through an SDK.
282
+ *
283
+ * Unlike the SDK options above this bounds the *whole* exchange, body included:
284
+ * `fetch` resolves at the response headers, but the signal stays live while the
285
+ * body streams. Pass a generous `defaultMs` on streaming paths, or compose with
286
+ * the caller's own signal, so a long generation is not cut off mid-answer.
287
+ *
288
+ * @param params - LLM parameters for the request
289
+ * @param defaultMs - Budget to use when the caller named none
290
+ * @returns An AbortSignal that aborts once the budget elapses
291
+ */
292
+ declare function buildRequestAbortSignal(params: Pick<LLMParams, 'requestTimeoutMs'>, defaultMs?: number): AbortSignal;
293
+
294
+ export { DEFAULT_REQUEST_TIMEOUT_MS, OpenAICompatibleProvider, type OpenAICompatibleProviderConfig, buildRequestAbortSignal, buildSdkClientOptions, buildSdkRequestOptions, resolveRequestTimeoutMs };
@@ -60,6 +60,29 @@ interface OpenAICompatibleProviderConfig {
60
60
  * @default 'OpenAI-compatible provider file attachment support varies by implementation (not yet implemented)'
61
61
  */
62
62
  attachmentErrorMessage?: string;
63
+ /**
64
+ * Default wall-clock budget per request, in milliseconds.
65
+ *
66
+ * The OpenAI SDK's own default is 600 000 ms, which means a service that
67
+ * accepts a connection and then goes quiet holds the caller for ten minutes
68
+ * before the first retry. That is far longer than any Quilltap caller wants
69
+ * to wait, so the base class lowers it.
70
+ *
71
+ * Individual requests can tighten this further via `LLMParams.requestTimeoutMs`.
72
+ *
73
+ * @default 300000 (5 minutes)
74
+ */
75
+ requestTimeoutMs?: number;
76
+ /**
77
+ * How many times the SDK may retry a failed or timed-out request.
78
+ *
79
+ * Applies only to requests that did *not* specify their own
80
+ * `LLMParams.requestTimeoutMs` — a caller-supplied budget is a hard ceiling
81
+ * and never retries.
82
+ *
83
+ * @default 2 (the OpenAI SDK default)
84
+ */
85
+ maxRetries?: number;
63
86
  }
64
87
  /**
65
88
  * Base provider class for OpenAI-compatible APIs.
@@ -93,6 +116,10 @@ declare class OpenAICompatibleProvider implements TextProvider {
93
116
  protected readonly requireApiKey: boolean;
94
117
  /** Error message for attachment failures */
95
118
  protected readonly attachmentErrorMessage: string;
119
+ /** Default per-request wall-clock budget in milliseconds */
120
+ protected readonly requestTimeoutMs: number;
121
+ /** Retry count for requests that carry no caller-supplied budget */
122
+ protected readonly maxRetries: number;
96
123
  /** Logger instance */
97
124
  protected readonly logger: PluginLogger;
98
125
  /**
@@ -133,6 +160,24 @@ declare class OpenAICompatibleProvider implements TextProvider {
133
160
  * @returns Configured OpenAI client instance
134
161
  */
135
162
  protected createClient(apiKey: string): OpenAI;
163
+ /**
164
+ * Builds the per-request options for a single call.
165
+ *
166
+ * When the caller supplied `params.requestTimeoutMs` it is a hard ceiling:
167
+ * the request uses that budget and does not retry, so the total time spent is
168
+ * what the caller asked for rather than a multiple of it. Otherwise the
169
+ * client-level defaults apply.
170
+ *
171
+ * Subclasses that build their own request bodies should pass the result as
172
+ * the second argument to `client.chat.completions.create(...)`.
173
+ *
174
+ * @param params - LLM parameters for the request
175
+ * @returns Request options for the OpenAI SDK, or undefined to use defaults
176
+ */
177
+ protected buildRequestOptions(params: LLMParams): {
178
+ timeout: number;
179
+ maxRetries: number;
180
+ } | undefined;
136
181
  /**
137
182
  * Sends a message and returns the complete response.
138
183
  *
@@ -165,4 +210,85 @@ declare class OpenAICompatibleProvider implements TextProvider {
165
210
  getAvailableModels(apiKey: string): Promise<string[]>;
166
211
  }
167
212
 
168
- export { OpenAICompatibleProvider, type OpenAICompatibleProviderConfig };
213
+ /**
214
+ * Per-request wall-clock budgets.
215
+ *
216
+ * Every provider SDK ships a generous default request timeout — the OpenAI and
217
+ * Anthropic SDKs both use ten minutes — on the assumption that a slow answer is
218
+ * better than no answer. That assumption is wrong for work Quilltap awaits
219
+ * inline on a user-visible turn: a service that accepts the connection and then
220
+ * goes silent holds the turn for the full ten minutes before the SDK's first
221
+ * retry, with nothing logged, because request logs are only written when a call
222
+ * finishes.
223
+ *
224
+ * `LLMParams.requestTimeoutMs` is the caller's answer to "how long is this
225
+ * worth waiting for". These helpers are the single place that decides what that
226
+ * number means, so an SDK-backed provider and a `fetch`-based one apply it the
227
+ * same way.
228
+ *
229
+ * @packageDocumentation
230
+ */
231
+
232
+ /**
233
+ * Fallback budget for a request whose caller expressed no preference.
234
+ *
235
+ * Half the SDK default: long enough for a slow non-streaming generation, short
236
+ * enough that a silently-stalled endpoint fails in minutes rather than tens of
237
+ * minutes.
238
+ */
239
+ declare const DEFAULT_REQUEST_TIMEOUT_MS = 300000;
240
+ /**
241
+ * The budget for one request, in milliseconds.
242
+ *
243
+ * @param params - LLM parameters, whose `requestTimeoutMs` wins when set
244
+ * @param defaultMs - Budget to use when the caller named none
245
+ * @returns The effective budget in milliseconds
246
+ */
247
+ declare function resolveRequestTimeoutMs(params: Pick<LLMParams, 'requestTimeoutMs'>, defaultMs?: number): number;
248
+ /**
249
+ * Per-request options for an OpenAI- or Anthropic-style SDK call.
250
+ *
251
+ * A caller-supplied budget is a *ceiling*, not a per-attempt allowance, so
252
+ * retries are disabled for it — three attempts at the requested budget would
253
+ * spend three times what the caller agreed to. Returns undefined when the
254
+ * caller named no budget, leaving the client's own defaults in charge.
255
+ *
256
+ * @param params - LLM parameters for the request
257
+ * @returns SDK request options, or undefined to use the client defaults
258
+ */
259
+ declare function buildSdkRequestOptions(params: Pick<LLMParams, 'requestTimeoutMs'>): {
260
+ timeout: number;
261
+ maxRetries: number;
262
+ } | undefined;
263
+ /**
264
+ * Client-construction options for a provider that builds a fresh SDK client
265
+ * per call, where "client default" and "this request" are the same thing.
266
+ *
267
+ * Always defined, unlike {@link buildSdkRequestOptions} — a per-call client has
268
+ * no pre-existing defaults worth preserving, so it takes the resolved budget
269
+ * either way and only forgoes retries when the caller set a ceiling.
270
+ *
271
+ * @param params - LLM parameters for the request
272
+ * @param defaultMs - Budget to use when the caller named none
273
+ * @returns `timeout` and `maxRetries` to spread into the SDK constructor
274
+ */
275
+ declare function buildSdkClientOptions(params: Pick<LLMParams, 'requestTimeoutMs'>, defaultMs?: number): {
276
+ timeout: number;
277
+ maxRetries: number;
278
+ };
279
+ /**
280
+ * An abort signal that fires at the request's budget, for providers that call
281
+ * `fetch` directly rather than through an SDK.
282
+ *
283
+ * Unlike the SDK options above this bounds the *whole* exchange, body included:
284
+ * `fetch` resolves at the response headers, but the signal stays live while the
285
+ * body streams. Pass a generous `defaultMs` on streaming paths, or compose with
286
+ * the caller's own signal, so a long generation is not cut off mid-answer.
287
+ *
288
+ * @param params - LLM parameters for the request
289
+ * @param defaultMs - Budget to use when the caller named none
290
+ * @returns An AbortSignal that aborts once the budget elapses
291
+ */
292
+ declare function buildRequestAbortSignal(params: Pick<LLMParams, 'requestTimeoutMs'>, defaultMs?: number): AbortSignal;
293
+
294
+ export { DEFAULT_REQUEST_TIMEOUT_MS, OpenAICompatibleProvider, type OpenAICompatibleProviderConfig, buildRequestAbortSignal, buildSdkClientOptions, buildSdkRequestOptions, resolveRequestTimeoutMs };
@@ -30,7 +30,12 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
30
30
  // src/providers/index.ts
31
31
  var providers_exports = {};
32
32
  __export(providers_exports, {
33
- OpenAICompatibleProvider: () => OpenAICompatibleProvider
33
+ DEFAULT_REQUEST_TIMEOUT_MS: () => DEFAULT_REQUEST_TIMEOUT_MS,
34
+ OpenAICompatibleProvider: () => OpenAICompatibleProvider,
35
+ buildRequestAbortSignal: () => buildRequestAbortSignal,
36
+ buildSdkClientOptions: () => buildSdkClientOptions,
37
+ buildSdkRequestOptions: () => buildSdkRequestOptions,
38
+ resolveRequestTimeoutMs: () => resolveRequestTimeoutMs
34
39
  });
35
40
  module.exports = __toCommonJS(providers_exports);
36
41
 
@@ -104,7 +109,30 @@ function getQuilltapUserAgent() {
104
109
  return `Quilltap/${getQuilltapVersion()}`;
105
110
  }
106
111
 
112
+ // src/providers/request-budget.ts
113
+ var DEFAULT_REQUEST_TIMEOUT_MS = 3e5;
114
+ function resolveRequestTimeoutMs(params, defaultMs = DEFAULT_REQUEST_TIMEOUT_MS) {
115
+ const requested = params.requestTimeoutMs;
116
+ return typeof requested === "number" && requested > 0 ? requested : defaultMs;
117
+ }
118
+ function buildSdkRequestOptions(params) {
119
+ const requested = params.requestTimeoutMs;
120
+ if (typeof requested !== "number" || requested <= 0) return void 0;
121
+ return { timeout: requested, maxRetries: 0 };
122
+ }
123
+ function buildSdkClientOptions(params, defaultMs = DEFAULT_REQUEST_TIMEOUT_MS) {
124
+ const capped = typeof params.requestTimeoutMs === "number" && params.requestTimeoutMs > 0;
125
+ return {
126
+ timeout: resolveRequestTimeoutMs(params, defaultMs),
127
+ maxRetries: capped ? 0 : 2
128
+ };
129
+ }
130
+ function buildRequestAbortSignal(params, defaultMs = DEFAULT_REQUEST_TIMEOUT_MS) {
131
+ return AbortSignal.timeout(resolveRequestTimeoutMs(params, defaultMs));
132
+ }
133
+
107
134
  // src/providers/openai-compatible.ts
135
+ var DEFAULT_MAX_RETRIES = 2;
108
136
  var OpenAICompatibleProvider = class {
109
137
  /**
110
138
  * Creates a new OpenAI-compatible provider instance.
@@ -123,11 +151,15 @@ var OpenAICompatibleProvider = class {
123
151
  this.providerName = "OpenAICompatible";
124
152
  this.requireApiKey = false;
125
153
  this.attachmentErrorMessage = "OpenAI-compatible provider file attachment support varies by implementation (not yet implemented)";
154
+ this.requestTimeoutMs = DEFAULT_REQUEST_TIMEOUT_MS;
155
+ this.maxRetries = DEFAULT_MAX_RETRIES;
126
156
  } else {
127
157
  this.baseUrl = config.baseUrl;
128
158
  this.providerName = config.providerName ?? "OpenAICompatible";
129
159
  this.requireApiKey = config.requireApiKey ?? false;
130
160
  this.attachmentErrorMessage = config.attachmentErrorMessage ?? "OpenAI-compatible provider file attachment support varies by implementation (not yet implemented)";
161
+ this.requestTimeoutMs = config.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
162
+ this.maxRetries = config.maxRetries ?? DEFAULT_MAX_RETRIES;
131
163
  }
132
164
  this.logger = createPluginLogger(`${this.providerName}Provider`);
133
165
  }
@@ -179,9 +211,28 @@ var OpenAICompatibleProvider = class {
179
211
  return new import_openai.default({
180
212
  apiKey: this.getEffectiveApiKey(apiKey),
181
213
  baseURL: this.baseUrl,
182
- defaultHeaders: { "User-Agent": getQuilltapUserAgent() }
214
+ defaultHeaders: { "User-Agent": getQuilltapUserAgent() },
215
+ timeout: this.requestTimeoutMs,
216
+ maxRetries: this.maxRetries
183
217
  });
184
218
  }
219
+ /**
220
+ * Builds the per-request options for a single call.
221
+ *
222
+ * When the caller supplied `params.requestTimeoutMs` it is a hard ceiling:
223
+ * the request uses that budget and does not retry, so the total time spent is
224
+ * what the caller asked for rather than a multiple of it. Otherwise the
225
+ * client-level defaults apply.
226
+ *
227
+ * Subclasses that build their own request bodies should pass the result as
228
+ * the second argument to `client.chat.completions.create(...)`.
229
+ *
230
+ * @param params - LLM parameters for the request
231
+ * @returns Request options for the OpenAI SDK, or undefined to use defaults
232
+ */
233
+ buildRequestOptions(params) {
234
+ return buildSdkRequestOptions(params);
235
+ }
185
236
  /**
186
237
  * Sends a message and returns the complete response.
187
238
  *
@@ -229,7 +280,7 @@ var OpenAICompatibleProvider = class {
229
280
  top_p: params.topP ?? 1,
230
281
  stop: params.stop,
231
282
  ...params.cacheKey ? { user: params.cacheKey } : {}
232
- });
283
+ }, this.buildRequestOptions(params));
233
284
  const choice = response.choices[0];
234
285
  return {
235
286
  content: choice.message.content ?? "",
@@ -300,7 +351,7 @@ var OpenAICompatibleProvider = class {
300
351
  stream: true,
301
352
  stream_options: { include_usage: true },
302
353
  ...params.cacheKey ? { user: params.cacheKey } : {}
303
- });
354
+ }, this.buildRequestOptions(params));
304
355
  let chunkCount = 0;
305
356
  let accumulatedUsage = null;
306
357
  for await (const chunk of stream) {
@@ -393,6 +444,11 @@ var OpenAICompatibleProvider = class {
393
444
  };
394
445
  // Annotate the CommonJS export names for ESM import in node:
395
446
  0 && (module.exports = {
396
- OpenAICompatibleProvider
447
+ DEFAULT_REQUEST_TIMEOUT_MS,
448
+ OpenAICompatibleProvider,
449
+ buildRequestAbortSignal,
450
+ buildSdkClientOptions,
451
+ buildSdkRequestOptions,
452
+ resolveRequestTimeoutMs
397
453
  });
398
454
  //# sourceMappingURL=index.js.map
@@ -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"],"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","/**\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';\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/**\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 /** 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 } 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 }\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 });\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 });\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 });\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"],"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"],"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"]}