@pwshub/aisdk 0.0.3 → 0.0.5

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.
@@ -0,0 +1,142 @@
1
+ /**
2
+ * @fileoverview Tests for config module.
3
+ */
4
+
5
+ import {
6
+ describe, it,
7
+ } from 'node:test'
8
+ import assert from 'node:assert'
9
+ import {
10
+ normalizeConfig, getWireMap,
11
+ } from '../src/config.js'
12
+
13
+ describe('config', () => {
14
+ describe('getWireMap', () => {
15
+ it('should return wire map for openai', () => {
16
+ const wireMap = getWireMap('openai')
17
+ assert.ok(wireMap)
18
+ assert.ok(wireMap.temperature)
19
+ assert.strictEqual(wireMap.temperature.wireKey, 'temperature')
20
+ })
21
+
22
+ it('should return wire map for anthropic', () => {
23
+ const wireMap = getWireMap('anthropic')
24
+ assert.ok(wireMap)
25
+ assert.ok(wireMap.temperature)
26
+ assert.strictEqual(wireMap.temperature.wireKey, 'temperature')
27
+ })
28
+
29
+ it('should return wire map for google', () => {
30
+ const wireMap = getWireMap('google')
31
+ assert.ok(wireMap)
32
+ assert.ok(wireMap.temperature)
33
+ assert.strictEqual(wireMap.temperature.wireKey, 'temperature')
34
+ assert.strictEqual(wireMap.temperature.scope, 'generationConfig')
35
+ })
36
+
37
+ it('should return wire map for dashscope', () => {
38
+ const wireMap = getWireMap('dashscope')
39
+ assert.ok(wireMap)
40
+ assert.ok(wireMap.temperature)
41
+ assert.strictEqual(wireMap.temperature.wireKey, 'temperature')
42
+ })
43
+
44
+ it('should return wire map for deepseek', () => {
45
+ const wireMap = getWireMap('deepseek')
46
+ assert.ok(wireMap)
47
+ assert.ok(wireMap.temperature)
48
+ assert.strictEqual(wireMap.temperature.wireKey, 'temperature')
49
+ })
50
+
51
+ it('should return undefined for unknown provider', () => {
52
+ const wireMap = getWireMap('unknown')
53
+ assert.strictEqual(wireMap, undefined)
54
+ })
55
+ })
56
+
57
+ describe('normalizeConfig', () => {
58
+ it('should normalize openai config', () => {
59
+ const config = {
60
+ temperature: 0.5,
61
+ maxTokens: 100,
62
+ topP: 0.9,
63
+ }
64
+ const supportedParams = ['temperature', 'maxTokens', 'topP']
65
+ const result = normalizeConfig(config, 'openai', supportedParams, 'gpt-4o')
66
+
67
+ assert.strictEqual(result.temperature, 0.5)
68
+ assert.strictEqual(result.max_completion_tokens, 100)
69
+ assert.strictEqual(result.top_p, 0.9)
70
+ })
71
+
72
+ it('should normalize anthropic config', () => {
73
+ const config = {
74
+ temperature: 0.5,
75
+ maxTokens: 100,
76
+ topK: 50,
77
+ }
78
+ const supportedParams = ['temperature', 'maxTokens', 'topK']
79
+ const result = normalizeConfig(config, 'anthropic', supportedParams, 'claude-sonnet')
80
+
81
+ assert.strictEqual(result.temperature, 0.5)
82
+ assert.strictEqual(result.max_tokens, 100)
83
+ assert.strictEqual(result.top_k, 50)
84
+ })
85
+
86
+ it('should normalize google config with generationConfig nesting', () => {
87
+ const config = {
88
+ temperature: 0.5,
89
+ maxTokens: 100,
90
+ topP: 0.9,
91
+ }
92
+ const supportedParams = ['temperature', 'maxTokens', 'topP']
93
+ const result = normalizeConfig(config, 'google', supportedParams, 'gemini-2.0-flash')
94
+
95
+ assert.strictEqual(result.temperature, undefined)
96
+ assert.strictEqual(result.maxTokens, undefined)
97
+ assert.strictEqual(result.topP, undefined)
98
+ assert.ok(result.generationConfig)
99
+ assert.strictEqual(result.generationConfig.temperature, 0.5)
100
+ assert.strictEqual(result.generationConfig.maxOutputTokens, 100)
101
+ assert.strictEqual(result.generationConfig.topP, 0.9)
102
+ })
103
+
104
+ it('should skip unsupported params', () => {
105
+ const config = {
106
+ temperature: 0.5,
107
+ topK: 50, // openai doesn't support topK
108
+ }
109
+ const supportedParams = ['temperature']
110
+ const result = normalizeConfig(config, 'openai', supportedParams, 'gpt-4o')
111
+
112
+ assert.strictEqual(result.temperature, 0.5)
113
+ assert.strictEqual(result.top_k, undefined)
114
+ })
115
+
116
+ it('should skip null/undefined values', () => {
117
+ const config = {
118
+ temperature: 0.5,
119
+ maxTokens: null,
120
+ topP: undefined,
121
+ }
122
+ const supportedParams = ['temperature', 'maxTokens', 'topP']
123
+ const result = normalizeConfig(config, 'openai', supportedParams, 'gpt-4o')
124
+
125
+ assert.strictEqual(result.temperature, 0.5)
126
+ assert.strictEqual(result.max_completion_tokens, undefined)
127
+ assert.strictEqual(result.top_p, undefined)
128
+ })
129
+
130
+ it('should return empty object when no config provided', () => {
131
+ const result = normalizeConfig({}, 'openai', ['temperature'], 'gpt-4o')
132
+ assert.deepStrictEqual(result, {})
133
+ })
134
+
135
+ it('should return empty object when no supported params match', () => {
136
+ const config = { unknownParam: 123 }
137
+ const supportedParams = ['temperature']
138
+ const result = normalizeConfig(config, 'openai', supportedParams, 'gpt-4o')
139
+ assert.deepStrictEqual(result, {})
140
+ })
141
+ })
142
+ })
package/src/errors.js CHANGED
@@ -35,7 +35,7 @@ export class ProviderError extends Error {
35
35
  * @param {object} meta
36
36
  * @param {number} meta.status - HTTP status code
37
37
  * @param {string} meta.provider - Provider ID
38
- * @param {string} meta.model - Model ID that was called
38
+ * @param {string} meta.model - Model name that was called
39
39
  * @param {string} [meta.raw] - Raw response body from provider
40
40
  */
41
41
  constructor(message, {
package/src/index.js CHANGED
@@ -6,7 +6,7 @@
6
6
  *
7
7
  * const ai = createAi()
8
8
  * const result = await ai.ask({
9
- * model: 'claude-sonnet-4-20250514',
9
+ * model: 'anthropic/claude-sonnet-4-20250514',
10
10
  * apikey: 'your-api-key',
11
11
  * prompt: 'What is the capital of Vietnam?',
12
12
  * temperature: 0.5,
@@ -16,18 +16,18 @@
16
16
  *
17
17
  * @example With fallbacks
18
18
  * const result = await ai.ask({
19
- * model: 'gpt-4o',
19
+ * model: 'openai/gpt-4o',
20
20
  * apikey: 'your-openai-key',
21
21
  * prompt: '...',
22
- * fallbacks: ['gpt-4o-mini', 'claude-haiku-4-5-20251001'],
22
+ * fallbacks: ['openai/gpt-4o-mini', 'anthropic/claude-haiku-4-5-20251001'],
23
23
  * })
24
- * if (result.model !== 'gpt-4o') {
24
+ * if (result.model !== 'openai/gpt-4o') {
25
25
  * console.warn('Fell back to', result.model)
26
26
  * }
27
27
  *
28
28
  * @example Google provider-specific options
29
29
  * const result = await ai.ask({
30
- * model: 'gemini-2.0-flash',
30
+ * model: 'google/gemini-2.0-flash',
31
31
  * apikey: 'your-google-key',
32
32
  * prompt: '...',
33
33
  * providerOptions: {
@@ -38,10 +38,21 @@
38
38
  * },
39
39
  * })
40
40
  *
41
+ * @example Using messages array for multi-turn conversations
42
+ * const result = await ai.ask({
43
+ * model: 'anthropic/claude-sonnet-4-20250514',
44
+ * apikey: 'your-api-key',
45
+ * messages: [
46
+ * { role: 'user', content: 'What is the capital of Vietnam?' },
47
+ * { role: 'assistant', content: 'The capital of Vietnam is Hanoi.' },
48
+ * { role: 'user', content: 'What is its population?' },
49
+ * ],
50
+ * })
51
+ *
41
52
  */
42
53
 
43
54
  import {
44
- getModel, listModels, setModels,
55
+ getModel, listModels, setModels, addModels,
45
56
  } from './registry.js'
46
57
  import { normalizeConfig } from './config.js'
47
58
  import { coerceConfig } from './coerce.js'
@@ -62,11 +73,12 @@ export {
62
73
 
63
74
  /**
64
75
  * @typedef {Object} AskParams
65
- * @property {string} model - Model ID (must be registered via setModels())
76
+ * @property {string} model - Model name or 'provider/name' format (e.g., 'gpt-4o', 'ollama/llama3.2')
66
77
  * @property {string} apikey - API key for the provider
67
- * @property {string} prompt - The user message
68
- * @property {string} [system] - Optional system prompt
69
- * @property {string[]} [fallbacks] - Ordered list of fallback model IDs
78
+ * @property {string} [prompt] - The user message (alternative to messages)
79
+ * @property {string} [system] - Optional system prompt (used with prompt)
80
+ * @property {import('./providers.js').Message[]} [messages] - Array of messages with role and content (alternative to prompt)
81
+ * @property {string[]} [fallbacks] - Ordered list of fallback models (same format as model)
70
82
  * @property {Record<string, unknown>} [providerOptions] - Provider-specific options merged into body
71
83
  * @property {number} [temperature]
72
84
  * @property {number} [maxTokens]
@@ -152,11 +164,11 @@ const callModel = async (modelId, params, gatewayUrl) => {
152
164
  const normalizedConfig = normalizeConfig(coerced, providerId, supportedParams, modelId)
153
165
 
154
166
  const {
155
- prompt, system, providerOptions = {},
167
+ prompt, system, messages, providerOptions = {},
156
168
  } = params
157
169
 
158
170
  /** @type {import('./providers.js').Message[]} */
159
- const messages = [
171
+ const messageList = messages ?? [
160
172
  ...(system ? [{
161
173
  role: 'system', content: system,
162
174
  }] : []),
@@ -166,7 +178,7 @@ const callModel = async (modelId, params, gatewayUrl) => {
166
178
  ]
167
179
 
168
180
  const url = gatewayUrl ?? adapter.url(modelName, apikey)
169
- const body = adapter.buildBody(modelName, messages, normalizedConfig, providerOptions)
181
+ const body = adapter.buildBody(modelName, messageList, normalizedConfig, providerOptions)
170
182
 
171
183
  let res
172
184
  try {
@@ -267,4 +279,4 @@ export const createAi = (opts = {}) => {
267
279
  }
268
280
  }
269
281
 
270
- export { setModels }
282
+ export { addModels, setModels, listModels }
package/src/models.js ADDED
@@ -0,0 +1,401 @@
1
+ /**
2
+ * @fileoverview Default model registry for @pwshub/aisdk.
3
+ *
4
+ * This module exports a default list of models that are loaded automatically
5
+ * when the library is imported. Users can modify this list via addModels()
6
+ * and setModels() from the main export.
7
+ */
8
+
9
+ /**
10
+ * @typedef {import('./registry.js').ModelRecord} ModelRecord
11
+ */
12
+
13
+ /** @type {ModelRecord[]} */
14
+ export const DEFAULT_MODELS = [
15
+ {
16
+ id: 'claude-haiku-4-5',
17
+ name: 'claude-haiku-4-5',
18
+ provider: 'anthropic',
19
+ input_price: 1,
20
+ output_price: 5,
21
+ cache_price: 0,
22
+ max_in: 200000,
23
+ max_out: 64000,
24
+ enable: true,
25
+ },
26
+ {
27
+ id: 'claude-sonnet-4-6',
28
+ name: 'claude-sonnet-4-6',
29
+ provider: 'anthropic',
30
+ input_price: 3,
31
+ output_price: 15,
32
+ cache_price: 0,
33
+ max_in: 200000,
34
+ max_out: 64000,
35
+ enable: true,
36
+ },
37
+ {
38
+ id: 'claude-sonnet-4-5',
39
+ name: 'claude-sonnet-4-5',
40
+ provider: 'anthropic',
41
+ input_price: 3,
42
+ output_price: 15,
43
+ cache_price: 0,
44
+ max_in: 200000,
45
+ max_out: 1000000,
46
+ enable: true,
47
+ },
48
+ {
49
+ id: 'claude-opus-4-6',
50
+ name: 'claude-opus-4-6',
51
+ provider: 'anthropic',
52
+ input_price: 5,
53
+ output_price: 25,
54
+ cache_price: 0,
55
+ max_in: 200000,
56
+ max_out: 128000,
57
+ enable: true,
58
+ },
59
+ {
60
+ id: 'gemini-2.5-flash',
61
+ name: 'gemini-2.5-flash',
62
+ provider: 'google',
63
+ input_price: 0.3,
64
+ output_price: 2.5,
65
+ cache_price: 0.03,
66
+ max_in: 1048576,
67
+ max_out: 65536,
68
+ enable: true,
69
+ },
70
+ {
71
+ id: 'gemini-2.5-flash-lite',
72
+ name: 'gemini-2.5-flash-lite',
73
+ provider: 'google',
74
+ input_price: 0.1,
75
+ output_price: 0.4,
76
+ cache_price: 0.01,
77
+ max_in: 1048576,
78
+ max_out: 65536,
79
+ enable: true,
80
+ },
81
+ {
82
+ id: 'gemini-2.5-pro',
83
+ name: 'gemini-2.5-pro',
84
+ provider: 'google',
85
+ input_price: 1.25,
86
+ output_price: 10,
87
+ cache_price: 0.125,
88
+ max_in: 1048576,
89
+ max_out: 65536,
90
+ enable: true,
91
+ },
92
+ {
93
+ id: 'gemini-3.1-pro-preview',
94
+ name: 'gemini-3.1-pro-preview',
95
+ provider: 'google',
96
+ input_price: 2,
97
+ output_price: 12,
98
+ cache_price: 0.2,
99
+ max_in: 1048576,
100
+ max_out: 65536,
101
+ enable: true,
102
+ },
103
+ {
104
+ id: 'gemini-3.1-flash-lite-preview',
105
+ name: 'gemini-3.1-flash-lite-preview',
106
+ provider: 'google',
107
+ input_price: 0.25,
108
+ output_price: 1.5,
109
+ cache_price: 0.025,
110
+ max_in: 1048576,
111
+ max_out: 65536,
112
+ enable: true,
113
+ },
114
+ {
115
+ id: 'gpt-4.1-nano',
116
+ name: 'gpt-4.1-nano',
117
+ provider: 'openai',
118
+ input_price: 0.1,
119
+ output_price: 0.4,
120
+ cache_price: 0.025,
121
+ max_in: 1047576,
122
+ max_out: 32768,
123
+ enable: true,
124
+ },
125
+ {
126
+ id: 'gpt-4.1-mini',
127
+ name: 'gpt-4.1-mini',
128
+ provider: 'openai',
129
+ input_price: 0.4,
130
+ output_price: 1.6,
131
+ cache_price: 0.1,
132
+ max_in: 1047576,
133
+ max_out: 32768,
134
+ enable: true,
135
+ },
136
+ {
137
+ id: 'gpt-4.1',
138
+ name: 'gpt-4.1',
139
+ provider: 'openai',
140
+ input_price: 2,
141
+ output_price: 8,
142
+ cache_price: 0.5,
143
+ max_in: 1047576,
144
+ max_out: 32768,
145
+ enable: true,
146
+ },
147
+ {
148
+ id: 'gpt-4o',
149
+ name: 'gpt-4o',
150
+ provider: 'openai',
151
+ input_price: 2.5,
152
+ output_price: 10,
153
+ cache_price: 1.25,
154
+ max_in: 128000,
155
+ max_out: 16384,
156
+ enable: true,
157
+ },
158
+ {
159
+ id: 'gpt-4o-mini',
160
+ name: 'gpt-4o-mini',
161
+ provider: 'openai',
162
+ input_price: 0.15,
163
+ output_price: 0.6,
164
+ cache_price: 0.075,
165
+ max_in: 128000,
166
+ max_out: 16384,
167
+ enable: true,
168
+ },
169
+ {
170
+ id: 'gpt-5',
171
+ name: 'gpt-5',
172
+ provider: 'openai',
173
+ input_price: 1.25,
174
+ output_price: 10,
175
+ cache_price: 0.125,
176
+ max_in: 400000,
177
+ max_out: 128000,
178
+ enable: true,
179
+ },
180
+ {
181
+ id: 'gpt-5-mini',
182
+ name: 'gpt-5-mini',
183
+ provider: 'openai',
184
+ input_price: 0.25,
185
+ output_price: 2,
186
+ cache_price: 0.025,
187
+ max_in: 400000,
188
+ max_out: 128000,
189
+ enable: true,
190
+ },
191
+ {
192
+ id: 'gpt-5-nano',
193
+ name: 'gpt-5-nano',
194
+ provider: 'openai',
195
+ input_price: 0.05,
196
+ output_price: 0.4,
197
+ cache_price: 0.005,
198
+ max_in: 400000,
199
+ max_out: 128000,
200
+ enable: true,
201
+ },
202
+ {
203
+ id: 'gpt-5.1',
204
+ name: 'gpt-5.1',
205
+ provider: 'openai',
206
+ input_price: 1.25,
207
+ output_price: 10,
208
+ cache_price: 0.125,
209
+ max_in: 400000,
210
+ max_out: 128000,
211
+ enable: true,
212
+ },
213
+ {
214
+ id: 'gpt-5.2',
215
+ name: 'gpt-5.2',
216
+ provider: 'openai',
217
+ input_price: 1.75,
218
+ output_price: 14,
219
+ cache_price: 0.175,
220
+ max_in: 400000,
221
+ max_out: 128000,
222
+ enable: true,
223
+ },
224
+ {
225
+ id: 'gpt-5.4',
226
+ name: 'gpt-5.4',
227
+ provider: 'openai',
228
+ input_price: 2.5,
229
+ output_price: 15,
230
+ cache_price: 0.25,
231
+ max_in: 1050000,
232
+ max_out: 128000,
233
+ enable: true,
234
+ },
235
+ {
236
+ id: 'o3-mini',
237
+ name: 'o3-mini',
238
+ provider: 'openai',
239
+ input_price: 1.1,
240
+ output_price: 4.4,
241
+ cache_price: 0.55,
242
+ max_in: 200000,
243
+ max_out: 100000,
244
+ enable: true,
245
+ },
246
+ {
247
+ id: 'o4-mini',
248
+ name: 'o4-mini',
249
+ provider: 'openai',
250
+ input_price: 1.1,
251
+ output_price: 4.4,
252
+ cache_price: 0.275,
253
+ max_in: 200000,
254
+ max_out: 100000,
255
+ enable: true,
256
+ },
257
+ {
258
+ id: 'deepseek-chat',
259
+ name: 'deepseek-chat',
260
+ provider: 'deepseek',
261
+ input_price: 0.28,
262
+ output_price: 0.42,
263
+ cache_price: 0.028,
264
+ max_in: 128000,
265
+ max_out: 8000,
266
+ enable: true,
267
+ },
268
+ {
269
+ id: 'deepseek-reasoner',
270
+ name: 'deepseek-reasoner',
271
+ provider: 'deepseek',
272
+ input_price: 0.28,
273
+ output_price: 0.42,
274
+ cache_price: 0.028,
275
+ max_in: 128000,
276
+ max_out: 64000,
277
+ enable: true,
278
+ },
279
+ {
280
+ id: 'qwen-flash',
281
+ name: 'qwen-flash',
282
+ provider: 'dashscope',
283
+ input_price: 0.05,
284
+ output_price: 0.4,
285
+ cache_price: 0,
286
+ max_in: 995904,
287
+ max_out: 32768,
288
+ enable: true,
289
+ },
290
+ {
291
+ id: 'qwen3.5-flash',
292
+ name: 'qwen3.5-flash',
293
+ provider: 'dashscope',
294
+ input_price: 0.1,
295
+ output_price: 0.4,
296
+ cache_price: 0,
297
+ max_in: 983616,
298
+ max_out: 65536,
299
+ enable: true,
300
+ },
301
+ {
302
+ id: 'qwen-plus',
303
+ name: 'qwen-plus',
304
+ provider: 'dashscope',
305
+ input_price: 0.4,
306
+ output_price: 1.2,
307
+ cache_price: 0,
308
+ max_in: 997952,
309
+ max_out: 32768,
310
+ enable: true,
311
+ },
312
+ {
313
+ id: 'qwen3.5-plus',
314
+ name: 'qwen3.5-plus',
315
+ provider: 'dashscope',
316
+ input_price: 0.4,
317
+ output_price: 2.4,
318
+ cache_price: 0,
319
+ max_in: 991808,
320
+ max_out: 65536,
321
+ enable: true,
322
+ },
323
+ {
324
+ id: 'qwen-max',
325
+ name: 'qwen-max',
326
+ provider: 'dashscope',
327
+ input_price: 1.6,
328
+ output_price: 6.4,
329
+ cache_price: 0,
330
+ max_in: 30720,
331
+ max_out: 8192,
332
+ enable: true,
333
+ },
334
+ {
335
+ id: 'qwen3-max',
336
+ name: 'qwen3-max',
337
+ provider: 'dashscope',
338
+ input_price: 1.2,
339
+ output_price: 6,
340
+ cache_price: 0,
341
+ max_in: 258048,
342
+ max_out: 65536,
343
+ enable: true,
344
+ },
345
+ // Mistral models
346
+ {
347
+ id: 'mistral-large-latest',
348
+ name: 'mistral-large-latest',
349
+ provider: 'mistral',
350
+ input_price: 0.5,
351
+ output_price: 1.5,
352
+ cache_price: 0,
353
+ max_in: 128000,
354
+ max_out: 128000,
355
+ enable: true,
356
+ },
357
+ {
358
+ id: 'mistral-medium-latest',
359
+ name: 'mistral-medium-latest',
360
+ provider: 'mistral',
361
+ input_price: 0.4,
362
+ output_price: 2,
363
+ cache_price: 0,
364
+ max_in: 64000,
365
+ max_out: 64000,
366
+ enable: true,
367
+ },
368
+ {
369
+ id: 'mistral-small-latest',
370
+ name: 'mistral-small-latest',
371
+ provider: 'mistral',
372
+ input_price: 0.15,
373
+ output_price: 0.6,
374
+ cache_price: 0,
375
+ max_in: 128000,
376
+ max_out: 128000,
377
+ enable: true,
378
+ },
379
+ {
380
+ id: 'magistral-medium-latest',
381
+ name: 'magistral-medium-latest',
382
+ provider: 'mistral',
383
+ input_price: 2,
384
+ output_price: 5,
385
+ cache_price: 0,
386
+ max_in: 64000,
387
+ max_out: 64000,
388
+ enable: true,
389
+ },
390
+ {
391
+ id: 'magistral-small-latest',
392
+ name: 'magistral-small-latest',
393
+ provider: 'mistral',
394
+ input_price: 0.5,
395
+ output_price: 1.5,
396
+ cache_price: 0,
397
+ max_in: 64000,
398
+ max_out: 64000,
399
+ enable: true,
400
+ },
401
+ ]