genai-lite 0.8.2 → 0.9.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/LICENSE +21 -21
- package/README.md +210 -207
- package/dist/adapters/image/GenaiElectronImageAdapter.d.ts +1 -0
- package/dist/adapters/image/GenaiElectronImageAdapter.js +5 -5
- package/dist/adapters/image/OpenAIImageAdapter.d.ts +1 -0
- package/dist/adapters/image/OpenAIImageAdapter.js +4 -4
- package/dist/config/llm-presets.json +24 -0
- package/dist/image/ImageService.js +2 -0
- package/dist/index.d.ts +4 -2
- package/dist/index.js +6 -1
- package/dist/llm/LLMService.d.ts +30 -1
- package/dist/llm/LLMService.js +43 -5
- package/dist/llm/clients/AnthropicClientAdapter.d.ts +6 -2
- package/dist/llm/clients/AnthropicClientAdapter.js +26 -11
- package/dist/llm/clients/GeminiClientAdapter.d.ts +6 -2
- package/dist/llm/clients/GeminiClientAdapter.js +21 -9
- package/dist/llm/clients/LlamaCppClientAdapter.d.ts +8 -4
- package/dist/llm/clients/LlamaCppClientAdapter.js +131 -28
- package/dist/llm/clients/MistralClientAdapter.d.ts +6 -2
- package/dist/llm/clients/MistralClientAdapter.js +22 -11
- package/dist/llm/clients/MockClientAdapter.d.ts +2 -2
- package/dist/llm/clients/MockClientAdapter.js +36 -19
- package/dist/llm/clients/OpenAIClientAdapter.d.ts +6 -2
- package/dist/llm/clients/OpenAIClientAdapter.js +33 -9
- package/dist/llm/clients/OpenRouterClientAdapter.d.ts +6 -2
- package/dist/llm/clients/OpenRouterClientAdapter.js +87 -16
- package/dist/llm/clients/types.d.ts +18 -1
- package/dist/llm/clients/types.js +4 -0
- package/dist/llm/config.d.ts +9 -2
- package/dist/llm/config.js +697 -33
- package/dist/llm/services/ModelResolver.d.ts +6 -0
- package/dist/llm/services/ModelResolver.js +57 -17
- package/dist/llm/services/RequestValidator.d.ts +8 -0
- package/dist/llm/services/RequestValidator.js +9 -2
- package/dist/llm/services/SettingsManager.d.ts +1 -1
- package/dist/llm/services/SettingsManager.js +92 -4
- package/dist/llm/types.d.ts +127 -0
- package/dist/prompting/index.d.ts +1 -1
- package/dist/prompting/index.js +2 -1
- package/dist/prompting/parser.d.ts +23 -0
- package/dist/prompting/parser.js +33 -0
- package/dist/shared/adapters/errorUtils.d.ts +15 -0
- package/dist/shared/adapters/errorUtils.js +69 -1
- package/dist/shared/adapters/logprobsUtils.d.ts +13 -0
- package/dist/shared/adapters/logprobsUtils.js +33 -0
- package/dist/shared/services/AdapterRegistry.js +3 -1
- package/dist/shared/services/withRetry.d.ts +50 -0
- package/dist/shared/services/withRetry.js +78 -0
- package/dist/types/image.d.ts +2 -0
- package/package.json +59 -59
- package/src/config/image-presets.json +212 -212
- package/src/config/llm-presets.json +623 -599
|
@@ -12,8 +12,9 @@ const errorUtils_1 = require("../../shared/adapters/errorUtils");
|
|
|
12
12
|
const systemMessageUtils_1 = require("../../shared/adapters/systemMessageUtils");
|
|
13
13
|
const LlamaCppServerClient_1 = require("./LlamaCppServerClient");
|
|
14
14
|
const config_1 = require("../config");
|
|
15
|
+
const parser_1 = require("../../prompting/parser");
|
|
16
|
+
const logprobsUtils_1 = require("../../shared/adapters/logprobsUtils");
|
|
15
17
|
const defaultLogger_1 = require("../../logging/defaultLogger");
|
|
16
|
-
const logger = (0, defaultLogger_1.createDefaultLogger)();
|
|
17
18
|
/**
|
|
18
19
|
* Client adapter for llama.cpp server integration
|
|
19
20
|
*
|
|
@@ -34,7 +35,7 @@ const logger = (0, defaultLogger_1.createDefaultLogger)();
|
|
|
34
35
|
* ```typescript
|
|
35
36
|
* // Create adapter for local server
|
|
36
37
|
* const adapter = new LlamaCppClientAdapter({
|
|
37
|
-
* baseURL: 'http://
|
|
38
|
+
* baseURL: 'http://127.0.0.1:8080',
|
|
38
39
|
* checkHealth: true
|
|
39
40
|
* });
|
|
40
41
|
*
|
|
@@ -58,9 +59,13 @@ class LlamaCppClientAdapter {
|
|
|
58
59
|
constructor(config) {
|
|
59
60
|
this.cachedModelCapabilities = null;
|
|
60
61
|
this.detectionAttempted = false;
|
|
61
|
-
|
|
62
|
+
// 127.0.0.1 rather than localhost: on Windows, localhost resolves to IPv6 (::1)
|
|
63
|
+
// first, and when llama-server listens on IPv4 only, every fresh connection waits
|
|
64
|
+
// ~2s for the IPv6 attempt to time out (~9x per-request slowdown measured).
|
|
65
|
+
this.baseURL = config?.baseURL || 'http://127.0.0.1:8080';
|
|
62
66
|
this.checkHealth = config?.checkHealth || false;
|
|
63
67
|
this.serverClient = new LlamaCppServerClient_1.LlamaCppServerClient(this.baseURL);
|
|
68
|
+
this.logger = config?.logger ?? (0, defaultLogger_1.createDefaultLogger)();
|
|
64
69
|
}
|
|
65
70
|
/**
|
|
66
71
|
* Gets model capabilities by detecting the loaded GGUF model
|
|
@@ -81,10 +86,10 @@ class LlamaCppClientAdapter {
|
|
|
81
86
|
}
|
|
82
87
|
// Attempt detection
|
|
83
88
|
try {
|
|
84
|
-
logger.debug(`Detecting model capabilities from llama.cpp server at ${this.baseURL}`);
|
|
89
|
+
this.logger.debug(`Detecting model capabilities from llama.cpp server at ${this.baseURL}`);
|
|
85
90
|
const { data } = await this.serverClient.getModels();
|
|
86
91
|
if (!data || data.length === 0) {
|
|
87
|
-
logger.warn('No models loaded in llama.cpp server');
|
|
92
|
+
this.logger.warn('No models loaded in llama.cpp server');
|
|
88
93
|
this.detectionAttempted = true;
|
|
89
94
|
return null;
|
|
90
95
|
}
|
|
@@ -94,15 +99,15 @@ class LlamaCppClientAdapter {
|
|
|
94
99
|
this.cachedModelCapabilities = capabilities;
|
|
95
100
|
this.detectionAttempted = true;
|
|
96
101
|
if (capabilities) {
|
|
97
|
-
logger.debug(`Cached model capabilities for: ${ggufFilename}`);
|
|
102
|
+
this.logger.debug(`Cached model capabilities for: ${ggufFilename}`);
|
|
98
103
|
}
|
|
99
104
|
else {
|
|
100
|
-
logger.debug(`No known pattern matched for: ${ggufFilename}`);
|
|
105
|
+
this.logger.debug(`No known pattern matched for: ${ggufFilename}`);
|
|
101
106
|
}
|
|
102
107
|
return capabilities;
|
|
103
108
|
}
|
|
104
109
|
catch (error) {
|
|
105
|
-
logger.warn('Failed to detect model capabilities:', error);
|
|
110
|
+
this.logger.warn('Failed to detect model capabilities:', error);
|
|
106
111
|
this.detectionAttempted = true;
|
|
107
112
|
return null;
|
|
108
113
|
}
|
|
@@ -116,7 +121,7 @@ class LlamaCppClientAdapter {
|
|
|
116
121
|
clearModelCache() {
|
|
117
122
|
this.cachedModelCapabilities = null;
|
|
118
123
|
this.detectionAttempted = false;
|
|
119
|
-
logger.debug('Cleared model capabilities cache');
|
|
124
|
+
this.logger.debug('Cleared model capabilities cache');
|
|
120
125
|
}
|
|
121
126
|
/**
|
|
122
127
|
* Sends a chat message to llama.cpp server
|
|
@@ -125,7 +130,7 @@ class LlamaCppClientAdapter {
|
|
|
125
130
|
* @param apiKey - Not used for llama.cpp (local server), but kept for interface compatibility
|
|
126
131
|
* @returns Promise resolving to success or failure response
|
|
127
132
|
*/
|
|
128
|
-
async sendMessage(request, apiKey) {
|
|
133
|
+
async sendMessage(request, apiKey, options) {
|
|
129
134
|
try {
|
|
130
135
|
// Optional health check before making request
|
|
131
136
|
if (this.checkHealth) {
|
|
@@ -145,7 +150,7 @@ class LlamaCppClientAdapter {
|
|
|
145
150
|
}
|
|
146
151
|
}
|
|
147
152
|
catch (healthError) {
|
|
148
|
-
logger.warn('Health check failed, proceeding with request anyway:', healthError);
|
|
153
|
+
this.logger.warn('Health check failed, proceeding with request anyway:', healthError);
|
|
149
154
|
}
|
|
150
155
|
}
|
|
151
156
|
// Initialize OpenAI client with llama.cpp base URL
|
|
@@ -153,6 +158,7 @@ class LlamaCppClientAdapter {
|
|
|
153
158
|
const openai = new openai_1.default({
|
|
154
159
|
apiKey: apiKey || 'not-needed',
|
|
155
160
|
baseURL: `${this.baseURL}/v1`,
|
|
161
|
+
maxRetries: 0, // retries are owned by the unified LLMService retry layer
|
|
156
162
|
});
|
|
157
163
|
// Format messages for OpenAI-compatible API
|
|
158
164
|
const messages = this.formatMessages(request);
|
|
@@ -172,6 +178,20 @@ class LlamaCppClientAdapter {
|
|
|
172
178
|
...(request.settings.presencePenalty !== 0 && {
|
|
173
179
|
presence_penalty: request.settings.presencePenalty,
|
|
174
180
|
}),
|
|
181
|
+
...(request.settings.seed !== undefined && {
|
|
182
|
+
seed: request.settings.seed,
|
|
183
|
+
}),
|
|
184
|
+
// llama.cpp-native sampling params (not in the OpenAI SDK types, but the
|
|
185
|
+
// SDK sends extra body fields as-is and llama-server reads them top-level)
|
|
186
|
+
...(request.settings.topK !== undefined && {
|
|
187
|
+
top_k: request.settings.topK,
|
|
188
|
+
}),
|
|
189
|
+
...(request.settings.minP !== undefined && {
|
|
190
|
+
min_p: request.settings.minP,
|
|
191
|
+
}),
|
|
192
|
+
...(request.settings.repeatPenalty !== undefined && {
|
|
193
|
+
repeat_penalty: request.settings.repeatPenalty,
|
|
194
|
+
}),
|
|
175
195
|
};
|
|
176
196
|
// Handle structured output configuration for llama.cpp
|
|
177
197
|
// llama.cpp uses response_format with type: 'json_object' and optional schema
|
|
@@ -182,27 +202,83 @@ class LlamaCppClientAdapter {
|
|
|
182
202
|
schema: so.schema,
|
|
183
203
|
};
|
|
184
204
|
}
|
|
185
|
-
|
|
205
|
+
// Reasoning toggle for detected hybrid GGUF models (Qwen 3.x, Gemma 4, ...).
|
|
206
|
+
// The chat template's thinking flag is sent explicitly — false unless reasoning
|
|
207
|
+
// was requested — so hybrid models don't silently burn thinking tokens.
|
|
208
|
+
// Requires llama-server running with --jinja; servers without it ignore the kwarg.
|
|
209
|
+
const detectedCaps = await this.getModelCapabilities();
|
|
210
|
+
const localReasoning = detectedCaps?.localReasoning;
|
|
211
|
+
const derivedKwargs = {};
|
|
212
|
+
if (localReasoning?.toggleKwarg) {
|
|
213
|
+
derivedKwargs[localReasoning.toggleKwarg] =
|
|
214
|
+
request.settings.reasoning?.enabled === true;
|
|
215
|
+
}
|
|
216
|
+
// User-supplied chat-template kwargs (escape hatch) win over derived ones
|
|
217
|
+
const chatTemplateKwargs = {
|
|
218
|
+
...derivedKwargs,
|
|
219
|
+
...(request.settings.llamacpp?.chatTemplateKwargs || {}),
|
|
220
|
+
};
|
|
221
|
+
if (Object.keys(chatTemplateKwargs).length > 0) {
|
|
222
|
+
// llama-server rejects assistant prefill together with enable_thinking=true
|
|
223
|
+
// (HTTP 400: "Assistant response prefill is incompatible with enable_thinking.")
|
|
224
|
+
// Fail fast with a clear message instead of surfacing the raw server error.
|
|
225
|
+
const thinkingKwarg = localReasoning?.toggleKwarg ?? 'enable_thinking';
|
|
226
|
+
const effectiveThinking = chatTemplateKwargs[thinkingKwarg] === true;
|
|
227
|
+
const lastMessage = messages[messages.length - 1];
|
|
228
|
+
if (effectiveThinking && lastMessage?.role === 'assistant') {
|
|
229
|
+
return {
|
|
230
|
+
provider: request.providerId,
|
|
231
|
+
model: request.modelId,
|
|
232
|
+
error: {
|
|
233
|
+
message: 'llama.cpp does not support assistant prefill (a trailing assistant message) ' +
|
|
234
|
+
'while thinking is enabled. Remove the trailing assistant message or disable reasoning.',
|
|
235
|
+
code: types_1.ADAPTER_ERROR_CODES.PROVIDER_ERROR,
|
|
236
|
+
type: 'invalid_request_error',
|
|
237
|
+
},
|
|
238
|
+
object: 'error',
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
completionParams.chat_template_kwargs = chatTemplateKwargs;
|
|
242
|
+
}
|
|
243
|
+
// GBNF grammar for constrained decoding (validated as mutually exclusive
|
|
244
|
+
// with structuredOutput upstream)
|
|
245
|
+
if (request.settings.llamacpp?.grammar) {
|
|
246
|
+
completionParams.grammar = request.settings.llamacpp.grammar;
|
|
247
|
+
}
|
|
248
|
+
// Per-token log probabilities (OpenAI-compatible request/response shape)
|
|
249
|
+
if (request.settings.logprobs === true) {
|
|
250
|
+
completionParams.logprobs = true;
|
|
251
|
+
if (request.settings.topLogprobs !== undefined) {
|
|
252
|
+
completionParams.top_logprobs = request.settings.topLogprobs;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
this.logger.debug(`llama.cpp API parameters:`, {
|
|
186
256
|
baseURL: this.baseURL,
|
|
187
257
|
model: completionParams.model,
|
|
188
258
|
temperature: completionParams.temperature,
|
|
189
259
|
max_tokens: completionParams.max_tokens,
|
|
190
260
|
top_p: completionParams.top_p,
|
|
191
261
|
});
|
|
192
|
-
logger.info(`Making llama.cpp API call for model: ${request.modelId}`);
|
|
262
|
+
this.logger.info(`Making llama.cpp API call for model: ${request.modelId}`);
|
|
193
263
|
// Make the API call
|
|
194
|
-
const
|
|
264
|
+
const transportOptions = {
|
|
265
|
+
...(options?.signal && { signal: options.signal }),
|
|
266
|
+
...(options?.timeoutMs !== undefined && { timeout: options.timeoutMs }),
|
|
267
|
+
};
|
|
268
|
+
const completion = Object.keys(transportOptions).length > 0
|
|
269
|
+
? await openai.chat.completions.create(completionParams, transportOptions)
|
|
270
|
+
: await openai.chat.completions.create(completionParams);
|
|
195
271
|
// Type guard to ensure we have a non-streaming response
|
|
196
272
|
if ('id' in completion && 'choices' in completion) {
|
|
197
|
-
logger.info(`llama.cpp API call successful, response ID: ${completion.id}`);
|
|
198
|
-
return this.createSuccessResponse(completion, request);
|
|
273
|
+
this.logger.info(`llama.cpp API call successful, response ID: ${completion.id}`);
|
|
274
|
+
return this.createSuccessResponse(completion, request, detectedCaps);
|
|
199
275
|
}
|
|
200
276
|
else {
|
|
201
277
|
throw new Error('Unexpected streaming response from llama.cpp server');
|
|
202
278
|
}
|
|
203
279
|
}
|
|
204
280
|
catch (error) {
|
|
205
|
-
logger.error("llama.cpp API error:", error);
|
|
281
|
+
this.logger.error("llama.cpp API error:", error);
|
|
206
282
|
// Clear cache on connection errors so we re-detect on next request
|
|
207
283
|
const errorMessage = error?.message || String(error);
|
|
208
284
|
if (errorMessage.includes("ECONNREFUSED") ||
|
|
@@ -296,7 +372,7 @@ class LlamaCppClientAdapter {
|
|
|
296
372
|
const modifiedIndex = (0, systemMessageUtils_1.prependSystemToFirstUserMessage)(simpleMessages, combinedSystemContent, request.settings.systemMessageFallback);
|
|
297
373
|
if (modifiedIndex !== -1) {
|
|
298
374
|
messages[modifiedIndex].content = simpleMessages[modifiedIndex].content;
|
|
299
|
-
logger.debug(`Model ${request.modelId} doesn't support system messages - prepended to first user message`);
|
|
375
|
+
this.logger.debug(`Model ${request.modelId} doesn't support system messages - prepended to first user message`);
|
|
300
376
|
}
|
|
301
377
|
}
|
|
302
378
|
}
|
|
@@ -309,36 +385,62 @@ class LlamaCppClientAdapter {
|
|
|
309
385
|
* @param request - Original request for context
|
|
310
386
|
* @returns Standardized LLM response
|
|
311
387
|
*/
|
|
312
|
-
createSuccessResponse(completion, request) {
|
|
388
|
+
createSuccessResponse(completion, request, detectedCaps) {
|
|
313
389
|
const choice = completion.choices[0];
|
|
314
390
|
if (!choice || !choice.message) {
|
|
315
391
|
throw new Error("No valid choices in llama.cpp completion response");
|
|
316
392
|
}
|
|
317
|
-
|
|
318
|
-
//
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
393
|
+
const local = detectedCaps?.localReasoning;
|
|
394
|
+
// Whether thinking is active for this request: explicitly requested, or the
|
|
395
|
+
// detected model reasons unconditionally (always-on models like GPT-OSS,
|
|
396
|
+
// Thinking-2507 checkpoints, DeepSeek R1).
|
|
397
|
+
const reasoningActive = request.settings.reasoning?.enabled === true ||
|
|
398
|
+
detectedCaps?.reasoning?.enabledByDefault === true ||
|
|
399
|
+
detectedCaps?.reasoning?.canDisable === false;
|
|
400
|
+
const excludeReasoning = request.settings.reasoning?.exclude === true;
|
|
323
401
|
return {
|
|
324
402
|
id: completion.id,
|
|
325
403
|
provider: request.providerId,
|
|
326
404
|
model: completion.model || request.modelId,
|
|
327
405
|
created: completion.created,
|
|
328
406
|
choices: completion.choices.map((c) => {
|
|
407
|
+
let content = c.message.content || "";
|
|
408
|
+
// Strip the template-injected "nothink" prefix (some chat templates leak an
|
|
409
|
+
// empty think block into content when thinking is disabled). Exact match.
|
|
410
|
+
if (local?.nothinkPrefix && content.startsWith(local.nothinkPrefix)) {
|
|
411
|
+
content = content.slice(local.nothinkPrefix.length);
|
|
412
|
+
}
|
|
329
413
|
const mappedChoice = {
|
|
330
414
|
message: {
|
|
331
415
|
role: "assistant",
|
|
332
|
-
content
|
|
416
|
+
content,
|
|
333
417
|
},
|
|
334
418
|
finish_reason: c.finish_reason,
|
|
335
419
|
index: c.index,
|
|
336
420
|
};
|
|
337
|
-
//
|
|
421
|
+
// Two-tier reasoning extraction:
|
|
422
|
+
// 1. Prefer the server-separated reasoning_content field (populated when
|
|
423
|
+
// llama-server's --reasoning-format handling recognizes the template).
|
|
424
|
+
// 2. Fall back to marker extraction from content — reasoning_content
|
|
425
|
+
// population is model/template-dependent, not guaranteed.
|
|
338
426
|
const messageReasoning = c.message.reasoning_content;
|
|
339
|
-
if (messageReasoning && request.settings.reasoning && !
|
|
427
|
+
if (messageReasoning && request.settings.reasoning && !excludeReasoning) {
|
|
340
428
|
mappedChoice.reasoning = messageReasoning;
|
|
341
429
|
}
|
|
430
|
+
else if (!messageReasoning && reasoningActive && local?.markers) {
|
|
431
|
+
const { content: cleaned, extracted } = (0, parser_1.extractMarkerDelimitedContent)(content, local.markers[0], local.markers[1]);
|
|
432
|
+
if (extracted) {
|
|
433
|
+
mappedChoice.message.content = cleaned;
|
|
434
|
+
if (!excludeReasoning) {
|
|
435
|
+
mappedChoice.reasoning = extracted;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
// Per-token log probabilities (OpenAI-compatible shape)
|
|
440
|
+
const logprobs = (0, logprobsUtils_1.mapOpenAIChatLogprobs)(c.logprobs);
|
|
441
|
+
if (logprobs) {
|
|
442
|
+
mappedChoice.logprobs = logprobs;
|
|
443
|
+
}
|
|
342
444
|
return mappedChoice;
|
|
343
445
|
}),
|
|
344
446
|
usage: completion.usage
|
|
@@ -389,6 +491,7 @@ class LlamaCppClientAdapter {
|
|
|
389
491
|
code: mappedError.errorCode,
|
|
390
492
|
type: mappedError.errorType,
|
|
391
493
|
...(mappedError.status && { status: mappedError.status }),
|
|
494
|
+
...(mappedError.retryAfterMs !== undefined && { retryAfterMs: mappedError.retryAfterMs }),
|
|
392
495
|
providerError: error,
|
|
393
496
|
},
|
|
394
497
|
object: "error",
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import type { LLMResponse, LLMFailureResponse } from "../types";
|
|
2
|
-
import type { ILLMClientAdapter, InternalLLMChatRequest } from "./types";
|
|
2
|
+
import type { ILLMClientAdapter, InternalLLMChatRequest, AdapterRequestOptions } from "./types";
|
|
3
|
+
import type { Logger } from "../../logging/types";
|
|
3
4
|
/**
|
|
4
5
|
* Configuration options for MistralClientAdapter
|
|
5
6
|
*/
|
|
6
7
|
export interface MistralClientConfig {
|
|
7
8
|
/** Base URL of the Mistral API (default: https://api.mistral.ai) */
|
|
8
9
|
baseURL?: string;
|
|
10
|
+
/** Logger instance for adapter logging */
|
|
11
|
+
logger?: Logger;
|
|
9
12
|
}
|
|
10
13
|
/**
|
|
11
14
|
* Client adapter for Mistral AI API integration
|
|
@@ -35,6 +38,7 @@ export interface MistralClientConfig {
|
|
|
35
38
|
*/
|
|
36
39
|
export declare class MistralClientAdapter implements ILLMClientAdapter {
|
|
37
40
|
private baseURL;
|
|
41
|
+
private logger;
|
|
38
42
|
/**
|
|
39
43
|
* Creates a new Mistral client adapter
|
|
40
44
|
*
|
|
@@ -48,7 +52,7 @@ export declare class MistralClientAdapter implements ILLMClientAdapter {
|
|
|
48
52
|
* @param apiKey - The Mistral API key
|
|
49
53
|
* @returns Promise resolving to success or failure response
|
|
50
54
|
*/
|
|
51
|
-
sendMessage(request: InternalLLMChatRequest, apiKey: string): Promise<LLMResponse | LLMFailureResponse>;
|
|
55
|
+
sendMessage(request: InternalLLMChatRequest, apiKey: string, options?: AdapterRequestOptions): Promise<LLMResponse | LLMFailureResponse>;
|
|
52
56
|
/**
|
|
53
57
|
* Validates Mistral API key format
|
|
54
58
|
*
|
|
@@ -8,7 +8,6 @@ const types_1 = require("./types");
|
|
|
8
8
|
const errorUtils_1 = require("../../shared/adapters/errorUtils");
|
|
9
9
|
const systemMessageUtils_1 = require("../../shared/adapters/systemMessageUtils");
|
|
10
10
|
const defaultLogger_1 = require("../../logging/defaultLogger");
|
|
11
|
-
const logger = (0, defaultLogger_1.createDefaultLogger)();
|
|
12
11
|
/**
|
|
13
12
|
* Client adapter for Mistral AI API integration
|
|
14
13
|
*
|
|
@@ -43,6 +42,7 @@ class MistralClientAdapter {
|
|
|
43
42
|
*/
|
|
44
43
|
constructor(config) {
|
|
45
44
|
this.baseURL = config?.baseURL || process.env.MISTRAL_API_BASE_URL || 'https://api.mistral.ai';
|
|
45
|
+
this.logger = config?.logger ?? (0, defaultLogger_1.createDefaultLogger)();
|
|
46
46
|
}
|
|
47
47
|
/**
|
|
48
48
|
* Sends a chat message to Mistral API
|
|
@@ -51,23 +51,24 @@ class MistralClientAdapter {
|
|
|
51
51
|
* @param apiKey - The Mistral API key
|
|
52
52
|
* @returns Promise resolving to success or failure response
|
|
53
53
|
*/
|
|
54
|
-
async sendMessage(request, apiKey) {
|
|
54
|
+
async sendMessage(request, apiKey, options) {
|
|
55
55
|
try {
|
|
56
|
-
// Initialize Mistral client
|
|
56
|
+
// Initialize Mistral client (Speakeasy SDK retry default is "none" — retries
|
|
57
|
+
// are owned by the unified LLMService retry layer)
|
|
57
58
|
const mistral = new mistralai_1.Mistral({
|
|
58
59
|
apiKey,
|
|
59
60
|
serverURL: this.baseURL !== 'https://api.mistral.ai' ? this.baseURL : undefined,
|
|
60
61
|
});
|
|
61
62
|
// Format messages for Mistral API
|
|
62
63
|
const messages = this.formatMessages(request);
|
|
63
|
-
logger.debug(`Mistral API parameters:`, {
|
|
64
|
+
this.logger.debug(`Mistral API parameters:`, {
|
|
64
65
|
baseURL: this.baseURL,
|
|
65
66
|
model: request.modelId,
|
|
66
67
|
temperature: request.settings.temperature,
|
|
67
68
|
max_tokens: request.settings.maxTokens,
|
|
68
69
|
top_p: request.settings.topP,
|
|
69
70
|
});
|
|
70
|
-
logger.info(`Making Mistral API call for model: ${request.modelId}`);
|
|
71
|
+
this.logger.info(`Making Mistral API call for model: ${request.modelId}`);
|
|
71
72
|
// Build request options
|
|
72
73
|
const requestOptions = {
|
|
73
74
|
model: request.modelId,
|
|
@@ -78,19 +79,28 @@ class MistralClientAdapter {
|
|
|
78
79
|
...(request.settings.stopSequences.length > 0 && {
|
|
79
80
|
stop: request.settings.stopSequences,
|
|
80
81
|
}),
|
|
82
|
+
...(request.settings.seed !== undefined && {
|
|
83
|
+
randomSeed: request.settings.seed,
|
|
84
|
+
}),
|
|
81
85
|
// Note: Mistral does not support frequency_penalty or presence_penalty
|
|
82
86
|
};
|
|
83
87
|
// Handle structured output configuration for Mistral
|
|
84
88
|
// Mistral only supports json_object mode, no schema validation
|
|
85
89
|
if (request.settings.structuredOutput?.schema && request.settings.structuredOutput.enabled !== false) {
|
|
86
90
|
requestOptions.responseFormat = { type: 'json_object' };
|
|
87
|
-
logger.warn(`Mistral does not support JSON schema validation. ` +
|
|
91
|
+
this.logger.warn(`Mistral does not support JSON schema validation. ` +
|
|
88
92
|
`Using json_object mode - schema validation will be client-side only.`);
|
|
89
93
|
}
|
|
90
|
-
// Make the API call
|
|
91
|
-
const
|
|
94
|
+
// Make the API call (per-request transport options: timeout + abort signal)
|
|
95
|
+
const transportOptions = {
|
|
96
|
+
...(options?.timeoutMs !== undefined && { timeoutMs: options.timeoutMs }),
|
|
97
|
+
...(options?.signal && { fetchOptions: { signal: options.signal } }),
|
|
98
|
+
};
|
|
99
|
+
const completion = Object.keys(transportOptions).length > 0
|
|
100
|
+
? await mistral.chat.complete(requestOptions, transportOptions)
|
|
101
|
+
: await mistral.chat.complete(requestOptions);
|
|
92
102
|
if (completion && completion.choices && completion.choices.length > 0) {
|
|
93
|
-
logger.info(`Mistral API call successful, response ID: ${completion.id}`);
|
|
103
|
+
this.logger.info(`Mistral API call successful, response ID: ${completion.id}`);
|
|
94
104
|
return this.createSuccessResponse(completion, request);
|
|
95
105
|
}
|
|
96
106
|
else {
|
|
@@ -98,7 +108,7 @@ class MistralClientAdapter {
|
|
|
98
108
|
}
|
|
99
109
|
}
|
|
100
110
|
catch (error) {
|
|
101
|
-
logger.error("Mistral API error:", error);
|
|
111
|
+
this.logger.error("Mistral API error:", error);
|
|
102
112
|
return this.createErrorResponse(error, request);
|
|
103
113
|
}
|
|
104
114
|
}
|
|
@@ -170,7 +180,7 @@ class MistralClientAdapter {
|
|
|
170
180
|
// Model doesn't support system messages - prepend to first user message
|
|
171
181
|
const modifiedIndex = (0, systemMessageUtils_1.prependSystemToFirstUserMessage)(messages, combinedSystemContent, request.settings.systemMessageFallback);
|
|
172
182
|
if (modifiedIndex !== -1) {
|
|
173
|
-
logger.debug(`Model ${request.modelId} doesn't support system messages - prepended to first user message`);
|
|
183
|
+
this.logger.debug(`Model ${request.modelId} doesn't support system messages - prepended to first user message`);
|
|
174
184
|
}
|
|
175
185
|
}
|
|
176
186
|
}
|
|
@@ -239,6 +249,7 @@ class MistralClientAdapter {
|
|
|
239
249
|
code: mappedError.errorCode,
|
|
240
250
|
type: mappedError.errorType,
|
|
241
251
|
...(mappedError.status && { status: mappedError.status }),
|
|
252
|
+
...(mappedError.retryAfterMs !== undefined && { retryAfterMs: mappedError.retryAfterMs }),
|
|
242
253
|
providerError: error,
|
|
243
254
|
},
|
|
244
255
|
object: "error",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { LLMResponse, LLMFailureResponse, ApiProviderId } from "../types";
|
|
2
|
-
import type { ILLMClientAdapter, InternalLLMChatRequest } from "./types";
|
|
2
|
+
import type { ILLMClientAdapter, InternalLLMChatRequest, AdapterRequestOptions } from "./types";
|
|
3
3
|
/**
|
|
4
4
|
* Mock client adapter for testing LLM functionality
|
|
5
5
|
*
|
|
@@ -20,7 +20,7 @@ export declare class MockClientAdapter implements ILLMClientAdapter {
|
|
|
20
20
|
* @param apiKey - The API key (ignored for mock)
|
|
21
21
|
* @returns Promise resolving to mock response
|
|
22
22
|
*/
|
|
23
|
-
sendMessage(request: InternalLLMChatRequest, apiKey: string): Promise<LLMResponse | LLMFailureResponse>;
|
|
23
|
+
sendMessage(request: InternalLLMChatRequest, apiKey: string, options?: AdapterRequestOptions): Promise<LLMResponse | LLMFailureResponse>;
|
|
24
24
|
/**
|
|
25
25
|
* Validates API key format (always returns true for mock)
|
|
26
26
|
*/
|
|
@@ -25,7 +25,20 @@ class MockClientAdapter {
|
|
|
25
25
|
* @param apiKey - The API key (ignored for mock)
|
|
26
26
|
* @returns Promise resolving to mock response
|
|
27
27
|
*/
|
|
28
|
-
async sendMessage(request, apiKey) {
|
|
28
|
+
async sendMessage(request, apiKey, options) {
|
|
29
|
+
// Honor abort signals like a real adapter would
|
|
30
|
+
if (options?.signal?.aborted) {
|
|
31
|
+
return {
|
|
32
|
+
provider: request.providerId,
|
|
33
|
+
model: request.modelId,
|
|
34
|
+
error: {
|
|
35
|
+
message: "Request was aborted",
|
|
36
|
+
code: types_1.ADAPTER_ERROR_CODES.REQUEST_ABORTED,
|
|
37
|
+
type: "abort_error",
|
|
38
|
+
},
|
|
39
|
+
object: "error",
|
|
40
|
+
};
|
|
41
|
+
}
|
|
29
42
|
// Simulate network delay
|
|
30
43
|
await this.simulateDelay(100, 500);
|
|
31
44
|
try {
|
|
@@ -213,15 +226,19 @@ class MockClientAdapter {
|
|
|
213
226
|
* Generates a response that shows current settings
|
|
214
227
|
*/
|
|
215
228
|
generateSettingsTestResponse(settings) {
|
|
216
|
-
return `Current mock settings:
|
|
217
|
-
- Temperature: ${settings.temperature}
|
|
218
|
-
- Max Tokens: ${settings.maxTokens}
|
|
219
|
-
- Top P: ${settings.topP}
|
|
229
|
+
return `Current mock settings:
|
|
230
|
+
- Temperature: ${settings.temperature}
|
|
231
|
+
- Max Tokens: ${settings.maxTokens}
|
|
232
|
+
- Top P: ${settings.topP}
|
|
220
233
|
- Stop Sequences: ${settings.stopSequences.length > 0
|
|
221
234
|
? settings.stopSequences.join(", ")
|
|
222
|
-
: "none"}
|
|
223
|
-
- Frequency Penalty: ${settings.frequencyPenalty}
|
|
224
|
-
- Presence Penalty: ${settings.presencePenalty}
|
|
235
|
+
: "none"}
|
|
236
|
+
- Frequency Penalty: ${settings.frequencyPenalty}
|
|
237
|
+
- Presence Penalty: ${settings.presencePenalty}
|
|
238
|
+
- Top K: ${settings.topK ?? "not set"}
|
|
239
|
+
- Min P: ${settings.minP ?? "not set"}
|
|
240
|
+
- Repeat Penalty: ${settings.repeatPenalty ?? "not set"}
|
|
241
|
+
- Seed: ${settings.seed ?? "not set"}
|
|
225
242
|
- User: ${settings.user || "not set"}`;
|
|
226
243
|
}
|
|
227
244
|
/**
|
|
@@ -285,17 +302,17 @@ class MockClientAdapter {
|
|
|
285
302
|
* Generates a longer mock response for testing
|
|
286
303
|
*/
|
|
287
304
|
generateLongResponse() {
|
|
288
|
-
return `This is a detailed mock response from the ${this.providerId} adapter.
|
|
289
|
-
|
|
290
|
-
I can simulate various types of responses based on your input. Here are some features:
|
|
291
|
-
|
|
292
|
-
1. **Error Simulation**: Include phrases like "error_rate_limit" to test error handling
|
|
293
|
-
2. **Variable Length**: Request "long" responses to test token limits
|
|
294
|
-
3. **Code Generation**: Ask about "programming" to get mock code snippets
|
|
295
|
-
4. **Conversational**: Simple greetings work too
|
|
296
|
-
|
|
297
|
-
The mock adapter is useful for testing the LLM integration without making real API calls. It simulates realistic response times, token usage, and various error conditions that you might encounter with real LLM providers.
|
|
298
|
-
|
|
305
|
+
return `This is a detailed mock response from the ${this.providerId} adapter.
|
|
306
|
+
|
|
307
|
+
I can simulate various types of responses based on your input. Here are some features:
|
|
308
|
+
|
|
309
|
+
1. **Error Simulation**: Include phrases like "error_rate_limit" to test error handling
|
|
310
|
+
2. **Variable Length**: Request "long" responses to test token limits
|
|
311
|
+
3. **Code Generation**: Ask about "programming" to get mock code snippets
|
|
312
|
+
4. **Conversational**: Simple greetings work too
|
|
313
|
+
|
|
314
|
+
The mock adapter is useful for testing the LLM integration without making real API calls. It simulates realistic response times, token usage, and various error conditions that you might encounter with real LLM providers.
|
|
315
|
+
|
|
299
316
|
This response demonstrates how the adapter can generate longer content while still respecting the maxTokens parameter if specified in the request settings.`;
|
|
300
317
|
}
|
|
301
318
|
/**
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { LLMResponse, LLMFailureResponse } from "../types";
|
|
2
|
-
import type { ILLMClientAdapter, InternalLLMChatRequest } from "./types";
|
|
2
|
+
import type { ILLMClientAdapter, InternalLLMChatRequest, AdapterRequestOptions } from "./types";
|
|
3
|
+
import type { Logger } from "../../logging/types";
|
|
3
4
|
/**
|
|
4
5
|
* Client adapter for OpenAI API integration
|
|
5
6
|
*
|
|
@@ -11,14 +12,17 @@ import type { ILLMClientAdapter, InternalLLMChatRequest } from "./types";
|
|
|
11
12
|
*/
|
|
12
13
|
export declare class OpenAIClientAdapter implements ILLMClientAdapter {
|
|
13
14
|
private baseURL?;
|
|
15
|
+
private logger;
|
|
14
16
|
/**
|
|
15
17
|
* Creates a new OpenAI client adapter
|
|
16
18
|
*
|
|
17
19
|
* @param config Optional configuration for the adapter
|
|
18
20
|
* @param config.baseURL Custom base URL for OpenAI-compatible APIs
|
|
21
|
+
* @param config.logger Custom logger instance
|
|
19
22
|
*/
|
|
20
23
|
constructor(config?: {
|
|
21
24
|
baseURL?: string;
|
|
25
|
+
logger?: Logger;
|
|
22
26
|
});
|
|
23
27
|
/**
|
|
24
28
|
* Sends a chat message to OpenAI's API
|
|
@@ -27,7 +31,7 @@ export declare class OpenAIClientAdapter implements ILLMClientAdapter {
|
|
|
27
31
|
* @param apiKey - The decrypted OpenAI API key
|
|
28
32
|
* @returns Promise resolving to success or failure response
|
|
29
33
|
*/
|
|
30
|
-
sendMessage(request: InternalLLMChatRequest, apiKey: string): Promise<LLMResponse | LLMFailureResponse>;
|
|
34
|
+
sendMessage(request: InternalLLMChatRequest, apiKey: string, options?: AdapterRequestOptions): Promise<LLMResponse | LLMFailureResponse>;
|
|
31
35
|
/**
|
|
32
36
|
* Validates OpenAI API key format
|
|
33
37
|
*
|