smoltalk 0.6.0 → 0.7.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 (55) hide show
  1. package/dist/classes/message/UserMessage.d.ts +66 -5
  2. package/dist/classes/message/UserMessage.js +100 -21
  3. package/dist/classes/message/contentParts.d.ts +259 -0
  4. package/dist/classes/message/contentParts.js +44 -0
  5. package/dist/classes/message/index.d.ts +8 -2
  6. package/dist/classes/message/index.js +12 -0
  7. package/dist/classes/message/renderers/AnthropicRenderer.d.ts +43 -0
  8. package/dist/classes/message/renderers/AnthropicRenderer.js +19 -0
  9. package/dist/classes/message/renderers/GoogleRenderer.d.ts +35 -0
  10. package/dist/classes/message/renderers/GoogleRenderer.js +26 -0
  11. package/dist/classes/message/renderers/JSONRenderer.d.ts +8 -0
  12. package/dist/classes/message/renderers/JSONRenderer.js +21 -0
  13. package/dist/classes/message/renderers/OpenAIChatRenderer.d.ts +30 -0
  14. package/dist/classes/message/renderers/OpenAIChatRenderer.js +20 -0
  15. package/dist/classes/message/renderers/OpenAIResponsesRenderer.d.ts +39 -0
  16. package/dist/classes/message/renderers/OpenAIResponsesRenderer.js +23 -0
  17. package/dist/classes/message/renderers/PartRenderer.d.ts +14 -0
  18. package/dist/classes/message/renderers/PartRenderer.js +16 -0
  19. package/dist/clients/anthropic.d.ts +24 -0
  20. package/dist/clients/anthropic.js +4 -3
  21. package/dist/clients/baseClient.d.ts +7 -0
  22. package/dist/clients/baseClient.js +39 -0
  23. package/dist/clients/google.js +3 -2
  24. package/dist/clients/ollama.js +7 -6
  25. package/dist/clients/openai.js +3 -2
  26. package/dist/clients/openaiResponses.js +3 -2
  27. package/dist/clients/resolveAttachments.d.ts +11 -0
  28. package/dist/clients/resolveAttachments.js +108 -0
  29. package/dist/files/BaseFileProvider.d.ts +29 -0
  30. package/dist/files/BaseFileProvider.js +38 -0
  31. package/dist/files/anthropic.d.ts +16 -0
  32. package/dist/files/anthropic.js +20 -0
  33. package/dist/files/google.d.ts +18 -0
  34. package/dist/files/google.js +38 -0
  35. package/dist/files/openai.d.ts +16 -0
  36. package/dist/files/openai.js +23 -0
  37. package/dist/files.d.ts +46 -0
  38. package/dist/files.js +70 -0
  39. package/dist/index.d.ts +4 -0
  40. package/dist/index.js +3 -0
  41. package/dist/models.d.ts +17 -2
  42. package/dist/models.js +29 -2
  43. package/dist/statelogClient.js +2 -1
  44. package/dist/types.d.ts +6 -8
  45. package/dist/types.js +1 -4
  46. package/dist/util/attachments.d.ts +34 -0
  47. package/dist/util/attachments.js +62 -0
  48. package/dist/util/hostedTools.js +5 -1
  49. package/dist/util/imageRef.d.ts +16 -1
  50. package/dist/util/imageRef.js +95 -10
  51. package/dist/util/modalities.d.ts +2 -0
  52. package/dist/util/modalities.js +31 -0
  53. package/dist/util/redact.d.ts +8 -0
  54. package/dist/util/redact.js +42 -0
  55. package/package.json +1 -1
