smoltalk 0.5.1 → 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 (77) hide show
  1. package/README.md +67 -10
  2. package/dist/classes/message/UserMessage.d.ts +66 -5
  3. package/dist/classes/message/UserMessage.js +100 -21
  4. package/dist/classes/message/contentParts.d.ts +259 -0
  5. package/dist/classes/message/contentParts.js +44 -0
  6. package/dist/classes/message/index.d.ts +8 -2
  7. package/dist/classes/message/index.js +12 -0
  8. package/dist/classes/message/renderers/AnthropicRenderer.d.ts +43 -0
  9. package/dist/classes/message/renderers/AnthropicRenderer.js +19 -0
  10. package/dist/classes/message/renderers/GoogleRenderer.d.ts +35 -0
  11. package/dist/classes/message/renderers/GoogleRenderer.js +26 -0
  12. package/dist/classes/message/renderers/JSONRenderer.d.ts +8 -0
  13. package/dist/classes/message/renderers/JSONRenderer.js +21 -0
  14. package/dist/classes/message/renderers/OpenAIChatRenderer.d.ts +30 -0
  15. package/dist/classes/message/renderers/OpenAIChatRenderer.js +20 -0
  16. package/dist/classes/message/renderers/OpenAIResponsesRenderer.d.ts +39 -0
  17. package/dist/classes/message/renderers/OpenAIResponsesRenderer.js +23 -0
  18. package/dist/classes/message/renderers/PartRenderer.d.ts +14 -0
  19. package/dist/classes/message/renderers/PartRenderer.js +16 -0
  20. package/dist/client.d.ts +4 -0
  21. package/dist/client.js +37 -16
  22. package/dist/clients/anthropic.d.ts +24 -2
  23. package/dist/clients/anthropic.js +9 -4
  24. package/dist/clients/baseClient.d.ts +7 -0
  25. package/dist/clients/baseClient.js +39 -0
  26. package/dist/clients/deepinfra.d.ts +17 -0
  27. package/dist/clients/deepinfra.js +27 -0
  28. package/dist/clients/google.js +6 -4
  29. package/dist/clients/litellm.d.ts +19 -0
  30. package/dist/clients/litellm.js +32 -0
  31. package/dist/clients/ollama.js +12 -9
  32. package/dist/clients/openai.d.ts +41 -5
  33. package/dist/clients/openai.js +78 -14
  34. package/dist/clients/openaiCompat.d.ts +22 -0
  35. package/dist/clients/openaiCompat.js +33 -0
  36. package/dist/clients/openaiResponses.js +6 -4
  37. package/dist/clients/openrouter.d.ts +23 -0
  38. package/dist/clients/openrouter.js +76 -0
  39. package/dist/clients/resolveAttachments.d.ts +11 -0
  40. package/dist/clients/resolveAttachments.js +108 -0
  41. package/dist/embed/openai.d.ts +7 -1
  42. package/dist/embed/openai.js +8 -2
  43. package/dist/embed.d.ts +18 -4
  44. package/dist/embed.js +32 -4
  45. package/dist/files/BaseFileProvider.d.ts +29 -0
  46. package/dist/files/BaseFileProvider.js +38 -0
  47. package/dist/files/anthropic.d.ts +16 -0
  48. package/dist/files/anthropic.js +20 -0
  49. package/dist/files/google.d.ts +18 -0
  50. package/dist/files/google.js +38 -0
  51. package/dist/files/openai.d.ts +16 -0
  52. package/dist/files/openai.js +23 -0
  53. package/dist/files.d.ts +46 -0
  54. package/dist/files.js +70 -0
  55. package/dist/image/openaiCompat.d.ts +9 -0
  56. package/dist/image/openaiCompat.js +60 -0
  57. package/dist/image.d.ts +13 -2
  58. package/dist/image.js +28 -3
  59. package/dist/index.d.ts +4 -0
  60. package/dist/index.js +3 -0
  61. package/dist/models.d.ts +22 -3
  62. package/dist/models.js +46 -3
  63. package/dist/statelogClient.js +2 -1
  64. package/dist/types.d.ts +28 -18
  65. package/dist/types.js +1 -4
  66. package/dist/util/attachments.d.ts +34 -0
  67. package/dist/util/attachments.js +62 -0
  68. package/dist/util/hostedTools.js +5 -1
  69. package/dist/util/imageRef.d.ts +16 -1
  70. package/dist/util/imageRef.js +95 -10
  71. package/dist/util/modalities.d.ts +2 -0
  72. package/dist/util/modalities.js +31 -0
  73. package/dist/util/provider.d.ts +32 -6
  74. package/dist/util/provider.js +38 -4
  75. package/dist/util/redact.d.ts +8 -0
  76. package/dist/util/redact.js +42 -0
  77. package/package.json +1 -1
