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.
Files changed (47) hide show
  1. package/README.md +139 -0
  2. package/dist/classes/ToolCall.js +4 -3
  3. package/dist/classes/message/AssistantMessage.d.ts +1 -0
  4. package/dist/classes/message/AssistantMessage.js +5 -3
  5. package/dist/classes/message/DeveloperMessage.js +5 -3
  6. package/dist/classes/message/SystemMessage.js +5 -3
  7. package/dist/classes/message/ToolMessage.js +4 -3
  8. package/dist/classes/message/UserMessage.js +5 -3
  9. package/dist/client.js +5 -3
  10. package/dist/clients/anthropic.d.ts +3 -1
  11. package/dist/clients/anthropic.js +71 -13
  12. package/dist/clients/baseClient.js +11 -1
  13. package/dist/clients/google.d.ts +4 -0
  14. package/dist/clients/google.js +92 -24
  15. package/dist/clients/ollama.d.ts +1 -0
  16. package/dist/clients/ollama.js +18 -7
  17. package/dist/clients/openai.js +7 -4
  18. package/dist/clients/openaiResponses.d.ts +3 -0
  19. package/dist/clients/openaiResponses.js +60 -7
  20. package/dist/embed/openai.js +3 -3
  21. package/dist/embed.d.ts +5 -2
  22. package/dist/embed.js +14 -3
  23. package/dist/image/google.js +3 -3
  24. package/dist/image/openai.js +6 -6
  25. package/dist/image.d.ts +5 -2
  26. package/dist/image.js +14 -3
  27. package/dist/index.d.ts +1 -0
  28. package/dist/index.js +1 -0
  29. package/dist/model.d.ts +4 -2
  30. package/dist/model.js +7 -5
  31. package/dist/modelData.d.ts +40 -0
  32. package/dist/modelData.js +222 -0
  33. package/dist/models.d.ts +658 -760
  34. package/dist/models.js +836 -60
  35. package/dist/smolError.d.ts +78 -5
  36. package/dist/smolError.js +134 -9
  37. package/dist/types/costEstimate.d.ts +2 -0
  38. package/dist/types/costEstimate.js +1 -0
  39. package/dist/types.d.ts +30 -1
  40. package/dist/types.js +2 -1
  41. package/dist/util/hostedTools.d.ts +19 -0
  42. package/dist/util/hostedTools.js +104 -0
  43. package/dist/util/httpError.d.ts +23 -0
  44. package/dist/util/httpError.js +237 -0
  45. package/dist/util/provider.d.ts +2 -1
  46. package/dist/util/provider.js +2 -2
  47. package/package.json +7 -2
