@pwshub/aisdk 0.0.5 → 0.0.6

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/src/logger.js ADDED
@@ -0,0 +1,48 @@
1
+ /**
2
+ * @fileoverview Configurable logger for @pwshub/aisdk
3
+ *
4
+ * Provides a default console logger with the ability to set custom
5
+ * or no-op loggers for production environments.
6
+ */
7
+
8
+ /**
9
+ * @typedef {Object} Logger
10
+ * @property {(message: string) => void} warn
11
+ * @property {(message: string) => void} error
12
+ * @property {(message: string) => void} debug
13
+ */
14
+
15
+ const defaultLogger = {
16
+ warn: (message) => console.warn(message),
17
+ error: (message) => console.error(message),
18
+ debug: (message) => console.debug(message),
19
+ }
20
+
21
+ let currentLogger = defaultLogger
22
+
23
+ /**
24
+ * Sets a custom logger instance
25
+ * @param {Logger} logger
26
+ * @throws {Error} When logger doesn't implement required methods
27
+ */
28
+ export const setLogger = (logger) => {
29
+ if (!logger || typeof logger.warn !== 'function' || typeof logger.error !== 'function') {
30
+ throw new Error('Logger must implement warn() and error() methods')
31
+ }
32
+ currentLogger = logger
33
+ }
34
+
35
+ /**
36
+ * Returns the current logger instance
37
+ * @returns {Logger}
38
+ */
39
+ export const getLogger = () => currentLogger
40
+
41
+ /**
42
+ * No-op logger for silencing all output
43
+ */
44
+ export const noopLogger = {
45
+ warn: () => {},
46
+ error: () => {},
47
+ debug: () => {},
48
+ }
package/src/models.js CHANGED
@@ -198,6 +198,11 @@ export const DEFAULT_MODELS = [
198
198
  max_in: 400000,
199
199
  max_out: 128000,
200
200
  enable: true,
201
+ // gpt-5-nano only supports default values for temperature and top_p
202
+ paramOverrides: {
203
+ temperature: { fixedValue: 1 },
204
+ topP: { fixedValue: 1 },
205
+ },
201
206
  },
202
207
  {
203
208
  id: 'gpt-5.1',
package/src/providers.js CHANGED
@@ -42,7 +42,7 @@ const openai = {
42
42
  Authorization: `Bearer ${apikey}`,
43
43
  'Content-Type': 'application/json',
44
44
  }),
