smoltalk 0.8.4 → 0.10.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 (75) hide show
  1. package/README.md +145 -18
  2. package/dist/classes/ToolCall.js +18 -10
  3. package/dist/classes/message/AssistantMessage.d.ts +2 -0
  4. package/dist/classes/message/ToolMessage.js +13 -10
  5. package/dist/classes/message/UserMessage.d.ts +21 -0
  6. package/dist/classes/message/UserMessage.js +3 -0
  7. package/dist/classes/message/contentParts.d.ts +71 -2
  8. package/dist/classes/message/contentParts.js +6 -0
  9. package/dist/classes/message/index.d.ts +5 -2
  10. package/dist/classes/message/index.js +7 -0
  11. package/dist/classes/message/renderers/AnthropicRenderer.d.ts +2 -1
  12. package/dist/classes/message/renderers/AnthropicRenderer.js +3 -0
  13. package/dist/classes/message/renderers/GoogleRenderer.d.ts +2 -1
  14. package/dist/classes/message/renderers/GoogleRenderer.js +3 -0
  15. package/dist/classes/message/renderers/JSONRenderer.d.ts +2 -1
  16. package/dist/classes/message/renderers/JSONRenderer.js +4 -0
  17. package/dist/classes/message/renderers/OpenAIChatRenderer.d.ts +8 -1
  18. package/dist/classes/message/renderers/OpenAIChatRenderer.js +18 -0
  19. package/dist/classes/message/renderers/OpenAIResponsesRenderer.d.ts +2 -1
  20. package/dist/classes/message/renderers/OpenAIResponsesRenderer.js +3 -0
  21. package/dist/classes/message/renderers/PartRenderer.d.ts +3 -2
  22. package/dist/classes/message/renderers/PartRenderer.js +3 -0
  23. package/dist/client.js +1 -0
  24. package/dist/clients/anthropic.js +1 -1
  25. package/dist/clients/baseClient.d.ts +13 -1
  26. package/dist/clients/baseClient.js +36 -7
  27. package/dist/clients/google.d.ts +2 -0
  28. package/dist/clients/google.js +125 -3
  29. package/dist/clients/ollama.js +1 -1
  30. package/dist/clients/openai.d.ts +2 -1
  31. package/dist/clients/openai.js +15 -3
  32. package/dist/clients/openaiCompat.d.ts +2 -0
  33. package/dist/clients/openaiCompat.js +5 -0
  34. package/dist/clients/openaiResponses.js +1 -1
  35. package/dist/clients/resolveAttachments.d.ts +8 -4
  36. package/dist/clients/resolveAttachments.js +101 -50
  37. package/dist/embed.d.ts +4 -0
  38. package/dist/files.d.ts +1 -1
  39. package/dist/files.js +1 -1
  40. package/dist/image/google.js +2 -2
  41. package/dist/image/openai.js +3 -3
  42. package/dist/image.d.ts +1 -1
  43. package/dist/index.d.ts +10 -2
  44. package/dist/index.js +7 -1
  45. package/dist/model.d.ts +15 -4
  46. package/dist/model.js +48 -7
  47. package/dist/models.d.ts +143 -19
  48. package/dist/models.js +137 -30
  49. package/dist/speech/baseSpeechClient.d.ts +31 -0
  50. package/dist/speech/baseSpeechClient.js +98 -0
  51. package/dist/speech/openai.d.ts +6 -0
  52. package/dist/speech/openai.js +39 -0
  53. package/dist/speech.d.ts +40 -0
  54. package/dist/speech.js +57 -0
  55. package/dist/transcription/baseTranscriptionClient.d.ts +31 -0
  56. package/dist/transcription/baseTranscriptionClient.js +107 -0
  57. package/dist/transcription/openai.d.ts +6 -0
  58. package/dist/transcription/openai.js +59 -0
  59. package/dist/transcription.d.ts +51 -0
  60. package/dist/transcription.js +58 -0
  61. package/dist/types/tokenUsage.d.ts +4 -0
  62. package/dist/types/tokenUsage.js +4 -0
  63. package/dist/types.d.ts +3 -0
  64. package/dist/util/attachments.d.ts +1 -1
  65. package/dist/util/audioMime.d.ts +9 -0
  66. package/dist/util/audioMime.js +33 -0
  67. package/dist/util/{imageRef.d.ts → blobRef.d.ts} +9 -9
  68. package/dist/util/{imageRef.js → blobRef.js} +6 -13
  69. package/dist/util/mime.d.ts +21 -0
  70. package/dist/util/mime.js +52 -0
  71. package/dist/util/modalities.d.ts +6 -2
  72. package/dist/util/modalities.js +13 -15
  73. package/dist/util/provider.d.ts +2 -0
  74. package/dist/util/provider.js +1 -1
  75. package/package.json +1 -1