package/dist/model.js CHANGED
@@ -4,11 +4,13 @@ import { round } from "./util/util.js";
4
4
  export class Model {
5
5
  model;
6
6
  provider;
7
- constructor(model, provider) {
7
+ modelData;
8
+ constructor(model, provider, modelData) {
8
9
  if (!ModelNameSchema.safeParse(model).success) {
9
10
  throw new SmolError(`Model ${JSON.stringify(model)} is not recognized. Please specify a known model name.`);
10
11
  }
11
12
  this.model = model;
13
+ this.modelData = modelData;
12
14
  this.provider = provider || this.lookupProvider();
13
15
  }
14
16
  getModel() {
@@ -18,11 +20,11 @@ export class Model {
18
20
  return this.provider;
19
21
  }
20
22
  lookupProvider() {
21
- const modelInfo = getModel(this.model);
23
+ const modelInfo = getModel(this.model, this.modelData);
22
24
  return modelInfo ? modelInfo.provider : undefined;
23
25
  }
24
26
  calculateCost(usage) {
25
- const model = getModel(this.model);
27
+ const model = getModel(this.model, this.modelData);
26
28
  if (!model || !isTextModel(model)) {
27
29
  return null;
28
30
  }
@@ -79,10 +81,10 @@ export class Model {
79
81
  toJSON() {
80
82
  return this.model;
81
83
  }
82
- static create(model, provider) {
84
+ static create(model, provider, modelData) {
83
85
  if (model instanceof Model) {
84
86
  return model;
85
87
  }
86
- return new Model(model, provider);
88
+ return new Model(model, provider, modelData);
87
89
  }
88
90
  }
@@ -0,0 +1,40 @@
1
+ import type { ModelType } from "./models.js";
2
+ import { Result } from "./types/result.js";
3
+ export declare const SUPPORTED_SCHEMA_VERSION = 1;
4
+ export type HostedToolPrice = {
5
+ unit: "per_call" | "per_session" | "per_hour" | "per_gb_day" | "tokens" | "free";
6
+ amount?: number;
7
+ freeAllowance?: string;
8
+ note?: string;
9
+ };
10
+ export type HostedToolPricing = HostedToolPrice & {
11
+ perModel?: Record<string, Partial<HostedToolPrice>>;
12
+ };
13
+ export type HostedTool = {
14
+ name: string;
15
+ provider: string;
16
+ category?: string;
17
+ description?: string;
18
+ providerToolId?: string;
19
+ models?: string[];
20
+ pricing?: HostedToolPricing;
21
+ disabled?: boolean;
22
+ };
23
+ export type ModelDataBlob = {
24
+ schemaVersion: number;
25
+ generatedAt: string;
26
+ models: ModelType[];
27
+ hostedTools: HostedTool[];
28
+ };
29
+ export declare function parseModelDataBlob(raw: string): Result<ModelDataBlob>;
30
+ export declare function deepMergeEntry<T>(base: T, overlay: T): T;
31
+ export declare function mergeModelData(base: ModelType[], overlay: ModelType[]): ModelType[];
32
+ export declare function mergeHostedTools(base: HostedTool[], overlay: HostedTool[]): HostedTool[];
33
+ export declare const DEFAULT_MODEL_DATA_URL = "https://raw.githubusercontent.com/egonSchiele/smoltalk/main/packages/smoltalk/data/model-data.json";
34
+ export type Fetcher = (url: string, signal?: AbortSignal) => Promise<string>;
35
+ export type RefreshOptions = {
36
+ url?: string;
37
+ fetcher?: Fetcher;
38
+ signal?: AbortSignal;
39
+ };
40
+ export declare function refreshModels(opts?: RefreshOptions): Promise<Result<ModelDataBlob>>;
@@ -0,0 +1,222 @@
1
+ import z from "zod";
2
+ import { readFile } from "node:fs/promises";
3
+ import { fileURLToPath } from "node:url";
4
+ import { success, failure } from "./types/result.js";
5
+ export const SUPPORTED_SCHEMA_VERSION = 1;
6
+ const EnvelopeSchema = z.object({
7
+ schemaVersion: z.number(),
8
+ generatedAt: z.string(),
9
+ models: z.array(z.unknown()),
10
+ hostedTools: z.array(z.unknown()).optional(),
11
+ });
12
+ // Permissive: require the fields smoltalk keys on, keep everything else.
13
+ const ModelEntrySchema = z
14
+ .object({
15
+ modelName: z.string(),
16
+ provider: z.string(),
17
+ type: z.string(),
18
+ })
19
+ .catchall(z.unknown());
20
+ // Per-model override: price fields only. No `.catchall`, so unknown keys
21
+ // (including a stray nested `perModel`) are stripped — the resolver can never
22
+ // reintroduce perModel into its result.
23
+ const PerModelPriceSchema = z.object({
24
+ unit: z.string().optional(),
25
+ amount: z.number().optional(),
26
+ freeAllowance: z.string().optional(),
27
+ note: z.string().optional(),
28
+ });
29
+ // `unit` stays a lenient string (forward-compat, mirroring ModelEntrySchema's
30
+ // `type: z.string()`); the closed union is the known/intended set.
31
+ const HostedToolPricingSchema = z
32
+ .object({
33
+ unit: z.string(),
34
+ amount: z.number().optional(),
35
+ freeAllowance: z.string().optional(),
36
+ note: z.string().optional(),
37
+ perModel: z.record(z.string(), PerModelPriceSchema).optional(),
38
+ })
39
+ .catchall(z.unknown());
40
+ const HostedToolSchema = z
41
+ .object({
42
+ name: z.string(),
43
+ provider: z.string(),
44
+ category: z.string().optional(),
45
+ description: z.string().optional(),
46
+ providerToolId: z.string().optional(),
47
+ models: z.array(z.string()).optional(),
48
+ pricing: HostedToolPricingSchema.optional(),
49
+ disabled: z.boolean().optional(),
50
+ })
51
+ .catchall(z.unknown());
52
+ export function parseModelDataBlob(raw) {
53
+ let json;
54
+ try {
55
+ json = JSON.parse(raw);
56
+ }
57
+ catch (err) {
58
+ return failure(`Model data is not valid JSON: ${String(err)}`);
59
+ }
60
+ const envelope = EnvelopeSchema.safeParse(json);
61
+ if (!envelope.success) {
62
+ return failure(`Model data blob is malformed: ${z.prettifyError(envelope.error)}`);
63
+ }
64
+ if (envelope.data.schemaVersion > SUPPORTED_SCHEMA_VERSION) {
65
+ return failure(`Model data schemaVersion ${envelope.data.schemaVersion} is newer than this smoltalk supports (${SUPPORTED_SCHEMA_VERSION}). Please upgrade smoltalk.`);
66
+ }
67
+ const models = [];
68
+ for (const entry of envelope.data.models) {
69
+ const parsed = ModelEntrySchema.safeParse(entry);
70
+ if (!parsed.success) {
71
+ console.warn(`Skipping invalid model entry: ${z.prettifyError(parsed.error)}`);
72
+ continue;
73
+ }
74
+ models.push(parsed.data);
75
+ }
76
+ const hostedTools = [];
77
+ let rawTools = [];
78
+ if (envelope.data.hostedTools) {
79
+ rawTools = envelope.data.hostedTools;
80
+ }
81
+ for (const entry of rawTools) {
82
+ const parsed = HostedToolSchema.safeParse(entry);
83
+ if (!parsed.success) {
84
+ console.warn(`Skipping invalid hosted tool: ${z.prettifyError(parsed.error)}`);
85
+ continue;
86
+ }
87
+ hostedTools.push(parsed.data);
88
+ }
89
+ return success({
90
+ schemaVersion: envelope.data.schemaVersion,
91
+ generatedAt: envelope.data.generatedAt,
92
+ models,
93
+ hostedTools,
94
+ });
95
+ }
96
+ function isPlainObject(value) {
97
+ if (value === null) {
98
+ return false;
99
+ }
100
+ if (typeof value !== "object") {
101
+ return false;
102
+ }
103
+ if (Array.isArray(value)) {
104
+ return false;
105
+ }
106
+ return true;
107
+ }
108
+ export function deepMergeEntry(base, overlay) {
109
+ if (!isPlainObject(base) || !isPlainObject(overlay)) {
110
+ return overlay;
111
+ }
112
+ const result = { ...base };
113
+ for (const key of Object.keys(overlay)) {
114
+ const overValue = overlay[key];
115
+ if (overValue === undefined) {
116
+ continue;
117
+ }
118
+ const currentValue = result[key];
119
+ if (isPlainObject(currentValue) && isPlainObject(overValue)) {
120
+ result[key] = deepMergeEntry(currentValue, overValue);
121
+ }
122
+ else {
123
+ result[key] = overValue;
124
+ }
125
+ }
126
+ return result;
127
+ }
128
+ function mergeByKey(base, overlay, keyOf) {
129
+ const order = [];
130
+ const map = new Map();
131
+ for (const item of base) {
132
+ const key = keyOf(item);
133
+ if (!map.has(key)) {
134
+ order.push(key);
135
+ }
136
+ map.set(key, item);
137
+ }
138
+ for (const item of overlay) {
139
+ const key = keyOf(item);
140
+ const existing = map.get(key);
141
+ if (existing === undefined) {
142
+ order.push(key);
143
+ map.set(key, item);
144
+ }
145
+ else {
146
+ map.set(key, deepMergeEntry(existing, item));
147
+ }
148
+ }
149
+ return order.map((key) => map.get(key));
150
+ }
151
+ export function mergeModelData(base, overlay) {
152
+ return mergeByKey(base, overlay, (m) => `${m.provider}:${m.modelName}`);
153
+ }
154
+ export function mergeHostedTools(base, overlay) {
155
+ return mergeByKey(base, overlay, (t) => `${t.provider}:${t.name}`);
156
+ }
157
+ export const DEFAULT_MODEL_DATA_URL = "https://raw.githubusercontent.com/egonSchiele/smoltalk/main/packages/smoltalk/data/model-data.json";
158
+ const MAX_BYTES = 10_000_000;
159
+ const TIMEOUT_MS = 15_000;
160
+ function enforceSizeCap(text) {
161
+ if (Buffer.byteLength(text, "utf8") > MAX_BYTES) {
162
+ throw new Error(`Model data exceeds the ${MAX_BYTES}-byte cap`);
163
+ }
164
+ return text;
165
+ }
166
+ // The default fetcher supports remote https:// URLs and local file:// URLs.
167
+ // file:// lets users point at a self-hosted on-disk catalog (and powers the
168
+ // integration test). Any other scheme is rejected.
169
+ async function defaultFetcher(url, signal) {
170
+ if (url.startsWith("file://")) {
171
+ const path = fileURLToPath(url);
172
+ const text = await readFile(path, "utf8");
173
+ return enforceSizeCap(text);
174
+ }
175
+ if (url.startsWith("https://")) {
176
+ const res = await fetch(url, { signal });
177
+ if (!res.ok) {
178
+ throw new Error(`HTTP ${res.status} ${res.statusText}`);
179
+ }
180
+ return enforceSizeCap(await res.text());
181
+ }
182
+ throw new Error(`Unsupported model-data URL scheme (expected https:// or file://): ${url}`);
183
+ }
184
+ export async function refreshModels(opts = {}) {
185
+ let url = DEFAULT_MODEL_DATA_URL;
186
+ if (process.env.SMOLTALK_MODEL_DATA_URL) {
187
+ url = process.env.SMOLTALK_MODEL_DATA_URL;
188
+ }
189
+ if (opts.url) {
190
+ url = opts.url;
191
+ }
192
+ let fetcher = defaultFetcher;
193
+ if (opts.fetcher) {
194
+ fetcher = opts.fetcher;
195
+ }
196
+ // Always drive the fetcher off our own controller so the timeout can abort
197
+ // it. Forward an externally-supplied signal into the same controller so both
198
+ // the timeout and the caller can cancel.
199
+ const controller = new AbortController();
200
+ const timer = setTimeout(() => controller.abort(), TIMEOUT_MS);
201
+ if (opts.signal) {
202
+ if (opts.signal.aborted) {
203
+ controller.abort();
204
+ }
205
+ else {
206
+ opts.signal.addEventListener("abort", () => controller.abort(), {
207
+ once: true,
208
+ });
209
+ }
210
+ }
211
+ let raw;
212
+ try {
213
+ raw = await fetcher(url, controller.signal);
214
+ }
215
+ catch (err) {
216
+ return failure(`Could not fetch model data from ${url}: ${String(err)}`);
217
+ }
218
+ finally {
219
+ clearTimeout(timer);
220
+ }
221
+ return parseModelDataBlob(raw);
222
+ }