@@ -0,0 +1,29 @@
1
+ import type { FileProvider, FileProviderContext } from "../files.js";
2
+ import type { ProviderFileRef } from "../classes/message/contentParts.js";
3
+ import { Result } from "../types/result.js";
4
+ /**
5
+ * Shared behaviour for the built-in file providers: the try/catch, secret
6
+ * redaction, logging, and Result wrapping are identical across providers, so
7
+ * they live here. Subclasses implement only the SDK-specific upload/delete and
8
+ * may throw freely — this class turns a thrown error into a redacted Failure.
9
+ */
10
+ export declare abstract class BaseFileProvider implements FileProvider {
11
+ /** Human-readable provider label used in log and error messages, e.g. "OpenAI". */
12
+ protected abstract readonly label: string;
13
+ /** SDK-specific upload. May throw; the base class handles logging + redaction. */
14
+ protected abstract doUpload(data: Uint8Array, mimeType: string, ctx: FileProviderContext): Promise<ProviderFileRef>;
15
+ /** SDK-specific delete. May throw; the base class handles logging + redaction. */
16
+ protected abstract doDelete(ref: ProviderFileRef, ctx: {
17
+ apiKey: string;
18
+ }): Promise<void>;
19
+ upload(data: Uint8Array, mimeType: string, ctx: FileProviderContext): Promise<Result<ProviderFileRef>>;
20
+ delete(ref: ProviderFileRef, ctx: {
21
+ apiKey: string;
22
+ }): Promise<Result<void>>;
23
+ /**
24
+ * Redact the API key once, then reuse the scrubbed message for BOTH the log
25
+ * call and the returned Failure. The raw error object is never handed to the
26
+ * logger — SDKs sometimes attach the Authorization header to it. (B1)
27
+ */
28
+ private fail;
29
+ }
@@ -0,0 +1,38 @@
1
+ import { success, failure } from "../types/result.js";
2
+ import { getLogger } from "../util/logger.js";
3
+ import { redactSecret } from "../util/redact.js";
4
+ /**
5
+ * Shared behaviour for the built-in file providers: the try/catch, secret
6
+ * redaction, logging, and Result wrapping are identical across providers, so
7
+ * they live here. Subclasses implement only the SDK-specific upload/delete and
8
+ * may throw freely — this class turns a thrown error into a redacted Failure.
9
+ */
10
+ export class BaseFileProvider {
11
+ async upload(data, mimeType, ctx) {
12
+ try {
13
+ return success(await this.doUpload(data, mimeType, ctx));
14
+ }
15
+ catch (err) {
16
+ return this.fail("upload", err, ctx.apiKey);
17
+ }
18
+ }
19
+ async delete(ref, ctx) {
20
+ try {
21
+ await this.doDelete(ref, ctx);
22
+ return success(undefined);
23
+ }
24
+ catch (err) {
25
+ return this.fail("delete", err, ctx.apiKey);
26
+ }
27
+ }
28
+ /**
29
+ * Redact the API key once, then reuse the scrubbed message for BOTH the log
30
+ * call and the returned Failure. The raw error object is never handed to the
31
+ * logger — SDKs sometimes attach the Authorization header to it. (B1)
32
+ */
33
+ fail(op, err, apiKey) {
34
+ const message = redactSecret(err?.message ?? String(err), apiKey);
35
+ getLogger().error(`${this.label} file ${op} failed:`, message);
36
+ return failure(`${this.label} file ${op} failed: ${message}`);
37
+ }
38
+ }
@@ -0,0 +1,16 @@
1
+ import type { FileProviderContext } from "../files.js";
2
+ import type { ProviderFileRef } from "../classes/message/contentParts.js";
3
+ import { BaseFileProvider } from "./BaseFileProvider.js";
4
+ export declare function anthropicFileRef(uploaded: {
5
+ id: string;
6
+ mime_type?: string;
7
+ }, mimeType: string): ProviderFileRef;
8
+ declare class AnthropicFileProvider extends BaseFileProvider {
9
+ protected readonly label = "Anthropic";
10
+ protected doUpload(data: Uint8Array, mimeType: string, ctx: FileProviderContext): Promise<ProviderFileRef>;
11
+ protected doDelete(ref: ProviderFileRef, ctx: {
12
+ apiKey: string;
13
+ }): Promise<void>;
14
+ }
15
+ export declare const anthropicFileProvider: AnthropicFileProvider;
16
+ export {};
@@ -0,0 +1,20 @@
1
+ import Anthropic, { toFile } from "@anthropic-ai/sdk";
2
+ import { BaseFileProvider } from "./BaseFileProvider.js";
3
+ const FILES_BETA = "files-api-2025-04-14";
4
+ export function anthropicFileRef(uploaded, mimeType) {
5
+ return { kind: "providerFile", provider: "anthropic", id: uploaded.id, mimeType: uploaded.mime_type ?? mimeType };
6
+ }
7
+ class AnthropicFileProvider extends BaseFileProvider {
8
+ label = "Anthropic";
9
+ async doUpload(data, mimeType, ctx) {
10
+ const client = new Anthropic({ apiKey: ctx.apiKey });
11
+ const file = await toFile(data, ctx.filename ?? "upload", { type: mimeType });
12
+ const uploaded = await client.beta.files.upload({ file, betas: [FILES_BETA] });
13
+ return anthropicFileRef(uploaded, mimeType);
14
+ }
15
+ async doDelete(ref, ctx) {
16
+ const client = new Anthropic({ apiKey: ctx.apiKey });
17
+ await client.beta.files.delete(ref.id, { betas: [FILES_BETA] });
18
+ }
19
+ }
20
+ export const anthropicFileProvider = new AnthropicFileProvider();
@@ -0,0 +1,18 @@
1
+ import type { FileProviderContext } from "../files.js";
2
+ import type { ProviderFileRef } from "../classes/message/contentParts.js";
3
+ import { BaseFileProvider } from "./BaseFileProvider.js";
4
+ export declare function googleFileRef(uploaded: {
5
+ name?: string;
6
+ uri?: string;
7
+ mimeType?: string;
8
+ expirationTime?: string;
9
+ }, mimeType: string): ProviderFileRef;
10
+ declare class GoogleFileProvider extends BaseFileProvider {
11
+ protected readonly label = "Google";
12
+ protected doUpload(data: Uint8Array, mimeType: string, ctx: FileProviderContext): Promise<ProviderFileRef>;
13
+ protected doDelete(ref: ProviderFileRef, ctx: {
14
+ apiKey: string;
15
+ }): Promise<void>;
16
+ }
17
+ export declare const googleFileProvider: GoogleFileProvider;
18
+ export {};
@@ -0,0 +1,38 @@
1
+ import { GoogleGenAI } from "@google/genai";
2
+ import { BaseFileProvider } from "./BaseFileProvider.js";
3
+ export function googleFileRef(uploaded, mimeType) {
4
+ if (!uploaded.name) {
5
+ throw new Error("Google files.upload returned no name; cannot build a ProviderFileRef.");
6
+ }
7
+ let expiresAt;
8
+ if (uploaded.expirationTime) {
9
+ const parsed = Date.parse(uploaded.expirationTime);
10
+ if (Number.isFinite(parsed)) {
11
+ expiresAt = parsed;
12
+ }
13
+ }
14
+ return {
15
+ kind: "providerFile",
16
+ provider: "google",
17
+ id: uploaded.name,
18
+ uri: uploaded.uri,
19
+ mimeType: uploaded.mimeType ?? mimeType,
20
+ expiresAt,
21
+ };
22
+ }
23
+ class GoogleFileProvider extends BaseFileProvider {
24
+ label = "Google";
25
+ async doUpload(data, mimeType, ctx) {
26
+ const ai = new GoogleGenAI({ apiKey: ctx.apiKey });
27
+ // P2 (follow-up): `new Blob([data])` copies the buffer; prefer a no-copy /
28
+ // streaming form if the SDK supports it. Blob is acceptable for v1.
29
+ const blob = new Blob([data], { type: mimeType });
30
+ const uploaded = await ai.files.upload({ file: blob, config: { mimeType } });
31
+ return googleFileRef(uploaded, mimeType);
32
+ }
33
+ async doDelete(ref, ctx) {
34
+ const ai = new GoogleGenAI({ apiKey: ctx.apiKey });
35
+ await ai.files.delete({ name: ref.id });
36
+ }
37
+ }
38
+ export const googleFileProvider = new GoogleFileProvider();
@@ -0,0 +1,16 @@
1
+ import type { FileProviderContext } from "../files.js";
2
+ import type { ProviderFileRef } from "../classes/message/contentParts.js";
3
+ import { BaseFileProvider } from "./BaseFileProvider.js";
4
+ /** Maps a created OpenAI file (extra response fields intentionally ignored) to a ref. */
5
+ export declare function openaiFileRef(created: {
6
+ id: string;
7
+ }, mimeType: string): ProviderFileRef;
8
+ declare class OpenAIFileProvider extends BaseFileProvider {
9
+ protected readonly label = "OpenAI";
10
+ protected doUpload(data: Uint8Array, mimeType: string, ctx: FileProviderContext): Promise<ProviderFileRef>;
11
+ protected doDelete(ref: ProviderFileRef, ctx: {
12
+ apiKey: string;
13
+ }): Promise<void>;
14
+ }
15
+ export declare const openaiFileProvider: OpenAIFileProvider;
16
+ export {};
@@ -0,0 +1,23 @@
1
+ import OpenAI from "openai";
2
+ import { toFile } from "openai/uploads";
3
+ import { BaseFileProvider } from "./BaseFileProvider.js";
4
+ /** Maps a created OpenAI file (extra response fields intentionally ignored) to a ref. */
5
+ export function openaiFileRef(created, mimeType) {
6
+ return { kind: "providerFile", provider: "openai", id: created.id, mimeType };
7
+ }
8
+ class OpenAIFileProvider extends BaseFileProvider {
9
+ label = "OpenAI";
10
+ async doUpload(data, mimeType, ctx) {
11
+ const client = new OpenAI({ apiKey: ctx.apiKey });
12
+ const file = await toFile(data, ctx.filename ?? "upload", { type: mimeType });
13
+ // S5: the mapper below is trivial and won't throw post-create, so no orphan
14
+ // is left here; a provider whose mapper does real work should best-effort delete.
15
+ const created = await client.files.create({ file, purpose: "user_data" });
16
+ return openaiFileRef(created, mimeType);
17
+ }
18
+ async doDelete(ref, ctx) {
19
+ const client = new OpenAI({ apiKey: ctx.apiKey });
20
+ await client.files.delete(ref.id);
21
+ }
22
+ }
23
+ export const openaiFileProvider = new OpenAIFileProvider();
@@ -0,0 +1,46 @@
1
+ import { Result } from "./types/result.js";
2
+ import type { SmolConfig } from "./types.js";
3
+ import { ProviderFileRef } from "./classes/message/contentParts.js";
4
+ import { BlobRef } from "./util/imageRef.js";
5
+ /** Default cap on a resolved upload's size. Callers can raise it via opts.maxBytes. */
6
+ export declare const DEFAULT_UPLOAD_BYTES: number;
7
+ /**
8
+ * Options for {@link uploadFile}.
9
+ *
10
+ * ⚠️ Security: `{ kind: "path" }` sources read any file the process can access —
11
+ * do NOT pass untrusted user input as a path. `{ kind: "url" }` sources fetch
12
+ * arbitrary http(s) URLs; in a server that accepts user URLs, treat this as an
13
+ * SSRF surface (a full private-IP guard is a planned follow-up).
14
+ *
15
+ * Lifecycle: uploaded files persist on the provider until you call `deleteFile`
16
+ * (OpenAI keeps them indefinitely — storage bills accrue; Anthropic applies a
17
+ * retention window, so a cached ref can 404 later; Google auto-expires ~48h,
18
+ * surfaced as `expiresAt`). `uploadFile` is the recommended path for files more
19
+ * than a few MB — it sends bytes directly, avoiding the base64 round-trip that
20
+ * inline attachments pay.
21
+ */
22
+ export type UploadFileOptions = {
23
+ provider: string;
24
+ mimeType?: string;
25
+ filename?: string;
26
+ /** Max resolved size in bytes. Default {@link DEFAULT_UPLOAD_BYTES} (100 MB). */
27
+ maxBytes?: number;
28
+ apiKey?: SmolConfig["apiKey"];
29
+ };
30
+ export type FileProviderContext = {
31
+ apiKey: string;
32
+ filename?: string;
33
+ };
34
+ export type FileProvider = {
35
+ upload(data: Uint8Array, mimeType: string, ctx: FileProviderContext): Promise<Result<ProviderFileRef>>;
36
+ delete(ref: ProviderFileRef, ctx: {
37
+ apiKey: string;
38
+ }): Promise<Result<void>>;
39
+ };
40
+ export declare function registerFileProvider(name: string, impl: FileProvider): void;
41
+ /** Test-only: clear all registered custom providers so registrations don't leak across tests. */
42
+ export declare function _resetForTests(): void;
43
+ export declare function uploadFile(source: BlobRef, opts: UploadFileOptions): Promise<Result<ProviderFileRef>>;
44
+ export declare function deleteFile(ref: ProviderFileRef, opts?: {
45
+ apiKey?: SmolConfig["apiKey"];
46
+ }): Promise<Result<void>>;
package/dist/files.js ADDED
@@ -0,0 +1,70 @@
1
+ import { failure } from "./types/result.js";
2
+ import { loadBlob } from "./util/imageRef.js";
3
+ import { fileFamily } from "./util/attachments.js";
4
+ import { resolveApiKey } from "./util/provider.js";
5
+ import { openaiFileProvider } from "./files/openai.js";
6
+ import { anthropicFileProvider } from "./files/anthropic.js";
7
+ import { googleFileProvider } from "./files/google.js";
8
+ /** Default cap on a resolved upload's size. Callers can raise it via opts.maxBytes. */
9
+ export const DEFAULT_UPLOAD_BYTES = 100 * 1024 * 1024;
10
+ const BUILT_INS = {
11
+ openai: openaiFileProvider,
12
+ anthropic: anthropicFileProvider,
13
+ google: googleFileProvider,
14
+ };
15
+ const registered = Object.create(null);
16
+ export function registerFileProvider(name, impl) {
17
+ registered[name] = impl;
18
+ }
19
+ /** Test-only: clear all registered custom providers so registrations don't leak across tests. */
20
+ export function _resetForTests() {
21
+ for (const name of Object.keys(registered)) {
22
+ delete registered[name];
23
+ }
24
+ }
25
+ // First-party built-ins win (not registry-overridable); unknown → registry.
26
+ function selectProvider(provider) {
27
+ const family = fileFamily(provider);
28
+ if (family !== null) {
29
+ return { impl: BUILT_INS[family], builtin: true };
30
+ }
31
+ const custom = registered[provider];
32
+ if (custom) {
33
+ return { impl: custom, builtin: false };
34
+ }
35
+ return null;
36
+ }
37
+ export async function uploadFile(source, opts) {
38
+ const sel = selectProvider(opts.provider);
39
+ if (!sel) {
40
+ return failure(`Provider "${opts.provider}" has no Files API. Supported: openai, anthropic, google.`);
41
+ }
42
+ const maxBytes = opts.maxBytes ?? DEFAULT_UPLOAD_BYTES;
43
+ let loaded;
44
+ try {
45
+ loaded = await loadBlob(source, { maxBytes });
46
+ }
47
+ catch (err) {
48
+ return failure(`Failed to load file for upload: ${err.message}`);
49
+ }
50
+ const mimeType = opts.mimeType ?? loaded.mimeType;
51
+ if (!mimeType || mimeType.trim() === "") {
52
+ return failure("Could not determine the file's MIME type; pass opts.mimeType.");
53
+ }
54
+ const apiKey = resolveApiKey(opts.provider, { apiKey: opts.apiKey });
55
+ if (sel.builtin && !apiKey) {
56
+ return failure(`No API key for provider "${opts.provider}". Set config.apiKey or the provider's env var.`);
57
+ }
58
+ return sel.impl.upload(loaded.data, mimeType, { apiKey: apiKey ?? "", filename: opts.filename });
59
+ }
60
+ export async function deleteFile(ref, opts = {}) {
61
+ const sel = selectProvider(ref.provider);
62
+ if (!sel) {
63
+ return failure(`Provider "${ref.provider}" has no Files API.`);
64
+ }
65
+ const apiKey = resolveApiKey(ref.provider, { apiKey: opts.apiKey });
66
+ if (sel.builtin && !apiKey) {
67
+ return failure(`No API key for provider "${ref.provider}".`);
68
+ }
69
+ return sel.impl.delete(ref, { apiKey: apiKey ?? "" });
70
+ }
package/dist/index.d.ts CHANGED
@@ -11,5 +11,9 @@ export * from "./functions.js";
11
11
  export * from "./classes/ToolCall.js";
