@broberg/ai-sdk 0.26.0 → 0.28.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 +50 -0
- package/dist/{chunk-IZG5UZH5.js → chunk-LUPUAAHZ.js} +7 -1
- package/dist/chunk-LUPUAAHZ.js.map +1 -0
- package/dist/index.d.ts +90 -3
- package/dist/index.js +196 -10
- package/dist/index.js.map +1 -1
- package/dist/pricing.js +1 -1
- package/package.json +1 -1
- package/dist/chunk-IZG5UZH5.js.map +0 -1
package/README.md
CHANGED
|
@@ -87,6 +87,56 @@ const ai = createAI({
|
|
|
87
87
|
Sinks: `upmetricsSink` (canonical), `discordSink`, `sqliteSink`, `multiSink`,
|
|
88
88
|
`noopSink`. A failing sink never crashes a call.
|
|
89
89
|
|
|
90
|
+
### Cost-tracking is on by default (v0.24+)
|
|
91
|
+
|
|
92
|
+
You don't have to wire a sink. If `UPMETRICS_API_KEY` is in the env, a bare
|
|
93
|
+
`createAI()` auto-attaches the upmetrics sink — this exists because most
|
|
94
|
+
call-sites passed no sink, leaving ~91% of Mistral spend invisible. No key in
|
|
95
|
+
the env → no sink, no crash (ship-dark).
|
|
96
|
+
|
|
97
|
+
```ts
|
|
98
|
+
createAI(); // key in env → tracked; no key → nothing happens
|
|
99
|
+
createAI({ costSink: mySink }); // explicit sink wins
|
|
100
|
+
createAI({ costSink: null }); // explicit OPT-OUT — see below
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
| Env var | Effect |
|
|
104
|
+
|---|---|
|
|
105
|
+
| `UPMETRICS_API_KEY` | **The only switch.** Present → tracking on. |
|
|
106
|
+
| `UPMETRICS_AGENT_NAME` | Row label. Set it — the fallback is `npm_package_name`, which is empty for a process not started via an npm/bun script (a `node dist/…` or Docker service logs as `unknown`). |
|
|
107
|
+
| `UPMETRICS_BASE_URL` | Ingest host. Defaults to `https://upmetrics.org`. |
|
|
108
|
+
| `UPMETRICS_COMPLIANCE` | `1` sets compliance mode. `Usage` carries no prompt/response text either way. |
|
|
109
|
+
|
|
110
|
+
**Adopting it in a repo — two things to do first, or the adoption corrupts the
|
|
111
|
+
numbers it was meant to reveal:**
|
|
112
|
+
|
|
113
|
+
1. **Opt out wherever you already report your own costs.** Pass
|
|
114
|
+
`costSink: null`. Otherwise the SDK adds a *second* reporting path on top of
|
|
115
|
+
yours and every call is counted **twice**, in production, with no error
|
|
116
|
+
anywhere. (Found by `buddy`, who aggregates to `cli_usage` and pushes hourly.)
|
|
117
|
+
|
|
118
|
+
2. **Keep the key out of your test run.** Test runners auto-load `.env` (Bun
|
|
119
|
+
does; Vitest with a dotenv setup does), so any suite that reaches a
|
|
120
|
+
`createAI()` will POST **fabricated usage into production telemetry**. Note
|
|
121
|
+
the trigger is *importing* a module that builds the client — a module-level
|
|
122
|
+
`createAI()` arms it without anything calling it. Strip the whole prefix:
|
|
123
|
+
|
|
124
|
+
```ts
|
|
125
|
+
// test-setup.ts — bunfig.toml: [test] preload = ["./test-setup.ts"]
|
|
126
|
+
for (const k of Object.keys(process.env)) {
|
|
127
|
+
if (k.startsWith("UPMETRICS_")) delete process.env[k];
|
|
128
|
+
}
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Strip the **prefix**, not a list of names — a guard you must remember to
|
|
132
|
+
update is one that silently rots. And assert the *effect* (no `UPMETRICS_*`
|
|
133
|
+
visible to a test) with a control proving the probe can go red; a test that
|
|
134
|
+
checks "the key is undefined" passes while protecting nothing on a machine
|
|
135
|
+
that never had the key.
|
|
136
|
+
|
|
137
|
+
Adoption is **per repo, opt-in** — there is no fleet-wide push. Turn it on when
|
|
138
|
+
a repo has spend worth watching, with both guards in place.
|
|
139
|
+
|
|
90
140
|
## License
|
|
91
141
|
|
|
92
142
|
FSL-1.1-Apache-2.0
|
|
@@ -67,6 +67,12 @@ var PRICING = {
|
|
|
67
67
|
"gemini:gemini-2.5-flash": { inputPer1M: 0.3, outputPer1M: 2.5, version: V },
|
|
68
68
|
// flash-lite is the default `video` tier (F019) — cheap native video understanding.
|
|
69
69
|
"gemini:gemini-2.5-flash-lite": { inputPer1M: 0.1, outputPer1M: 0.4, version: "2026-06-04-or-xref" },
|
|
70
|
+
// Vertex AI (F038) — the EU-resident route to the SAME Gemini models, so Google's
|
|
71
|
+
// published Gemini token prices apply. Listed separately because cost lookups key on
|
|
72
|
+
// `provider:model`: without these rows an EU vision/video call would silently log
|
|
73
|
+
// $0, which is worse than no tracking (a confident wrong number).
|
|
74
|
+
"vertex:gemini-2.5-flash": { inputPer1M: 0.3, outputPer1M: 2.5, version: V },
|
|
75
|
+
"vertex:gemini-2.5-flash-lite": { inputPer1M: 0.1, outputPer1M: 0.4, version: "2026-06-04-or-xref" },
|
|
70
76
|
// Mistral (direct, La Plateforme). Official prices from mistral.ai/pricing
|
|
71
77
|
// (2026-06-04, per Christian's CD report). EU/Paris-hosted — the designated
|
|
72
78
|
// GDPR-safe provider for client/personal-data workloads (see F015). NB:
|
|
@@ -105,4 +111,4 @@ export {
|
|
|
105
111
|
PRICING,
|
|
106
112
|
getPrice
|
|
107
113
|
};
|
|
108
|
-
//# sourceMappingURL=chunk-
|
|
114
|
+
//# sourceMappingURL=chunk-LUPUAAHZ.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/cost/pricing.ts"],"sourcesContent":["// Versioned per-(provider, model) pricing. F3.6 populates the table + adds tests\n// + MiniMax coverage. F3.1 ships the type + lookup with an empty table, so\n// computeCost returns 0 for every model until F3.6 lands (calls still complete).\nexport interface PricingEntry {\n /** USD per 1M input tokens. */\n inputPer1M: number;\n /** USD per 1M output tokens. */\n outputPer1M: number;\n /** USD per 1M cache-read tokens (falls back to input rate if unset). */\n cacheReadPer1M?: number;\n /** USD per 1M cache-write/creation tokens (falls back to input rate if unset). */\n cacheWritePer1M?: number;\n /** Pricing snapshot version (date or tag) so stale entries are detectable. */\n version: string;\n}\n\n// USD per 1M tokens. Anthropic cache multipliers follow the standard model:\n// cache-read ≈ 0.1× input, cache-write ≈ 1.25× input. Verified against the\n// pricing tables in cms (packages/cms-ai/src/providers) + trail (model-lab).\n// MiniMax M2.7 is an estimate pending confirmation against OpenRouter's live\n// price page — flagged in its version string.\nconst V = \"2026-06-02\";\n// Mistral prices come straight from mistral.ai/pricing (per Christian's CD report).\nconst MS = \"2026-06-04-mistral.ai\";\n\n/** Keyed `${provider}:${model}`. Exported so the catalogue-research job (F014)\n * can enumerate every priced entry and diff it against the live provider lists. */\nexport const PRICING: Record<string, PricingEntry> = {\n // Anthropic (direct API). DEFAULT_TIER_MAP: fast/cheap=haiku, smart/vision=sonnet, powerful=opus.\n \"anthropic:claude-haiku-4-5\": {\n inputPer1M: 0.8,\n outputPer1M: 4.0,\n cacheReadPer1M: 0.08,\n cacheWritePer1M: 1.0,\n version: V,\n },\n \"anthropic:claude-sonnet-4-6\": {\n inputPer1M: 3.0,\n outputPer1M: 15.0,\n cacheReadPer1M: 0.3,\n cacheWritePer1M: 3.75,\n version: V,\n },\n \"anthropic:claude-opus-4-8\": {\n inputPer1M: 15.0,\n outputPer1M: 75.0,\n cacheReadPer1M: 1.5,\n cacheWritePer1M: 18.75,\n version: V,\n },\n\n // OpenAI. embedding default tier = text-embedding-3-small (no output tokens).\n \"openai:text-embedding-3-small\": { inputPer1M: 0.02, outputPer1M: 0, version: V },\n \"openai:text-embedding-3-large\": { inputPer1M: 0.13, outputPer1M: 0, version: V },\n \"openai:gpt-4o\": { inputPer1M: 2.5, outputPer1M: 10.0, version: V },\n \"openai:gpt-4o-mini\": { inputPer1M: 0.15, outputPer1M: 0.6, version: V },\n // Whisper is priced per minute, not per token — not representable here; transcribe\n // (F5.6) computes its own cost. Listed as 0 so token-based compute never charges it.\n \"openai:whisper-1\": { inputPer1M: 0, outputPer1M: 0, version: V },\n\n // OpenRouter (meta-router — model slugs include the upstream vendor). Slugs use\n // dots (claude-sonnet-4.6) to match OpenRouter's live ids; the dashed forms\n // never matched a real call. Caught by the F014 catalogue research.\n \"openrouter:anthropic/claude-sonnet-4.6\": { inputPer1M: 3.0, outputPer1M: 15.0, version: V },\n // OpenRouter ground-truth $1/$5 — a markup over Anthropic-direct's $0.8/$4\n // (the `anthropic:` entry above). Was masked while the slug used dashes.\n \"openrouter:anthropic/claude-haiku-4.5\": { inputPer1M: 1.0, outputPer1M: 5.0, version: \"2026-06-04\" },\n \"openrouter:google/gemini-2.5-flash\": { inputPer1M: 0.3, outputPer1M: 2.5, version: V },\n // Ground-truth from OpenRouter /api/v1/models (was a 0.3 estimate; now 0.279).\n \"openrouter:minimax/minimax-m2.7\": {\n inputPer1M: 0.279,\n outputPer1M: 1.2,\n version: \"2026-06-04\",\n },\n // DeepSeek V4 (CN-hosted — NOT GDPR-safe; non-personal-data workloads only).\n // On 2026-05-22 DeepSeek made the \"75% off\" promo the permanent official price.\n // V4-Pro $0.435/$0.87 is ~34x cheaper than GPT-5.5 on output; flash is cheaper\n // still. Numbers match OpenRouter /api/v1/models 1:1 (no router markup). A strong\n // cheap route for fleet background work once `claude -p` is API-billed (15 Jun).\n \"openrouter:deepseek/deepseek-v4-pro\": { inputPer1M: 0.435, outputPer1M: 0.87, version: \"2026-05-22-deepseek-official\" },\n \"openrouter:deepseek/deepseek-v4-flash\": { inputPer1M: 0.0983, outputPer1M: 0.1966, version: \"2026-05-22-deepseek-official\" },\n // DeepSeek DIRECT API (provider \"deepseek\", F030 non-PII secondary). Rates from\n // api-docs.deepseek.com 2026-06-30 ($0.14/$0.28 per 1M; both map to deepseek-v4-flash).\n // `deepseek-chat` (non-thinking) + `deepseek-reasoner` (thinking) DEPRECATE 2026-07-24.\n // (The bare `deepseek-v4-flash` basename is already priced via the openrouter entry\n // above — kept distinct here to avoid a basename collision in the F027 pricing-API.)\n // Verify against a real key when it lands.\n \"deepseek:deepseek-chat\": { inputPer1M: 0.14, outputPer1M: 0.28, version: \"2026-06-30-deepseek-direct\" },\n \"deepseek:deepseek-reasoner\": { inputPer1M: 0.14, outputPer1M: 0.28, version: \"2026-06-30-deepseek-direct\" },\n\n // Google Gemini (direct). Provider key is \"gemini\" — matches the adapter's\n // usage.provider + the override.provider callers pass. (Image-gen models are\n // priced per-image in the adapter, not here.)\n \"gemini:gemini-2.5-flash\": { inputPer1M: 0.3, outputPer1M: 2.5, version: V },\n // flash-lite is the default `video` tier (F019) — cheap native video understanding.\n \"gemini:gemini-2.5-flash-lite\": { inputPer1M: 0.1, outputPer1M: 0.4, version: \"2026-06-04-or-xref\" },\n\n // Vertex AI (F038) — the EU-resident route to the SAME Gemini models, so Google's\n // published Gemini token prices apply. Listed separately because cost lookups key on\n // `provider:model`: without these rows an EU vision/video call would silently log\n // $0, which is worse than no tracking (a confident wrong number).\n \"vertex:gemini-2.5-flash\": { inputPer1M: 0.3, outputPer1M: 2.5, version: V },\n \"vertex:gemini-2.5-flash-lite\": { inputPer1M: 0.1, outputPer1M: 0.4, version: \"2026-06-04-or-xref\" },\n\n // Mistral (direct, La Plateforme). Official prices from mistral.ai/pricing\n // (2026-06-04, per Christian's CD report). EU/Paris-hosted — the designated\n // GDPR-safe provider for client/personal-data workloads (see F015). NB:\n // medium-3.5 is the premium \"Vibe\" coding tier ($1.5/$7.5); Large 3 ($0.5/$1.5)\n // is the cheaper frontier general-purpose model despite the higher number.\n \"mistral:mistral-large-latest\": { inputPer1M: 0.5, outputPer1M: 1.5, version: MS },\n \"mistral:mistral-large-2512\": { inputPer1M: 0.5, outputPer1M: 1.5, version: MS },\n \"mistral:mistral-medium-latest\": { inputPer1M: 1.5, outputPer1M: 7.5, version: MS },\n \"mistral:mistral-medium-3.5\": { inputPer1M: 1.5, outputPer1M: 7.5, version: MS },\n \"mistral:mistral-medium-3\": { inputPer1M: 0.4, outputPer1M: 2.0, version: \"2026-06-04-or-xref\" },\n \"mistral:mistral-small-latest\": { inputPer1M: 0.1, outputPer1M: 0.3, version: MS },\n \"mistral:mistral-small-2603\": { inputPer1M: 0.1, outputPer1M: 0.3, version: MS },\n \"mistral:ministral-3b-latest\": { inputPer1M: 0.1, outputPer1M: 0.1, version: MS },\n \"mistral:ministral-8b-latest\": { inputPer1M: 0.15, outputPer1M: 0.15, version: MS },\n \"mistral:ministral-14b-latest\": { inputPer1M: 0.2, outputPer1M: 0.2, version: MS },\n \"mistral:magistral-medium-latest\": { inputPer1M: 2.0, outputPer1M: 5.0, version: MS },\n \"mistral:magistral-small-latest\": { inputPer1M: 0.5, outputPer1M: 1.5, version: MS },\n \"mistral:devstral-latest\": { inputPer1M: 0.4, outputPer1M: 2.0, version: MS },\n \"mistral:codestral-latest\": { inputPer1M: 0.3, outputPer1M: 0.9, version: MS },\n \"mistral:open-mistral-nemo\": { inputPer1M: 0.15, outputPer1M: 0.15, version: MS },\n // Moderation (F016.4) — per input token; output 0. (OCR is per-page in the adapter.)\n \"mistral:mistral-moderation-latest\": { inputPer1M: 0.1, outputPer1M: 0, version: MS },\n // Embeddings (F016.5) — per input token.\n \"mistral:mistral-embed\": { inputPer1M: 0.1, outputPer1M: 0, version: MS },\n \"mistral:codestral-embed\": { inputPer1M: 0.15, outputPer1M: 0, version: MS },\n};\n\nexport function getPrice(provider: string, model: string): PricingEntry | undefined {\n const exact = PRICING[`${provider}:${model}`];\n if (exact) return exact;\n // Providers ship dated model snapshots, e.g. \"claude-haiku-4-5-20251001\".\n // Strip a trailing -YYYYMMDD and retry the base lookup so a dated variant\n // prices the same as its base model instead of falling through to 0 — a real\n // paid call must never be logged as $0 (F012). Covers openrouter slugs too.\n const base = model.replace(/-\\d{8}$/, \"\");\n if (base !== model) return PRICING[`${provider}:${base}`];\n return undefined;\n}\n"],"mappings":"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","names":[]}
|
package/dist/index.d.ts
CHANGED
|
@@ -1640,6 +1640,9 @@ declare const ttsInputSchema: z.ZodObject<{
|
|
|
1640
1640
|
labels: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
1641
1641
|
text: z.ZodString;
|
|
1642
1642
|
voice: z.ZodString;
|
|
1643
|
+
/** F037: voice to use if `voice` is one we know the provider has retired. Without
|
|
1644
|
+
* it a retired voice throws VoiceUnavailableError rather than reaching the API. */
|
|
1645
|
+
voiceFallback: z.ZodOptional<z.ZodString>;
|
|
1643
1646
|
lang: z.ZodOptional<z.ZodString>;
|
|
1644
1647
|
format: z.ZodOptional<z.ZodString>;
|
|
1645
1648
|
rate: z.ZodOptional<z.ZodNumber>;
|
|
@@ -1660,6 +1663,7 @@ declare const ttsInputSchema: z.ZodObject<{
|
|
|
1660
1663
|
})[] | undefined;
|
|
1661
1664
|
labels?: Record<string, string> | undefined;
|
|
1662
1665
|
format?: string | undefined;
|
|
1666
|
+
voiceFallback?: string | undefined;
|
|
1663
1667
|
lang?: string | undefined;
|
|
1664
1668
|
rate?: number | undefined;
|
|
1665
1669
|
}, {
|
|
@@ -1679,6 +1683,7 @@ declare const ttsInputSchema: z.ZodObject<{
|
|
|
1679
1683
|
})[] | undefined;
|
|
1680
1684
|
labels?: Record<string, string> | undefined;
|
|
1681
1685
|
format?: string | undefined;
|
|
1686
|
+
voiceFallback?: string | undefined;
|
|
1682
1687
|
lang?: string | undefined;
|
|
1683
1688
|
rate?: number | undefined;
|
|
1684
1689
|
}>;
|
|
@@ -2048,8 +2053,8 @@ declare const falStubAdapter: ProviderAdapter;
|
|
|
2048
2053
|
* wires the live adapters. */
|
|
2049
2054
|
declare const stubProviders: Record<string, ProviderAdapter>;
|
|
2050
2055
|
|
|
2051
|
-
declare const VERSION: "0.
|
|
2052
|
-
declare const SDK_TAG: "@broberg/ai-sdk@0.
|
|
2056
|
+
declare const VERSION: "0.28.0";
|
|
2057
|
+
declare const SDK_TAG: "@broberg/ai-sdk@0.28.0";
|
|
2053
2058
|
|
|
2054
2059
|
/** Built-in defaults. Every entry is overridable via AiConfig.defaults or a
|
|
2055
2060
|
* per-call override.
|
|
@@ -2102,6 +2107,88 @@ declare function refreshAvailability(opts?: RefreshOptions): Promise<RefreshResu
|
|
|
2102
2107
|
/** Reset the overlay back to the curated defaults. For tests. */
|
|
2103
2108
|
declare function resetRegistry(): void;
|
|
2104
2109
|
|
|
2110
|
+
type VoiceStatus = "available" | "retired" | "unknown";
|
|
2111
|
+
type VoiceProvider = "elevenlabs" | "azure";
|
|
2112
|
+
/** One row of the shared voice read — what a UI voice-picker renders. */
|
|
2113
|
+
interface VoiceInfo {
|
|
2114
|
+
/** Full provider voice id, e.g. "da-DK-ChristelNeural" / "4RklGmuxoAskAbGXplXN". */
|
|
2115
|
+
id: string;
|
|
2116
|
+
/** Curated friendly name a caller passes as `voice`, e.g. "christel". */
|
|
2117
|
+
name: string;
|
|
2118
|
+
provider: VoiceProvider;
|
|
2119
|
+
/** BCP-47 locale of the voice itself, e.g. "da-DK". NB: an Azure multilingual
|
|
2120
|
+
* voice speaking Danish reports its own locale ("de-DE" for Seraphina) — that
|
|
2121
|
+
* is what the adapter actually sends as xml:lang. */
|
|
2122
|
+
locale: string;
|
|
2123
|
+
/** Only where the provider publishes it — absent is "we do not know", never a
|
|
2124
|
+
* guess from the first name. */
|
|
2125
|
+
gender?: "female" | "male";
|
|
2126
|
+
available: boolean;
|
|
2127
|
+
status: VoiceStatus;
|
|
2128
|
+
/** Why it is unavailable, or any caveat worth showing in a picker. */
|
|
2129
|
+
note?: string;
|
|
2130
|
+
/** ISO date this row's status was last confirmed AGAINST THE PROVIDER. Exposed
|
|
2131
|
+
* on purpose (the F034.1 lesson): a registry that cannot be seen to go stale
|
|
2132
|
+
* is worse than one that can, because nobody knows when to re-check it. */
|
|
2133
|
+
checkedAt: string;
|
|
2134
|
+
}
|
|
2135
|
+
/** Result of checkVoice — deliberately the same shape as ResolveResult (F022) so
|
|
2136
|
+
* a consumer learns one idiom for models and voices. */
|
|
2137
|
+
interface VoiceResolveResult {
|
|
2138
|
+
/** True when the requested voice itself is usable. */
|
|
2139
|
+
ok: boolean;
|
|
2140
|
+
/** The id to actually send: the requested voice's id when ok, else the
|
|
2141
|
+
* fallback's. When there is no usable fallback this is the dead id and `ok`
|
|
2142
|
+
* is false — mirroring resolveModel, the caller must read `ok`. */
|
|
2143
|
+
voiceId: string;
|
|
2144
|
+
/** What the caller asked for, verbatim. */
|
|
2145
|
+
requested: string;
|
|
2146
|
+
provider?: VoiceProvider;
|
|
2147
|
+
/** True when `voiceId` differs from what was requested because we fell back. */
|
|
2148
|
+
fellBack: boolean;
|
|
2149
|
+
status: VoiceStatus;
|
|
2150
|
+
/** Why it degraded / why it is unavailable. */
|
|
2151
|
+
reason?: string;
|
|
2152
|
+
}
|
|
2153
|
+
/** Thrown when the requested voice is retired and no usable fallback exists.
|
|
2154
|
+
* Callers flag on `.code === "voice_unavailable"`. */
|
|
2155
|
+
declare class VoiceUnavailableError extends Error {
|
|
2156
|
+
readonly code = "voice_unavailable";
|
|
2157
|
+
readonly requested: string;
|
|
2158
|
+
readonly provider?: VoiceProvider;
|
|
2159
|
+
readonly note?: string;
|
|
2160
|
+
constructor(requested: string, note?: string, provider?: VoiceProvider);
|
|
2161
|
+
}
|
|
2162
|
+
|
|
2163
|
+
interface CheckVoiceOptions {
|
|
2164
|
+
/** One name/id or an ordered chain to try when `requested` is retired. */
|
|
2165
|
+
fallback?: string | string[];
|
|
2166
|
+
/** Throw VoiceUnavailableError instead of returning ok:false when nothing in the
|
|
2167
|
+
* chain is usable. For callers that want to fail loudly rather than degrade. */
|
|
2168
|
+
throwIfUnavailable?: boolean;
|
|
2169
|
+
}
|
|
2170
|
+
/**
|
|
2171
|
+
* The shared voice read — a UI picker greys out `available:false` rows and can show
|
|
2172
|
+
* `checkedAt` so a stale registry is visible rather than silently trusted.
|
|
2173
|
+
*
|
|
2174
|
+
* NB: distinct from `elevenlabsAdapter().listVoices()`, which is an async call to
|
|
2175
|
+
* ElevenLabs' live API. This one is synchronous, cross-provider, and reads only the
|
|
2176
|
+
* curated registry.
|
|
2177
|
+
*/
|
|
2178
|
+
declare function listVoices(opts?: {
|
|
2179
|
+
provider?: VoiceProvider;
|
|
2180
|
+
}): VoiceInfo[];
|
|
2181
|
+
/**
|
|
2182
|
+
* Resolve a requested voice (curated name or raw provider id) to one that is
|
|
2183
|
+
* actually usable. Synchronous + offline by contract.
|
|
2184
|
+
*
|
|
2185
|
+
* - Available → pass through ({ ok:true, fellBack:false }).
|
|
2186
|
+
* - Retired + a usable fallback → swap ({ ok:false, fellBack:true }).
|
|
2187
|
+
* - Retired + no usable fallback → throw (throwIfUnavailable) or return ok:false.
|
|
2188
|
+
* - Untracked id → treated usable ({ status:"unknown" }), passed through verbatim.
|
|
2189
|
+
*/
|
|
2190
|
+
declare function checkVoice(requested: string, opts?: CheckVoiceOptions): VoiceResolveResult;
|
|
2191
|
+
|
|
2105
2192
|
/**
|
|
2106
2193
|
* Cost in USD for a call. cache-read/creation tokens are priced separately when
|
|
2107
2194
|
* the pricing entry defines rates for them; otherwise they fall back to the
|
|
@@ -2376,4 +2463,4 @@ interface StreamTransportRequest extends TransportRequest {
|
|
|
2376
2463
|
*/
|
|
2377
2464
|
declare function streamTransport(req: StreamTransportRequest): AsyncIterable<string>;
|
|
2378
2465
|
|
|
2379
|
-
export { AZURE_DANISH_VOICES, AZURE_DANISH_VOICE_LIST, type AiClient, type AiConfig, type AzureVoiceInfo, type BatchJob, type BatchRequestItem, type BatchResultItem, type BflAdapterConfig, type BflCredits, type BudgetConfig, BudgetExceededError, BudgetGuard, type BudgetStore, type CallOptions, type Capability, type ChatInput, type ChatRequest, type ChatResult, type ChatStreamEvent, type ClassifyInput, type ClassifyResult, type ContentPart, type Contracts, type CostQuery, type CostSink, type CostSummary, type CostSummaryQuery, type CostTimeseriesQuery, DEFAULT_TIER_MAP, type DesignInput, type DesignResult, type DialogueRequest, type DialogueTurn, type DiscordSinkConfig, ELEVENLABS_DANISH_VOICES, type EmbeddingInput, type EmbeddingRequest, type EmbeddingResult, type ExtractInput, type ExtractResult, type FalAdapterConfig, type HttpResponse, type ImageInput, type ImageRequest, type ImageResult, type LoraWeight, type Message, type MockupInput, type MockupResult, type ModerationInput, type ModerationItem, type ModerationRequest, type ModerationResult, type OcrInput, type OcrPage, type OcrRequest, type OcrResult, type OpenAICompatibleConfig, type PodcastInput, type PodcastResult, type PricingEntry, type ProviderAdapter, type RefreshOptions, type RefreshResult, type RerankInput, type RerankResult, type Role, SDK_TAG, type SqliteBudgetStoreConfig, type SqliteSinkConfig, StreamHttpError, type SubprocessResponse, type Tier, type TierSpec, type Tool, type ToolCall, type TrainStyleInput, type TrainStyleRequest, type TrainStyleResult, type TranscribeInput, type TranscribeRequest, type TranscribeResult, type TranslateInput, type TranslateResult, type Transport, type TransportRequest, type TransportResponse, type TtsInput, type TtsRequest, type UpmetricsCostClientConfig, UpmetricsCostError, type UpmetricsCostRow, type UpmetricsCostSummary, type UpmetricsCostTimeseries, type UpmetricsSinkConfig, type Usage, VERSION, type VideoInput, type VisionInput, aiConfigSchema, anthropicAdapter, anthropicApiAdapter, anthropicSubprocessAdapter, azureAdapter, bflAdapter, bflCredits, chatInputSchema, computeCost, createAI, deepinfraAdapter, deeplAdapter, deepseekAdapter, defaultProviders, discordSink, elevenlabsAdapter, embeddingInputSchema, falAdapter, falStubAdapter, freshUsage, fromProviderToolCall, geminiAdapter, getCostSummary, getPrice, httpTransport, imageInputSchema, listAzureDanishVoices, makeContracts, makeOpenAICompatibleAdapter, messageSchema, mistralAdapter, mistralStubAdapter, multiSink, noopSink, openaiAdapter, openaiStubAdapter, openrouterAdapter, parseClaudeCliJson, parseJsonLoose, refreshAvailability, requestyAdapter, resetRefreshState, resetRegistry, resolveAzureVoice, resolveTier, resolveVoice, sqliteBudgetStore, sqliteSink, streamTransport, stubProviders, subprocessTransport, tierSpecSchema, toProviderTools, toolSchema, translateInputSchema, upmetricsCostClient, upmetricsSink, usdFromMicro, vertexAdapter, visionInputSchema };
|
|
2466
|
+
export { AZURE_DANISH_VOICES, AZURE_DANISH_VOICE_LIST, type AiClient, type AiConfig, type AzureVoiceInfo, type BatchJob, type BatchRequestItem, type BatchResultItem, type BflAdapterConfig, type BflCredits, type BudgetConfig, BudgetExceededError, BudgetGuard, type BudgetStore, type CallOptions, type Capability, type ChatInput, type ChatRequest, type ChatResult, type ChatStreamEvent, type CheckVoiceOptions, type ClassifyInput, type ClassifyResult, type ContentPart, type Contracts, type CostQuery, type CostSink, type CostSummary, type CostSummaryQuery, type CostTimeseriesQuery, DEFAULT_TIER_MAP, type DesignInput, type DesignResult, type DialogueRequest, type DialogueTurn, type DiscordSinkConfig, ELEVENLABS_DANISH_VOICES, type EmbeddingInput, type EmbeddingRequest, type EmbeddingResult, type ExtractInput, type ExtractResult, type FalAdapterConfig, type HttpResponse, type ImageInput, type ImageRequest, type ImageResult, type LoraWeight, type Message, type MockupInput, type MockupResult, type ModerationInput, type ModerationItem, type ModerationRequest, type ModerationResult, type OcrInput, type OcrPage, type OcrRequest, type OcrResult, type OpenAICompatibleConfig, type PodcastInput, type PodcastResult, type PricingEntry, type ProviderAdapter, type RefreshOptions, type RefreshResult, type RerankInput, type RerankResult, type Role, SDK_TAG, type SqliteBudgetStoreConfig, type SqliteSinkConfig, StreamHttpError, type SubprocessResponse, type Tier, type TierSpec, type Tool, type ToolCall, type TrainStyleInput, type TrainStyleRequest, type TrainStyleResult, type TranscribeInput, type TranscribeRequest, type TranscribeResult, type TranslateInput, type TranslateResult, type Transport, type TransportRequest, type TransportResponse, type TtsInput, type TtsRequest, type UpmetricsCostClientConfig, UpmetricsCostError, type UpmetricsCostRow, type UpmetricsCostSummary, type UpmetricsCostTimeseries, type UpmetricsSinkConfig, type Usage, VERSION, type VideoInput, type VisionInput, type VoiceInfo, type VoiceProvider, type VoiceResolveResult, type VoiceStatus, VoiceUnavailableError, aiConfigSchema, anthropicAdapter, anthropicApiAdapter, anthropicSubprocessAdapter, azureAdapter, bflAdapter, bflCredits, chatInputSchema, checkVoice, computeCost, createAI, deepinfraAdapter, deeplAdapter, deepseekAdapter, defaultProviders, discordSink, elevenlabsAdapter, embeddingInputSchema, falAdapter, falStubAdapter, freshUsage, fromProviderToolCall, geminiAdapter, getCostSummary, getPrice, httpTransport, imageInputSchema, listAzureDanishVoices, listVoices, makeContracts, makeOpenAICompatibleAdapter, messageSchema, mistralAdapter, mistralStubAdapter, multiSink, noopSink, openaiAdapter, openaiStubAdapter, openrouterAdapter, parseClaudeCliJson, parseJsonLoose, refreshAvailability, requestyAdapter, resetRefreshState, resetRegistry, resolveAzureVoice, resolveTier, resolveVoice, sqliteBudgetStore, sqliteSink, streamTransport, stubProviders, subprocessTransport, tierSpecSchema, toProviderTools, toolSchema, translateInputSchema, upmetricsCostClient, upmetricsSink, usdFromMicro, vertexAdapter, visionInputSchema };
|
package/dist/index.js
CHANGED
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
} from "./chunk-IT7HNKLY.js";
|
|
9
9
|
import {
|
|
10
10
|
getPrice
|
|
11
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-LUPUAAHZ.js";
|
|
12
12
|
|
|
13
13
|
// src/routing/tier-map.ts
|
|
14
14
|
var DEFAULT_TIER_MAP = {
|
|
@@ -1390,13 +1390,13 @@ function elevenlabsAdapter(config = {}) {
|
|
|
1390
1390
|
const audio = new Uint8Array(await res.arrayBuffer());
|
|
1391
1391
|
return { audio, mimeType: "audio/mpeg", usage: priceFor(req.text.length, model) };
|
|
1392
1392
|
}
|
|
1393
|
-
async function
|
|
1393
|
+
async function listVoices2() {
|
|
1394
1394
|
const res = await fetchImpl(`${baseUrl}/voices`, { headers: { "xi-api-key": key() } });
|
|
1395
1395
|
if (!res.ok) throw new Error(`elevenlabs voices ${res.status}`);
|
|
1396
1396
|
const data = await res.json();
|
|
1397
1397
|
return (data.voices ?? []).map((v) => ({ voiceId: v.voice_id, name: v.name, language: v.labels?.language }));
|
|
1398
1398
|
}
|
|
1399
|
-
return { name: "elevenlabs", dialogue, tts, listVoices };
|
|
1399
|
+
return { name: "elevenlabs", dialogue, tts, listVoices: listVoices2 };
|
|
1400
1400
|
}
|
|
1401
1401
|
|
|
1402
1402
|
// src/providers/azure.ts
|
|
@@ -1633,8 +1633,8 @@ function vertexAdapter(config = {}) {
|
|
|
1633
1633
|
return config.region ?? process.env.GOOGLE_VERTEX_REGION ?? DEFAULT_REGION2;
|
|
1634
1634
|
}
|
|
1635
1635
|
function project() {
|
|
1636
|
-
const p = config.project ?? process.env.GOOGLE_VERTEX_PROJECT;
|
|
1637
|
-
if (!p) throw new Error("vertex adapter: project not set (config.project or
|
|
1636
|
+
const p = config.project ?? process.env.GOOGLE_VERTEX_PROJECT ?? resolveCredentials(config).project_id;
|
|
1637
|
+
if (!p) throw new Error("vertex adapter: project not set (config.project, env GOOGLE_VERTEX_PROJECT, or project_id in the credentials)");
|
|
1638
1638
|
return p;
|
|
1639
1639
|
}
|
|
1640
1640
|
async function accessToken() {
|
|
@@ -1710,7 +1710,47 @@ function vertexAdapter(config = {}) {
|
|
|
1710
1710
|
usage.costUsd = perSec * (req.durationSec ?? 8);
|
|
1711
1711
|
return { url: `vertex://${op.name}`, bytes: Buffer.from(videoB64, "base64"), mimeType: videoMime, usage };
|
|
1712
1712
|
}
|
|
1713
|
-
|
|
1713
|
+
async function vision(req) {
|
|
1714
|
+
const token = await accessToken();
|
|
1715
|
+
const proj = project();
|
|
1716
|
+
const reg = region();
|
|
1717
|
+
const systemParts = [];
|
|
1718
|
+
const contents = [];
|
|
1719
|
+
for (const m of req.messages) {
|
|
1720
|
+
if (m.role === "system") systemParts.push(...partsFrom(m.content));
|
|
1721
|
+
else contents.push({ role: m.role === "assistant" ? "model" : "user", parts: partsFrom(m.content) });
|
|
1722
|
+
}
|
|
1723
|
+
const body = { contents };
|
|
1724
|
+
if (systemParts.length > 0) body.systemInstruction = { parts: systemParts };
|
|
1725
|
+
const genConfig = {};
|
|
1726
|
+
if (req.maxTokens !== void 0) genConfig.maxOutputTokens = req.maxTokens;
|
|
1727
|
+
if (req.temperature !== void 0) genConfig.temperature = req.temperature;
|
|
1728
|
+
if (Object.keys(genConfig).length > 0) body.generationConfig = genConfig;
|
|
1729
|
+
const url = `https://${reg}-aiplatform.googleapis.com/v1/projects/${proj}/locations/${reg}/publishers/google/models/${req.spec.model}:generateContent`;
|
|
1730
|
+
const res = await fetchImpl(url, {
|
|
1731
|
+
method: "POST",
|
|
1732
|
+
headers: { authorization: `Bearer ${token}`, "content-type": "application/json" },
|
|
1733
|
+
body: JSON.stringify(body)
|
|
1734
|
+
});
|
|
1735
|
+
if (!res.ok) {
|
|
1736
|
+
throw new Error(`vertex vision ${res.status}: ${(await res.text().catch(() => "")).slice(0, 300)}`);
|
|
1737
|
+
}
|
|
1738
|
+
const data = await res.json();
|
|
1739
|
+
const text = (data.candidates?.[0]?.content?.parts ?? []).map((p) => p.text ?? "").join("").trim();
|
|
1740
|
+
const inputTokens = data.usageMetadata?.promptTokenCount ?? 0;
|
|
1741
|
+
const outputTokens = data.usageMetadata?.candidatesTokenCount ?? 0;
|
|
1742
|
+
const usage = freshUsage({
|
|
1743
|
+
provider: "vertex",
|
|
1744
|
+
model: req.spec.model,
|
|
1745
|
+
transport: "http",
|
|
1746
|
+
capability: "vision",
|
|
1747
|
+
inputTokens,
|
|
1748
|
+
outputTokens
|
|
1749
|
+
});
|
|
1750
|
+
usage.costUsd = computeCost("vertex", req.spec.model, inputTokens, outputTokens);
|
|
1751
|
+
return { text, usage };
|
|
1752
|
+
}
|
|
1753
|
+
return { name: "vertex", animate, vision };
|
|
1714
1754
|
}
|
|
1715
1755
|
|
|
1716
1756
|
// src/providers/deepl.ts
|
|
@@ -2409,6 +2449,143 @@ ${JSON.stringify(input.items)}`,
|
|
|
2409
2449
|
};
|
|
2410
2450
|
}
|
|
2411
2451
|
|
|
2452
|
+
// src/voices/registry.ts
|
|
2453
|
+
var CHECKED_AT = {
|
|
2454
|
+
// All 5 curated ids confirmed by SYNTHESIS — POST /v1/text-to-speech/{id} returned
|
|
2455
|
+
// 200 with distinct audio per voice (5 distinct sha256), and a fabricated id
|
|
2456
|
+
// returned 404, so a 200 means something.
|
|
2457
|
+
//
|
|
2458
|
+
// NB: GET /v1/voices/{id} is NOT a liveness test and must not be used as one. It
|
|
2459
|
+
// answers "is this voice saved in our account", and returns voice_not_found for a
|
|
2460
|
+
// public/shared voice that synthesizes perfectly well. Using it cost us a false
|
|
2461
|
+
// retirement (see the note on `mads` below) — the only honest liveness check for
|
|
2462
|
+
// ElevenLabs is a real synthesis call, which is exactly why v1 does no live check.
|
|
2463
|
+
elevenlabs: "2026-08-11",
|
|
2464
|
+
// F026 (6ca38c4) verified the 6 curated names against Azure's voices/list. Not
|
|
2465
|
+
// re-probed since: this machine has no Azure Speech key, and inventing a fresher
|
|
2466
|
+
// date than the last real check is exactly the lie checkedAt exists to prevent.
|
|
2467
|
+
azure: "2026-06-23"
|
|
2468
|
+
};
|
|
2469
|
+
var RETIRED_DEFAULT = {};
|
|
2470
|
+
var RETIRED = RETIRED_DEFAULT;
|
|
2471
|
+
var ELEVENLABS_GENDER = {
|
|
2472
|
+
soren: "male",
|
|
2473
|
+
jesper: "male",
|
|
2474
|
+
noam: "male",
|
|
2475
|
+
camilla: "female"
|
|
2476
|
+
};
|
|
2477
|
+
var NOTES = {
|
|
2478
|
+
mads: "usable, but not saved to the ElevenLabs account, so the voices endpoint publishes no metadata for it (verified by synthesis 2026-08-11)"
|
|
2479
|
+
};
|
|
2480
|
+
function build() {
|
|
2481
|
+
const rows = [];
|
|
2482
|
+
for (const [name, id] of Object.entries(ELEVENLABS_DANISH_VOICES)) {
|
|
2483
|
+
const retired = RETIRED[name];
|
|
2484
|
+
rows.push({
|
|
2485
|
+
id,
|
|
2486
|
+
name,
|
|
2487
|
+
provider: "elevenlabs",
|
|
2488
|
+
// The curated ElevenLabs roster IS the Danish one (see the constant's name
|
|
2489
|
+
// + F020); the multilingual model speaks it as da-DK.
|
|
2490
|
+
locale: "da-DK",
|
|
2491
|
+
gender: ELEVENLABS_GENDER[name],
|
|
2492
|
+
available: retired === void 0,
|
|
2493
|
+
status: retired === void 0 ? "available" : "retired",
|
|
2494
|
+
note: retired ?? NOTES[name],
|
|
2495
|
+
checkedAt: CHECKED_AT.elevenlabs
|
|
2496
|
+
});
|
|
2497
|
+
}
|
|
2498
|
+
for (const v of AZURE_DANISH_VOICE_LIST) {
|
|
2499
|
+
const retired = RETIRED[v.name];
|
|
2500
|
+
rows.push({
|
|
2501
|
+
id: v.voiceId,
|
|
2502
|
+
name: v.name,
|
|
2503
|
+
provider: "azure",
|
|
2504
|
+
locale: localeOf(v.voiceId),
|
|
2505
|
+
gender: v.gender,
|
|
2506
|
+
available: retired === void 0,
|
|
2507
|
+
status: retired === void 0 ? "available" : "retired",
|
|
2508
|
+
note: retired ?? NOTES[v.name],
|
|
2509
|
+
checkedAt: CHECKED_AT.azure
|
|
2510
|
+
});
|
|
2511
|
+
}
|
|
2512
|
+
return rows;
|
|
2513
|
+
}
|
|
2514
|
+
var ROWS = build();
|
|
2515
|
+
function allVoices(provider) {
|
|
2516
|
+
return provider ? ROWS.filter((v) => v.provider === provider) : [...ROWS];
|
|
2517
|
+
}
|
|
2518
|
+
function getVoice(nameOrId) {
|
|
2519
|
+
return ROWS.find((v) => v.name === nameOrId || v.id === nameOrId);
|
|
2520
|
+
}
|
|
2521
|
+
|
|
2522
|
+
// src/voices/types.ts
|
|
2523
|
+
var VoiceUnavailableError = class extends Error {
|
|
2524
|
+
code = "voice_unavailable";
|
|
2525
|
+
requested;
|
|
2526
|
+
provider;
|
|
2527
|
+
note;
|
|
2528
|
+
constructor(requested, note, provider) {
|
|
2529
|
+
super(`voice "${requested}" is unavailable${note ? ` (${note})` : ""}`);
|
|
2530
|
+
this.name = "VoiceUnavailableError";
|
|
2531
|
+
this.requested = requested;
|
|
2532
|
+
this.note = note;
|
|
2533
|
+
this.provider = provider;
|
|
2534
|
+
}
|
|
2535
|
+
};
|
|
2536
|
+
|
|
2537
|
+
// src/voices/resolve.ts
|
|
2538
|
+
function listVoices(opts = {}) {
|
|
2539
|
+
return allVoices(opts.provider);
|
|
2540
|
+
}
|
|
2541
|
+
function usable(nameOrId) {
|
|
2542
|
+
const v = getVoice(nameOrId);
|
|
2543
|
+
return v ? v.available : true;
|
|
2544
|
+
}
|
|
2545
|
+
function idFor(nameOrId) {
|
|
2546
|
+
return getVoice(nameOrId)?.id ?? nameOrId;
|
|
2547
|
+
}
|
|
2548
|
+
function checkVoice(requested, opts = {}) {
|
|
2549
|
+
const entry = getVoice(requested);
|
|
2550
|
+
const provider = entry?.provider;
|
|
2551
|
+
if (usable(requested)) {
|
|
2552
|
+
return {
|
|
2553
|
+
ok: true,
|
|
2554
|
+
voiceId: idFor(requested),
|
|
2555
|
+
requested,
|
|
2556
|
+
provider,
|
|
2557
|
+
fellBack: false,
|
|
2558
|
+
status: entry?.status ?? "unknown"
|
|
2559
|
+
};
|
|
2560
|
+
}
|
|
2561
|
+
const chain = opts.fallback === void 0 ? [] : Array.isArray(opts.fallback) ? opts.fallback : [opts.fallback];
|
|
2562
|
+
for (const fb of chain) {
|
|
2563
|
+
if (usable(fb)) {
|
|
2564
|
+
return {
|
|
2565
|
+
ok: false,
|
|
2566
|
+
voiceId: idFor(fb),
|
|
2567
|
+
requested,
|
|
2568
|
+
provider: getVoice(fb)?.provider ?? provider,
|
|
2569
|
+
fellBack: true,
|
|
2570
|
+
status: entry?.status ?? "retired",
|
|
2571
|
+
reason: entry?.note ?? `${requested} is unavailable`
|
|
2572
|
+
};
|
|
2573
|
+
}
|
|
2574
|
+
}
|
|
2575
|
+
if (opts.throwIfUnavailable) {
|
|
2576
|
+
throw new VoiceUnavailableError(requested, entry?.note, provider);
|
|
2577
|
+
}
|
|
2578
|
+
return {
|
|
2579
|
+
ok: false,
|
|
2580
|
+
voiceId: idFor(requested),
|
|
2581
|
+
requested,
|
|
2582
|
+
provider,
|
|
2583
|
+
fellBack: false,
|
|
2584
|
+
status: entry?.status ?? "retired",
|
|
2585
|
+
reason: entry?.note ?? `${requested} is unavailable`
|
|
2586
|
+
};
|
|
2587
|
+
}
|
|
2588
|
+
|
|
2412
2589
|
// src/schema/inputs.ts
|
|
2413
2590
|
import { z } from "zod";
|
|
2414
2591
|
var transportSchema = z.enum(["http", "subprocess"]);
|
|
@@ -2583,6 +2760,9 @@ var podcastInputSchema = z.object({
|
|
|
2583
2760
|
var ttsInputSchema = z.object({
|
|
2584
2761
|
text: z.string(),
|
|
2585
2762
|
voice: z.string(),
|
|
2763
|
+
/** F037: voice to use if `voice` is one we know the provider has retired. Without
|
|
2764
|
+
* it a retired voice throws VoiceUnavailableError rather than reaching the API. */
|
|
2765
|
+
voiceFallback: z.string().optional(),
|
|
2586
2766
|
lang: z.string().optional(),
|
|
2587
2767
|
format: z.string().optional(),
|
|
2588
2768
|
rate: z.number().positive().optional(),
|
|
@@ -2610,8 +2790,8 @@ var aiConfigSchema = z.object({
|
|
|
2610
2790
|
});
|
|
2611
2791
|
|
|
2612
2792
|
// src/version.ts
|
|
2613
|
-
var VERSION = "0.
|
|
2614
|
-
var SDK_TAG = "@broberg/ai-sdk@0.
|
|
2793
|
+
var VERSION = "0.28.0";
|
|
2794
|
+
var SDK_TAG = "@broberg/ai-sdk@0.28.0";
|
|
2615
2795
|
|
|
2616
2796
|
// src/cost/sinks/upmetrics.ts
|
|
2617
2797
|
function upmetricsSink(config) {
|
|
@@ -3093,7 +3273,9 @@ function createAI(config = {}) {
|
|
|
3093
3273
|
const inputs = input.script.map((turn) => {
|
|
3094
3274
|
const mapped = input.voices[turn.speaker];
|
|
3095
3275
|
if (!mapped) throw new Error(`ai.podcast: no voice mapped for speaker "${turn.speaker}"`);
|
|
3096
|
-
|
|
3276
|
+
const res = checkVoice(mapped);
|
|
3277
|
+
if (!res.ok) throw new VoiceUnavailableError(mapped, `${res.reason} \u2014 speaker "${turn.speaker}"`, res.provider);
|
|
3278
|
+
return { text: turn.text, voiceId: res.voiceId };
|
|
3097
3279
|
});
|
|
3098
3280
|
const chars = input.script.reduce((n, t) => n + t.text.length, 0);
|
|
3099
3281
|
return runCapability({
|
|
@@ -3114,6 +3296,7 @@ function createAI(config = {}) {
|
|
|
3114
3296
|
},
|
|
3115
3297
|
async tts(input) {
|
|
3116
3298
|
input = ttsInputSchema.parse(input);
|
|
3299
|
+
const { voiceId } = checkVoice(input.voice, { fallback: input.voiceFallback, throwIfUnavailable: true });
|
|
3117
3300
|
return runCapability({
|
|
3118
3301
|
primary: { ...DEFAULT_TTS_SPEC, ...input.override },
|
|
3119
3302
|
fallback: input.fallback,
|
|
@@ -3126,7 +3309,7 @@ function createAI(config = {}) {
|
|
|
3126
3309
|
invoke: async (spec) => {
|
|
3127
3310
|
const adapter = pickProvider(spec.provider);
|
|
3128
3311
|
if (!adapter.tts) throw new Error(`createAI: provider "${spec.provider}" does not support tts`);
|
|
3129
|
-
return adapter.tts({ text: input.text, voiceId
|
|
3312
|
+
return adapter.tts({ text: input.text, voiceId, lang: input.lang, format: input.format, rate: input.rate, spec });
|
|
3130
3313
|
}
|
|
3131
3314
|
});
|
|
3132
3315
|
},
|
|
@@ -3574,6 +3757,7 @@ export {
|
|
|
3574
3757
|
StreamHttpError,
|
|
3575
3758
|
UpmetricsCostError,
|
|
3576
3759
|
VERSION,
|
|
3760
|
+
VoiceUnavailableError,
|
|
3577
3761
|
aiConfigSchema,
|
|
3578
3762
|
anthropicAdapter,
|
|
3579
3763
|
anthropicApiAdapter,
|
|
@@ -3582,6 +3766,7 @@ export {
|
|
|
3582
3766
|
bflAdapter,
|
|
3583
3767
|
bflCredits,
|
|
3584
3768
|
chatInputSchema,
|
|
3769
|
+
checkVoice,
|
|
3585
3770
|
computeCost,
|
|
3586
3771
|
createAI,
|
|
3587
3772
|
deepinfraAdapter,
|
|
@@ -3602,6 +3787,7 @@ export {
|
|
|
3602
3787
|
imageInputSchema,
|
|
3603
3788
|
listAzureDanishVoices,
|
|
3604
3789
|
listModels,
|
|
3790
|
+
listVoices,
|
|
3605
3791
|
makeContracts,
|
|
3606
3792
|
makeOpenAICompatibleAdapter,
|
|
3607
3793
|
messageSchema,
|