45
- url: () => 'https://api.openai.com/v1/chat/completions',
45
+ url: (modelName, apikey, gatewayUrl) => gatewayUrl || 'https://api.openai.com/v1/chat/completions',
46
46
  buildBody: (modelName, messages, config, providerOptions) => ({
47
47
  model: modelName,
48
48
  messages,
@@ -96,7 +96,7 @@ const anthropic = {
96
96
  'anthropic-version': '2023-06-01',
97
97
  'Content-Type': 'application/json',
98
98
  }),
99
- url: () => 'https://api.anthropic.com/v1/messages',
99
+ url: (modelName, apikey, gatewayUrl) => gatewayUrl || 'https://api.anthropic.com/v1/messages',
100
100
  buildBody: (modelName, messages, config, providerOptions) => {
101
101
  const system = messages.find((m) => m.role === 'system')?.content
102
102
  const filtered = messages.filter((m) => m.role !== 'system')
@@ -129,8 +129,12 @@ const anthropic = {
129
129
  /** @type {ProviderAdapter} */
130
130
  const google = {
131
131
  headers: () => ({ 'Content-Type': 'application/json' }),
132
- url: (modelName, apikey) =>
133
- `https://generativelanguage.googleapis.com/v1/models/${modelName}:generateContent?key=${apikey}`,
132
+ url: (modelName, apikey, gatewayUrl) => {
133
+ if (gatewayUrl) {
134
+ return gatewayUrl
135
+ }
136
+ return `https://generativelanguage.googleapis.com/v1/models/${modelName}:generateContent?key=${apikey}`
137
+ },
134
138
  buildBody: (modelName, messages, config, providerOptions) => {
135
139
  const system = messages.find((m) => m.role === 'system')?.content
136
140
  const contents = messages
@@ -243,7 +247,7 @@ const dashscope = {
243
247
  }),
244
248
  // International users should use dashscope-intl.aliyuncs.com
245
249
  // China users can use dashscope.aliyuncs.com
246
- url: () => 'https://dashscope-intl.aliyuncs.com/compatible-mode/v1/chat/completions',
250
+ url: (modelName, apikey, gatewayUrl) => gatewayUrl || 'https://dashscope-intl.aliyuncs.com/compatible-mode/v1/chat/completions',
247
251
  buildBody: (modelName, messages, config, providerOptions) => ({
248
252
  model: modelName,
249
253
  messages,
@@ -276,7 +280,7 @@ const deepseek = {
276
280
  Authorization: `Bearer ${apikey}`,
277
281
  'Content-Type': 'application/json',
278
282
  }),
279
- url: () => 'https://api.deepseek.com/chat/completions',
283
+ url: (modelName, apikey, gatewayUrl) => gatewayUrl || 'https://api.deepseek.com/chat/completions',
280
284
  buildBody: (modelName, messages, config, providerOptions) => ({
281
285
  model: modelName,
282
286
  messages,
@@ -305,7 +309,7 @@ const mistral = {
305
309
  'Content-Type': 'application/json',
306
310
  Accept: 'application/json',
307
311
  }),
308
- url: () => 'https://api.mistral.ai/v1/chat/completions',
312
+ url: (modelName, apikey, gatewayUrl) => gatewayUrl || 'https://api.mistral.ai/v1/chat/completions',
309
313
  buildBody: (modelName, messages, config, providerOptions) => ({
310
314
  model: modelName,
311
315
  messages,
@@ -333,8 +337,14 @@ const ollama = {
333
337
  'Content-Type': 'application/json',
334
338
  ...(apikey && { Authorization: `Bearer ${apikey}` }),
335
339
  }),
336
- // Default to localhost, but can be overridden via gatewayUrl
337
- url: () => 'http://localhost:11434/api/chat',
340
+ url: (modelName, apikey, gatewayUrl) => {
341
+ if (gatewayUrl) {
342
+ return gatewayUrl
343
+ }
344
+ // Warn about localhost default in development
345
+ console.warn('[ai-client] Ollama using default localhost URL (http://localhost:11434). Set gatewayUrl in createAi() for production.')
346
+ return 'http://localhost:11434/api/chat'
347
+ },
338
348
  buildBody: (modelName, messages, config, providerOptions) => {
339
349
  // Ollama uses snake_case options
340
350
  const options = {}
package/src/registry.js CHANGED
@@ -30,6 +30,7 @@ import { DEFAULT_MODELS } from './models.js'
30
30
  * @property {number} max_out - Max output tokens
31
31
  * @property {boolean} enable
32
32
  * @property {string[]} [supportedParams] - Canonical param names; falls back to provider default
33
+ * @property {Record<string, import('./coerce.js').ParamOverride>} [paramOverrides] - Model-specific param overrides
33
34
  */
34
35
 
35
36
  /**
@@ -51,11 +52,116 @@ export const PROVIDER_DEFAULT_PARAMS = {
51
52
  /** @type {ProviderId[]} */
52
53
  const VALID_PROVIDERS = ['openai', 'anthropic', 'google', 'dashscope', 'deepseek', 'mistral', 'ollama']
53
54
 
55
+ /**
56
+ * Creates a new registry instance with the given models
57
+ * @param {import('./models.js').ModelRecord[]} [initialModels] - Initial models to load (defaults to DEFAULT_MODELS)
58
+ * @returns {{
59
+ * getModel: (modelId: string) => { record: ModelRecord, supportedParams: string[], paramOverrides: Record<string, import('./coerce.js').ParamOverride> },
60
+ * listModels: () => ModelRecord[],
61
+ * addModels: (models: ModelRecord[]) => void,
62
+ * setModels: (models: ModelRecord[]) => void
63
+ * }}
64
+ */
65
+ export const createRegistry = (initialModels = DEFAULT_MODELS) => {
66
+ /** @type {Map<string, ModelRecord>} */
67
+ let REGISTRY = new Map(initialModels.map((model) => {
68
+ const normalized = normalizeModelRecord(model)
69
+ return [normalized.id, normalized]
70
+ }))
71
+
72
+ /**
73
+ * Looks up a model by provider/name format.
74
+ * @param {string} modelId - Model in 'provider/name' format
75
+ * @returns {{ record: ModelRecord, supportedParams: string[], paramOverrides: Record<string, import('./coerce.js').ParamOverride> }}
76
+ */
77
+ const getModel = (modelId) => {
78
+ if (!modelId.includes('/')) {
79
+ const available = [...REGISTRY.values()].map(m => `${m.provider}/${m.name}`).join(', ')
80
+ throw new Error(`Model must be in 'provider/name' format. Got: "${modelId}". Available: ${available}`)
81
+ }
82
+
83
+ const parts = modelId.split('/')
84
+ if (parts.length !== 2) {
85
+ const available = [...REGISTRY.values()].map(m => `${m.provider}/${m.name}`).join(', ')
86
+ throw new Error(`Model must be in 'provider/name' format. Got: "${modelId}". Available: ${available}`)
87
+ }
88
+
89
+ const [provider, name] = parts
90
+
91
+ for (const m of REGISTRY.values()) {
92
+ if (m.name === name && m.provider === provider) {
93
+ const record = m
94
+
95
+ if (!record.enable) {
96
+ throw new Error(`Model "${record.provider}/${record.name}" is currently disabled.`)
97
+ }
98
+
99
+ const supportedParams = record.supportedParams ?? PROVIDER_DEFAULT_PARAMS[record.provider]
100
+ const paramOverrides = record.paramOverrides ?? {}
101
+
102
+ return {
103
+ record, supportedParams, paramOverrides,
104
+ }
105
+ }
106
+ }
107
+
108
+ const available = [...REGISTRY.values()].map(m => `${m.provider}/${m.name}`).join(', ')
109
+ throw new Error(`Unknown model "${modelId}". Available: ${available}`)
110
+ }
111
+
112
+ /**
113
+ * Returns all enabled model records.
114
+ * @returns {ModelRecord[]}
115
+ */
116
+ const listModels = () =>
117
+ [...REGISTRY.values()].filter((m) => m.enable)
118
+
119
+ /**
120
+ * Adds one or more models to the registry.
121
+ * @param {ModelRecord[]} models - Array of model records to add
122
+ */
123
+ const addModels = (models) => {
124
+ if (!Array.isArray(models)) {
125
+ throw new Error(`addModels expects an array. Got: ${typeof models}`)
126
+ }
127
+
128
+ models.forEach((model, index) => {
129
+ validateModelRecord(model, index)
130
+ })
131
+
132
+ models.forEach((model) => {
133
+ const normalized = normalizeModelRecord(model)
134
+ REGISTRY.set(normalized.id, normalized)
135
+ })
136
+ }
137
+
138
+ /**
139
+ * Replaces the entire registry with a new list of models.
140
+ * @param {ModelRecord[]} models - Array of model records
141
+ */
142
+ const setModels = (models) => {
143
+ if (!Array.isArray(models)) {
144
+ throw new Error(`setModels expects an array. Got: ${typeof models}`)
145
+ }
146
+
147
+ models.forEach((model, index) => {
148
+ validateModelRecord(model, index)
149
+ })
150
+
151
+ REGISTRY = new Map(models.map((model) => {
152
+ const normalized = normalizeModelRecord(model)
153
+ return [normalized.id, normalized]
154
+ }))
155
+ }
156
+
157
+ return { getModel, listModels, addModels, setModels }
158
+ }
159
+
54
160
  /** @type {Map<string, ModelRecord>} */
55
161
  let REGISTRY = new Map()
56
162
 
57
163
  /**
58
- * Initializes the registry with default models.
164
+ * Initializes the global registry with default models.
59
165
  * Called automatically at module import.
60
166
  */
61
167
  const initRegistry = () => {
@@ -114,6 +220,50 @@ const validateModelRecord = (model, index) => {
114
220
  }
115
221
  }
116
222
 
223
+ // Check optional paramOverrides if present
224
+ if (model.paramOverrides !== undefined) {
225
+ if (typeof model.paramOverrides !== 'object' || model.paramOverrides === null || Array.isArray(model.paramOverrides)) {
226
+ errors.push('"paramOverrides" must be an object')
227
+ } else {
228
+ for (const [param, override] of Object.entries(model.paramOverrides)) {
229
+ if (typeof override !== 'object' || override === null) {
230
+ errors.push(`"paramOverrides.${param}" must be an object`)
231
+ } else {
232
+ // Check that override has at least one valid property
233
+ const validKeys = ['fixedValue', 'supportedValues', 'range']
234
+ const hasValidKey = Object.keys(override).some((k) => validKeys.includes(k))
235
+ if (!hasValidKey) {
236
+ errors.push(`"paramOverrides.${param}" must have at least one of: ${validKeys.join(', ')}`)
237
+ }
238
+ // Validate fixedValue type
239
+ if (override.fixedValue !== undefined && typeof override.fixedValue !== 'number') {
240
+ errors.push(`"paramOverrides.${param}.fixedValue" must be a number`)
241
+ }
242
+ // Validate supportedValues type
243
+ if (override.supportedValues !== undefined) {
244
+ if (!Array.isArray(override.supportedValues)) {
245
+ errors.push(`"paramOverrides.${param}.supportedValues" must be an array`)
246
+ } else {
247
+ override.supportedValues.forEach((v, i) => {
248
+ if (typeof v !== 'number') {
249
+ errors.push(`"paramOverrides.${param}.supportedValues[${i}]" must be a number`)
250
+ }
251
+ })
252
+ }
253
+ }
254
+ // Validate range type
255
+ if (override.range !== undefined) {
256
+ if (typeof override.range !== 'object' || override.range === null) {
257
+ errors.push(`"paramOverrides.${param}.range" must be an object`)
258
+ } else if (typeof override.range.min !== 'number' || typeof override.range.max !== 'number') {
259
+ errors.push(`"paramOverrides.${param}.range" must have numeric min and max`)
260
+ }
261
+ }
262
+ }
263
+ }
264
+ }
265
+ }
266
+
117
267
  if (errors.length > 0) {
118
268
  throw new Error(`Invalid model record at index ${index}: ${errors.join('; ')}`)
119
269
  }
@@ -138,55 +288,55 @@ const normalizeModelRecord = (model) => {
138
288
  max_out: model.max_out ?? 8000,
139
289
  enable: model.enable ?? true,
140
290
  ...(model.supportedParams !== undefined && { supportedParams: model.supportedParams }),
291
+ ...(model.paramOverrides !== undefined && { paramOverrides: model.paramOverrides }),
141
292
  }
142
293
  }
143
294
 
144
295
  /**
145
- * Looks up a model by provider/name format.
146
- * Validates it is enabled, and resolves its effective supported params.
296
+ * Gets a model from the global registry (for backward compatibility).
297
+ * For isolated registries, use the getModel from createRegistry() instead.
147
298
  *
148
- * @param {string} modelId - Model in 'provider/name' format (e.g., 'openai/gpt-4o', 'ollama/llama3.2')
149
- * @returns {{ record: ModelRecord, supportedParams: string[] }}
150
- * @throws {Error} When the model is not found or is disabled
299
+ * @param {string} modelId - Model in 'provider/name' format
300
+ * @returns {{ record: ModelRecord, supportedParams: string[], paramOverrides: Record<string, import('./coerce.js').ParamOverride> }}
151
301
  */
152
302
  export const getModel = (modelId) => {
153
- // Require provider/name format
154
303
  if (!modelId.includes('/')) {
155
304
  const available = [...REGISTRY.values()].map(m => `${m.provider}/${m.name}`).join(', ')
156
305
  throw new Error(`Model must be in 'provider/name' format. Got: "${modelId}". Available: ${available}`)
157
306
  }
158
-
307
+
159
308
  const parts = modelId.split('/')
160
309
  if (parts.length !== 2) {
161
310
  const available = [...REGISTRY.values()].map(m => `${m.provider}/${m.name}`).join(', ')
162
311
  throw new Error(`Model must be in 'provider/name' format. Got: "${modelId}". Available: ${available}`)
163
312
  }
164
-
313
+
165
314
  const [provider, name] = parts
166
-
167
- // Search for model by name and provider
315
+
168
316
  for (const m of REGISTRY.values()) {
169
317
  if (m.name === name && m.provider === provider) {
170
318
  const record = m
171
-
319
+
172
320
  if (!record.enable) {
173
321
  throw new Error(`Model "${record.provider}/${record.name}" is currently disabled.`)
174
322
  }
175
323
 
176
324
  const supportedParams = record.supportedParams ?? PROVIDER_DEFAULT_PARAMS[record.provider]
325
+ const paramOverrides = record.paramOverrides ?? {}
177
326
 
178
327
  return {
179
- record, supportedParams,
328
+ record, supportedParams, paramOverrides,
180
329
  }
181
330
  }
182
331
  }
183
-
332
+
184
333
  const available = [...REGISTRY.values()].map(m => `${m.provider}/${m.name}`).join(', ')
185
334
  throw new Error(`Unknown model "${modelId}". Available: ${available}`)
186
335
  }
187
336
 
188
337
  /**
189
- * Returns all enabled model records.
338
+ * Returns all enabled model records from the global registry.
339
+ * For isolated registries, use the listModels from createRegistry() instead.
190
340
  *
191
341
  * @returns {ModelRecord[]}
192
342
  */
@@ -194,23 +344,20 @@ export const listModels = () =>
194
344
  [...REGISTRY.values()].filter((m) => m.enable)
195
345
 
196
346
  /**
197
- * Adds one or more models to the registry.
198
- * Existing models with the same ID are overwritten.
347
+ * Adds one or more models to the global registry.
348
+ * For isolated registries, use the addModels from createRegistry() instead.
199
349
  *
200
350
  * @param {ModelRecord[]} models - Array of model records to add
201
- * @throws {Error} When models is not an array or contains invalid records
202
351
  */
203
352
  export const addModels = (models) => {
204
353
  if (!Array.isArray(models)) {
205
354
  throw new Error(`addModels expects an array. Got: ${typeof models}`)
206
355
  }
207
356
 
208
- // Validate and normalize each model record
209
357
  models.forEach((model, index) => {
210
358
  validateModelRecord(model, index)
211
359
  })
212
360
 
213
- // Add normalized models to the registry
214
361
  models.forEach((model) => {
215
362
  const normalized = normalizeModelRecord(model)
216
363
  REGISTRY.set(normalized.id, normalized)
@@ -218,18 +365,16 @@ export const addModels = (models) => {
218
365
  }
219
366
 
220
367
  /**
221
- * Replaces the entire model registry with a new list of models.
222
- * Use this to load models from a CMS or other external source.
368
+ * Replaces the entire global registry with a new list of models.
369
+ * For isolated registries, use the setModels from createRegistry() instead.
223
370
  *
224
371
  * @param {ModelRecord[]} models - Array of model records
225
- * @throws {Error} When models is not an array or contains invalid records
226
372
  */
227
373
  export const setModels = (models) => {
228
374
  if (!Array.isArray(models)) {
229
375
  throw new Error(`setModels expects an array. Got: ${typeof models}`)
230
376
  }
231
377
 
232
- // Validate and normalize each model record
233
378
  models.forEach((model, index) => {
234
379
  validateModelRecord(model, index)
235
380
  })
@@ -0,0 +1,114 @@
1
+ /**
2
+ * @fileoverview Security utilities for @pwshub/aisdk
3
+ *
4
+ * Provides API key validation, sanitization for logging,
5
+ * and other security-related functions.
6
+ */
7
+
8
+ import { InputError } from './errors.js'
9
+
10
+ /**
11
+ * Sensitive field patterns to redact from logs
12
+ * @type {string[]}
13
+ */
14
+ const SENSITIVE_KEYS = [
15
+ 'apikey',
16
+ 'api_key',
17
+ 'apiKey',
18
+ 'authorization',
19
+ 'key',
20
+ 'token',
21
+ 'secret',
22
+ 'password',
23
+ 'credential',
24
+ ]
25
+
26
+ /**
27
+ * Sanitizes an object for safe logging by removing/masking sensitive fields
28
+ * @param {unknown} obj - The object to sanitize
29
+ * @returns {unknown} Sanitized object safe for logging
30
+ */
31
+ export const sanitizeForLogging = (obj) => {
32
+ if (typeof obj !== 'object' || obj === null) {
33
+ return obj
34
+ }
35
+
36
+ if (Array.isArray(obj)) {
37
+ return obj.map(sanitizeForLogging)
38
+ }
39
+
40
+ const sanitized = {}
41
+
42
+ for (const [key, value] of Object.entries(obj)) {
43
+ const lowerKey = key.toLowerCase()
44
+ if (SENSITIVE_KEYS.some((s) => lowerKey.includes(s))) {
45
+ sanitized[key] = '[REDACTED]'
46
+ } else if (typeof value === 'object' && value !== null) {
47
+ sanitized[key] = sanitizeForLogging(value)
48
+ } else {
49
+ sanitized[key] = value
50
+ }
51
+ }
52
+
53
+ return sanitized
54
+ }
55
+
56
+ /**
57
+ * Provider-specific API key format patterns for validation warnings
58
+ * @type {Record<string, {pattern: RegExp, message: string}>}
59
+ */
60
+ const API_KEY_PATTERNS = {
61
+ openai: {
62
+ pattern: /^sk-[a-zA-Z0-9]{20,}/,
63
+ message: 'OpenAI API key format looks incorrect (should start with sk-)',
64
+ },
65
+ anthropic: {
66
+ pattern: /^sk-ant-[a-zA-Z0-9_-]{20,}/i,
67
+ message: 'Anthropic API key format looks incorrect (should start with sk-ant-)',
68
+ },
69
+ google: {
70
+ pattern: /^AIza[0-9A-Za-z_-]{35}/,
71
+ message: 'Google API key format looks incorrect (should start with AIza)',
72
+ },
73
+ deepseek: {
74
+ pattern: /^sk-[a-zA-Z0-9]{20,}/,
75
+ message: 'DeepSeek API key format looks incorrect (should start with sk-)',
76
+ },
77
+ mistral: {
78
+ pattern: /^[a-zA-Z0-9]{32,}/,
79
+ message: 'Mistral API key format looks incorrect',
80
+ },
81
+ dashscope: {
82
+ pattern: /^sk-[a-zA-Z0-9]{20,}/,
83
+ message: 'DashScope API key format looks incorrect (should start with sk-)',
84
+ },
85
+ ollama: {
86
+ // Ollama doesn't require API keys, so no validation
87
+ pattern: /.*/,
88
+ message: '',
89
+ },
90
+ }
91
+
92
+ /**
93
+ * Validates API key format per provider
94
+ * @param {string} apikey - The API key to validate
95
+ * @param {string} provider - The provider ID
96
+ * @param {import('./logger.js').Logger} logger - Logger instance for warnings
97
+ * @throws {InputError} When API key is missing or empty
98
+ */
99
+ export const validateApiKey = (apikey, provider, logger) => {
100
+ // Check if API key is provided and not empty
101
+ if (!apikey || typeof apikey !== 'string' || apikey.trim() === '') {
102
+ throw new InputError('API key is required', {
103
+ status: 401,
104
+ provider,
105
+ model: 'unknown',
106
+ })
107
+ }
108
+
109
+ // Provider-specific format warnings (not throwing, just warning)
110
+ const providerWarning = API_KEY_PATTERNS[provider]
111
+ if (providerWarning && providerWarning.message && !providerWarning.pattern.test(apikey)) {
112
+ logger.warn(`[ai-client] ${providerWarning.message}`)
113
+ }
114
+ }
package/src/validation.js CHANGED
@@ -48,8 +48,8 @@ export const validateAskOptions = (params) => {
48
48
  }
49
49
 
50
50
  // When using messages, system can still be provided (will be prepended)
51
- if (params.prompt !== undefined && typeof params.prompt !== 'string') {
52
- errors.push('"prompt" must be a string')
51
+ if (params.prompt !== undefined && (typeof params.prompt !== 'string' || params.prompt.trim() === '')) {
52
+ errors.push('"prompt" must be a non-empty string')
53
53
  }
54
54
 
55
55
  // Optional string fields
@@ -75,8 +75,8 @@ export const validateAskOptions = (params) => {
75
75
  errors.push(`messages[${i}] must be an object`)
76
76
  } else if (!['user', 'assistant', 'system'].includes(msg.role)) {
77
77
  errors.push(`messages[${i}].role must be 'user', 'assistant', or 'system'`)
78
- } else if (typeof msg.content !== 'string') {
79
- errors.push(`messages[${i}].content must be a string`)
78
+ } else if (typeof msg.content !== 'string' || msg.content.trim() === '') {
79
+ errors.push(`messages[${i}].content must be a non-empty string`)
80
80
  }
81
81
  })
82
82
  }
@@ -81,11 +81,12 @@ describe('validateAskOptions', () => {
81
81
  )
82
82
  })
83
83
 
84
- it('should pass with empty string prompt', () => {
85
- assert.doesNotThrow(
84
+ it('should throw when prompt is empty string', () => {
85
+ assert.throws(
86
86
  () => validateAskOptions({
87
87
  model: 'gpt-4o', apikey: 'test-key', prompt: '',
88
- })
88
+ }),
89
+ /"prompt" must be a non-empty string/
89
90
  )
90
91
  })
91
92
 
@@ -94,7 +95,7 @@ describe('validateAskOptions', () => {
94
95
  () => validateAskOptions({
95
96
  model: 'gpt-4o', apikey: 'test-key', prompt: 123,
96
97
  }),
97
- /"prompt" must be a string/
98
+ /"prompt" must be a non-empty string/
98
99
  )
99
100
  })
100
101
 
@@ -138,7 +139,7 @@ describe('validateAskOptions', () => {
138
139
  apikey: 'test-key',
139
140
  messages: [{ role: 'user' }],
140
141
  }),
141
- /messages\[0\]\.content must be a string/
142
+ /messages\[0\]\.content must be a non-empty string/
142
143
  )
143
144
  })
144
145
  })
@@ -280,7 +281,7 @@ describe('validateAskOptions', () => {
280
281
  role: 'user', content: 123,
281
282
  }],
282
283
  }),
283
- /messages\[0\].content must be a string/
284
+ /messages\[0\].content must be a non-empty string/
284
285
  )
285
286
  })
286
287
  })