@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 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.23.0";
2021
- declare const SDK_TAG: "@broberg/ai-sdk@0.23.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
- return { text: data.text ?? "", usage };
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 (!cfg.costSink) return;
2739
+ if (!costSink) return;
2643
2740
  try {
2644
- await cfg.costSink.record(usage);
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
- return adapter.transcribe({ audio, language: input.language, durationSec: input.durationSec, phrases: input.phrases, spec });
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;