smoltalk 0.15.3 → 0.17.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 (54) hide show
  1. package/README.md +62 -0
  2. package/dist/clients/anthropic.js +3 -2
  3. package/dist/clients/baseClient.js +4 -3
  4. package/dist/clients/google.js +2 -1
  5. package/dist/clients/llamaCppLoader.js +2 -2
  6. package/dist/clients/mlx.d.ts +12 -1
  7. package/dist/clients/mlx.js +23 -0
  8. package/dist/clients/ollama.d.ts +1 -1
  9. package/dist/clients/ollama.js +1 -1
  10. package/dist/clients/openai.d.ts +2 -1
  11. package/dist/clients/openai.js +5 -3
  12. package/dist/clients/openaiResponses.js +3 -2
  13. package/dist/clients/resolveAttachments.js +2 -1
  14. package/dist/clients/structuredAfterTools.d.ts +36 -0
  15. package/dist/clients/structuredAfterTools.js +87 -0
  16. package/dist/clients/validationRetry.d.ts +5 -0
  17. package/dist/clients/validationRetry.js +24 -0
  18. package/dist/embed/openai.js +3 -2
  19. package/dist/files/anthropic.js +2 -2
  20. package/dist/files/openai.js +2 -2
  21. package/dist/image/google.js +3 -2
  22. package/dist/image/openai.js +3 -2
  23. package/dist/image/openaiCompat.js +3 -2
  24. package/dist/index.d.ts +1 -0
  25. package/dist/index.js +2 -0
  26. package/dist/modelRefresh.js +10 -11
  27. package/dist/models.d.ts +298 -56
  28. package/dist/models.js +331 -66
  29. package/dist/platform.browser.d.ts +5 -0
  30. package/dist/platform.browser.js +13 -0
  31. package/dist/platform.d.ts +33 -0
  32. package/dist/platform.js +13 -0
  33. package/dist/platform.node.d.ts +5 -0
  34. package/dist/platform.node.js +17 -0
  35. package/dist/speech/google.js +2 -1
  36. package/dist/speech/groq.js +1 -0
  37. package/dist/speech/mlx.js +1 -0
  38. package/dist/speech/openai.js +1 -1
  39. package/dist/speech/openaiCompat.js +1 -1
  40. package/dist/statelogClient.js +2 -1
  41. package/dist/transcription/google.js +3 -2
  42. package/dist/transcription/groq.js +1 -0
  43. package/dist/transcription/openai.js +1 -1
  44. package/dist/transcription/openaiCompat.js +1 -1
  45. package/dist/types.d.ts +6 -0
  46. package/dist/util/attachments.js +2 -1
  47. package/dist/util/base64.d.ts +17 -0
  48. package/dist/util/base64.js +59 -0
  49. package/dist/util/blobRef.js +18 -12
  50. package/dist/util/fileExtension.d.ts +7 -0
  51. package/dist/util/fileExtension.js +15 -0
  52. package/dist/util/provider.d.ts +1 -0
  53. package/dist/util/provider.js +16 -14
  54. package/package.json +14 -2
