@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.
package/README.md CHANGED
@@ -1,6 +1,10 @@
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
+
5
+ [![npm version](https://badge.fury.io/js/@pwshub%2Faisdk.svg)](https://badge.fury.io/js/@pwshub%2Faisdk)
6
+ ![CodeQL](https://github.com/pwshub/aisdk/workflows/CodeQL/badge.svg)
7
+ ![CI test](https://github.com/pwshub/aisdk/workflows/ci-test/badge.svg)
4
8
 
5
9
  ## Features
6
10
 
@@ -41,7 +45,7 @@ const ai = createAi()
41
45
 
42
46
  // Basic usage
43
47
  const result = await ai.ask({
44
- model: 'gpt-4o',
48
+ model: 'openai/gpt-4o',
45
49
  apikey: 'your-api-key-here',
46
50
  prompt: 'What is the capital of Vietnam?',
47
51
  temperature: 0.5,
@@ -69,11 +73,11 @@ Creates an AI client instance.
69
73
  Sends a text generation request.
70
74
 
71
75
  **Parameters:**
72
- - `model` (string, required): Model ID (must exist in models.json)
73
- - `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.
74
78
  - `prompt` (string, required): The user message
75
79
  - `system` (string, optional): Optional system prompt
76
- - `fallbacks` (string[], optional): Ordered list of fallback model IDs
80
+ - `fallbacks` (string[], optional): Ordered list of fallback models (same format as `model`)
77
81
  - `providerOptions` (object, optional): Provider-specific options
78
82
  - `temperature` (number, optional): Sampling temperature
79
83
  - `maxTokens` (number, optional): Maximum output tokens
@@ -111,7 +115,7 @@ import { createAi } from '@pwshub/aisdk'
111
115
  const ai = createAi()
112
116
 
113
117
  const result = await ai.ask({
114
- model: 'gpt-4o',
118
+ model: 'openai/gpt-4o',
115
119
  apikey: process.env.OPENAI_API_KEY,
116
120
  prompt: 'Explain quantum entanglement',
117
121
  temperature: 0.7,
@@ -123,7 +127,7 @@ const result = await ai.ask({
123
127
 
124
128
  ```javascript
125
129
  const result = await ai.ask({
126
- model: 'claude-sonnet-4-6',
130
+ model: 'anthropic/claude-sonnet-4-6',
127
131
  apikey: process.env.ANTHROPIC_API_KEY,
128
132
  prompt: 'Write a haiku about TypeScript',
129
133
  temperature: 0.5,
@@ -134,7 +138,7 @@ const result = await ai.ask({
134
138
 
135
139
  ```javascript
136
140
  const result = await ai.ask({
137
- model: 'gemini-2.5-flash',
141
+ model: 'google/gemini-2.5-flash',
138
142
  apikey: process.env.GOOGLE_API_KEY,
139
143
  prompt: 'What is 2+2?',
140
144
  providerOptions: {
@@ -151,7 +155,7 @@ Gemini 2.5 Pro and other reasoning models use thinking tokens by default. Disabl
151
155
 
152
156
  ```javascript
153
157
  const result = await ai.ask({
154
- model: 'gemini-2.5-pro',
158
+ model: 'google/gemini-2.5-pro',
155
159
  apikey: process.env.GOOGLE_API_KEY,
156
160
  prompt: 'What is the capital of Vietnam?',
157
161
  maxTokens: 256,
@@ -171,10 +175,10 @@ const result = await ai.ask({
171
175
  ```javascript
172
176
  try {
173
177
  const result = await ai.ask({
174
- model: 'gpt-4o',
178
+ model: 'openai/gpt-4o',
175
179
  apikey: process.env.OPENAI_API_KEY,
176
180
  prompt: 'Hello',
177
- fallbacks: ['gpt-4o-mini', 'claude-haiku-4-5'],
181
+ fallbacks: ['openai/gpt-4o-mini', 'anthropic/claude-haiku-4-5'],
178
182
  })
179
183
 
180
184
  if (result.model !== 'gpt-4o') {
@@ -193,7 +197,7 @@ try {
193
197
 
194
198
  ```javascript
195
199
  const result = await ai.ask({
196
- model: 'qwen3.5-plus',
200
+ model: 'dashscope/qwen3.5-plus',
197
201
  apikey: process.env.DASHSCOPE_API_KEY,
198
202
  prompt: 'Hello',
199
203
  })
@@ -223,7 +227,7 @@ const aiCN = createAi({
223
227
 
224
228
  // Use the regional client
225
229
  const result = await aiSingapore.ask({
226
- model: 'qwen3.5-plus',
230
+ model: 'dashscope/qwen3.5-plus',
227
231
  apikey: process.env.DASHSCOPE_API_KEY,
228
232
  prompt: 'Hello from Singapore!',
229
233
  })
@@ -233,56 +237,146 @@ const result = await aiSingapore.ask({
233
237
 
234
238
  ```javascript
235
239
  const result = await ai.ask({
236
- model: 'deepseek-chat',
240
+ model: 'deepseek/deepseek-chat',
237
241
  apikey: process.env.DEEPSEEK_API_KEY,
238
242
  prompt: 'Hello',
239
243
  })
240
244
  ```
241
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
+
242
270
  ## Supported Models
243
271
 
244
- This library does not ship with a predefined list of models. Instead, it accepts **any model** from the supported providers:
272
+ The library comes with just a few popular models configured in src/models.js
273
+
274
+ ## Model Management
245
275
 
246
- - **OpenAI**: Any OpenAI model
247
- - **Anthropic**: Any Anthropic model
248
- - **Google**: Any Google model
249
- - **DashScope**: Any DashScope model
250
- - **DeepSeek**: Any DeepSeek model
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).
251
277
 
252
- ### Loading Models
278
+ ### Adding Custom Models
253
279
 
254
- Models are loaded programmatically via `setModels()` from external sources (CMS, API, or local files for evaluation):
280
+ Use `addModels()` to add models to the existing registry. Only `name` and `provider` are required other fields get sensible defaults:
281
+
282
+ ```javascript
283
+ import { createAi, addModels, listModels } from '@pwshub/aisdk'
284
+
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
+ ])
291
+
292
+ // Add models with custom pricing
293
+ addModels([
294
+ {
295
+ name: 'my-custom-model',
296
+ provider: 'openai',
297
+ input_price: 0.5,
298
+ output_price: 1.5,
299
+ max_in: 128000,
300
+ max_out: 16384,
301
+ },
302
+ ])
303
+
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:
255
318
 
256
319
  ```javascript
257
320
  import { createAi, setModels } from '@pwshub/aisdk'
258
321
 
259
- // Load models from your CMS or API
322
+ // Fetch models from your CMS
260
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
+
261
331
  setModels(modelsFromCms)
262
332
 
263
333
  const ai = createAi()
264
- const result = await ai.ask({
265
- model: 'gemini-2.5-flash',
266
- apikey: 'your-api-key',
267
- prompt: 'Hello!',
334
+ ```
335
+
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',
268
363
  })
269
364
  ```
270
365
 
271
366
  ### Model Record Format
272
367
 
273
- Each model record should include:
274
- - `id`: Model identifier used in requests
275
- - `name`: Official model name (used in API calls)
276
- - `provider`: Provider ID (openai, anthropic, google, dashscope, deepseek)
277
- - `input_price`: Price per 1M input tokens (USD)
278
- - `output_price`: Price per 1M output tokens (USD)
279
- - `cache_price`: Price per 1M cached tokens (USD)
280
- - `max_in`: Maximum input tokens (context window)
281
- - `max_out`: Maximum output tokens
282
- - `enable`: Boolean to enable/disable the model
283
- - `supportedParams` (optional): Array of supported parameter names
284
-
285
- > **Note**: The `examples/` folder includes `models.json` as a reference for running evaluation scripts.
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 |
286
380
 
287
381
  ## Error Handling
288
382
 
@@ -293,7 +387,7 @@ const ai = createAi()
293
387
 
294
388
  try {
295
389
  const result = await ai.ask({
296
- model: 'gpt-4o',
390
+ model: 'openai/gpt-4o',
297
391
  apikey: process.env.OPENAI_API_KEY,
298
392
  prompt: 'Hello',
299
393
  })
@@ -329,6 +423,9 @@ DASHSCOPE_API_KEY=your-key npm run eval:dashscope
329
423
 
330
424
  # DeepSeek
331
425
  DEEPSEEK_API_KEY=your-key npm run eval:deepseek
426
+
427
+ # Mistral
428
+ MISTRAL_API_KEY=your-key npm run eval:mistral
332
429
  ```
333
430
 
334
431
  ## Development
@@ -357,4 +454,4 @@ npm run lint:fix
357
454
 
358
455
  ## License
359
456
 
360
- 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
 
@@ -70,5 +79,6 @@ export interface AiClient {
70
79
  }
71
80
 
72
81
  export function createAi(opts?: AiOptions): AiClient;
82
+ export function addModels(models: ModelRecord[]): void;
73
83
  export function setModels(models: ModelRecord[]): void;
74
84
  export function listModels(): ModelRecord[];
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@pwshub/aisdk",
3
- "version": "0.0.3",
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
  /**