genai-lite 0.12.0 → 0.13.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.
@@ -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, LLMStreamEvent } from "./types";
3
+ import type { LLMChatRequest, LLMChatRequestWithPreset, LLMResponse, LLMFailureResponse, ProviderInfo, ModelInfo, ApiProviderId, LLMSettings, ModelContext, LLMMessage, LLMStreamEvent, LLMRequestCapabilityPreflight, LLMRequestCapabilityValidationResult, ModelCapabilitiesResult, StructuredOutputSupport } from "./types";
4
4
  import type { ModelPreset } from "../types/presets";
5
5
  import { type RetryPolicy } from "../shared/services/withRetry";
6
6
  export type { PresetMode };
@@ -101,6 +101,31 @@ export declare class LLMService {
101
101
  * @returns Promise resolving to array of model information
102
102
  */
103
103
  getModels(providerId: ApiProviderId): Promise<ModelInfo[]>;
104
+ /**
105
+ * Gets static capability metadata for a provider/model pair without retrieving
106
+ * API keys, calling provider adapters, or performing network I/O.
107
+ *
108
+ * Unknown or unregistered models use the same fallback model resolution policy
109
+ * as sendMessage(), but their optional capabilities are reported as unknown.
110
+ */
111
+ getModelCapabilities(providerId: ApiProviderId, modelId: string): Promise<ModelCapabilitiesResult | LLMFailureResponse>;
112
+ /**
113
+ * Convenience wrapper for checking structured-output support.
114
+ *
115
+ * Returns `unknown` when genai-lite has no explicit capability metadata. Callers
116
+ * should decide whether unknown is acceptable for their use case.
117
+ */
118
+ supportsStructuredOutput(providerId: ApiProviderId, modelId: string): Promise<StructuredOutputSupport | LLMFailureResponse>;
119
+ /**
120
+ * Preflights provider/model capability requirements for a request without
121
+ * retrieving API keys, calling provider adapters, or performing network I/O.
122
+ *
123
+ * This checks the effective settings after preset and model defaults are
124
+ * applied. Unknown/unspecified capabilities are reported as unknown and remain
125
+ * valid; explicitly unsupported capabilities return the same validation error
126
+ * shape as sendMessage() where possible.
127
+ */
128
+ validateRequestCapabilities(request: LLMRequestCapabilityPreflight): Promise<LLMRequestCapabilityValidationResult>;
104
129
  /**
105
130
  * Sends a chat message to an LLM provider
106
131
  *
@@ -116,6 +141,10 @@ export declare class LLMService {
116
141
  * are yielded as a single `error` event.
117
142
  */
118
143
  streamMessage(request: LLMChatRequest | LLMChatRequestWithPreset, callOptions?: StreamMessageOptions): AsyncGenerator<LLMStreamEvent>;
144
+ private resolveAndValidateCapabilities;
145
+ private buildModelCapabilities;
146
+ private getStructuredOutputSupport;
147
+ private getCapabilitySource;
119
148
  private prepareRequest;
120
149
  private postProcessResponse;
121
150
  /**
@@ -76,6 +76,93 @@ class LLMService {
76
76
  this.logger.debug(`Found ${models.length} models for provider: ${providerId}`);
77
77
  return [...models]; // Return a copy to prevent external modification
78
78
  }
79
+ /**
80
+ * Gets static capability metadata for a provider/model pair without retrieving
81
+ * API keys, calling provider adapters, or performing network I/O.
82
+ *
83
+ * Unknown or unregistered models use the same fallback model resolution policy
84
+ * as sendMessage(), but their optional capabilities are reported as unknown.
85
+ */
86
+ async getModelCapabilities(providerId, modelId) {
87
+ this.logger.debug(`LLMService.getModelCapabilities called for provider: ${providerId}, model: ${modelId}`);
88
+ const resolved = await this.modelResolver.resolve({ providerId, modelId }, { detectLocalCapabilities: false });
89
+ if (resolved.error) {
90
+ return resolved.error;
91
+ }
92
+ const resolvedProviderId = resolved.providerId;
93
+ const resolvedModelId = resolved.modelId;
94
+ const modelInfo = resolved.modelInfo;
95
+ const source = this.getCapabilitySource(resolvedProviderId, resolvedModelId);
96
+ return {
97
+ object: "model.capabilities",
98
+ provider: resolvedProviderId,
99
+ model: resolvedModelId,
100
+ modelInfo: { ...modelInfo },
101
+ structuredOutput: this.getStructuredOutputSupport(modelInfo, source),
102
+ };
103
+ }
104
+ /**
105
+ * Convenience wrapper for checking structured-output support.
106
+ *
107
+ * Returns `unknown` when genai-lite has no explicit capability metadata. Callers
108
+ * should decide whether unknown is acceptable for their use case.
109
+ */
110
+ async supportsStructuredOutput(providerId, modelId) {
111
+ const capabilities = await this.getModelCapabilities(providerId, modelId);
112
+ if (capabilities.object === "error") {
113
+ return capabilities;
114
+ }
115
+ return capabilities.structuredOutput;
116
+ }
117
+ /**
118
+ * Preflights provider/model capability requirements for a request without
119
+ * retrieving API keys, calling provider adapters, or performing network I/O.
120
+ *
121
+ * This checks the effective settings after preset and model defaults are
122
+ * applied. Unknown/unspecified capabilities are reported as unknown and remain
123
+ * valid; explicitly unsupported capabilities return the same validation error
124
+ * shape as sendMessage() where possible.
125
+ */
126
+ async validateRequestCapabilities(request) {
127
+ this.logger.debug(`LLMService.validateRequestCapabilities called with presetId: ${request.presetId}, provider: ${request.providerId}, model: ${request.modelId}`);
128
+ try {
129
+ const validation = await this.resolveAndValidateCapabilities(request, {
130
+ validateStructure: false,
131
+ detectLocalCapabilities: false,
132
+ });
133
+ if ("error" in validation) {
134
+ return {
135
+ ...validation.error,
136
+ valid: false,
137
+ ...(validation.capabilities && { capabilities: validation.capabilities }),
138
+ };
139
+ }
140
+ return {
141
+ object: "capability.validation",
142
+ valid: true,
143
+ provider: validation.context.providerId,
144
+ model: validation.context.modelId,
145
+ capabilities: validation.context.capabilities,
146
+ };
147
+ }
148
+ catch (error) {
149
+ this.logger.error("Error in LLMService.validateRequestCapabilities:", error);
150
+ return {
151
+ provider: request.providerId || request.presetId || 'unknown',
152
+ model: request.modelId || request.presetId || 'unknown',
153
+ error: {
154
+ message: error instanceof Error
155
+ ? error.message
156
+ : "Unexpected error during capability preflight",
157
+ code: "UNKNOWN_ERROR",
158
+ type: "server_error",
159
+ providerError: error,
160
+ },
161
+ object: "error",
162
+ valid: false,
163
+ };
164
+ }
165
+ }
79
166
  /**
80
167
  * Sends a chat message to an LLM provider
81
168
  *
@@ -248,44 +335,95 @@ class LLMService {
248
335
  };
249
336
  }
250
337
  }
251
- async prepareRequest(request, callOptions) {
252
- // Resolve model information from preset or direct IDs
253
- const resolved = await this.modelResolver.resolve(request);
338
+ async resolveAndValidateCapabilities(request, options) {
339
+ const resolved = await this.modelResolver.resolve(request, {
340
+ detectLocalCapabilities: options.detectLocalCapabilities,
341
+ });
254
342
  if (resolved.error) {
255
343
  return { error: resolved.error };
256
344
  }
257
- const { providerId, modelId, modelInfo, settings: resolvedSettings } = resolved;
258
- // Create a proper LLMChatRequest with resolved values
345
+ const providerId = resolved.providerId;
346
+ const modelId = resolved.modelId;
347
+ const modelInfo = resolved.modelInfo;
348
+ const source = this.getCapabilitySource(providerId, modelId);
349
+ const capabilities = this.buildModelCapabilities(modelInfo, source);
259
350
  const resolvedRequest = {
260
351
  ...request,
261
- providerId: providerId,
262
- modelId: modelId,
352
+ providerId,
353
+ modelId,
354
+ messages: "messages" in request && Array.isArray(request.messages)
355
+ ? request.messages
356
+ : [],
263
357
  };
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 };
358
+ if (options.validateStructure) {
359
+ const structureValidationResult = this.requestValidator.validateRequestStructure(resolvedRequest);
360
+ if (structureValidationResult) {
361
+ return { error: structureValidationResult, capabilities };
275
362
  }
276
363
  }
277
- // Apply model-specific defaults and merge with user settings
364
+ const combinedSettings = {
365
+ ...(resolved.settings || {}),
366
+ ...(request.settings || {}),
367
+ };
368
+ const settingsValidation = this.requestValidator.validateSettings(combinedSettings, providerId, modelId);
369
+ if (settingsValidation) {
370
+ return { error: settingsValidation, capabilities };
371
+ }
278
372
  const finalSettings = this.settingsManager.mergeSettingsForModel(modelId, providerId, combinedSettings, modelInfo);
279
- // Validate reasoning settings for model capabilities
280
373
  const reasoningValidation = this.requestValidator.validateReasoningSettings(modelInfo, finalSettings.reasoning, resolvedRequest);
281
374
  if (reasoningValidation) {
282
- return { error: reasoningValidation };
375
+ return { error: reasoningValidation, capabilities };
283
376
  }
284
- // Validate structured output settings for model capabilities
285
377
  const structuredOutputValidation = this.requestValidator.validateStructuredOutputSettings(modelInfo, finalSettings.structuredOutput, resolvedRequest);
286
378
  if (structuredOutputValidation) {
287
- return { error: structuredOutputValidation };
379
+ return { error: structuredOutputValidation, capabilities };
380
+ }
381
+ return {
382
+ context: {
383
+ providerId,
384
+ modelId,
385
+ modelInfo,
386
+ resolvedRequest,
387
+ finalSettings,
388
+ capabilities,
389
+ },
390
+ };
391
+ }
392
+ buildModelCapabilities(modelInfo, source) {
393
+ return {
394
+ structuredOutput: this.getStructuredOutputSupport(modelInfo, source),
395
+ };
396
+ }
397
+ getStructuredOutputSupport(modelInfo, source) {
398
+ if (modelInfo.structuredOutput === undefined) {
399
+ return {
400
+ status: "unknown",
401
+ source,
402
+ };
403
+ }
404
+ return {
405
+ status: modelInfo.structuredOutput.supported ? "supported" : "unsupported",
406
+ ...(modelInfo.structuredOutput.strictMode !== undefined && {
407
+ strictMode: modelInfo.structuredOutput.strictMode,
408
+ }),
409
+ ...(modelInfo.structuredOutput.notes && {
410
+ notes: modelInfo.structuredOutput.notes,
411
+ }),
412
+ source,
413
+ };
414
+ }
415
+ getCapabilitySource(providerId, modelId) {
416
+ return (0, config_1.getModelById)(modelId, providerId) ? "registry" : "fallback";
417
+ }
418
+ async prepareRequest(request, callOptions) {
419
+ const validation = await this.resolveAndValidateCapabilities(request, {
420
+ validateStructure: true,
421
+ detectLocalCapabilities: true,
422
+ });
423
+ if ("error" in validation) {
424
+ return { error: validation.error };
288
425
  }
426
+ const { providerId, modelId, modelInfo, resolvedRequest, finalSettings, } = validation.context;
289
427
  // Get provider info for parameter filtering
290
428
  const providerInfo = (0, config_1.getProviderById)(providerId);
291
429
  if (!providerInfo) {
@@ -355,9 +493,9 @@ class LLMService {
355
493
  };
356
494
  return {
357
495
  prepared: {
358
- providerId: providerId,
359
- modelId: modelId,
360
- modelInfo: modelInfo,
496
+ providerId,
497
+ modelId,
498
+ modelInfo,
361
499
  resolvedRequest,
362
500
  internalRequest,
363
501
  clientAdapter,
@@ -23,6 +23,17 @@ export interface ModelResolution {
23
23
  settings?: LLMSettings;
24
24
  error?: LLMFailureResponse;
25
25
  }
26
+ /**
27
+ * Options controlling model resolution side effects.
28
+ */
29
+ export interface ModelResolutionOptions {
30
+ /**
31
+ * Whether resolution may query local provider adapters for dynamic model
32
+ * capabilities. Defaults to true for sendMessage(), but public capability
33
+ * preflight uses false so it remains no-network/no-adapter.
34
+ */
35
+ detectLocalCapabilities?: boolean;
36
+ }
26
37
  /**
27
38
  * Resolves model information from presets or direct provider/model IDs
28
39
  */
@@ -37,7 +48,7 @@ export declare class ModelResolver {
37
48
  * @param options Options containing either presetId or providerId/modelId
38
49
  * @returns Resolved model info and settings or error response
39
50
  */
40
- resolve(options: ModelSelectionOptions): Promise<ModelResolution>;
51
+ resolve(options: ModelSelectionOptions, resolutionOptions?: ModelResolutionOptions): Promise<ModelResolution>;
41
52
  /**
42
53
  * Asks the llama.cpp adapter which GGUF model the server has loaded and returns
43
54
  * its detected capabilities (cached inside the adapter). Returns undefined when
@@ -18,7 +18,8 @@ class ModelResolver {
18
18
  * @param options Options containing either presetId or providerId/modelId
19
19
  * @returns Resolved model info and settings or error response
20
20
  */
21
- async resolve(options) {
21
+ async resolve(options, resolutionOptions = {}) {
22
+ const detectLocalCapabilities = resolutionOptions.detectLocalCapabilities !== false;
22
23
  // If presetId is provided, use it
23
24
  if (options.presetId) {
24
25
  const preset = this.presetManager.resolvePreset(options.presetId);
@@ -53,7 +54,7 @@ class ModelResolver {
53
54
  }
54
55
  // Overlay detected GGUF capabilities for llama.cpp presets (the server decides
55
56
  // which model is actually loaded, regardless of the preset's modelId)
56
- if (preset.providerId === 'llamacpp') {
57
+ if (preset.providerId === 'llamacpp' && detectLocalCapabilities) {
57
58
  const detected = await this.detectLlamaCppCapabilities();
58
59
  if (detected) {
59
60
  modelInfo = { ...modelInfo, ...detected };
@@ -108,7 +109,7 @@ class ModelResolver {
108
109
  // decides which model is actually loaded, so detected capabilities and vendor
109
110
  // default settings must overlay whatever the registry says.
110
111
  let detectedCapabilities;
111
- if (options.providerId === 'llamacpp') {
112
+ if (options.providerId === 'llamacpp' && detectLocalCapabilities) {
112
113
  detectedCapabilities = await this.detectLlamaCppCapabilities();
113
114
  }
114
115
  if (modelInfo) {
@@ -119,6 +119,38 @@ export interface ModelStructuredOutputCapabilities {
119
119
  /** Additional notes about the model's structured output support */
120
120
  notes?: string;
121
121
  }
122
+ /**
123
+ * Capability support status for provider/model features.
124
+ *
125
+ * `unknown` means genai-lite does not have explicit metadata for the feature.
126
+ * Callers should decide whether to allow, reject, or fall back for unknown
127
+ * capabilities.
128
+ */
129
+ export type CapabilityStatus = "supported" | "unsupported" | "unknown";
130
+ /**
131
+ * Source of a capability decision.
132
+ */
133
+ export type CapabilitySource = "registry" | "detected" | "fallback";
134
+ /**
135
+ * Structured-output support status for a provider/model pair.
136
+ */
137
+ export interface StructuredOutputSupport {
138
+ /** Whether structured output is known supported, known unsupported, or unknown */
139
+ status: CapabilityStatus;
140
+ /** Whether strict provider-side schema enforcement is supported, when known */
141
+ strictMode?: boolean;
142
+ /** Additional notes from model metadata */
143
+ notes?: string;
144
+ /** Where the capability decision came from */
145
+ source: CapabilitySource;
146
+ }
147
+ /**
148
+ * Capability summary for a provider/model pair.
149
+ */
150
+ export interface ModelCapabilities {
151
+ /** Structured-output support status */
152
+ structuredOutput: StructuredOutputSupport;
153
+ }
122
154
  /**
123
155
  * Message roles supported by LLM APIs
124
156
  */
@@ -427,6 +459,35 @@ export interface LLMChatRequestWithPreset extends Omit<LLMChatRequest, 'provider
427
459
  /** Preset ID (alternative to providerId/modelId) */
428
460
  presetId?: string;
429
461
  }
462
+ /**
463
+ * Request used by LLMService.validateRequestCapabilities().
464
+ *
465
+ * This intentionally does not require messages: callers often need to validate
466
+ * provider/model/settings compatibility during configuration, before the final
467
+ * prompt messages exist.
468
+ */
469
+ export interface LLMRequestCapabilityPreflight {
470
+ /** Provider ID (required if not using presetId) */
471
+ providerId?: ApiProviderId;
472
+ /** Model ID (required if not using presetId) */
473
+ modelId?: string;
474
+ /** Preset ID (alternative to providerId/modelId) */
475
+ presetId?: string;
476
+ /** Settings whose provider/model capability requirements should be checked */
477
+ settings?: LLMSettings;
478
+ }
479
+ /**
480
+ * Result of LLMService.getModelCapabilities().
481
+ */
482
+ export interface ModelCapabilitiesResult {
483
+ object: "model.capabilities";
484
+ provider: ApiProviderId;
485
+ model: string;
486
+ /** Resolved model metadata used to derive the capability summary */
487
+ modelInfo: ModelInfo;
488
+ /** Structured-output support status for this provider/model pair */
489
+ structuredOutput: StructuredOutputSupport;
490
+ }
430
491
  /**
431
492
  * Individual choice in an LLM response
432
493
  */
@@ -496,6 +557,30 @@ export interface LLMFailureResponse {
496
557
  /** The partial response that was generated before the error occurred (if available) */
497
558
  partialResponse?: Omit<LLMResponse, 'object'>;
498
559
  }
560
+ /**
561
+ * Successful result of LLMService.validateRequestCapabilities().
562
+ */
563
+ export interface LLMRequestCapabilityValidationSuccess {
564
+ object: "capability.validation";
565
+ valid: true;
566
+ provider: ApiProviderId;
567
+ model: string;
568
+ capabilities: ModelCapabilities;
569
+ }
570
+ /**
571
+ * Failure result of LLMService.validateRequestCapabilities().
572
+ *
573
+ * The base error envelope intentionally matches LLMFailureResponse so callers
574
+ * can use the same diagnostic handling as sendMessage().
575
+ */
576
+ export type LLMRequestCapabilityValidationFailure = LLMFailureResponse & {
577
+ valid: false;
578
+ capabilities?: ModelCapabilities;
579
+ };
580
+ /**
581
+ * Result of LLMService.validateRequestCapabilities().
582
+ */
583
+ export type LLMRequestCapabilityValidationResult = LLMRequestCapabilityValidationSuccess | LLMRequestCapabilityValidationFailure;
499
584
  /**
500
585
  * Streaming event emitted by LLMService.streamMessage().
501
586
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "genai-lite",
3
- "version": "0.12.0",
3
+ "version": "0.13.0",
4
4
  "description": "A lightweight, portable toolkit for interacting with various Generative AI APIs.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",