@@ -0,0 +1,5 @@
1
+ import type { Platform } from "./platform.js";
2
+ export declare const readFileBytes: Platform["readFileBytes"];
3
+ export declare const readTextFromFileUrl: Platform["readTextFromFileUrl"];
4
+ export declare const pathToFileUrl: Platform["pathToFileUrl"];
5
+ export declare const env: Platform["env"];
@@ -0,0 +1,13 @@
1
+ function unavailable(fn) {
2
+ return new Error(`${fn} is not available in a browser build of smoltalk.`);
3
+ }
4
+ export const readFileBytes = async () => {
5
+ throw unavailable("readFileBytes");
6
+ };
7
+ export const readTextFromFileUrl = async () => {
8
+ throw unavailable("readTextFromFileUrl");
9
+ };
10
+ export const pathToFileUrl = () => {
11
+ throw unavailable("pathToFileUrl");
12
+ };
13
+ export const env = () => undefined;
@@ -0,0 +1,33 @@
1
+ /**
2
+ * The functions smoltalk takes from the platform it runs on. Two files
3
+ * implement them, `platform.node.ts` and `platform.browser.ts`, and the
4
+ * `#platform` entry in package.json's `imports` picks one: Node resolves
5
+ * it to the Node file, and a bundler run with its browser condition
6
+ * (esbuild `--platform=browser`, Vite, webpack `target: "web"`) picks the
7
+ * browser file. Shared code imports `#platform` and never `node:*`.
8
+ *
9
+ * The browser file throws from every file function and answers `undefined`
10
+ * for every environment variable, so a browser caller passes API keys in
11
+ * and cannot use path attachments, `file:` model data, or the llama.cpp
12
+ * loader's `entryPath`. Pure string work such as a path's extension is not
13
+ * on the platform; see `lib/util/fileExtension.ts`.
14
+ */
15
+ export type Platform = {
16
+ /** The bytes of the file at `path`. With `maxBytes`, refuses a larger
17
+ * file before reading it, with a {@link FileTooLargeError}. */
18
+ readFileBytes(path: string, maxBytes?: number): Promise<Uint8Array>;
19
+ /** The text of the file a `file:` URL names, as UTF-8. */
20
+ readTextFromFileUrl(url: string): Promise<string>;
21
+ /** The `file:` URL for a file system path. */
22
+ pathToFileUrl(path: string): string;
23
+ /** The environment variable `name`, or `undefined` when unset or when
24
+ * the platform has no environment. */
25
+ env(name: string): string | undefined;
26
+ };
27
+ /** Thrown by `readFileBytes` when the file is larger than `maxBytes`. */
28
+ export declare class FileTooLargeError extends Error {
29
+ readonly path: string;
30
+ readonly size: number;
31
+ readonly maxBytes: number;
32
+ constructor(path: string, size: number, maxBytes: number);
33
+ }
@@ -0,0 +1,13 @@
1
+ /** Thrown by `readFileBytes` when the file is larger than `maxBytes`. */
2
+ export class FileTooLargeError extends Error {
3
+ path;
4
+ size;
5
+ maxBytes;
6
+ constructor(path, size, maxBytes) {
7
+ super(`File ${path} is ${size} bytes, over the limit of ${maxBytes} bytes.`);
8
+ this.path = path;
9
+ this.size = size;
10
+ this.maxBytes = maxBytes;
11
+ this.name = "FileTooLargeError";
12
+ }
13
+ }
@@ -0,0 +1,5 @@
1
+ import { type Platform } from "./platform.js";
2
+ export declare const readFileBytes: Platform["readFileBytes"];
3
+ export declare const readTextFromFileUrl: Platform["readTextFromFileUrl"];
4
+ export declare const pathToFileUrl: Platform["pathToFileUrl"];
5
+ export declare const env: Platform["env"];
@@ -0,0 +1,17 @@
1
+ // The Node side of #platform. See platform.ts.
2
+ import { readFile, stat } from "node:fs/promises";
3
+ import { fileURLToPath, pathToFileURL } from "node:url";
4
+ import { FileTooLargeError } from "./platform.js";
5
+ export const readFileBytes = async (path, maxBytes) => {
6
+ if (maxBytes !== undefined) {
7
+ const info = await stat(path);
8
+ if (info.size > maxBytes) {
9
+ throw new FileTooLargeError(path, info.size, maxBytes);
10
+ }
11
+ }
12
+ const buf = await readFile(path);
13
+ return new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength);
14
+ };
15
+ export const readTextFromFileUrl = (url) => readFile(fileURLToPath(url), "utf8");
16
+ export const pathToFileUrl = (path) => pathToFileURL(path).href;
17
+ export const env = (name) => process.env[name];
@@ -1,3 +1,4 @@
1
+ import { base64ToBytes } from "../util/base64.js";
1
2
  import { GoogleGenAI } from "@google/genai";
2
3
  import { success, failure } from "../types/result.js";
3
4
  import { pcmToWav } from "../util/audioMime.js";
