smoltalk 0.10.0 → 0.11.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.
- package/README.md +74 -6
- package/dist/client.d.ts +6 -0
- package/dist/client.js +12 -0
- package/dist/clients/llamaCppLoader.d.ts +42 -0
- package/dist/clients/llamaCppLoader.js +109 -0
- package/dist/functions.js +8 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/model.js +30 -11
- package/dist/models.d.ts +61 -3
- package/dist/models.js +74 -0
- package/dist/speech/baseSpeechClient.d.ts +5 -0
- package/dist/speech/baseSpeechClient.js +21 -2
- 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 +8 -0
- package/dist/speech/openai.js +16 -4
- package/dist/speech/openaiCompat.d.ts +13 -0
- package/dist/speech/openaiCompat.js +22 -0
- package/dist/speech.d.ts +5 -0
- package/dist/speech.js +6 -0
- package/dist/transcription/baseTranscriptionClient.d.ts +5 -0
- package/dist/transcription/baseTranscriptionClient.js +44 -18
- 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 +5 -0
- package/dist/transcription/openai.js +11 -3
- package/dist/transcription/openaiCompat.d.ts +13 -0
- package/dist/transcription/openaiCompat.js +22 -0
- package/dist/transcription.d.ts +3 -0
- package/dist/transcription.js +6 -0
- package/dist/types.d.ts +1 -0
- package/dist/util/audioMime.d.ts +17 -0
- package/dist/util/audioMime.js +42 -1
- package/dist/util/googleAudioUsage.d.ts +14 -0
- package/dist/util/googleAudioUsage.js +52 -0
- package/dist/util/mime.js +2 -0
- package/dist/util/provider.d.ts +1 -0
- package/dist/util/provider.js +2 -0
- package/package.json +9 -1
package/README.md
CHANGED
|
@@ -507,13 +507,79 @@ Text, transcription, and speech are classes: a base class owns the shared
|
|
|
507
507
|
behavior (validation, cost, error handling) and the subclass implements only
|
|
508
508
|
the provider call. Embeddings and images are one-shot functions.
|
|
509
509
|
|
|
510
|
+
## Local models (llama-cpp)
|
|
511
|
+
|
|
512
|
+
Install the optional plugin and name the provider — no wiring code:
|
|
513
|
+
|
|
514
|
+
```bash
|
|
515
|
+
npm i smoltalk-llama-cpp
|
|
516
|
+
```
|
|
517
|
+
|
|
518
|
+
```typescript
|
|
519
|
+
import { textSync, userMessage } from "smoltalk";
|
|
520
|
+
|
|
521
|
+
const result = await textSync({
|
|
522
|
+
provider: "llama-cpp",
|
|
523
|
+
model: "/path/to/llama-3.gguf",
|
|
524
|
+
messages: [userMessage("Hello!")],
|
|
525
|
+
});
|
|
526
|
+
```
|
|
527
|
+
|
|
528
|
+
smoltalk lazily imports and registers the plugin on the first `llama-cpp`
|
|
529
|
+
call; if the package is missing you get an install hint instead of a
|
|
530
|
+
resolution stack trace. Hosts with unusual layouts (e.g. a globally-installed
|
|
531
|
+
CLI with the plugin installed globally beside it) can hand smoltalk the
|
|
532
|
+
plugin's entry path explicitly and skip Node resolution:
|
|
533
|
+
|
|
534
|
+
```typescript
|
|
535
|
+
import { loadLlamaCpp } from "smoltalk";
|
|
536
|
+
|
|
537
|
+
const { resolveModel } = await loadLlamaCpp({
|
|
538
|
+
entryPath: "/path/to/smoltalk-llama-cpp/dist/index.js",
|
|
539
|
+
});
|
|
540
|
+
// resolveModel downloads hf: URIs (and absolutizes existing local paths):
|
|
541
|
+
const modelPath = await resolveModel("hf:org/repo/model.gguf", "/models/cache");
|
|
542
|
+
```
|
|
543
|
+
|
|
510
544
|
## Audio (STT/TTS)
|
|
511
545
|
|
|
512
|
-
Three audio primitives
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
546
|
+
Three audio primitives. `transcribe()` (speech-to-text) and `speak()`
|
|
547
|
+
(text-to-speech) are async and return `Result<T>` (never throw). `audioPart()`
|
|
548
|
+
(attach audio to a chat message) is different: it's a synchronous plain-object
|
|
549
|
+
constructor, not a `Result`-returning call — see "Audio in chat" below.
|
|
550
|
+
|
|
551
|
+
`transcribe()` and `speak()` support **OpenAI**, **Groq** (OpenAI-compatible
|
|
552
|
+
endpoints), and **Google Gemini** (native multimodal). For any other provider
|
|
553
|
+
that exposes OpenAI-shaped `/audio/*` endpoints, use the generic
|
|
554
|
+
**`openai-compat`** provider with `baseUrl` (mirrors the chat client). Anthropic,
|
|
555
|
+
OpenRouter, and Ollama have no audio endpoints and return a `Failure`.
|
|
556
|
+
|
|
557
|
+
```ts
|
|
558
|
+
// example: skip-typecheck
|
|
559
|
+
// Groq STT (OpenAI-compatible; provider inferred from the model)
|
|
560
|
+
await transcribe(src, { model: "whisper-large-v3" });
|
|
561
|
+
|
|
562
|
+
// Gemini STT (native multimodal — a general Gemini model transcribes)
|
|
563
|
+
await transcribe(src, { model: "gemini-2.5-flash", provider: "google" });
|
|
564
|
+
|
|
565
|
+
// Groq TTS → WAV by default
|
|
566
|
+
await speak("Hello", { model: "canopylabs/orpheus-v1-english", voice: "troy" });
|
|
567
|
+
|
|
568
|
+
// Gemini TTS → raw PCM by default; format: "wav" wraps it in a WAV header.
|
|
569
|
+
// Gemini has no numeric `speed` (rejected) and produces PCM/WAV only.
|
|
570
|
+
await speak("Hello", {
|
|
571
|
+
model: "gemini-2.5-flash-preview-tts", voice: "Kore",
|
|
572
|
+
provider: "google", format: "wav",
|
|
573
|
+
});
|
|
574
|
+
|
|
575
|
+
// Any OpenAI-compatible /audio endpoint (vLLM, LiteLLM, a proxy, …)
|
|
576
|
+
await transcribe(src, {
|
|
577
|
+
model: "whisper-1",
|
|
578
|
+
provider: "openai-compat",
|
|
579
|
+
apiKey: { openAiCompat: "..." }, // or OPENAI_COMPAT_API_KEY
|
|
580
|
+
baseUrl: { openAiCompat: "https://my-proxy/v1" }, // or OPENAI_COMPAT_BASE_URL
|
|
581
|
+
});
|
|
582
|
+
```
|
|
517
583
|
|
|
518
584
|
### Speech-to-text
|
|
519
585
|
|
|
@@ -529,7 +595,9 @@ if (result.success) {
|
|
|
529
595
|
}
|
|
530
596
|
```
|
|
531
597
|
|
|
532
|
-
`whisper-1`
|
|
598
|
+
Baked-in STT models: `whisper-1` (OpenAI) and `whisper-large-v3` /
|
|
599
|
+
`whisper-large-v3-turbo` (Groq); Gemini transcribes with a general model such as
|
|
600
|
+
`gemini-2.5-flash`. Options: `language`, `prompt`,
|
|
533
601
|
`timestampGranularity` (`"segment"` | `"word"`), `maxBytes` (a safety limit —
|
|
534
602
|
the effective cap is the smaller of your limit and the model's declared upload
|
|
535
603
|
cap, 25 MB for `whisper-1`). The result carries `text` plus optional
|
package/dist/client.d.ts
CHANGED
|
@@ -12,4 +12,10 @@ import { BaseClient } from "./clients/baseClient.js";
|
|
|
12
12
|
import { SmolClientConfig } from "./types.js";
|
|
13
13
|
export declare function registerProvider(providerName: string, clientClass: typeof BaseClient): void;
|
|
14
14
|
export declare function unregisterProvider(providerName: string): boolean;
|
|
15
|
+
/**
|
|
16
|
+
* True when `providerName` has been registered via registerProvider().
|
|
17
|
+
* Built-in providers (the switch cases in getClient) are not its concern —
|
|
18
|
+
* this only consults the custom registry.
|
|
19
|
+
*/
|
|
20
|
+
export declare function hasProvider(providerName: string): boolean;
|
|
15
21
|
export declare function getClient(config: SmolClientConfig): BaseClient;
|
package/dist/client.js
CHANGED
|
@@ -33,6 +33,14 @@ export function unregisterProvider(providerName) {
|
|
|
33
33
|
}
|
|
34
34
|
return false;
|
|
35
35
|
}
|
|
36
|
+
/**
|
|
37
|
+
* True when `providerName` has been registered via registerProvider().
|
|
38
|
+
* Built-in providers (the switch cases in getClient) are not its concern —
|
|
39
|
+
* this only consults the custom registry.
|
|
40
|
+
*/
|
|
41
|
+
export function hasProvider(providerName) {
|
|
42
|
+
return providerName in registeredProviders;
|
|
43
|
+
}
|
|
36
44
|
export function getClient(config) {
|
|
37
45
|
const modelName = config.model;
|
|
38
46
|
const provider = resolveProvider(modelName, config.provider, config.modelData);
|
|
@@ -100,6 +108,10 @@ export function getClient(config) {
|
|
|
100
108
|
const ClientClass = registeredProviders[provider];
|
|
101
109
|
return new ClientClass(clientConfig);
|
|
102
110
|
}
|
|
111
|
+
if (provider === "llama-cpp") {
|
|
112
|
+
throw new SmolError("The llama-cpp provider loads automatically when called through text()/textSync()/textStream(). " +
|
|
113
|
+
"For direct getClient() use, await loadLlamaCpp() first (install smoltalk-llama-cpp if it is missing).");
|
|
114
|
+
}
|
|
103
115
|
throw new SmolError(`Model provider ${provider} is not supported. To use a custom provider, register it first via registerProvider(name, ClientClass).`);
|
|
104
116
|
}
|
|
105
117
|
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { BaseClient } from "./baseClient.js";
|
|
2
|
+
/**
|
|
3
|
+
* Minimal structural view of smoltalk-llama-cpp's module. Declared here (not
|
|
4
|
+
* imported from the plugin) so smoltalk compiles without the plugin installed
|
|
5
|
+
* and the workspace gains no build-order cycle.
|
|
6
|
+
*/
|
|
7
|
+
export type LlamaCppModule = {
|
|
8
|
+
LlamaCPP: typeof BaseClient;
|
|
9
|
+
resolveModel: (uriOrPath: string, cacheDir: string) => Promise<string>;
|
|
10
|
+
};
|
|
11
|
+
type ImportFn = (specifier: string) => Promise<Record<string, unknown>>;
|
|
12
|
+
/**
|
|
13
|
+
* Test-only: swap the dynamic import (pass undefined to restore the real one)
|
|
14
|
+
* and clear the load cache. Deliberately NOT exported from the package index.
|
|
15
|
+
*/
|
|
16
|
+
export declare function _setImportForTests(fn?: ImportFn): void;
|
|
17
|
+
/**
|
|
18
|
+
* Load and register the optional smoltalk-llama-cpp plugin, once per process.
|
|
19
|
+
*
|
|
20
|
+
* - Without options, imports the bare specifier "smoltalk-llama-cpp" using
|
|
21
|
+
* Node resolution from smoltalk's location. The optional peer declaration
|
|
22
|
+
* in package.json is what makes that resolvable under pnpm's strict layout.
|
|
23
|
+
* - `entryPath` is the escape hatch for hosts whose plugin install is not
|
|
24
|
+
* resolvable from smoltalk (e.g. globally-installed CLIs): the file is
|
|
25
|
+
* imported directly and Node resolution is skipped. Hosts own discovering
|
|
26
|
+
* that path; smoltalk never probes global npm roots and reads no env vars.
|
|
27
|
+
* - Registers the module's LlamaCPP class under "llama-cpp" unless that name
|
|
28
|
+
* is already registered. An existing registration is left untouched, but
|
|
29
|
+
* the module is still imported, validated, and returned — an existing
|
|
30
|
+
* registration wins the registry, never the return value. Registration is
|
|
31
|
+
* re-ensured from the cached module on EVERY call, so a later
|
|
32
|
+
* unregisterProvider("llama-cpp") is undone by the next load call without
|
|
33
|
+
* a second import.
|
|
34
|
+
* - Concurrent first calls share one in-flight load. A failed load clears
|
|
35
|
+
* the cache so a later call can retry (e.g. after installing the package).
|
|
36
|
+
* A second call with a different entryPath after a successful load returns
|
|
37
|
+
* the already-loaded module (first load wins).
|
|
38
|
+
*/
|
|
39
|
+
export declare function loadLlamaCpp(options?: {
|
|
40
|
+
entryPath?: string;
|
|
41
|
+
}): Promise<LlamaCppModule>;
|
|
42
|
+
export {};
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { pathToFileURL } from "url";
|
|
2
|
+
import { hasProvider, registerProvider } from "../client.js";
|
|
3
|
+
import { SmolError } from "../smolError.js";
|
|
4
|
+
const realImport = (specifier) => import(specifier);
|
|
5
|
+
let importFn = realImport;
|
|
6
|
+
let cachedLoad;
|
|
7
|
+
/**
|
|
8
|
+
* Test-only: swap the dynamic import (pass undefined to restore the real one)
|
|
9
|
+
* and clear the load cache. Deliberately NOT exported from the package index.
|
|
10
|
+
*/
|
|
11
|
+
export function _setImportForTests(fn) {
|
|
12
|
+
if (fn) {
|
|
13
|
+
importFn = fn;
|
|
14
|
+
}
|
|
15
|
+
else {
|
|
16
|
+
importFn = realImport;
|
|
17
|
+
}
|
|
18
|
+
cachedLoad = undefined;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Load and register the optional smoltalk-llama-cpp plugin, once per process.
|
|
22
|
+
*
|
|
23
|
+
* - Without options, imports the bare specifier "smoltalk-llama-cpp" using
|
|
24
|
+
* Node resolution from smoltalk's location. The optional peer declaration
|
|
25
|
+
* in package.json is what makes that resolvable under pnpm's strict layout.
|
|
26
|
+
* - `entryPath` is the escape hatch for hosts whose plugin install is not
|
|
27
|
+
* resolvable from smoltalk (e.g. globally-installed CLIs): the file is
|
|
28
|
+
* imported directly and Node resolution is skipped. Hosts own discovering
|
|
29
|
+
* that path; smoltalk never probes global npm roots and reads no env vars.
|
|
30
|
+
* - Registers the module's LlamaCPP class under "llama-cpp" unless that name
|
|
31
|
+
* is already registered. An existing registration is left untouched, but
|
|
32
|
+
* the module is still imported, validated, and returned — an existing
|
|
33
|
+
* registration wins the registry, never the return value. Registration is
|
|
34
|
+
* re-ensured from the cached module on EVERY call, so a later
|
|
35
|
+
* unregisterProvider("llama-cpp") is undone by the next load call without
|
|
36
|
+
* a second import.
|
|
37
|
+
* - Concurrent first calls share one in-flight load. A failed load clears
|
|
38
|
+
* the cache so a later call can retry (e.g. after installing the package).
|
|
39
|
+
* A second call with a different entryPath after a successful load returns
|
|
40
|
+
* the already-loaded module (first load wins).
|
|
41
|
+
*/
|
|
42
|
+
export function loadLlamaCpp(options) {
|
|
43
|
+
if (!cachedLoad) {
|
|
44
|
+
const load = doLoad(options?.entryPath);
|
|
45
|
+
cachedLoad = load;
|
|
46
|
+
load.catch(() => {
|
|
47
|
+
if (cachedLoad === load) {
|
|
48
|
+
cachedLoad = undefined;
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
// Registration happens on EVERY call, not only inside the first load: a
|
|
53
|
+
// consumer can unregisterProvider("llama-cpp") after a successful load,
|
|
54
|
+
// and the cached module must be re-registered on the next call or the
|
|
55
|
+
// provider stays missing for the life of the process.
|
|
56
|
+
return cachedLoad.then((plugin) => {
|
|
57
|
+
if (!hasProvider("llama-cpp")) {
|
|
58
|
+
registerProvider("llama-cpp", plugin.LlamaCPP);
|
|
59
|
+
}
|
|
60
|
+
return plugin;
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
async function doLoad(entryPath) {
|
|
64
|
+
let importSource;
|
|
65
|
+
if (entryPath) {
|
|
66
|
+
importSource = pathToFileURL(entryPath).href;
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
importSource = "smoltalk-llama-cpp";
|
|
70
|
+
}
|
|
71
|
+
let mod;
|
|
72
|
+
try {
|
|
73
|
+
mod = await importFn(importSource);
|
|
74
|
+
}
|
|
75
|
+
catch (error) {
|
|
76
|
+
if (!entryPath && isPluginNotInstalledError(error)) {
|
|
77
|
+
throw new SmolError("The llama-cpp provider needs the optional smoltalk-llama-cpp package. " +
|
|
78
|
+
"Install it (npm i smoltalk-llama-cpp) and try again.", { cause: error });
|
|
79
|
+
}
|
|
80
|
+
throw new SmolError(`Failed to load smoltalk-llama-cpp from ${importSource}: ${errorMessage(error)}`, { cause: error });
|
|
81
|
+
}
|
|
82
|
+
if (typeof mod.LlamaCPP !== "function") {
|
|
83
|
+
throw new SmolError(`The module imported as ${importSource} does not export LlamaCPP — ` +
|
|
84
|
+
"it does not appear to be the smoltalk-llama-cpp package.");
|
|
85
|
+
}
|
|
86
|
+
if (typeof mod.resolveModel !== "function") {
|
|
87
|
+
throw new SmolError("Your installed smoltalk-llama-cpp is too old for this version of smoltalk. " +
|
|
88
|
+
"Upgrade it (npm i smoltalk-llama-cpp@latest; >=0.2.0 required).");
|
|
89
|
+
}
|
|
90
|
+
return mod;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* True only when the smoltalk-llama-cpp specifier itself failed to resolve —
|
|
94
|
+
* not when the package exists but its own import chain broke (e.g. a
|
|
95
|
+
* node-llama-cpp binary problem), where an install hint would mislead.
|
|
96
|
+
*/
|
|
97
|
+
function isPluginNotInstalledError(error) {
|
|
98
|
+
const code = error?.code;
|
|
99
|
+
if (code !== "ERR_MODULE_NOT_FOUND") {
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
return errorMessage(error).includes("'smoltalk-llama-cpp'");
|
|
103
|
+
}
|
|
104
|
+
function errorMessage(error) {
|
|
105
|
+
if (error instanceof Error) {
|
|
106
|
+
return error.message;
|
|
107
|
+
}
|
|
108
|
+
return String(error);
|
|
109
|
+
}
|
package/dist/functions.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { BaseMessage, messageFromJSON, } from "./classes/message/index.js";
|
|
2
|
-
import { getClient } from "./client.js";
|
|
2
|
+
import { getClient, hasProvider } from "./client.js";
|
|
3
|
+
import { loadLlamaCpp } from "./clients/llamaCppLoader.js";
|
|
3
4
|
import { getLogger } from "./util/logger.js";
|
|
4
5
|
function fixMessagesIfNecessary(messages) {
|
|
5
6
|
if (messages && messages.length > 0) {
|
|
@@ -16,10 +17,16 @@ export function text(config) {
|
|
|
16
17
|
return textSync(config);
|
|
17
18
|
}
|
|
18
19
|
export async function textSync(config) {
|
|
20
|
+
if (config.provider === "llama-cpp" && !hasProvider("llama-cpp")) {
|
|
21
|
+
await loadLlamaCpp();
|
|
22
|
+
}
|
|
19
23
|
config.messages = fixMessagesIfNecessary(config.messages);
|
|
20
24
|
return getClient(config).textSync(config);
|
|
21
25
|
}
|
|
22
26
|
export async function* textStream(config) {
|
|
27
|
+
if (config.provider === "llama-cpp" && !hasProvider("llama-cpp")) {
|
|
28
|
+
await loadLlamaCpp();
|
|
29
|
+
}
|
|
23
30
|
config.messages = fixMessagesIfNecessary(config.messages);
|
|
24
31
|
yield* getClient(config).textStream(config);
|
|
25
32
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -8,6 +8,8 @@ export * from "./util/util.js";
|
|
|
8
8
|
export * from "./util/tool.js";
|
|
9
9
|
export * from "./classes/message/index.js";
|
|
10
10
|
export * from "./functions.js";
|
|
11
|
+
export { loadLlamaCpp } from "./clients/llamaCppLoader.js";
|
|
12
|
+
export type { LlamaCppModule } from "./clients/llamaCppLoader.js";
|
|
11
13
|
export * from "./classes/ToolCall.js";
|
|
12
14
|
export * from "./embed.js";
|
|
13
15
|
export * from "./image.js";
|
package/dist/index.js
CHANGED
|
@@ -8,6 +8,8 @@ export * from "./util/util.js";
|
|
|
8
8
|
export * from "./util/tool.js";
|
|
9
9
|
export * from "./classes/message/index.js";
|
|
10
10
|
export * from "./functions.js";
|
|
11
|
+
// Explicit (not `export *`) so the test-only `_setImportForTests` stays off the public surface.
|
|
12
|
+
export { loadLlamaCpp } from "./clients/llamaCppLoader.js";
|
|
11
13
|
export * from "./classes/ToolCall.js";
|
|
12
14
|
export * from "./embed.js";
|
|
13
15
|
export * from "./image.js";
|
package/dist/model.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { getModel, getModelForProvider, isSpeechToTextModel,
|
|
1
|
+
import { getModel, getModelForProvider, isSpeechToTextModel, isTextToSpeechModel, ModelNameSchema, } from "./models.js";
|
|
2
2
|
import { SmolError } from "./smolError.js";
|
|
3
3
|
import { round } from "./util/util.js";
|
|
4
4
|
const TOKEN_COST_UNIT = 1_000_000;
|
|
@@ -32,7 +32,23 @@ export class Model {
|
|
|
32
32
|
else {
|
|
33
33
|
model = getModel(this.model, this.modelData);
|
|
34
34
|
}
|
|
35
|
-
if (!model
|
|
35
|
+
if (!model) {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
// This token engine prices text generation and token-billed audio models
|
|
39
|
+
// (e.g. Gemini TTS). Image and embeddings models have their own cost paths,
|
|
40
|
+
// so they are never priced here even if they carry text-token rates.
|
|
41
|
+
if (model.type === "image" || model.type === "embeddings") {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
// BaseModel token-rate fields, read structurally across the model union.
|
|
45
|
+
const rates = model;
|
|
46
|
+
// Price only models that carry at least one token rate; those without
|
|
47
|
+
// (per-minute STT, per-char TTS) return null so their dedicated helpers apply.
|
|
48
|
+
if (rates.inputTokenCost === undefined &&
|
|
49
|
+
rates.outputTokenCost === undefined &&
|
|
50
|
+
rates.inputAudioTokenCost === undefined &&
|
|
51
|
+
rates.outputAudioTokenCost === undefined) {
|
|
36
52
|
return null;
|
|
37
53
|
}
|
|
38
54
|
const cachedTokens = usage.cachedInputTokens ?? 0;
|
|
@@ -40,15 +56,15 @@ export class Model {
|
|
|
40
56
|
// Disjoint buckets. If a discount price isn't defined for this model,
|
|
41
57
|
// the tokens were still billed by the provider — charge them at the
|
|
42
58
|
// full input rate so totalCost stays honest.
|
|
43
|
-
const cachedRate =
|
|
44
|
-
const cacheCreationRate =
|
|
45
|
-
const inputCost = round((usage.inputTokens * (
|
|
46
|
-
const outputCost = round((usage.outputTokens * (
|
|
59
|
+
const cachedRate = rates.cachedInputTokenCost ?? rates.inputTokenCost ?? 0;
|
|
60
|
+
const cacheCreationRate = rates.cacheCreationInputTokenCost ?? rates.inputTokenCost ?? 0;
|
|
61
|
+
const inputCost = round((usage.inputTokens * (rates.inputTokenCost || 0)) / TOKEN_COST_UNIT, 6);
|
|
62
|
+
const outputCost = round((usage.outputTokens * (rates.outputTokenCost || 0)) / TOKEN_COST_UNIT, 6);
|
|
47
63
|
const audioInTokens = usage.inputAudioTokens ?? 0;
|
|
48
64
|
const audioOutTokens = usage.outputAudioTokens ?? 0;
|
|
49
65
|
// Fall back to the text rate if no audio rate is defined so the total stays honest.
|
|
50
|
-
const audioInRate =
|
|
51
|
-
const audioOutRate =
|
|
66
|
+
const audioInRate = rates.inputAudioTokenCost ?? rates.inputTokenCost ?? 0;
|
|
67
|
+
const audioOutRate = rates.outputAudioTokenCost ?? rates.outputTokenCost ?? 0;
|
|
52
68
|
const audioInCost = round((audioInTokens * audioInRate) / TOKEN_COST_UNIT, 6);
|
|
53
69
|
const audioOutCost = round((audioOutTokens * audioOutRate) / TOKEN_COST_UNIT, 6);
|
|
54
70
|
// Only expose cachedInputCost / cacheCreationInputCost when the model
|
|
@@ -59,7 +75,7 @@ export class Model {
|
|
|
59
75
|
let foldedInputDollars = 0;
|
|
60
76
|
if (cachedTokens > 0) {
|
|
61
77
|
const dollars = (cachedTokens * cachedRate) / 1_000_000;
|
|
62
|
-
if (
|
|
78
|
+
if (rates.cachedInputTokenCost != null) {
|
|
63
79
|
cachedInputCost = round(dollars, 6);
|
|
64
80
|
}
|
|
65
81
|
else {
|
|
@@ -68,7 +84,7 @@ export class Model {
|
|
|
68
84
|
}
|
|
69
85
|
if (cacheCreationTokens > 0) {
|
|
70
86
|
const dollars = (cacheCreationTokens * cacheCreationRate) / 1_000_000;
|
|
71
|
-
if (
|
|
87
|
+
if (rates.cacheCreationInputTokenCost != null) {
|
|
72
88
|
cacheCreationInputCost = round(dollars, 6);
|
|
73
89
|
}
|
|
74
90
|
else {
|
|
@@ -115,7 +131,10 @@ export function calculateTranscriptionCost(model, durationSeconds) {
|
|
|
115
131
|
if (model.perMinuteCost === undefined || durationSeconds === undefined || durationSeconds === null) {
|
|
116
132
|
return undefined;
|
|
117
133
|
}
|
|
118
|
-
|
|
134
|
+
// Providers may bill a minimum duration regardless of actual length
|
|
135
|
+
// (e.g. Groq rounds up to 10s), so a shorter clip isn't understated.
|
|
136
|
+
const billedSeconds = Math.max(durationSeconds, model.minimumBillableSeconds ?? 0);
|
|
137
|
+
const inputCost = round((billedSeconds / 60) * model.perMinuteCost, 6);
|
|
119
138
|
return { inputCost, outputCost: 0, totalCost: inputCost, currency: "USD" };
|
|
120
139
|
}
|
|
121
140
|
/** Per-code-point TTS pricing from a registry entry; same omission semantics. */
|
package/dist/models.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { type ModelDataBlob, type HostedTool, type HostedToolPrice } from "./modelData.js";
|
|
3
|
-
export declare const providers: readonly ["ollama", "openai", "openai-responses", "anthropic", "google", "replicate", "modal", "openrouter", "deepinfra", "litellm", "openai-compat"];
|
|
3
|
+
export declare const providers: readonly ["ollama", "openai", "openai-responses", "anthropic", "google", "replicate", "modal", "openrouter", "deepinfra", "litellm", "openai-compat", "groq"];
|
|
4
4
|
export declare const ProviderSchema: z.ZodEnum<{
|
|
5
5
|
openai: "openai";
|
|
6
6
|
anthropic: "anthropic";
|
|
@@ -13,6 +13,7 @@ export declare const ProviderSchema: z.ZodEnum<{
|
|
|
13
13
|
deepinfra: "deepinfra";
|
|
14
14
|
litellm: "litellm";
|
|
15
15
|
"openai-compat": "openai-compat";
|
|
16
|
+
groq: "groq";
|
|
16
17
|
}>;
|
|
17
18
|
export type Provider = z.infer<typeof ProviderSchema>;
|
|
18
19
|
export type BaseModel = {
|
|
@@ -23,6 +24,8 @@ export type BaseModel = {
|
|
|
23
24
|
cachedInputTokenCost?: number;
|
|
24
25
|
cacheCreationInputTokenCost?: number;
|
|
25
26
|
outputTokenCost?: number;
|
|
27
|
+
inputAudioTokenCost?: number;
|
|
28
|
+
outputAudioTokenCost?: number;
|
|
26
29
|
disabled?: boolean;
|
|
27
30
|
costUnit?: "tokens" | "characters" | "minutes";
|
|
28
31
|
knowledge?: string;
|
|
@@ -34,6 +37,8 @@ export type BaseModel = {
|
|
|
34
37
|
export type SpeechToTextModel = BaseModel & {
|
|
35
38
|
type: "speech-to-text";
|
|
36
39
|
perMinuteCost?: number;
|
|
40
|
+
/** Provider's minimum billable duration in seconds (e.g. Groq bills >= 10s). */
|
|
41
|
+
minimumBillableSeconds?: number;
|
|
37
42
|
/** Canonical MIME types accepted after alias normalization through AUDIO_FORMATS. */
|
|
38
43
|
supportedMimeTypes?: readonly string[];
|
|
39
44
|
/** Provider upload cap in bytes. */
|
|
@@ -90,10 +95,11 @@ export type TextModel = BaseModel & {
|
|
|
90
95
|
input: string[];
|
|
91
96
|
output: string[];
|
|
92
97
|
};
|
|
98
|
+
/** Audio-input constraints when this multimodal model is used for transcription. */
|
|
99
|
+
supportedMimeTypes?: readonly string[];
|
|
100
|
+
maxBytes?: number;
|
|
93
101
|
structuredOutput?: boolean;
|
|
94
102
|
temperatureSupported?: boolean;
|
|
95
|
-
inputAudioTokenCost?: number;
|
|
96
|
-
outputAudioTokenCost?: number;
|
|
97
103
|
/** Pricing that applies above a context-size threshold (e.g. Gemini >200k). */
|
|
98
104
|
longContext?: {
|
|
99
105
|
thresholdTokens: number;
|
|
@@ -116,6 +122,22 @@ export declare const speechToTextModels: readonly [{
|
|
|
116
122
|
readonly provider: "openai";
|
|
117
123
|
readonly supportedMimeTypes: readonly ["audio/flac", "audio/mpeg", "audio/mp4", "audio/m4a", "audio/ogg", "audio/wav", "audio/webm"];
|
|
118
124
|
readonly maxBytes: number;
|
|
125
|
+
}, {
|
|
126
|
+
readonly type: "speech-to-text";
|
|
127
|
+
readonly modelName: "whisper-large-v3";
|
|
128
|
+
readonly provider: "groq";
|
|
129
|
+
readonly perMinuteCost: 0.00185;
|
|
130
|
+
readonly minimumBillableSeconds: 10;
|
|
131
|
+
readonly supportedMimeTypes: readonly ["audio/flac", "audio/mpeg", "audio/mp4", "audio/m4a", "audio/ogg", "audio/wav", "audio/webm"];
|
|
132
|
+
readonly maxBytes: number;
|
|
133
|
+
}, {
|
|
134
|
+
readonly type: "speech-to-text";
|
|
135
|
+
readonly modelName: "whisper-large-v3-turbo";
|
|
136
|
+
readonly provider: "groq";
|
|
137
|
+
readonly perMinuteCost: 0.000667;
|
|
138
|
+
readonly minimumBillableSeconds: 10;
|
|
139
|
+
readonly supportedMimeTypes: readonly ["audio/flac", "audio/mpeg", "audio/mp4", "audio/m4a", "audio/ogg", "audio/wav", "audio/webm"];
|
|
140
|
+
readonly maxBytes: number;
|
|
119
141
|
}];
|
|
120
142
|
export declare const textToSpeechModels: readonly [{
|
|
121
143
|
readonly type: "text-to-speech";
|
|
@@ -139,6 +161,34 @@ export declare const textToSpeechModels: readonly [{
|
|
|
139
161
|
readonly max: 4;
|
|
140
162
|
};
|
|
141
163
|
readonly formats: readonly ["mp3", "opus", "aac", "flac", "wav", "pcm"];
|
|
164
|
+
}, {
|
|
165
|
+
readonly type: "text-to-speech";
|
|
166
|
+
readonly modelName: "canopylabs/orpheus-v1-english";
|
|
167
|
+
readonly provider: "groq";
|
|
168
|
+
readonly perCharacterCost: 0.000022;
|
|
169
|
+
readonly maxInputChars: 200;
|
|
170
|
+
readonly formats: readonly ["wav"];
|
|
171
|
+
}, {
|
|
172
|
+
readonly type: "text-to-speech";
|
|
173
|
+
readonly modelName: "canopylabs/orpheus-arabic-saudi";
|
|
174
|
+
readonly provider: "groq";
|
|
175
|
+
readonly perCharacterCost: 0.00004;
|
|
176
|
+
readonly maxInputChars: 200;
|
|
177
|
+
readonly formats: readonly ["wav"];
|
|
178
|
+
}, {
|
|
179
|
+
readonly type: "text-to-speech";
|
|
180
|
+
readonly modelName: "gemini-2.5-flash-preview-tts";
|
|
181
|
+
readonly provider: "google";
|
|
182
|
+
readonly inputTokenCost: 0.5;
|
|
183
|
+
readonly outputAudioTokenCost: 10;
|
|
184
|
+
readonly formats: readonly ["pcm", "wav"];
|
|
185
|
+
}, {
|
|
186
|
+
readonly type: "text-to-speech";
|
|
187
|
+
readonly modelName: "gemini-2.5-pro-preview-tts";
|
|
188
|
+
readonly provider: "google";
|
|
189
|
+
readonly inputTokenCost: 1;
|
|
190
|
+
readonly outputAudioTokenCost: 20;
|
|
191
|
+
readonly formats: readonly ["pcm", "wav"];
|
|
142
192
|
}];
|
|
143
193
|
export declare const textModels: readonly [{
|
|
144
194
|
readonly type: "text";
|
|
@@ -1203,6 +1253,8 @@ export declare const textModels: readonly [{
|
|
|
1203
1253
|
readonly input: readonly ["text", "image", "audio", "video", "pdf"];
|
|
1204
1254
|
readonly output: readonly ["text"];
|
|
1205
1255
|
};
|
|
1256
|
+
readonly supportedMimeTypes: readonly ["audio/wav", "audio/mpeg", "audio/aac", "audio/ogg", "audio/flac", "audio/aiff"];
|
|
1257
|
+
readonly maxBytes: 14000000;
|
|
1206
1258
|
readonly knowledge: "2025-01";
|
|
1207
1259
|
readonly releaseDate: "2025-06-17";
|
|
1208
1260
|
readonly lastUpdated: "2025-06-17";
|
|
@@ -1771,5 +1823,11 @@ export declare function isImageModel(model: ModelType): model is ImageModel;
|
|
|
1771
1823
|
export declare function isTextModel(model: ModelType): model is TextModel;
|
|
1772
1824
|
export declare function isSpeechToTextModel(model: ModelType): model is SpeechToTextModel;
|
|
1773
1825
|
export declare function isTextToSpeechModel(model: ModelType): model is TextToSpeechModel;
|
|
1826
|
+
/** Audio-input constraints, readable off either a dedicated STT model or a
|
|
1827
|
+
* multimodal text model. Empty for any other model type. */
|
|
1828
|
+
export declare function audioInputConstraints(model: ModelType): {
|
|
1829
|
+
maxBytes?: number;
|
|
1830
|
+
supportedMimeTypes?: readonly string[];
|
|
1831
|
+
};
|
|
1774
1832
|
export declare function isEmbeddingsModel(model: ModelType): model is EmbeddingsModel;
|
|
1775
1833
|
export declare const ModelNameSchema: z.ZodString;
|
package/dist/models.js
CHANGED
|
@@ -12,6 +12,7 @@ export const providers = [
|
|
|
12
12
|
"deepinfra",
|
|
13
13
|
"litellm",
|
|
14
14
|
"openai-compat",
|
|
15
|
+
"groq",
|
|
15
16
|
];
|
|
16
17
|
export const ProviderSchema = z.enum(providers);
|
|
17
18
|
export const speechToTextModels = [
|
|
@@ -26,6 +27,32 @@ export const speechToTextModels = [
|
|
|
26
27
|
],
|
|
27
28
|
maxBytes: 25 * 1024 * 1024,
|
|
28
29
|
},
|
|
30
|
+
{
|
|
31
|
+
type: "speech-to-text",
|
|
32
|
+
modelName: "whisper-large-v3",
|
|
33
|
+
provider: "groq",
|
|
34
|
+
perMinuteCost: 0.00185, // $0.111/hr, verified 2026-08-09
|
|
35
|
+
minimumBillableSeconds: 10, // Groq bills a 10s minimum per request
|
|
36
|
+
supportedMimeTypes: [
|
|
37
|
+
"audio/flac", "audio/mpeg", "audio/mp4", "audio/m4a", "audio/ogg",
|
|
38
|
+
"audio/wav", "audio/webm",
|
|
39
|
+
],
|
|
40
|
+
// Conservative free-tier / direct-attachment cap; Groq's developer tier
|
|
41
|
+
// allows 100 MB, but a single baked-in record cannot vary by account tier.
|
|
42
|
+
maxBytes: 25 * 1024 * 1024,
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
type: "speech-to-text",
|
|
46
|
+
modelName: "whisper-large-v3-turbo",
|
|
47
|
+
provider: "groq",
|
|
48
|
+
perMinuteCost: 0.000667, // $0.04/hr, verified 2026-08-09
|
|
49
|
+
minimumBillableSeconds: 10, // Groq bills a 10s minimum per request
|
|
50
|
+
supportedMimeTypes: [
|
|
51
|
+
"audio/flac", "audio/mpeg", "audio/mp4", "audio/m4a", "audio/ogg",
|
|
52
|
+
"audio/wav", "audio/webm",
|
|
53
|
+
],
|
|
54
|
+
maxBytes: 25 * 1024 * 1024,
|
|
55
|
+
},
|
|
29
56
|
];
|
|
30
57
|
export const textToSpeechModels = [
|
|
31
58
|
{
|
|
@@ -46,6 +73,40 @@ export const textToSpeechModels = [
|
|
|
46
73
|
speedRange: { min: 0.25, max: 4 },
|
|
47
74
|
formats: ["mp3", "opus", "aac", "flac", "wav", "pcm"],
|
|
48
75
|
},
|
|
76
|
+
{
|
|
77
|
+
type: "text-to-speech",
|
|
78
|
+
modelName: "canopylabs/orpheus-v1-english",
|
|
79
|
+
provider: "groq",
|
|
80
|
+
perCharacterCost: 0.000022, // $22 / 1M chars, verified 2026-08-09
|
|
81
|
+
maxInputChars: 200,
|
|
82
|
+
formats: ["wav"],
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
type: "text-to-speech",
|
|
86
|
+
modelName: "canopylabs/orpheus-arabic-saudi",
|
|
87
|
+
provider: "groq",
|
|
88
|
+
perCharacterCost: 0.00004, // $40 / 1M chars, verified 2026-08-09
|
|
89
|
+
maxInputChars: 200,
|
|
90
|
+
formats: ["wav"],
|
|
91
|
+
},
|
|
92
|
+
// Gemini TTS is token-billed (text input + audio output). No maxInputChars:
|
|
93
|
+
// Gemini documents a 32k-token context, and characters are not a sound proxy.
|
|
94
|
+
{
|
|
95
|
+
type: "text-to-speech",
|
|
96
|
+
modelName: "gemini-2.5-flash-preview-tts",
|
|
97
|
+
provider: "google",
|
|
98
|
+
inputTokenCost: 0.5, // $/1M text-input tokens, verified 2026-08-09
|
|
99
|
+
outputAudioTokenCost: 10.0, // $/1M audio-output tokens
|
|
100
|
+
formats: ["pcm", "wav"],
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
type: "text-to-speech",
|
|
104
|
+
modelName: "gemini-2.5-pro-preview-tts",
|
|
105
|
+
provider: "google",
|
|
106
|
+
inputTokenCost: 1.0, // $/1M text-input tokens, verified 2026-08-09
|
|
107
|
+
outputAudioTokenCost: 20.0, // $/1M audio-output tokens
|
|
108
|
+
formats: ["pcm", "wav"],
|
|
109
|
+
},
|
|
49
110
|
];
|
|
50
111
|
export const textModels = [
|
|
51
112
|
{
|
|
@@ -1148,6 +1209,11 @@ export const textModels = [
|
|
|
1148
1209
|
input: ["text", "image", "audio", "video", "pdf"],
|
|
1149
1210
|
output: ["text"],
|
|
1150
1211
|
},
|
|
1212
|
+
// Audio-input (transcription) constraints. maxBytes is a conservative raw cap
|
|
1213
|
+
// leaving room for base64 expansion + instructions under Gemini's 20 MB total
|
|
1214
|
+
// inline request limit; the client also checks the encoded request size.
|
|
1215
|
+
supportedMimeTypes: ["audio/wav", "audio/mpeg", "audio/aac", "audio/ogg", "audio/flac", "audio/aiff"],
|
|
1216
|
+
maxBytes: 14_000_000,
|
|
1151
1217
|
knowledge: "2025-01",
|
|
1152
1218
|
releaseDate: "2025-06-17",
|
|
1153
1219
|
lastUpdated: "2025-06-17",
|
|
@@ -1975,6 +2041,14 @@ export function isSpeechToTextModel(model) {
|
|
|
1975
2041
|
export function isTextToSpeechModel(model) {
|
|
1976
2042
|
return model.type === "text-to-speech";
|
|
1977
2043
|
}
|
|
2044
|
+
/** Audio-input constraints, readable off either a dedicated STT model or a
|
|
2045
|
+
* multimodal text model. Empty for any other model type. */
|
|
2046
|
+
export function audioInputConstraints(model) {
|
|
2047
|
+
if (model.type === "speech-to-text" || model.type === "text") {
|
|
2048
|
+
return { maxBytes: model.maxBytes, supportedMimeTypes: model.supportedMimeTypes };
|
|
2049
|
+
}
|
|
2050
|
+
return {};
|
|
2051
|
+
}
|
|
1978
2052
|
export function isEmbeddingsModel(model) {
|
|
1979
2053
|
return model.type === "embeddings";
|
|
1980
2054
|
}
|