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.
- package/README.md +175 -7
- package/dist/classes/message/AssistantMessage.d.ts +2 -0
- package/dist/classes/message/UserMessage.d.ts +21 -0
- package/dist/classes/message/UserMessage.js +3 -0
- package/dist/classes/message/contentParts.d.ts +71 -2
- package/dist/classes/message/contentParts.js +6 -0
- package/dist/classes/message/index.d.ts +5 -2
- package/dist/classes/message/index.js +7 -0
- package/dist/classes/message/renderers/AnthropicRenderer.d.ts +2 -1
- package/dist/classes/message/renderers/AnthropicRenderer.js +3 -0
- package/dist/classes/message/renderers/GoogleRenderer.d.ts +2 -1
- package/dist/classes/message/renderers/GoogleRenderer.js +3 -0
- package/dist/classes/message/renderers/JSONRenderer.d.ts +2 -1
- package/dist/classes/message/renderers/JSONRenderer.js +4 -0
- package/dist/classes/message/renderers/OpenAIChatRenderer.d.ts +8 -1
- package/dist/classes/message/renderers/OpenAIChatRenderer.js +18 -0
- package/dist/classes/message/renderers/OpenAIResponsesRenderer.d.ts +2 -1
- package/dist/classes/message/renderers/OpenAIResponsesRenderer.js +3 -0
- package/dist/classes/message/renderers/PartRenderer.d.ts +3 -2
- package/dist/classes/message/renderers/PartRenderer.js +3 -0
- package/dist/client.js +1 -0
- package/dist/clients/anthropic.js +1 -1
- package/dist/clients/baseClient.d.ts +13 -1
- package/dist/clients/baseClient.js +36 -7
- package/dist/clients/google.js +1 -1
- package/dist/clients/ollama.js +1 -1
- package/dist/clients/openai.d.ts +2 -1
- package/dist/clients/openai.js +15 -3
- package/dist/clients/openaiCompat.d.ts +2 -0
- package/dist/clients/openaiCompat.js +5 -0
- package/dist/clients/openaiResponses.js +1 -1
- package/dist/clients/resolveAttachments.d.ts +8 -4
- package/dist/clients/resolveAttachments.js +101 -50
- package/dist/embed.d.ts +4 -0
- package/dist/files.d.ts +1 -1
- package/dist/files.js +1 -1
- package/dist/image/google.js +2 -2
- package/dist/image/openai.js +3 -3
- package/dist/image.d.ts +1 -1
- package/dist/index.d.ts +10 -2
- package/dist/index.js +7 -1
- package/dist/model.d.ts +15 -4
- package/dist/model.js +72 -12
- package/dist/models.d.ts +204 -22
- package/dist/models.js +211 -30
- package/dist/speech/baseSpeechClient.d.ts +36 -0
- package/dist/speech/baseSpeechClient.js +117 -0
- package/dist/speech/google.d.ts +6 -0
- package/dist/speech/google.js +54 -0
- package/dist/speech/groq.d.ts +11 -0
- package/dist/speech/groq.js +19 -0
- package/dist/speech/openai.d.ts +14 -0
- package/dist/speech/openai.js +51 -0
- package/dist/speech/openaiCompat.d.ts +13 -0
- package/dist/speech/openaiCompat.js +22 -0
- package/dist/speech.d.ts +45 -0
- package/dist/speech.js +63 -0
- package/dist/transcription/baseTranscriptionClient.d.ts +36 -0
- package/dist/transcription/baseTranscriptionClient.js +133 -0
- package/dist/transcription/google.d.ts +6 -0
- package/dist/transcription/google.js +56 -0
- package/dist/transcription/groq.d.ts +10 -0
- package/dist/transcription/groq.js +17 -0
- package/dist/transcription/openai.d.ts +11 -0
- package/dist/transcription/openai.js +67 -0
- package/dist/transcription/openaiCompat.d.ts +13 -0
- package/dist/transcription/openaiCompat.js +22 -0
- package/dist/transcription.d.ts +54 -0
- package/dist/transcription.js +64 -0
- package/dist/types/tokenUsage.d.ts +4 -0
- package/dist/types/tokenUsage.js +4 -0
- package/dist/types.d.ts +4 -0
- package/dist/util/attachments.d.ts +1 -1
- package/dist/util/audioMime.d.ts +26 -0
- package/dist/util/audioMime.js +74 -0
- package/dist/util/{imageRef.d.ts → blobRef.d.ts} +9 -9
- package/dist/util/{imageRef.js → blobRef.js} +6 -13
- package/dist/util/googleAudioUsage.d.ts +14 -0
- package/dist/util/googleAudioUsage.js +52 -0
- package/dist/util/mime.d.ts +21 -0
- package/dist/util/mime.js +54 -0
- package/dist/util/modalities.d.ts +6 -2
- package/dist/util/modalities.js +13 -15
- package/dist/util/provider.d.ts +3 -0
- package/dist/util/provider.js +3 -1
- package/package.json +1 -1
package/dist/types/tokenUsage.js
CHANGED
|
@@ -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 "./
|
|
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
|
|
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
|
-
/**
|
|
24
|
-
export type
|
|
25
|
-
export type
|
|
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
|
|
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
|
|
31
|
+
export declare function normalizeBlob(ref: BlobRef, options?: {
|
|
32
32
|
allowedMimePrefixes?: string[];
|
|
33
33
|
maxBytes?: number;
|
|
34
|
-
}): Promise<
|
|
34
|
+
}): Promise<NormalizedBlob>;
|
|
35
35
|
/**
|
|
36
|
-
* Load a source to bytes WITHOUT the
|
|
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:
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 {
|
|
2
|
-
|
|
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[];
|
package/dist/util/modalities.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { UserMessage } from "../classes/message/index.js";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
export
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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
|
-
|
|
18
|
+
needed.add("image");
|
|
18
19
|
}
|
|
19
20
|
if (part.type === "file") {
|
|
20
|
-
|
|
21
|
+
needed.add("pdf");
|
|
22
|
+
}
|
|
23
|
+
if (part.type === "audio") {
|
|
24
|
+
needed.add("audio");
|
|
21
25
|
}
|
|
22
26
|
}
|
|
23
27
|
}
|
|
24
|
-
|
|
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
|
}
|
package/dist/util/provider.d.ts
CHANGED
|
@@ -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;
|
package/dist/util/provider.js
CHANGED
|
@@ -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
|
|
43
|
+
return config.apiKey?.[provider];
|
|
42
44
|
}
|
|
43
45
|
}
|
|
44
46
|
/**
|