12
12
  export * from "./embed.js";
13
13
  export * from "./image.js";
14
+ export { uploadFile, deleteFile, registerFileProvider, DEFAULT_UPLOAD_BYTES } from "./files.js";
15
+ export type { UploadFileOptions, FileProviderContext, FileProvider } from "./files.js";
16
+ export { normalizeImageRef, loadBlob } from "./util/imageRef.js";
17
+ export type { BlobRef } from "./util/imageRef.js";
14
18
  export { getLogger, EgonLog } from "./util/logger.js";
15
19
  export type { LogLevel } from "./util/logger.js";
package/dist/index.js CHANGED
@@ -11,4 +11,7 @@ export * from "./functions.js";
11
11
  export * from "./classes/ToolCall.js";
12
12
  export * from "./embed.js";
13
13
  export * from "./image.js";
14
+ // Explicit (not `export *`) so the test-only `_resetForTests` stays off the public surface.
15
+ export { uploadFile, deleteFile, registerFileProvider, DEFAULT_UPLOAD_BYTES } from "./files.js";
16
+ export { normalizeImageRef, loadBlob } from "./util/imageRef.js";
14
17
  export { getLogger, EgonLog } from "./util/logger.js";
package/dist/models.d.ts CHANGED
@@ -2,11 +2,11 @@ import { z } from "zod";
2
2
  import { type ModelDataBlob, type HostedTool, type HostedToolPrice } from "./modelData.js";
