@broberg/ai-sdk 0.23.0 → 0.25.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 +9 -1
- package/dist/index.d.ts +28 -2
- package/dist/index.js +102 -76
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -30,7 +30,7 @@ const emb = await ai.embedding({ text: ["a", "b"] });
|
|
|
30
30
|
|
|
31
31
|
## Capabilities
|
|
32
32
|
|
|
33
|
-
`chat` · `vision` · `translate` · `image` (fal.ai) · `embedding` · `transcribe`
|
|
33
|
+
`chat` · `vision` · `translate` · `image` (fal.ai default / OpenRouter) · `embedding` · `transcribe`
|
|
34
34
|
(Whisper), plus **prompt contracts** with structured output:
|
|
35
35
|
|
|
36
36
|
```ts
|
|
@@ -56,6 +56,14 @@ await ai.chat({ prompt: "…", tier: "powerful" });
|
|
|
56
56
|
await ai.chat({ prompt: "…", override: { provider: "openrouter", model: "minimax/minimax-m2.7" } });
|
|
57
57
|
```
|
|
58
58
|
|
|
59
|
+
> **Images — raster vs. vector.** `ai.image()` defaults to fal.ai (raster PNG). For
|
|
60
|
+
> **vector/SVG** output (logos), override to OpenRouter Recraft — the slug is an
|
|
61
|
+
> **OpenRouter** model, not a fal app-id:
|
|
62
|
+
> ```ts
|
|
63
|
+
> await ai.image({ prompt: "…", override: { provider: "openrouter", model: "recraft/recraft-v4.1-vector" } });
|
|
64
|
+
> // → data:image/svg+xml;base64,… with ground-truth cost
|
|
65
|
+
> ```
|
|
66
|
+
|
|
59
67
|
`cheap` defaults to the cheapest-that's-good-enough cloud model — **Mistral Small**
|
|
60
68
|
(EU/Paris-hosted, GDPR-safe, ~$0.10/$0.30) — so a cost-tier call is safe for
|
|
61
69
|
personal data by default; override per call for an even cheaper non-personal route.
|
package/dist/index.d.ts
CHANGED
|
@@ -239,6 +239,20 @@ interface EmbeddingResult {
|
|
|
239
239
|
vectors: number[][];
|
|
240
240
|
usage: Usage;
|
|
241
241
|
}
|
|
242
|
+
/** Timestamp granularity a caller can request from `ai.transcribe` (F036). */
|
|
243
|
+
type TimestampGranularity = "word" | "segment";
|
|
244
|
+
/** A single word with its start/end offset in seconds. */
|
|
245
|
+
interface WordTimestamp {
|
|
246
|
+
word: string;
|
|
247
|
+
start: number;
|
|
248
|
+
end: number;
|
|
249
|
+
}
|
|
250
|
+
/** A phrase/sentence segment with its start/end offset in seconds. */
|
|
251
|
+
interface SegmentTimestamp {
|
|
252
|
+
text: string;
|
|
253
|
+
start: number;
|
|
254
|
+
end: number;
|
|
255
|
+
}
|
|
242
256
|
interface TranscribeRequest {
|
|
243
257
|
/** Raw audio bytes (the client resolves a URL to bytes before calling). */
|
|
244
258
|
audio: Uint8Array;
|
|
@@ -248,10 +262,17 @@ interface TranscribeRequest {
|
|
|
248
262
|
/** Bias recognition toward these brand/jargon terms (Azure phraseList, F029.3).
|
|
249
263
|
* Providers without biasing support (Voxtral/Whisper) ignore it. */
|
|
250
264
|
phrases?: string[];
|
|
265
|
+
/** Request timestamps (F036). The client normalizes the input to this array.
|
|
266
|
+
* Adapters without timestamp support ignore it (result omits words/segments). */
|
|
267
|
+
timestamps?: TimestampGranularity[];
|
|
251
268
|
spec: TierSpec;
|
|
252
269
|
}
|
|
253
270
|
interface TranscribeResult {
|
|
254
271
|
text: string;
|
|
272
|
+
/** Word-level timing — present only when "word" timestamps were requested. */
|
|
273
|
+
words?: WordTimestamp[];
|
|
274
|
+
/** Segment (phrase/sentence) timing — present when any timestamps were requested. */
|
|
275
|
+
segments?: SegmentTimestamp[];
|
|
255
276
|
usage: Usage;
|
|
256
277
|
}
|
|
257
278
|
interface OcrRequest {
|
|
@@ -1331,6 +1352,9 @@ declare const transcribeInputSchema: z.ZodObject<{
|
|
|
1331
1352
|
durationSec: z.ZodOptional<z.ZodNumber>;
|
|
1332
1353
|
/** Bias toward brand/jargon terms (Azure phraseList, F029.3); others ignore it. */
|
|
1333
1354
|
phrases: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
|
|
1355
|
+
/** Opt-in timestamps (F036) — a single granularity or an array. Omit → the
|
|
1356
|
+
* current { text, usage } shape, unchanged. Pass ["word","segment"] for both. */
|
|
1357
|
+
timestamps: z.ZodOptional<z.ZodUnion<[z.ZodEnum<["word", "segment"]>, z.ZodArray<z.ZodEnum<["word", "segment"]>, "many">]>>;
|
|
1334
1358
|
}, "strip", z.ZodTypeAny, {
|
|
1335
1359
|
audio: string | Uint8Array<ArrayBuffer>;
|
|
1336
1360
|
language?: string | undefined;
|
|
@@ -1349,6 +1373,7 @@ declare const transcribeInputSchema: z.ZodObject<{
|
|
|
1349
1373
|
labels?: Record<string, string> | undefined;
|
|
1350
1374
|
durationSec?: number | undefined;
|
|
1351
1375
|
phrases?: string[] | undefined;
|
|
1376
|
+
timestamps?: "word" | "segment" | ("word" | "segment")[] | undefined;
|
|
1352
1377
|
}, {
|
|
1353
1378
|
audio: string | Uint8Array<ArrayBuffer>;
|
|
1354
1379
|
language?: string | undefined;
|
|
@@ -1367,6 +1392,7 @@ declare const transcribeInputSchema: z.ZodObject<{
|
|
|
1367
1392
|
labels?: Record<string, string> | undefined;
|
|
1368
1393
|
durationSec?: number | undefined;
|
|
1369
1394
|
phrases?: string[] | undefined;
|
|
1395
|
+
timestamps?: "word" | "segment" | ("word" | "segment")[] | undefined;
|
|
1370
1396
|
}>;
|
|
1371
1397
|
declare const ocrInputSchema: z.ZodObject<{
|
|
1372
1398
|
tier: z.ZodOptional<z.ZodEnum<["fast", "smart", "powerful", "cheap", "vision", "video", "embedding"]>>;
|
|
@@ -2017,8 +2043,8 @@ declare const falStubAdapter: ProviderAdapter;
|
|
|
2017
2043
|
* wires the live adapters. */
|
|
2018
2044
|
declare const stubProviders: Record<string, ProviderAdapter>;
|
|
2019
2045
|
|
|
2020
|
-
declare const VERSION: "0.
|
|
2021
|
-
declare const SDK_TAG: "@broberg/ai-sdk@0.
|
|
2046
|
+
declare const VERSION: "0.25.0";
|
|
2047
|
+
declare const SDK_TAG: "@broberg/ai-sdk@0.25.0";
|
|
2022
2048
|
|
|
2023
2049
|
/** Built-in defaults. Every entry is overridable via AiConfig.defaults or a
|
|
2024
2050
|
* per-call override.
|
package/dist/index.js
CHANGED
|
@@ -730,6 +730,10 @@ function openaiAdapter(config = {}) {
|
|
|
730
730
|
form.append("file", new Blob([req.audio]), "audio");
|
|
731
731
|
form.append("model", req.spec.model);
|
|
732
732
|
if (req.language) form.append("language", req.language);
|
|
733
|
+
if (req.timestamps && req.timestamps.length > 0) {
|
|
734
|
+
form.append("response_format", "verbose_json");
|
|
735
|
+
for (const g of req.timestamps) form.append("timestamp_granularities[]", g);
|
|
736
|
+
}
|
|
733
737
|
const fetchImpl = config.fetch ?? fetch;
|
|
734
738
|
const res = await fetchImpl(`${baseUrl}/audio/transcriptions`, {
|
|
735
739
|
method: "POST",
|
|
@@ -753,7 +757,14 @@ function openaiAdapter(config = {}) {
|
|
|
753
757
|
const perMinute = WHISPER_PRICE_PER_MIN[req.spec.model] ?? 0;
|
|
754
758
|
usage.costUsd = req.durationSec / 60 * perMinute;
|
|
755
759
|
}
|
|
756
|
-
|
|
760
|
+
const result = { text: data.text ?? "", usage };
|
|
761
|
+
if (req.timestamps?.includes("word") && data.words) {
|
|
762
|
+
result.words = data.words.map((w) => ({ word: w.word, start: w.start, end: w.end }));
|
|
763
|
+
}
|
|
764
|
+
if (req.timestamps && req.timestamps.length > 0 && data.segments) {
|
|
765
|
+
result.segments = data.segments.map((s) => ({ text: s.text, start: s.start, end: s.end }));
|
|
766
|
+
}
|
|
767
|
+
return result;
|
|
757
768
|
}
|
|
758
769
|
return { ...base, embedding, transcribe };
|
|
759
770
|
}
|
|
@@ -2526,6 +2537,9 @@ var transcribeInputSchema = z.object({
|
|
|
2526
2537
|
durationSec: z.number().positive().optional(),
|
|
2527
2538
|
/** Bias toward brand/jargon terms (Azure phraseList, F029.3); others ignore it. */
|
|
2528
2539
|
phrases: z.array(z.string()).optional(),
|
|
2540
|
+
/** Opt-in timestamps (F036) — a single granularity or an array. Omit → the
|
|
2541
|
+
* current { text, usage } shape, unchanged. Pass ["word","segment"] for both. */
|
|
2542
|
+
timestamps: z.union([z.enum(["word", "segment"]), z.array(z.enum(["word", "segment"]))]).optional(),
|
|
2529
2543
|
...callOptions
|
|
2530
2544
|
});
|
|
2531
2545
|
var ocrInputSchema = z.object({
|
|
@@ -2571,6 +2585,78 @@ var aiConfigSchema = z.object({
|
|
|
2571
2585
|
availability: availabilitySchema.optional()
|
|
2572
2586
|
});
|
|
2573
2587
|
|
|
2588
|
+
// src/version.ts
|
|
2589
|
+
var VERSION = "0.25.0";
|
|
2590
|
+
var SDK_TAG = "@broberg/ai-sdk@0.25.0";
|
|
2591
|
+
|
|
2592
|
+
// src/cost/sinks/upmetrics.ts
|
|
2593
|
+
function upmetricsSink(config) {
|
|
2594
|
+
const doFetch = config.fetch ?? fetch;
|
|
2595
|
+
const url = `${config.baseUrl.replace(/\/$/, "")}/api/agent`;
|
|
2596
|
+
return {
|
|
2597
|
+
async record(usage) {
|
|
2598
|
+
try {
|
|
2599
|
+
const startedAt = usage.ts || (/* @__PURE__ */ new Date()).toISOString();
|
|
2600
|
+
const endedAt = new Date(
|
|
2601
|
+
new Date(startedAt).getTime() + (usage.latencyMs || 0)
|
|
2602
|
+
).toISOString();
|
|
2603
|
+
const agentKind = config.agentKind ?? (usage.capability === "embedding" ? "embedding" : "chatbot");
|
|
2604
|
+
const body = {
|
|
2605
|
+
mode: "record",
|
|
2606
|
+
agent_kind: agentKind,
|
|
2607
|
+
agent_name: config.agentName,
|
|
2608
|
+
provider: usage.provider,
|
|
2609
|
+
model: usage.model,
|
|
2610
|
+
status: "success",
|
|
2611
|
+
input_tokens: usage.inputTokens,
|
|
2612
|
+
output_tokens: usage.outputTokens,
|
|
2613
|
+
cache_read_tokens: usage.cacheReadTokens,
|
|
2614
|
+
cache_creation_tokens: usage.cacheCreationTokens,
|
|
2615
|
+
cost_usd: usage.costUsd,
|
|
2616
|
+
duration_ms: usage.latencyMs,
|
|
2617
|
+
started_at: startedAt,
|
|
2618
|
+
ended_at: endedAt,
|
|
2619
|
+
tags: {
|
|
2620
|
+
// Consumer attribution labels (e.g. tenantId) ride in tags so no new
|
|
2621
|
+
// top-level field risks the strict-shape ingest schema (F011). The
|
|
2622
|
+
// SDK-owned keys win — a label can never clobber capability/transport/sdk.
|
|
2623
|
+
...usage.labels,
|
|
2624
|
+
capability: usage.capability,
|
|
2625
|
+
transport: usage.transport,
|
|
2626
|
+
sdk: SDK_TAG
|
|
2627
|
+
}
|
|
2628
|
+
};
|
|
2629
|
+
if (usage.tier !== void 0) body.tier = usage.tier;
|
|
2630
|
+
if (usage.purpose !== void 0) body.purpose = usage.purpose;
|
|
2631
|
+
if (usage.toolCalls) {
|
|
2632
|
+
body.tool_calls = usage.toolCalls.map((t) => ({
|
|
2633
|
+
name: t.name,
|
|
2634
|
+
count: t.count,
|
|
2635
|
+
error_count: t.errorCount ?? 0
|
|
2636
|
+
}));
|
|
2637
|
+
}
|
|
2638
|
+
void config.complianceMode;
|
|
2639
|
+
const res = await doFetch(url, {
|
|
2640
|
+
method: "POST",
|
|
2641
|
+
headers: {
|
|
2642
|
+
"content-type": "application/json",
|
|
2643
|
+
"X-Upmetrics-Key": config.apiKey
|
|
2644
|
+
},
|
|
2645
|
+
body: JSON.stringify(body)
|
|
2646
|
+
});
|
|
2647
|
+
if (!res.ok) {
|
|
2648
|
+
const text = await res.text().catch(() => "");
|
|
2649
|
+
config.onError?.(
|
|
2650
|
+
new Error(`upmetricsSink: ingest returned ${res.status}: ${text.slice(0, 200)}`)
|
|
2651
|
+
);
|
|
2652
|
+
}
|
|
2653
|
+
} catch (err) {
|
|
2654
|
+
config.onError?.(err);
|
|
2655
|
+
}
|
|
2656
|
+
}
|
|
2657
|
+
};
|
|
2658
|
+
}
|
|
2659
|
+
|
|
2574
2660
|
// src/client.ts
|
|
2575
2661
|
var DEFAULT_IMAGE_SPEC = {
|
|
2576
2662
|
provider: "fal",
|
|
@@ -2608,9 +2694,20 @@ var DEFAULT_MODERATION_SPEC = { provider: "mistral", model: "mistral-moderation-
|
|
|
2608
2694
|
var DEFAULT_PODCAST_SPEC = { provider: "elevenlabs", model: "eleven_v3", transport: "http" };
|
|
2609
2695
|
var DEFAULT_TTS_SPEC = { provider: "elevenlabs", model: "eleven_multilingual_v2", transport: "http" };
|
|
2610
2696
|
var DEFAULT_BATCH_SPEC = { provider: "mistral", model: "mistral-small-latest", transport: "http" };
|
|
2697
|
+
function defaultCostSink() {
|
|
2698
|
+
const apiKey = process.env.UPMETRICS_API_KEY;
|
|
2699
|
+
if (!apiKey) return void 0;
|
|
2700
|
+
return upmetricsSink({
|
|
2701
|
+
baseUrl: process.env.UPMETRICS_BASE_URL ?? "https://upmetrics.org",
|
|
2702
|
+
apiKey,
|
|
2703
|
+
agentName: process.env.UPMETRICS_AGENT_NAME ?? process.env.npm_package_name ?? "unknown",
|
|
2704
|
+
complianceMode: process.env.UPMETRICS_COMPLIANCE === "1"
|
|
2705
|
+
});
|
|
2706
|
+
}
|
|
2611
2707
|
function createAI(config = {}) {
|
|
2612
2708
|
const cfg = aiConfigSchema.parse(config);
|
|
2613
2709
|
const providers = cfg.providers ?? defaultProviders;
|
|
2710
|
+
const costSink = cfg.costSink ?? defaultCostSink();
|
|
2614
2711
|
const budget = cfg.budget ? new BudgetGuard(cfg.budget) : void 0;
|
|
2615
2712
|
const estTokens = (s) => Math.ceil(s.length / 4);
|
|
2616
2713
|
async function preflight(spec, estInTokens, estOutTokens) {
|
|
@@ -2639,9 +2736,9 @@ function createAI(config = {}) {
|
|
|
2639
2736
|
return usage;
|
|
2640
2737
|
}
|
|
2641
2738
|
async function report(usage) {
|
|
2642
|
-
if (!
|
|
2739
|
+
if (!costSink) return;
|
|
2643
2740
|
try {
|
|
2644
|
-
await
|
|
2741
|
+
await costSink.record(usage);
|
|
2645
2742
|
} catch {
|
|
2646
2743
|
}
|
|
2647
2744
|
}
|
|
@@ -3043,7 +3140,8 @@ function createAI(config = {}) {
|
|
|
3043
3140
|
invoke: async (spec) => {
|
|
3044
3141
|
const adapter = pickProvider(spec.provider);
|
|
3045
3142
|
if (!adapter.transcribe) throw new Error(`createAI: provider "${spec.provider}" does not support transcribe`);
|
|
3046
|
-
|
|
3143
|
+
const timestamps = input.timestamps === void 0 ? void 0 : Array.isArray(input.timestamps) ? input.timestamps : [input.timestamps];
|
|
3144
|
+
return adapter.transcribe({ audio, language: input.language, durationSec: input.durationSec, phrases: input.phrases, timestamps, spec });
|
|
3047
3145
|
}
|
|
3048
3146
|
});
|
|
3049
3147
|
},
|
|
@@ -3170,10 +3268,6 @@ var stubProviders = {
|
|
|
3170
3268
|
fal: falStubAdapter
|
|
3171
3269
|
};
|
|
3172
3270
|
|
|
3173
|
-
// src/version.ts
|
|
3174
|
-
var VERSION = "0.23.0";
|
|
3175
|
-
var SDK_TAG = "@broberg/ai-sdk@0.23.0";
|
|
3176
|
-
|
|
3177
3271
|
// src/availability/refresh.ts
|
|
3178
3272
|
var NOT_REFRESHED = { refreshed: false, checked: 0, markedUnavailable: [] };
|
|
3179
3273
|
var DEFAULT_TTL_MS = 60 * 60 * 1e3;
|
|
@@ -3263,74 +3357,6 @@ function multiSink(sinks) {
|
|
|
3263
3357
|
};
|
|
3264
3358
|
}
|
|
3265
3359
|
|
|
3266
|
-
// src/cost/sinks/upmetrics.ts
|
|
3267
|
-
function upmetricsSink(config) {
|
|
3268
|
-
const doFetch = config.fetch ?? fetch;
|
|
3269
|
-
const url = `${config.baseUrl.replace(/\/$/, "")}/api/agent`;
|
|
3270
|
-
return {
|
|
3271
|
-
async record(usage) {
|
|
3272
|
-
try {
|
|
3273
|
-
const startedAt = usage.ts || (/* @__PURE__ */ new Date()).toISOString();
|
|
3274
|
-
const endedAt = new Date(
|
|
3275
|
-
new Date(startedAt).getTime() + (usage.latencyMs || 0)
|
|
3276
|
-
).toISOString();
|
|
3277
|
-
const agentKind = config.agentKind ?? (usage.capability === "embedding" ? "embedding" : "chatbot");
|
|
3278
|
-
const body = {
|
|
3279
|
-
mode: "record",
|
|
3280
|
-
agent_kind: agentKind,
|
|
3281
|
-
agent_name: config.agentName,
|
|
3282
|
-
provider: usage.provider,
|
|
3283
|
-
model: usage.model,
|
|
3284
|
-
status: "success",
|
|
3285
|
-
input_tokens: usage.inputTokens,
|
|
3286
|
-
output_tokens: usage.outputTokens,
|
|
3287
|
-
cache_read_tokens: usage.cacheReadTokens,
|
|
3288
|
-
cache_creation_tokens: usage.cacheCreationTokens,
|
|
3289
|
-
cost_usd: usage.costUsd,
|
|
3290
|
-
duration_ms: usage.latencyMs,
|
|
3291
|
-
started_at: startedAt,
|
|
3292
|
-
ended_at: endedAt,
|
|
3293
|
-
tags: {
|
|
3294
|
-
// Consumer attribution labels (e.g. tenantId) ride in tags so no new
|
|
3295
|
-
// top-level field risks the strict-shape ingest schema (F011). The
|
|
3296
|
-
// SDK-owned keys win — a label can never clobber capability/transport/sdk.
|
|
3297
|
-
...usage.labels,
|
|
3298
|
-
capability: usage.capability,
|
|
3299
|
-
transport: usage.transport,
|
|
3300
|
-
sdk: SDK_TAG
|
|
3301
|
-
}
|
|
3302
|
-
};
|
|
3303
|
-
if (usage.tier !== void 0) body.tier = usage.tier;
|
|
3304
|
-
if (usage.purpose !== void 0) body.purpose = usage.purpose;
|
|
3305
|
-
if (usage.toolCalls) {
|
|
3306
|
-
body.tool_calls = usage.toolCalls.map((t) => ({
|
|
3307
|
-
name: t.name,
|
|
3308
|
-
count: t.count,
|
|
3309
|
-
error_count: t.errorCount ?? 0
|
|
3310
|
-
}));
|
|
3311
|
-
}
|
|
3312
|
-
void config.complianceMode;
|
|
3313
|
-
const res = await doFetch(url, {
|
|
3314
|
-
method: "POST",
|
|
3315
|
-
headers: {
|
|
3316
|
-
"content-type": "application/json",
|
|
3317
|
-
"X-Upmetrics-Key": config.apiKey
|
|
3318
|
-
},
|
|
3319
|
-
body: JSON.stringify(body)
|
|
3320
|
-
});
|
|
3321
|
-
if (!res.ok) {
|
|
3322
|
-
const text = await res.text().catch(() => "");
|
|
3323
|
-
config.onError?.(
|
|
3324
|
-
new Error(`upmetricsSink: ingest returned ${res.status}: ${text.slice(0, 200)}`)
|
|
3325
|
-
);
|
|
3326
|
-
}
|
|
3327
|
-
} catch (err) {
|
|
3328
|
-
config.onError?.(err);
|
|
3329
|
-
}
|
|
3330
|
-
}
|
|
3331
|
-
};
|
|
3332
|
-
}
|
|
3333
|
-
|
|
3334
3360
|
// src/cost/sinks/discord.ts
|
|
3335
3361
|
function discordSink(config) {
|
|
3336
3362
|
const doFetch = config.fetch ?? fetch;
|