smoltalk 0.2.2 → 0.3.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/dist/client.js CHANGED
@@ -11,6 +11,7 @@ import { SmolOpenAi } from "./clients/openai.js";
11
11
  import { SmolOpenAiResponses } from "./clients/openaiResponses.js";
12
12
  import { getModel, isTextModel } from "./models.js";
13
13
  import { SmolError } from "./smolError.js";
14
+ import { resolveApiKey, resolveProvider } from "./util/provider.js";
14
15
  const registeredProviders = {};
15
16
  export function registerProvider(providerName, clientClass) {
16
17
  registeredProviders[providerName] = clientClass;
@@ -23,22 +24,20 @@ export function unregisterProvider(providerName) {
23
24
  return false;
24
25
  }
25
26
  export function getClient(config) {
26
- let provider = config.provider;
27
27
  const modelName = config.model;
28
- if (!provider) {
28
+ const provider = resolveProvider(modelName, config.provider);
29
+ // For getClient, validate that the model is a text model when no explicit
30
+ // provider is given (since this factory only returns text-generation clients).
31
+ if (!config.provider) {
29
32
  const model = getModel(modelName);
30
- if (model === undefined) {
31
- throw new SmolError(`Model ${modelName} is not recognized. Please specify a known model, or explicitly set the provider option in the config.`);
32
- }
33
- if (!isTextModel(model)) {
33
+ if (model && !isTextModel(model)) {
34
34
  throw new SmolError(`Only text models are supported currently. ${modelName} is a ${model?.type} model.`);
35
35
  }
36
- provider = model.provider;
37
36
  }
38
37
  const resolvedKeys = {
39
- openAiApiKey: config.openAiApiKey || process.env.OPENAI_API_KEY,
40
- googleApiKey: config.googleApiKey || process.env.GEMINI_API_KEY,
41
- anthropicApiKey: config.anthropicApiKey || process.env.ANTHROPIC_API_KEY,
38
+ openAiApiKey: resolveApiKey("openai", config),
39
+ googleApiKey: resolveApiKey("google", config),
40
+ anthropicApiKey: resolveApiKey("anthropic", config),
42
41
  };
43
42
  const clientConfig = {
44
43
  messages: [],
@@ -2,6 +2,7 @@ import { userMessage, assistantMessage } from "../classes/message/index.js";
2
2
  import { getLogger } from "../util/logger.js";
3
3
  import { SmolStructuredOutputError } from "../smolError.js";
4
4
  import { getStatelogClient } from "../statelogClient.js";
5
+ import { stripCodeFence } from "../util/util.js";
5
6
  import { success, } from "../types.js";
6
7
  import { z } from "zod";
7
8
  const DEFAULT_NUM_RETRIES = 2;
@@ -143,10 +144,7 @@ export class BaseClient {
143
144
  }
144
145
  // 2. String → try JSON.parse, then recurse
145
146
  if (typeof rawValue === "string") {
146
- const stripped = rawValue
147
- .trim()
148
- .replace(/^```json\s*/, "")
149
- .replace(/```\s*$/, "");
147
+ const stripped = stripCodeFence(rawValue);
150
148
  try {
151
149
  return this.extractResponse(promptConfig, JSON.parse(stripped), schema, depth + 1);
152
150
  }
@@ -0,0 +1,3 @@
1
+ import { EmbedConfig, EmbedResult } from "../embed.js";
2
+ import { Result } from "../types/result.js";
3
+ export declare function googleEmbed(inputs: string[], config: EmbedConfig, apiKey: string): Promise<Result<EmbedResult>>;
@@ -0,0 +1,25 @@
1
+ import { GoogleGenAI } from "@google/genai";
2
+ import { success, failure } from "../types/result.js";
3
+ export async function googleEmbed(inputs, config, apiKey) {
4
+ try {
5
+ const client = new GoogleGenAI({ apiKey });
6
+ const response = await client.models.embedContent({
7
+ model: config.model,
8
+ contents: inputs,
9
+ ...(config.dimensions !== undefined
10
+ ? { config: { outputDimensionality: config.dimensions } }
11
+ : {}),
12
+ });
13
+ const embeddings = (response.embeddings ?? []).map((e) => e.values ?? []);
14
+ return success({
15
+ embeddings,
16
+ model: config.model,
17
+ // Google Gemini embeddings API does not return token usage in the
18
+ // response. Cost cannot be auto-computed without a separate
19
+ // countTokens() call.
20
+ });
21
+ }
22
+ catch (err) {
23
+ return failure(err instanceof Error ? err.message : "Google embedding request failed");
24
+ }
25
+ }
@@ -0,0 +1,3 @@
1
+ import { EmbedConfig, EmbedResult } from "../embed.js";
2
+ import { Result } from "../types/result.js";
3
+ export declare function ollamaEmbed(inputs: string[], config: EmbedConfig, apiKey?: string, ollamaHost?: string): Promise<Result<EmbedResult>>;
@@ -0,0 +1,35 @@
1
+ import { Ollama } from "ollama";
2
+ import { success, failure } from "../types/result.js";
3
+ import { DEFAULT_OLLAMA_HOST } from "../clients/ollama.js";
4
+ export async function ollamaEmbed(inputs, config, apiKey, ollamaHost) {
5
+ try {
6
+ let client;
7
+ if (apiKey) {
8
+ client = new Ollama({
9
+ host: "https://cloud.ollama.com",
10
+ headers: { Authorization: "Bearer " + apiKey },
11
+ });
12
+ }
13
+ else {
14
+ client = new Ollama({ host: ollamaHost || DEFAULT_OLLAMA_HOST });
15
+ }
16
+ const response = await client.embed({
17
+ model: config.model,
18
+ input: inputs,
19
+ ...(config.dimensions !== undefined
20
+ ? { dimensions: config.dimensions }
21
+ : {}),
22
+ });
23
+ return success({
24
+ embeddings: response.embeddings,
25
+ model: response.model,
26
+ tokenUsage: {
27
+ inputTokens: response.prompt_eval_count ?? 0,
28
+ outputTokens: 0,
29
+ },
30
+ });
31
+ }
32
+ catch (err) {
33
+ return failure(err instanceof Error ? err.message : "Ollama embedding request failed");
34
+ }
35
+ }
@@ -0,0 +1,3 @@
1
+ import { EmbedConfig, EmbedResult } from "../embed.js";
2
+ import { Result } from "../types/result.js";
3
+ export declare function openaiEmbed(inputs: string[], config: EmbedConfig, apiKey: string): Promise<Result<EmbedResult>>;
@@ -0,0 +1,42 @@
1
+ import OpenAI from "openai";
2
+ import { success, failure } from "../types/result.js";
3
+ import { getModel, isEmbeddingsModel } from "../models.js";
4
+ import { round } from "../util/util.js";
5
+ export async function openaiEmbed(inputs, config, apiKey) {
6
+ try {
7
+ const client = new OpenAI({ apiKey });
8
+ const response = await client.embeddings.create({
9
+ model: config.model,
10
+ input: inputs,
11
+ ...(config.dimensions !== undefined
12
+ ? { dimensions: config.dimensions }
13
+ : {}),
14
+ });
15
+ const embeddings = [...response.data]
16
+ .sort((a, b) => a.index - b.index)
17
+ .map((d) => d.embedding);
18
+ const inputTokens = response.usage.prompt_tokens;
19
+ const costEstimate = calculateEmbeddingCost(config.model, inputTokens);
20
+ return success({
21
+ embeddings,
22
+ model: response.model,
23
+ tokenUsage: { inputTokens, outputTokens: 0 },
24
+ costEstimate,
25
+ });
26
+ }
27
+ catch (err) {
28
+ return failure(err instanceof Error ? err.message : "OpenAI embedding request failed");
29
+ }
30
+ }
31
+ function calculateEmbeddingCost(modelName, inputTokens) {
32
+ const model = getModel(modelName);
33
+ if (!model || !isEmbeddingsModel(model) || !model.tokenCost)
34
+ return undefined;
35
+ const inputCost = round((inputTokens * model.tokenCost) / 1_000_000, 6);
36
+ return {
37
+ inputCost,
38
+ outputCost: 0,
39
+ totalCost: inputCost,
40
+ currency: "USD",
41
+ };
42
+ }
@@ -0,0 +1,21 @@
1
+ import { Provider } from "./models.js";
2
+ import { Result } from "./types/result.js";
3
+ import { TokenUsage } from "./types/tokenUsage.js";
4
+ import { CostEstimate } from "./types/costEstimate.js";
5
+ export type EmbedConfig = {
6
+ model: string;
7
+ provider?: Provider;
8
+ dimensions?: number;
9
+ openAiApiKey?: string;
10
+ googleApiKey?: string;
11
+ ollamaApiKey?: string;
12
+ ollamaHost?: string;
13
+ metadata?: Record<string, unknown>;
14
+ };
15
+ export type EmbedResult = {
16
+ embeddings: number[][];
17
+ model: string;
18
+ tokenUsage?: TokenUsage;
19
+ costEstimate?: CostEstimate;
20
+ };
21
+ export declare function embed(input: string | string[], config: EmbedConfig): Promise<Result<EmbedResult>>;
package/dist/embed.js ADDED
@@ -0,0 +1,35 @@
1
+ import { failure } from "./types/result.js";
2
+ import { resolveProvider, resolveApiKey } from "./util/provider.js";
3
+ import { openaiEmbed } from "./embed/openai.js";
4
+ import { googleEmbed } from "./embed/google.js";
5
+ import { ollamaEmbed } from "./embed/ollama.js";
6
+ export async function embed(input, config) {
7
+ const inputs = Array.isArray(input) ? input : [input];
8
+ let provider;
9
+ try {
10
+ provider = resolveProvider(config.model, config.provider);
11
+ }
12
+ catch (err) {
13
+ return failure(err instanceof Error ? err.message : "Failed to resolve provider");
14
+ }
15
+ const apiKey = resolveApiKey(provider, config);
16
+ switch (provider) {
17
+ case "openai":
18
+ case "openai-responses": {
19
+ if (!apiKey) {
20
+ return failure("No OpenAI API key provided. Set openAiApiKey in config or the OPENAI_API_KEY environment variable.");
21
+ }
22
+ return openaiEmbed(inputs, config, apiKey);
23
+ }
24
+ case "google": {
25
+ if (!apiKey) {
26
+ return failure("No Google API key provided. Set googleApiKey in config or the GEMINI_API_KEY environment variable.");
27
+ }
28
+ return googleEmbed(inputs, config, apiKey);
29
+ }
30
+ case "ollama":
31
+ return ollamaEmbed(inputs, config, apiKey, config.ollamaHost);
32
+ default:
33
+ return failure(`Provider "${provider}" does not support embeddings`);
34
+ }
35
+ }
@@ -0,0 +1,3 @@
1
+ import { ImageInput, ImageConfig, ImageGenResult } from "../image.js";
2
+ import { Result } from "../types/result.js";
3
+ export declare function googleImage(input: ImageInput, config: ImageConfig, apiKey: string): Promise<Result<ImageGenResult>>;
@@ -0,0 +1,57 @@
1
+ import { GoogleGenAI } from "@google/genai";
2
+ import { success, failure } from "../types/result.js";
3
+ import { getModel, isImageModel } from "../models.js";
4
+ import { normalizeImageRef } from "../util/imageRef.js";
5
+ import { COST_DECIMAL_PLACES, round } from "../util/util.js";
6
+ export async function googleImage(input, config, apiKey) {
7
+ try {
8
+ const normalized = typeof input === "string" ? { prompt: input } : input;
9
+ const client = new GoogleGenAI({ apiKey });
10
+ const parts = [{ text: normalized.prompt }];
11
+ if (normalized.images && normalized.images.length > 0) {
12
+ const normalizedImages = await Promise.all(normalized.images.map((ref) => normalizeImageRef(ref)));
13
+ for (const img of normalizedImages) {
14
+ parts.push({
15
+ inlineData: {
16
+ mimeType: img.mimeType,
17
+ data: Buffer.from(img.data).toString("base64"),
18
+ },
19
+ });
20
+ }
21
+ }
22
+ const response = await client.models.generateContent({
23
+ model: config.model,
24
+ contents: [{ role: "user", parts }],
25
+ config: {
26
+ responseModalities: ["IMAGE", "TEXT"],
27
+ ...(config.metadata ?? {}),
28
+ },
29
+ });
30
+ const images = [];
31
+ for (const candidate of response.candidates ?? []) {
32
+ for (const part of candidate.content?.parts ?? []) {
33
+ if (part.inlineData?.data && part.inlineData?.mimeType) {
34
+ images.push({
35
+ data: new Uint8Array(Buffer.from(part.inlineData.data, "base64")),
36
+ mimeType: part.inlineData.mimeType,
37
+ });
38
+ }
39
+ }
40
+ }
41
+ return success({
42
+ images,
43
+ model: config.model,
44
+ costEstimate: calculateGoogleImageCost(config.model, images.length),
45
+ });
46
+ }
47
+ catch (err) {
48
+ return failure(err instanceof Error ? err.message : "Google image request failed");
49
+ }
50
+ }
51
+ function calculateGoogleImageCost(modelName, imageCount) {
52
+ const model = getModel(modelName);
53
+ if (!model || !isImageModel(model) || !model.costPerImage)
54
+ return undefined;
55
+ const totalCost = round(model.costPerImage * imageCount, COST_DECIMAL_PLACES);
56
+ return { inputCost: 0, outputCost: totalCost, totalCost, currency: "USD" };
57
+ }
@@ -0,0 +1,3 @@
1
+ import { ImageInput, ImageConfig, ImageGenResult } from "../image.js";
2
+ import { Result } from "../types/result.js";
3
+ export declare function openaiImage(input: ImageInput, config: ImageConfig, apiKey: string): Promise<Result<ImageGenResult>>;
@@ -0,0 +1,140 @@
1
+ import OpenAI from "openai";
2
+ import { toFile } from "openai/uploads";
3
+ import { success, failure } from "../types/result.js";
4
+ import { getModel, isImageModel } from "../models.js";
5
+ import { normalizeImageRef } from "../util/imageRef.js";
6
+ import { COST_DECIMAL_PLACES, omitUndefined, round, tokenCost, } from "../util/util.js";
7
+ export async function openaiImage(input, config, apiKey) {
8
+ try {
9
+ const normalized = typeof input === "string" ? { prompt: input } : input;
10
+ const hasImages = !!(normalized.images && normalized.images.length > 0);
11
+ if (normalized.mask && !hasImages) {
12
+ return failure("A mask was provided without any input images. Masks are only valid for image edits — pass at least one entry in `images` alongside the mask.");
13
+ }
14
+ const client = new OpenAI({ apiKey });
15
+ const baseParams = buildBaseParams(config, normalized.prompt);
16
+ const response = hasImages
17
+ ? await callEdit(client, baseParams, normalized)
18
+ : await client.images.generate(baseParams);
19
+ const mimeType = mimeFromFormat(config.outputFormat) ?? "image/png";
20
+ const images = (response.data ?? []).map((d) => ({
21
+ data: new Uint8Array(Buffer.from(d.b64_json, "base64")),
22
+ mimeType,
23
+ revisedPrompt: d.revised_prompt,
24
+ }));
25
+ const tokenUsage = extractUsage(response);
26
+ const costEstimate = tokenUsage
27
+ ? calculateImageCost(config.model, tokenUsage)
28
+ : calculatePerImageCost(config.model, images.length);
29
+ return success({
30
+ images,
31
+ model: config.model,
32
+ tokenUsage,
33
+ costEstimate,
34
+ });
35
+ }
36
+ catch (err) {
37
+ return failure(err instanceof Error ? err.message : "OpenAI image request failed");
38
+ }
39
+ }
40
+ function buildBaseParams(config, prompt) {
41
+ return omitUndefined({
42
+ model: config.model,
43
+ prompt,
44
+ n: config.n,
45
+ size: config.size,
46
+ quality: config.quality,
47
+ output_format: config.outputFormat,
48
+ background: config.background,
49
+ ...(config.metadata ?? {}),
50
+ });
51
+ }
52
+ async function callEdit(client, baseParams, normalized) {
53
+ const imageFiles = await Promise.all((normalized.images ?? []).map(async (ref, i) => {
54
+ const n = await normalizeImageRef(ref);
55
+ return toFileFor(n, `image-${i}`);
56
+ }));
57
+ const maskFile = normalized.mask
58
+ ? await toFileFor(await normalizeImageRef(normalized.mask), "mask")
59
+ : undefined;
60
+ return client.images.edit(omitUndefined({
61
+ ...baseParams,
62
+ image: imageFiles.length === 1 ? imageFiles[0] : imageFiles,
63
+ mask: maskFile,
64
+ }));
65
+ }
66
+ async function toFileFor(img, baseName) {
67
+ return toFile(img.data, `${baseName}.${extFromMime(img.mimeType)}`, {
68
+ type: img.mimeType,
69
+ });
70
+ }
71
+ function extFromMime(mime) {
72
+ if (mime === "image/jpeg")
73
+ return "jpg";
74
+ if (mime === "image/webp")
75
+ return "webp";
76
+ return "png";
77
+ }
78
+ function mimeFromFormat(fmt) {
79
+ if (!fmt)
80
+ return undefined;
81
+ if (fmt === "jpeg")
82
+ return "image/jpeg";
83
+ if (fmt === "webp")
84
+ return "image/webp";
85
+ return "image/png";
86
+ }
87
+ function extractUsage(response) {
88
+ const u = response?.usage;
89
+ if (!u)
90
+ return undefined;
91
+ return {
92
+ inputTokens: u.input_tokens ?? 0,
93
+ outputTokens: u.output_tokens ?? 0,
94
+ cachedInputTokens: u.input_tokens_details?.cached_tokens,
95
+ inputImageTokens: u.input_tokens_details?.image_tokens,
96
+ inputTextTokens: u.input_tokens_details?.text_tokens,
97
+ totalTokens: u.total_tokens,
98
+ };
99
+ }
100
+ function calculateImageCost(modelName, usage) {
101
+ const model = getModel(modelName);
102
+ if (!model || !isImageModel(model))
103
+ return undefined;
104
+ const totalIn = usage.inputTokens ?? 0;
105
+ const cachedIn = usage.cachedInputTokens ?? 0;
106
+ const imgOut = usage.outputTokens ?? 0;
107
+ // Prefer the detailed breakdown if the API returned it; otherwise treat
108
+ // all non-cached input tokens as text input.
109
+ let textIn;
110
+ let imageIn;
111
+ if (usage.inputTextTokens !== undefined ||
112
+ usage.inputImageTokens !== undefined) {
113
+ textIn = Math.max(0, (usage.inputTextTokens ?? 0) - cachedIn);
114
+ imageIn = usage.inputImageTokens ?? 0;
115
+ }
116
+ else {
117
+ textIn = Math.max(0, totalIn - cachedIn);
118
+ imageIn = 0;
119
+ }
120
+ const textInputCost = tokenCost(textIn, model.inputTokenCost);
121
+ const imageInputCost = tokenCost(imageIn, model.inputImageTokenCost);
122
+ const cachedCost = tokenCost(cachedIn, model.cachedInputTokenCost);
123
+ const outputCost = tokenCost(imgOut, model.outputImageTokenCost);
124
+ const inputCost = round(textInputCost + imageInputCost, COST_DECIMAL_PLACES);
125
+ const totalCost = round(inputCost + cachedCost + outputCost, COST_DECIMAL_PLACES);
126
+ return {
127
+ inputCost,
128
+ outputCost,
129
+ cachedInputCost: cachedCost,
130
+ totalCost,
131
+ currency: "USD",
132
+ };
133
+ }
134
+ function calculatePerImageCost(modelName, imageCount) {
135
+ const model = getModel(modelName);
136
+ if (!model || !isImageModel(model) || !model.costPerImage)
137
+ return undefined;
138
+ const totalCost = round(model.costPerImage * imageCount, COST_DECIMAL_PLACES);
139
+ return { inputCost: 0, outputCost: totalCost, totalCost, currency: "USD" };
140
+ }
@@ -0,0 +1,35 @@
1
+ import { Provider } from "./models.js";
2
+ import { Result } from "./types/result.js";
3
+ import { TokenUsage } from "./types/tokenUsage.js";
4
+ import { CostEstimate } from "./types/costEstimate.js";
5
+ import { ImageRef } from "./util/imageRef.js";
6
+ export { ImageRef };
7
+ export type ImageInput = string | {
8
+ prompt: string;
9
+ images?: ImageRef[];
10
+ mask?: ImageRef;
11
+ };
12
+ export type ImageConfig = {
13
+ model: string;
14
+ provider?: Provider;
15
+ n?: number;
16
+ size?: string;
17
+ quality?: "low" | "medium" | "high" | "auto";
18
+ outputFormat?: "png" | "jpeg" | "webp";
19
+ background?: "transparent" | "opaque" | "auto";
20
+ openAiApiKey?: string;
21
+ googleApiKey?: string;
22
+ metadata?: Record<string, unknown>;
23
+ };
24
+ export type GeneratedImage = {
25
+ data: Uint8Array;
26
+ mimeType: string;
27
+ revisedPrompt?: string;
28
+ };
29
+ export type ImageGenResult = {
30
+ images: GeneratedImage[];
31
+ model: string;
32
+ tokenUsage?: TokenUsage;
33
+ costEstimate?: CostEstimate;
34
+ };
35
+ export declare function image(input: ImageInput, config: ImageConfig): Promise<Result<ImageGenResult>>;
package/dist/image.js ADDED
@@ -0,0 +1,37 @@
1
+ import { failure } from "./types/result.js";
2
+ import { resolveProvider, resolveApiKey } from "./util/provider.js";
3
+ import { openaiImage } from "./image/openai.js";
4
+ import { googleImage } from "./image/google.js";
5
+ export async function image(input, config) {
6
+ let provider;
7
+ try {
8
+ provider = resolveProvider(config.model, config.provider);
9
+ }
10
+ catch (err) {
11
+ return failure(err instanceof Error ? err.message : "Failed to resolve provider");
12
+ }
13
+ const apiKey = resolveApiKey(provider, config);
14
+ // `mask` is only meaningful for OpenAI inpainting. Reject up front so
15
+ // other providers don't silently drop it.
16
+ const hasMask = typeof input !== "string" && !!input.mask;
17
+ if (hasMask && provider !== "openai" && provider !== "openai-responses") {
18
+ return failure(`\`mask\` is only supported by the OpenAI image edit endpoint; provider "${provider}" cannot use it.`);
19
+ }
20
+ switch (provider) {
21
+ case "openai":
22
+ case "openai-responses": {
23
+ if (!apiKey) {
24
+ return failure("No OpenAI API key provided. Set openAiApiKey in config or the OPENAI_API_KEY environment variable.");
25
+ }
26
+ return openaiImage(input, config, apiKey);
27
+ }
28
+ case "google": {
29
+ if (!apiKey) {
30
+ return failure("No Google API key provided. Set googleApiKey in config or the GEMINI_API_KEY environment variable.");
31
+ }
32
+ return googleImage(input, config, apiKey);
33
+ }
34
+ default:
35
+ return failure(`Provider "${provider}" does not support image generation`);
36
+ }
37
+ }
package/dist/index.d.ts CHANGED
@@ -4,8 +4,11 @@ export * from "./models.js";
4
4
  export * from "./model.js";
5
5
  export * from "./smolError.js";
6
6
  export * from "./util/util.js";
7
+ export * from "./util/tool.js";
7
8
  export * from "./classes/message/index.js";
8
9
  export * from "./functions.js";
9
10
  export * from "./classes/ToolCall.js";
11
+ export * from "./embed.js";
12
+ export * from "./image.js";
10
13
  export { getLogger, EgonLog } from "./util/logger.js";
11
14
  export type { LogLevel } from "./util/logger.js";
package/dist/index.js CHANGED
@@ -4,7 +4,10 @@ export * from "./models.js";
4
4
  export * from "./model.js";
5
5
  export * from "./smolError.js";
6
6
  export * from "./util/util.js";
7
+ export * from "./util/tool.js";
7
8
  export * from "./classes/message/index.js";
8
9
  export * from "./functions.js";
9
10
  export * from "./classes/ToolCall.js";
11
+ export * from "./embed.js";
12
+ export * from "./image.js";
10
13
  export { getLogger, EgonLog } from "./util/logger.js";