@broberg/ai-sdk 0.24.0 → 0.25.1

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,11 @@ 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
+ * Supported by Whisper (openai) + Azure fast-transcription (F036.1); other
1358
+ * adapters (Voxtral/mistral) ignore it and return text only. */
1359
+ timestamps: z.ZodOptional<z.ZodUnion<[z.ZodEnum<["word", "segment"]>, z.ZodArray<z.ZodEnum<["word", "segment"]>, "many">]>>;
1334
1360
  }, "strip", z.ZodTypeAny, {
1335
1361
  audio: string | Uint8Array<ArrayBuffer>;
1336
1362
  language?: string | undefined;
@@ -1349,6 +1375,7 @@ declare const transcribeInputSchema: z.ZodObject<{
1349
1375
  labels?: Record<string, string> | undefined;
1350
1376
  durationSec?: number | undefined;
1351
1377
  phrases?: string[] | undefined;
1378
+ timestamps?: "word" | "segment" | ("word" | "segment")[] | undefined;
1352
1379
  }, {
1353
1380
  audio: string | Uint8Array<ArrayBuffer>;
1354
1381
  language?: string | undefined;
@@ -1367,6 +1394,7 @@ declare const transcribeInputSchema: z.ZodObject<{
1367
1394
  labels?: Record<string, string> | undefined;
1368
1395
  durationSec?: number | undefined;
1369
1396
  phrases?: string[] | undefined;
1397
+ timestamps?: "word" | "segment" | ("word" | "segment")[] | undefined;
1370
1398
  }>;
1371
1399
  declare const ocrInputSchema: z.ZodObject<{
1372
1400
  tier: z.ZodOptional<z.ZodEnum<["fast", "smart", "powerful", "cheap", "vision", "video", "embedding"]>>;
@@ -2017,8 +2045,8 @@ declare const falStubAdapter: ProviderAdapter;
2017
2045
  * wires the live adapters. */
2018
2046
  declare const stubProviders: Record<string, ProviderAdapter>;
2019
2047
 
2020
- declare const VERSION: "0.24.0";
2021
- declare const SDK_TAG: "@broberg/ai-sdk@0.24.0";
2048
+ declare const VERSION: "0.25.1";
2049
+ declare const SDK_TAG: "@broberg/ai-sdk@0.25.1";
2022
2050
 
2023
2051
  /** Built-in defaults. Every entry is overridable via AiConfig.defaults or a
2024
2052
  * 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
  }
@@ -1523,7 +1534,26 @@ function azureAdapter(config = {}) {
1523
1534
  outputTokens: 0
1524
1535
  });
1525
1536
  usage.costUsd = minutes * (config.sttPricePerMin ?? AZURE_STT_PRICE_PER_MIN);
1526
- return { text, usage };
1537
+ const result = { text, usage };
1538
+ if (req.timestamps && req.timestamps.length > 0 && data.phrases) {
1539
+ const toSec = (ms) => (ms ?? 0) / 1e3;
1540
+ if (req.timestamps.includes("word")) {
1541
+ const words = data.phrases.flatMap((p) => p.words ?? []);
1542
+ if (words.length > 0) {
1543
+ result.words = words.map((w) => ({
1544
+ word: w.text ?? "",
1545
+ start: toSec(w.offsetMilliseconds),
1546
+ end: toSec((w.offsetMilliseconds ?? 0) + (w.durationMilliseconds ?? 0))
1547
+ }));
1548
+ }
1549
+ }
1550
+ result.segments = data.phrases.map((p) => ({
1551
+ text: p.text ?? "",
1552
+ start: toSec(p.offsetMilliseconds),
1553
+ end: toSec((p.offsetMilliseconds ?? 0) + (p.durationMilliseconds ?? 0))
1554
+ }));
1555
+ }
1556
+ return result;
1527
1557
  }
1528
1558
  return { name: "azure", tts, transcribe };
1529
1559
  }
@@ -2526,6 +2556,11 @@ var transcribeInputSchema = z.object({
2526
2556
  durationSec: z.number().positive().optional(),
2527
2557
  /** Bias toward brand/jargon terms (Azure phraseList, F029.3); others ignore it. */
2528
2558
  phrases: z.array(z.string()).optional(),
2559
+ /** Opt-in timestamps (F036) — a single granularity or an array. Omit → the
2560
+ * current { text, usage } shape, unchanged. Pass ["word","segment"] for both.
2561
+ * Supported by Whisper (openai) + Azure fast-transcription (F036.1); other
2562
+ * adapters (Voxtral/mistral) ignore it and return text only. */
2563
+ timestamps: z.union([z.enum(["word", "segment"]), z.array(z.enum(["word", "segment"]))]).optional(),
2529
2564
  ...callOptions
2530
2565
  });
2531
2566
  var ocrInputSchema = z.object({
@@ -2572,8 +2607,8 @@ var aiConfigSchema = z.object({
2572
2607
  });
2573
2608
 
2574
2609
  // src/version.ts
2575
- var VERSION = "0.24.0";
2576
- var SDK_TAG = "@broberg/ai-sdk@0.24.0";
2610
+ var VERSION = "0.25.1";
2611
+ var SDK_TAG = "@broberg/ai-sdk@0.25.1";
2577
2612
 
2578
2613
  // src/cost/sinks/upmetrics.ts
2579
2614
  function upmetricsSink(config) {
@@ -3126,7 +3161,8 @@ function createAI(config = {}) {
3126
3161
  invoke: async (spec) => {
3127
3162
  const adapter = pickProvider(spec.provider);
3128
3163
  if (!adapter.transcribe) throw new Error(`createAI: provider "${spec.provider}" does not support transcribe`);
3129
- return adapter.transcribe({ audio, language: input.language, durationSec: input.durationSec, phrases: input.phrases, spec });
3164
+ const timestamps = input.timestamps === void 0 ? void 0 : Array.isArray(input.timestamps) ? input.timestamps : [input.timestamps];
3165
+ return adapter.transcribe({ audio, language: input.language, durationSec: input.durationSec, phrases: input.phrases, timestamps, spec });
3130
3166
  }
3131
3167
  });
3132
3168
  },