smoltalk 0.9.0 → 0.10.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. package/README.md +175 -7
  2. package/dist/classes/message/AssistantMessage.d.ts +2 -0
  3. package/dist/classes/message/UserMessage.d.ts +21 -0
  4. package/dist/classes/message/UserMessage.js +3 -0
  5. package/dist/classes/message/contentParts.d.ts +71 -2
  6. package/dist/classes/message/contentParts.js +6 -0
  7. package/dist/classes/message/index.d.ts +5 -2
  8. package/dist/classes/message/index.js +7 -0
  9. package/dist/classes/message/renderers/AnthropicRenderer.d.ts +2 -1
  10. package/dist/classes/message/renderers/AnthropicRenderer.js +3 -0
  11. package/dist/classes/message/renderers/GoogleRenderer.d.ts +2 -1
  12. package/dist/classes/message/renderers/GoogleRenderer.js +3 -0
  13. package/dist/classes/message/renderers/JSONRenderer.d.ts +2 -1
  14. package/dist/classes/message/renderers/JSONRenderer.js +4 -0
  15. package/dist/classes/message/renderers/OpenAIChatRenderer.d.ts +8 -1
  16. package/dist/classes/message/renderers/OpenAIChatRenderer.js +18 -0
  17. package/dist/classes/message/renderers/OpenAIResponsesRenderer.d.ts +2 -1
  18. package/dist/classes/message/renderers/OpenAIResponsesRenderer.js +3 -0
  19. package/dist/classes/message/renderers/PartRenderer.d.ts +3 -2
  20. package/dist/classes/message/renderers/PartRenderer.js +3 -0
  21. package/dist/client.js +1 -0
  22. package/dist/clients/anthropic.js +1 -1
  23. package/dist/clients/baseClient.d.ts +13 -1
  24. package/dist/clients/baseClient.js +36 -7
  25. package/dist/clients/google.js +1 -1
  26. package/dist/clients/ollama.js +1 -1
  27. package/dist/clients/openai.d.ts +2 -1
  28. package/dist/clients/openai.js +15 -3
  29. package/dist/clients/openaiCompat.d.ts +2 -0
  30. package/dist/clients/openaiCompat.js +5 -0
  31. package/dist/clients/openaiResponses.js +1 -1
  32. package/dist/clients/resolveAttachments.d.ts +8 -4
  33. package/dist/clients/resolveAttachments.js +101 -50
  34. package/dist/embed.d.ts +4 -0
  35. package/dist/files.d.ts +1 -1
  36. package/dist/files.js +1 -1
  37. package/dist/image/google.js +2 -2
  38. package/dist/image/openai.js +3 -3
  39. package/dist/image.d.ts +1 -1
  40. package/dist/index.d.ts +10 -2
  41. package/dist/index.js +7 -1
  42. package/dist/model.d.ts +15 -4
  43. package/dist/model.js +72 -12
  44. package/dist/models.d.ts +204 -22
  45. package/dist/models.js +211 -30
  46. package/dist/speech/baseSpeechClient.d.ts +36 -0
  47. package/dist/speech/baseSpeechClient.js +117 -0
  48. package/dist/speech/google.d.ts +6 -0
  49. package/dist/speech/google.js +54 -0
  50. package/dist/speech/groq.d.ts +11 -0
  51. package/dist/speech/groq.js +19 -0
  52. package/dist/speech/openai.d.ts +14 -0
  53. package/dist/speech/openai.js +51 -0
  54. package/dist/speech/openaiCompat.d.ts +13 -0
  55. package/dist/speech/openaiCompat.js +22 -0
  56. package/dist/speech.d.ts +45 -0
  57. package/dist/speech.js +63 -0
  58. package/dist/transcription/baseTranscriptionClient.d.ts +36 -0
  59. package/dist/transcription/baseTranscriptionClient.js +133 -0
  60. package/dist/transcription/google.d.ts +6 -0
  61. package/dist/transcription/google.js +56 -0
  62. package/dist/transcription/groq.d.ts +10 -0
  63. package/dist/transcription/groq.js +17 -0
  64. package/dist/transcription/openai.d.ts +11 -0
  65. package/dist/transcription/openai.js +67 -0
  66. package/dist/transcription/openaiCompat.d.ts +13 -0
  67. package/dist/transcription/openaiCompat.js +22 -0
  68. package/dist/transcription.d.ts +54 -0
  69. package/dist/transcription.js +64 -0
  70. package/dist/types/tokenUsage.d.ts +4 -0
  71. package/dist/types/tokenUsage.js +4 -0
  72. package/dist/types.d.ts +4 -0
  73. package/dist/util/attachments.d.ts +1 -1
  74. package/dist/util/audioMime.d.ts +26 -0
  75. package/dist/util/audioMime.js +74 -0
  76. package/dist/util/{imageRef.d.ts → blobRef.d.ts} +9 -9
  77. package/dist/util/{imageRef.js → blobRef.js} +6 -13
  78. package/dist/util/googleAudioUsage.d.ts +14 -0
  79. package/dist/util/googleAudioUsage.js +52 -0
  80. package/dist/util/mime.d.ts +21 -0
  81. package/dist/util/mime.js +54 -0
  82. package/dist/util/modalities.d.ts +6 -2
  83. package/dist/util/modalities.js +13 -15
  84. package/dist/util/provider.d.ts +3 -0
  85. package/dist/util/provider.js +3 -1
  86. package/package.json +1 -1
