@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/src/registry.js
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Model registry — in-memory store for model records.
|
|
3
|
+
*
|
|
4
|
+
* Models are loaded programmatically via setModels() from external sources
|
|
5
|
+
* (CMS, API, or local files for evaluation). This module provides O(1) lookups
|
|
6
|
+
* at runtime via a Map indexed by model ID.
|
|
7
|
+
*
|
|
8
|
+
* `supportedParams` is optional per record. When absent, the provider's
|
|
9
|
+
* default param set is used.
|
|
10
|
+
*
|
|
11
|
+
* @typedef {'openai'|'anthropic'|'google'|'dashscope'|'deepseek'} ProviderId
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Mirrors the Directus collection schema exactly.
|
|
16
|
+
* `supportedParams` is optional — added later via Directus field.
|
|
17
|
+
*
|
|
18
|
+
* @typedef {Object} ModelRecord
|
|
19
|
+
* @property {string} id
|
|
20
|
+
* @property {string} name - Official model name used in API calls
|
|
21
|
+
* @property {ProviderId} provider
|
|
22
|
+
* @property {number} input_price - Per 1M tokens, USD
|
|
23
|
+
* @property {number} output_price - Per 1M tokens, USD
|
|
24
|
+
* @property {number} cache_price - Per 1M tokens, USD
|
|
25
|
+
* @property {number} max_in - Max input tokens (context window)
|
|
26
|
+
* @property {number} max_out - Max output tokens
|
|
27
|
+
* @property {boolean} enable
|
|
28
|
+
* @property {string[]} [supportedParams] - Canonical param names; falls back to provider default
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Default supported params per provider.
|
|
33
|
+
* Used as fallback when a model record has no `supportedParams` field.
|
|
34
|
+
*
|
|
35
|
+
* @type {Record<ProviderId, string[]>}
|
|
36
|
+
*/
|
|
37
|
+
export const PROVIDER_DEFAULT_PARAMS = {
|
|
38
|
+
openai: ['temperature', 'maxTokens', 'topP', 'frequencyPenalty', 'presencePenalty', 'seed', 'stop'],
|
|
39
|
+
anthropic: ['temperature', 'maxTokens', 'topP', 'topK', 'stop'],
|
|
40
|
+
google: ['temperature', 'maxTokens', 'topP', 'topK', 'seed', 'stop'],
|
|
41
|
+
dashscope: ['temperature', 'maxTokens', 'topP', 'topK', 'stop'],
|
|
42
|
+
deepseek: ['temperature', 'maxTokens', 'topP', 'frequencyPenalty', 'presencePenalty', 'stop'],
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** @type {ProviderId[]} */
|
|
46
|
+
const VALID_PROVIDERS = ['openai', 'anthropic', 'google', 'dashscope', 'deepseek']
|
|
47
|
+
|
|
48
|
+
/** @type {Map<string, ModelRecord>} */
|
|
49
|
+
let REGISTRY = new Map()
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Validates a single model record structure and types.
|
|
53
|
+
*
|
|
54
|
+
* @param {Object} model - The model record to validate
|
|
55
|
+
* @param {number} index - Index in the array for error messages
|
|
56
|
+
* @throws {Error} When validation fails
|
|
57
|
+
*/
|
|
58
|
+
const validateModelRecord = (model, index) => {
|
|
59
|
+
const errors = []
|
|
60
|
+
|
|
61
|
+
// Check required string fields
|
|
62
|
+
if (!model.id || typeof model.id !== 'string') {
|
|
63
|
+
errors.push('"id" must be a non-empty string')
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (!model.name || typeof model.name !== 'string') {
|
|
67
|
+
errors.push('"name" must be a non-empty string')
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Check provider is valid
|
|
71
|
+
if (!model.provider || typeof model.provider !== 'string') {
|
|
72
|
+
errors.push('"provider" must be a string')
|
|
73
|
+
} else if (!VALID_PROVIDERS.includes(model.provider)) {
|
|
74
|
+
errors.push(`"provider" must be one of: ${VALID_PROVIDERS.join(', ')}. Got: "${model.provider}"`)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Check required number fields (must be non-negative)
|
|
78
|
+
const numberFields = ['input_price', 'output_price', 'cache_price', 'max_in', 'max_out']
|
|
79
|
+
for (const field of numberFields) {
|
|
80
|
+
if (typeof model[field] !== 'number') {
|
|
81
|
+
errors.push(`"${field}" must be a number`)
|
|
82
|
+
} else if (model[field] < 0) {
|
|
83
|
+
errors.push(`"${field}" must be non-negative, got: ${model[field]}`)
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Check enable is boolean
|
|
88
|
+
if (typeof model.enable !== 'boolean') {
|
|
89
|
+
errors.push('"enable" must be a boolean')
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Check optional supportedParams if present
|
|
93
|
+
if (model.supportedParams !== undefined) {
|
|
94
|
+
if (!Array.isArray(model.supportedParams)) {
|
|
95
|
+
errors.push('"supportedParams" must be an array')
|
|
96
|
+
} else {
|
|
97
|
+
model.supportedParams.forEach((param, i) => {
|
|
98
|
+
if (typeof param !== 'string') {
|
|
99
|
+
errors.push(`"supportedParams[${i}]" must be a string`)
|
|
100
|
+
}
|
|
101
|
+
})
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (errors.length > 0) {
|
|
106
|
+
throw new Error(`Invalid model record at index ${index}: ${errors.join('; ')}`)
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Looks up a model by ID, validates it is enabled, and resolves its
|
|
112
|
+
* effective supported params (record-level override or provider default).
|
|
113
|
+
*
|
|
114
|
+
* @param {string} modelId
|
|
115
|
+
* @returns {{ record: ModelRecord, supportedParams: string[] }}
|
|
116
|
+
* @throws {Error} When the model is not found or is disabled
|
|
117
|
+
*/
|
|
118
|
+
export const getModel = (modelId) => {
|
|
119
|
+
const record = REGISTRY.get(modelId)
|
|
120
|
+
|
|
121
|
+
if (!record) {
|
|
122
|
+
const available = [...REGISTRY.keys()].join(', ')
|
|
123
|
+
throw new Error(`Unknown model "${modelId}". Available: ${available}`)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (!record.enable) {
|
|
127
|
+
throw new Error(`Model "${modelId}" is currently disabled.`)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const supportedParams = record.supportedParams ?? PROVIDER_DEFAULT_PARAMS[record.provider]
|
|
131
|
+
|
|
132
|
+
return {
|
|
133
|
+
record, supportedParams,
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Returns all enabled model records.
|
|
139
|
+
*
|
|
140
|
+
* @returns {ModelRecord[]}
|
|
141
|
+
*/
|
|
142
|
+
export const listModels = () =>
|
|
143
|
+
[...REGISTRY.values()].filter((m) => m.enable)
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Programmatically sets the model registry from an array of model records.
|
|
147
|
+
* Use this when loading models from a CMS or other external source instead of
|
|
148
|
+
* the built-in models.json file.
|
|
149
|
+
*
|
|
150
|
+
* @param {ModelRecord[]} models - Array of model records (same format as models.json)
|
|
151
|
+
* @throws {Error} When models is not an array or contains invalid records
|
|
152
|
+
*/
|
|
153
|
+
export const setModels = (models) => {
|
|
154
|
+
if (!Array.isArray(models)) {
|
|
155
|
+
throw new Error(`setModels expects an array. Got: ${typeof models}`)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Validate each model record strictly
|
|
159
|
+
models.forEach((model, index) => {
|
|
160
|
+
validateModelRecord(model, index)
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
REGISTRY = new Map(models.map((model) => [model.id, model]))
|
|
164
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Simple validation for AI client input.
|
|
3
|
+
*
|
|
4
|
+
* Uses loose validation — only checks structure and types, not ranges.
|
|
5
|
+
* Range validation is handled by coerce.js which clamps values silently.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @typedef {Object} AskParams
|
|
10
|
+
* @property {string} model
|
|
11
|
+
* @property {string} apikey
|
|
12
|
+
* @property {string} prompt
|
|
13
|
+
* @property {string} [system]
|
|
14
|
+
* @property {import('../index.js').Message[]} [messages]
|
|
15
|
+
* @property {number} [temperature]
|
|
16
|
+
* @property {number} [maxTokens]
|
|
17
|
+
* @property {number} [topP]
|
|
18
|
+
* @property {number} [topK]
|
|
19
|
+
* @property {number} [frequencyPenalty]
|
|
20
|
+
* @property {number} [presencePenalty]
|
|
21
|
+
* @property {number} [seed]
|
|
22
|
+
* @property {string|string[]} [stop]
|
|
23
|
+
* @property {string[]} [fallbacks]
|
|
24
|
+
* @property {Record<string, unknown>} [providerOptions]
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Validates ask() options structure and types.
|
|
29
|
+
* @param {AskParams} params
|
|
30
|
+
* @returns {void}
|
|
31
|
+
* @throws {Error}
|
|
32
|
+
*/
|
|
33
|
+
export const validateAskOptions = (params) => {
|
|
34
|
+
const errors = []
|
|
35
|
+
|
|
36
|
+
// Required fields
|
|
37
|
+
if (!params.model || typeof params.model !== 'string') {
|
|
38
|
+
errors.push('"model" must be a non-empty string')
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (!params.apikey || typeof params.apikey !== 'string') {
|
|
42
|
+
errors.push('"apikey" must be a non-empty string')
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (!params.prompt || typeof params.prompt !== 'string') {
|
|
46
|
+
errors.push('"prompt" must be a non-empty string')
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Optional string fields
|
|
50
|
+
if (params.system !== undefined && typeof params.system !== 'string') {
|
|
51
|
+
errors.push('"system" must be a string')
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Optional number fields
|
|
55
|
+
const numberFields = ['temperature', 'maxTokens', 'topP', 'topK', 'frequencyPenalty', 'presencePenalty', 'seed']
|
|
56
|
+
for (const field of numberFields) {
|
|
57
|
+
if (params[field] !== undefined && typeof params[field] !== 'number') {
|
|
58
|
+
errors.push(`"${field}" must be a number`)
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Optional array fields
|
|
63
|
+
if (params.messages !== undefined) {
|
|
64
|
+
if (!Array.isArray(params.messages)) {
|
|
65
|
+
errors.push('"messages" must be an array')
|
|
66
|
+
} else {
|
|
67
|
+
params.messages.forEach((msg, i) => {
|
|
68
|
+
if (!msg || typeof msg !== 'object') {
|
|
69
|
+
errors.push(`messages[${i}] must be an object`)
|
|
70
|
+
} else if (!['user', 'assistant', 'system'].includes(msg.role)) {
|
|
71
|
+
errors.push(`messages[${i}].role must be 'user', 'assistant', or 'system'`)
|
|
72
|
+
} else if (typeof msg.content !== 'string') {
|
|
73
|
+
errors.push(`messages[${i}].content must be a string`)
|
|
74
|
+
}
|
|
75
|
+
})
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (params.fallbacks !== undefined) {
|
|
80
|
+
if (!Array.isArray(params.fallbacks)) {
|
|
81
|
+
errors.push('"fallbacks" must be an array')
|
|
82
|
+
} else {
|
|
83
|
+
params.fallbacks.forEach((model, i) => {
|
|
84
|
+
if (typeof model !== 'string') {
|
|
85
|
+
errors.push(`fallbacks[${i}] must be a string`)
|
|
86
|
+
}
|
|
87
|
+
})
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// stop can be string or array of strings
|
|
92
|
+
if (params.stop !== undefined) {
|
|
93
|
+
if (typeof params.stop !== 'string' && !Array.isArray(params.stop)) {
|
|
94
|
+
errors.push('"stop" must be a string or array of strings')
|
|
95
|
+
} else if (Array.isArray(params.stop)) {
|
|
96
|
+
params.stop.forEach((s, i) => {
|
|
97
|
+
if (typeof s !== 'string') {
|
|
98
|
+
errors.push(`stop[${i}] must be a string`)
|
|
99
|
+
}
|
|
100
|
+
})
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// providerOptions must be an object
|
|
105
|
+
if (params.providerOptions !== undefined &&
|
|
106
|
+
(typeof params.providerOptions !== 'object' || params.providerOptions === null || Array.isArray(params.providerOptions))) {
|
|
107
|
+
errors.push('"providerOptions" must be an object')
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (errors.length > 0) {
|
|
111
|
+
throw new Error(errors.join('; '))
|
|
112
|
+
}
|
|
113
|
+
}
|