genai-lite 0.11.0 → 0.12.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/README.md CHANGED
@@ -6,6 +6,7 @@ A lightweight, portable Node.js/TypeScript library providing a unified interface
6
6
 
7
7
  - 🔌 **Unified API** - Single interface for multiple AI providers
8
8
  - 🏠 **Local & Cloud Models** - Run models locally with llama.cpp or use cloud APIs
9
+ - ⚡ **Text Streaming** - Async iterable token deltas for OpenAI, Anthropic, Gemini, Mistral, OpenRouter, and llama.cpp
9
10
  - 🖼️ **Image Generation** - First-class support for AI image generation (OpenAI, local diffusion)
10
11
  - 🔐 **Flexible API Key Management** - Bring your own key storage solution
11
12
  - 📦 **Zero Electron Dependencies** - Works in any Node.js environment
@@ -77,6 +78,33 @@ if (response.object === 'chat.completion') {
77
78
  }
78
79
  ```
79
80
 
81
+ ### Streaming Text
82
+
83
+ ```typescript
84
+ import { LLMService } from 'genai-lite';
85
+
86
+ const llmService = new LLMService(async () => 'not-needed');
87
+
88
+ for await (const event of llmService.streamMessage({
89
+ providerId: 'llamacpp',
90
+ modelId: 'llamacpp',
91
+ messages: [{ role: 'user', content: 'Say hello in five words.' }],
92
+ settings: { maxTokens: 32 }
93
+ })) {
94
+ if (event.type === 'content_delta') {
95
+ process.stdout.write(event.delta);
96
+ } else if (event.type === 'reasoning_delta') {
97
+ process.stderr.write(event.delta);
98
+ } else if (event.type === 'complete') {
99
+ console.log('\nTokens:', event.response.usage?.total_tokens);
100
+ } else if (event.type === 'error') {
101
+ console.error(event.error.error.message);
102
+ }
103
+ }
104
+ ```
105
+
106
+ Streaming is implemented for all text providers: `openai`, `anthropic`, `gemini`, `mistral`, `openrouter`, and `llamacpp`. The final `complete.response` event contains the same normalized response shape returned by `sendMessage()`.
107
+
80
108
  ### Image Generation
81
109
 
82
110
  ```typescript
