@pwshub/aisdk 0.0.1
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/LICENSE +21 -0
- package/README.md +297 -0
- package/package.json +54 -0
- package/src/coerce.js +52 -0
- package/src/config.js +209 -0
- package/src/errors.js +106 -0
- package/src/index.js +269 -0
- package/src/providers.js +249 -0
- package/src/registry.js +164 -0
- package/src/validation.js +113 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Dong Nguyen
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
# @pwshub/aisdk
|
|
2
|
+
|
|
3
|
+
A thin, unified AI client for OpenAI, Anthropic, Google, DashScope, and DeepSeek with automatic parameter normalization and fallback support.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Unified API**: Single interface for multiple AI providers
|
|
8
|
+
- **Automatic parameter normalization**: Canonical camelCase params are translated to provider-specific wire format
|
|
9
|
+
- **Parameter clamping**: Values are automatically clamped to provider-accepted ranges
|
|
10
|
+
- **Fallback support**: Chain multiple models with automatic fallback on provider errors
|
|
11
|
+
- **Token usage tracking**: Detailed token counts and estimated cost per request
|
|
12
|
+
- **Provider-specific options**: Pass provider-specific parameters when needed
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npm i @pwshub/aisdk
|
|
18
|
+
# or
|
|
19
|
+
pnpm i @pwshub/aisdk
|
|
20
|
+
# or
|
|
21
|
+
bun add @pwshub/aisdk
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Quick Start
|
|
25
|
+
|
|
26
|
+
```javascript
|
|
27
|
+
import { createAi } from '@pwshub/aisdk'
|
|
28
|
+
|
|
29
|
+
const ai = createAi()
|
|
30
|
+
|
|
31
|
+
// Basic usage
|
|
32
|
+
const result = await ai.ask({
|
|
33
|
+
model: 'gpt-4o',
|
|
34
|
+
apikey: 'your-api-key-here',
|
|
35
|
+
prompt: 'What is the capital of Vietnam?',
|
|
36
|
+
temperature: 0.5,
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
console.log(result.text)
|
|
40
|
+
console.log(result.usage) // { inputTokens, outputTokens, cacheTokens, estimatedCost }
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## API
|
|
44
|
+
|
|
45
|
+
### `createAi(options?)`
|
|
46
|
+
|
|
47
|
+
Creates an AI client instance.
|
|
48
|
+
|
|
49
|
+
**Options:**
|
|
50
|
+
- `gatewayUrl` (optional): Override the default API endpoint URL
|
|
51
|
+
|
|
52
|
+
**Returns:** An object with:
|
|
53
|
+
- `ask(params)`: Send a generation request
|
|
54
|
+
- `listModels()`: Get all available models from the registry
|
|
55
|
+
|
|
56
|
+
### `ai.ask(params)`
|
|
57
|
+
|
|
58
|
+
Sends a text generation request.
|
|
59
|
+
|
|
60
|
+
**Parameters:**
|
|
61
|
+
- `model` (string, required): Model ID (must exist in models.json)
|
|
62
|
+
- `apikey` (string, required): API key for the provider
|
|
63
|
+
- `prompt` (string, required): The user message
|
|
64
|
+
- `system` (string, optional): Optional system prompt
|
|
65
|
+
- `fallbacks` (string[], optional): Ordered list of fallback model IDs
|
|
66
|
+
- `providerOptions` (object, optional): Provider-specific options
|
|
67
|
+
- `temperature` (number, optional): Sampling temperature
|
|
68
|
+
- `maxTokens` (number, optional): Maximum output tokens
|
|
69
|
+
- `topP` (number, optional): Nucleus sampling parameter
|
|
70
|
+
- `topK` (number, optional): Top-K sampling
|
|
71
|
+
- `frequencyPenalty` (number, optional): Frequency penalty
|
|
72
|
+
- `presencePenalty` (number, optional): Presence penalty
|
|
73
|
+
|
|
74
|
+
**Returns:** Promise resolving to:
|
|
75
|
+
```javascript
|
|
76
|
+
{
|
|
77
|
+
text: string, // Generated text
|
|
78
|
+
model: string, // Model that responded
|
|
79
|
+
usage: {
|
|
80
|
+
inputTokens: number,
|
|
81
|
+
outputTokens: number,
|
|
82
|
+
cacheTokens: number,
|
|
83
|
+
estimatedCost: number // USD
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
**Throws:**
|
|
89
|
+
- `ProviderError`: Transient provider errors (429, 5xx) — safe to retry
|
|
90
|
+
- `InputError`: Invalid input (400, 401, 403, 422) — fix input, do not retry
|
|
91
|
+
|
|
92
|
+
## Examples
|
|
93
|
+
|
|
94
|
+
### OpenAI
|
|
95
|
+
|
|
96
|
+
```javascript
|
|
97
|
+
import { createAi } from '@pwshub/aisdk'
|
|
98
|
+
|
|
99
|
+
const ai = createAi()
|
|
100
|
+
|
|
101
|
+
const result = await ai.ask({
|
|
102
|
+
model: 'gpt-4o',
|
|
103
|
+
apikey: process.env.OPENAI_API_KEY,
|
|
104
|
+
prompt: 'Explain quantum entanglement',
|
|
105
|
+
temperature: 0.7,
|
|
106
|
+
maxTokens: 500,
|
|
107
|
+
})
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### Anthropic
|
|
111
|
+
|
|
112
|
+
```javascript
|
|
113
|
+
const result = await ai.ask({
|
|
114
|
+
model: 'claude-sonnet-4-6',
|
|
115
|
+
apikey: process.env.ANTHROPIC_API_KEY,
|
|
116
|
+
prompt: 'Write a haiku about TypeScript',
|
|
117
|
+
temperature: 0.5,
|
|
118
|
+
})
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### Google
|
|
122
|
+
|
|
123
|
+
```javascript
|
|
124
|
+
const result = await ai.ask({
|
|
125
|
+
model: 'gemini-2.5-flash',
|
|
126
|
+
apikey: process.env.GOOGLE_API_KEY,
|
|
127
|
+
prompt: 'What is 2+2?',
|
|
128
|
+
providerOptions: {
|
|
129
|
+
safetySettings: [
|
|
130
|
+
{ category: 'HARM_CATEGORY_HARASSMENT', threshold: 'BLOCK_NONE' },
|
|
131
|
+
],
|
|
132
|
+
},
|
|
133
|
+
})
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
### With Fallbacks
|
|
137
|
+
|
|
138
|
+
```javascript
|
|
139
|
+
try {
|
|
140
|
+
const result = await ai.ask({
|
|
141
|
+
model: 'gpt-4o',
|
|
142
|
+
apikey: process.env.OPENAI_API_KEY,
|
|
143
|
+
prompt: 'Hello',
|
|
144
|
+
fallbacks: ['gpt-4o-mini', 'claude-haiku-4-5'],
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
if (result.model !== 'gpt-4o') {
|
|
148
|
+
console.warn(`Fell back to ${result.model}`)
|
|
149
|
+
}
|
|
150
|
+
} catch (error) {
|
|
151
|
+
if (error instanceof ProviderError) {
|
|
152
|
+
console.error('All models failed:', error.message)
|
|
153
|
+
} else if (error instanceof InputError) {
|
|
154
|
+
console.error('Invalid request:', error.message)
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
### DashScope (Alibaba)
|
|
160
|
+
|
|
161
|
+
```javascript
|
|
162
|
+
const result = await ai.ask({
|
|
163
|
+
model: 'qwen3.5-plus',
|
|
164
|
+
apikey: process.env.DASHSCOPE_API_KEY,
|
|
165
|
+
prompt: 'Hello',
|
|
166
|
+
})
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
### DeepSeek
|
|
170
|
+
|
|
171
|
+
```javascript
|
|
172
|
+
const result = await ai.ask({
|
|
173
|
+
model: 'deepseek-chat',
|
|
174
|
+
apikey: process.env.DEEPSEEK_API_KEY,
|
|
175
|
+
prompt: 'Hello',
|
|
176
|
+
})
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
## Supported Models
|
|
180
|
+
|
|
181
|
+
This library does not ship with a predefined list of models. Instead, it accepts **any model** from the supported providers:
|
|
182
|
+
|
|
183
|
+
- **OpenAI**: Any OpenAI model
|
|
184
|
+
- **Anthropic**: Any Anthropic model
|
|
185
|
+
- **Google**: Any Google model
|
|
186
|
+
- **DashScope**: Any DashScope model
|
|
187
|
+
- **DeepSeek**: Any DeepSeek model
|
|
188
|
+
|
|
189
|
+
### Loading Models
|
|
190
|
+
|
|
191
|
+
Models are loaded programmatically via `setModels()` from external sources (CMS, API, or local files for evaluation):
|
|
192
|
+
|
|
193
|
+
```javascript
|
|
194
|
+
import { createAi, setModels } from '@pwshub/aisdk'
|
|
195
|
+
|
|
196
|
+
// Load models from your CMS or API
|
|
197
|
+
const modelsFromCms = await fetch('https://cms.example.com/api/models').then(r => r.json())
|
|
198
|
+
setModels(modelsFromCms)
|
|
199
|
+
|
|
200
|
+
const ai = createAi()
|
|
201
|
+
const result = await ai.ask({
|
|
202
|
+
model: 'gemini-2.5-flash',
|
|
203
|
+
apikey: 'your-api-key',
|
|
204
|
+
prompt: 'Hello!',
|
|
205
|
+
})
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
### Model Record Format
|
|
209
|
+
|
|
210
|
+
Each model record should include:
|
|
211
|
+
- `id`: Model identifier used in requests
|
|
212
|
+
- `name`: Official model name (used in API calls)
|
|
213
|
+
- `provider`: Provider ID (openai, anthropic, google, dashscope, deepseek)
|
|
214
|
+
- `input_price`: Price per 1M input tokens (USD)
|
|
215
|
+
- `output_price`: Price per 1M output tokens (USD)
|
|
216
|
+
- `cache_price`: Price per 1M cached tokens (USD)
|
|
217
|
+
- `max_in`: Maximum input tokens (context window)
|
|
218
|
+
- `max_out`: Maximum output tokens
|
|
219
|
+
- `enable`: Boolean to enable/disable the model
|
|
220
|
+
- `supportedParams` (optional): Array of supported parameter names
|
|
221
|
+
|
|
222
|
+
> **Note**: The `examples/` folder includes `models.json` as a reference for running evaluation scripts.
|
|
223
|
+
|
|
224
|
+
## Error Handling
|
|
225
|
+
|
|
226
|
+
```javascript
|
|
227
|
+
import { createAi, ProviderError, InputError } from '@pwshub/aisdk'
|
|
228
|
+
|
|
229
|
+
const ai = createAi()
|
|
230
|
+
|
|
231
|
+
try {
|
|
232
|
+
const result = await ai.ask({
|
|
233
|
+
model: 'gpt-4o',
|
|
234
|
+
apikey: process.env.OPENAI_API_KEY,
|
|
235
|
+
prompt: 'Hello',
|
|
236
|
+
})
|
|
237
|
+
} catch (error) {
|
|
238
|
+
if (error instanceof ProviderError) {
|
|
239
|
+
// Provider-side error (rate limit, server error)
|
|
240
|
+
// Safe to retry or fallback to another model
|
|
241
|
+
console.error('Provider error:', error.status, error.message)
|
|
242
|
+
} else if (error instanceof InputError) {
|
|
243
|
+
// Client-side error (bad request, invalid API key)
|
|
244
|
+
// Do NOT retry — fix the input
|
|
245
|
+
console.error('Input error:', error.status, error.message)
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
## Running Evaluation Scripts
|
|
251
|
+
|
|
252
|
+
The package includes evaluation scripts to test each provider:
|
|
253
|
+
|
|
254
|
+
```bash
|
|
255
|
+
# OpenAI
|
|
256
|
+
OPENAI_API_KEY=your-key npm run eval:openai
|
|
257
|
+
|
|
258
|
+
# Anthropic
|
|
259
|
+
ANTHROPIC_API_KEY=your-key npm run eval:anthropic
|
|
260
|
+
|
|
261
|
+
# Google
|
|
262
|
+
GOOGLE_API_KEY=your-key npm run eval:google
|
|
263
|
+
|
|
264
|
+
# DashScope
|
|
265
|
+
DASHSCOPE_API_KEY=your-key npm run eval:dashscope
|
|
266
|
+
|
|
267
|
+
# DeepSeek
|
|
268
|
+
DEEPSEEK_API_KEY=your-key npm run eval:deepseek
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
## Development
|
|
272
|
+
|
|
273
|
+
```bash
|
|
274
|
+
# Install dependencies
|
|
275
|
+
npm install
|
|
276
|
+
|
|
277
|
+
# Run tests
|
|
278
|
+
npm test
|
|
279
|
+
|
|
280
|
+
# Run linter
|
|
281
|
+
npm run lint
|
|
282
|
+
|
|
283
|
+
# Auto-fix linting issues
|
|
284
|
+
npm run lint:fix
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
## AI Agents team
|
|
288
|
+
|
|
289
|
+
- Claude Code: initiator
|
|
290
|
+
- Qwen Code: implementer
|
|
291
|
+
- Google Gemini: reviewer
|
|
292
|
+
- DeepSeek: supporter
|
|
293
|
+
- Ollama: supporter
|
|
294
|
+
|
|
295
|
+
## License
|
|
296
|
+
|
|
297
|
+
MIT
|
package/package.json
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@pwshub/aisdk",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "A thin, unified AI client for OpenAI, Anthropic, Google, DashScope, and DeepSeek with automatic param normalization and fallback support",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "https://github.com/ndaidong/aisdk"
|
|
8
|
+
},
|
|
9
|
+
"engines": {
|
|
10
|
+
"node": ">=22.0.0",
|
|
11
|
+
"bun": ">=1.0.0"
|
|
12
|
+
},
|
|
13
|
+
"type": "module",
|
|
14
|
+
"main": "./src/index.js",
|
|
15
|
+
"exports": {
|
|
16
|
+
".": "./src/index.js"
|
|
17
|
+
},
|
|
18
|
+
"types": "./src/index.js",
|
|
19
|
+
"files": [
|
|
20
|
+
"src"
|
|
21
|
+
],
|
|
22
|
+
"scripts": {
|
|
23
|
+
"test": "node --test test/*.test.js",
|
|
24
|
+
"lint": "eslint src/ test/",
|
|
25
|
+
"lint:fix": "eslint src/ test/ --fix",
|
|
26
|
+
"eval:openai": "node examples/openai.js",
|
|
27
|
+
"eval:anthropic": "node examples/anthropic.js",
|
|
28
|
+
"eval:google": "node examples/google.js",
|
|
29
|
+
"eval:dashscope": "node examples/dashscope.js",
|
|
30
|
+
"eval:deepseek": "node examples/deepseek.js"
|
|
31
|
+
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"@eslint/js": "^10.0.1",
|
|
34
|
+
"eslint": "^10.0.3",
|
|
35
|
+
"globals": "^17.4.0"
|
|
36
|
+
},
|
|
37
|
+
"keywords": [
|
|
38
|
+
"ai",
|
|
39
|
+
"llm",
|
|
40
|
+
"openai",
|
|
41
|
+
"anthropic",
|
|
42
|
+
"google",
|
|
43
|
+
"gemini",
|
|
44
|
+
"claude",
|
|
45
|
+
"gpt",
|
|
46
|
+
"qwen",
|
|
47
|
+
"deepseek",
|
|
48
|
+
"chat",
|
|
49
|
+
"generation",
|
|
50
|
+
"sdk"
|
|
51
|
+
],
|
|
52
|
+
"author": "ndaidong",
|
|
53
|
+
"license": "MIT"
|
|
54
|
+
}
|
package/src/coerce.js
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Per-provider param clamping.
|
|
3
|
+
*
|
|
4
|
+
* Coerces generation config values to each provider's acceptable ranges.
|
|
5
|
+
* Instead of throwing errors, values are silently clamped to min/max bounds
|
|
6
|
+
* with a console.warn for visibility.
|
|
7
|
+
*
|
|
8
|
+
* Uses the merged WIRE_KEYS config from config.js for range information.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { getWireMap } from './config.js'
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @typedef {import('./registry.js').ProviderId} ProviderId
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const clamp = (value, min, max) => Math.min(Math.max(value, min), max)
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Coerce config values to provider's acceptable ranges.
|
|
21
|
+
* Logs warnings for clamped values.
|
|
22
|
+
*
|
|
23
|
+
* @param {Record<string, unknown>} config
|
|
24
|
+
* @param {ProviderId} providerId
|
|
25
|
+
* @returns {Record<string, unknown>}
|
|
26
|
+
*/
|
|
27
|
+
export const coerceConfig = (config, providerId) => {
|
|
28
|
+
const wireMap = getWireMap(providerId)
|
|
29
|
+
if (!wireMap) {
|
|
30
|
+
return config
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const result = { ...config }
|
|
34
|
+
|
|
35
|
+
for (const [key, value] of Object.entries(config)) {
|
|
36
|
+
const descriptor = wireMap[key]
|
|
37
|
+
const range = descriptor?.range
|
|
38
|
+
if (!range || typeof value !== 'number') {
|
|
39
|
+
continue
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const clamped = clamp(value, range.min, range.max)
|
|
43
|
+
if (clamped !== value) {
|
|
44
|
+
console.warn(
|
|
45
|
+
`[ai-client] "${key}" value ${value} out of range for ${providerId}, clamped to ${clamped}`
|
|
46
|
+
)
|
|
47
|
+
result[key] = clamped
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return result
|
|
52
|
+
}
|
package/src/config.js
ADDED
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Generation config normalizer.
|
|
3
|
+
*
|
|
4
|
+
* Validates canonical camelCase params against the model's supportedParams list
|
|
5
|
+
* (from registry), warns on unsupported ones, then translates to provider
|
|
6
|
+
* wire-format keys.
|
|
7
|
+
*
|
|
8
|
+
* Wire-key mappings are defined per-provider here. Google additionally needs
|
|
9
|
+
* some params nested under `generationConfig`.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* @typedef {import('./registry.js').ProviderId} ProviderId
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* @typedef {Object} GenerationConfig
|
|
18
|
+
* @property {number} [temperature]
|
|
19
|
+
* @property {number} [maxTokens]
|
|
20
|
+
* @property {number} [topP]
|
|
21
|
+
* @property {number} [topK]
|
|
22
|
+
* @property {number} [frequencyPenalty]
|
|
23
|
+
* @property {number} [presencePenalty]
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* @typedef {Object} ParamRange
|
|
28
|
+
* @property {number} min
|
|
29
|
+
* @property {number} max
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* @typedef {Object} WireDescriptor
|
|
34
|
+
* @property {string} wireKey
|
|
35
|
+
* @property {'root'|'generationConfig'} [scope] - Google nests some params
|
|
36
|
+
* @property {ParamRange} [range] - Valid range for the param (for clamping)
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Wire-key translation table per provider.
|
|
41
|
+
* Only lists params that exist for that provider.
|
|
42
|
+
* Includes range info for param clamping.
|
|
43
|
+
*
|
|
44
|
+
* @type {Record<ProviderId, Record<string, WireDescriptor>>}
|
|
45
|
+
*/
|
|
46
|
+
const WIRE_KEYS = {
|
|
47
|
+
openai: {
|
|
48
|
+
maxTokens: { wireKey: 'max_completion_tokens' },
|
|
49
|
+
temperature: {
|
|
50
|
+
wireKey: 'temperature', range: {
|
|
51
|
+
min: 0, max: 2,
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
topP: {
|
|
55
|
+
wireKey: 'top_p', range: {
|
|
56
|
+
min: 0, max: 1,
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
frequencyPenalty: {
|
|
60
|
+
wireKey: 'frequency_penalty', range: {
|
|
61
|
+
min: -2, max: 2,
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
presencePenalty: {
|
|
65
|
+
wireKey: 'presence_penalty', range: {
|
|
66
|
+
min: -2, max: 2,
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
// https://developers.openai.com/api/reference/resources/chat/subresources/completions/methods/create
|
|
70
|
+
},
|
|
71
|
+
anthropic: {
|
|
72
|
+
maxTokens: { wireKey: 'max_tokens' },
|
|
73
|
+
temperature: {
|
|
74
|
+
wireKey: 'temperature', range: {
|
|
75
|
+
min: 0, max: 1,
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
topP: {
|
|
79
|
+
wireKey: 'top_p', range: {
|
|
80
|
+
min: 0, max: 1,
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
topK: {
|
|
84
|
+
wireKey: 'top_k', range: {
|
|
85
|
+
min: 1, max: 100,
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
google: {
|
|
90
|
+
temperature: {
|
|
91
|
+
wireKey: 'temperature', scope: 'generationConfig', range: {
|
|
92
|
+
min: 0, max: 2,
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
maxTokens: {
|
|
96
|
+
wireKey: 'maxOutputTokens', scope: 'generationConfig',
|
|
97
|
+
},
|
|
98
|
+
topP: {
|
|
99
|
+
wireKey: 'topP', scope: 'generationConfig', range: {
|
|
100
|
+
min: 0, max: 1,
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
topK: {
|
|
104
|
+
wireKey: 'topK', scope: 'generationConfig', range: {
|
|
105
|
+
min: 1, max: 100,
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
dashscope: {
|
|
110
|
+
temperature: {
|
|
111
|
+
wireKey: 'temperature', range: {
|
|
112
|
+
min: 0, max: 2,
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
maxTokens: { wireKey: 'max_tokens' },
|
|
116
|
+
topP: {
|
|
117
|
+
wireKey: 'top_p', range: {
|
|
118
|
+
min: 0, max: 1,
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
topK: {
|
|
122
|
+
wireKey: 'top_k', range: {
|
|
123
|
+
min: 1, max: 100,
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
deepseek: {
|
|
128
|
+
temperature: {
|
|
129
|
+
wireKey: 'temperature', range: {
|
|
130
|
+
min: 0, max: 2,
|
|
131
|
+
},
|
|
132
|
+
},
|
|
133
|
+
maxTokens: { wireKey: 'max_tokens' },
|
|
134
|
+
topP: {
|
|
135
|
+
wireKey: 'top_p', range: {
|
|
136
|
+
min: 0, max: 1,
|
|
137
|
+
},
|
|
138
|
+
},
|
|
139
|
+
frequencyPenalty: {
|
|
140
|
+
wireKey: 'frequency_penalty', range: {
|
|
141
|
+
min: -2, max: 2,
|
|
142
|
+
},
|
|
143
|
+
},
|
|
144
|
+
presencePenalty: {
|
|
145
|
+
wireKey: 'presence_penalty', range: {
|
|
146
|
+
min: -2, max: 2,
|
|
147
|
+
},
|
|
148
|
+
},
|
|
149
|
+
},
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Normalizes a GenerationConfig to provider wire format, using the model's
|
|
154
|
+
* `supportedParams` list as the source of truth for what's allowed.
|
|
155
|
+
*
|
|
156
|
+
* - Params not in `supportedParams` → console.warn + drop
|
|
157
|
+
* - Params in `supportedParams` → translate to provider wire key
|
|
158
|
+
* - Google params with scope 'generationConfig' → nested automatically
|
|
159
|
+
*
|
|
160
|
+
* @param {GenerationConfig} config
|
|
161
|
+
* @param {ProviderId} providerId
|
|
162
|
+
* @param {string[]} supportedParams - From the model's registry entry
|
|
163
|
+
* @param {string} modelId - Used in warning messages
|
|
164
|
+
* @returns {Record<string, unknown>}
|
|
165
|
+
*/
|
|
166
|
+
export const normalizeConfig = (config, providerId, supportedParams, modelId) => {
|
|
167
|
+
const wireMap = WIRE_KEYS[providerId]
|
|
168
|
+
const supported = new Set(supportedParams)
|
|
169
|
+
const root = {}
|
|
170
|
+
const generationConfig = {}
|
|
171
|
+
|
|
172
|
+
for (const [canonicalKey, value] of Object.entries(config)) {
|
|
173
|
+
if (value === null || value === undefined) {
|
|
174
|
+
continue
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (!supported.has(canonicalKey)) {
|
|
178
|
+
console.warn(
|
|
179
|
+
`[ai-client] "${canonicalKey}" is not supported by model "${modelId}" — skipping.`
|
|
180
|
+
)
|
|
181
|
+
continue
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const descriptor = wireMap[canonicalKey]
|
|
185
|
+
if (!descriptor) {
|
|
186
|
+
continue
|
|
187
|
+
} // paranoia guard — should not happen if registry is correct
|
|
188
|
+
|
|
189
|
+
if (descriptor.scope === 'generationConfig') {
|
|
190
|
+
generationConfig[descriptor.wireKey] = value
|
|
191
|
+
} else {
|
|
192
|
+
root[descriptor.wireKey] = value
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (Object.keys(generationConfig).length > 0) {
|
|
197
|
+
root.generationConfig = generationConfig
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return root
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Returns the wire-key map for a provider.
|
|
205
|
+
* Used by coerce.js for param range validation.
|
|
206
|
+
* @param {ProviderId} providerId
|
|
207
|
+
* @returns {Record<string, WireDescriptor>}
|
|
208
|
+
*/
|
|
209
|
+
export const getWireMap = (providerId) => WIRE_KEYS[providerId]
|