3
3
  export declare const providers: readonly ["ollama", "openai", "openai-responses", "anthropic", "google", "replicate", "modal", "openrouter", "deepinfra", "litellm", "openai-compat"];
4
4
  export declare const ProviderSchema: z.ZodEnum<{
5
- ollama: "ollama";
6
5
  openai: "openai";
7
- "openai-responses": "openai-responses";
8
6
  anthropic: "anthropic";
9
7
  google: "google";
8
+ "openai-responses": "openai-responses";
9
+ ollama: "ollama";
10
10
  replicate: "replicate";
11
11
  modal: "modal";
12
12
  openrouter: "openrouter";
@@ -1454,7 +1454,22 @@ export declare function registerTextModel(model: Omit<TextModel, "type"> & {
1454
1454
  export declare function registerModelData(blob: ModelDataBlob): void;
1455
1455
  export declare function clearModelData(): void;
1456
1456
  export declare function getRegisteredModelData(): ModelDataBlob | null;
1457
+ /**
1458
+ * All known models: the baked-in catalog merged with any registered
1459
+ * (refreshed) model data and optional per-request overrides. This is the same
1460
+ * data `getModel` searches, exposed as a list so callers can enumerate, filter,
1461
+ * or display the catalog (e.g. a model picker). Fields worth filtering on live
1462
+ * on `ModelType`: `provider`, `openWeights`, `inputTokenCost` / `outputTokenCost`,
1463
+ * `maxInputTokens`, and `modalities`.
1464
+ */
1465
+ export declare function getAllModels(requestData?: ModelDataBlob): ModelType[];
1457
1466
  export declare function getModel(modelName: ModelName, requestData?: ModelDataBlob): ModelType | undefined;
1467
+ /**
1468
+ * Whether a model is known to accept the given input modality ("image", "pdf", …).
1469
+ * Returns undefined when the model is unknown or carries no `modalities` data —
1470
+ * callers should treat undefined as "don't gate".
1471
+ */
1472
+ export declare function modelSupportsInputModality(modelName: ModelName, modality: string, requestData?: ModelDataBlob): boolean | undefined;
1458
1473
  export declare function getHostedTools(opts?: {
1459
1474
  provider?: string;
1460
1475
  model?: string;
package/dist/models.js CHANGED
@@ -1597,7 +1597,15 @@ function baselineModels() {
1597
1597
  ...embeddingsModels,
1598
1598
  ];
1599
1599
  }
1600
- export function getModel(modelName, requestData) {
1600
+ /**
1601
+ * All known models: the baked-in catalog merged with any registered
1602
+ * (refreshed) model data and optional per-request overrides. This is the same
1603
+ * data `getModel` searches, exposed as a list so callers can enumerate, filter,
1604
+ * or display the catalog (e.g. a model picker). Fields worth filtering on live
1605
+ * on `ModelType`: `provider`, `openWeights`, `inputTokenCost` / `outputTokenCost`,
1606
+ * `maxInputTokens`, and `modalities`.
1607
+ */
1608
+ export function getAllModels(requestData) {
1601
1609
  let models = baselineModels();
1602
1610
  if (registeredModelData) {
1603
1611
  models = mergeModelData(models, registeredModelData.models);
@@ -1605,7 +1613,26 @@ export function getModel(modelName, requestData) {
1605
1613
  if (requestData) {
1606
1614
  models = mergeModelData(models, requestData.models);
1607
1615
  }
1608
- return models.find((model) => model.modelName === modelName);
1616
+ return models;
1617
+ }
1618
+ export function getModel(modelName, requestData) {
1619
+ return getAllModels(requestData).find((model) => model.modelName === modelName);
1620
+ }
1621
+ /**
1622
+ * Whether a model is known to accept the given input modality ("image", "pdf", …).
1623
+ * Returns undefined when the model is unknown or carries no `modalities` data —
1624
+ * callers should treat undefined as "don't gate".
1625
+ */
1626
+ export function modelSupportsInputModality(modelName, modality, requestData) {
1627
+ const model = getModel(modelName, requestData);
1628
+ if (!model || model.type !== "text") {
1629
+ return undefined;
1630
+ }
1631
+ const inputs = model.modalities?.input;
1632
+ if (!inputs) {
1633
+ return undefined;
1634
+ }
1635
+ return inputs.includes(modality);
1609
1636
  }
1610
1637
  export function getHostedTools(opts = {}) {
1611
1638
  // Start from a copy so callers can never mutate the baseline registry.
@@ -1,5 +1,6 @@
1
1
  import { nanoid } from "nanoid";
2
2
  import { failure, mergeResults, success } from "./types/result.js";
3
+ import { redactAttachments } from "./util/redact.js";
3
4
  export function mergeUploadResults(_results) {
4
5
  const results = mergeResults(_results);
5
6
  if (!results.success) {
@@ -277,7 +278,7 @@ export class StatelogClient {
277
278
  const postBody = JSON.stringify({
278
279
  trace_id: this.traceId,
279
280
  project_id: this.projectId,
280
- data: { ...body, timestamp: new Date().toISOString() },
281
+ data: redactAttachments({ ...body, timestamp: new Date().toISOString() }),
281
282
  });
282
283
  if (this.host.toLowerCase() === "stdout") {
283
284
  console.log(postBody);
package/dist/types.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export * from "./types/result.js";
2
+ export * from "./classes/message/contentParts.js";
2
3
  import { LogLevel } from "./util/logger.js";
3
4
  import z, { ZodType } from "zod";
4
5
  import { Message } from "./classes/message/index.js";
@@ -57,6 +58,11 @@ export type SmolConfig = {
57
58
  }>;
58
59
  /** Arbitrary metadata passed to custom model providers. */
59
60
  metadata?: Record<string, any>;
61
+ /** Options for image/PDF attachments on user messages. */
62
+ attachments?: {
63
+ /** Max resolved attachment size in bytes. Default 20 MB. Over → Failure. */
64
+ maxBytes?: number;
65
+ };
60
66
  /** Refreshed model/hosted-tool data (from refreshModels) to layer over the baked-in registry for this call. */
61
67
  modelData?: ModelDataBlob;
62
68
  /** The conversation messages to send to the model. */
@@ -173,10 +179,6 @@ export interface SmolClient {
173
179
  textStream(config: SmolConfig): AsyncGenerator<StreamChunk>;
174
180
  _textStream(config: SmolConfig): AsyncGenerator<StreamChunk>;
175
181
  }
176
- export type TextPart = {
177
- type: "text";
178
- text: string;
179
- };
180
182
  /** Loose variant of SmolConfig for `getClient()` — messages are not required at construction time. */
181
183
  export type SmolClientConfig = Omit<SmolConfig, "messages"> & {
182
184
  messages?: Message[];
@@ -186,10 +188,6 @@ export type ThinkingBlock = {
186
188
  text: string;
187
189
  signature: string;
188
190
  };
189
- export declare const TextPartSchema: z.ZodObject<{
190
- type: z.ZodLiteral<"text">;
191
- text: z.ZodString;
192
- }, z.z.core.$strip>;
193
191
  export declare const ThinkingBlockSchema: z.ZodObject<{
194
192
  text: z.ZodString;
195
193
  signature: z.ZodString;
package/dist/types.js CHANGED
@@ -1,4 +1,5 @@
1
1
  export * from "./types/result.js";
2
+ export * from "./classes/message/contentParts.js";
2
3
  import z from "zod";
3
4
  export * from "./types/costEstimate.js";
4
5
  export * from "./types/tokenUsage.js";
@@ -13,10 +14,6 @@ export function promptResult({ output, toolCalls, thinkingBlocks, usage, cost, m
13
14
  hostedToolResults,
14
15
  };
15
16
  }
16
- export const TextPartSchema = z.object({
17
- type: z.literal("text"),
18
- text: z.string(),
19
- });
20
17
  export const ThinkingBlockSchema = z.object({
21
18
  text: z.string(),
22
19
  signature: z.string(),
@@ -0,0 +1,34 @@
1
+ import type { ImageRef } from "./imageRef.js";
2
+ /** Filename sent to providers that require one when a file part omits its own. */
3
+ export declare const DEFAULT_ATTACHMENT_FILENAME = "attachment.pdf";
4
+ /** A file part's filename, falling back to the shared default when unset. */
5
+ export declare function attachmentFilename(filename?: string): string;
6
+ /**
7
+ * Extract base64 + MIME from an ImageRef that has already been resolved to
8
+ * in-memory bytes. `path`/`url` refs must be resolved (via the BaseClient
9
+ * normalize pass) before serialization — reaching here with one is a bug.
10
+ */
11
+ export declare function refToBase64(ref: ImageRef): {
12
+ base64: string;
13
+ mimeType: string;
14
+ };
15
+ export declare function toDataUri(base64: string, mimeType: string): string;
16
+ /** The Files-API namespace for a provider, or null when it has no Files API. */
17
+ export declare function fileFamily(provider: string): "openai" | "anthropic" | "google" | null;
18
+ /**
19
+ * OpenAI-style image URL: a passthrough `url` ref stays a remote URL; anything
20
+ * else becomes a base64 data URI.
21
+ */
22
+ export declare function openAiImageUrl(ref: ImageRef): string;
23
+ /**
24
+ * Anthropic image/document source: a passthrough `url` ref becomes a url source;
25
+ * anything else becomes a base64 source.
26
+ */
27
+ export declare function anthropicSource(ref: ImageRef): {
28
+ type: "url";
29
+ url: string;
30
+ } | {
31
+ type: "base64";
32
+ media_type: string;
33
+ data: string;
34
+ };
@@ -0,0 +1,62 @@
1
+ /** Filename sent to providers that require one when a file part omits its own. */
2
+ export const DEFAULT_ATTACHMENT_FILENAME = "attachment.pdf";
3
+ /** A file part's filename, falling back to the shared default when unset. */
4
+ export function attachmentFilename(filename) {
5
+ if (filename === undefined) {
6
+ return DEFAULT_ATTACHMENT_FILENAME;
7
+ }
8
+ return filename;
9
+ }
10
+ /**
11
+ * Extract base64 + MIME from an ImageRef that has already been resolved to
12
+ * in-memory bytes. `path`/`url` refs must be resolved (via the BaseClient
13
+ * normalize pass) before serialization — reaching here with one is a bug.
14
+ */
15
+ export function refToBase64(ref) {
16
+ if (ref.kind === "base64") {
17
+ return { base64: ref.base64, mimeType: ref.mimeType };
18
+ }
19
+ if (ref.kind === "bytes") {
20
+ return { base64: Buffer.from(ref.data).toString("base64"), mimeType: ref.mimeType };
21
+ }
22
+ throw new Error(`Attachment ref must be resolved to base64 or bytes before serialization ` +
23
+ `(got kind="${ref.kind}"). This is a smoltalk bug.`);
24
+ }
25
+ export function toDataUri(base64, mimeType) {
26
+ return `data:${mimeType};base64,${base64}`;
27
+ }
28
+ /** The Files-API namespace for a provider, or null when it has no Files API. */
29
+ export function fileFamily(provider) {
30
+ if (provider === "openai" || provider === "openai-responses") {
31
+ return "openai";
32
+ }
33
+ if (provider === "anthropic") {
34
+ return "anthropic";
35
+ }
36
+ if (provider === "google") {
37
+ return "google";
38
+ }
39
+ return null;
40
+ }
41
+ /**
42
+ * OpenAI-style image URL: a passthrough `url` ref stays a remote URL; anything
43
+ * else becomes a base64 data URI.
44
+ */
45
+ export function openAiImageUrl(ref) {
46
+ if (ref.kind === "url") {
47
+ return ref.url;
48
+ }
49
+ const { base64, mimeType } = refToBase64(ref);
50
+ return toDataUri(base64, mimeType);
51
+ }
52
+ /**
53
+ * Anthropic image/document source: a passthrough `url` ref becomes a url source;
54
+ * anything else becomes a base64 source.
55
+ */
56
+ export function anthropicSource(ref) {
57
+ if (ref.kind === "url") {
58
+ return { type: "url", url: ref.url };
59
+ }
60
+ const { base64, mimeType } = refToBase64(ref);
61
+ return { type: "base64", media_type: mimeType, data: base64 };
62
+ }
@@ -27,7 +27,11 @@ export function validateHostedTools(requested, model, provider, modelData) {
27
27
  if (!effectiveProvider) {
28
28
  effectiveProvider = getModel(model, modelData)?.provider ?? "unknown";
29
29
  }
30
- return `${name} is a hosted capability; ${model} (${effectiveProvider}) doesn't offer it — pass a search function as a tool instead.`;
30
+ let err = `${name} is a hosted capability; ${model} (${effectiveProvider}) doesn't offer it — pass a search function as a tool instead.`;
31
+ if (effectiveProvider === "openai") {
32
+ err += " You can also use the provider 'openai-responses' to access hosted tools for OpenAI models (you're currently using the base 'openai' provider).";
33
+ }
34
+ return err;
31
35
  }
32
36
  }
33
37
  return null;
@@ -20,10 +20,25 @@ export type ImageRef = {
20
20
  */
21
21
  timeoutMs?: number;
22
22
  };
23
+ /** Neutral alias of the source union for non-image uses (uploads via {@link loadBlob}). */
24
+ export type BlobRef = ImageRef;
23
25
  export type NormalizedImage = {
24
26
  data: Uint8Array;
25
27
  mimeType: string;
26
28
  };
27
29
  /** Default timeout for fetching image URLs during normalization (60 seconds). */
28
30
  export declare const DEFAULT_FETCH_TIMEOUT_MS = 60000;
29
- export declare function normalizeImageRef(ref: ImageRef): Promise<NormalizedImage>;
31
+ export declare function normalizeImageRef(ref: ImageRef, options?: {
32
+ allowedMimePrefixes?: string[];
33
+ maxBytes?: number;
34
+ }): Promise<NormalizedImage>;
35
+ /**
36
+ * Load a source to bytes WITHOUT the image MIME gate (used by file uploads,
37
+ * which accept any type). Enforces `maxBytes` across all kinds.
38
+ */
39
+ export declare function loadBlob(ref: ImageRef, options?: {
40
+ maxBytes?: number;
41
+ }): Promise<{
42
+ data: Uint8Array;
43
+ mimeType?: string;
44
+ }>;