@@ -134,7 +162,7 @@ Comprehensive documentation is available in the **[`genai-lite-docs`](./genai-li
134
162
  - **Google Gemini** - Gemini 3 (Pro, Flash preview), Gemini 2.5, Gemma 3 & 4 (free)
135
163
  - **Mistral** - Codestral, Devstral
136
164
  - **OpenRouter** - Unified gateway to 100+ models (unknown models assumed reasoning-capable)
137
- - **llama.cpp** - Run any GGUF model locally (no API keys required); local reasoning toggle for detected models
165
+ - **llama.cpp** - Run any GGUF model locally (no API keys required); streaming and local reasoning toggle for detected models
138
166
 
139
167
  ### Image Providers
140
168
  - **OpenAI Images** - gpt-image-1, dall-e-3, dall-e-2
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  export type { ApiKeyProvider, PresetMode } from "./types";
2
2
  export { LLMService } from "./llm/LLMService";
3
- export type { LLMServiceOptions, SendMessageOptions, CreateMessagesResult } from "./llm/LLMService";
3
+ export type { LLMServiceOptions, SendMessageOptions, StreamMessageOptions, CreateMessagesResult } from "./llm/LLMService";
4
4
  export { withRetry, DEFAULT_RETRY_POLICY } from "./shared/services/withRetry";
5
5
  export type { RetryPolicy, RetryVerdict, WithRetryOptions } from "./shared/services/withRetry";
6
6
  export type { ModelPreset } from "./types/presets";
@@ -1,6 +1,6 @@
1
1
  import type { ApiKeyProvider, PresetMode } from '../types';
2
2
  import type { Logger, LogLevel } from '../logging/types';
3
- import type { LLMChatRequest, LLMChatRequestWithPreset, LLMResponse, LLMFailureResponse, ProviderInfo, ModelInfo, ApiProviderId, LLMSettings, ModelContext, LLMMessage } from "./types";
3
+ import type { LLMChatRequest, LLMChatRequestWithPreset, LLMResponse, LLMFailureResponse, ProviderInfo, ModelInfo, ApiProviderId, LLMSettings, ModelContext, LLMMessage, LLMStreamEvent } from "./types";
4
4
  import type { ModelPreset } from "../types/presets";
5
5
  import { type RetryPolicy } from "../shared/services/withRetry";
6
6
  export type { PresetMode };
@@ -43,6 +43,18 @@ export interface SendMessageOptions {
43
43
  /** Per-request retry cap (overrides the service-level retry.maxRetries) */
44
44
  maxRetries?: number;
45
45
  }
46
+ /**
47
+ * Per-call options for LLMService.streamMessage
48
+ */
49
+ export interface StreamMessageOptions {
50
+ /**
51
+ * Abort signal to cancel the request (client-side - the provider may still
52
+ * process and bill an already-dispatched request).
53
+ */
54
+ signal?: AbortSignal;
55
+ /** Per-request timeout in ms (overrides the service-level timeoutMs) */
56
+ timeoutMs?: number;
57
+ }
46
58
  /**
47
59
  * Result from createMessages method
48
60
  */
@@ -96,6 +108,16 @@ export declare class LLMService {
96
108
  * @returns Promise resolving to either success or failure response
97
109
  */
98
110
  sendMessage(request: LLMChatRequest | LLMChatRequestWithPreset, callOptions?: SendMessageOptions): Promise<LLMResponse | LLMFailureResponse>;
111
+ /**
112
+ * Streams a chat message from an LLM provider.
113
+ *
114
+ * The final `complete` event contains the same normalized response shape as
115
+ * sendMessage(). Validation/API key failures and unsupported streaming adapters
116
+ * are yielded as a single `error` event.
117
+ */
118
+ streamMessage(request: LLMChatRequest | LLMChatRequestWithPreset, callOptions?: StreamMessageOptions): AsyncGenerator<LLMStreamEvent>;
119
+ private prepareRequest;
120
+ private postProcessResponse;
99
121
  /**
100
122
  * Gets all configured model presets
101
123
  *
@@ -85,116 +85,23 @@ class LLMService {
85
85
  async sendMessage(request, callOptions) {
86
86
  this.logger.info(`LLMService.sendMessage called with presetId: ${request.presetId}, provider: ${request.providerId}, model: ${request.modelId}`);
87
87
  try {
88
- // Resolve model information from preset or direct IDs
89
- const resolved = await this.modelResolver.resolve(request);
90
- if (resolved.error) {
91
- return resolved.error;
92
- }
93
- const { providerId, modelId, modelInfo, settings: resolvedSettings } = resolved;
94
- // Create a proper LLMChatRequest with resolved values
95
- const resolvedRequest = {
96
- ...request,
97
- providerId: providerId,
98
- modelId: modelId,
99
- };
100
- // Validate basic request structure
101
- const structureValidationResult = this.requestValidator.validateRequestStructure(resolvedRequest);
102
- if (structureValidationResult) {
103
- return structureValidationResult;
104
- }
105
- // Validate settings if provided
106
- const combinedSettings = { ...resolvedSettings, ...request.settings };
107
- if (combinedSettings) {
108
- const settingsValidation = this.requestValidator.validateSettings(combinedSettings, providerId, modelId);
109
- if (settingsValidation) {
110
- return settingsValidation;
111
- }
112
- }
113
- // Apply model-specific defaults and merge with user settings
114
- const finalSettings = this.settingsManager.mergeSettingsForModel(modelId, providerId, combinedSettings, modelInfo);
115
- // Validate reasoning settings for model capabilities
116
- const reasoningValidation = this.requestValidator.validateReasoningSettings(modelInfo, finalSettings.reasoning, resolvedRequest);
117
- if (reasoningValidation) {
118
- return reasoningValidation;
88
+ const preparedResult = await this.prepareRequest(request, callOptions);
89
+ if ("error" in preparedResult) {
90
+ return preparedResult.error;
119
91
  }
120
- // Validate structured output settings for model capabilities
121
- const structuredOutputValidation = this.requestValidator.validateStructuredOutputSettings(modelInfo, finalSettings.structuredOutput, resolvedRequest);
122
- if (structuredOutputValidation) {
123
- return structuredOutputValidation;
124
- }
125
- // Get provider info for parameter filtering
126
- const providerInfo = (0, config_1.getProviderById)(providerId);
127
- if (!providerInfo) {
128
- return {
129
- provider: providerId,
130
- model: modelId,
131
- error: {
132
- message: `Provider information not found: ${providerId}`,
133
- code: "PROVIDER_ERROR",
134
- type: "server_error",
135
- },
136
- object: "error",
137
- };
138
- }
139
- // Filter out unsupported parameters
140
- const filteredSettings = this.settingsManager.filterUnsupportedParameters(finalSettings, modelInfo, providerInfo);
141
- const internalRequest = {
142
- ...resolvedRequest,
143
- settings: filteredSettings,
144
- };
145
- this.logger.debug(`Processing LLM request with (potentially filtered) settings:`, {
146
- provider: providerId,
147
- model: modelId,
148
- settings: filteredSettings,
149
- messageCount: resolvedRequest.messages.length,
150
- });
151
- // Get client adapter
152
- const clientAdapter = this.adapterRegistry.getAdapter(providerId);
153
- // Use ApiKeyProvider to get the API key and make the request
92
+ const prepared = preparedResult.prepared;
154
93
  try {
155
- const apiKey = await this.getApiKey(providerId);
156
- if (!apiKey) {
157
- return {
158
- provider: providerId,
159
- model: modelId,
160
- error: {
161
- message: `API key for provider '${providerId}' could not be retrieved. Ensure your ApiKeyProvider is configured correctly.`,
162
- code: "API_KEY_ERROR",
163
- type: "authentication_error",
164
- },
165
- object: "error",
166
- };
167
- }
168
- // Validate API key format if adapter supports it
169
- if (clientAdapter.validateApiKey && !clientAdapter.validateApiKey(apiKey)) {
170
- return {
171
- provider: providerId,
172
- model: modelId,
173
- error: {
174
- message: `Invalid API key format for provider '${providerId}'. Please check your API key.`,
175
- code: "INVALID_API_KEY",
176
- type: "authentication_error",
177
- },
178
- object: "error",
179
- };
180
- }
181
- this.logger.info(`Making LLM request with ${clientAdapter.constructor.name} for provider: ${providerId}`);
94
+ this.logger.info(`Making LLM request with ${prepared.clientAdapter.constructor.name} for provider: ${prepared.providerId}`);
182
95
  // Unified retry layer: adapters never throw, so retry decisions are made on
183
96
  // RETURNED failure responses. SDK-internal retries are disabled (maxRetries: 0
184
97
  // at client construction) — this loop is the single owner of retry behavior.
185
- const adapterOptions = {
186
- ...(callOptions?.signal && { signal: callOptions.signal }),
187
- ...((callOptions?.timeoutMs ?? this.defaultTimeoutMs) !== undefined && {
188
- timeoutMs: callOptions?.timeoutMs ?? this.defaultTimeoutMs,
189
- }),
190
- };
191
98
  const retryOnTimeout = this.retryOptions?.retryOnTimeout ?? true;
192
99
  const retryableCodes = new Set([
193
100
  types_1.ADAPTER_ERROR_CODES.RATE_LIMIT_EXCEEDED,
194
101
  types_1.ADAPTER_ERROR_CODES.NETWORK_ERROR,
195
102
  ...(retryOnTimeout ? [types_1.ADAPTER_ERROR_CODES.REQUEST_TIMEOUT] : []),
196
103
  ]);
197
- const result = await (0, withRetry_1.withRetry)(() => clientAdapter.sendMessage(internalRequest, apiKey, adapterOptions), (res) => {
104
+ const result = await (0, withRetry_1.withRetry)(() => prepared.clientAdapter.sendMessage(prepared.internalRequest, prepared.apiKey, prepared.adapterOptions), (res) => {
198
105
  if (res.object !== 'error') {
199
106
  return { retry: false };
200
107
  }
@@ -212,99 +119,19 @@ class LLMService {
212
119
  }),
213
120
  signal: callOptions?.signal,
214
121
  logger: this.logger,
215
- label: `${providerId}/${modelId}`,
122
+ label: `${prepared.providerId}/${prepared.modelId}`,
216
123
  });
217
- // Post-process for thinking tag fallback
218
- // This feature extracts reasoning from XML tags when native reasoning is not active.
219
- // It's a fallback mechanism for models without native reasoning or when native is disabled.
220
- const fallbackSettings = internalRequest.settings.thinkingTagFallback;
221
- if (result.object === 'chat.completion' && fallbackSettings && fallbackSettings.enabled !== false) {
222
- const tagName = fallbackSettings.tagName || 'thinking';
223
- // Check if native reasoning is active for this request
224
- const isNativeReasoningActive = modelInfo.reasoning?.supported === true &&
225
- (internalRequest.settings.reasoning?.enabled === true ||
226
- (modelInfo.reasoning?.enabledByDefault === true &&
227
- internalRequest.settings.reasoning?.enabled !== false) ||
228
- modelInfo.reasoning?.canDisable === false);
229
- // Process the response - extract thinking tags if present
230
- const choice = result.choices[0];
231
- if (choice?.message?.content) {
232
- const { extracted, remaining } = (0, parser_1.extractInitialTaggedContent)(choice.message.content, tagName);
233
- if (extracted !== null) {
234
- // Success: thinking tag found
235
- this.logger.debug(`Extracted <${tagName}> block from response.`);
236
- // Handle the edge case: append to existing reasoning if present (e.g., native reasoning + thinking tags)
237
- const existingReasoning = choice.reasoning || '';
238
- if (existingReasoning) {
239
- // Use a neutral markdown header that works for any consumer (human or AI)
240
- choice.reasoning = `${existingReasoning}\n\n#### Additional Reasoning\n\n${extracted}`;
241
- }
242
- else {
243
- // No existing reasoning, just use the extracted content directly
244
- choice.reasoning = extracted;
245
- }
246
- choice.message.content = remaining;
247
- }
248
- else {
249
- // Tag was not found
250
- // Enforce only if: (1) enforce: true AND (2) native reasoning is NOT active
251
- if (fallbackSettings.enforce === true && !isNativeReasoningActive) {
252
- const nativeReasoningCapable = modelInfo.reasoning?.supported === true;
253
- return {
254
- provider: providerId,
255
- model: modelId,
256
- error: {
257
- message: `Model response missing required <${tagName}> tags.`,
258
- code: "THINKING_TAGS_MISSING",
259
- type: "validation_error",
260
- param: nativeReasoningCapable && !isNativeReasoningActive
261
- ? `You disabled native reasoning for this model (${modelId}). ` +
262
- `To see its reasoning, you must prompt it to use <${tagName}> tags. ` +
263
- `Example: "Write your step-by-step reasoning in <${tagName}> tags before answering."`
264
- : `This model (${modelId}) does not support native reasoning. ` +
265
- `To get reasoning, you must prompt it to use <${tagName}> tags. ` +
266
- `Example: "Write your step-by-step reasoning in <${tagName}> tags before answering."`,
267
- },
268
- object: "error",
269
- partialResponse: {
270
- id: result.id,
271
- provider: result.provider,
272
- model: result.model,
273
- created: result.created,
274
- choices: result.choices,
275
- usage: result.usage
276
- }
277
- };
278
- }
279
- // If enforce: false or native reasoning is active, do nothing
280
- }
281
- }
282
- }
283
- // Post-process for structured output auto-parsing
284
- // Parse JSON response when structuredOutput is enabled and autoParse is not disabled
285
- const structuredOutputSettings = internalRequest.settings.structuredOutput;
286
- if (result.object === 'chat.completion' && structuredOutputSettings &&
287
- structuredOutputSettings.enabled !== false && structuredOutputSettings.autoParse !== false) {
288
- for (const choice of result.choices) {
289
- if (choice.message?.content) {
290
- try {
291
- choice.parsedContent = JSON.parse(choice.message.content);
292
- }
293
- catch (e) {
294
- choice.parseError = `JSON parse failed: ${e instanceof Error ? e.message : String(e)}`;
295
- this.logger.warn(`Failed to parse structured output for choice ${choice.index}: ${choice.parseError}`);
296
- }
297
- }
298
- }
124
+ const processedResult = this.postProcessResponse(result, prepared);
125
+ if (processedResult.object === "chat.completion") {
126
+ this.logger.info(`LLM request completed successfully for model: ${prepared.modelId}`);
299
127
  }
300
- this.logger.info(`LLM request completed successfully for model: ${modelId}`);
301
- return result;
128
+ return processedResult;
302
129
  }
303
130
  catch (error) {
304
131
  this.logger.error("Error in LLMService.sendMessage:", error);
305
132
  return {
306
- provider: providerId,
307
- model: modelId,
133
+ provider: prepared.providerId,
134
+ model: prepared.modelId,
308
135
  error: {
309
136
  message: error instanceof Error
310
137
  ? error.message
@@ -334,6 +161,319 @@ class LLMService {
334
161
  };
335
162
  }
336
163
  }
164
+ /**
165
+ * Streams a chat message from an LLM provider.
166
+ *
167
+ * The final `complete` event contains the same normalized response shape as
168
+ * sendMessage(). Validation/API key failures and unsupported streaming adapters
169
+ * are yielded as a single `error` event.
170
+ */
171
+ async *streamMessage(request, callOptions) {
172
+ this.logger.info(`LLMService.streamMessage called with presetId: ${request.presetId}, provider: ${request.providerId}, model: ${request.modelId}`);
173
+ try {
174
+ const preparedResult = await this.prepareRequest(request, callOptions);
175
+ if ("error" in preparedResult) {
176
+ yield { type: "error", error: preparedResult.error };
177
+ return;
178
+ }
179
+ const prepared = preparedResult.prepared;
180
+ if (!prepared.clientAdapter.streamMessage) {
181
+ yield {
182
+ type: "error",
183
+ error: {
184
+ provider: prepared.providerId,
185
+ model: prepared.modelId,
186
+ error: {
187
+ message: `Streaming is not supported for provider '${prepared.providerId}'.`,
188
+ code: "PROVIDER_ERROR",
189
+ type: "unsupported_feature",
190
+ },
191
+ object: "error",
192
+ },
193
+ };
194
+ return;
195
+ }
196
+ try {
197
+ for await (const event of prepared.clientAdapter.streamMessage(prepared.internalRequest, prepared.apiKey, prepared.adapterOptions)) {
198
+ if (event.type === "complete") {
199
+ const processedResult = this.postProcessResponse(event.response, prepared);
200
+ if (processedResult.object === "error") {
201
+ yield { type: "error", error: processedResult };
202
+ }
203
+ else {
204
+ yield { type: "complete", response: processedResult };
205
+ }
206
+ continue;
207
+ }
208
+ yield event;
209
+ }
210
+ }
211
+ catch (error) {
212
+ this.logger.error("Error in LLMService.streamMessage:", error);
213
+ yield {
214
+ type: "error",
215
+ error: {
216
+ provider: prepared.providerId,
217
+ model: prepared.modelId,
218
+ error: {
219
+ message: error instanceof Error
220
+ ? error.message
221
+ : "An unknown error occurred during message streaming.",
222
+ code: "PROVIDER_ERROR",
223
+ type: "server_error",
224
+ providerError: error,
225
+ },
226
+ object: "error",
227
+ },
228
+ };
229
+ }
230
+ }
231
+ catch (error) {
232
+ this.logger.error("Error in LLMService.streamMessage (outer):", error);
233
+ yield {
234
+ type: "error",
235
+ error: {
236
+ provider: request.providerId || request.presetId || 'unknown',
237
+ model: request.modelId || request.presetId || 'unknown',
238
+ error: {
239
+ message: error instanceof Error
240
+ ? error.message
241
+ : "An unknown error occurred.",
242
+ code: "UNEXPECTED_ERROR",
243
+ type: "server_error",
244
+ providerError: error,
245
+ },
246
+ object: "error",
247
+ },
248
+ };
249
+ }
250
+ }
251
+ async prepareRequest(request, callOptions) {
252
+ // Resolve model information from preset or direct IDs
253
+ const resolved = await this.modelResolver.resolve(request);
254
+ if (resolved.error) {
255
+ return { error: resolved.error };
256
+ }
257
+ const { providerId, modelId, modelInfo, settings: resolvedSettings } = resolved;
258
+ // Create a proper LLMChatRequest with resolved values
259
+ const resolvedRequest = {
260
+ ...request,
261
+ providerId: providerId,
262
+ modelId: modelId,
263
+ };
264
+ // Validate basic request structure
265
+ const structureValidationResult = this.requestValidator.validateRequestStructure(resolvedRequest);
266
+ if (structureValidationResult) {
267
+ return { error: structureValidationResult };
268
+ }
269
+ // Validate settings if provided
270
+ const combinedSettings = { ...resolvedSettings, ...request.settings };
271
+ if (combinedSettings) {
272
+ const settingsValidation = this.requestValidator.validateSettings(combinedSettings, providerId, modelId);
273
+ if (settingsValidation) {
274
+ return { error: settingsValidation };
275
+ }
276
+ }
277
+ // Apply model-specific defaults and merge with user settings
278
+ const finalSettings = this.settingsManager.mergeSettingsForModel(modelId, providerId, combinedSettings, modelInfo);
279
+ // Validate reasoning settings for model capabilities
280
+ const reasoningValidation = this.requestValidator.validateReasoningSettings(modelInfo, finalSettings.reasoning, resolvedRequest);
281
+ if (reasoningValidation) {
282
+ return { error: reasoningValidation };
283
+ }
284
+ // Validate structured output settings for model capabilities
285
+ const structuredOutputValidation = this.requestValidator.validateStructuredOutputSettings(modelInfo, finalSettings.structuredOutput, resolvedRequest);
286
+ if (structuredOutputValidation) {
287
+ return { error: structuredOutputValidation };
288
+ }
289
+ // Get provider info for parameter filtering
290
+ const providerInfo = (0, config_1.getProviderById)(providerId);
291
+ if (!providerInfo) {
292
+ return {
293
+ error: {
294
+ provider: providerId,
295
+ model: modelId,
296
+ error: {
297
+ message: `Provider information not found: ${providerId}`,
298
+ code: "PROVIDER_ERROR",
299
+ type: "server_error",
300
+ },
301
+ object: "error",
302
+ },
303
+ };
304
+ }
305
+ // Filter out unsupported parameters
306
+ const filteredSettings = this.settingsManager.filterUnsupportedParameters(finalSettings, modelInfo, providerInfo);
307
+ const internalRequest = {
308
+ ...resolvedRequest,
309
+ settings: filteredSettings,
310
+ };
311
+ this.logger.debug(`Processing LLM request with (potentially filtered) settings:`, {
312
+ provider: providerId,
313
+ model: modelId,
314
+ settings: filteredSettings,
315
+ messageCount: resolvedRequest.messages.length,
316
+ });
317
+ // Get client adapter
318
+ const clientAdapter = this.adapterRegistry.getAdapter(providerId);
319
+ try {
320
+ const apiKey = await this.getApiKey(providerId);
321
+ if (!apiKey) {
322
+ return {
323
+ error: {
324
+ provider: providerId,
325
+ model: modelId,
326
+ error: {
327
+ message: `API key for provider '${providerId}' could not be retrieved. Ensure your ApiKeyProvider is configured correctly.`,
328
+ code: "API_KEY_ERROR",
329
+ type: "authentication_error",
330
+ },
331
+ object: "error",
332
+ },
333
+ };
334
+ }
335
+ // Validate API key format if adapter supports it
336
+ if (clientAdapter.validateApiKey && !clientAdapter.validateApiKey(apiKey)) {
337
+ return {
338
+ error: {
339
+ provider: providerId,
340
+ model: modelId,
341
+ error: {
342
+ message: `Invalid API key format for provider '${providerId}'. Please check your API key.`,
343
+ code: "INVALID_API_KEY",
344
+ type: "authentication_error",
345
+ },
346
+ object: "error",
347
+ },
348
+ };
349
+ }
350
+ const adapterOptions = {
351
+ ...(callOptions?.signal && { signal: callOptions.signal }),
352
+ ...((callOptions?.timeoutMs ?? this.defaultTimeoutMs) !== undefined && {
353
+ timeoutMs: callOptions?.timeoutMs ?? this.defaultTimeoutMs,
354
+ }),
355
+ };
356
+ return {
357
+ prepared: {
358
+ providerId: providerId,
359
+ modelId: modelId,
360
+ modelInfo: modelInfo,
361
+ resolvedRequest,
362
+ internalRequest,
363
+ clientAdapter,
364
+ apiKey,
365
+ adapterOptions,
366
+ },
367
+ };
368
+ }
369
+ catch (error) {
370
+ this.logger.error("Error preparing LLM request:", error);
371
+ return {
372
+ error: {
373
+ provider: providerId,
374
+ model: modelId,
375
+ error: {
376
+ message: error instanceof Error
377
+ ? error.message
378
+ : "An unknown error occurred during request preparation.",
379
+ code: "PROVIDER_ERROR",
380
+ type: "server_error",
381
+ providerError: error,
382
+ },
383
+ object: "error",
384
+ },
385
+ };
386
+ }
387
+ }
388
+ postProcessResponse(result, prepared) {
389
+ if (result.object === "error") {
390
+ return result;
391
+ }
392
+ // Post-process for thinking tag fallback
393
+ // This feature extracts reasoning from XML tags when native reasoning is not active.
394
+ // It's a fallback mechanism for models without native reasoning or when native is disabled.
395
+ const fallbackSettings = prepared.internalRequest.settings.thinkingTagFallback;
396
+ if (fallbackSettings && fallbackSettings.enabled !== false) {
397
+ const tagName = fallbackSettings.tagName || 'thinking';
398
+ // Check if native reasoning is active for this request
399
+ const isNativeReasoningActive = prepared.modelInfo.reasoning?.supported === true &&
400
+ (prepared.internalRequest.settings.reasoning?.enabled === true ||
401
+ (prepared.modelInfo.reasoning?.enabledByDefault === true &&
402
+ prepared.internalRequest.settings.reasoning?.enabled !== false) ||
403
+ prepared.modelInfo.reasoning?.canDisable === false);
404
+ // Process the response - extract thinking tags if present
405
+ const choice = result.choices[0];
406
+ if (choice?.message?.content) {
407
+ const { extracted, remaining } = (0, parser_1.extractInitialTaggedContent)(choice.message.content, tagName);
408
+ if (extracted !== null) {
409
+ // Success: thinking tag found
410
+ this.logger.debug(`Extracted <${tagName}> block from response.`);
411
+ // Handle the edge case: append to existing reasoning if present (e.g., native reasoning + thinking tags)
412
+ const existingReasoning = choice.reasoning || '';
413
+ if (existingReasoning) {
414
+ // Use a neutral markdown header that works for any consumer (human or AI)
415
+ choice.reasoning = `${existingReasoning}\n\n#### Additional Reasoning\n\n${extracted}`;
416
+ }
417
+ else {
418
+ // No existing reasoning, just use the extracted content directly
419
+ choice.reasoning = extracted;
420
+ }
421
+ choice.message.content = remaining;
422
+ }
423
+ else {
424
+ // Tag was not found
425
+ // Enforce only if: (1) enforce: true AND (2) native reasoning is NOT active
426
+ if (fallbackSettings.enforce === true && !isNativeReasoningActive) {
427
+ const nativeReasoningCapable = prepared.modelInfo.reasoning?.supported === true;
428
+ return {
429
+ provider: prepared.providerId,
430
+ model: prepared.modelId,
431
+ error: {
432
+ message: `Model response missing required <${tagName}> tags.`,
433
+ code: "THINKING_TAGS_MISSING",
434
+ type: "validation_error",
435
+ param: nativeReasoningCapable && !isNativeReasoningActive
436
+ ? `You disabled native reasoning for this model (${prepared.modelId}). ` +
437
+ `To see its reasoning, you must prompt it to use <${tagName}> tags. ` +
438
+ `Example: "Write your step-by-step reasoning in <${tagName}> tags before answering."`
439
+ : `This model (${prepared.modelId}) does not support native reasoning. ` +
440
+ `To get reasoning, you must prompt it to use <${tagName}> tags. ` +
441
+ `Example: "Write your step-by-step reasoning in <${tagName}> tags before answering."`,
442
+ },
443
+ object: "error",
444
+ partialResponse: {
445
+ id: result.id,
446
+ provider: result.provider,
447
+ model: result.model,
448
+ created: result.created,
449
+ choices: result.choices,
450
+ usage: result.usage
451
+ }
452
+ };
453
+ }
454
+ // If enforce: false or native reasoning is active, do nothing
455
+ }
456
+ }
457
+ }
458
+ // Post-process for structured output auto-parsing
459
+ // Parse JSON response when structuredOutput is enabled and autoParse is not disabled
460
+ const structuredOutputSettings = prepared.internalRequest.settings.structuredOutput;
461
+ if (structuredOutputSettings &&
462
+ structuredOutputSettings.enabled !== false && structuredOutputSettings.autoParse !== false) {
463
+ for (const choice of result.choices) {
464
+ if (choice.message?.content) {
465
+ try {
466
+ choice.parsedContent = JSON.parse(choice.message.content);
467
+ }
468
+ catch (e) {
469
+ choice.parseError = `JSON parse failed: ${e instanceof Error ? e.message : String(e)}`;
470
+ this.logger.warn(`Failed to parse structured output for choice ${choice.index}: ${choice.parseError}`);
471
+ }
472
+ }
473
+ }
474
+ }
475
+ return result;
476
+ }
337
477
  /**
338
478
  * Gets all configured model presets
339
479
  *