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
@@ -3,11 +3,65 @@ import { ToolCall } from "../classes/ToolCall.js";
3
3
  import { getLogger } from "../util/logger.js";
4
4
  import { addCosts, addTokenUsage, success, } from "../types.js";
5
5
  import { zodToGoogleTool } from "../util/tool.js";
6
- import { SmolContentPolicyError, SmolContextWindowExceededError, } from "../smolError.js";
6
+ import { SmolContentPolicyError, SmolContextWindowExceededError, smolErrorForStatus, } from "../smolError.js";
7
+ import { extractHttpErrorFields } from "../util/httpError.js";
7
8
  import { sanitizeAttributes } from "../util/util.js";
8
9
  import { BaseClient } from "./baseClient.js";
10
+ import { WEB_SEARCH, webSearchResult, applyHostedToolCost } from "../util/hostedTools.js";
9
11
  import { Model } from "../model.js";
10
12
  import { userMessage } from "../classes/message/index.js";
13
+ export function googleWebSearchEntries(hostedTools) {
14
+ if (hostedTools && hostedTools.includes(WEB_SEARCH)) {
15
+ return [{ googleSearch: {} }];
16
+ }
17
+ return [];
18
+ }
19
+ export function parseGoogleHostedTools(result, provider, model) {
20
+ const queries = [];
21
+ const sources = [];
22
+ const citations = [];
23
+ const raw = [];
24
+ for (const candidate of result.candidates || []) {
25
+ const gm = candidate.groundingMetadata;
26
+ // A candidate without groundingMetadata simply didn't ground — nothing to
27
+ // extract. If no candidate grounded, this returns [] overall.
28
+ if (!gm) {
29
+ continue;
30
+ }
31
+ raw.push(gm);
32
+ for (const q of gm.webSearchQueries || []) {
33
+ queries.push(q);
34
+ }
35
+ const chunks = gm.groundingChunks || [];
36
+ for (const c of chunks) {
37
+ if (c.web && typeof c.web.uri === "string") {
38
+ sources.push({ url: c.web.uri, title: c.web.title });
39
+ }
40
+ }
41
+ for (const s of gm.groundingSupports || []) {
42
+ for (const idx of s.groundingChunkIndices || []) {
43
+ const chunk = chunks[idx];
44
+ if (chunk && chunk.web && typeof chunk.web.uri === "string") {
45
+ citations.push({
46
+ url: chunk.web.uri,
47
+ title: chunk.web.title,
48
+ startIndex: s.segment?.startIndex,
49
+ endIndex: s.segment?.endIndex,
50
+ });
51
+ }
52
+ }
53
+ }
54
+ }
55
+ if (queries.length === 0 && sources.length === 0) {
56
+ return [];
57
+ }
58
+ // Gemini 2.5 bills per prompt (1), Gemini 3+ per query.
59
+ let callCount = queries.length;
60
+ if (model.startsWith("gemini-2.5")) {
61
+ callCount = 1;
62
+ }
63
+ return [webSearchResult(provider, { queries, sources, citations, callCount, raw })];
64
+ }
11
65
  export class SmolGoogle extends BaseClient {
12
66
  client;
13
67
  logger;
@@ -19,7 +73,7 @@ export class SmolGoogle extends BaseClient {
19
73
  }
20
74
  this.client = new GoogleGenAI({ apiKey: config.googleApiKey });
21
75
  this.logger = getLogger();
22
- this.model = new Model(config.model);
76
+ this.model = new Model(config.model, undefined, config.modelData);
23
77
  }
