@pwshub/aisdk 0.0.4 → 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.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @pwshub/aisdk
2
2
 
3
- A thin, unified AI client for OpenAI, Anthropic, Google, DashScope, and DeepSeek with automatic parameter normalization and fallback support.
3
+ A thin, unified AI client for OpenAI, Anthropic, Google, DashScope, DeepSeek, and Mistral with automatic parameter normalization and fallback support.
4
4
 
5
5
  [![npm version](https://badge.fury.io/js/@pwshub%2Faisdk.svg)](https://badge.fury.io/js/@pwshub%2Faisdk)
6
6
  ![CodeQL](https://github.com/pwshub/aisdk/workflows/CodeQL/badge.svg)
@@ -45,7 +45,7 @@ const ai = createAi()
45
45
 
46
46
  // Basic usage
47
47
  const result = await ai.ask({
48
- model: 'gpt-4o',
48
+ model: 'openai/gpt-4o',
49
49
  apikey: 'your-api-key-here',
50
50
  prompt: 'What is the capital of Vietnam?',
51
51
  temperature: 0.5,
@@ -73,11 +73,11 @@ Creates an AI client instance.
73
73
  Sends a text generation request.
74
74
 
75
75
  **Parameters:**
76
- - `model` (string, required): Model ID (must exist in models.json)
77
- - `apikey` (string, required): API key for the provider
76
+ - `model` (string, required): Use `provider/name` format (e.g., `anthropic/claude-sonnet-4-6`)
77
+ - `apikey` (string, required): API key for the provider. With ollama local, set to any string.
78
78
  - `prompt` (string, required): The user message
79
79
  - `system` (string, optional): Optional system prompt
80
- - `fallbacks` (string[], optional): Ordered list of fallback model IDs
80
+ - `fallbacks` (string[], optional): Ordered list of fallback models (same format as `model`)
81
81
  - `providerOptions` (object, optional): Provider-specific options
82
82
  - `temperature` (number, optional): Sampling temperature
83
83
  - `maxTokens` (number, optional): Maximum output tokens
@@ -115,7 +115,7 @@ import { createAi } from '@pwshub/aisdk'
115
115
  const ai = createAi()
116
116
 
117
117
  const result = await ai.ask({
118
- model: 'gpt-4o',
118
+ model: 'openai/gpt-4o',
119
119
  apikey: process.env.OPENAI_API_KEY,
120
120
  prompt: 'Explain quantum entanglement',
121
121
  temperature: 0.7,
@@ -127,7 +127,7 @@ const result = await ai.ask({
127
127
 
128
128
  ```javascript
129
129
  const result = await ai.ask({
130
- model: 'claude-sonnet-4-6',
130
+ model: 'anthropic/claude-sonnet-4-6',
131
131
  apikey: process.env.ANTHROPIC_API_KEY,
132
132
  prompt: 'Write a haiku about TypeScript',
133
133
  temperature: 0.5,
@@ -138,7 +138,7 @@ const result = await ai.ask({
138
138
 
139
139
  ```javascript
140
140
  const result = await ai.ask({
141
- model: 'gemini-2.5-flash',
141
+ model: 'google/gemini-2.5-flash',
142
142
  apikey: process.env.GOOGLE_API_KEY,
143
143
  prompt: 'What is 2+2?',
144
144
  providerOptions: {
@@ -155,7 +155,7 @@ Gemini 2.5 Pro and other reasoning models use thinking tokens by default. Disabl
155
155
 
156
156
  ```javascript
157
157
  const result = await ai.ask({
158
- model: 'gemini-2.5-pro',
158
+ model: 'google/gemini-2.5-pro',
159
159
  apikey: process.env.GOOGLE_API_KEY,
160
160
  prompt: 'What is the capital of Vietnam?',
161
161
  maxTokens: 256,
@@ -175,10 +175,10 @@ const result = await ai.ask({
175
175
  ```javascript
176
176
  try {
177
177
  const result = await ai.ask({
178
- model: 'gpt-4o',
178
+ model: 'openai/gpt-4o',
179
179
  apikey: process.env.OPENAI_API_KEY,
180
180
  prompt: 'Hello',
181
- fallbacks: ['gpt-4o-mini', 'claude-haiku-4-5'],
181
+ fallbacks: ['openai/gpt-4o-mini', 'anthropic/claude-haiku-4-5'],
182
182
  })
183
183
 
184
184
  if (result.model !== 'gpt-4o') {
@@ -197,7 +197,7 @@ try {
197
197
 
198
198
  ```javascript
199
199
  const result = await ai.ask({
200
- model: 'qwen3.5-plus',
200
+ model: 'dashscope/qwen3.5-plus',
201
201
  apikey: process.env.DASHSCOPE_API_KEY,
202
202
  prompt: 'Hello',
203
203
  })
@@ -227,7 +227,7 @@ const aiCN = createAi({
227
227
 
228
228
  // Use the regional client
229
229
  const result = await aiSingapore.ask({
230
- model: 'qwen3.5-plus',
230
+ model: 'dashscope/qwen3.5-plus',
231
231
  apikey: process.env.DASHSCOPE_API_KEY,
232
232
  prompt: 'Hello from Singapore!',
233
233
  })
@@ -237,74 +237,146 @@ const result = await aiSingapore.ask({
237
237
 
238
238
  ```javascript
239
239
  const result = await ai.ask({
240
- model: 'deepseek-chat',
240
+ model: 'deepseek/deepseek-chat',
241
241
  apikey: process.env.DEEPSEEK_API_KEY,
242
242
  prompt: 'Hello',
243
243
  })
244
244
  ```
245
245
 
246
+ ### Mistral
247
+
248
+ ```javascript
249
+ const result = await ai.ask({
250
+ model: 'mistral/mistral-large-latest',
251
+ apikey: process.env.MISTRAL_API_KEY,
252
+ prompt: 'Hello',
253
+ temperature: 0.7,
254
+ })
255
+ ```
256
+
257
+ ### Mistral with Random Seed
258
+
259
+ For reproducible results, use `randomSeed`:
260
+
261
+ ```javascript
262
+ const result = await ai.ask({
263
+ model: 'mistral/mistral-medium-latest',
264
+ apikey: process.env.MISTRAL_API_KEY,
265
+ prompt: 'Write a poem',
266
+ randomSeed: 42,
267
+ })
268
+ ```
269
+
246
270
  ## Supported Models
247
271
 
248
- The library comes with **30 pre-configured models** from all supported providers:
272
+ The library comes with just a few popular models configured in src/models.js
273
+
274
+ ## Model Management
249
275
 
250
- - **OpenAI**: gpt-4.1-nano, gpt-4.1-mini, gpt-4.1, gpt-4o, gpt-4o-mini, gpt-5, gpt-5-mini, gpt-5-nano, gpt-5.1, gpt-5.2, gpt-5.4, o3-mini, o4-mini
251
- - **Anthropic**: claude-haiku-4-5, claude-sonnet-4-6, claude-sonnet-4-5, claude-opus-4-6
252
- - **Google**: gemini-2.5-flash, gemini-2.5-flash-lite, gemini-2.5-pro, gemini-3.1-pro-preview, gemini-3.1-flash-lite-preview
253
- - **DashScope**: qwen-flash, qwen3.5-flash, qwen-plus, qwen3.5-plus, qwen-max, qwen3-max
254
- - **DeepSeek**: deepseek-chat, deepseek-reasoner
276
+ Models are automatically loaded from the built-in registry when the library is imported. You can add custom models or replace the entire list with your own (e.g., from a CMS).
255
277
 
256
- ### Managing Models
278
+ ### Adding Custom Models
257
279
 
258
- Models are managed via `addModels()` and `setModels()`:
280
+ Use `addModels()` to add models to the existing registry. Only `name` and `provider` are required — other fields get sensible defaults:
259
281
 
260
282
  ```javascript
261
- import { createAi, addModels, setModels, listModels } from '@pwshub/aisdk'
283
+ import { createAi, addModels, listModels } from '@pwshub/aisdk'
262
284
 
263
- // List all available models (30 models loaded by default)
264
- console.log(listModels())
285
+ // Add minimal model records (auto-generates ID and sets defaults)
286
+ addModels([
287
+ { name: 'llama3.2', provider: 'ollama' },
288
+ { name: 'mistral', provider: 'ollama' },
289
+ { name: 'gemma3', provider: 'ollama' },
290
+ ])
265
291
 
266
- // Add more models to the existing list
292
+ // Add models with custom pricing
267
293
  addModels([
268
294
  {
269
- id: 'my-custom-model',
270
295
  name: 'my-custom-model',
271
296
  provider: 'openai',
272
- input_price: 1,
273
- output_price: 2,
274
- cache_price: 0.5,
297
+ input_price: 0.5,
298
+ output_price: 1.5,
275
299
  max_in: 128000,
276
300
  max_out: 16384,
277
- enable: true,
278
301
  },
279
302
  ])
280
303
 
281
- // Replace all models with your own list (e.g., from CMS)
304
+ // View all available models
305
+ console.log(listModels())
306
+ ```
307
+
308
+ **Default values for missing fields:**
309
+ - `id`: Auto-generated as `${provider}_${name}` (e.g., `ollama_llama3.2`)
310
+ - `input_price`, `output_price`, `cache_price`: `0`
311
+ - `max_in`: `32000`
312
+ - `max_out`: `8000`
313
+ - `enable`: `true`
314
+
315
+ ### Loading Models from CMS
316
+
317
+ Use `setModels()` to replace the entire registry with models from your CMS:
318
+
319
+ ```javascript
320
+ import { createAi, setModels } from '@pwshub/aisdk'
321
+
322
+ // Fetch models from your CMS
282
323
  const modelsFromCms = await fetch('https://cms.example.com/api/models').then(r => r.json())
324
+
325
+ // Expected format from CMS:
326
+ // [
327
+ // { id: 'uuid-123', name: 'llama3.2', provider: 'ollama', ... },
328
+ // { id: 'uuid-456', name: 'mistral', provider: 'ollama', ... }
329
+ // ]
330
+
283
331
  setModels(modelsFromCms)
284
332
 
285
333
  const ai = createAi()
286
- const result = await ai.ask({
287
- model: 'gemini-2.5-flash',
288
- apikey: 'your-api-key',
289
- prompt: 'Hello!',
290
- })
291
334
  ```
292
335
 
293
- > **Note:** Models are loaded automatically from `src/models.js` when the library is imported. You don't need to call `setModels()` unless you want to use a custom model list.
336
+ > **Note:** Model `id` can be any unique string (UUID, slug, etc.). The library uses it for internal tracking. When using models from CMS, you reference them by `provider/name` format (see below).
337
+
338
+ ### Using Models
339
+
340
+ Models MUST be referenced in `provider/name` format:
341
+
342
+ ```javascript
343
+ const ai = createAi()
344
+
345
+ // Correct: provider/name format
346
+ await ai.ask({
347
+ model: 'openai/gpt-4o',
348
+ apikey: process.env.OPENAI_API_KEY,
349
+ prompt: 'Hello',
350
+ })
351
+
352
+ // Correct: works for all providers
353
+ await ai.ask({
354
+ model: 'ollama/llama3.2',
355
+ apikey: '',
356
+ prompt: 'Hello',
357
+ })
358
+
359
+ await ai.ask({
360
+ model: 'anthropic/claude-sonnet-4-6',
361
+ apikey: process.env.ANTHROPIC_API_KEY,
362
+ prompt: 'Hello',
363
+ })
364
+ ```
294
365
 
295
366
  ### Model Record Format
296
367
 
297
- Each model record should include:
298
- - `id`: Model identifier used in requests
299
- - `name`: Official model name (used in API calls)
300
- - `provider`: Provider ID (openai, anthropic, google, dashscope, deepseek)
301
- - `input_price`: Price per 1M input tokens (USD)
302
- - `output_price`: Price per 1M output tokens (USD)
303
- - `cache_price`: Price per 1M cached tokens (USD)
304
- - `max_in`: Maximum input tokens (context window)
305
- - `max_out`: Maximum output tokens
306
- - `enable`: Boolean to enable/disable the model
307
- - `supportedParams` (optional): Array of supported parameter names
368
+ | Field | Required | Default | Description |
369
+ |-------|----------|---------|-------------|
370
+ | `name` | Yes | - | Model name used in API calls |
371
+ | `provider` | Yes | - | Provider ID (openai, anthropic, google, dashscope, deepseek, mistral, ollama) |
372
+ | `id` | No | `${provider}_${name}` | Unique identifier (auto-generated if not provided) |
373
+ | `input_price` | No | `0` | Price per 1M input tokens (USD) |
374
+ | `output_price` | No | `0` | Price per 1M output tokens (USD) |
375
+ | `cache_price` | No | `0` | Price per 1M cached tokens (USD) |
376
+ | `max_in` | No | `32000` | Maximum input tokens (context window) |
377
+ | `max_out` | No | `8000` | Maximum output tokens |
378
+ | `enable` | No | `true` | Enable/disable the model |
379
+ | `supportedParams` | No | Provider defaults | Array of supported parameter names |
308
380
 
309
381
  ## Error Handling
310
382
 
@@ -315,7 +387,7 @@ const ai = createAi()
315
387
 
316
388
  try {
317
389
  const result = await ai.ask({
318
- model: 'gpt-4o',
390
+ model: 'openai/gpt-4o',
319
391
  apikey: process.env.OPENAI_API_KEY,
320
392
  prompt: 'Hello',
321
393
  })
@@ -351,6 +423,9 @@ DASHSCOPE_API_KEY=your-key npm run eval:dashscope
351
423
 
352
424
  # DeepSeek
353
425
  DEEPSEEK_API_KEY=your-key npm run eval:deepseek
426
+
427
+ # Mistral
428
+ MISTRAL_API_KEY=your-key npm run eval:mistral
354
429
  ```
355
430
 
356
431
  ## Development
@@ -379,4 +454,4 @@ npm run lint:fix
379
454
 
380
455
  ## License
381
456
 
382
- MIT
457
+ The MIT License (MIT)
package/index.d.ts CHANGED
@@ -6,11 +6,17 @@ export interface AiOptions {
6
6
  gatewayUrl?: string;
7
7
  }
8
8
 
9
+ export interface Message {
10
+ role: 'user' | 'assistant' | 'system';
11
+ content: string;
12
+ }
13
+
9
14
  export interface AskParams {
10
15
  model: string;
11
16
  apikey: string;
12
- prompt: string;
17
+ prompt?: string;
13
18
  system?: string;
19
+ messages?: Message[];
14
20
  fallbacks?: string[];
15
21
  providerOptions?: Record<string, unknown>;
16
22
  temperature?: number;
@@ -19,6 +25,9 @@ export interface AskParams {
19
25
  topK?: number;
20
26
  frequencyPenalty?: number;
21
27
  presencePenalty?: number;
28
+ randomSeed?: number;
29
+ seed?: number;
30
+ numPredict?: number;
22
31
  }
23
32
 
24
33
  export interface Usage {
@@ -36,15 +45,15 @@ export interface AskResult {
36
45
  }
37
46
 
38
47
  export interface ModelRecord {
39
- id: string;
48
+ id?: string;
40
49
  name: string;
41
50
  provider: string;
42
- input_price: number;
43
- output_price: number;
44
- cache_price: number;
45
- max_in: number;
46
- max_out: number;
47
- enable: boolean;
51
+ input_price?: number;
52
+ output_price?: number;
53
+ cache_price?: number;
54
+ max_in?: number;
55
+ max_out?: number;
56
+ enable?: boolean;
48
57
  supportedParams?: string[];
49
58
  }
50
59
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@pwshub/aisdk",
3
- "version": "0.0.4",
4
- "description": "A thin, unified AI client for OpenAI, Anthropic, Google, DashScope, and DeepSeek with automatic param normalization and fallback support",
3
+ "version": "0.0.5",
4
+ "description": "A thin, unified AI client for OpenAI, Anthropic, Google, DashScope, DeepSeek, and Mistral with automatic param normalization and fallback support",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "https://github.com/pwshub/aisdk"
@@ -22,14 +22,15 @@
22
22
  "index.d.ts"
23
23
  ],
24
24
  "scripts": {
25
- "test": "node --test test/*.test.js",
26
- "lint": "eslint src/ test/",
27
- "lint:fix": "eslint src/ test/ --fix",
25
+ "test": "node --test src/*.test.js",
26
+ "lint": "eslint src/",
27
+ "lint:fix": "eslint src/ --fix",
28
28
  "eval:openai": "node examples/openai.js",
29
29
  "eval:anthropic": "node examples/anthropic.js",
30
30
  "eval:google": "node examples/google.js",
31
31
  "eval:dashscope": "node examples/dashscope.js",
32
- "eval:deepseek": "node examples/deepseek.js"
32
+ "eval:deepseek": "node examples/deepseek.js",
33
+ "eval:mistral": "node examples/mistral.js"
33
34
  },
34
35
  "devDependencies": {
35
36
  "@eslint/js": "^10.0.1",
@@ -47,6 +48,7 @@
47
48
  "gpt",
48
49
  "qwen",
49
50
  "deepseek",
51
+ "mistral",
50
52
  "chat",
51
53
  "generation",
52
54
  "sdk"
@@ -0,0 +1,142 @@
1
+ /**
2
+ * @fileoverview Tests for coerce module.
3
+ */
4
+
5
+ import {
6
+ describe, it,
7
+ } from 'node:test'
8
+ import assert from 'node:assert'
9
+ import { coerceConfig } from '../src/coerce.js'
10
+
11
+ describe('coerceConfig', () => {
12
+ describe('openai', () => {
13
+ it('should clamp temperature to valid range [0, 2]', () => {
14
+ const config = { temperature: 3 }
15
+ const result = coerceConfig(config, 'openai')
16
+ assert.strictEqual(result.temperature, 2)
17
+ })
18
+
19
+ it('should clamp temperature below range', () => {
20
+ const config = { temperature: -1 }
21
+ const result = coerceConfig(config, 'openai')
22
+ assert.strictEqual(result.temperature, 0)
23
+ })
24
+
25
+ it('should not clamp temperature within range', () => {
26
+ const config = { temperature: 1 }
27
+ const result = coerceConfig(config, 'openai')
28
+ assert.strictEqual(result.temperature, 1)
29
+ })
30
+
31
+ it('should clamp topP to valid range [0, 1]', () => {
32
+ const config = { topP: 1.5 }
33
+ const result = coerceConfig(config, 'openai')
34
+ assert.strictEqual(result.topP, 1)
35
+ })
36
+
37
+ it('should clamp frequencyPenalty to valid range [-2, 2]', () => {
38
+ const config = { frequencyPenalty: 3 }
39
+ const result = coerceConfig(config, 'openai')
40
+ assert.strictEqual(result.frequencyPenalty, 2)
41
+ })
42
+
43
+ it('should clamp presencePenalty to valid range [-2, 2]', () => {
44
+ const config = { presencePenalty: -3 }
45
+ const result = coerceConfig(config, 'openai')
46
+ assert.strictEqual(result.presencePenalty, -2)
47
+ })
48
+ })
49
+
50
+ describe('anthropic', () => {
51
+ it('should clamp temperature to valid range [0, 1]', () => {
52
+ const config = { temperature: 1.5 }
53
+ const result = coerceConfig(config, 'anthropic')
54
+ assert.strictEqual(result.temperature, 1)
55
+ })
56
+
57
+ it('should clamp topK to valid range [1, 100]', () => {
58
+ const config = { topK: 150 }
59
+ const result = coerceConfig(config, 'anthropic')
60
+ assert.strictEqual(result.topK, 100)
61
+ })
62
+
63
+ it('should clamp topK below range', () => {
64
+ const config = { topK: 0 }
65
+ const result = coerceConfig(config, 'anthropic')
66
+ assert.strictEqual(result.topK, 1)
67
+ })
68
+ })
69
+
70
+ describe('google', () => {
71
+ it('should clamp temperature to valid range [0, 2]', () => {
72
+ const config = { temperature: 3 }
73
+ const result = coerceConfig(config, 'google')
74
+ assert.strictEqual(result.temperature, 2)
75
+ })
76
+
77
+ it('should clamp topK to valid range [1, 100]', () => {
78
+ const config = { topK: 200 }
79
+ const result = coerceConfig(config, 'google')
80
+ assert.strictEqual(result.topK, 100)
81
+ })
82
+ })
83
+
84
+ describe('dashscope', () => {
85
+ it('should clamp temperature to valid range [0, 2]', () => {
86
+ const config = { temperature: 5 }
87
+ const result = coerceConfig(config, 'dashscope')
88
+ assert.strictEqual(result.temperature, 2)
89
+ })
90
+
91
+ it('should clamp topP to valid range [0, 1]', () => {
92
+ const config = { topP: 2 }
93
+ const result = coerceConfig(config, 'dashscope')
94
+ assert.strictEqual(result.topP, 1)
95
+ })
96
+ })
97
+
98
+ describe('deepseek', () => {
99
+ it('should clamp temperature to valid range [0, 2]', () => {
100
+ const config = { temperature: 3 }
101
+ const result = coerceConfig(config, 'deepseek')
102
+ assert.strictEqual(result.temperature, 2)
103
+ })
104
+
105
+ it('should clamp frequencyPenalty to valid range [-2, 2]', () => {
106
+ const config = { frequencyPenalty: 5 }
107
+ const result = coerceConfig(config, 'deepseek')
108
+ assert.strictEqual(result.frequencyPenalty, 2)
109
+ })
110
+ })
111
+
112
+ describe('edge cases', () => {
113
+ it('should return config unchanged for unknown provider', () => {
114
+ const config = { temperature: 100 }
115
+ const result = coerceConfig(config, 'unknown')
116
+ assert.strictEqual(result.temperature, 100)
117
+ })
118
+
119
+ it('should not clamp non-numeric values', () => {
120
+ const config = { temperature: 'hot' }
121
+ const result = coerceConfig(config, 'openai')
122
+ assert.strictEqual(result.temperature, 'hot')
123
+ })
124
+
125
+ it('should handle empty config', () => {
126
+ const result = coerceConfig({}, 'openai')
127
+ assert.deepStrictEqual(result, {})
128
+ })
129
+
130
+ it('should clamp multiple values at once', () => {
131
+ const config = {
132
+ temperature: 5,
133
+ topP: 2,
134
+ maxTokens: 100,
135
+ }
136
+ const result = coerceConfig(config, 'openai')
137
+ assert.strictEqual(result.temperature, 2)
138
+ assert.strictEqual(result.topP, 1)
139
+ assert.strictEqual(result.maxTokens, 100) // maxTokens has no range
140
+ })
141
+ })
142
+ })
package/src/config.js CHANGED
@@ -21,6 +21,7 @@
21
21
  * @property {number} [topK]
22
22
  * @property {number} [frequencyPenalty]
23
23
  * @property {number} [presencePenalty]
24
+ * @property {number} [randomSeed]
24
25
  */
25
26
 
26
27
  /**
@@ -147,6 +148,35 @@ const WIRE_KEYS = {
147
148
  },
148
149
  },
149
150
  },
151
+ mistral: {
152
+ temperature: {
153
+ wireKey: 'temperature', range: {
154
+ min: 0, max: 2,
155
+ },
156
+ },
157
+ maxTokens: { wireKey: 'max_tokens' },
158
+ topP: {
159
+ wireKey: 'top_p', range: {
160
+ min: 0, max: 1,
161
+ },
162
+ },
163
+ randomSeed: { wireKey: 'random_seed' },
164
+ },
165
+ ollama: {
166
+ temperature: {
167
+ wireKey: 'temperature', range: {
168
+ min: 0, max: 2,
169
+ },
170
+ },
171
+ maxTokens: { wireKey: 'num_predict' },
172
+ topP: {
173
+ wireKey: 'top_p', range: {
174
+ min: 0, max: 1,
175
+ },
176
+ },
177
+ topK: { wireKey: 'top_k' },
178
+ seed: { wireKey: 'seed' },
179
+ },
150
180
  }
151
181
 
152
182
  /**