smoltalk 0.4.1 → 0.5.0
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 +139 -0
- package/dist/classes/ToolCall.js +4 -3
- package/dist/classes/message/AssistantMessage.d.ts +1 -0
- package/dist/classes/message/AssistantMessage.js +5 -3
- package/dist/classes/message/DeveloperMessage.js +5 -3
- package/dist/classes/message/SystemMessage.js +5 -3
- package/dist/classes/message/ToolMessage.js +4 -3
- package/dist/classes/message/UserMessage.js +5 -3
- package/dist/client.js +5 -3
- package/dist/clients/anthropic.d.ts +3 -1
- package/dist/clients/anthropic.js +71 -13
- package/dist/clients/baseClient.js +11 -1
- package/dist/clients/google.d.ts +4 -0
- package/dist/clients/google.js +92 -24
- package/dist/clients/ollama.d.ts +1 -0
- package/dist/clients/ollama.js +18 -7
- package/dist/clients/openai.js +7 -4
- package/dist/clients/openaiResponses.d.ts +3 -0
- package/dist/clients/openaiResponses.js +60 -7
- package/dist/embed/openai.js +3 -3
- package/dist/embed.d.ts +5 -2
- package/dist/embed.js +14 -3
- package/dist/image/google.js +3 -3
- package/dist/image/openai.js +6 -6
- package/dist/image.d.ts +5 -2
- package/dist/image.js +14 -3
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/model.d.ts +4 -2
- package/dist/model.js +7 -5
- package/dist/modelData.d.ts +40 -0
- package/dist/modelData.js +222 -0
- package/dist/models.d.ts +658 -760
- package/dist/models.js +836 -60
- package/dist/smolError.d.ts +78 -5
- package/dist/smolError.js +134 -9
- package/dist/types/costEstimate.d.ts +2 -0
- package/dist/types/costEstimate.js +1 -0
- package/dist/types.d.ts +30 -1
- package/dist/types.js +2 -1
- package/dist/util/hostedTools.d.ts +19 -0
- package/dist/util/hostedTools.js +104 -0
- package/dist/util/httpError.d.ts +23 -0
- package/dist/util/httpError.js +237 -0
- package/dist/util/provider.d.ts +2 -1
- package/dist/util/provider.js +2 -2
- package/package.json +7 -2
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Explicit allowlist (not denylist, not prefix-wildcard). smoltalk can be
|
|
3
|
+
* pointed at proxies (LiteLLM, OpenRouter) that re-emit upstream headers
|
|
4
|
+
* verbatim, and some APIs echo credentials back on errors — a denylist would
|
|
5
|
+
* pass `authorization`, `x-api-key`, forwarded `llm_provider-*` headers, etc.
|
|
6
|
+
* (cf. Traefik GHSA-p6hg-qh38-555r). A prefix wildcard like `x-ratelimit-*`
|
|
7
|
+
* is safer than a denylist but still risks absorbing a future
|
|
8
|
+
* `x-ratelimit-organization` style header that carries an identifier or
|
|
9
|
+
* secret. Capture only known-safe headers with diagnostic value; the raw
|
|
10
|
+
* provider error is reachable on `SmolError.cause` (non-enumerable) as the
|
|
11
|
+
* escape hatch for callers that explicitly opt in.
|
|
12
|
+
*/
|
|
13
|
+
const ALLOWED_HEADERS = new Set([
|
|
14
|
+
// Retry hints
|
|
15
|
+
"retry-after",
|
|
16
|
+
"retry-after-ms",
|
|
17
|
+
// Request correlation
|
|
18
|
+
"request-id",
|
|
19
|
+
"x-request-id",
|
|
20
|
+
// Diagnostic
|
|
21
|
+
"content-type",
|
|
22
|
+
// OpenAI rate-limit family (exhaustive as of writing — new headers added
|
|
23
|
+
// by providers must be added explicitly here).
|
|
24
|
+
"x-ratelimit-limit-requests",
|
|
25
|
+
"x-ratelimit-limit-tokens",
|
|
26
|
+
"x-ratelimit-remaining-requests",
|
|
27
|
+
"x-ratelimit-remaining-tokens",
|
|
28
|
+
"x-ratelimit-reset-requests",
|
|
29
|
+
"x-ratelimit-reset-tokens",
|
|
30
|
+
// Anthropic rate-limit family
|
|
31
|
+
"anthropic-ratelimit-requests-limit",
|
|
32
|
+
"anthropic-ratelimit-requests-remaining",
|
|
33
|
+
"anthropic-ratelimit-requests-reset",
|
|
34
|
+
"anthropic-ratelimit-tokens-limit",
|
|
35
|
+
"anthropic-ratelimit-tokens-remaining",
|
|
36
|
+
"anthropic-ratelimit-tokens-reset",
|
|
37
|
+
"anthropic-ratelimit-input-tokens-limit",
|
|
38
|
+
"anthropic-ratelimit-input-tokens-remaining",
|
|
39
|
+
"anthropic-ratelimit-input-tokens-reset",
|
|
40
|
+
"anthropic-ratelimit-output-tokens-limit",
|
|
41
|
+
"anthropic-ratelimit-output-tokens-remaining",
|
|
42
|
+
"anthropic-ratelimit-output-tokens-reset",
|
|
43
|
+
]);
|
|
44
|
+
function isAllowedHeader(name) {
|
|
45
|
+
return ALLOWED_HEADERS.has(name.toLowerCase());
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Upper bound on `retryAfterMs`. Anything longer is almost certainly a
|
|
49
|
+
* misparsed HTTP-date or a hostile/buggy proxy injecting a far-future value
|
|
50
|
+
* (e.g. `Sat, 01 Jan 9999 …`). 5 minutes is well beyond any sane provider
|
|
51
|
+
* rate-limit window — callers that wanted to wait longer would have given
|
|
52
|
+
* up by then anyway.
|
|
53
|
+
*/
|
|
54
|
+
const MAX_RETRY_AFTER_MS = 5 * 60 * 1000;
|
|
55
|
+
function clampRetryAfter(ms) {
|
|
56
|
+
return Math.min(MAX_RETRY_AFTER_MS, Math.max(0, Math.round(ms)));
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Read a property without letting a throwing accessor break the whole
|
|
60
|
+
* extraction. Some error shapes (especially JSON-parsed bodies that bubble
|
|
61
|
+
* up as errors) can carry getters that throw or have side effects.
|
|
62
|
+
*/
|
|
63
|
+
function safeGet(obj, key) {
|
|
64
|
+
try {
|
|
65
|
+
return obj[key];
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
return undefined;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
export function extractHttpErrorFields(error) {
|
|
72
|
+
const fields = {};
|
|
73
|
+
if (!error || typeof error !== "object") {
|
|
74
|
+
return fields;
|
|
75
|
+
}
|
|
76
|
+
const err = error;
|
|
77
|
+
const status = safeGet(err, "status") ??
|
|
78
|
+
safeGet(err, "statusCode") ??
|
|
79
|
+
safeGet(err, "status_code");
|
|
80
|
+
if (typeof status === "number") {
|
|
81
|
+
fields.status = status;
|
|
82
|
+
}
|
|
83
|
+
const headers = normalizeHeaders(safeGet(err, "headers"));
|
|
84
|
+
if (headers) {
|
|
85
|
+
fields.headers = headers;
|
|
86
|
+
const retryAfterMs = parseRetryAfterMs(headers);
|
|
87
|
+
if (retryAfterMs !== undefined) {
|
|
88
|
+
fields.retryAfterMs = retryAfterMs;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
const requestId = extractRequestId(err, headers);
|
|
92
|
+
if (requestId !== undefined) {
|
|
93
|
+
fields.requestId = requestId;
|
|
94
|
+
}
|
|
95
|
+
return fields;
|
|
96
|
+
}
|
|
97
|
+
function extractRequestId(err, headers) {
|
|
98
|
+
// The OpenAI/Anthropic SDK errors carry a parsed request id directly.
|
|
99
|
+
// NOTE: providers may embed account/org identifiers in request ids (e.g.
|
|
100
|
+
// OpenAI's `req_<orghash>_<reqhash>`). These are loggable but not entirely
|
|
101
|
+
// opaque — keep that in mind when forwarding to third-party telemetry.
|
|
102
|
+
const requestID = safeGet(err, "requestID");
|
|
103
|
+
if (typeof requestID === "string") {
|
|
104
|
+
return requestID;
|
|
105
|
+
}
|
|
106
|
+
const request_id = safeGet(err, "request_id");
|
|
107
|
+
if (typeof request_id === "string") {
|
|
108
|
+
return request_id;
|
|
109
|
+
}
|
|
110
|
+
if (headers) {
|
|
111
|
+
return (headerValue(headers, "x-request-id") ?? headerValue(headers, "request-id"));
|
|
112
|
+
}
|
|
113
|
+
return undefined;
|
|
114
|
+
}
|
|
115
|
+
function normalizeHeaders(raw) {
|
|
116
|
+
if (!raw || typeof raw !== "object") {
|
|
117
|
+
return undefined;
|
|
118
|
+
}
|
|
119
|
+
const out = {};
|
|
120
|
+
// Web `Headers` (used by the OpenAI/Anthropic SDKs) is iterable via forEach.
|
|
121
|
+
// We always lowercase the stored key so downstream `headers["x-request-id"]`
|
|
122
|
+
// lookups work regardless of the provider's original casing.
|
|
123
|
+
try {
|
|
124
|
+
if (typeof raw.forEach === "function") {
|
|
125
|
+
raw.forEach((value, key) => {
|
|
126
|
+
if (isAllowedHeader(key)) {
|
|
127
|
+
out[key.toLowerCase()] = value;
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
else {
|
|
132
|
+
// Fall back to a plain object of header key/value pairs.
|
|
133
|
+
for (const [key, value] of Object.entries(raw)) {
|
|
134
|
+
if (typeof value === "string" && isAllowedHeader(key)) {
|
|
135
|
+
out[key.toLowerCase()] = value;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
// A malformed Headers-shaped object with a throwing iterator — give up
|
|
142
|
+
// on header capture rather than poisoning the entire extraction.
|
|
143
|
+
return undefined;
|
|
144
|
+
}
|
|
145
|
+
return Object.keys(out).length > 0 ? out : undefined;
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Parse a retry delay (in milliseconds) from the response headers, mirroring
|
|
149
|
+
* the logic the OpenAI and Anthropic SDKs use:
|
|
150
|
+
* 1. `retry-after-ms` (non-standard, ms precision) takes precedence.
|
|
151
|
+
* 2. Otherwise `retry-after` (RFC 9110): either delta-seconds or an HTTP-date.
|
|
152
|
+
*
|
|
153
|
+
* Always returns a non-negative value capped at `MAX_RETRY_AFTER_MS`.
|
|
154
|
+
*/
|
|
155
|
+
function parseRetryAfterMs(headers) {
|
|
156
|
+
// `retry-after-ms` (non-standard, OpenAI-specific) is always purely numeric;
|
|
157
|
+
// try whole-value first, then first-of-list, then give up.
|
|
158
|
+
const msRaw = headerValue(headers, "retry-after-ms");
|
|
159
|
+
const ms = numericHeader(msRaw) ?? numericHeader(firstNumericToken(msRaw));
|
|
160
|
+
if (ms !== undefined) {
|
|
161
|
+
return clampRetryAfter(ms);
|
|
162
|
+
}
|
|
163
|
+
const retryAfter = headerValue(headers, "retry-after");
|
|
164
|
+
if (retryAfter !== undefined) {
|
|
165
|
+
// 1. Try the whole value as a delta-seconds number ("30", "-3600").
|
|
166
|
+
const seconds = numericHeader(retryAfter);
|
|
167
|
+
if (seconds !== undefined) {
|
|
168
|
+
return clampRetryAfter(seconds * 1000);
|
|
169
|
+
}
|
|
170
|
+
// 2. Comma-separated numeric list? Take the first token if numeric.
|
|
171
|
+
// NOTE: HTTP-dates contain commas ("Sat, 01 Jan ..."), so we only
|
|
172
|
+
// take the first token when it parses as a pure number — otherwise
|
|
173
|
+
// we'd mangle a perfectly good HTTP-date into "Sat".
|
|
174
|
+
const firstNumeric = numericHeader(firstNumericToken(retryAfter));
|
|
175
|
+
if (firstNumeric !== undefined) {
|
|
176
|
+
return clampRetryAfter(firstNumeric * 1000);
|
|
177
|
+
}
|
|
178
|
+
// 3. Otherwise treat as RFC 9110 HTTP-date.
|
|
179
|
+
const dateMs = Date.parse(retryAfter) - Date.now();
|
|
180
|
+
if (!Number.isNaN(dateMs)) {
|
|
181
|
+
return clampRetryAfter(dateMs);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
return undefined;
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Returns the first comma-separated token of `value` only if it parses as a
|
|
188
|
+
* number. Returns undefined for non-numeric first tokens (notably HTTP-dates
|
|
189
|
+
* like `"Sat, 01 Jan 2026 ..."`) so the caller can fall back to date parsing.
|
|
190
|
+
*/
|
|
191
|
+
function firstNumericToken(value) {
|
|
192
|
+
if (value === undefined) {
|
|
193
|
+
return undefined;
|
|
194
|
+
}
|
|
195
|
+
const first = value.split(",")[0];
|
|
196
|
+
if (first === undefined) {
|
|
197
|
+
return undefined;
|
|
198
|
+
}
|
|
199
|
+
const trimmed = first.trim();
|
|
200
|
+
if (trimmed === "" || Number.isNaN(Number(trimmed))) {
|
|
201
|
+
return undefined;
|
|
202
|
+
}
|
|
203
|
+
return trimmed;
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Strict numeric parse — unlike `parseFloat`, this rejects values with trailing
|
|
207
|
+
* junk (`"5xyz"`, `"30abc"`) instead of silently accepting a prefix.
|
|
208
|
+
*/
|
|
209
|
+
function numericHeader(value) {
|
|
210
|
+
if (value === undefined) {
|
|
211
|
+
return undefined;
|
|
212
|
+
}
|
|
213
|
+
const trimmed = value.trim();
|
|
214
|
+
if (trimmed === "") {
|
|
215
|
+
return undefined;
|
|
216
|
+
}
|
|
217
|
+
const parsed = Number(trimmed);
|
|
218
|
+
if (Number.isNaN(parsed)) {
|
|
219
|
+
return undefined;
|
|
220
|
+
}
|
|
221
|
+
return parsed;
|
|
222
|
+
}
|
|
223
|
+
function headerValue(headers, name) {
|
|
224
|
+
// Keys are stored lowercased by `normalizeHeaders`, so a direct lookup is
|
|
225
|
+
// sufficient. Keep a case-insensitive fallback for defense in depth in case
|
|
226
|
+
// a caller hands us a non-normalized object.
|
|
227
|
+
const lower = name.toLowerCase();
|
|
228
|
+
if (lower in headers) {
|
|
229
|
+
return headers[lower];
|
|
230
|
+
}
|
|
231
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
232
|
+
if (key.toLowerCase() === lower) {
|
|
233
|
+
return value;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
return undefined;
|
|
237
|
+
}
|
package/dist/util/provider.d.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
+
import type { ModelDataBlob } from "../modelData.js";
|
|
1
2
|
/**
|
|
2
3
|
* Resolve the provider for a given model name.
|
|
3
4
|
* If an explicit provider is given, returns it directly.
|
|
4
5
|
* Otherwise looks up the model in the registry.
|
|
5
6
|
*/
|
|
6
|
-
export declare function resolveProvider(modelName: string, explicitProvider?: string): string;
|
|
7
|
+
export declare function resolveProvider(modelName: string, explicitProvider?: string, modelData?: ModelDataBlob): string;
|
|
7
8
|
type ApiKeyConfig = {
|
|
8
9
|
openAiApiKey?: string;
|
|
9
10
|
googleApiKey?: string;
|
package/dist/util/provider.js
CHANGED
|
@@ -5,10 +5,10 @@ import { SmolError } from "../smolError.js";
|
|
|
5
5
|
* If an explicit provider is given, returns it directly.
|
|
6
6
|
* Otherwise looks up the model in the registry.
|
|
7
7
|
*/
|
|
8
|
-
export function resolveProvider(modelName, explicitProvider) {
|
|
8
|
+
export function resolveProvider(modelName, explicitProvider, modelData) {
|
|
9
9
|
if (explicitProvider)
|
|
10
10
|
return explicitProvider;
|
|
11
|
-
const model = getModel(modelName);
|
|
11
|
+
const model = getModel(modelName, modelData);
|
|
12
12
|
if (model === undefined) {
|
|
13
13
|
throw new SmolError(`Model ${modelName} is not recognized. Please specify a known model, or explicitly set the provider option in the config.`);
|
|
14
14
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "smoltalk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "A common interface for LLM APIs",
|
|
5
5
|
"homepage": "https://github.com/egonSchiele/smoltalk",
|
|
6
6
|
"files": [
|
|
@@ -35,6 +35,9 @@
|
|
|
35
35
|
"openai": "^6.15.0",
|
|
36
36
|
"zod": "^4.3.6"
|
|
37
37
|
},
|
|
38
|
+
"devDependencies": {
|
|
39
|
+
"tsx": "^4.19.2"
|
|
40
|
+
},
|
|
38
41
|
"scripts": {
|
|
39
42
|
"test": "vitest --exclude=**/*.live.test.ts",
|
|
40
43
|
"test:live": "vitest run lib/clients/*.live.test.ts lib/embed/*.live.test.ts lib/image/*.live.test.ts",
|
|
@@ -42,6 +45,8 @@
|
|
|
42
45
|
"build": "rm -rf dist && tsc",
|
|
43
46
|
"start": "cd dist && node index.js",
|
|
44
47
|
"doc": "typedoc --disableSources --out docs lib && prettier docs/ --write",
|
|
45
|
-
"typecheck": "tsc --noEmit"
|
|
48
|
+
"typecheck": "tsc --noEmit",
|
|
49
|
+
"seed-data": "tsx scripts/seed-model-data.ts",
|
|
50
|
+
"refresh-data": "tsx scripts/refresh-from-modelsdev.ts"
|
|
46
51
|
}
|
|
47
52
|
}
|