@broberg/ai-sdk 0.42.0 → 0.43.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
@@ -6,12 +6,51 @@ cost control on every call**.
6
6
  A provider-agnostic facade: your code calls `ai.chat()`, `ai.vision()`,
7
7
  `ai.image()` — never a provider SDK directly. Swap providers by changing a tier,
8
8
  not your call-sites. Every call returns a `Usage` (tokens, cost, latency,
9
- transport) and can fan that out to any cost sink.
9
+ transport, **data-residency region**) and can fan that out to any cost sink.
10
10
 
11
11
  ```bash
12
12
  bun add @broberg/ai-sdk # or: npm i @broberg/ai-sdk
13
13
  ```
14
14
 
15
+ ## Where did the data go? — `usage.region`
16
+
17
+ Every response carries the **data residency of the endpoint that actually answered**:
18
+
19
+ ```ts
20
+ const { text, usage } = await ai.chat({ prompt, tier: "smart" });
21
+ if (usage.region !== "eu") throw new Error(`personal data would have left the EU (${usage.region})`);
22
+ ```
23
+
24
+ **Only `"eu"` is a positive claim.** `"unknown"` means we cannot say, so
25
+ `region !== "us"` is **not** an EU check — it passes every single OpenRouter call.
26
+
27
+ `region` is derived from the **host** the request used, not from the provider's name —
28
+ `vertex`, `azure` and `bfl` are EU by default but each takes an override, so a
29
+ name-based table would report `eu` for a call someone had pointed at `us-central1`.
30
+
31
+ **Three things that decide whether your guard works:**
32
+
33
+ | | |
34
+ |---|---|
35
+ | **After the call** | `usage.region` — `"eu"` \| `"us"` \| `"cn"` \| `"unknown"` |
36
+ | **Before the call** | `regionOfHost(urlOrHostname)` — the only way to refuse *before* bytes leave |
37
+ | **Never** | `regionOfProvider(name)` — see below |
38
+
39
+ ```ts
40
+ import { regionOfHost } from "@broberg/ai-sdk";
41
+ if (regionOfHost("https://api.mistral.ai/v1") !== "eu") throw new Error("not EU");
42
+ ```
43
+
44
+ **Do not build a residency guard on `regionOfProvider`.** A name cannot answer for
45
+ hosting when the provider takes a `baseUrl`, so `regionOfProvider("mistral")` is
46
+ `"unknown"` — and a guard written as `regionOfProvider(p) === "eu"` therefore rejects
47
+ **Mistral, the only EU route there is.** Fail-closed and useless: it filters out the
48
+ thing it exists to allow.
49
+
50
+ An allowlist of `(provider, model)` pairs has the same hole from the other side: a
51
+ gateway in front of Mistral is still `"mistral"`/`"mistral-large-latest"`, matches both
52
+ fields, and passes. `regionOfHost` answers `"unknown"` for it, which is the honest answer.
53
+
15
54
  ## Quick start
16
55
 