24
78
  getClient() {
25
79
  return this.client;
@@ -68,8 +122,16 @@ export class SmolGoogle extends BaseClient {
68
122
  if (systemParts.length > 0) {
69
123
  genConfig.systemInstruction = systemParts.join("\n");
70
124
  }
71
- if (tools.length > 0) {
72
- genConfig.tools = [{ functionDeclarations: tools }];
125
+ const hostedEntries = googleWebSearchEntries(config.hostedTools);
126
+ if (tools.length > 0 || hostedEntries.length > 0) {
127
+ const toolGroups = [];
128
+ if (tools.length > 0) {
129
+ toolGroups.push({ functionDeclarations: tools });
130
+ }
131
+ for (const entry of hostedEntries) {
132
+ toolGroups.push(entry);
133
+ }
134
+ genConfig.tools = toolGroups;
73
135
  }
74
136
  if (config.responseFormat) {
75
137
  genConfig.responseMimeType = "application/json";
@@ -88,6 +150,18 @@ export class SmolGoogle extends BaseClient {
88
150
  ...sanitizeAttributes(config.rawAttributes),
89
151
  };
90
152
  }
153
+ rethrowAsSmolError(error) {
154
+ const http = { ...extractHttpErrorFields(error), cause: error };
155
+ const msg = (error.message || "").toLowerCase();
156
+ if (msg.includes("token") &&
157
+ (msg.includes("exceed") || msg.includes("too long") || msg.includes("limit"))) {
158
+ throw new SmolContextWindowExceededError(error.message, http);
159
+ }
160
+ if (http.status !== undefined) {
161
+ throw smolErrorForStatus(error.message, http);
162
+ }
163
+ throw error;
164
+ }
91
165
  async _textSync(config) {
92
166
  const signal = this.getAbortSignal(config);
93
167
  const request = {
@@ -180,14 +254,7 @@ export class SmolGoogle extends BaseClient {
180
254
  result = await this.client.models.generateContent(request);
181
255
  }
182
256
  catch (error) {
183
- const msg = (error.message || "").toLowerCase();
184
- if (msg.includes("token") &&
185
- (msg.includes("exceed") ||
186
- msg.includes("too long") ||
187
- msg.includes("limit"))) {
188
- throw new SmolContextWindowExceededError(error.message);
189
- }
190
- throw error;
257
+ this.rethrowAsSmolError(error);
191
258
  }
192
259
  this.logger.debug("Response from Google Gemini:", JSON.stringify(result, null, 2));
193
260
  this.statelogClient?.promptResponse(result);
@@ -226,15 +293,23 @@ export class SmolGoogle extends BaseClient {
226
293
  const output = textContent || null;
227
294
  // Extract usage and calculate cost
228
295
  const { usage, cost } = this.calculateUsageAndCost(result.usageMetadata);
296
+ const parsed = parseGoogleHostedTools(result, "google", request.model);
297
+ const { results: hostedToolResults, cost: finalCost } = applyHostedToolCost(parsed, cost, request.model, this.config.modelData);
229
298
  // Return the response, updating the chat history
230
- return success({
299
+ const promptResult = {
231
300
  output,
232
301
  toolCalls,
233
- ...(thinkingBlocks.length > 0 && { thinkingBlocks }),
234
302
  usage,
235
- cost,
303
+ cost: finalCost,
236
304
  model: request.model,
237
- });
305
+ };
306
+ if (thinkingBlocks.length > 0) {
307
+ promptResult.thinkingBlocks = thinkingBlocks;
308
+ }
309
+ if (hostedToolResults.length > 0) {
310
+ promptResult.hostedToolResults = hostedToolResults;
311
+ }
312
+ return success(promptResult);
238
313
  }
239
314
  async *_textStream(config) {
240
315
  const signal = this.getAbortSignal(config);
@@ -257,14 +332,7 @@ export class SmolGoogle extends BaseClient {
257
332
  stream = await this.client.models.generateContentStream(request);
258
333
  }
259
334
  catch (error) {
260
- const msg = (error.message || "").toLowerCase();
261
- if (msg.includes("token") &&
262
- (msg.includes("exceed") ||
263
- msg.includes("too long") ||
264
- msg.includes("limit"))) {
265
- throw new SmolContextWindowExceededError(error.message);
266
- }
267
- throw error;
335
+ this.rethrowAsSmolError(error);
268
336
  }
269
337
  let content = "";
270
338
  const toolCallsMap = new Map();
@@ -12,6 +12,7 @@ export declare class SmolOllama extends BaseClient implements SmolClient {
12
12
  getClient(): Ollama;
13
13
  getModel(): ModelName;
14
14
  private calculateUsageAndCost;
15
+ private rethrowAsSmolError;
15
16
  _textSync(config: SmolConfig): Promise<Result<PromptResult>>;
16
17
  _textStream(config: SmolConfig): AsyncGenerator<StreamChunk>;
17
18
  }
@@ -5,7 +5,8 @@ import { success, } from "../types.js";
5
5
  import { zodToGoogleTool } from "../util/tool.js";
6
6
  import { sanitizeAttributes } from "../util/util.js";
7
7
  import { BaseClient } from "./baseClient.js";
8
- import { SmolContextWindowExceededError } from "../smolError.js";
8
+ import { SmolContextWindowExceededError, smolErrorForStatus, } from "../smolError.js";
9
+ import { extractHttpErrorFields } from "../util/httpError.js";
9
10
  import { Model } from "../model.js";
10
11
  export const DEFAULT_OLLAMA_HOST = "http://localhost:11434";
11
12
  export class SmolOllama extends BaseClient {
@@ -15,7 +16,7 @@ export class SmolOllama extends BaseClient {
15
16
  constructor(config) {
16
17
  super(config);
17
18
  this.logger = getLogger();
18
- this.model = new Model(config.model);
19
+ this.model = new Model(config.model, undefined, config.modelData);
19
20
  if (config.ollamaApiKey) {
20
21
  this.client = new Ollama({
21
22
  host: "https://cloud.ollama.com",
@@ -51,6 +52,17 @@ export class SmolOllama extends BaseClient {
51
52
  }
52
53
  return { usage, cost };
53
54
  }
55
+ rethrowAsSmolError(error) {
56
+ const http = { ...extractHttpErrorFields(error), cause: error };
57
+ const msg = (error.message || "").toLowerCase();
58
+ if (msg.includes("context length") || msg.includes("context window")) {
59
+ throw new SmolContextWindowExceededError(error.message, http);
60
+ }
61
+ if (http.status !== undefined) {
62
+ throw smolErrorForStatus(error.message, http);
63
+ }
64
+ throw error;
65
+ }
54
66
  async _textSync(config) {
55
67
  const messages = config.messages.map((msg) => msg.toOpenAIMessage());
56
68
  const tools = (config.tools || []).map((tool) => {
@@ -82,11 +94,7 @@ export class SmolOllama extends BaseClient {
82
94
  result = await this.client.chat(request);
83
95
  }
84
96
  catch (error) {
85
- const msg = (error.message || "").toLowerCase();
86
- if (msg.includes("context length") || msg.includes("context window")) {
87
- throw new SmolContextWindowExceededError(error.message);
88
- }
89
- throw error;
97
+ this.rethrowAsSmolError(error);
90
98
  }
91
99
  finally {
92
100
  if (signal && abortHandler) {
@@ -201,6 +209,9 @@ export class SmolOllama extends BaseClient {
201
209
  },
202
210
  };
203
211
  }
212
+ catch (error) {
213
+ this.rethrowAsSmolError(error);
214
+ }
204
215
  finally {
205
216
  if (signal && abortHandler) {
206
217
  signal.removeEventListener("abort", abortHandler);
@@ -4,7 +4,8 @@ import { ToolCall } from "../classes/ToolCall.js";
4
4
  import { isFunctionToolCall, sanitizeAttributes } from "../util/util.js";
5
5
  import { getLogger } from "../util/logger.js";
6
6
  import { BaseClient } from "./baseClient.js";
7
- import { SmolContentPolicyError, SmolContextWindowExceededError, } from "../smolError.js";
7
+ import { SmolContentPolicyError, SmolContextWindowExceededError, smolErrorForStatus, } from "../smolError.js";
8
+ import { extractHttpErrorFields } from "../util/httpError.js";
8
9
  import { zodToOpenAITool } from "../util/tool.js";
9
10
  import { Model } from "../model.js";
10
11
  export class SmolOpenAi extends BaseClient {
@@ -18,7 +19,7 @@ export class SmolOpenAi extends BaseClient {
18
19
  }
19
20
  this.client = new OpenAI({ apiKey: config.openAiApiKey });
20
21
  this.logger = getLogger();
21
- this.model = new Model(config.model);
22
+ this.model = new Model(config.model, undefined, config.modelData);
22
23
  }
23
24
  getClient() {
24
25
  return this.client;
@@ -74,12 +75,14 @@ export class SmolOpenAi extends BaseClient {
74
75
  }
75
76
  rethrowAsSmolError(error) {
76
77
  if (error instanceof OpenAI.APIError) {
78
+ const http = { ...extractHttpErrorFields(error), cause: error };
77
79
  if (error.code === "context_length_exceeded") {
78
- throw new SmolContextWindowExceededError(error.message);
80
+ throw new SmolContextWindowExceededError(error.message, http);
79
81
  }
80
82
  if (error.code === "content_policy_violation") {
81
- throw new SmolContentPolicyError(error.message);
83
+ throw new SmolContentPolicyError(error.message, http);
82
84
  }
85
+ throw smolErrorForStatus(error.message, http);
83
86
  }
84
87
  throw error;
85
88
  }
@@ -2,7 +2,10 @@ import OpenAI from "openai";
2
2
  import { PromptResult, Result, SmolClient, SmolConfig, StreamChunk } from "../types.js";
3
3
  import { BaseClient } from "./baseClient.js";
4
4
  import { ModelName } from "../models.js";
5
+ import { HostedToolResult } from "../types.js";
5
6
  export type SmolOpenAiResponsesConfig = SmolConfig;
7
+ export declare function openaiResponsesWebSearchEntries(hostedTools?: string[]): any[];
8
+ export declare function parseOpenAIResponsesHostedTools(response: any, provider: string): HostedToolResult[];
6
9
  export declare class SmolOpenAiResponses extends BaseClient implements SmolClient {
7
10
  private client;
8
11
  private logger;
@@ -5,8 +5,48 @@ import { getLogger } from "../util/logger.js";
5
5
  import { BaseClient } from "./baseClient.js";
6
6
  import { zodToOpenAIResponsesTool } from "../util/tool.js";
7
7
  import { sanitizeAttributes } from "../util/util.js";
8
+ import { WEB_SEARCH, webSearchResult, applyHostedToolCost } from "../util/hostedTools.js";
8
9
  import { Model } from "../model.js";
9
- import { SmolContentPolicyError, SmolContextWindowExceededError, } from "../smolError.js";
10
+ import { SmolContentPolicyError, SmolContextWindowExceededError, smolErrorForStatus, } from "../smolError.js";
11
+ import { extractHttpErrorFields } from "../util/httpError.js";
12
+ export function openaiResponsesWebSearchEntries(hostedTools) {
13
+ if (hostedTools && hostedTools.includes(WEB_SEARCH)) {
14
+ return [{ type: "web_search" }];
15
+ }
16
+ return [];
17
+ }
18
+ export function parseOpenAIResponsesHostedTools(response, provider) {
19
+ const queries = [];
20
+ const sources = [];
21
+ const citations = [];
22
+ const raw = [];
23
+ let callCount = 0;
24
+ for (const item of response.output || []) {
25
+ if (item.type === "web_search_call") {
26
+ raw.push(item);
27
+ callCount += 1;
28
+ // action.query is present for `search` actions but not always (e.g. open_page).
29
+ const query = item.action?.query;
30
+ if (typeof query === "string") {
31
+ queries.push(query);
32
+ }
33
+ }
34
+ else if (item.type === "message") {
35
+ for (const part of item.content || []) {
36
+ for (const ann of part.annotations || []) {
37
+ if (ann.type === "url_citation" && typeof ann.url === "string") {
38
+ citations.push({ url: ann.url, title: ann.title, startIndex: ann.start_index, endIndex: ann.end_index });
39
+ sources.push({ url: ann.url, title: ann.title });
40
+ }
41
+ }
42
+ }
43
+ }
44
+ }
45
+ if (callCount === 0 && citations.length === 0) {
46
+ return [];
47
+ }
48
+ return [webSearchResult(provider, { queries, sources, citations, callCount, raw })];
49
+ }
10
50
  export class SmolOpenAiResponses extends BaseClient {
11
51
  client;
12
52
  logger;
@@ -18,7 +58,7 @@ export class SmolOpenAiResponses extends BaseClient {
18
58
  }
19
59
  this.client = new OpenAI({ apiKey: config.openAiApiKey });
20
60
  this.logger = getLogger();
21
- this.model = new Model(config.model);
61
+ this.model = new Model(config.model, undefined, config.modelData);
22
62
  }
23
63
  getClient() {
24
64
  return this.client;
@@ -61,6 +101,11 @@ export class SmolOpenAiResponses extends BaseClient {
61
101
  description: tool.description,
62
102
  }));
63
103
  }
104
+ const hostedEntries = openaiResponsesWebSearchEntries(config.hostedTools);
105
+ if (hostedEntries.length > 0) {
106
+ const existing = Array.isArray(request.tools) ? request.tools : [];
107
+ request.tools = [...existing, ...hostedEntries];
108
+ }
64
109
  if (config.temperature !== undefined) {
65
110
  request.temperature = config.temperature;
66
111
  }
@@ -107,12 +152,14 @@ export class SmolOpenAiResponses extends BaseClient {
107
152
  }
108
153
  rethrowAsSmolError(error) {
109
154
  if (error instanceof OpenAI.APIError) {
155
+ const http = { ...extractHttpErrorFields(error), cause: error };
110
156
  if (error.code === "context_length_exceeded") {
111
- throw new SmolContextWindowExceededError(error.message);
157
+ throw new SmolContextWindowExceededError(error.message, http);
112
158
  }
113
159
  if (error.code === "content_policy_violation") {
114
- throw new SmolContentPolicyError(error.message);
160
+ throw new SmolContentPolicyError(error.message, http);
115
161
  }
162
+ throw smolErrorForStatus(error.message, http);
116
163
  }
117
164
  throw error;
118
165
  }
@@ -141,13 +188,19 @@ export class SmolOpenAiResponses extends BaseClient {
141
188
  }
142
189
  }
143
190
  const { usage, cost } = this.calculateUsageAndCost(response.usage);
144
- return success({
191
+ const parsed = parseOpenAIResponsesHostedTools(response, "openai-responses");
192
+ const { results: hostedToolResults, cost: finalCost } = applyHostedToolCost(parsed, cost, this.getModel(), this.config.modelData);
193
+ const result = {
145
194
  output,
146
195
  toolCalls,
147
196
  usage,
148
- cost,
197
+ cost: finalCost,
149
198
  model: this.getModel(),
150
- });
199
+ };
200
+ if (hostedToolResults.length > 0) {
201
+ result.hostedToolResults = hostedToolResults;
202
+ }
203
+ return success(result);
151
204
  }
152
205
  async *_textStream(config) {
153
206
  const request = this.buildRequest(config);
@@ -16,7 +16,7 @@ export async function openaiEmbed(inputs, config, apiKey) {
16
16
  .sort((a, b) => a.index - b.index)
17
17
  .map((d) => d.embedding);
18
18
  const inputTokens = response.usage.prompt_tokens;
19
- const costEstimate = calculateEmbeddingCost(config.model, inputTokens);
19
+ const costEstimate = calculateEmbeddingCost(config.model, inputTokens, config.modelData);
20
20
  return success({
21
21
  embeddings,
22
22
  model: response.model,
@@ -28,8 +28,8 @@ export async function openaiEmbed(inputs, config, apiKey) {
28
28
  return failure(err instanceof Error ? err.message : "OpenAI embedding request failed");
29
29
  }
30
30
  }
31
- function calculateEmbeddingCost(modelName, inputTokens) {
32
- const model = getModel(modelName);
31
+ function calculateEmbeddingCost(modelName, inputTokens, modelData) {
32
+ const model = getModel(modelName, modelData);
33
33
  if (!model || !isEmbeddingsModel(model) || !model.tokenCost)
34
34
  return undefined;
35
35
  const inputCost = round((inputTokens * model.tokenCost) / 1_000_000, 6);
package/dist/embed.d.ts CHANGED
@@ -1,16 +1,17 @@
1
- import { Provider } from "./models.js";
1
+ import type { ModelDataBlob } from "./modelData.js";
2
2
  import { Result } from "./types/result.js";
3
3
  import { TokenUsage } from "./types/tokenUsage.js";
4
4
  import { CostEstimate } from "./types/costEstimate.js";
5
5
  export type EmbedConfig = {
6
6
  model: string;
7
- provider?: Provider;
7
+ provider?: string;
8
8
  dimensions?: number;
9
9
  openAiApiKey?: string;
10
10
  googleApiKey?: string;
11
11
  ollamaApiKey?: string;
12
12
  ollamaHost?: string;
13
13
  metadata?: Record<string, unknown>;
14
+ modelData?: ModelDataBlob;
14
15
  };
15
16
  export type EmbedResult = {
16
17
  embeddings: number[][];
@@ -18,4 +19,6 @@ export type EmbedResult = {
18
19
  tokenUsage?: TokenUsage;
19
20
  costEstimate?: CostEstimate;
20
21
  };
22
+ export type EmbedProvider = (inputs: string[], config: EmbedConfig) => Promise<Result<EmbedResult>>;
23
+ export declare function registerEmbeddingProvider(name: string, fn: EmbedProvider): void;
21
24
  export declare function embed(input: string | string[], config: EmbedConfig): Promise<Result<EmbedResult>>;
package/dist/embed.js CHANGED
@@ -3,11 +3,17 @@ import { resolveProvider, resolveApiKey } from "./util/provider.js";
3
3
  import { openaiEmbed } from "./embed/openai.js";
4
4
  import { googleEmbed } from "./embed/google.js";
5
5
  import { ollamaEmbed } from "./embed/ollama.js";
6
+ // Null-prototype so provider names like "toString"/"__proto__" can't collide
7
+ // with Object.prototype or pollute the registry.
8
+ const registeredEmbedProviders = Object.create(null);
9
+ export function registerEmbeddingProvider(name, fn) {
10
+ registeredEmbedProviders[name] = fn;
11
+ }
6
12
  export async function embed(input, config) {
7
13
  const inputs = Array.isArray(input) ? input : [input];
8
14
  let provider;
9
15
  try {
10
- provider = resolveProvider(config.model, config.provider);
16
+ provider = resolveProvider(config.model, config.provider, config.modelData);
11
17
  }
12
18
  catch (err) {
13
19
  return failure(err instanceof Error ? err.message : "Failed to resolve provider");
@@ -29,7 +35,12 @@ export async function embed(input, config) {
29
35
  }
30
36
  case "ollama":
31
37
  return ollamaEmbed(inputs, config, apiKey, config.ollamaHost);
32
- default:
33
- return failure(`Provider "${provider}" does not support embeddings`);
38
+ default: {
39
+ const custom = registeredEmbedProviders[provider];
40
+ if (custom) {
41
+ return custom(inputs, config);
42
+ }
43
+ return failure(`Provider "${provider}" does not support embeddings. Register one with registerEmbeddingProvider(name, fn).`);
44
+ }
34
45
  }
35
46
  }
@@ -41,15 +41,15 @@ export async function googleImage(input, config, apiKey) {
41
41
  return success({
42
42
  images,
43
43
  model: config.model,
44
- costEstimate: calculateGoogleImageCost(config.model, images.length),
44
+ costEstimate: calculateGoogleImageCost(config.model, images.length, config.modelData),
45
45
  });
46
46
  }
47
47
  catch (err) {
48
48
  return failure(err instanceof Error ? err.message : "Google image request failed");
49
49
  }
50
50
  }
51
- function calculateGoogleImageCost(modelName, imageCount) {
52
- const model = getModel(modelName);
51
+ function calculateGoogleImageCost(modelName, imageCount, modelData) {
52
+ const model = getModel(modelName, modelData);
53
53
  if (!model || !isImageModel(model) || !model.costPerImage)
54
54
  return undefined;
55
55
  const totalCost = round(model.costPerImage * imageCount, COST_DECIMAL_PLACES);
@@ -24,8 +24,8 @@ export async function openaiImage(input, config, apiKey) {
24
24
  }));
25
25
  const tokenUsage = extractUsage(response);
26
26
  const costEstimate = tokenUsage
27
- ? calculateImageCost(config.model, tokenUsage)
28
- : calculatePerImageCost(config.model, images.length);
27
+ ? calculateImageCost(config.model, tokenUsage, config.modelData)
28
+ : calculatePerImageCost(config.model, images.length, config.modelData);
29
29
  return success({
30
30
  images,
31
31
  model: config.model,
@@ -97,8 +97,8 @@ function extractUsage(response) {
97
97
  totalTokens: u.total_tokens,
98
98
  };
99
99
  }
100
- function calculateImageCost(modelName, usage) {
101
- const model = getModel(modelName);
100
+ function calculateImageCost(modelName, usage, modelData) {
101
+ const model = getModel(modelName, modelData);
102
102
  if (!model || !isImageModel(model))
103
103
  return undefined;
104
104
  const totalIn = usage.inputTokens ?? 0;
@@ -131,8 +131,8 @@ function calculateImageCost(modelName, usage) {
131
131
  currency: "USD",
132
132
  };
133
133
  }
134
- function calculatePerImageCost(modelName, imageCount) {
135
- const model = getModel(modelName);
134
+ function calculatePerImageCost(modelName, imageCount, modelData) {
135
+ const model = getModel(modelName, modelData);
136
136
  if (!model || !isImageModel(model) || !model.costPerImage)
137
137
  return undefined;
138
138
  const totalCost = round(model.costPerImage * imageCount, COST_DECIMAL_PLACES);
package/dist/image.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { Provider } from "./models.js";
1
+ import type { ModelDataBlob } from "./modelData.js";
2
2
  import { Result } from "./types/result.js";
3
3
  import { TokenUsage } from "./types/tokenUsage.js";
4
4
  import { CostEstimate } from "./types/costEstimate.js";
@@ -11,7 +11,7 @@ export type ImageInput = string | {
11
11
  };
12
12
  export type ImageConfig = {
13
13
  model: string;
14
- provider?: Provider;
14
+ provider?: string;
15
15
  n?: number;
16
16
  size?: string;
17
17
  quality?: "low" | "medium" | "high" | "auto";
@@ -20,6 +20,7 @@ export type ImageConfig = {
20
20
  openAiApiKey?: string;
21
21
  googleApiKey?: string;
22
22
  metadata?: Record<string, unknown>;
23
+ modelData?: ModelDataBlob;
23
24
  };
24
25
  export type GeneratedImage = {
25
26
  data: Uint8Array;
@@ -32,4 +33,6 @@ export type ImageGenResult = {
32
33
  tokenUsage?: TokenUsage;
33
34
  costEstimate?: CostEstimate;
34
35
  };
36
+ export type ImageProvider = (input: ImageInput, config: ImageConfig) => Promise<Result<ImageGenResult>>;
37
+ export declare function registerImageProvider(name: string, fn: ImageProvider): void;
35
38
  export declare function image(input: ImageInput, config: ImageConfig): Promise<Result<ImageGenResult>>;
package/dist/image.js CHANGED
@@ -2,10 +2,16 @@ import { failure } from "./types/result.js";
2
2
  import { resolveProvider, resolveApiKey } from "./util/provider.js";
3
3
  import { openaiImage } from "./image/openai.js";
4
4
  import { googleImage } from "./image/google.js";
5
+ // Null-prototype so provider names like "toString"/"__proto__" can't collide
6
+ // with Object.prototype or pollute the registry.
7
+ const registeredImageProviders = Object.create(null);
8
+ export function registerImageProvider(name, fn) {
9
+ registeredImageProviders[name] = fn;
10
+ }
5
11
  export async function image(input, config) {
6
12
  let provider;
7
13
  try {
8
- provider = resolveProvider(config.model, config.provider);
14
+ provider = resolveProvider(config.model, config.provider, config.modelData);
9
15
  }
10
16
  catch (err) {
11
17
  return failure(err instanceof Error ? err.message : "Failed to resolve provider");
@@ -31,7 +37,12 @@ export async function image(input, config) {
31
37
  }
32
38
  return googleImage(input, config, apiKey);
33
39
  }
34
- default:
35
- return failure(`Provider "${provider}" does not support image generation`);
40
+ default: {
41
+ const custom = registeredImageProviders[provider];
42
+ if (custom) {
43
+ return custom(input, config);
44
+ }
45
+ return failure(`Provider "${provider}" does not support image generation. Register one with registerImageProvider(name, fn).`);
46
+ }
36
47
  }
37
48
  }
package/dist/index.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  export * from "./client.js";
2
2
  export * from "./types.js";
3
3
  export * from "./models.js";
4
+ export * from "./modelData.js";
4
5
  export * from "./model.js";
5
6
  export * from "./smolError.js";
6
7
  export * from "./util/util.js";
package/dist/index.js CHANGED
@@ -1,6 +1,7 @@
1
1
  export * from "./client.js";
2
2
  export * from "./types.js";
3
3
  export * from "./models.js";
4
+ export * from "./modelData.js";
4
5
  export * from "./model.js";
5
6
  export * from "./smolError.js";
6
7
  export * from "./util/util.js";
package/dist/model.d.ts CHANGED
@@ -1,9 +1,11 @@
1
1
  import { ModelName, Provider } from "./models.js";
2
2
  import { ModelLike } from "./types.js";
3
+ import type { ModelDataBlob } from "./modelData.js";
3
4
  export declare class Model {
4
5
  private model;
5
6
  private provider?;
6
- constructor(model: ModelName, provider?: Provider);
7
+ private modelData?;
8
+ constructor(model: ModelName, provider?: Provider, modelData?: ModelDataBlob);
7
9
  getModel(): ModelName;
8
10
  getProvider(): Provider | undefined;
9
11
  private lookupProvider;
@@ -22,5 +24,5 @@ export declare class Model {
22
24
  } | null;
23
25
  toString(): string;
24
26
  toJSON(): ModelName;
25
- static create(model: ModelLike, provider?: Provider): Model;
27
+ static create(model: ModelLike, provider?: Provider, modelData?: ModelDataBlob): Model;
26
28
  }