@@ -0,0 +1,108 @@
1
+ import { UserMessage } from "../classes/message/index.js";
2
+ import { normalizeImageRef } from "../util/imageRef.js";
3
+ import { fileFamily } from "../util/attachments.js";
4
+ import { success, failure } from "../types.js";
5
+ export const DEFAULT_MAX_ATTACHMENT_BYTES = 20 * 1024 * 1024;
6
+ const URL_IMAGE_PROVIDERS = new Set([
7
+ "openai",
8
+ "openai-responses",
9
+ "openai-compat",
10
+ "openrouter",
11
+ "deepinfra",
12
+ "litellm",
13
+ "anthropic",
14
+ ]);
15
+ const URL_PDF_PROVIDERS = new Set(["openai-responses", "anthropic"]);
16
+ /** Whether any user message carries an image/file attachment part. */
17
+ export function messagesHaveAttachments(messages) {
18
+ for (const msg of messages) {
19
+ if (!(msg instanceof UserMessage)) {
20
+ continue;
21
+ }
22
+ const parts = msg.getContentParts();
23
+ if (parts === null) {
24
+ continue;
25
+ }
26
+ for (const part of parts) {
27
+ if (part.type === "image" || part.type === "file") {
28
+ return true;
29
+ }
30
+ }
31
+ }
32
+ return false;
33
+ }
34
+ /** Whether `provider` accepts a remote URL directly for this part type. */
35
+ export function acceptsRemoteUrl(provider, partType) {
36
+ if (partType === "image") {
37
+ return URL_IMAGE_PROVIDERS.has(provider);
38
+ }
39
+ return URL_PDF_PROVIDERS.has(provider);
40
+ }
41
+ export async function resolveMessageAttachments(messages, options) {
42
+ const out = [];
43
+ for (const msg of messages) {
44
+ if (!(msg instanceof UserMessage)) {
45
+ out.push(msg);
46
+ continue;
47
+ }
48
+ const parts = msg.getContentParts();
49
+ if (parts === null) {
50
+ out.push(msg);
51
+ continue;
52
+ }
53
+ const resolvedParts = [];
54
+ for (const part of parts) {
55
+ if (part.type === "text") {
56
+ resolvedParts.push(part);
57
+ continue;
58
+ }
59
+ // Provider file references are validated and passed through (no download/cap).
60
+ if (part.source.kind === "providerFile") {
61
+ const family = fileFamily(options.provider);
62
+ if (family === null || part.source.provider !== family) {
63
+ return failure(`Attachment references a "${part.source.provider}" file, but this call targets provider ` +
64
+ `"${options.provider}" (file family ${family ?? "none"}).`);
65
+ }
66
+ if (part.type === "image" && options.provider === "openai") {
67
+ return failure("An image file reference requires the openai-responses provider (OpenAI Chat Completions has no image-by-file_id form).");
68
+ }
69
+ resolvedParts.push(part);
70
+ continue;
71
+ }
72
+ // Passthrough: keep a url ref when the target provider accepts a remote URL.
73
+ if (part.source.kind === "url" && acceptsRemoteUrl(options.provider, part.type)) {
74
+ resolvedParts.push(part);
75
+ continue;
76
+ }
77
+ let allowed;
78
+ if (part.type === "image") {
79
+ allowed = ["image/"];
80
+ }
81
+ else {
82
+ allowed = ["application/pdf"];
83
+ }
84
+ try {
85
+ const { data, mimeType } = await normalizeImageRef(part.source, {
86
+ allowedMimePrefixes: allowed,
87
+ maxBytes: options.maxBytes,
88
+ });
89
+ const source = {
90
+ kind: "base64",
91
+ base64: Buffer.from(data).toString("base64"),
92
+ mimeType,
93
+ };
94
+ if (part.type === "image") {
95
+ resolvedParts.push({ type: "image", source });
96
+ }
97
+ else {
98
+ resolvedParts.push({ type: "file", source, filename: part.filename });
99
+ }
100
+ }
101
+ catch (err) {
102
+ return failure(`Failed to load ${part.type} attachment: ${err.message}`);
103
+ }
104
+ }
105
+ out.push(new UserMessage(resolvedParts, { name: msg.name, rawData: msg.rawData }));
106
+ }
107
+ return success(out);
108
+ }
@@ -1,3 +1,9 @@
1
1
  import { EmbedConfig, EmbedResult } from "../embed.js";