@@ -0,0 +1,6 @@
1
+ import { Result } from "../types/result.js";
2
+ import { BaseTranscriptionClient } from "./baseTranscriptionClient.js";
3
+ import type { TranscriptionResult } from "../transcription.js";
4
+ export declare class OpenAITranscriptionClient extends BaseTranscriptionClient {
5
+ protected _transcribe(data: Uint8Array, mimeType: string): Promise<Result<TranscriptionResult>>;
6
+ }
@@ -0,0 +1,59 @@
1
+ import OpenAI, { toFile } from "openai";
2
+ import { success, failure } from "../types/result.js";
3
+ import { transcriptionAudioType } from "../util/audioMime.js";
4
+ import { BaseTranscriptionClient } from "./baseTranscriptionClient.js";
5
+ export class OpenAITranscriptionClient extends BaseTranscriptionClient {
6
+ // No try/catch here: BaseTranscriptionClient.transcribe() is the single
7
+ // redacting/logging exception boundary.
8
+ async _transcribe(data, mimeType) {
9
+ if (!this.config.apiKey) {
10
+ return failure("No OpenAI API key provided. Set apiKey.openAi or OPENAI_API_KEY.");
11
+ }
12
+ // Filename is an OpenAI upload detail, not part of the provider-neutral
13
+ // operation contract. Derive the synthetic name from the normalized MIME.
14
+ const filename = transcriptionAudioType(mimeType)?.filename ?? "audio.bin";
15
+ const client = new OpenAI({ apiKey: this.config.apiKey });
16
+ const file = await toFile(data, filename, { type: mimeType });
17
+ const granularities = [];
18
+ if (this.config.timestampGranularity) {
19
+ granularities.push(this.config.timestampGranularity);
20
+ }
21
+ const requestBody = {
22
+ file,
23
+ model: this.config.model,
24
+ response_format: "verbose_json",
25
+ };
26
+ if (this.config.language) {
27
+ requestBody.language = this.config.language;
28
+ }
29
+ if (this.config.prompt) {
30
+ requestBody.prompt = this.config.prompt;
31
+ }
32
+ if (granularities.length > 0) {
33
+ requestBody.timestamp_granularities = granularities;
34
+ }
35
+ const res = (await client.audio.transcriptions.create(requestBody));
36
+ const result = { text: res.text, raw: res };
37
+ if (res.language) {
38
+ result.language = res.language;
39
+ }
40
+ if (typeof res.duration === "number") {
41
+ result.durationSeconds = res.duration;
42
+ }
43
+ if (Array.isArray(res.segments)) {
44
+ result.segments = res.segments.map((segment) => ({
45
+ start: segment.start,
46
+ end: segment.end,
47
+ text: segment.text,
48
+ }));
49
+ }
50
+ if (Array.isArray(res.words)) {
51
+ result.words = res.words.map((word) => ({
52
+ start: word.start,
53
+ end: word.end,
54
+ word: word.word,
55
+ }));
56
+ }
57
+ return success(result);
58
+ }
59
+ }
@@ -0,0 +1,51 @@
1
+ import type { ModelDataBlob } from "./modelData.js";
2
+ import type { SmolConfig } from "./types.js";
3
+ import { Result } from "./types/result.js";
4
+ import { TokenUsage } from "./types/tokenUsage.js";
5
+ import { CostEstimate } from "./types/costEstimate.js";
6
+ import { BlobRef } from "./util/blobRef.js";
7
+ import { BaseTranscriptionClient, TranscriptionClientConfig } from "./transcription/baseTranscriptionClient.js";
8
+ export { DEFAULT_TRANSCRIBE_BYTES } from "./transcription/baseTranscriptionClient.js";
9
+ export type TranscribeOptions = {
10
+ model: string;
11
+ provider?: string;
12
+ modelData?: ModelDataBlob;
13
+ apiKey?: SmolConfig["apiKey"];
14
+ language?: string;
15
+ prompt?: string;
16
+ timestampGranularity?: "segment" | "word";
17
+ maxBytes?: number;
18
+ metadata?: Record<string, unknown>;
19
+ };
20
+ export type TranscriptionSegment = {
21
+ start: number;
22
+ end: number;
23
+ text: string;
24
+ };
25
+ export type TranscriptionWord = {
26
+ start: number;
27
+ end: number;
28
+ word: string;
29
+ };
30
+ export type TranscriptionResult = {
31
+ text: string;
32
+ language?: string;
33
+ durationSeconds?: number;
34
+ segments?: TranscriptionSegment[];
35
+ words?: TranscriptionWord[];
36
+ usage?: TokenUsage;
37
+ cost?: CostEstimate;
38
+ raw?: unknown;
39
+ };
40
+ export type TranscriptionClientClass = new (config: TranscriptionClientConfig) => BaseTranscriptionClient;
41
+ export declare function registerTranscriptionProvider(name: string, cls: TranscriptionClientClass): void;
42
+ /** Test-only: clear all registered custom providers so registrations don't leak across tests. */
43
+ export declare function _resetForTests(): void;
44
+ /**
45
+ * Resolve provider + API key and instantiate the matching transcription client
46
+ * for the declarative transcribe() operation. Never throws: a custom client
47
+ * class's constructor can throw, and this internal factory's catch redacts the
48
+ * resolved key so a constructor error cannot leak through the public wrapper.
49
+ */
50
+ export declare function getTranscriptionClient(opts: TranscribeOptions): Result<BaseTranscriptionClient>;
51
+ export declare function transcribe(source: BlobRef, opts: TranscribeOptions): Promise<Result<TranscriptionResult>>;
@@ -0,0 +1,58 @@
1
+ import { success, failure } from "./types/result.js";
2
+ import { redactSecret } from "./util/redact.js";
3
+ import { getLogger } from "./util/logger.js";
4
+ import { resolveProvider, resolveApiKey } from "./util/provider.js";
5
+ import { OpenAITranscriptionClient } from "./transcription/openai.js";
6
+ export { DEFAULT_TRANSCRIBE_BYTES } from "./transcription/baseTranscriptionClient.js";
7
+ // Checked before the user registry so a registered "openai" can't hijack the built-in.
8
+ const builtinClients = Object.create(null);
9
+ builtinClients["openai"] = OpenAITranscriptionClient;
10
+ // Null-prototype so provider names like "toString"/"__proto__" can't collide
11
+ // with Object.prototype or pollute the registry.
12
+ const registered = Object.create(null);
13
+ export function registerTranscriptionProvider(name, cls) {
14
+ registered[name] = cls;
15
+ }
16
+ /** Test-only: clear all registered custom providers so registrations don't leak across tests. */
17
+ export function _resetForTests() {
18
+ for (const key of Object.keys(registered)) {
19
+ delete registered[key];
20
+ }
21
+ }
22
+ /**
23
+ * Resolve provider + API key and instantiate the matching transcription client
24
+ * for the declarative transcribe() operation. Never throws: a custom client
25
+ * class's constructor can throw, and this internal factory's catch redacts the
26
+ * resolved key so a constructor error cannot leak through the public wrapper.
27
+ */
28
+ export function getTranscriptionClient(opts) {
29
+ let apiKeyForRedaction = "";
30
+ try {
31
+ const provider = resolveProvider(opts.model, opts.provider, opts.modelData);
32
+ const ClientClass = builtinClients[provider] ?? registered[provider];
33
+ if (ClientClass === undefined) {
34
+ return failure(`Provider "${provider}" has no transcription API. Register one with registerTranscriptionProvider(name, ClientClass).`);
35
+ }
36
+ const apiKey = resolveApiKey(provider, opts) ?? "";
37
+ apiKeyForRedaction = apiKey;
38
+ const { apiKey: _callerKeys, ...clientOpts } = opts;
39
+ const config = { ...clientOpts, provider, apiKey };
40
+ return success(new ClientClass(config));
41
+ }
42
+ catch (err) {
43
+ let msg = "getTranscriptionClient() failed";
44
+ if (err instanceof Error) {
45
+ msg = err.message;
46
+ }
47
+ const redacted = redactSecret(msg, apiKeyForRedaction);
48
+ getLogger().error("getTranscriptionClient() failed:", redacted);
49
+ return failure(redacted);
50
+ }
51
+ }
52
+ export async function transcribe(source, opts) {
53
+ const client = getTranscriptionClient(opts);
54
+ if (!client.success) {
55
+ return client;
56
+ }
57
+ return client.value.transcribe(source);
58
+ }
@@ -4,6 +4,8 @@ export type TokenUsage = {
4
4
  outputTokens: number;
5
5
  cachedInputTokens?: number;
6
6
  cacheCreationInputTokens?: number;
7
+ inputAudioTokens?: number;
8
+ outputAudioTokens?: number;
7
9
  totalTokens?: number;
8
10
  };
