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.
Files changed (44) hide show
  1. package/README.md +74 -6
  2. package/dist/client.d.ts +6 -0
  3. package/dist/client.js +12 -0
  4. package/dist/clients/llamaCppLoader.d.ts +42 -0
  5. package/dist/clients/llamaCppLoader.js +109 -0
  6. package/dist/functions.js +8 -1
  7. package/dist/index.d.ts +2 -0
  8. package/dist/index.js +2 -0
  9. package/dist/model.js +30 -11
  10. package/dist/models.d.ts +61 -3
  11. package/dist/models.js +74 -0
  12. package/dist/speech/baseSpeechClient.d.ts +5 -0
  13. package/dist/speech/baseSpeechClient.js +21 -2
  14. package/dist/speech/google.d.ts +6 -0
  15. package/dist/speech/google.js +54 -0
  16. package/dist/speech/groq.d.ts +11 -0
  17. package/dist/speech/groq.js +19 -0
  18. package/dist/speech/openai.d.ts +8 -0
  19. package/dist/speech/openai.js +16 -4
  20. package/dist/speech/openaiCompat.d.ts +13 -0
  21. package/dist/speech/openaiCompat.js +22 -0
  22. package/dist/speech.d.ts +5 -0
  23. package/dist/speech.js +6 -0
  24. package/dist/transcription/baseTranscriptionClient.d.ts +5 -0
  25. package/dist/transcription/baseTranscriptionClient.js +44 -18
  26. package/dist/transcription/google.d.ts +6 -0
  27. package/dist/transcription/google.js +56 -0
  28. package/dist/transcription/groq.d.ts +10 -0
  29. package/dist/transcription/groq.js +17 -0
  30. package/dist/transcription/openai.d.ts +5 -0
  31. package/dist/transcription/openai.js +11 -3
  32. package/dist/transcription/openaiCompat.d.ts +13 -0
  33. package/dist/transcription/openaiCompat.js +22 -0
  34. package/dist/transcription.d.ts +3 -0
  35. package/dist/transcription.js +6 -0
  36. package/dist/types.d.ts +1 -0
  37. package/dist/util/audioMime.d.ts +17 -0
  38. package/dist/util/audioMime.js +42 -1
  39. package/dist/util/googleAudioUsage.d.ts +14 -0
  40. package/dist/util/googleAudioUsage.js +52 -0
  41. package/dist/util/mime.js +2 -0
  42. package/dist/util/provider.d.ts +1 -0
  43. package/dist/util/provider.js +2 -0
  44. 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, all OpenAI-only in v1. `transcribe()` (speech-to-text)
513
- and `speak()` (text-to-speech) are async and return `Result<T>` (never throw).
514
- `audioPart()` (attach audio to a chat message) is different: it's a
515
- synchronous plain-object constructor, not a `Result`-returning call — see
516
- "Audio in chat" below.
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` is the only baked-in model in v1. Options: `language`, `prompt`,
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, isTextModel, isTextToSpeechModel, ModelNameSchema, } from "./models.js";
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 || !isTextModel(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 = model.cachedInputTokenCost ?? model.inputTokenCost ?? 0;
44
- const cacheCreationRate = model.cacheCreationInputTokenCost ?? model.inputTokenCost ?? 0;
45
- const inputCost = round((usage.inputTokens * (model.inputTokenCost || 0)) / TOKEN_COST_UNIT, 6);
46
- const outputCost = round((usage.outputTokens * (model.outputTokenCost || 0)) / TOKEN_COST_UNIT, 6);
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 = model.inputAudioTokenCost ?? model.inputTokenCost ?? 0;
51
- const audioOutRate = model.outputAudioTokenCost ?? model.outputTokenCost ?? 0;
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 (model.cachedInputTokenCost != null) {
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 (model.cacheCreationInputTokenCost != null) {
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
- const inputCost = round((durationSeconds / 60) * model.perMinuteCost, 6);
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
  }