2
2
  import { Result } from "../types/result.js";
3
- export declare function openaiEmbed(inputs: string[], config: EmbedConfig, apiKey: string): Promise<Result<EmbedResult>>;
3
+ /**
4
+ * OpenAI-compatible embedding call. Used by openai directly and by other
5
+ * OpenAI-shape backends (deepinfra, litellm, openai-compat) which pass a
6
+ * custom `baseURL`. Cost comes from the smoltalk model registry; provider-
7
+ * returned cost fields aren't standardized on this endpoint.
8
+ */
9
+ export declare function openaiEmbed(inputs: string[], config: EmbedConfig, apiKey: string, baseURL?: string): Promise<Result<EmbedResult>>;
@@ -2,9 +2,15 @@ import OpenAI from "openai";
2
2
  import { success, failure } from "../types/result.js";
3
3
  import { getModel, isEmbeddingsModel } from "../models.js";
4
4
  import { round } from "../util/util.js";
5
- export async function openaiEmbed(inputs, config, apiKey) {
5
+ /**
6
+ * OpenAI-compatible embedding call. Used by openai directly and by other
7
+ * OpenAI-shape backends (deepinfra, litellm, openai-compat) which pass a
8
+ * custom `baseURL`. Cost comes from the smoltalk model registry; provider-
9
+ * returned cost fields aren't standardized on this endpoint.
10
+ */
11
+ export async function openaiEmbed(inputs, config, apiKey, baseURL) {
6
12
  try {
7
- const client = new OpenAI({ apiKey });
13
+ const client = new OpenAI({ apiKey, ...(baseURL ? { baseURL } : {}) });
8
14
  const response = await client.embeddings.create({
9
15
  model: config.model,
10
16
  input: inputs,
package/dist/embed.d.ts CHANGED
@@ -6,10 +6,24 @@ export type EmbedConfig = {
6
6
  model: string;
7
7
  provider?: string;
8
8
  dimensions?: number;
9
- openAiApiKey?: string;
10
- googleApiKey?: string;
11
- ollamaApiKey?: string;
12
- ollamaHost?: string;
9
+ /** API keys, nested by provider. Falls back to env vars
10
+ * (OPENAI_API_KEY / GEMINI_API_KEY / DEEPINFRA_API_KEY /
11
+ * LITELLM_API_KEY / OPENAI_COMPAT_API_KEY). */
12
+ apiKey?: {
13
+ openAi?: string;
14
+ google?: string;
15
+ ollama?: string;
16
+ deepInfra?: string;
17
+ liteLlm?: string;
18
+ openAiCompat?: string;
19
+ };
20
+ /** Custom base URLs, nested by provider. */
21
+ baseUrl?: {
22
+ ollama?: string;
23
+ deepInfra?: string;
24
+ liteLlm?: string;
25
+ openAiCompat?: string;
26
+ };
13
27
  metadata?: Record<string, unknown>;
14
28
  modelData?: ModelDataBlob;
15
29
  };
package/dist/embed.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { failure } from "./types/result.js";
2
- import { resolveProvider, resolveApiKey } from "./util/provider.js";
2
+ import { resolveProvider, resolveApiKey, resolveBaseUrl } 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";
@@ -23,18 +23,46 @@ export async function embed(input, config) {
23
23
  case "openai":
24
24
  case "openai-responses": {
25
25
  if (!apiKey) {
26
- return failure("No OpenAI API key provided. Set openAiApiKey in config or the OPENAI_API_KEY environment variable.");
26
+ return failure("No OpenAI API key provided. Set config.apiKey.openAi or the OPENAI_API_KEY environment variable.");
27
27
  }
28
28
  return openaiEmbed(inputs, config, apiKey);
29
29
  }
30
30
  case "google": {
31
31
  if (!apiKey) {
32
- return failure("No Google API key provided. Set googleApiKey in config or the GEMINI_API_KEY environment variable.");
32
+ return failure("No Google API key provided. Set config.apiKey.google or the GEMINI_API_KEY environment variable.");
33
33
  }
34
34
  return googleEmbed(inputs, config, apiKey);
35
35
  }
36
36
  case "ollama":
37
- return ollamaEmbed(inputs, config, apiKey, config.ollamaHost);
37
+ return ollamaEmbed(inputs, config, apiKey, resolveBaseUrl("ollama", config));
38
+ case "openrouter":
39
+ return failure("openrouter does not expose an embeddings endpoint; use deepinfra, openai-compat, or litellm instead.");
40
+ case "deepinfra": {
41
+ if (!apiKey) {
42
+ return failure("No DeepInfra API key provided. Set config.apiKey.deepInfra or the DEEPINFRA_API_KEY environment variable.");
43
+ }
44
+ return openaiEmbed(inputs, config, apiKey, resolveBaseUrl("deepinfra", config));
45
+ }
46
+ case "litellm": {
47
+ if (!apiKey) {
48
+ return failure("No LiteLLM API key provided. Set config.apiKey.liteLlm or the LITELLM_API_KEY environment variable.");
49
+ }
50
+ const baseURL = resolveBaseUrl("litellm", config);
51
+ if (!baseURL) {
52
+ return failure("No LiteLLM base URL provided. Set config.baseUrl.liteLlm or the LITELLM_BASE_URL environment variable.");
53
+ }
54
+ return openaiEmbed(inputs, config, apiKey, baseURL);
55
+ }
56
+ case "openai-compat": {
57
+ if (!apiKey) {
58
+ return failure("No openai-compat API key provided. Set config.apiKey.openAiCompat or the OPENAI_COMPAT_API_KEY environment variable.");
59
+ }
60
+ const baseURL = resolveBaseUrl("openai-compat", config);
61
+ if (!baseURL) {
62
+ return failure("No openai-compat base URL provided. Set config.baseUrl.openAiCompat or the OPENAI_COMPAT_BASE_URL environment variable.");
63
+ }
64
+ return openaiEmbed(inputs, config, apiKey, baseURL);
65
+ }
38
66
  default: {
39
67
  const custom = registeredEmbedProviders[provider];
40
68
  if (custom) {
@@ -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
+ }
@@ -0,0 +1,9 @@
1
+ import { ImageInput, ImageConfig, ImageGenResult } from "../image.js";
2
+ import { Result } from "../types/result.js";
3
+ /**
4
+ * Generic OpenAI-compatible image generation. Used by litellm/openai-compat —
5
+ * any backend that speaks the OpenAI `/images/generations` shape. Image edits
6
+ * are not supported here (the multipart upload shape varies between backends);
7
+ * pass a plain prompt only.
8
+ */
9
+ export declare function openaiCompatImage(input: ImageInput, config: ImageConfig, apiKey: string, baseURL: string): Promise<Result<ImageGenResult>>;
@@ -0,0 +1,60 @@
1
+ import OpenAI from "openai";
2
+ import { success, failure } from "../types/result.js";
3
+ import { omitUndefined } from "../util/util.js";
4
+ /**
5
+ * Generic OpenAI-compatible image generation. Used by litellm/openai-compat —
6
+ * any backend that speaks the OpenAI `/images/generations` shape. Image edits
7
+ * are not supported here (the multipart upload shape varies between backends);
8
+ * pass a plain prompt only.
9
+ */
10
+ export async function openaiCompatImage(input, config, apiKey, baseURL) {
11
+ try {
12
+ const normalized = typeof input === "string" ? { prompt: input } : input;
13
+ const hasImages = !!(normalized.images && normalized.images.length > 0);
14
+ if (hasImages) {
15
+ return failure("openai-compat: image edits (passing `images`) are not supported on this generic adapter. Use the `openai` provider for OpenAI image edits.");
16
+ }
17
+ if (normalized.mask) {
18
+ return failure("openai-compat: image masks are not supported on this generic adapter.");
19
+ }
20
+ const client = new OpenAI({ apiKey, baseURL });
21
+ const params = omitUndefined({
22
+ model: config.model,
23
+ prompt: normalized.prompt,
24
+ n: config.n,
25
+ size: config.size,
26
+ quality: config.quality,
27
+ output_format: config.outputFormat,
28
+ background: config.background,
29
+ ...(config.metadata ?? {}),
30
+ });
31
+ const response = await client.images.generate(params);
32
+ const mimeType = mimeFromFormat(config.outputFormat) ?? "image/png";
33
+ const images = (response.data ?? []).map((d) => ({
34
+ data: new Uint8Array(Buffer.from(d.b64_json, "base64")),
35
+ mimeType,
36
+ revisedPrompt: d.revised_prompt,
37
+ }));
38
+ return success({
39
+ images,
40
+ model: config.model,
41
+ });
42
+ }
43
+ catch (err) {
44
+ return failure(err instanceof Error
45
+ ? err.message
46
+ : "OpenAI-compatible image request failed");
47
+ }
48
+ }
49
+ function mimeFromFormat(format) {
50
+ switch (format) {
51
+ case "png":
52
+ return "image/png";
53
+ case "jpeg":
54
+ return "image/jpeg";
55
+ case "webp":
56
+ return "image/webp";
57
+ default:
58
+ return undefined;
59
+ }
60
+ }
package/dist/image.d.ts CHANGED
@@ -17,8 +17,19 @@ export type ImageConfig = {
17
17
  quality?: "low" | "medium" | "high" | "auto";
18
18
  outputFormat?: "png" | "jpeg" | "webp";
19
19
  background?: "transparent" | "opaque" | "auto";
20
- openAiApiKey?: string;
21
- googleApiKey?: string;
20
+ /** API keys, nested by provider. Falls back to env vars
21
+ * (OPENAI_API_KEY / GEMINI_API_KEY / LITELLM_API_KEY / OPENAI_COMPAT_API_KEY). */
22
+ apiKey?: {
23
+ openAi?: string;
24
+ google?: string;
25
+ liteLlm?: string;
26
+ openAiCompat?: string;
27
+ };
28
+ /** Custom base URLs, nested by provider. */
29
+ baseUrl?: {
30
+ liteLlm?: string;
31
+ openAiCompat?: string;
32
+ };
22
33
  metadata?: Record<string, unknown>;
23
34
  modelData?: ModelDataBlob;
24
35
  };