genai-lite 0.8.3 → 0.9.1
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/config/llm-presets.json +24 -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 +42 -4
- package/dist/llm/clients/AnthropicClientAdapter.d.ts +2 -2
- package/dist/llm/clients/AnthropicClientAdapter.js +17 -3
- package/dist/llm/clients/GeminiClientAdapter.d.ts +2 -2
- package/dist/llm/clients/GeminiClientAdapter.js +14 -3
- package/dist/llm/clients/LlamaCppClientAdapter.d.ts +4 -4
- package/dist/llm/clients/LlamaCppClientAdapter.js +118 -15
- package/dist/llm/clients/MistralClientAdapter.d.ts +2 -2
- package/dist/llm/clients/MistralClientAdapter.js +15 -4
- package/dist/llm/clients/MockClientAdapter.d.ts +2 -2
- package/dist/llm/clients/MockClientAdapter.js +36 -19
- package/dist/llm/clients/OpenAIClientAdapter.d.ts +2 -2
- package/dist/llm/clients/OpenAIClientAdapter.js +26 -3
- package/dist/llm/clients/OpenRouterClientAdapter.d.ts +2 -2
- package/dist/llm/clients/OpenRouterClientAdapter.js +81 -10
- 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 +695 -33
- package/dist/llm/services/ModelResolver.d.ts +6 -0
- package/dist/llm/services/ModelResolver.js +57 -17
- 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/withRetry.d.ts +50 -0
- package/dist/shared/services/withRetry.js +78 -0
- package/package.json +59 -59
- package/src/config/image-presets.json +212 -212
- package/src/config/llm-presets.json +623 -599
package/dist/llm/LLMService.js
CHANGED
|
@@ -18,6 +18,8 @@ const AdapterRegistry_1 = require("../shared/services/AdapterRegistry");
|
|
|
18
18
|
const RequestValidator_1 = require("./services/RequestValidator");
|
|
19
19
|
const SettingsManager_1 = require("./services/SettingsManager");
|
|
20
20
|
const ModelResolver_1 = require("./services/ModelResolver");
|
|
21
|
+
const withRetry_1 = require("../shared/services/withRetry");
|
|
22
|
+
const types_1 = require("./clients/types");
|
|
21
23
|
/**
|
|
22
24
|
* Main process service for LLM operations
|
|
23
25
|
*
|
|
@@ -32,6 +34,8 @@ const ModelResolver_1 = require("./services/ModelResolver");
|
|
|
32
34
|
class LLMService {
|
|
33
35
|
constructor(getApiKey, options = {}) {
|
|
34
36
|
this.getApiKey = getApiKey;
|
|
37
|
+
this.retryOptions = options.retry;
|
|
38
|
+
this.defaultTimeoutMs = options.timeoutMs;
|
|
35
39
|
// Initialize logger - custom logger takes precedence over logLevel
|
|
36
40
|
this.logger = options.logger ?? (0, defaultLogger_1.createDefaultLogger)(options.logLevel);
|
|
37
41
|
// Initialize services with logger
|
|
@@ -78,7 +82,7 @@ class LLMService {
|
|
|
78
82
|
* @param request - The LLM chat request
|
|
79
83
|
* @returns Promise resolving to either success or failure response
|
|
80
84
|
*/
|
|
81
|
-
async sendMessage(request) {
|
|
85
|
+
async sendMessage(request, callOptions) {
|
|
82
86
|
this.logger.info(`LLMService.sendMessage called with presetId: ${request.presetId}, provider: ${request.providerId}, model: ${request.modelId}`);
|
|
83
87
|
try {
|
|
84
88
|
// Resolve model information from preset or direct IDs
|
|
@@ -107,7 +111,7 @@ class LLMService {
|
|
|
107
111
|
}
|
|
108
112
|
}
|
|
109
113
|
// Apply model-specific defaults and merge with user settings
|
|
110
|
-
const finalSettings = this.settingsManager.mergeSettingsForModel(modelId, providerId, combinedSettings);
|
|
114
|
+
const finalSettings = this.settingsManager.mergeSettingsForModel(modelId, providerId, combinedSettings, modelInfo);
|
|
111
115
|
// Validate reasoning settings for model capabilities
|
|
112
116
|
const reasoningValidation = this.requestValidator.validateReasoningSettings(modelInfo, finalSettings.reasoning, resolvedRequest);
|
|
113
117
|
if (reasoningValidation) {
|
|
@@ -175,7 +179,41 @@ class LLMService {
|
|
|
175
179
|
};
|
|
176
180
|
}
|
|
177
181
|
this.logger.info(`Making LLM request with ${clientAdapter.constructor.name} for provider: ${providerId}`);
|
|
178
|
-
|
|
182
|
+
// Unified retry layer: adapters never throw, so retry decisions are made on
|
|
183
|
+
// RETURNED failure responses. SDK-internal retries are disabled (maxRetries: 0
|
|
184
|
+
// 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
|
+
const retryOnTimeout = this.retryOptions?.retryOnTimeout ?? true;
|
|
192
|
+
const retryableCodes = new Set([
|
|
193
|
+
types_1.ADAPTER_ERROR_CODES.RATE_LIMIT_EXCEEDED,
|
|
194
|
+
types_1.ADAPTER_ERROR_CODES.NETWORK_ERROR,
|
|
195
|
+
...(retryOnTimeout ? [types_1.ADAPTER_ERROR_CODES.REQUEST_TIMEOUT] : []),
|
|
196
|
+
]);
|
|
197
|
+
const result = await (0, withRetry_1.withRetry)(() => clientAdapter.sendMessage(internalRequest, apiKey, adapterOptions), (res) => {
|
|
198
|
+
if (res.object !== 'error') {
|
|
199
|
+
return { retry: false };
|
|
200
|
+
}
|
|
201
|
+
const code = String(res.error.code);
|
|
202
|
+
const status = res.error.status;
|
|
203
|
+
const retryable = retryableCodes.has(code) ||
|
|
204
|
+
(code === types_1.ADAPTER_ERROR_CODES.PROVIDER_ERROR &&
|
|
205
|
+
typeof status === 'number' &&
|
|
206
|
+
(status === 408 || status === 409 || status >= 500));
|
|
207
|
+
return { retry: retryable, retryAfterMs: res.error.retryAfterMs };
|
|
208
|
+
}, {
|
|
209
|
+
...this.retryOptions,
|
|
210
|
+
...(callOptions?.maxRetries !== undefined && {
|
|
211
|
+
maxRetries: callOptions.maxRetries,
|
|
212
|
+
}),
|
|
213
|
+
signal: callOptions?.signal,
|
|
214
|
+
logger: this.logger,
|
|
215
|
+
label: `${providerId}/${modelId}`,
|
|
216
|
+
});
|
|
179
217
|
// Post-process for thinking tag fallback
|
|
180
218
|
// This feature extracts reasoning from XML tags when native reasoning is not active.
|
|
181
219
|
// It's a fallback mechanism for models without native reasoning or when native is disabled.
|
|
@@ -375,7 +413,7 @@ class LLMService {
|
|
|
375
413
|
else {
|
|
376
414
|
const { providerId, modelId, modelInfo, settings } = resolved;
|
|
377
415
|
// Merge settings with model defaults
|
|
378
|
-
const mergedSettings = this.settingsManager.mergeSettingsForModel(modelId, providerId, settings || {});
|
|
416
|
+
const mergedSettings = this.settingsManager.mergeSettingsForModel(modelId, providerId, settings || {}, modelInfo);
|
|
379
417
|
// Calculate native reasoning status
|
|
380
418
|
const nativeReasoningActive = !!(modelInfo.reasoning?.supported &&
|
|
381
419
|
(mergedSettings.reasoning?.enabled === true ||
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { LLMResponse, LLMFailureResponse } from "../types";
|
|
2
|
-
import type { ILLMClientAdapter, InternalLLMChatRequest } from "./types";
|
|
2
|
+
import type { ILLMClientAdapter, InternalLLMChatRequest, AdapterRequestOptions } from "./types";
|
|
3
3
|
import type { Logger } from "../../logging/types";
|
|
4
4
|
/**
|
|
5
5
|
* Client adapter for Anthropic API integration
|
|
@@ -32,7 +32,7 @@ export declare class AnthropicClientAdapter implements ILLMClientAdapter {
|
|
|
32
32
|
* @param apiKey - The decrypted Anthropic API key
|
|
33
33
|
* @returns Promise resolving to success or failure response
|
|
34
34
|
*/
|
|
35
|
-
sendMessage(request: InternalLLMChatRequest, apiKey: string): Promise<LLMResponse | LLMFailureResponse>;
|
|
35
|
+
sendMessage(request: InternalLLMChatRequest, apiKey: string, options?: AdapterRequestOptions): Promise<LLMResponse | LLMFailureResponse>;
|
|
36
36
|
/**
|
|
37
37
|
* Validates Anthropic API key format
|
|
38
38
|
*
|
|
@@ -40,7 +40,7 @@ class AnthropicClientAdapter {
|
|
|
40
40
|
* @param apiKey - The decrypted Anthropic API key
|
|
41
41
|
* @returns Promise resolving to success or failure response
|
|
42
42
|
*/
|
|
43
|
-
async sendMessage(request, apiKey) {
|
|
43
|
+
async sendMessage(request, apiKey, options) {
|
|
44
44
|
try {
|
|
45
45
|
// Check if structured output is requested - need beta API
|
|
46
46
|
const useStructuredOutput = request.settings.structuredOutput?.schema &&
|
|
@@ -49,7 +49,13 @@ class AnthropicClientAdapter {
|
|
|
49
49
|
const anthropic = new sdk_1.default({
|
|
50
50
|
apiKey,
|
|
51
51
|
...(this.baseURL && { baseURL: this.baseURL }),
|
|
52
|
+
maxRetries: 0, // retries are owned by the unified LLMService retry layer
|
|
52
53
|
});
|
|
54
|
+
// Per-request transport options (abort signal, timeout)
|
|
55
|
+
const requestTransportOptions = {
|
|
56
|
+
...(options?.signal && { signal: options.signal }),
|
|
57
|
+
...(options?.timeoutMs !== undefined && { timeout: options.timeoutMs }),
|
|
58
|
+
};
|
|
53
59
|
// Format messages for Anthropic API (Claude has specific requirements)
|
|
54
60
|
const { messages, systemMessage } = this.formatMessagesForAnthropic(request);
|
|
55
61
|
// Prepare API call parameters
|
|
@@ -59,6 +65,9 @@ class AnthropicClientAdapter {
|
|
|
59
65
|
max_tokens: request.settings.maxTokens,
|
|
60
66
|
temperature: request.settings.temperature,
|
|
61
67
|
top_p: request.settings.topP,
|
|
68
|
+
...(request.settings.topK !== undefined && {
|
|
69
|
+
top_k: request.settings.topK,
|
|
70
|
+
}),
|
|
62
71
|
...(systemMessage && { system: systemMessage }),
|
|
63
72
|
...(request.settings.stopSequences.length > 0 && {
|
|
64
73
|
stop_sequences: request.settings.stopSequences,
|
|
@@ -135,10 +144,14 @@ class AnthropicClientAdapter {
|
|
|
135
144
|
headers: {
|
|
136
145
|
'anthropic-beta': 'structured-outputs-2025-11-13',
|
|
137
146
|
},
|
|
147
|
+
...requestTransportOptions,
|
|
138
148
|
});
|
|
139
149
|
}
|
|
140
150
|
else {
|
|
141
|
-
completion =
|
|
151
|
+
completion =
|
|
152
|
+
Object.keys(requestTransportOptions).length > 0
|
|
153
|
+
? await anthropic.messages.create(messageParams, requestTransportOptions)
|
|
154
|
+
: await anthropic.messages.create(messageParams);
|
|
142
155
|
}
|
|
143
156
|
this.logger.info(`Anthropic API call successful, response ID: ${completion.id}`);
|
|
144
157
|
// Convert to standardized response format
|
|
@@ -387,7 +400,7 @@ class AnthropicClientAdapter {
|
|
|
387
400
|
createErrorResponse(error, request) {
|
|
388
401
|
// Use shared error mapping utility for common error patterns
|
|
389
402
|
const initialProviderMessage = error instanceof sdk_1.default.APIError ? error.message : undefined;
|
|
390
|
-
let { errorCode, errorMessage, errorType, status } = (0, errorUtils_1.getCommonMappedErrorDetails)(error, initialProviderMessage);
|
|
403
|
+
let { errorCode, errorMessage, errorType, status, retryAfterMs } = (0, errorUtils_1.getCommonMappedErrorDetails)(error, initialProviderMessage);
|
|
391
404
|
// Apply Anthropic-specific refinements for 400 errors based on message content
|
|
392
405
|
if (error instanceof sdk_1.default.APIError && status === 400) {
|
|
393
406
|
if (error.message.toLowerCase().includes("context length") ||
|
|
@@ -409,6 +422,7 @@ class AnthropicClientAdapter {
|
|
|
409
422
|
code: errorCode,
|
|
410
423
|
type: errorType,
|
|
411
424
|
...(status && { status }),
|
|
425
|
+
...(retryAfterMs !== undefined && { retryAfterMs }),
|
|
412
426
|
providerError: error,
|
|
413
427
|
},
|
|
414
428
|
object: "error",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { LLMResponse, LLMFailureResponse } from "../types";
|
|
2
|
-
import type { ILLMClientAdapter, InternalLLMChatRequest } from "./types";
|
|
2
|
+
import type { ILLMClientAdapter, InternalLLMChatRequest, AdapterRequestOptions } from "./types";
|
|
3
3
|
import type { Logger } from "../../logging/types";
|
|
4
4
|
/**
|
|
5
5
|
* Client adapter for Google Gemini API integration
|
|
@@ -32,7 +32,7 @@ export declare class GeminiClientAdapter implements ILLMClientAdapter {
|
|
|
32
32
|
* @param apiKey - The decrypted Gemini API key
|
|
33
33
|
* @returns Promise resolving to success or failure response
|
|
34
34
|
*/
|
|
35
|
-
sendMessage(request: InternalLLMChatRequest, apiKey: string): Promise<LLMResponse | LLMFailureResponse>;
|
|
35
|
+
sendMessage(request: InternalLLMChatRequest, apiKey: string, options?: AdapterRequestOptions): Promise<LLMResponse | LLMFailureResponse>;
|
|
36
36
|
/**
|
|
37
37
|
* Validates Gemini API key format
|
|
38
38
|
*
|
|
@@ -37,9 +37,9 @@ class GeminiClientAdapter {
|
|
|
37
37
|
* @param apiKey - The decrypted Gemini API key
|
|
38
38
|
* @returns Promise resolving to success or failure response
|
|
39
39
|
*/
|
|
40
|
-
async sendMessage(request, apiKey) {
|
|
40
|
+
async sendMessage(request, apiKey, options) {
|
|
41
41
|
try {
|
|
42
|
-
// Initialize Gemini client
|
|
42
|
+
// Initialize Gemini client (note: @google/genai performs no automatic retries)
|
|
43
43
|
const genAI = new genai_1.GoogleGenAI({ apiKey });
|
|
44
44
|
// Format the request for Gemini API
|
|
45
45
|
const { contents, generationConfig, safetySettings, systemInstruction } = this.formatInternalRequestToGemini(request);
|
|
@@ -60,6 +60,10 @@ class GeminiClientAdapter {
|
|
|
60
60
|
...generationConfig,
|
|
61
61
|
safetySettings: safetySettings,
|
|
62
62
|
...(systemInstruction && { systemInstruction: systemInstruction }),
|
|
63
|
+
...(options?.signal && { abortSignal: options.signal }),
|
|
64
|
+
...(options?.timeoutMs !== undefined && {
|
|
65
|
+
httpOptions: { timeout: options.timeoutMs },
|
|
66
|
+
}),
|
|
63
67
|
},
|
|
64
68
|
});
|
|
65
69
|
this.logger.info(`Gemini API call successful, processing response`);
|
|
@@ -153,6 +157,12 @@ class GeminiClientAdapter {
|
|
|
153
157
|
maxOutputTokens: request.settings.maxTokens,
|
|
154
158
|
temperature: request.settings.temperature,
|
|
155
159
|
...(request.settings.topP && { topP: request.settings.topP }),
|
|
160
|
+
...(request.settings.topK !== undefined && {
|
|
161
|
+
topK: request.settings.topK,
|
|
162
|
+
}),
|
|
163
|
+
...(request.settings.seed !== undefined && {
|
|
164
|
+
seed: request.settings.seed,
|
|
165
|
+
}),
|
|
156
166
|
...(request.settings.stopSequences &&
|
|
157
167
|
request.settings.stopSequences.length > 0 && {
|
|
158
168
|
stopSequences: request.settings.stopSequences,
|
|
@@ -307,7 +317,7 @@ class GeminiClientAdapter {
|
|
|
307
317
|
createErrorResponse(error, request) {
|
|
308
318
|
// Use shared error mapping utility for common error patterns
|
|
309
319
|
const initialProviderMessage = error?.message;
|
|
310
|
-
let { errorCode, errorMessage, errorType, status } = (0, errorUtils_1.getCommonMappedErrorDetails)(error, initialProviderMessage);
|
|
320
|
+
let { errorCode, errorMessage, errorType, status, retryAfterMs } = (0, errorUtils_1.getCommonMappedErrorDetails)(error, initialProviderMessage);
|
|
311
321
|
// Apply Gemini-specific refinements for certain error types
|
|
312
322
|
if (error && error.message) {
|
|
313
323
|
const message = error.message.toLowerCase();
|
|
@@ -337,6 +347,7 @@ class GeminiClientAdapter {
|
|
|
337
347
|
code: errorCode,
|
|
338
348
|
type: errorType,
|
|
339
349
|
...(status && { status }),
|
|
350
|
+
...(retryAfterMs !== undefined && { retryAfterMs }),
|
|
340
351
|
providerError: error,
|
|
341
352
|
},
|
|
342
353
|
object: "error",
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import type { LLMResponse, LLMFailureResponse, ModelInfo } from "../types";
|
|
2
|
-
import type { ILLMClientAdapter, InternalLLMChatRequest } from "./types";
|
|
2
|
+
import type { ILLMClientAdapter, InternalLLMChatRequest, AdapterRequestOptions } from "./types";
|
|
3
3
|
import { LlamaCppServerClient } from "./LlamaCppServerClient";
|
|
4
4
|
import type { Logger } from "../../logging/types";
|
|
5
5
|
/**
|
|
6
6
|
* Configuration options for LlamaCppClientAdapter
|
|
7
7
|
*/
|
|
8
8
|
export interface LlamaCppClientConfig {
|
|
9
|
-
/** Base URL of the llama.cpp server (default: http://
|
|
9
|
+
/** Base URL of the llama.cpp server (default: http://127.0.0.1:8080) */
|
|
10
10
|
baseURL?: string;
|
|
11
11
|
/** Whether to check server health before sending requests (default: false) */
|
|
12
12
|
checkHealth?: boolean;
|
|
@@ -33,7 +33,7 @@ export interface LlamaCppClientConfig {
|
|
|
33
33
|
* ```typescript
|
|
34
34
|
* // Create adapter for local server
|
|
35
35
|
* const adapter = new LlamaCppClientAdapter({
|
|
36
|
-
* baseURL: 'http://
|
|
36
|
+
* baseURL: 'http://127.0.0.1:8080',
|
|
37
37
|
* checkHealth: true
|
|
38
38
|
* });
|
|
39
39
|
*
|
|
@@ -84,7 +84,7 @@ export declare class LlamaCppClientAdapter implements ILLMClientAdapter {
|
|
|
84
84
|
* @param apiKey - Not used for llama.cpp (local server), but kept for interface compatibility
|
|
85
85
|
* @returns Promise resolving to success or failure response
|
|
86
86
|
*/
|
|
87
|
-
sendMessage(request: InternalLLMChatRequest, apiKey: string): Promise<LLMResponse | LLMFailureResponse>;
|
|
87
|
+
sendMessage(request: InternalLLMChatRequest, apiKey: string, options?: AdapterRequestOptions): Promise<LLMResponse | LLMFailureResponse>;
|
|
88
88
|
/**
|
|
89
89
|
* Validates API key format
|
|
90
90
|
*
|
|
@@ -12,6 +12,8 @@ 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
18
|
/**
|
|
17
19
|
* Client adapter for llama.cpp server integration
|
|
@@ -33,7 +35,7 @@ const defaultLogger_1 = require("../../logging/defaultLogger");
|
|
|
33
35
|
* ```typescript
|
|
34
36
|
* // Create adapter for local server
|
|
35
37
|
* const adapter = new LlamaCppClientAdapter({
|
|
36
|
-
* baseURL: 'http://
|
|
38
|
+
* baseURL: 'http://127.0.0.1:8080',
|
|
37
39
|
* checkHealth: true
|
|
38
40
|
* });
|
|
39
41
|
*
|
|
@@ -57,7 +59,10 @@ class LlamaCppClientAdapter {
|
|
|
57
59
|
constructor(config) {
|
|
58
60
|
this.cachedModelCapabilities = null;
|
|
59
61
|
this.detectionAttempted = false;
|
|
60
|
-
|
|
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';
|
|
61
66
|
this.checkHealth = config?.checkHealth || false;
|
|
62
67
|
this.serverClient = new LlamaCppServerClient_1.LlamaCppServerClient(this.baseURL);
|
|
63
68
|
this.logger = config?.logger ?? (0, defaultLogger_1.createDefaultLogger)();
|
|
@@ -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) {
|
|
@@ -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,6 +202,56 @@ class LlamaCppClientAdapter {
|
|
|
182
202
|
schema: so.schema,
|
|
183
203
|
};
|
|
184
204
|
}
|
|
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
|
+
}
|
|
185
255
|
this.logger.debug(`llama.cpp API parameters:`, {
|
|
186
256
|
baseURL: this.baseURL,
|
|
187
257
|
model: completionParams.model,
|
|
@@ -191,11 +261,17 @@ class LlamaCppClientAdapter {
|
|
|
191
261
|
});
|
|
192
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
273
|
this.logger.info(`llama.cpp API call successful, response ID: ${completion.id}`);
|
|
198
|
-
return this.createSuccessResponse(completion, request);
|
|
274
|
+
return this.createSuccessResponse(completion, request, detectedCaps);
|
|
199
275
|
}
|
|
200
276
|
else {
|
|
201
277
|
throw new Error('Unexpected streaming response from llama.cpp server');
|
|
@@ -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,5 +1,5 @@
|
|
|
1
1
|
import type { LLMResponse, LLMFailureResponse } from "../types";
|
|
2
|
-
import type { ILLMClientAdapter, InternalLLMChatRequest } from "./types";
|
|
2
|
+
import type { ILLMClientAdapter, InternalLLMChatRequest, AdapterRequestOptions } from "./types";
|
|
3
3
|
import type { Logger } from "../../logging/types";
|
|
4
4
|
/**
|
|
5
5
|
* Configuration options for MistralClientAdapter
|
|
@@ -52,7 +52,7 @@ export declare class MistralClientAdapter implements ILLMClientAdapter {
|
|
|
52
52
|
* @param apiKey - The Mistral API key
|
|
53
53
|
* @returns Promise resolving to success or failure response
|
|
54
54
|
*/
|
|
55
|
-
sendMessage(request: InternalLLMChatRequest, apiKey: string): Promise<LLMResponse | LLMFailureResponse>;
|
|
55
|
+
sendMessage(request: InternalLLMChatRequest, apiKey: string, options?: AdapterRequestOptions): Promise<LLMResponse | LLMFailureResponse>;
|
|
56
56
|
/**
|
|
57
57
|
* Validates Mistral API key format
|
|
58
58
|
*
|
|
@@ -51,9 +51,10 @@ 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,
|
|
@@ -78,6 +79,9 @@ 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
|
|
@@ -87,8 +91,14 @@ class MistralClientAdapter {
|
|
|
87
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
103
|
this.logger.info(`Mistral API call successful, response ID: ${completion.id}`);
|
|
94
104
|
return this.createSuccessResponse(completion, request);
|
|
@@ -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
|
*/
|