9
11
  export declare const TokenUsageSchema: z.ZodObject<{
@@ -11,6 +13,8 @@ export declare const TokenUsageSchema: z.ZodObject<{
11
13
  outputTokens: z.ZodNumber;
12
14
  cachedInputTokens: z.ZodOptional<z.ZodNumber>;
13
15
  cacheCreationInputTokens: z.ZodOptional<z.ZodNumber>;
16
+ inputAudioTokens: z.ZodOptional<z.ZodNumber>;
17
+ outputAudioTokens: z.ZodOptional<z.ZodNumber>;
14
18
  totalTokens: z.ZodOptional<z.ZodNumber>;
15
19
  }, z.core.$strip>;
16
20
  export declare function addTokenUsage(_a?: TokenUsage, _b?: TokenUsage): TokenUsage;
@@ -4,6 +4,8 @@ export const TokenUsageSchema = z.object({
4
4
  outputTokens: z.number(),
5
5
  cachedInputTokens: z.number().optional(),
6
6
  cacheCreationInputTokens: z.number().optional(),
7
+ inputAudioTokens: z.number().optional(),
8
+ outputAudioTokens: z.number().optional(),
7
9
  totalTokens: z.number().optional(),
8
10
  });
9
11
  export function addTokenUsage(_a, _b) {
@@ -22,6 +24,8 @@ export function addTokenUsage(_a, _b) {
22
24
  outputTokens: a.outputTokens + b.outputTokens,
23
25
  cachedInputTokens: (a.cachedInputTokens || 0) + (b.cachedInputTokens || 0),
24
26
  cacheCreationInputTokens: (a.cacheCreationInputTokens || 0) + (b.cacheCreationInputTokens || 0),
27
+ inputAudioTokens: (a.inputAudioTokens || 0) + (b.inputAudioTokens || 0),
28
+ outputAudioTokens: (a.outputAudioTokens || 0) + (b.outputAudioTokens || 0),
25
29
  totalTokens: (a.totalTokens || 0) + (b.totalTokens || 0),
26
30
  };
27
31
  }
package/dist/types.d.ts CHANGED
@@ -30,6 +30,9 @@ export type SmolConfig = {
30
30
  deepInfra?: string;
31
31
  liteLlm?: string;
32
32
  openAiCompat?: string;
33
+ /** Arbitrary provider names, for keys targeting a custom-registered provider
34
+ * (e.g. registerProvider("acme", ...)), keyed by the exact registered name. */
35
+ [provider: string]: string | undefined;
33
36
  };
34
37
  /** Custom base URLs, nested by provider. Defaults are baked in where applicable
35
38
  * (e.g. openrouter, deepinfra); litellm and openai-compat require an explicit URL. */
@@ -1,4 +1,4 @@
1
- import type { ImageRef } from "./imageRef.js";
1
+ import type { ImageRef } from "./blobRef.js";
2
2
  /** Filename sent to providers that require one when a file part omits its own. */
3
3
  export declare const DEFAULT_ATTACHMENT_FILENAME = "attachment.pdf";
4
4
  /** A file part's filename, falling back to the shared default when unset. */
@@ -0,0 +1,9 @@
1
+ export type SpeakFormat = "mp3" | "opus" | "aac" | "flac" | "wav" | "pcm";
2
+ export type TranscriptionAudioType = {
3
+ extension: string;
4
+ filename: string;
5
+ };
6
+ export declare function transcriptionAudioType(mime: string): TranscriptionAudioType | null;
7
+ export declare function chatAudioFormat(mime: string): "mp3" | "wav" | null;
8
+ export declare const SPEECH_FORMAT_TO_MIME: Record<SpeakFormat, string>;
9
+ export declare function isSpeakFormat(value: string): value is SpeakFormat;
@@ -0,0 +1,33 @@
1
+ import { audioFormatForMime } from "./mime.js";
2
+ export function transcriptionAudioType(mime) {
3
+ const format = audioFormatForMime(mime);
4
+ if (format === null) {
5
+ return null;
6
+ }
7
+ return { extension: format.extension, filename: `audio.${format.extension}` };
8
+ }
9
+ export function chatAudioFormat(mime) {
10
+ const format = audioFormatForMime(mime);
11
+ if (format === null) {
12
+ return null;
13
+ }
14
+ if (format.extension === "mp3" || format.extension === "wav") {
15
+ return format.extension;
16
+ }
17
+ return null;
18
+ }
19
+ // PCM from OpenAI is headerless s16le / 24kHz / mono, which audio/L16 (big-endian
20
+ // per RFC) would misdescribe — use octet-stream + structured metadata instead.
21
+ export const SPEECH_FORMAT_TO_MIME = {
22
+ mp3: "audio/mpeg",
23
+ opus: "audio/ogg",
24
+ aac: "audio/aac",
25
+ flac: "audio/flac",
26
+ wav: "audio/wav",
27
+ pcm: "application/octet-stream",
28
+ };
29
+ // Object.hasOwn (not `in`) so prototype keys like "toString"/"__proto__"
30
+ // never pass the guard.
31
+ export function isSpeakFormat(value) {
32
+ return Object.hasOwn(SPEECH_FORMAT_TO_MIME, value);
33
+ }
@@ -1,4 +1,4 @@
1
- export type ImageRef = {
1
+ export type BlobRef = {
2
2
  kind: "bytes";
3
3
  data: Uint8Array;
4
4
  mimeType: string;
@@ -20,23 +20,23 @@ 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;
25
- export type NormalizedImage = {
23
+ /** Alias of {@link BlobRef} for image call sites, where it documents intent. */
24
+ export type ImageRef = BlobRef;
25
+ export type NormalizedBlob = {
26
26
  data: Uint8Array;
27
27
  mimeType: string;
28
28
  };
29
- /** Default timeout for fetching image URLs during normalization (60 seconds). */
29
+ /** Default timeout for fetching attachment URLs during normalization (60 seconds). */
30
30
  export declare const DEFAULT_FETCH_TIMEOUT_MS = 60000;
31
- export declare function normalizeImageRef(ref: ImageRef, options?: {
31
+ export declare function normalizeBlob(ref: BlobRef, options?: {
32
32
  allowedMimePrefixes?: string[];
33
33
  maxBytes?: number;
34
- }): Promise<NormalizedImage>;
34
+ }): Promise<NormalizedBlob>;
35
35
  /**
36
- * Load a source to bytes WITHOUT the image MIME gate (used by file uploads,
36
+ * Load a source to bytes WITHOUT the MIME gate (used by file uploads,
37
37
  * which accept any type). Enforces `maxBytes` across all kinds.
38
38
  */
39
- export declare function loadBlob(ref: ImageRef, options?: {
39
+ export declare function loadBlob(ref: BlobRef, options?: {
40
40
  maxBytes?: number;
41
41
  }): Promise<{
42
42
  data: Uint8Array;
@@ -1,15 +1,8 @@
1
1
  import { readFile, stat } from "node:fs/promises";
2
2
  import { extname } from "node:path";
3
- /** Default timeout for fetching image URLs during normalization (60 seconds). */
3
+ import { EXT_TO_MIME } from "./mime.js";
4
+ /** Default timeout for fetching attachment URLs during normalization (60 seconds). */
4
5
  export const DEFAULT_FETCH_TIMEOUT_MS = 60_000;
5
- const EXT_TO_MIME = {
6
- ".png": "image/png",
7
- ".jpg": "image/jpeg",
8
- ".jpeg": "image/jpeg",
9
- ".webp": "image/webp",
10
- ".gif": "image/gif",
11
- ".pdf": "application/pdf",
12
- };
13
6
  function isAllowedMime(mimeType, allowedPrefixes) {
14
7
  for (const prefix of allowedPrefixes) {
15
8
  if (mimeType.startsWith(prefix)) {
@@ -18,7 +11,7 @@ function isAllowedMime(mimeType, allowedPrefixes) {
18
11
  }
19
12
  return false;
20
13
  }
21
- export async function normalizeImageRef(ref, options = {}) {
14
+ export async function normalizeBlob(ref, options = {}) {
22
15
  const allowed = options.allowedMimePrefixes ?? ["image/"];
23
16
  const result = await loadRef(ref, allowed, options.maxBytes);
24
17
  if (options.maxBytes !== undefined && result.data.length > options.maxBytes) {
@@ -31,7 +24,7 @@ export async function normalizeImageRef(ref, options = {}) {
31
24
  return { data: result.data, mimeType: result.mimeType };
32
25
  }
33
26
  /**
34
- * Load a source to bytes WITHOUT the image MIME gate (used by file uploads,
27
+ * Load a source to bytes WITHOUT the MIME gate (used by file uploads,
35
28
  * which accept any type). Enforces `maxBytes` across all kinds.
36
29
  */
37
30
  export async function loadBlob(ref, options = {}) {
@@ -94,7 +87,7 @@ async function loadRef(ref, allowed, maxBytes) {
94
87
  const mimeType = ref.mimeType ?? inferred;
95
88
  if (allowed !== null && (!mimeType || !isAllowedMime(mimeType, allowed))) {
96
89
  throw new Error(`Could not determine an allowed MIME type for path "${ref.path}". ` +
97
- `Allowed: ${allowed.join(", ")}. Pass an explicit mimeType on the ImageRef.`);
90
+ `Allowed: ${allowed.join(", ")}. Pass an explicit mimeType on the BlobRef.`);
98
91
  }
99
92
  return { data: new Uint8Array(buf), mimeType };
100
93
  }
@@ -128,7 +121,7 @@ async function loadRef(ref, allowed, maxBytes) {
128
121
  if (allowed !== null && (!mimeType || !isAllowedMime(mimeType, allowed))) {
129
122
  throw new Error(`Could not determine an allowed MIME type for URL "${ref.url}". ` +
130
123
  `Response Content-Type was "${contentType ?? "missing"}". ` +
131
- `Allowed: ${allowed.join(", ")}. Pass an explicit mimeType on the ImageRef.`);
124
+ `Allowed: ${allowed.join(", ")}. Pass an explicit mimeType on the BlobRef.`);
132
125
  }
133
126
  return { data: buf, mimeType };
134
127
  }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Shared extension/MIME knowledge for attachments and audio. One table per
3
+ * format; every other ext↔MIME mapping in the package derives from these so
4
+ * the forward and inverse maps can't drift apart.
5
+ */
6
+ export type AudioFormat = {
7
+ /** Primary file extension, without the dot. */
8
+ extension: string;
9
+ /** Canonical MIME type. */
10
+ mimeType: string;
11
+ /** Other MIME strings that identify the same container. */
12
+ aliasMimeTypes: readonly string[];
13
+ /** Other extensions that map to this format. */
14
+ aliasExtensions: readonly string[];
15
+ };
16
+ export declare const AUDIO_FORMATS: readonly AudioFormat[];
17
+ /** Extension (with leading dot, lowercase) → MIME, across images, PDF, and audio. */
18
+ export declare const EXT_TO_MIME: Record<string, string>;
19
+ export declare function canonicalizeMime(mime: string): string;
20
+ /** The audio format a MIME string identifies, or null when unrecognized. */
21
+ export declare function audioFormatForMime(mime: string): AudioFormat | null;
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Shared extension/MIME knowledge for attachments and audio. One table per
3
+ * format; every other ext↔MIME mapping in the package derives from these so
4
+ * the forward and inverse maps can't drift apart.
5
+ */
6
+ export const AUDIO_FORMATS = [
7
+ { extension: "mp3", mimeType: "audio/mpeg", aliasMimeTypes: ["audio/mp3"], aliasExtensions: ["mpeg", "mpga"] },
8
+ { extension: "wav", mimeType: "audio/wav", aliasMimeTypes: ["audio/x-wav"], aliasExtensions: [] },
9
+ { extension: "m4a", mimeType: "audio/m4a", aliasMimeTypes: ["audio/x-m4a"], aliasExtensions: [] },
10
+ { extension: "mp4", mimeType: "audio/mp4", aliasMimeTypes: ["video/mp4"], aliasExtensions: [] },
11
+ { extension: "ogg", mimeType: "audio/ogg", aliasMimeTypes: [], aliasExtensions: [] },
12
+ { extension: "flac", mimeType: "audio/flac", aliasMimeTypes: [], aliasExtensions: [] },
13
+ { extension: "webm", mimeType: "audio/webm", aliasMimeTypes: [], aliasExtensions: [] },
14
+ ];
15
+ const IMAGE_AND_DOCUMENT_EXT_TO_MIME = {
16
+ ".png": "image/png",
17
+ ".jpg": "image/jpeg",
18
+ ".jpeg": "image/jpeg",
19
+ ".webp": "image/webp",
20
+ ".gif": "image/gif",
21
+ ".pdf": "application/pdf",
22
+ };
23
+ function buildExtToMime() {
24
+ const table = { ...IMAGE_AND_DOCUMENT_EXT_TO_MIME };
25
+ for (const format of AUDIO_FORMATS) {
26
+ table[`.${format.extension}`] = format.mimeType;
27
+ for (const alias of format.aliasExtensions) {
28
+ table[`.${alias}`] = format.mimeType;
29
+ }
30
+ }
31
+ return table;
32
+ }
33
+ /** Extension (with leading dot, lowercase) → MIME, across images, PDF, and audio. */
34
+ export const EXT_TO_MIME = buildExtToMime();
35
+ // Strips parameters (e.g. ";codecs=opus") and normalizes case, so MediaRecorder-
36
+ // style MIME strings like "audio/webm;codecs=opus" or "AUDIO/MPEG" still match.
37
+ export function canonicalizeMime(mime) {
38
+ return mime.split(";")[0].trim().toLowerCase();
39
+ }
40
+ /** The audio format a MIME string identifies, or null when unrecognized. */
41
+ export function audioFormatForMime(mime) {
42
+ const canonical = canonicalizeMime(mime);
43
+ for (const format of AUDIO_FORMATS) {
44
+ if (format.mimeType === canonical) {
45
+ return format;
46
+ }
47
+ if (format.aliasMimeTypes.includes(canonical)) {
48
+ return format;
49
+ }
50
+ }
51
+ return null;
52
+ }
@@ -1,2 +1,6 @@
1
- import { PromptResult, Result, SmolConfig } from "../types.js";
2
- export declare function validateModalities(config: SmolConfig): Result<PromptResult> | null;
1
+ import { Message } from "../classes/message/index.js";
2
+ /** Modalities a model must positively declare in its data block — for these,
3
+ * "unknown" means "unsupported" (audio serialization is model-specific). */
4
+ export declare const MODALITIES_REQUIRING_DECLARATION: ReadonlySet<string>;
5
+ /** Which non-text input modalities the user messages actually use. */
6
+ export declare function neededInputModalities(messages: Message[]): string[];
@@ -1,10 +1,11 @@
1
1
  import { UserMessage } from "../classes/message/index.js";
2
- import { modelSupportsInputModality } from "../models.js";
3
- import { failure } from "../types.js";
4
- export function validateModalities(config) {
5
- let needsImage = false;
6
- let needsPdf = false;
7
- for (const msg of config.messages) {
2
+ /** Modalities a model must positively declare in its data block — for these,
3
+ * "unknown" means "unsupported" (audio serialization is model-specific). */
4
+ export const MODALITIES_REQUIRING_DECLARATION = new Set(["audio"]);
5
+ /** Which non-text input modalities the user messages actually use. */
6
+ export function neededInputModalities(messages) {
7
+ const needed = new Set();
8
+ for (const msg of messages) {
8
9
  if (!(msg instanceof UserMessage)) {
9
10
  continue;
10
11
  }
@@ -14,18 +15,15 @@ export function validateModalities(config) {
14
15
  }
15
16
  for (const part of parts) {
16
17
  if (part.type === "image") {
17
- needsImage = true;
18
+ needed.add("image");
18
19
  }
19
20
  if (part.type === "file") {
20
- needsPdf = true;
21
+ needed.add("pdf");
22
+ }
23
+ if (part.type === "audio") {
24
+ needed.add("audio");
21
25
  }
22
26
  }
23
27
  }
24
- if (needsImage && modelSupportsInputModality(config.model, "image", config.modelData) === false) {
25
- return failure(`Model ${config.model} does not support image input.`);
26
- }
27
- if (needsPdf && modelSupportsInputModality(config.model, "pdf", config.modelData) === false) {
28
- return failure(`Model ${config.model} does not support PDF/document input.`);
29
- }
30
- return null;
28
+ return [...needed];
31
29
  }
@@ -19,6 +19,8 @@ type NestedKeyConfig = {
19
19
  deepInfra?: string;
20
20
  liteLlm?: string;
21
21
  openAiCompat?: string;
22
+ /** Arbitrary provider names, for keys targeting a custom-registered provider. */
23
+ [provider: string]: string | undefined;
22
24
  };
23
25
  baseUrl?: {
24
26
  ollama?: string;
@@ -38,7 +38,7 @@ export function resolveApiKey(provider, config) {
38
38
  case "openai-compat":
39
39
  return k?.openAiCompat || process.env.OPENAI_COMPAT_API_KEY;
40
40
  default:
41
- return undefined;
41
+ return config.apiKey?.[provider];
42
42
  }
43
43
  }
44
44
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "smoltalk",
3
- "version": "0.8.4",
3
+ "version": "0.10.0",
4
4
  "description": "A common interface for LLM APIs",
5
5
  "homepage": "https://github.com/egonSchiele/smoltalk",
6
6
  "files": [