17
56
  ```ts
package/dist/index.d.ts CHANGED
@@ -61,6 +61,24 @@ interface Pronunciation {
61
61
  word: string;
62
62
  /** Say this instead. Azure `<sub alias>`; ElevenLabs plain substitution. */
63
63
  alias?: string;
64
+ /** Also match INSIDE a hyphenated compound — `AI` in `AI-agenter` (F051.3).
65
+ *
66
+ * Danish puts abbreviations there constantly. cms measured their own articles:
67
+ * "AI-agenter" 13 times, "AI-native" 12, 60+ occurrences in all, every one read aloud
68
+ * as one mangled word. Christian heard it as "HTLM" and asked why AI was not spelled
69
+ * out too — it WAS, just never in a compound.
70
+ *
71
+ * **Per entry, and defaulting to `false`, because the right answer depends on the
72
+ * word.** `AI` yes; `mail` no — a rule for `mail` must not fire inside `e-mail`.
73
+ * Measured rather than reasoned: `@broberg/speech-dictionary` ships the general
74
+ * loosening and pays exactly that price today. A single global answer is wrong for
75
+ * one of the two classes whichever way it is set, so the person who wrote the entry
76
+ * decides.
77
+ *
78
+ * Default `false` is also the safe direction to be wrong in: flipping it would change
79
+ * the audio for every existing consumer IN SOUND, where there is no compile error, no
80
+ * red test and no log line to notice it by. */
81
+ matchInCompounds?: boolean;
64
82
  /** IPA phonemes. Azure `<phoneme alphabet="ipa" ph>`. ElevenLabs cannot do this
65
83
  * (it THROWS rather than silently skipping). See the type doc above before reaching
66
84
  * for this on a foreign word. */
@@ -1899,13 +1917,17 @@ declare const ttsInputSchema: z.ZodObject<{
1899
1917
  alias: z.ZodOptional<z.ZodString>;
1900
1918
  ipa: z.ZodOptional<z.ZodString>;
1901
1919
  lang: z.ZodOptional<z.ZodString>;
1920
+ /** F051.3 — also match inside a hyphenated compound ("AI" in "AI-agenter"). */
1921
+ matchInCompounds: z.ZodOptional<z.ZodBoolean>;
1902
1922
  }, "strip", z.ZodTypeAny, {
1903
1923
  word: string;
1924
+ matchInCompounds?: boolean | undefined;
1904
1925
  alias?: string | undefined;
1905
1926
  ipa?: string | undefined;
1906
1927
  lang?: string | undefined;
1907
1928
  }, {
1908
1929
  word: string;
1930
+ matchInCompounds?: boolean | undefined;
1909
1931
  alias?: string | undefined;
1910
1932
  ipa?: string | undefined;
1911
1933
  lang?: string | undefined;
@@ -1944,6 +1966,7 @@ declare const ttsInputSchema: z.ZodObject<{
1944
1966
  lang?: string | undefined;
1945
1967
  pronunciations?: {
1946
1968
  word: string;
1969
+ matchInCompounds?: boolean | undefined;
1947
1970
  alias?: string | undefined;
1948
1971
  ipa?: string | undefined;
1949
1972
  lang?: string | undefined;
@@ -1970,6 +1993,7 @@ declare const ttsInputSchema: z.ZodObject<{
1970
1993
  lang?: string | undefined;
1971
1994
  pronunciations?: {
1972
1995
  word: string;
1996
+ matchInCompounds?: boolean | undefined;
1973
1997
  alias?: string | undefined;
1974
1998
  ipa?: string | undefined;
1975
1999
  lang?: string | undefined;
@@ -2366,8 +2390,8 @@ declare const falStubAdapter: ProviderAdapter;
2366
2390
  * wires the live adapters. */
2367
2391
  declare const stubProviders: Record<string, ProviderAdapter>;
2368
2392
 
2369
- declare const VERSION: "0.42.0";
2370
- declare const SDK_TAG: "@broberg/ai-sdk@0.42.0";
2393
+ declare const VERSION: "0.43.0";
2394
+ declare const SDK_TAG: "@broberg/ai-sdk@0.43.0";
2371
2395
 
2372
2396
  /** Built-in defaults. Every entry is overridable via AiConfig.defaults or a
2373
2397
  * per-call override.
package/dist/index.js CHANGED
@@ -1557,10 +1557,14 @@ function applyPronunciations(haystack, list, render, escape = (s) => s) {
1557
1557
  if (!byLower.has(k)) byLower.set(k, p);
1558
1558
  }
1559
1559
  const alternation = sorted.map((p) => escapeRegex(escape(p.word))).join("|");
1560
- const re = new RegExp(`(?<![\\w-])(${alternation})(?![\\w-])`, "gi");
1561
- return haystack.replace(re, (matched) => {
1560
+ const re = new RegExp(`(?<!\\w)(${alternation})(?!\\w)`, "gi");
1561
+ return haystack.replace(re, (matched, _g1, offset) => {
1562
1562
  const entry = byLower.get(matched.toLowerCase());
1563
- return entry ? render(entry, matched) : matched;
1563
+ if (!entry) return matched;
1564
+ if (!entry.matchInCompounds) {
1565
+ if (haystack[offset - 1] === "-" || haystack[offset + matched.length] === "-") return matched;
1566
+ }
1567
+ return render(entry, matched);
1564
1568
  });
1565
1569
  }
1566
1570
 
@@ -3061,7 +3065,9 @@ var pronunciationSchema = z.object({
3061
3065
  word: z.string(),
3062
3066
  alias: z.string().optional(),
3063
3067
  ipa: z.string().optional(),
3064
- lang: z.string().optional()
3068
+ lang: z.string().optional(),
3069
+ /** F051.3 — also match inside a hyphenated compound ("AI" in "AI-agenter"). */
3070
+ matchInCompounds: z.boolean().optional()
3065
3071
  });
3066
3072
  var ttsInputSchema = z.object({
3067
3073
  text: z.string(),
@@ -3110,8 +3116,8 @@ var aiConfigSchema = z.object({
3110
3116
  });
3111
3117
 
3112
3118
  // src/version.ts
3113
- var VERSION = "0.42.0";
3114
- var SDK_TAG = "@broberg/ai-sdk@0.42.0";
3119
+ var VERSION = "0.43.0";
3120
+ var SDK_TAG = "@broberg/ai-sdk@0.43.0";
3115
3121
 
3116
3122
  // src/cost/sinks/upmetrics.ts
3117
3123
  function upmetricsSink(config) {