@@ -36,7 +37,7 @@ export class GoogleSpeechClient extends BaseSpeechClient {
36
37
  if (!dataB64) {
37
38
  return failure("Gemini returned no audio data.");
38
39
  }
39
- const pcm = new Uint8Array(Buffer.from(dataB64, "base64"));
40
+ const pcm = base64ToBytes(dataB64);
40
41
  let audio = pcm;
41
42
  let mimeType = "application/octet-stream";
42
43
  if (format === "wav") {
@@ -6,6 +6,7 @@ import { OpenAISpeechClient } from "./openai.js";
6
6
  export class GroqSpeechClient extends OpenAISpeechClient {
7
7
  makeClient() {
8
8
  return new OpenAI({
9
+ dangerouslyAllowBrowser: true,
9
10
  apiKey: this.config.apiKey,
10
11
  baseURL: "https://api.groq.com/openai/v1",
11
12
  });
@@ -21,6 +21,7 @@ export class MlxSpeechClient extends OpenAISpeechClient {
21
21
  const baseURL = resolveBaseUrl("mlx", { baseUrl: this.config.baseUrl });
22
22
  // The OpenAI SDK refuses an empty key. The local server never reads it.
23
23
  return new OpenAI({
24
+ dangerouslyAllowBrowser: true,
24
25
  apiKey: "mlx-local",
25
26
  baseURL,
26
27
  maxRetries: 0,
@@ -5,7 +5,7 @@ import { BaseSpeechClient } from "./baseSpeechClient.js";
5
5
  export class OpenAISpeechClient extends BaseSpeechClient {
6
6
  /** Build the OpenAI SDK client. Subclasses override to point at a compatible base URL. */
7
7
  makeClient() {
8
- return new OpenAI({ apiKey: this.config.apiKey });
8
+ return new OpenAI({ dangerouslyAllowBrowser: true, apiKey: this.config.apiKey });
9
9
  }
10
10
  /** Provider default used when the declarative call omits format. */
11
11
  defaultFormat() {
@@ -14,7 +14,7 @@ export class OpenAiCompatSpeechClient extends OpenAISpeechClient {
14
14
  if (!baseURL) {
15
15
  throw new Error("openai-compat: base URL required (config.baseUrl.openAiCompat or OPENAI_COMPAT_BASE_URL).");
16
16
  }
17
- return new OpenAI({ apiKey: this.config.apiKey, baseURL });
17
+ return new OpenAI({ dangerouslyAllowBrowser: true, apiKey: this.config.apiKey, baseURL });
18
18
  }
19
19
  noKeyMessage() {
20
20
  return "No API key provided. Set apiKey.openAiCompat or OPENAI_COMPAT_API_KEY.";
@@ -1,3 +1,4 @@
1
+ import { env } from "#platform";
1
2
  import { nanoid } from "nanoid";
2
3
  import { failure, mergeResults, success } from "./types/result.js";
3
4
  import { redactAttachments } from "./util/redact.js";
@@ -311,7 +312,7 @@ export function getStatelogClient(config) {
311
312
  const statelogConfig = {
312
313
  host: config.host,
313
314
  traceId: config.traceId || nanoid(),
314
- apiKey: process.env.STATELOG_SMOLTALK_API_KEY || "",
315
+ apiKey: env("STATELOG_SMOLTALK_API_KEY") || "",
315
316
  projectId: config.projectId,
316
317
  debugMode: config.debugMode || false,
317
318
  };
@@ -1,3 +1,4 @@
1
+ import { bytesToBase64, utf8ByteLength } from "../util/base64.js";
1
2
  import { GoogleGenAI } from "@google/genai";
2
3
  import { success, failure } from "../types/result.js";
3
4
  import { BaseTranscriptionClient } from "./baseTranscriptionClient.js";
@@ -22,7 +23,7 @@ export class GoogleTranscriptionClient extends BaseTranscriptionClient {
22
23
  if (this.config.prompt) {
23
24
  instruction += ` ${this.config.prompt}`;
24
25
  }
25
- const base64 = Buffer.from(data).toString("base64");
26
+ const base64 = bytesToBase64(data);
26
27
  const request = {
27
28
  model: this.config.model,
28
29
  contents: [{
@@ -33,7 +34,7 @@ export class GoogleTranscriptionClient extends BaseTranscriptionClient {
33
34
  ],
34
35
  }],
35
36
  };
36
- if (Buffer.byteLength(JSON.stringify(request), "utf8") > GOOGLE_INLINE_REQUEST_MAX_BYTES) {
37
+ if (utf8ByteLength(JSON.stringify(request)) > GOOGLE_INLINE_REQUEST_MAX_BYTES) {
37
38
  return failure("Audio and instructions exceed Gemini's 20 MB inline request limit; use a smaller source.");
38
39
  }
39
40
  const ai = new GoogleGenAI({ apiKey: this.config.apiKey });
@@ -7,6 +7,7 @@ import { OpenAITranscriptionClient } from "./openai.js";
7
7
  export class GroqTranscriptionClient extends OpenAITranscriptionClient {
8
8
  makeClient() {
9
9
  return new OpenAI({
10
+ dangerouslyAllowBrowser: true,
10
11
  apiKey: this.config.apiKey,
11
12
  baseURL: "https://api.groq.com/openai/v1",
12
13
  });
@@ -5,7 +5,7 @@ import { BaseTranscriptionClient } from "./baseTranscriptionClient.js";
5
5
  export class OpenAITranscriptionClient extends BaseTranscriptionClient {
6
6
  /** Build the OpenAI SDK client. Subclasses override to point at a compatible base URL. */
7
7
  makeClient() {
8
- return new OpenAI({ apiKey: this.config.apiKey });
8
+ return new OpenAI({ dangerouslyAllowBrowser: true, apiKey: this.config.apiKey });
9
9
  }
10
10
  /** Provider-specific diagnostic when no API key is resolved. Subclasses override. */
11
11
  noKeyMessage() {
@@ -14,7 +14,7 @@ export class OpenAiCompatTranscriptionClient extends OpenAITranscriptionClient {
14
14
  if (!baseURL) {
15
15
  throw new Error("openai-compat: base URL required (config.baseUrl.openAiCompat or OPENAI_COMPAT_BASE_URL).");
16
16
  }
17
- return new OpenAI({ apiKey: this.config.apiKey, baseURL });
17
+ return new OpenAI({ dangerouslyAllowBrowser: true, apiKey: this.config.apiKey, baseURL });
18
18
  }
19
19
  noKeyMessage() {
20
20
  return "No API key provided. Set apiKey.openAiCompat or OPENAI_COMPAT_API_KEY.";
package/dist/types.d.ts CHANGED
@@ -119,6 +119,12 @@ export type SmolConfig = {
119
119
  strict: boolean;
120
120
  numRetries: number;
121
121
  allowExtraKeys: boolean;
122
+ /** Local providers (`mlx`, `llama-cpp`) cannot enforce a schema on a
123
+ * request that carries tools, so a call with both is made as two
124
+ * requests: the tool round first, then the reply in the schema. Set
125
+ * this to `false` to send one request and leave the schema unenforced.
126
+ * Defaults to `true`. Google always makes two requests. */
127
+ separateFromTools: boolean;
122
128
  }>;
123
129
  /** Arbitrary provider-specific attributes passed directly to the underlying API call. */
124
130
  rawAttributes?: Record<string, any>;
@@ -1,3 +1,4 @@
1
+ import { bytesToBase64 } from "./base64.js";
1
2
  /** Filename sent to providers that require one when a file part omits its own. */
2
3
  export const DEFAULT_ATTACHMENT_FILENAME = "attachment.pdf";
3
4
  /** A file part's filename, falling back to the shared default when unset. */
@@ -17,7 +18,7 @@ export function refToBase64(ref) {
17
18
  return { base64: ref.base64, mimeType: ref.mimeType };
18
19
  }
19
20
  if (ref.kind === "bytes") {
20
- return { base64: Buffer.from(ref.data).toString("base64"), mimeType: ref.mimeType };
21
+ return { base64: bytesToBase64(ref.data), mimeType: ref.mimeType };
21
22
  }
22
23
  throw new Error(`Attachment ref must be resolved to base64 or bytes before serialization ` +
23
24
  `(got kind="${ref.kind}"). This is a smoltalk bug.`);
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Bytes to and from base64, and two other byte helpers, written without
3
+ * `Buffer` so they run in a browser. `Uint8Array.fromBase64` and
4
+ * `toBase64` are used where the runtime has them, and `atob`/`btoa` with a
5
+ * loop elsewhere.
6
+ */
7
+ export declare function bytesToBase64(bytes: Uint8Array): string;
8
+ /**
9
+ * Decodes standard base64 and base64url (`-` and `_` for `+` and `/`),
10
+ * with or without padding. Any other character throws. `Buffer`, which
11
+ * this replaced, skipped characters it did not know.
12
+ */
13
+ export declare function base64ToBytes(text: string): Uint8Array;
14
+ /** One array holding every chunk in order. */
15
+ export declare function concatBytes(chunks: Uint8Array[], total?: number): Uint8Array;
16
+ /** The number of bytes `text` takes as UTF-8. */
17
+ export declare function utf8ByteLength(text: string): number;
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Bytes to and from base64, and two other byte helpers, written without
3
+ * `Buffer` so they run in a browser. `Uint8Array.fromBase64` and
4
+ * `toBase64` are used where the runtime has them, and `atob`/`btoa` with a
5
+ * loop elsewhere.
6
+ */
7
+ export function bytesToBase64(bytes) {
8
+ const native = bytes.toBase64;
9
+ if (typeof native === "function") {
10
+ return native.call(bytes);
11
+ }
12
+ let binary = "";
13
+ const chunk = 0x8000;
14
+ for (let i = 0; i < bytes.length; i += chunk) {
15
+ binary += String.fromCharCode(...bytes.subarray(i, i + chunk));
16
+ }
17
+ return btoa(binary);
18
+ }
19
+ /**
20
+ * Decodes standard base64 and base64url (`-` and `_` for `+` and `/`),
21
+ * with or without padding. Any other character throws. `Buffer`, which
22
+ * this replaced, skipped characters it did not know.
23
+ */
24
+ export function base64ToBytes(text) {
25
+ if (text.includes("-") || text.includes("_")) {
26
+ text = text.replace(/-/g, "+").replace(/_/g, "/");
27
+ }
28
+ const native = Uint8Array.fromBase64;
29
+ if (typeof native === "function") {
30
+ return native.call(Uint8Array, text);
31
+ }
32
+ const binary = atob(text);
33
+ const out = new Uint8Array(binary.length);
34
+ for (let i = 0; i < binary.length; i++) {
35
+ out[i] = binary.charCodeAt(i);
36
+ }
37
+ return out;
38
+ }
39
+ /** One array holding every chunk in order. */
40
+ export function concatBytes(chunks, total) {
41
+ let length = total;
42
+ if (length === undefined) {
43
+ length = 0;
44
+ for (const chunk of chunks) {
45
+ length += chunk.byteLength;
46
+ }
47
+ }
48
+ const out = new Uint8Array(length);
49
+ let offset = 0;
50
+ for (const chunk of chunks) {
51
+ out.set(chunk, offset);
52
+ offset += chunk.byteLength;
53
+ }
54
+ return out;
55
+ }
56
+ /** The number of bytes `text` takes as UTF-8. */
57
+ export function utf8ByteLength(text) {
58
+ return new TextEncoder().encode(text).byteLength;
59
+ }
@@ -1,5 +1,7 @@
1
- import { readFile, stat } from "node:fs/promises";
2
- import { extname } from "node:path";
1
+ import { readFileBytes } from "#platform";
2
+ import { FileTooLargeError } from "../platform.js";
3
+ import { base64ToBytes, concatBytes } from "./base64.js";
4
+ import { fileExtension } from "./fileExtension.js";
3
5
  import { EXT_TO_MIME } from "./mime.js";
4
6
  /** Default timeout for fetching attachment URLs during normalization (60 seconds). */
5
7
  export const DEFAULT_FETCH_TIMEOUT_MS = 60_000;
@@ -62,7 +64,7 @@ async function readBodyWithLimit(res, maxBytes) {
62
64
  chunks.push(value);
63
65
  }
64
66
  // Pass the known total so concat doesn't re-scan the chunk list for its length.
65
- return new Uint8Array(Buffer.concat(chunks, total));
67
+ return concatBytes(chunks, total);
66
68
  }
67
69
  async function loadRef(ref, allowed, maxBytes) {
68
70
  switch (ref.kind) {
@@ -70,26 +72,30 @@ async function loadRef(ref, allowed, maxBytes) {
70
72
  return { data: ref.data, mimeType: ref.mimeType };
71
73
  case "base64":
72
74
  return {
73
- data: new Uint8Array(Buffer.from(ref.base64, "base64")),
75
+ data: base64ToBytes(ref.base64),
74
76
  mimeType: ref.mimeType,
75
77
  };
76
78
  case "path": {
77
- // S1: bound memory — reject on stat before reading the whole file.
78
- if (maxBytes !== undefined) {
79
- const info = await stat(ref.path);
80
- if (info.size > maxBytes) {
81
- throw tooLargeError(maxBytes, info.size);
79
+ // S1: bound memory — readFileBytes rejects on the file's size before
80
+ // reading it. In a browser it throws: there is no file system.
81
+ let buf;
82
+ try {
83
+ buf = await readFileBytes(ref.path, maxBytes);
84
+ }
85
+ catch (err) {
86
+ if (err instanceof FileTooLargeError) {
87
+ throw tooLargeError(err.maxBytes, err.size);
82
88
  }
89
+ throw err;
83
90
  }
84
- const buf = await readFile(ref.path);
85
- const ext = extname(ref.path).toLowerCase();
91
+ const ext = fileExtension(ref.path);
86
92
  const inferred = EXT_TO_MIME[ext];
87
93
  const mimeType = ref.mimeType ?? inferred;
88
94
  if (allowed !== null && (!mimeType || !isAllowedMime(mimeType, allowed))) {
89
95
  throw new Error(`Could not determine an allowed MIME type for path "${ref.path}". ` +
90
96
  `Allowed: ${allowed.join(", ")}. Pass an explicit mimeType on the BlobRef.`);
91
97
  }
92
- return { data: new Uint8Array(buf), mimeType };
98
+ return { data: buf, mimeType };
93
99
  }
94
100
  case "url": {
95
101
  // S2: only http(s) — reject file:/ftp:/gopher:/data: etc.
@@ -0,0 +1,7 @@
1
+ /**
2
+ * The extension of a path including its dot, lower-cased, or "" when there
3
+ * is none. Follows Node's `path.extname`: only the last segment counts, a
4
+ * leading dot (`.bashrc`) is not an extension, and a trailing dot gives ".", and ".." gives "".
5
+ * String work only, so it is the same on every platform.
6
+ */
7
+ export declare function fileExtension(path: string): string;
@@ -0,0 +1,15 @@
1
+ /**
2
+ * The extension of a path including its dot, lower-cased, or "" when there
3
+ * is none. Follows Node's `path.extname`: only the last segment counts, a
4
+ * leading dot (`.bashrc`) is not an extension, and a trailing dot gives ".", and ".." gives "".
5
+ * String work only, so it is the same on every platform.
6
+ */
7
+ export function fileExtension(path) {
8
+ const slash = Math.max(path.lastIndexOf("/"), path.lastIndexOf("\\"));
9
+ const base = path.slice(slash + 1);
10
+ const dot = base.lastIndexOf(".");
11
+ if (dot <= 0 || base === "..") {
12
+ return "";
13
+ }
14
+ return base.slice(dot).toLowerCase();
15
+ }
@@ -36,6 +36,7 @@ type NestedKeyConfig = {
36
36
  };
37
37
  /**
38
38
  * Resolve the API key for a provider, checking config then env vars.
39
+ * In a browser `env` answers nothing, so the caller passes the key.
39
40
  */
40
41
  export declare function resolveApiKey(provider: string, config: NestedKeyConfig): string | undefined;
41
42
  /**
@@ -1,3 +1,4 @@
1
+ import { env } from "#platform";
1
2
  import { getModel } from "../models.js";
2
3
  import { SmolError } from "../smolError.js";
3
4
  /**
@@ -16,31 +17,32 @@ export function resolveProvider(modelName, explicitProvider, modelData) {
16
17
  }
17
18
  /**
18
19
  * Resolve the API key for a provider, checking config then env vars.
20
+ * In a browser `env` answers nothing, so the caller passes the key.
19
21
  */
20
22
  export function resolveApiKey(provider, config) {
21
23
  const k = config.apiKey;
22
24
  switch (provider) {
23
25
  case "openai":
24
26
  case "openai-responses":
25
- return k?.openAi || process.env.OPENAI_API_KEY;
27
+ return k?.openAi || env("OPENAI_API_KEY");
26
28
  case "google":
27
- return k?.google || process.env.GEMINI_API_KEY;
29
+ return k?.google || env("GEMINI_API_KEY");
28
30
  case "anthropic":
29
- return k?.anthropic || process.env.ANTHROPIC_API_KEY;
31
+ return k?.anthropic || env("ANTHROPIC_API_KEY");
30
32
  case "ollama":
31
33
  return k?.ollama;
32
34
  case "openrouter":
33
- return k?.openRouter || process.env.OPENROUTER_API_KEY;
35
+ return k?.openRouter || env("OPENROUTER_API_KEY");
34
36
  case "deepinfra":
35
- return k?.deepInfra || process.env.DEEPINFRA_API_KEY;
37
+ return k?.deepInfra || env("DEEPINFRA_API_KEY");
36
38
  case "litellm":
37
- return k?.liteLlm || process.env.LITELLM_API_KEY;
39
+ return k?.liteLlm || env("LITELLM_API_KEY");
38
40
  case "openai-compat":
39
- return k?.openAiCompat || process.env.OPENAI_COMPAT_API_KEY;
41
+ return k?.openAiCompat || env("OPENAI_COMPAT_API_KEY");
40
42
  case "groq":
41
- return k?.groq || process.env.GROQ_API_KEY;
43
+ return k?.groq || env("GROQ_API_KEY");
42
44
  case "typesafe":
43
- return k?.typesafe || process.env.TYPESAFE_API_KEY;
45
+ return k?.typesafe || env("TYPESAFE_API_KEY");
44
46
  default:
45
47
  return config.apiKey?.[provider];
46
48
  }
@@ -57,20 +59,20 @@ export function resolveBaseUrl(provider, config) {
57
59
  const b = config.baseUrl;
58
60
  switch (provider) {
59
61
  case "ollama":
60
- return b?.ollama || process.env.OLLAMA_HOST;
62
+ return b?.ollama || env("OLLAMA_HOST");
61
63
  case "openrouter":
62
64
  return b?.openRouter || "https://openrouter.ai/api/v1";
63
65
  case "deepinfra":
64
66
  return b?.deepInfra || "https://api.deepinfra.com/v1/openai";
65
67
  case "litellm":
66
- return b?.liteLlm || process.env.LITELLM_BASE_URL;
68
+ return b?.liteLlm || env("LITELLM_BASE_URL");
67
69
  case "openai-compat":
68
- return b?.openAiCompat || process.env.OPENAI_COMPAT_BASE_URL;
70
+ return b?.openAiCompat || env("OPENAI_COMPAT_BASE_URL");
69
71
  case "mlx":
70
- return b?.mlx || process.env.MLX_BASE_URL || "http://127.0.0.1:8080/v1";
72
+ return b?.mlx || env("MLX_BASE_URL") || "http://127.0.0.1:8080/v1";
71
73
  case "typesafe":
72
74
  // A Laya server (`laya-serve`) speaks the same protocol; point this at it.
73
- return b?.typesafe || process.env.TYPESAFE_BASE_URL || "https://api.typesafe.ai";
75
+ return b?.typesafe || env("TYPESAFE_BASE_URL") || "https://api.typesafe.ai";
74
76
  default:
75
77
  return undefined;
76
78
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "smoltalk",
3
- "version": "0.15.3",
3
+ "version": "0.17.0",
4
4
  "description": "A common interface for LLM APIs",
5
5
  "homepage": "https://github.com/egonSchiele/smoltalk",
6
6
  "files": [
@@ -23,6 +23,16 @@
23
23
  "require": "./dist/testing/index.js"
24
24
  }
25
25
  },
26
+ "imports": {
27
+ "#platform": {
28
+ "browser": "./dist/platform.browser.js",
29
+ "default": "./dist/platform.node.js"
30
+ },
31
+ "#ollama": {
32
+ "browser": "ollama/browser",
33
+ "default": "ollama"
34
+ }
35
+ },
26
36
  "type": "module",
27
37
  "types": "./dist/index.d.ts",
28
38
  "keywords": [
@@ -41,6 +51,7 @@
41
51
  "zod": "^4.4.3"
42
52
  },
43
53
  "devDependencies": {
54
+ "esbuild": "^0.28.2",
44
55
  "tsx": "^4.19.2"
45
56
  },
46
57
  "peerDependencies": {
@@ -60,6 +71,7 @@
60
71
  "doc": "typedoc --disableSources --out docs lib && prettier docs/ --write",
61
72
  "typecheck": "tsc --noEmit",
62
73
  "seed-data": "tsx scripts/seed-model-data.ts",
63
- "refresh-data": "tsx scripts/refresh-from-modelsdev.ts"
74
+ "refresh-data": "tsx scripts/refresh-from-modelsdev.ts",
75
+ "check:browser": "node scripts/check-browser-bundle.mjs"
64
76
  }
65
77
  }