@@ -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,10 @@ export type SmolConfig = {
30
30
  deepInfra?: string;
31
31
  liteLlm?: string;
32
32
  openAiCompat?: string;
33
+ groq?: string;
34
+ /** Arbitrary provider names, for keys targeting a custom-registered provider
35
+ * (e.g. registerProvider("acme", ...)), keyed by the exact registered name. */
36
+ [provider: string]: string | undefined;
33
37
  };
34
38
  /** Custom base URLs, nested by provider. Defaults are baked in where applicable
35
39
  * (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,26 @@
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;
10
+ /**
11
+ * Translate a canonical/alias audio MIME to the wire value Google expects.
12
+ * Google documents MP3 as `audio/mp3`, whereas this repo canonicalizes it to
13
+ * `audio/mpeg`; everything else passes through canonicalized.
14
+ */
15
+ export declare function googleAudioWireMime(mimeType: string): string;
16
+ export type PcmWavOptions = {
17
+ sampleRateHz: number;
18
+ channels: number;
19
+ bitsPerSample: number;
20
+ };
21
+ /**
22
+ * Wrap raw signed-integer little-endian PCM in a 44-byte RIFF/WAVE header so it
23
+ * becomes a directly-playable .wav. Pure function, no dependency. Used for
24
+ * Gemini TTS output, which is only ever raw PCM.
25
+ */
26
+ export declare function pcmToWav(pcm: Uint8Array, opts: PcmWavOptions): Uint8Array;
@@ -0,0 +1,74 @@
1
+ import { audioFormatForMime, canonicalizeMime } 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
+ }
34
+ /**
35
+ * Translate a canonical/alias audio MIME to the wire value Google expects.
36
+ * Google documents MP3 as `audio/mp3`, whereas this repo canonicalizes it to
37
+ * `audio/mpeg`; everything else passes through canonicalized.
38
+ */
39
+ export function googleAudioWireMime(mimeType) {
40
+ const format = audioFormatForMime(mimeType);
41
+ const canonical = format?.mimeType ?? canonicalizeMime(mimeType);
42
+ return canonical === "audio/mpeg" ? "audio/mp3" : canonical;
43
+ }
44
+ /**
45
+ * Wrap raw signed-integer little-endian PCM in a 44-byte RIFF/WAVE header so it
46
+ * becomes a directly-playable .wav. Pure function, no dependency. Used for
47
+ * Gemini TTS output, which is only ever raw PCM.
48
+ */
49
+ export function pcmToWav(pcm, opts) {
50
+ const { sampleRateHz, channels, bitsPerSample } = opts;
51
+ const blockAlign = (channels * bitsPerSample) / 8;
52
+ const byteRate = sampleRateHz * blockAlign;
53
+ const out = new Uint8Array(44 + pcm.length);
54
+ const view = new DataView(out.buffer);
55
+ const writeAscii = (offset, s) => {
56
+ for (let i = 0; i < s.length; i++)
57
+ view.setUint8(offset + i, s.charCodeAt(i));
58
+ };
59
+ writeAscii(0, "RIFF");
60
+ view.setUint32(4, 36 + pcm.length, true);
61
+ writeAscii(8, "WAVE");
62
+ writeAscii(12, "fmt ");
63
+ view.setUint32(16, 16, true); // PCM fmt chunk size
64
+ view.setUint16(20, 1, true); // audio format = PCM
65
+ view.setUint16(22, channels, true);
66
+ view.setUint32(24, sampleRateHz, true);
67
+ view.setUint32(28, byteRate, true);
68
+ view.setUint16(32, blockAlign, true);
69
+ view.setUint16(34, bitsPerSample, true);
70
+ writeAscii(36, "data");
71
+ view.setUint32(40, pcm.length, true);
72
+ out.set(pcm, 44);
73
+ return out;
74
+ }
@@ -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,14 @@
1
+ import type { GenerateContentResponseUsageMetadata } from "@google/genai";
2
+ import type { TokenUsage } from "../types/tokenUsage.js";
3
+ export type GoogleAudioDirection = "input" | "output";
4
+ /**
5
+ * Normalize Gemini's usage metadata into smoltalk's TokenUsage, separating the
6
+ * audio bucket for the given direction so the token-priced cost engine can bill
7
+ * it at the audio rate. STT sets audioDirection "input" (audio prompt tokens);
8
+ * TTS sets "output" (audio candidate tokens).
9
+ *
10
+ * Thinking tokens (`thoughtsTokenCount`) are reported separately from
11
+ * `candidatesTokenCount` and are always text — they go into the text-output
12
+ * bucket, never the audio-output bucket.
13
+ */
14
+ export declare function normalizeGoogleAudioUsage(metadata: GenerateContentResponseUsageMetadata | undefined, audioDirection: GoogleAudioDirection): TokenUsage | undefined;
@@ -0,0 +1,52 @@
1
+ function modalityTokens(details, modality) {
2
+ return (details ?? [])
3
+ // `modality` is a MediaModality enum; compare by its string value.
4
+ .filter((detail) => String(detail.modality) === modality)
5
+ .reduce((sum, detail) => sum + (detail.tokenCount ?? 0), 0);
6
+ }
7
+ /**
8
+ * Normalize Gemini's usage metadata into smoltalk's TokenUsage, separating the
9
+ * audio bucket for the given direction so the token-priced cost engine can bill
10
+ * it at the audio rate. STT sets audioDirection "input" (audio prompt tokens);
11
+ * TTS sets "output" (audio candidate tokens).
12
+ *
13
+ * Thinking tokens (`thoughtsTokenCount`) are reported separately from
14
+ * `candidatesTokenCount` and are always text — they go into the text-output
15
+ * bucket, never the audio-output bucket.
16
+ */
17
+ export function normalizeGoogleAudioUsage(metadata, audioDirection) {
18
+ if (metadata === undefined) {
19
+ return undefined;
20
+ }
21
+ const prompt = metadata.promptTokenCount ?? 0;
22
+ const candidates = metadata.candidatesTokenCount ?? 0;
23
+ const thoughts = metadata.thoughtsTokenCount ?? 0;
24
+ if (audioDirection === "input") {
25
+ const audioIn = modalityTokens(metadata.promptTokensDetails, "AUDIO");
26
+ const usage = {
27
+ inputTokens: Math.max(0, prompt - audioIn),
28
+ outputTokens: candidates + thoughts,
29
+ };
30
+ if (audioIn > 0)
31
+ usage.inputAudioTokens = audioIn;
32
+ if (metadata.totalTokenCount !== undefined)
33
+ usage.totalTokens = metadata.totalTokenCount;
34
+ return usage;
35
+ }
36
+ // Output (TTS): split the audio bucket out of candidate tokens. Only assume
37
+ // "all candidates are audio" when the details array is missing/empty; when it
38
+ // is present, trust its AUDIO total even if that total is 0 (otherwise
39
+ // purely-text candidate tokens would be mispriced at the audio rate).
40
+ const details = metadata.candidatesTokensDetails;
41
+ const detailsPresent = Array.isArray(details) && details.length > 0;
42
+ const audioOut = detailsPresent ? modalityTokens(details, "AUDIO") : candidates;
43
+ const usage = {
44
+ inputTokens: prompt,
45
+ outputTokens: Math.max(0, candidates - audioOut) + thoughts,
46
+ };
47
+ if (audioOut > 0)
48
+ usage.outputAudioTokens = audioOut;
49
+ if (metadata.totalTokenCount !== undefined)
50
+ usage.totalTokens = metadata.totalTokenCount;
51
+ return usage;
52
+ }
@@ -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,54 @@
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
+ { extension: "aac", mimeType: "audio/aac", aliasMimeTypes: [], aliasExtensions: [] },
15
+ { extension: "aiff", mimeType: "audio/aiff", aliasMimeTypes: ["audio/x-aiff"], aliasExtensions: ["aif"] },
16
+ ];
17
+ const IMAGE_AND_DOCUMENT_EXT_TO_MIME = {
18
+ ".png": "image/png",
19
+ ".jpg": "image/jpeg",
20
+ ".jpeg": "image/jpeg",
21
+ ".webp": "image/webp",
22
+ ".gif": "image/gif",
23
+ ".pdf": "application/pdf",
24
+ };
25
+ function buildExtToMime() {
26
+ const table = { ...IMAGE_AND_DOCUMENT_EXT_TO_MIME };
27
+ for (const format of AUDIO_FORMATS) {
28
+ table[`.${format.extension}`] = format.mimeType;
29
+ for (const alias of format.aliasExtensions) {
30
+ table[`.${alias}`] = format.mimeType;
31
+ }
32
+ }
33
+ return table;
34
+ }
35
+ /** Extension (with leading dot, lowercase) → MIME, across images, PDF, and audio. */
36
+ export const EXT_TO_MIME = buildExtToMime();
37
+ // Strips parameters (e.g. ";codecs=opus") and normalizes case, so MediaRecorder-
38
+ // style MIME strings like "audio/webm;codecs=opus" or "AUDIO/MPEG" still match.
39
+ export function canonicalizeMime(mime) {
40
+ return mime.split(";")[0].trim().toLowerCase();
41
+ }
42
+ /** The audio format a MIME string identifies, or null when unrecognized. */
43
+ export function audioFormatForMime(mime) {
44
+ const canonical = canonicalizeMime(mime);
45
+ for (const format of AUDIO_FORMATS) {
46
+ if (format.mimeType === canonical) {
47
+ return format;
48
+ }
49
+ if (format.aliasMimeTypes.includes(canonical)) {
50
+ return format;
51
+ }
52
+ }
53
+ return null;
54
+ }
@@ -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,9 @@ type NestedKeyConfig = {
19
19
  deepInfra?: string;
20
20
  liteLlm?: string;
21
21
  openAiCompat?: string;
22
+ groq?: string;
23
+ /** Arbitrary provider names, for keys targeting a custom-registered provider. */
24
+ [provider: string]: string | undefined;
22
25
  };
23
26
  baseUrl?: {
24
27
  ollama?: string;
@@ -37,8 +37,10 @@ export function resolveApiKey(provider, config) {
37
37
  return k?.liteLlm || process.env.LITELLM_API_KEY;
38
38
  case "openai-compat":
39
39
  return k?.openAiCompat || process.env.OPENAI_COMPAT_API_KEY;
40
+ case "groq":
41
+ return k?.groq || process.env.GROQ_API_KEY;
40
42
  default:
41
- return undefined;
43
+ return config.apiKey?.[provider];
42
44
  }
43
45
  }
44
46
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "smoltalk",
3
- "version": "0.9.0",
3
+ "version": "0.10.1",
4
4
  "description": "A common interface for LLM APIs",
5
5
  "homepage": "https://github.com/egonSchiele/smoltalk",
6
6
  "files": [